js-libp2p/test/metrics/index.node.ts
Alex Potsides f439d9b589
deps!: update all deps to support no-copy operations (#1335)
Updates all deps needed to support passing lists of byte arrays where they have been created from multiple input buffers.

When reading multiplexed data, all messages arrive in length-prefixed buffers, which means the first few bytes tell the consumer how many bytes long next chunk will be.

One length prefixed chunk can be delivered in several payloads from the underlying network transport. The first payload can also include the length prefix and some or all of the data, so we stitch these together in a `Uint8ArrayList` to avoid having to concatenate `Uint8Array`s together.

Previously once we'd received enough bytes to satisfy the length prefix we'd concatenate the bytes together, but this is a potentially expensive operation where transports have small message sizes so instead just pass the `Uint8ArrayList` to the consumer and let them decide wether to concatenate or not as some consumers will be smart enough to operate on lists of `Uint8Array`s instead of always requiring a contiguous block of memory.

BREAKING CHANGE: Streams are now `Duplex<Uint8ArrayList, Uint8ArrayList | Uint8Array>`
2022-08-11 13:21:04 +01:00

207 lines
6.0 KiB
TypeScript

/* eslint-env mocha */
import { expect } from 'aegir/chai'
import sinon from 'sinon'
import { randomBytes } from '@libp2p/crypto'
import { pipe } from 'it-pipe'
import toBuffer from 'it-to-buffer'
import delay from 'delay'
import { createNode, populateAddressBooks } from '../utils/creators/peer.js'
import { createBaseOptions } from '../utils/base-options.js'
import type { Libp2pNode } from '../../src/libp2p.js'
import type { Libp2pOptions } from '../../src/index.js'
import type { DefaultMetrics } from '../../src/metrics/index.js'
import pWaitFor from 'p-wait-for'
import drain from 'it-drain'
import map from 'it-map'
describe('libp2p.metrics', () => {
let libp2p: Libp2pNode
let remoteLibp2p: Libp2pNode
afterEach(async () => {
if (libp2p != null) {
await libp2p.stop()
}
if (remoteLibp2p != null) {
await remoteLibp2p.stop()
}
})
it('should disable metrics by default', async () => {
libp2p = await createNode({
config: createBaseOptions()
})
expect(libp2p.components.getMetrics()).to.be.undefined()
})
it('should start/stop metrics on startup/shutdown when enabled', async () => {
const config: Libp2pOptions = createBaseOptions({
metrics: {
enabled: true,
computeThrottleMaxQueueSize: 1, // compute after every message
movingAverageIntervals: [10]
}
})
libp2p = await createNode({ started: false, config })
const metrics = libp2p.components.getMetrics() as DefaultMetrics
if (metrics == null) {
throw new Error('Metrics not configured')
}
const metricsStartSpy = sinon.spy(metrics, 'start')
const metricsStopSpy = sinon.spy(metrics, 'stop')
await libp2p.start()
expect(metricsStartSpy).to.have.property('callCount', 1)
await libp2p.stop()
expect(metricsStopSpy).to.have.property('callCount', 1)
})
it('should record metrics on connections and streams when enabled', async () => {
[libp2p, remoteLibp2p] = await Promise.all([
createNode({
config: createBaseOptions({
metrics: {
enabled: true,
computeThrottleMaxQueueSize: 1, // compute after every message
movingAverageIntervals: [10]
}
})
}),
createNode({
config: createBaseOptions({
metrics: {
enabled: true,
computeThrottleMaxQueueSize: 1, // compute after every message
movingAverageIntervals: [10]
}
})
})
])
await populateAddressBooks([libp2p, remoteLibp2p])
void remoteLibp2p.handle('/echo/1.0.0', ({ stream }) => {
void pipe(stream, stream)
})
const connection = await libp2p.dial(remoteLibp2p.peerId)
const stream = await connection.newStream('/echo/1.0.0')
const bytes = randomBytes(512)
const result = await pipe(
[bytes],
stream,
async (source) => await toBuffer(map(source, (list) => list.subarray()))
)
// Flush the call stack
await delay(0)
expect(result).to.have.length(bytes.length)
const metrics = libp2p.components.getMetrics()
if (metrics == null) {
throw new Error('Metrics not configured')
}
// Protocol stats should equal the echo size
const protocolStats = metrics.forProtocol('/echo/1.0.0')?.getSnapshot()
expect(protocolStats?.dataReceived).to.equal(BigInt(bytes.length))
expect(protocolStats?.dataSent).to.equal(BigInt(bytes.length))
// A lot more traffic will be sent over the wire for the peer
const peerStats = metrics.forPeer(connection.remotePeer)?.getSnapshot()
expect(parseInt(peerStats?.dataReceived.toString() ?? '0')).to.be.at.least(bytes.length)
await remoteLibp2p.stop()
})
it('should move disconnected peers to the old peers list', async () => {
[libp2p, remoteLibp2p] = await Promise.all([
createNode({
config: createBaseOptions({
metrics: {
enabled: true,
computeThrottleMaxQueueSize: 1, // compute after every message
movingAverageIntervals: [10]
},
connectionManager: {
autoDial: false
}
})
}),
createNode({
config: createBaseOptions({
metrics: {
enabled: true,
computeThrottleMaxQueueSize: 1, // compute after every message
movingAverageIntervals: [10]
},
connectionManager: {
autoDial: false
}
})
})
])
await populateAddressBooks([libp2p, remoteLibp2p])
await remoteLibp2p.handle('/echo/1.0.0', ({ stream }) => {
void pipe(stream, stream)
})
const connection = await libp2p.dial(remoteLibp2p.peerId)
const stream = await connection.newStream('/echo/1.0.0')
const bytes = randomBytes(512)
await pipe(
[bytes],
stream,
drain
)
const metrics = libp2p.components.getMetrics()
if (metrics == null) {
throw new Error('Metrics not configured')
}
await pWaitFor(() => {
const peerStats = metrics.forPeer(connection.remotePeer)?.getSnapshot()
const transferred = parseInt(peerStats?.dataReceived.toString() ?? '0')
if (transferred < bytes.length) {
return false
}
return true
}, {
interval: 100
})
const peerStats = metrics.forPeer(connection.remotePeer)?.getSnapshot()
expect(parseInt(peerStats?.dataReceived.toString() ?? '0')).to.be.at.least(bytes.length)
const metricsOnPeerDisconnectedSpy = sinon.spy(metrics, 'onPeerDisconnected')
await libp2p.hangUp(connection.remotePeer)
// Flush call stack
await delay(0)
expect(metricsOnPeerDisconnectedSpy).to.have.property('callCount', 1)
// forPeer should still give us the old peer stats,
// even though its not in the active peer list
const peerStatsAfterHangup = metrics.forPeer(connection.remotePeer)?.getSnapshot()
expect(parseInt(peerStatsAfterHangup?.dataReceived.toString() ?? '0')).to.be.at.least(bytes.length)
await remoteLibp2p.stop()
})
})