From 84da22aebe1fd2aea1295cd97f5353577bdbbeea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Abadesso?= Date: Fri, 17 Apr 2026 13:10:51 -0300 Subject: [PATCH] perf(daemon): add sync benchmark harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs the daemon's SyncMachine against an integration-test event simulator, captures per-span timings via an in-memory OTel exporter, and writes aggregated stats to JSON for branch-to-branch comparison. Smoke-tested against VOIDED_TOKEN_AUTHORITY (66 events): captures the handlers called out in #395 (handleVertexAccepted, getTransactionById, addOrUpdateTx, addUtxos, getAddressWalletInfo, etc). Current simulator scenarios are too small for stable absolute numbers — a larger scenario is needed before drawing conclusions, but the harness itself is stack-complete. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + packages/daemon/package.json | 1 + packages/daemon/src/scripts/bench-sync.ts | 314 ++++++++++++++++++++++ 3 files changed, 316 insertions(+) create mode 100644 packages/daemon/src/scripts/bench-sync.ts diff --git a/.gitignore b/.gitignore index ef1f9d32..2688a4ca 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ packages/wallet-service/.warmup .yarn/ .env.* *.tsbuildinfo +packages/daemon/bench-results-*.json diff --git a/packages/daemon/package.json b/packages/daemon/package.json index a37989cf..ac145f40 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -19,6 +19,7 @@ "dev:migrate": "cd ../.. && DB_ENDPOINT=localhost DB_NAME=wallet_service DB_USER=hathor DB_PASS=hathor DB_PORT=3306 npx sequelize-cli db:migrate && DB_ENDPOINT=localhost DB_NAME=wallet_service DB_USER=hathor DB_PASS=hathor DB_PORT=3306 npx sequelize-cli db:seed:all", "dev:fetch-ids": "FULLNODE_WEBSOCKET_BASEURL=${FULLNODE_HOST} node ../../scripts/fetch-fullnode-ids.js", "replay-balance": "yarn dlx ts-node src/scripts/replay-balance.ts", + "bench:sync": "yarn dlx ts-node src/scripts/bench-sync.ts", "watch": "tsc -w", "test_images_up": "docker compose -f ./__tests__/integration/scripts/docker-compose.yml up -d", "test_images_down": "docker compose -f ./__tests__/integration/scripts/docker-compose.yml down", diff --git a/packages/daemon/src/scripts/bench-sync.ts b/packages/daemon/src/scripts/bench-sync.ts new file mode 100644 index 00000000..63c5a69c --- /dev/null +++ b/packages/daemon/src/scripts/bench-sync.ts @@ -0,0 +1,314 @@ +/** + * Copyright (c) Hathor Labs and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Sync benchmark harness. + * + * Runs the daemon's SyncMachine against an integration-test event simulator, + * captures per-span timings via an in-memory OTel exporter, and writes + * aggregated stats to JSON. Produces numbers that can be compared across + * branches to reason about per-event sync performance. + * + * Prerequisites (run from packages/daemon): + * yarn test_images_up # starts MySQL + all simulator containers + * yarn test_images_wait_for_db + * yarn test_images_migrate + * yarn test_images_wait_for_ws + * + * Usage: + * yarn bench:sync --scenario UNVOIDED --runs 5 --warmup 1 --label master + * yarn bench:sync --scenario VOIDED_TOKEN_AUTHORITY --runs 10 --out bench.json + * + * Scenarios mirror __tests__/integration/config.ts. Current scenarios all top + * out at <70 events, which is too few for stable per-event timing — fullnode + * connect / MySQL pool warmup / JIT noise dominate. Add a larger scenario + * before drawing conclusions from absolute numbers; the harness is + * otherwise correct. + */ + +// Disable the daemon's built-in OTLP exporter — we install our own +// in-memory exporter below. Must be set before any daemon import. +process.env.OTEL_SDK_DISABLED = 'true'; + +import { writeFileSync } from 'node:fs'; +import { performance } from 'node:perf_hooks'; + +// --------------------------------------------------------------------------- +// CLI argument parsing +// --------------------------------------------------------------------------- + +interface Opts { + scenario: string; + runs: number; + warmup: number; + label: string; + out: string; +} + +function parseArgs(): Opts { + const args = process.argv.slice(2); + const opts: Opts = { + scenario: 'UNVOIDED', + runs: 5, + warmup: 1, + label: 'local', + out: '', + }; + for (let i = 0; i < args.length; i++) { + const a = args[i]; + const v = args[i + 1]; + switch (a) { + case '--scenario': opts.scenario = v; i++; break; + case '--runs': opts.runs = parseInt(v, 10); i++; break; + case '--warmup': opts.warmup = parseInt(v, 10); i++; break; + case '--label': opts.label = v; i++; break; + case '--out': opts.out = v; i++; break; + case '--help': + case '-h': + printHelp(); + process.exit(0); + break; + default: + console.error(`Unknown arg: ${a}`); + printHelp(); + process.exit(1); + } + } + if (!opts.out) opts.out = `bench-results-${opts.label}.json`; + return opts; +} + +function printHelp() { + console.log(`Usage: bench-sync [options] + +Options: + --scenario Simulator scenario (default: UNVOIDED) + --runs Measured runs (default: 5) + --warmup Discarded warmup runs (default: 1) + --label Label for the output file and opts block (default: local) + --out Output JSON path (default: bench-results-