From 1735480c65814ee2a3528b16927c7c3ad6249430 Mon Sep 17 00:00:00 2001 From: dozyio Date: Sat, 16 May 2026 14:48:39 +0100 Subject: [PATCH 1/6] chore: fix gossipsub benchmarks --- packages/gossipsub/package.json | 6 +- .../test/benchmark/asyncIterable.test.ts | 73 +++++---- .../gossipsub/test/benchmark/index.test.ts | 151 ++++++++++-------- .../gossipsub/test/benchmark/protobuf.test.ts | 44 +++-- .../test/benchmark/time-cache.test.ts | 39 ++++- 5 files changed, 199 insertions(+), 114 deletions(-) diff --git a/packages/gossipsub/package.json b/packages/gossipsub/package.json index dba401e82b..77479f392f 100644 --- a/packages/gossipsub/package.json +++ b/packages/gossipsub/package.json @@ -52,8 +52,8 @@ "release": "aegir release --no-types", "build": "aegir build", "generate": "protons ./src/message/rpc.proto", - "benchmark": "yarn benchmark:files test/benchmark/**/*.test.ts", - "benchmark:files": "NODE_OPTIONS='--max-old-space-size=4096 --loader=ts-node/esm' benchmark --config .benchrc.yaml --defaultBranch master", + "benchmark": "yarn build && yarn benchmark:files dist/test/benchmark/**/*.test.js", + "benchmark:files": "NODE_OPTIONS='--max-old-space-size=4096' mocha --timeout 0", "test": "aegir test -f dist/test/*.spec.js", "test:node": "aegir test -t node -f dist/test/*.spec.js -f dist/test/unit/*.spec.js -f dist/test/e2e/*.spec.js", "test:chrome": "aegir test -f dist/test/*.spec.js -t browser", @@ -96,7 +96,6 @@ }, "devDependencies": { "@chainsafe/as-sha256": "^1.2.0", - "@dapplion/benchmark": "^1.0.0", "@libp2p/floodsub": "^11.0.21", "@libp2p/logger": "^6.2.7", "@libp2p/peer-store": "^12.0.20", @@ -104,6 +103,7 @@ "@types/sinon": "^21.0.1", "abortable-iterator": "^5.1.0", "aegir": "^47.0.21", + "benchmark": "^2.1.4", "datastore-core": "^11.0.1", "delay": "^7.0.0", "it-all": "^3.0.6", diff --git a/packages/gossipsub/test/benchmark/asyncIterable.test.ts b/packages/gossipsub/test/benchmark/asyncIterable.test.ts index 724c202559..525d3d9fd6 100644 --- a/packages/gossipsub/test/benchmark/asyncIterable.test.ts +++ b/packages/gossipsub/test/benchmark/asyncIterable.test.ts @@ -1,5 +1,6 @@ -import { itBench } from '@dapplion/benchmark' import { abortableSource } from 'abortable-iterator' +// @ts-expect-error no types +import Benchmark from 'benchmark' import all from 'it-all' import { pipe } from 'it-pipe' @@ -16,21 +17,18 @@ describe('abortableSource cost', function () { } for (let k = 0; k < 5; k++) { - itBench({ - id: `async iterate abortable x${k} bytesSource ${n}`, - beforeEach: () => { + it(`async iterate abortable x${k} bytesSource ${n}`, async () => { + await runBenchmark(`async iterate abortable x${k} bytesSource ${n}`, async () => { let source = bytesSource() for (let i = 0; i < k; i++) { source = abortableSource(source, controller.signal) } - return source - }, - fn: async (source) => { + for await (const chunk of source) { // eslint-disable-next-line @typescript-eslint/no-unused-expressions chunk } - } + }) }) } }) @@ -51,35 +49,31 @@ describe('pipe extra iterables cost', function () { } } - itBench({ - id: `async iterate pipe x0 transforms ${n}`, - fn: async () => { + it(`async iterate pipe x0 transforms ${n}`, async () => { + await runBenchmark(`async iterate pipe x0 transforms ${n}`, async () => { await pipe(numberSource, all) - } + }) }) - itBench({ - id: `async iterate pipe x1 transforms ${n}`, - fn: async () => { + it(`async iterate pipe x1 transforms ${n}`, async () => { + await runBenchmark(`async iterate pipe x1 transforms ${n}`, async () => { await pipe(numberSource, numberTransform, all) - } + }) }) - itBench({ - id: `async iterate pipe x2 transforms ${n}`, - fn: async () => { + it(`async iterate pipe x2 transforms ${n}`, async () => { + await runBenchmark(`async iterate pipe x2 transforms ${n}`, async () => { await pipe( numberSource, numberTransform, numberTransform, all ) - } + }) }) - itBench({ - id: `async iterate pipe x4 transforms ${n}`, - fn: async () => { + it(`async iterate pipe x4 transforms ${n}`, async () => { + await runBenchmark(`async iterate pipe x4 transforms ${n}`, async () => { await pipe( numberSource, numberTransform, @@ -88,12 +82,11 @@ describe('pipe extra iterables cost', function () { numberTransform, all ) - } + }) }) - itBench({ - id: `async iterate pipe x8 transforms ${n}`, - fn: async () => { + it(`async iterate pipe x8 transforms ${n}`, async () => { + await runBenchmark(`async iterate pipe x8 transforms ${n}`, async () => { await pipe( numberSource, numberTransform, @@ -106,6 +99,30 @@ describe('pipe extra iterables cost', function () { numberTransform, all ) - } + }) }) }) + +async function runBenchmark (name: string, fn: () => void | Promise): Promise { + await new Promise((resolve, reject) => { + new Benchmark(name, { + defer: true, + initCount: 1, + maxTime: 1, + minSamples: 1, + minTime: 0.1, + fn (deferred: { resolve(): void }) { + Promise.resolve() + .then(fn) + .then(() => { deferred.resolve() }) + .catch(reject) + } + }) + .on('complete', function (this: { hz: number, count: number }) { + process.stdout.write(` ${name}: ${this.hz.toFixed(4)} ops/s ${(1000 / this.hz).toFixed(6)} ms/op ${this.count} runs\n`) + resolve() + }) + .on('error', reject) + .run({ async: true }) + }) +} diff --git a/packages/gossipsub/test/benchmark/index.test.ts b/packages/gossipsub/test/benchmark/index.test.ts index e2a1daea9a..ea638687fe 100644 --- a/packages/gossipsub/test/benchmark/index.test.ts +++ b/packages/gossipsub/test/benchmark/index.test.ts @@ -1,5 +1,6 @@ -import { itBench } from '@dapplion/benchmark' import { expect } from 'aegir/chai' +// @ts-expect-error no types +import Benchmark from 'benchmark' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import { connectPubsubNodes, @@ -8,7 +9,6 @@ import { } from '../utils/create-pubsub.js' import { awaitEvents, checkReceivedSubscriptions, checkReceivedSubscription } from '../utils/events.js' -import type { GossipSubAndComponents } from '../utils/create-pubsub.js' describe('heartbeat', function () { const topic = 'foobar' @@ -48,75 +48,71 @@ describe('heartbeat', function () { * | 0 | 0, 1, 2, 3| * | 1 | 0, 2, 3, 4| */ - itBench({ - id: 'heartbeat', - before: async () => { - const psubs = await createComponentsArray({ - number: numPeers, - init: { - scoreParams: { - IPColocationFactorWeight: 0 - }, - floodPublish: true, - // TODO: why we need to configure this low score - // probably we should tweak topic score params - // is that why we don't have mesh peers? - scoreThresholds: { - gossipThreshold: -10, - publishThreshold: -100, - graylistThreshold: -1000 - } + it('heartbeat', async () => { + const psubs = await createComponentsArray({ + number: numPeers, + init: { + scoreParams: { + IPColocationFactorWeight: 0 + }, + floodPublish: true, + // TODO: why we need to configure this low score + // probably we should tweak topic score params + // is that why we don't have mesh peers? + scoreThresholds: { + gossipThreshold: -10, + publishThreshold: -100, + graylistThreshold: -1000 } - }) + } + }) - // build the star - await Promise.all(psubs.slice(1).map(async (ps) => connectPubsubNodes(psubs[0], ps))) - await Promise.all(psubs.map(async (ps) => awaitEvents(ps.pubsub, 'gossipsub:heartbeat', 2))) + // build the star + await Promise.all(psubs.slice(1).map(async (ps) => connectPubsubNodes(psubs[0], ps))) + await Promise.all(psubs.map(async (ps) => awaitEvents(ps.pubsub, 'gossipsub:heartbeat', 2))) - await denseConnect(psubs) + await denseConnect(psubs) - // make sure psub 0 has `numPeers - 1` peers - expect(psubs[0].pubsub.getPeers().length).to.be.gte( - numPeers - 1, - `peer 0 should have at least ${numPeers - 1} peers` - ) + // make sure psub 0 has `numPeers - 1` peers + expect(psubs[0].pubsub.getPeers().length).to.be.gte( + numPeers - 1, + `peer 0 should have at least ${numPeers - 1} peers` + ) - const peerIds = psubs.map((psub) => psub.components.peerId.toString()) - for (let topicIndex = 0; topicIndex < numTopic; topicIndex++) { - const topic = getTopic(topicIndex) - psubs.forEach((ps) => { ps.pubsub.subscribe(topic) }) - const peerIndices = getTopicPeerIndices(topicIndex) - const peerIdsOnTopic = peerIndices.map((peerIndex) => peerIds[peerIndex]) - // peer 0 see all subscriptions from other - const subscription = checkReceivedSubscriptions(psubs[0], peerIdsOnTopic, topic) - // other peers should see the subsription from peer 0 to prevent PublishError.InsufficientPeers error - const otherSubscriptions = peerIndices - .slice(1) - .map((peerIndex) => psubs[peerIndex]) - .map(async (psub) => checkReceivedSubscription(psub, peerIds[0], topic, 0)) - peerIndices.forEach((peerIndex) => { psubs[peerIndex].pubsub.subscribe(topic) }) - await Promise.all([subscription, ...otherSubscriptions]) - } + const peerIds = psubs.map((psub) => psub.components.peerId.toString()) + for (let topicIndex = 0; topicIndex < numTopic; topicIndex++) { + const topic = getTopic(topicIndex) + psubs.forEach((ps) => { ps.pubsub.subscribe(topic) }) + const peerIndices = getTopicPeerIndices(topicIndex) + const peerIdsOnTopic = peerIndices.map((peerIndex) => peerIds[peerIndex]) + // peer 0 see all subscriptions from other + const subscription = checkReceivedSubscriptions(psubs[0], peerIdsOnTopic, topic) + // other peers should see the subsription from peer 0 to prevent PublishError.InsufficientPeers error + const otherSubscriptions = peerIndices + .slice(1) + .map((peerIndex) => psubs[peerIndex]) + .map(async (psub) => checkReceivedSubscription(psub, peerIds[0], topic, 0)) + peerIndices.forEach((peerIndex) => { psubs[peerIndex].pubsub.subscribe(topic) }) + await Promise.all([subscription, ...otherSubscriptions]) + } - // wait for heartbeats to build mesh - await Promise.all(psubs.map(async (ps) => awaitEvents(ps.pubsub, 'gossipsub:heartbeat', 3))) + // wait for heartbeats to build mesh + await Promise.all(psubs.map(async (ps) => awaitEvents(ps.pubsub, 'gossipsub:heartbeat', 3))) - // make sure psubs 0 have at least 10 topic peers and 4 mesh peers for each topic - for (let i = 0; i < numTopic; i++) { - expect((psubs[0].pubsub).getSubscribers(getTopic(i)).length).to.be.gte( - 10, - `psub 0: topic ${i} does not have enough topic peers` - ) + // make sure psubs 0 have at least 10 topic peers and 4 mesh peers for each topic + for (let i = 0; i < numTopic; i++) { + expect((psubs[0].pubsub).getSubscribers(getTopic(i)).length).to.be.gte( + 10, + `psub 0: topic ${i} does not have enough topic peers` + ) - expect((psubs[0].pubsub).getMeshPeers(getTopic(i)).length).to.be.gte( - 4, - `psub 0: topic ${i} does not have enough mesh peers` - ) - } + expect((psubs[0].pubsub).getMeshPeers(getTopic(i)).length).to.be.gte( + 4, + `psub 0: topic ${i} does not have enough mesh peers` + ) + } - return psubs - }, - beforeEach: async (psubs) => { + await runBenchmark('heartbeat', async () => { numLoop++ const msg = `its not a flooooood ${numLoop}` const promises = [] @@ -132,10 +128,31 @@ describe('heartbeat', function () { } await Promise.all(promises) - return psubs[0] - }, - fn: async (firstPsub: GossipSubAndComponents) => { - return (firstPsub.pubsub).heartbeat() - } + await psubs[0].pubsub.heartbeat() + }) }) }) + +async function runBenchmark (name: string, fn: () => void | Promise): Promise { + await new Promise((resolve, reject) => { + new Benchmark(name, { + defer: true, + initCount: 1, + maxTime: 1, + minSamples: 1, + minTime: 0.1, + fn (deferred: { resolve(): void }) { + Promise.resolve() + .then(fn) + .then(() => { deferred.resolve() }) + .catch(reject) + } + }) + .on('complete', function (this: { hz: number, count: number }) { + process.stdout.write(` ${name}: ${this.hz.toFixed(4)} ops/s ${(1000 / this.hz).toFixed(6)} ms/op ${this.count} runs\n`) + resolve() + }) + .on('error', reject) + .run({ async: true }) + }) +} diff --git a/packages/gossipsub/test/benchmark/protobuf.test.ts b/packages/gossipsub/test/benchmark/protobuf.test.ts index e94b369a8a..31402065c0 100644 --- a/packages/gossipsub/test/benchmark/protobuf.test.ts +++ b/packages/gossipsub/test/benchmark/protobuf.test.ts @@ -1,5 +1,6 @@ import crypto from 'node:crypto' -import { itBench } from '@dapplion/benchmark' +// @ts-expect-error no types +import Benchmark from 'benchmark' import { RPC } from '../../src/message/rpc.js' describe('protobuf', function () { @@ -30,24 +31,45 @@ describe('protobuf', function () { const runsFactor = 1000 - itBench({ - id: `decode ${name} message ${length} bytes`, - fn: () => { + it(`decode ${name} message ${length} bytes`, async () => { + await runBenchmark(`decode ${name} message ${length} bytes`, () => { for (let i = 0; i < runsFactor; i++) { RPC.decode(bytes) } - }, - runsFactor + }, runsFactor) }) - itBench({ - id: `encode ${name} message ${length} bytes`, - fn: () => { + it(`encode ${name} message ${length} bytes`, async () => { + await runBenchmark(`encode ${name} message ${length} bytes`, () => { for (let i = 0; i < runsFactor; i++) { RPC.encode(rpc) } - }, - runsFactor + }, runsFactor) }) } }) + +async function runBenchmark (name: string, fn: () => void | Promise, runsFactor = 1): Promise { + await new Promise((resolve, reject) => { + new Benchmark(name, { + defer: true, + initCount: 1, + maxTime: 1, + minSamples: 1, + minTime: 0.1, + fn (deferred: { resolve(): void }) { + Promise.resolve() + .then(fn) + .then(() => { deferred.resolve() }) + .catch(reject) + } + }) + .on('complete', function (this: { hz: number, count: number }) { + const hz = this.hz * runsFactor + process.stdout.write(` ${name}: ${hz.toFixed(4)} ops/s ${(1000 / hz).toFixed(6)} ms/op ${this.count} runs\n`) + resolve() + }) + .on('error', reject) + .run({ async: true }) + }) +} diff --git a/packages/gossipsub/test/benchmark/time-cache.test.ts b/packages/gossipsub/test/benchmark/time-cache.test.ts index ef06d8d2de..b047bf3e62 100644 --- a/packages/gossipsub/test/benchmark/time-cache.test.ts +++ b/packages/gossipsub/test/benchmark/time-cache.test.ts @@ -1,4 +1,5 @@ -import { itBench } from '@dapplion/benchmark' +// @ts-expect-error no types +import Benchmark from 'benchmark' // @ts-expect-error no types import TimeCache from 'time-cache' import { SimpleTimeCache } from '../../src/utils/time-cache.js' @@ -10,12 +11,40 @@ describe('npm TimeCache vs SimpleTimeCache', () => { const simpleTimeCache = new SimpleTimeCache({ validityMs: 1000 }) for (const iteration of iterations) { - itBench(`npm TimeCache.put x${iteration}`, () => { - for (let j = 0; j < iteration; j++) { timeCache.put(String(j)) } + it(`npm TimeCache.put x${iteration}`, async () => { + await runBenchmark(`npm TimeCache.put x${iteration}`, () => { + for (let j = 0; j < iteration; j++) { timeCache.put(String(j)) } + }) }) - itBench(`SimpleTimeCache.put x${iteration}`, () => { - for (let j = 0; j < iteration; j++) { simpleTimeCache.put(String(j), true) } + it(`SimpleTimeCache.put x${iteration}`, async () => { + await runBenchmark(`SimpleTimeCache.put x${iteration}`, () => { + for (let j = 0; j < iteration; j++) { simpleTimeCache.put(String(j), true) } + }) }) } }) + +async function runBenchmark (name: string, fn: () => void | Promise): Promise { + await new Promise((resolve, reject) => { + new Benchmark(name, { + defer: true, + initCount: 1, + maxTime: 1, + minSamples: 1, + minTime: 0.1, + fn (deferred: { resolve(): void }) { + Promise.resolve() + .then(fn) + .then(() => { deferred.resolve() }) + .catch(reject) + } + }) + .on('complete', function (this: { hz: number, count: number }) { + process.stdout.write(` ${name}: ${this.hz.toFixed(4)} ops/s ${(1000 / this.hz).toFixed(6)} ms/op ${this.count} runs\n`) + resolve() + }) + .on('error', reject) + .run({ async: true }) + }) +} From d98a3e305bfa852afeb020f52fda4581ee748bcd Mon Sep 17 00:00:00 2001 From: dozyio Date: Mon, 25 May 2026 13:39:31 +0100 Subject: [PATCH 2/6] chore: yarn -> npm --- packages/gossipsub/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gossipsub/package.json b/packages/gossipsub/package.json index 77479f392f..7b2c5dda87 100644 --- a/packages/gossipsub/package.json +++ b/packages/gossipsub/package.json @@ -52,7 +52,7 @@ "release": "aegir release --no-types", "build": "aegir build", "generate": "protons ./src/message/rpc.proto", - "benchmark": "yarn build && yarn benchmark:files dist/test/benchmark/**/*.test.js", + "benchmark": "npm run build && npm run benchmark:files -- dist/test/benchmark/**/*.test.js", "benchmark:files": "NODE_OPTIONS='--max-old-space-size=4096' mocha --timeout 0", "test": "aegir test -f dist/test/*.spec.js", "test:node": "aegir test -t node -f dist/test/*.spec.js -f dist/test/unit/*.spec.js -f dist/test/e2e/*.spec.js", From dfc5a985e9eb202572a4dcf559bd480c586000a7 Mon Sep 17 00:00:00 2001 From: tabcat Date: Tue, 26 May 2026 10:56:20 +0700 Subject: [PATCH 3/6] chore: dedupe gossipsub benchmark runBenchmark helper Extract the per-file runBenchmark helper (identical except protobuf's runsFactor, now defaulted to 1) into test/utils/benchmark.ts. --- .../test/benchmark/asyncIterable.test.ts | 27 +----------------- .../gossipsub/test/benchmark/index.test.ts | 27 +----------------- .../gossipsub/test/benchmark/protobuf.test.ts | 28 +------------------ .../test/benchmark/time-cache.test.ts | 27 +----------------- packages/gossipsub/test/utils/benchmark.ts | 27 ++++++++++++++++++ 5 files changed, 31 insertions(+), 105 deletions(-) create mode 100644 packages/gossipsub/test/utils/benchmark.ts diff --git a/packages/gossipsub/test/benchmark/asyncIterable.test.ts b/packages/gossipsub/test/benchmark/asyncIterable.test.ts index 525d3d9fd6..48ac4beca5 100644 --- a/packages/gossipsub/test/benchmark/asyncIterable.test.ts +++ b/packages/gossipsub/test/benchmark/asyncIterable.test.ts @@ -1,8 +1,7 @@ import { abortableSource } from 'abortable-iterator' -// @ts-expect-error no types -import Benchmark from 'benchmark' import all from 'it-all' import { pipe } from 'it-pipe' +import { runBenchmark } from '../utils/benchmark.js' describe('abortableSource cost', function () { const n = 10000 @@ -102,27 +101,3 @@ describe('pipe extra iterables cost', function () { }) }) }) - -async function runBenchmark (name: string, fn: () => void | Promise): Promise { - await new Promise((resolve, reject) => { - new Benchmark(name, { - defer: true, - initCount: 1, - maxTime: 1, - minSamples: 1, - minTime: 0.1, - fn (deferred: { resolve(): void }) { - Promise.resolve() - .then(fn) - .then(() => { deferred.resolve() }) - .catch(reject) - } - }) - .on('complete', function (this: { hz: number, count: number }) { - process.stdout.write(` ${name}: ${this.hz.toFixed(4)} ops/s ${(1000 / this.hz).toFixed(6)} ms/op ${this.count} runs\n`) - resolve() - }) - .on('error', reject) - .run({ async: true }) - }) -} diff --git a/packages/gossipsub/test/benchmark/index.test.ts b/packages/gossipsub/test/benchmark/index.test.ts index ea638687fe..a66776ec5d 100644 --- a/packages/gossipsub/test/benchmark/index.test.ts +++ b/packages/gossipsub/test/benchmark/index.test.ts @@ -1,7 +1,6 @@ import { expect } from 'aegir/chai' -// @ts-expect-error no types -import Benchmark from 'benchmark' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { runBenchmark } from '../utils/benchmark.js' import { connectPubsubNodes, createComponentsArray, @@ -132,27 +131,3 @@ describe('heartbeat', function () { }) }) }) - -async function runBenchmark (name: string, fn: () => void | Promise): Promise { - await new Promise((resolve, reject) => { - new Benchmark(name, { - defer: true, - initCount: 1, - maxTime: 1, - minSamples: 1, - minTime: 0.1, - fn (deferred: { resolve(): void }) { - Promise.resolve() - .then(fn) - .then(() => { deferred.resolve() }) - .catch(reject) - } - }) - .on('complete', function (this: { hz: number, count: number }) { - process.stdout.write(` ${name}: ${this.hz.toFixed(4)} ops/s ${(1000 / this.hz).toFixed(6)} ms/op ${this.count} runs\n`) - resolve() - }) - .on('error', reject) - .run({ async: true }) - }) -} diff --git a/packages/gossipsub/test/benchmark/protobuf.test.ts b/packages/gossipsub/test/benchmark/protobuf.test.ts index 31402065c0..86522374b8 100644 --- a/packages/gossipsub/test/benchmark/protobuf.test.ts +++ b/packages/gossipsub/test/benchmark/protobuf.test.ts @@ -1,7 +1,6 @@ import crypto from 'node:crypto' -// @ts-expect-error no types -import Benchmark from 'benchmark' import { RPC } from '../../src/message/rpc.js' +import { runBenchmark } from '../utils/benchmark.js' describe('protobuf', function () { const testCases: Array<{ name: string, length: number }> = [ @@ -48,28 +47,3 @@ describe('protobuf', function () { }) } }) - -async function runBenchmark (name: string, fn: () => void | Promise, runsFactor = 1): Promise { - await new Promise((resolve, reject) => { - new Benchmark(name, { - defer: true, - initCount: 1, - maxTime: 1, - minSamples: 1, - minTime: 0.1, - fn (deferred: { resolve(): void }) { - Promise.resolve() - .then(fn) - .then(() => { deferred.resolve() }) - .catch(reject) - } - }) - .on('complete', function (this: { hz: number, count: number }) { - const hz = this.hz * runsFactor - process.stdout.write(` ${name}: ${hz.toFixed(4)} ops/s ${(1000 / hz).toFixed(6)} ms/op ${this.count} runs\n`) - resolve() - }) - .on('error', reject) - .run({ async: true }) - }) -} diff --git a/packages/gossipsub/test/benchmark/time-cache.test.ts b/packages/gossipsub/test/benchmark/time-cache.test.ts index b047bf3e62..ac5d0d8e7f 100644 --- a/packages/gossipsub/test/benchmark/time-cache.test.ts +++ b/packages/gossipsub/test/benchmark/time-cache.test.ts @@ -1,8 +1,7 @@ // @ts-expect-error no types -import Benchmark from 'benchmark' -// @ts-expect-error no types import TimeCache from 'time-cache' import { SimpleTimeCache } from '../../src/utils/time-cache.js' +import { runBenchmark } from '../utils/benchmark.js' // TODO: errors with "Error: root suite not found" describe('npm TimeCache vs SimpleTimeCache', () => { @@ -24,27 +23,3 @@ describe('npm TimeCache vs SimpleTimeCache', () => { }) } }) - -async function runBenchmark (name: string, fn: () => void | Promise): Promise { - await new Promise((resolve, reject) => { - new Benchmark(name, { - defer: true, - initCount: 1, - maxTime: 1, - minSamples: 1, - minTime: 0.1, - fn (deferred: { resolve(): void }) { - Promise.resolve() - .then(fn) - .then(() => { deferred.resolve() }) - .catch(reject) - } - }) - .on('complete', function (this: { hz: number, count: number }) { - process.stdout.write(` ${name}: ${this.hz.toFixed(4)} ops/s ${(1000 / this.hz).toFixed(6)} ms/op ${this.count} runs\n`) - resolve() - }) - .on('error', reject) - .run({ async: true }) - }) -} diff --git a/packages/gossipsub/test/utils/benchmark.ts b/packages/gossipsub/test/utils/benchmark.ts new file mode 100644 index 0000000000..5bd1eb719a --- /dev/null +++ b/packages/gossipsub/test/utils/benchmark.ts @@ -0,0 +1,27 @@ +// @ts-expect-error no types +import Benchmark from 'benchmark' + +export async function runBenchmark (name: string, fn: () => void | Promise, runsFactor = 1): Promise { + await new Promise((resolve, reject) => { + new Benchmark(name, { + defer: true, + initCount: 1, + maxTime: 1, + minSamples: 1, + minTime: 0.1, + fn (deferred: { resolve(): void }) { + Promise.resolve() + .then(fn) + .then(() => { deferred.resolve() }) + .catch(reject) + } + }) + .on('complete', function (this: { hz: number, count: number }) { + const hz = this.hz * runsFactor + process.stdout.write(` ${name}: ${hz.toFixed(4)} ops/s ${(1000 / hz).toFixed(6)} ms/op ${this.count} runs\n`) + resolve() + }) + .on('error', reject) + .run({ async: true }) + }) +} From 91c3c028ed76d9cfa393706026bd2ac5ca2918b9 Mon Sep 17 00:00:00 2001 From: tabcat Date: Tue, 26 May 2026 11:01:14 +0700 Subject: [PATCH 4/6] chore: add @types/benchmark to gossipsub Replace the @ts-expect-error on the benchmark import with the real types (Benchmark.Deferred, Benchmark) in the shared benchmark helper. --- packages/gossipsub/package.json | 1 + packages/gossipsub/test/utils/benchmark.ts | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gossipsub/package.json b/packages/gossipsub/package.json index 7b2c5dda87..2804321776 100644 --- a/packages/gossipsub/package.json +++ b/packages/gossipsub/package.json @@ -99,6 +99,7 @@ "@libp2p/floodsub": "^11.0.21", "@libp2p/logger": "^6.2.7", "@libp2p/peer-store": "^12.0.20", + "@types/benchmark": "^2.1.5", "@types/node": "^22.18.1", "@types/sinon": "^21.0.1", "abortable-iterator": "^5.1.0", diff --git a/packages/gossipsub/test/utils/benchmark.ts b/packages/gossipsub/test/utils/benchmark.ts index 5bd1eb719a..552868d116 100644 --- a/packages/gossipsub/test/utils/benchmark.ts +++ b/packages/gossipsub/test/utils/benchmark.ts @@ -1,4 +1,3 @@ -// @ts-expect-error no types import Benchmark from 'benchmark' export async function runBenchmark (name: string, fn: () => void | Promise, runsFactor = 1): Promise { @@ -9,14 +8,14 @@ export async function runBenchmark (name: string, fn: () => void | Promise maxTime: 1, minSamples: 1, minTime: 0.1, - fn (deferred: { resolve(): void }) { + fn (deferred: Benchmark.Deferred) { Promise.resolve() .then(fn) .then(() => { deferred.resolve() }) .catch(reject) } }) - .on('complete', function (this: { hz: number, count: number }) { + .on('complete', function (this: Benchmark) { const hz = this.hz * runsFactor process.stdout.write(` ${name}: ${hz.toFixed(4)} ops/s ${(1000 / hz).toFixed(6)} ms/op ${this.count} runs\n`) resolve() From 3e197259d6aa404dd12d3e2a8f92cb0bc9f6bd41 Mon Sep 17 00:00:00 2001 From: tabcat Date: Tue, 26 May 2026 11:35:51 +0700 Subject: [PATCH 5/6] chore: run gossipsub benchmarks via aegir test Use aegir's bundled mocha (no direct mocha dep) and forward --exit so the run terminates after benchmarks leave handles open. --- packages/gossipsub/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gossipsub/package.json b/packages/gossipsub/package.json index 2804321776..fb28d4bf11 100644 --- a/packages/gossipsub/package.json +++ b/packages/gossipsub/package.json @@ -52,8 +52,8 @@ "release": "aegir release --no-types", "build": "aegir build", "generate": "protons ./src/message/rpc.proto", - "benchmark": "npm run build && npm run benchmark:files -- dist/test/benchmark/**/*.test.js", - "benchmark:files": "NODE_OPTIONS='--max-old-space-size=4096' mocha --timeout 0", + "benchmark": "npm run benchmark:files -- 'dist/test/benchmark/**/*.test.js' -- --exit", + "benchmark:files": "NODE_OPTIONS='--max-old-space-size=4096' aegir test -t node --timeout 0 -f", "test": "aegir test -f dist/test/*.spec.js", "test:node": "aegir test -t node -f dist/test/*.spec.js -f dist/test/unit/*.spec.js -f dist/test/e2e/*.spec.js", "test:chrome": "aegir test -f dist/test/*.spec.js -t browser", From cbf01d0fdd53fae9d820791a061468e65dd40ee7 Mon Sep 17 00:00:00 2001 From: tabcat Date: Tue, 26 May 2026 11:52:30 +0700 Subject: [PATCH 6/6] chore: measure gossipsub heartbeat in isolation, stop nodes after Times heartbeat() instead of the publish fan-out (now a one-time warm-up), and stops the nodes so their timers don't keep the run alive. --- .../gossipsub/test/benchmark/index.test.ts | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/gossipsub/test/benchmark/index.test.ts b/packages/gossipsub/test/benchmark/index.test.ts index a66776ec5d..34a8be74f1 100644 --- a/packages/gossipsub/test/benchmark/index.test.ts +++ b/packages/gossipsub/test/benchmark/index.test.ts @@ -14,7 +14,6 @@ describe('heartbeat', function () { const numTopic = 70 const numPeers = 50 const numPeersPerTopic = 30 - let numLoop = 0 const getTopic = (i: number): string => { return topic + String(i) @@ -111,23 +110,27 @@ describe('heartbeat', function () { ) } - await runBenchmark('heartbeat', async () => { - numLoop++ - const msg = `its not a flooooood ${numLoop}` - const promises = [] - for (let topicIndex = 0; topicIndex < numTopic; topicIndex++) { - for (const peerIndex of getTopicPeerIndices(topicIndex)) { - promises.push( - psubs[peerIndex].pubsub.publish( - getTopic(topicIndex), - uint8ArrayFromString(psubs[peerIndex].components.peerId.toString() + msg) - ) + // publish one round of messages so the measured heartbeat has gossip to emit + const msg = 'its not a flooooood' + const promises = [] + for (let topicIndex = 0; topicIndex < numTopic; topicIndex++) { + for (const peerIndex of getTopicPeerIndices(topicIndex)) { + promises.push( + psubs[peerIndex].pubsub.publish( + getTopic(topicIndex), + uint8ArrayFromString(psubs[peerIndex].components.peerId.toString() + msg) ) - } + ) } - await Promise.all(promises) + } + await Promise.all(promises) + // measure heartbeat() in isolation — the publish round above is excluded from the timing + await runBenchmark('heartbeat', async () => { await psubs[0].pubsub.heartbeat() }) + + // stop the nodes so their heartbeat timers don't keep the process alive + await Promise.allSettled(psubs.map(async (ps) => ps.pubsub.stop())) }) })