-
Notifications
You must be signed in to change notification settings - Fork 1
perf(daemon): add sync benchmark harness #396
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,3 +16,4 @@ packages/wallet-service/.warmup | |
| .yarn/ | ||
| .env.* | ||
| *.tsbuildinfo | ||
| packages/daemon/bench-results-*.json | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 <name> Simulator scenario (default: UNVOIDED) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --runs <n> Measured runs (default: 5) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --warmup <n> Discarded warmup runs (default: 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --label <str> Label for the output file and opts block (default: local) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --out <path> Output JSON path (default: bench-results-<label>.json) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Available scenarios: ${Object.keys(SCENARIOS).join(', ')} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Keep in sync with __tests__/integration/config.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const SCENARIOS: Record<string, { port: number; lastEvent: number }> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UNVOIDED: { port: 8081, lastEvent: 39 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| REORG: { port: 8082, lastEvent: 18 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SINGLE_CHAIN_BLOCKS_AND_TRANSACTIONS: { port: 8083, lastEvent: 37 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INVALID_MEMPOOL_TRANSACTION: { port: 8085, lastEvent: 40 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CUSTOM_SCRIPT: { port: 8086, lastEvent: 37 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| EMPTY_SCRIPT: { port: 8087, lastEvent: 37 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NC_EVENTS: { port: 8088, lastEvent: 36 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TRANSACTION_VOIDING_CHAIN: { port: 8089, lastEvent: 52 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VOIDED_TOKEN_AUTHORITY: { port: 8090, lastEvent: 66 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SINGLE_VOIDED_CREATE_TOKEN_TRANSACTION: { port: 8091, lastEvent: 50 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SINGLE_VOIDED_REGULAR_TRANSACTION: { port: 8092, lastEvent: 60 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TOKEN_CREATION: { port: 8093, lastEvent: 45 }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const opts = parseArgs(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scenario = SCENARIOS[opts.scenario]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!scenario) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Unknown scenario: ${opts.scenario}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Available: ${Object.keys(SCENARIOS).join(', ')}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Environment setup — must happen before any daemon import (config is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // env-driven at module load time). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Object.assign(process.env, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NETWORK: 'testnet', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SERVICE_NAME: 'daemon-bench', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CONSOLE_LEVEL: 'error', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TX_CACHE_SIZE: '100', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BLOCK_REWARD_LOCK: '300', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FULLNODE_PEER_ID: 'simulator_peer_id', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STREAM_ID: 'simulator_stream_id', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FULLNODE_NETWORK: 'unittests', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| FULLNODE_HOST: `127.0.0.1:${scenario.port}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| USE_SSL: 'false', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DB_ENDPOINT: '127.0.0.1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DB_NAME: 'hathor', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DB_USER: 'root', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DB_PASS: 'hathor', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DB_PORT: '3380', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ACK_TIMEOUT_MS: '300000', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IDLE_EVENT_TIMEOUT_MS: String(5 * 60 * 1000), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STUCK_PROCESSING_TIMEOUT_MS: String(5 * 60 * 1000), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RECONNECTION_STORM_THRESHOLD: '10', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RECONNECTION_STORM_WINDOW_MS: String(5 * 60 * 1000), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // checkEnvVariables() requires these but they are never actually called | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // because the AWS/lambda/SQS paths are stubbed below. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| NEW_TX_SQS: 'bench-stub', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PUSH_NOTIFICATION_ENABLED: 'false', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WALLET_SERVICE_LAMBDA_ENDPOINT: 'bench-stub', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STAGE: 'local', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ACCOUNT_ID: '000000000000', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ALERT_MANAGER_TOPIC: 'bench-stub', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ALERT_MANAGER_REGION: 'us-east-1', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| APPLICATION_NAME: 'bench', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // In-memory OTel capture. Using BasicTracerProvider directly (instead of | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // NodeSDK) because .register() is synchronous — the global tracer provider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // is guaranteed to be in place before any `trace.getTracer()` call from the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // daemon modules imported below resolves its first span. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BasicTracerProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| InMemorySpanExporter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SimpleSpanProcessor, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '@opentelemetry/sdk-trace-base'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const exporter = new InMemorySpanExporter(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const provider = new BasicTracerProvider({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spanProcessors: [new SimpleSpanProcessor(exporter)], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider.register(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Stub external services that the daemon would otherwise try to reach | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // (SQS, push-notification lambda, fullnode HTTP API). Mirrors the jest.mock | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // calls in __tests__/integration/balances.test.ts. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as awsUtils from '../utils/aws'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as services from '../services'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (awsUtils as any).sendRealtimeTx = async () => undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (awsUtils as any).invokeOnTxPushNotificationRequestedLambda = async () => undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (services as any).checkForMissedEvents = async () => ({ hasNewEvents: false, events: [] }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (services as any).fetchMinRewardBlocks = async () => 300; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* eslint-enable @typescript-eslint/no-explicit-any */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { interpret } from 'xstate'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SyncMachine } from '../machines'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { getDbConnection } from '../db'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cleanDatabase, transitionUntilEvent } from '../../__tests__/integration/utils'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Run loop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface RunResult { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalMs: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** span name → list of individual span durations observed during the run */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spans: Map<string, number[]>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function runOnce(mysql: Awaited<ReturnType<typeof getDbConnection>>): Promise<RunResult> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await cleanDatabase(mysql); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exporter.reset(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const machine = interpret(SyncMachine); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const start = performance.now(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await transitionUntilEvent(mysql as any, machine as any, scenario.lastEvent); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const totalMs = performance.now() - start; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const spans = new Map<string, number[]>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const span of exporter.getFinishedSpans()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [s, ns] = span.duration; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ms = s * 1000 + ns / 1e6; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const list = spans.get(span.name) ?? []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| list.push(ms); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spans.set(span.name, list); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { totalMs, spans }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface Summary { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| n: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| min: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p50: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p95: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mean: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function summarize(values: number[]): Summary | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (values.length === 0) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sorted = [...values].sort((a, b) => a - b); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pick = (q: number) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| n: values.length, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| min: sorted[0], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p50: pick(0.5), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| p95: pick(0.95), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| max: sorted[sorted.length - 1], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mean: values.reduce((a, b) => a + b, 0) / values.length, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+241
to
+253
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Percentile formula uses For For the current typical 🛠️ Suggested fix- const pick = (q: number) => sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))];
+ const pick = (q: number) =>
+ sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1))];📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function main() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mysql = await getDbConnection(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const totalMsRuns: number[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Per span name: one entry per measured run, each entry is the sum of all | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // durations of that span within the run. Summing makes runs comparable even | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // when the number of span occurrences differs (e.g., voided vs un-voided | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // paths fire different spans). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const perSpanRuns = new Map<string, number[]>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const total = opts.warmup + opts.runs; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = 0; i < total; i++) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const phase = i < opts.warmup ? `warmup ${i + 1}/${opts.warmup}` : `run ${i - opts.warmup + 1}/${opts.runs}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.stderr.write(`[${phase}] `); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await runOnce(mysql); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.stderr.write(`totalMs=${result.totalMs.toFixed(2)}\n`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (i < opts.warmup) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalMsRuns.push(result.totalMs); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const [name, durations] of result.spans) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sum = durations.reduce((a, b) => a + b, 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const arr = perSpanRuns.get(name) ?? []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arr.push(sum); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| perSpanRuns.set(name, arr); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const output = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opts: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scenario: opts.scenario, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| port: scenario.port, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastEvent: scenario.lastEvent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runs: opts.runs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| warmup: opts.warmup, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| label: opts.label, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalMs: summarize(totalMsRuns), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| perSpan: Object.fromEntries( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [...perSpanRuns.entries()] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .sort(([a], [b]) => a.localeCompare(b)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map(([name, runs]) => [name, summarize(runs)]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writeFileSync(opts.out, JSON.stringify(output, null, 2)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Wrote ${opts.out}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ('release' in (mysql as any)) (mysql as any).release(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await provider.shutdown(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| main().then( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| () => process.exit(0), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (err) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(err); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+303
to
+313
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
ast-grep --pattern 'export $_ getDbConnection = $$$'
rg -nP --type=ts 'getDbConnection' -C3 packages/daemon/src/dbRepository: HathorNetwork/hathor-wallet-service Length of output: 483 🏁 Script executed: #!/bin/bash
# Check the type definition of MysqlConnection
rg -nP --type=ts 'type MysqlConnection|interface MysqlConnection' packages/daemon/src/db
# Look at the full getDbConnection implementation
cat -n packages/daemon/src/db/index.ts | head -80 | tail -40
# Check the actual bench-sync.ts code around lines 303-313
cat -n packages/daemon/src/scripts/bench-sync.ts | sed -n '290,320p'Repository: HathorNetwork/hathor-wallet-service Length of output: 1889 🏁 Script executed: #!/bin/bash
# Find MysqlConnection type definition
rg -nP --type=ts 'MysqlConnection' packages/daemon/src/types
# Check the main() function in full
cat -n packages/daemon/src/scripts/bench-sync.ts | sed -n '250,315p'
# Check imports to understand which mysql library is being used
head -30 packages/daemon/src/scripts/bench-sync.tsRepository: HathorNetwork/hathor-wallet-service Length of output: 3705 🏁 Script executed: #!/bin/bash
# Find the imports at the top of bench-sync.ts to see what mysql library is used
head -50 packages/daemon/src/scripts/bench-sync.ts
# Check the type imports/definitions
rg -nP --type=ts 'import.*MysqlConnection|type MysqlConnection|interface MysqlConnection' packages/daemon/srcRepository: HathorNetwork/hathor-wallet-service Length of output: 2194 🏁 Script executed: #!/bin/bash
# Check runOnce function to see if it can throw
rg -nP --type=ts 'async function runOnce|runOnce\(' packages/daemon/src/scripts/bench-sync.ts -A 2
# Look for error handling patterns in the codebase for similar scripts
rg -nP --type=ts 'try.*finally|\.then\(' packages/daemon/src/scripts/ -A 3
# Check if there are other places where provider.shutdown() is called
rg -nP --type=ts 'provider\.shutdown' packages/daemon/srcRepository: HathorNetwork/hathor-wallet-service Length of output: 696 🏁 Script executed: #!/bin/bash
# Verify what methods the mysql2 connection object actually has
# Let's check the package.json to see mysql2 version
rg -nP '"mysql2"' packages/daemon/package.json
# Check if there are other examples of release() or end() being called
rg -nP '\.release\(\)|\.end\(\)' packages/daemon/src --type=ts -B 2Repository: HathorNetwork/hathor-wallet-service Length of output: 2225 🏁 Script executed: #!/bin/bash
# Check how mysql connection is used in runOnce and cleanDatabase
rg -nP --type=ts 'async function cleanDatabase|cleanDatabase\(' packages/daemon/src/scripts/bench-sync.ts -A 5
# Check what methods Connection from mysql2 actually exposes by looking at how it's used
rg -nP --type=ts 'mysql\.(destroy|release|end)\(' packages/daemon/src --type=ts -B 2Repository: HathorNetwork/hathor-wallet-service Length of output: 2417 🏁 Script executed: #!/bin/bash
# Find where provider is instantiated/initialized
rg -nP --type=ts 'const provider|provider =' packages/daemon/src/scripts/bench-sync.ts -B 2 -A 2
# Check what provider.shutdown() does and whether it needs error handling
rg -nP --type=ts 'provider\.shutdown|\.shutdown\(\)' packages/daemon/src/scripts/bench-sync.ts -B 5Repository: HathorNetwork/hathor-wallet-service Length of output: 522 🏁 Script executed: #!/bin/bash
# Double-check mysql2/promise Connection API to be thorough
# Look at how release() is actually used in tests or elsewhere
rg -nP --type=ts 'release\(\)' packages/daemon --type=ts -B 3 -A 1
# Check if there are any tests that verify connection cleanup
fd -t f '.*test.*' packages/daemon/src/scripts/ --exec basename {}Repository: HathorNetwork/hathor-wallet-service Length of output: 1994 Wrap If 🛠️ Suggested fix async function main() {
const mysql = await getDbConnection();
-
- const totalMsRuns: number[] = [];
+ try {
+ const totalMsRuns: number[] = [];
// ... existing body ...
- writeFileSync(opts.out, JSON.stringify(output, null, 2));
- console.log(`Wrote ${opts.out}`);
-
- // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
- if ('release' in (mysql as any)) (mysql as any).release();
- await provider.shutdown();
+ writeFileSync(opts.out, JSON.stringify(output, null, 2));
+ console.log(`Wrote ${opts.out}`);
+ } finally {
+ // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
+ if ('release' in (mysql as any)) (mysql as any).release();
+ await provider.shutdown();
+ }
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate CLI argument values (NaN / missing).
Two small gaps in the arg parser:
parseInt(v, 10)on lines 66-67 silently returnsNaNfor non-numeric input.--runs abcresults infor (let i = 0; i < NaN; i++)executing zero iterations, yet the script still writes an "empty" results file and exits0, which is misleading for a benchmarking tool.yarn bench:sync --scenario),visundefinedand gets assigned toopts.scenario/opts.label/opts.outwithout detection.--scenario undefinedthen falls through to theSCENARIOS[opts.scenario]check, but flags like--labelwould silently producebench-results-undefined.json.🛠️ Suggested fix
for (let i = 0; i < args.length; i++) { const a = args[i]; const v = args[i + 1]; + const requireValue = () => { + if (v === undefined) { + console.error(`Missing value for ${a}`); + process.exit(1); + } + return v; + }; + const requireInt = () => { + const n = parseInt(requireValue(), 10); + if (!Number.isFinite(n) || n < 0) { + console.error(`Invalid integer for ${a}: ${v}`); + process.exit(1); + } + return n; + }; 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 '--scenario': opts.scenario = requireValue(); i++; break; + case '--runs': opts.runs = requireInt(); i++; break; + case '--warmup': opts.warmup = requireInt(); i++; break; + case '--label': opts.label = requireValue(); i++; break; + case '--out': opts.out = requireValue(); i++; break;Also consider rejecting
opts.runs === 0(no measured runs) explicitly so the results file isn't produced withn: 0/ null summaries.🤖 Prompt for AI Agents