Loopback HTTP benchmark
This commit is contained in:
@@ -5,6 +5,7 @@ import { join } from 'node:path';
|
||||
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
||||
import { createServer } from 'node:http';
|
||||
import { MARKET_RECONCILIATION_BATCH_SIZE, Market } from '../server/market/store.ts';
|
||||
import { CHAIN_ID, GENESIS_HASH, transactionId } from '../server/testnet/consensus.ts';
|
||||
import { NETWORK } from '../shared/testnet.ts';
|
||||
@@ -15,13 +16,17 @@ const text = process.argv[2] ?? '10000';
|
||||
if (!/^[1-9]\d{0,5}$/.test(text) || Number(text) > 100000) throw new Error('Fixture size must be 1..100000 orders');
|
||||
const count = Number(text), scenario = process.argv[3] ?? 'terminal';
|
||||
if (!['terminal', 'confirmed-delivery'].includes(scenario)) throw new Error('Scenario must be terminal or confirmed-delivery');
|
||||
const workerEnabled = process.argv[4] === 'worker';
|
||||
const executionMode = process.argv[4] ?? 'sync';
|
||||
if (!['sync', 'worker'].includes(executionMode)) throw new Error('Execution mode must be sync or worker');
|
||||
const workerEnabled = executionMode === 'worker';
|
||||
const httpLoadEnabled = process.argv[5] === 'http';
|
||||
const directory = await mkdtemp(join(tmpdir(), 'wikideck-market-reconciliation-'));
|
||||
const streamId = randomUUID(), tip = 'a'.repeat(64);
|
||||
const ledgerFile = join(directory, NETWORK.includes('mainnet') ? 'mainnet' : 'testnet', 'ledger.sqlite');
|
||||
const seller = '1'.repeat(64), buyer = '2'.repeat(64);
|
||||
let market: Market | undefined;
|
||||
let ledgerDb: DatabaseSync | undefined;
|
||||
let loadServer: ReturnType<typeof createServer> | undefined;
|
||||
try {
|
||||
// Indexed-only adapter: the benchmark must not fall back to materializing chain snapshots.
|
||||
await mkdir(join(directory, NETWORK.includes('mainnet') ? 'mainnet' : 'testnet'), { recursive: true });
|
||||
@@ -90,6 +95,37 @@ try {
|
||||
db.exec('COMMIT');
|
||||
} catch (error) { db.exec('ROLLBACK'); throw error; }
|
||||
} finally { db.close(); }
|
||||
let httpLatencies: number[] = [], httpRequests = 0;
|
||||
let httpLoadPromise: Promise<void> | undefined;
|
||||
if (httpLoadEnabled) {
|
||||
loadServer = createServer((request, response) => {
|
||||
try {
|
||||
if (request.url === '/health') {
|
||||
response.writeHead(200, { 'content-type': 'application/json' }); response.end('{"ok":true}'); return;
|
||||
}
|
||||
if (request.url === '/market') {
|
||||
const result = market!.browse();
|
||||
response.writeHead(200, { 'content-type': 'application/json' });
|
||||
response.end(JSON.stringify({ listingCount: result.listings.length, backlog: market!.hasReconciliationBacklog() }));
|
||||
return;
|
||||
}
|
||||
response.writeHead(404); response.end();
|
||||
} catch {
|
||||
response.writeHead(503, { 'retry-after': '1' }); response.end('{"error":"reconciling"}');
|
||||
}
|
||||
});
|
||||
await new Promise<void>(resolve => loadServer!.listen(0, '127.0.0.1', resolve));
|
||||
const address = loadServer.address();
|
||||
if (!address || typeof address === 'string') throw new Error('Benchmark HTTP listener did not bind TCP');
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
httpLoadPromise = Promise.all(Array.from({ length: 160 }, (_, i) => (async () => {
|
||||
const before = performance.now();
|
||||
const response = await fetch(`${baseUrl}${i % 4 === 0 ? '/market' : '/health'}`);
|
||||
if (response.status !== 200) throw new Error(`Loopback benchmark endpoint returned ${response.status}`);
|
||||
await response.arrayBuffer();
|
||||
httpLatencies.push(performance.now() - before);
|
||||
})())).then(() => { httpLatencies.sort((a, b) => a - b); httpRequests = httpLatencies.length; });
|
||||
}
|
||||
const latencies: number[] = [];
|
||||
const deadline = Date.now() + Math.max(30_000, count * 20);
|
||||
const eventLoopDelay = monitorEventLoopDelay({ resolution: 10 });
|
||||
@@ -97,7 +133,8 @@ try {
|
||||
const start = performance.now();
|
||||
let started = false;
|
||||
try {
|
||||
while (Date.now() < deadline && (!started || market.hasReconciliationBacklog() || market.isReconciliationWorkerBusy())) {
|
||||
while (Date.now() < deadline && (!started || market.hasReconciliationBacklog() || market.isReconciliationWorkerBusy() ||
|
||||
(!!httpLoadPromise && httpRequests === 0))) {
|
||||
if (!workerEnabled || !market.isReconciliationWorkerBusy()) {
|
||||
const before = performance.now();
|
||||
market.browse();
|
||||
@@ -110,19 +147,24 @@ try {
|
||||
} finally { eventLoopDelay.disable(); }
|
||||
const elapsedMs = performance.now() - start;
|
||||
if (!started || market.hasReconciliationBacklog() || market.isReconciliationWorkerBusy()) throw new Error('Reconciliation did not finish before benchmark deadline');
|
||||
await httpLoadPromise;
|
||||
await market.close(); market = undefined;
|
||||
ledgerDb.close(); ledgerDb = undefined;
|
||||
const bytes = (await stat(join(directory, NETWORK.includes('mainnet') ? 'mainnet' : 'testnet', 'market.sqlite'))).size;
|
||||
latencies.sort((a, b) => a - b);
|
||||
const percentile = (p: number) => latencies[Math.min(latencies.length - 1, Math.ceil(latencies.length * p) - 1)];
|
||||
console.log(JSON.stringify({ fixture: `synthetic-${scenario}-orders-and-listings`, workerEnabled, orders: count, listings: count,
|
||||
const httpPercentile = (p: number) => httpLatencies.length ? httpLatencies[Math.min(httpLatencies.length - 1, Math.ceil(httpLatencies.length * p) - 1)] : null;
|
||||
console.log(JSON.stringify({ fixture: `synthetic-${scenario}-orders-and-listings`, workerEnabled, httpLoadEnabled, orders: count, listings: count,
|
||||
batchSizePerTable: MARKET_RECONCILIATION_BATCH_SIZE, reconciliationPasses: latencies.length, elapsedMs,
|
||||
rowsPerSecond: Math.round((count * 2 * 1000) / elapsedMs), passLatencyP50Ms: percentile(0.50),
|
||||
passLatencyP95Ms: percentile(0.95), passLatencyMaxMs: latencies.at(-1), sqliteBytes: bytes,
|
||||
eventLoopDelayResolutionMs: 10, eventLoopDelayP95Ms: eventLoopDelay.percentile(95) / 1e6,
|
||||
eventLoopDelayMaxMs: eventLoopDelay.max / 1e6,
|
||||
httpRequests, httpLatencyP50Ms: httpPercentile(0.5), httpLatencyP95Ms: httpPercentile(0.95),
|
||||
httpLatencyMaxMs: httpLatencies.at(-1) ?? null,
|
||||
warning: 'Synthetic fixture on this development host only; SQLite Ledger-shaped facts are not mined or validated chain evidence. Excludes concurrent HTTP load, PostgreSQL, and production SLO certification.' }, null, 2));
|
||||
} finally {
|
||||
if (loadServer) await new Promise<void>(resolve => loadServer!.close(() => resolve()));
|
||||
if (market) await market.close();
|
||||
ledgerDb?.close();
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
|
||||
Reference in New Issue
Block a user