type SweepResult = { name: string; runner: "p2p" | "couchdb"; rttMs?: number; result: Record; }; function readEnvString(name: string, fallback: string): string { const value = Deno.env.get(name)?.trim(); return value && value.length > 0 ? value : fallback; } function timestamp(): string { const d = new Date(); const pad = (n: number) => String(n).padStart(2, "0"); return ( `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-` + `${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}` ); } function parseRttList(raw: string): number[] { const values = raw .split(",") .map((value) => Number(value.trim())) .filter((value) => Number.isFinite(value) && value > 0) .map((value) => Math.floor(value)); if (values.length === 0) { throw new Error(`BENCH_SWEEP_RTT_MS must contain at least one positive number, got '${raw}'`); } return values; } function buildBaseEnv(): Record { return { BENCH_MD_FILE_COUNT: readEnvString("BENCH_MD_FILE_COUNT", "20"), BENCH_MD_MIN_SIZE_BYTES: readEnvString("BENCH_MD_MIN_SIZE_BYTES", "512"), BENCH_MD_MAX_SIZE_BYTES: readEnvString("BENCH_MD_MAX_SIZE_BYTES", "2048"), BENCH_BIN_FILE_COUNT: readEnvString("BENCH_BIN_FILE_COUNT", "5"), BENCH_BIN_SIZE_BYTES: readEnvString("BENCH_BIN_SIZE_BYTES", "8192"), BENCH_SYNC_TIMEOUT: readEnvString("BENCH_SYNC_TIMEOUT", "300"), BENCH_PEERS_TIMEOUT: readEnvString("BENCH_PEERS_TIMEOUT", "60"), BENCH_SEED: readEnvString("BENCH_SEED", "livesync-benchmark-seed"), LIVESYNC_TEST_TEE: readEnvString("BENCH_LIVESYNC_TEST_TEE", "0"), }; } async function runBenchmark(options: { taskName: "bench:p2p" | "bench:couchdb"; name: string; outputDir: string; env: Record; }): Promise> { const resultPath = `${options.outputDir}/${options.name}.json`; const env = { ...Deno.env.toObject(), ...options.env, BENCH_RESULT_JSON: resultPath, }; console.log(`[latency-sweep] running ${options.name}`); const child = new Deno.Command("deno", { args: ["task", options.taskName], cwd: import.meta.dirname, env, stdin: "null", stdout: "inherit", stderr: "inherit", }).spawn(); const status = await child.status; if (status.code !== 0) { throw new Error(`benchmark failed: ${options.name} (exit ${status.code})`); } return JSON.parse(await Deno.readTextFile(resultPath)) as Record; } async function main(): Promise { const outRoot = readEnvString("BENCH_SWEEP_ROOT", `${import.meta.dirname}/bench-results`); const outputDir = `${outRoot}/latency-sweep-${timestamp()}`; const rtts = parseRttList(readEnvString("BENCH_SWEEP_RTT_MS", "20,50,100,150,300")); const base = buildBaseEnv(); await Deno.mkdir(outputDir, { recursive: true }); const results: SweepResult[] = []; if (readEnvString("BENCH_SWEEP_INCLUDE_P2P", "true") !== "false") { const p2pResult = await runBenchmark({ taskName: "bench:p2p", name: "p2p-direct-local", outputDir, env: { ...base, BENCH_CASE: "p2p-direct-local", BENCH_TURN_SERVERS: "", }, }); results.push({ name: "p2p-direct-local", runner: "p2p", result: p2pResult }); } for (const rtt of rtts) { const name = `couchdb-rtt-${rtt}ms`; const couchdbResult = await runBenchmark({ taskName: "bench:couchdb", name, outputDir, env: { ...base, BENCH_CASE: name, BENCH_COUCHDB_RTT_MS: String(rtt), }, }); results.push({ name, runner: "couchdb", rttMs: rtt, result: couchdbResult }); } const summary = { generatedAt: new Date().toISOString(), outputDir, note: "This sweep models additional remote CouchDB request latency through the existing HTTP proxy. It is not a full netem model of jitter, loss, MTU, bandwidth, or VPN encapsulation.", rtts, results, }; await Deno.writeTextFile(`${outputDir}/summary.json`, JSON.stringify(summary, null, 2)); console.log(JSON.stringify(summary, null, 2)); console.log(`[latency-sweep] result directory: ${outputDir}`); } if (import.meta.main) { main().catch((error) => { console.error("[Fatal Error]", error); Deno.exit(1); }); }