// bench/reader-worker.ts
//
// Child-process entrypoint for bench/reader-catchup.ts. Not run directly.
//
// preload
// Write n keys (`pre:`, all routed to ) then compact, so the
// shard holds a large snapshot + a nearly-empty WAL when the reader
// benchmark starts.
// hammer
// Overwrite `hot:` keys on as fast as possible until SIGTERM,
// keeping the shard's WAL hot so the parent's reads never see a quiet
// fingerprint.
import { ClusterDb } from '../src/cluster/index.js';
import { shardFor } from '../src/cluster/utils.js';
const [, , mode, ...rest] = process.argv;
function out(report: Record): void {
process.stdout.write(JSON.stringify(report) + '\n');
}
/** Keys `${seed}:${n}` (n = 0,1,2,...) that route to `shard`, in order. */
function* keysOnShard(seed: string, shard: number, shards: number): Generator {
for (let n = 0; ; n++) {
const key = `${seed}:${n}`;
if (shardFor(key, shards) === shard) yield key;
}
}
function value(i: number, valueBytes: number): { p: string; i: number; pad: string } {
return { p: 'b', i, pad: 'x'.repeat(valueBytes) };
}
async function main(): Promise {
if (mode === 'preload') {
const [dir, shards, shard, n, valueBytes] = rest;
const db = await ClusterDb.open({
dir: dir!,
shardCount: Number(shards),
valueCodec: 'json',
fsyncPolicy: 'no',
autoCompact: false,
});
const gen = keysOnShard('pre', Number(shard), Number(shards));
const t0 = performance.now();
const count = Number(n);
for (let i = 0; i < count; i++) {
await db.set(gen.next().value!, value(i, Number(valueBytes)));
}
const writeMs = performance.now() - t0;
const t1 = performance.now();
await db.compact();
const compactMs = performance.now() - t1;
out({ ok: 1, mode, n: count, ms: writeMs, compactMs });
await db.close();
return;
}
if (mode === 'hammer') {
const [dir, shards, shard, valueBytes, paceMs = '5'] = rest;
const db = await ClusterDb.open({
dir: dir!,
shardCount: Number(shards),
valueCodec: 'json',
fsyncPolicy: 'no',
// No compaction during the measured window: rotation costs are covered
// by tests, mixing one into a latency sample only muddies the numbers.
autoCompact: false,
// Keep the shard lock held: the default 250ms hold window would force a
// writer reopen (full WAL replay) several times a second, which would
// throttle the WAL drip this benchmark is meant to produce.
lockHoldMs: 0,
});
let stop = false;
process.on('SIGTERM', () => {
stop = true;
});
// A small yield between writes paces the WAL into a steady drip: un-paced,
// the writer's own group commit coalesces hundreds of frames into one
// writev, so a reader only observes the file change a few times a second
// instead of on (almost) every read.
const pace = Number(paceMs);
const gen = keysOnShard('hot', Number(shard), Number(shards));
let i = 0;
const t0 = performance.now();
while (!stop) {
await db.set(gen.next().value!, value(i, Number(valueBytes)));
i++;
if (pace > 0) await new Promise((r) => setTimeout(r, pace));
}
out({ ok: 1, mode, n: i, ms: performance.now() - t0 });
await db.close();
return;
}
out({ ok: 0, error: `unknown mode: ${mode}` });
process.exit(1);
}
main().catch((e) => {
out({ ok: 0, mode, error: String(e && (e as Error).stack ? (e as Error).stack : e) });
process.exit(1);
});