test(fast-browser): retain per-operation timing and failure costs

This commit is contained in:
jb
2026-09-21 13:33:51 -04:00
parent 6a725c1f47
commit 12a6bb6afb
2 changed files with 48 additions and 9 deletions
@@ -6,6 +6,7 @@ import {
pairedArmOrder, pairedArmOrder,
safeFailure, safeFailure,
summarizeBenchmarkAttempts, summarizeBenchmarkAttempts,
timingDelta,
validWorkerCounters, validWorkerCounters,
summarizeTimings, summarizeTimings,
validateBenchmarkOptions, validateBenchmarkOptions,
@@ -27,6 +28,11 @@ test("timing aggregation reports bounded p50 and p95 without wall-clock assumpti
assert.deepEqual(summarizeTimings([]), { count: 0, p50: null, p95: null }); assert.deepEqual(summarizeTimings([]), { count: 0, p50: null, p95: null });
}); });
test("transport timer deltas preserve bounded operation names and failed costs", () => {
assert.deepEqual(timingDelta({ observe: 12, closeTab: 4 }, { observe: 19, closeTab: 4, waitPopup: 8 }), { observe: 7, waitPopup: 8 });
assert.deepEqual(timingDelta({ observe: 10 }, { observe: 9 }), {});
});
test("paired order alternates AB and BA while retaining failed attempts", () => { test("paired order alternates AB and BA while retaining failed attempts", () => {
assert.deepEqual(pairedArmOrder(1), ["baseline", "optimized"]); assert.deepEqual(pairedArmOrder(1), ["baseline", "optimized"]);
assert.deepEqual(pairedArmOrder(2), ["optimized", "baseline"]); assert.deepEqual(pairedArmOrder(2), ["optimized", "baseline"]);
@@ -8,7 +8,7 @@ import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { createBrowserTransport, type ChildProcessLike, type SpawnLike } from "./bridge.ts"; import { createBrowserTransport, type ChildProcessLike, type SpawnLike } from "./bridge.ts";
import { createBrowserSessionManager, type BrowserSession, type BrowserSessionManager } from "./browser-session.ts"; import { createBrowserSessionManager, type BrowserSession, type BrowserSessionManager } from "./browser-session.ts";
import type { BrowserRuntime, BrowserTransportFactory, Candidate, PrivateArtifactStore } from "./contracts.ts"; import type { BrowserOperation, BrowserRuntime, BrowserTransport, BrowserTransportFactory, BrowserTransportRequest, BrowserTransportResultFor, Candidate, PrivateArtifactStore } from "./contracts.ts";
import { createBrowserRuntimeFactory } from "./runtime.ts"; import { createBrowserRuntimeFactory } from "./runtime.ts";
const ROOT = dirname(fileURLToPath(import.meta.url)); const ROOT = dirname(fileURLToPath(import.meta.url));
@@ -26,6 +26,8 @@ const MAX_COUNTS = 3;
type Arm = "baseline" | "optimized"; type Arm = "baseline" | "optimized";
type Phase = "warm" | "cold-start"; type Phase = "warm" | "cold-start";
export type TimedOperation = "observe" | "act" | "read" | "assert" | "navigate" | "reload" | "listTabs" | "selectTab" | "waitPopup" | "closeTab" | "captureEvidence" | "narrowOriginPolicy" | "close";
export type OperationTimings = Partial<Record<TimedOperation, number>>;
export interface PerformanceBenchmarkOptions { export interface PerformanceBenchmarkOptions {
baselineWorkerPath: string; baselineWorkerPath: string;
@@ -54,6 +56,7 @@ export interface BenchmarkAttempt {
error?: string; error?: string;
protocolOperations?: Record<string, number>; protocolOperations?: Record<string, number>;
densityObserveMs?: Partial<Record<"0" | "10" | "60", number>>; densityObserveMs?: Partial<Record<"0" | "10" | "60", number>>;
transportMs?: OperationTimings;
} }
export interface WorkerMethodCounts { export interface WorkerMethodCounts {
@@ -90,6 +93,7 @@ interface ArmHandle {
session: BrowserSession; session: BrowserSession;
runtime: BrowserRuntime; runtime: BrowserRuntime;
protocol: Record<string, number>; protocol: Record<string, number>;
transportMs: OperationTimings;
countFile: string; countFile: string;
closed: boolean; closed: boolean;
} }
@@ -205,10 +209,26 @@ function instrumentedSpawn(
}; };
} }
function timedTransport(base: BrowserTransport, timings: OperationTimings): BrowserTransport {
return {
async dispatch<Operation extends BrowserOperation>(request: BrowserTransportRequest<Operation>, signal: AbortSignal): Promise<BrowserTransportResultFor<Operation>> {
const operation = request.operation.op as TimedOperation;
const started = performance.now();
try {
return await base.dispatch(request, signal);
} finally {
timings[operation] = (timings[operation] ?? 0) + performance.now() - started;
}
},
close: () => base.close(),
};
}
function createInstrumentedRuntime( function createInstrumentedRuntime(
armWorkerPath: string, armWorkerPath: string,
countFile: string, countFile: string,
protocol: Record<string, number>, protocol: Record<string, number>,
transportMs: OperationTimings,
session: BrowserSession, session: BrowserSession,
origin: string, origin: string,
pythonPath: string, pythonPath: string,
@@ -216,11 +236,12 @@ function createInstrumentedRuntime(
): BrowserRuntime { ): BrowserRuntime {
const transportFactory: BrowserTransportFactory = { const transportFactory: BrowserTransportFactory = {
create(options, parentSignal) { create(options, parentSignal) {
return createBrowserTransport({ const transport = createBrowserTransport({
...options, ...options,
workerPath: PROBE, workerPath: PROBE,
spawn: instrumentedSpawn(armWorkerPath, countFile, protocol), spawn: instrumentedSpawn(armWorkerPath, countFile, protocol),
}, parentSignal); }, parentSignal);
return timedTransport(transport, transportMs);
}, },
}; };
return createBrowserRuntimeFactory(transportFactory).create({ return createBrowserRuntimeFactory(transportFactory).create({
@@ -384,6 +405,15 @@ function snapshotCounts(value: Record<string, number>): Record<string, number> {
return { ...value }; return { ...value };
} }
export function timingDelta(before: OperationTimings, after: OperationTimings): OperationTimings {
const result: OperationTimings = {};
for (const key of new Set([...Object.keys(before), ...Object.keys(after)] as TimedOperation[])) {
const difference = (after[key] ?? 0) - (before[key] ?? 0);
if (difference > 0) result[key] = difference;
}
return result;
}
function countDelta(before: Record<string, number>, after: Record<string, number>): Record<string, number> { function countDelta(before: Record<string, number>, after: Record<string, number>): Record<string, number> {
const result: Record<string, number> = {}; const result: Record<string, number> = {};
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) { for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
@@ -452,6 +482,7 @@ export async function runPerformanceBenchmark(options: PerformanceBenchmarkOptio
for (let sample = 1; sample <= config.coldSamples; sample += 1) { for (let sample = 1; sample <= config.coldSamples; sample += 1) {
const countFile = join(root, `${arm}-cold-${sample}.json`); const countFile = join(root, `${arm}-cold-${sample}.json`);
const protocol: Record<string, number> = {}; const protocol: Record<string, number> = {};
const transportMs: OperationTimings = {};
let session: BrowserSession | undefined; let session: BrowserSession | undefined;
let runtime: BrowserRuntime | undefined; let runtime: BrowserRuntime | undefined;
const started = Date.now(); const started = Date.now();
@@ -460,13 +491,13 @@ export async function runPerformanceBenchmark(options: PerformanceBenchmarkOptio
const opened = Date.now(); const opened = Date.now();
session = await manager.open(launchConfig(fixture.origin, `${fixture.origin}/performance.html?mode=density&count=10`, config.browserPath), controller.signal); session = await manager.open(launchConfig(fixture.origin, `${fixture.origin}/performance.html?mode=density&count=10`, config.browserPath), controller.signal);
const first = Date.now(); const first = Date.now();
runtime = createInstrumentedRuntime(armPaths[arm], countFile, protocol, session, fixture.origin, config.pythonPath, controller.signal); runtime = createInstrumentedRuntime(armPaths[arm], countFile, protocol, transportMs, session, fixture.origin, config.pythonPath, controller.signal);
const observation = await runtime.observe(controller.signal); const observation = await runtime.observe(controller.signal);
assert.equal(observation.candidates.length, 10); assert.equal(observation.candidates.length, 10);
startupMs = first - opened + (Date.now() - first); startupMs = first - opened + (Date.now() - first);
attempts.push({ arm, phase: "cold-start", sample, status: "passed", elapsedMs: Date.now() - started, startupMs, protocolOperations: { ...protocol } }); attempts.push({ arm, phase: "cold-start", sample, status: "passed", elapsedMs: Date.now() - started, startupMs, protocolOperations: { ...protocol }, transportMs: { ...transportMs } });
} catch (error) { } catch (error) {
attempts.push({ arm, phase: "cold-start", sample, status: "failed", elapsedMs: null, startupMs, error: safeFailure("cold-start"), protocolOperations: { ...protocol } }); attempts.push({ arm, phase: "cold-start", sample, status: "failed", elapsedMs: Date.now() - started, startupMs, error: safeFailure("cold-start"), protocolOperations: { ...protocol }, transportMs: { ...transportMs } });
} finally { } finally {
try { await runtime?.close(); } catch { cleanupError ??= safeFailure("cleanup"); } try { await runtime?.close(); } catch { cleanupError ??= safeFailure("cleanup"); }
if (session) await manager.close(session.id).catch(() => { cleanupError ??= safeFailure("cleanup"); }); if (session) await manager.close(session.id).catch(() => { cleanupError ??= safeFailure("cleanup"); });
@@ -478,9 +509,10 @@ export async function runPerformanceBenchmark(options: PerformanceBenchmarkOptio
try { try {
const countFile = join(root, `${arm}-warm.json`); const countFile = join(root, `${arm}-warm.json`);
const protocol: Record<string, number> = {}; const protocol: Record<string, number> = {};
const transportMs: OperationTimings = {};
const session = await manager.open(launchConfig(fixture.origin, `${fixture.origin}/performance.html?mode=density&count=0`, config.browserPath), controller.signal); const session = await manager.open(launchConfig(fixture.origin, `${fixture.origin}/performance.html?mode=density&count=0`, config.browserPath), controller.signal);
const runtime = createInstrumentedRuntime(armPaths[arm], countFile, protocol, session, fixture.origin, config.pythonPath, controller.signal); const runtime = createInstrumentedRuntime(armPaths[arm], countFile, protocol, transportMs, session, fixture.origin, config.pythonPath, controller.signal);
const handle: ArmHandle = { arm, session, runtime, protocol, countFile, closed: false }; const handle: ArmHandle = { arm, session, runtime, protocol, transportMs, countFile, closed: false };
handles[arm] = handle; handles[arm] = handle;
await runtime.observe(controller.signal); // prime the persistent worker; excluded from warm samples await runtime.observe(controller.signal); // prime the persistent worker; excluded from warm samples
} catch (error) { } catch (error) {
@@ -501,13 +533,14 @@ export async function runPerformanceBenchmark(options: PerformanceBenchmarkOptio
const arm = order[position]!; const arm = order[position]!;
const handle = handles[arm]; const handle = handles[arm];
const before = handle ? snapshotCounts(handle.protocol) : {}; const before = handle ? snapshotCounts(handle.protocol) : {};
const beforeTransport = handle ? { ...handle.transportMs } : {};
const started = Date.now(); const started = Date.now();
try { try {
if (!handle) throw new Error("arm setup failed"); if (!handle) throw new Error("arm setup failed");
const densityObserveMs = await runWarmFlow(handle, fixture.origin, config.candidateCounts, controller.signal); const densityObserveMs = await runWarmFlow(handle, fixture.origin, config.candidateCounts, controller.signal);
attempts.push({ arm, phase: "warm", sample, order: (position + 1) as 1 | 2, status: "passed", elapsedMs: Date.now() - started, startupMs: null, protocolOperations: countDelta(before, handle.protocol), densityObserveMs }); attempts.push({ arm, phase: "warm", sample, order: (position + 1) as 1 | 2, status: "passed", elapsedMs: Date.now() - started, startupMs: null, protocolOperations: countDelta(before, handle.protocol), transportMs: timingDelta(beforeTransport, handle.transportMs), densityObserveMs });
} catch (error) { } catch (error) {
attempts.push({ arm, phase: "warm", sample, order: (position + 1) as 1 | 2, status: "failed", elapsedMs: null, startupMs: null, error: handle ? failureFor(error, "warm-flow") : (setupErrors[arm] ?? safeFailure("warm-setup")), protocolOperations: handle ? countDelta(before, handle.protocol) : {} }); attempts.push({ arm, phase: "warm", sample, order: (position + 1) as 1 | 2, status: "failed", elapsedMs: Date.now() - started, startupMs: null, error: handle ? failureFor(error, "warm-flow") : (setupErrors[arm] ?? safeFailure("warm-setup")), protocolOperations: handle ? countDelta(before, handle.protocol) : {}, transportMs: handle ? timingDelta(beforeTransport, handle.transportMs) : {} });
} }
} }
} }