Skip to content

Commit fd20db0

Browse files
committed
test(subagent): prove cold-start overlap
1 parent 91d9cc0 commit fd20db0

2 files changed

Lines changed: 277 additions & 47 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
#!/usr/bin/env bun
2+
3+
import { mkdir, mkdtemp, rm } from "node:fs/promises";
4+
import { tmpdir } from "node:os";
5+
import { join, resolve } from "node:path";
6+
import { performance } from "node:perf_hooks";
7+
8+
import {
9+
createSkillSearchTool,
10+
workerSkillSearchDefinition,
11+
} from "../src/agent/skill-search.js";
12+
import {
13+
createUseSkillTool,
14+
workerUseSkillDefinition,
15+
} from "../src/agent/use-skill.js";
16+
import { discoverSkills } from "../src/extensions/skills.js";
17+
import { assembleInferenceBase } from "../src/session/assemble-runtime.js";
18+
import { createSessionStores } from "../src/session/optimized-context-store.js";
19+
20+
type Mode = "sequential" | "overlap";
21+
type Measurement = {
22+
a: number;
23+
b: number;
24+
c: number;
25+
readiness: number;
26+
};
27+
type Sample = {
28+
sample: number;
29+
sequential: Measurement;
30+
overlap: Measurement;
31+
savings: number;
32+
};
33+
34+
const DEFAULT_SAMPLES = 7;
35+
const repositoryRoot = resolve(import.meta.dir, "..");
36+
const skillDirs = [join(repositoryRoot, "plugins/corbits-skills")];
37+
const allowedSkillNames = [
38+
"style",
39+
"philosophy",
40+
"native-runtime",
41+
"idiot-proof",
42+
"ponytail",
43+
];
44+
const attachedSkills = ["style", "philosophy"];
45+
46+
function parseSampleCount(args: readonly string[]): number {
47+
const raw = args
48+
.find((arg) => arg.startsWith("--samples="))
49+
?.slice("--samples=".length);
50+
if (raw === undefined) return DEFAULT_SAMPLES;
51+
52+
const samples = Number(raw);
53+
if (!Number.isSafeInteger(samples) || samples < 1) {
54+
throw new Error("--samples must be a positive integer");
55+
}
56+
return samples;
57+
}
58+
59+
function elapsedSince(startedAt: number): number {
60+
return performance.now() - startedAt;
61+
}
62+
63+
async function measureLeaf(mode: Mode, workdir: string): Promise<Measurement> {
64+
const readinessStartedAt = performance.now();
65+
66+
let phaseStartedAt = performance.now();
67+
await assembleInferenceBase();
68+
const a = elapsedSince(phaseStartedAt);
69+
70+
phaseStartedAt = performance.now();
71+
const skills = await discoverSkills(repositoryRoot, skillDirs);
72+
createSkillSearchTool({
73+
skills,
74+
allowedNames: allowedSkillNames,
75+
definition: workerSkillSearchDefinition,
76+
});
77+
createUseSkillTool(
78+
repositoryRoot,
79+
skillDirs,
80+
undefined,
81+
allowedSkillNames,
82+
workerUseSkillDefinition,
83+
attachedSkills,
84+
);
85+
const b = elapsedSince(phaseStartedAt);
86+
87+
phaseStartedAt = performance.now();
88+
if (mode === "sequential") {
89+
await mkdir(workdir, { recursive: true });
90+
await createSessionStores(workdir);
91+
} else {
92+
const storesPromise = createSessionStores(workdir);
93+
void storesPromise.catch(() => undefined);
94+
await mkdir(workdir, { recursive: true });
95+
await storesPromise;
96+
}
97+
const c = elapsedSince(phaseStartedAt);
98+
99+
return { a, b, c, readiness: elapsedSince(readinessStartedAt) };
100+
}
101+
102+
function summary(values: readonly number[]) {
103+
const sorted = [...values].sort((left, right) => left - right);
104+
const middle = Math.floor(sorted.length / 2);
105+
const median =
106+
sorted.length % 2 === 0
107+
? ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
108+
: (sorted[middle] ?? 0);
109+
return {
110+
median,
111+
mean: values.reduce((sum, value) => sum + value, 0) / values.length,
112+
};
113+
}
114+
115+
function round(value: number): number {
116+
return Number(value.toFixed(3));
117+
}
118+
119+
function roundedSummary(values: readonly number[]) {
120+
const result = summary(values);
121+
return { median: round(result.median), mean: round(result.mean) };
122+
}
123+
124+
async function main(): Promise<void> {
125+
const sampleCount = parseSampleCount(process.argv.slice(2));
126+
const root = await mkdtemp(join(tmpdir(), "corbits-subagent-cold-start-"));
127+
const samples: Sample[] = [];
128+
129+
try {
130+
await measureLeaf("sequential", join(root, "warmup-sequential"));
131+
await measureLeaf("overlap", join(root, "warmup-overlap"));
132+
133+
for (let sample = 1; sample <= sampleCount; sample++) {
134+
const modes: readonly Mode[] =
135+
sample % 2 === 1
136+
? ["sequential", "overlap"]
137+
: ["overlap", "sequential"];
138+
const measurements = new Map<Mode, Measurement>();
139+
140+
for (const mode of modes) {
141+
measurements.set(
142+
mode,
143+
await measureLeaf(mode, join(root, `${sample}-${mode}`)),
144+
);
145+
}
146+
147+
const sequential = measurements.get("sequential");
148+
const overlap = measurements.get("overlap");
149+
if (sequential === undefined || overlap === undefined) {
150+
throw new Error("both benchmark modes must complete");
151+
}
152+
samples.push({
153+
sample,
154+
sequential,
155+
overlap,
156+
savings: sequential.readiness - overlap.readiness,
157+
});
158+
}
159+
160+
const roundedSamples = samples.map((sample) => ({
161+
sample: sample.sample,
162+
sequential: Object.fromEntries(
163+
Object.entries(sample.sequential).map(([key, value]) => [
164+
key,
165+
round(value),
166+
]),
167+
),
168+
overlap: Object.fromEntries(
169+
Object.entries(sample.overlap).map(([key, value]) => [
170+
key,
171+
round(value),
172+
]),
173+
),
174+
savings: round(sample.savings),
175+
}));
176+
const sequentialReadiness = samples.map(
177+
(sample) => sample.sequential.readiness,
178+
);
179+
const overlapReadiness = samples.map((sample) => sample.overlap.readiness);
180+
const savings = samples.map((sample) => sample.savings);
181+
const overlapResidual = samples.map((sample) => sample.overlap.c);
182+
183+
console.log(
184+
JSON.stringify(
185+
{
186+
samplesPerMode: sampleCount,
187+
warmupRunsPerMode: 1,
188+
units: "milliseconds",
189+
boundaries: {
190+
a: "assemble inference dependencies",
191+
b: "discover skills and construct skill tools",
192+
c: "make the workdir and initialize session stores",
193+
readiness: "a + b + c, immediately before agent construction",
194+
sequential: "await mkdir, then initialize stores",
195+
overlap: "start stores, await mkdir, then await stores",
196+
residual:
197+
"overlap c latency still visible on the leaf-readiness critical path",
198+
},
199+
samples: roundedSamples,
200+
summary: {
201+
beforeSequentialReadiness: roundedSummary(sequentialReadiness),
202+
afterOverlapReadiness: roundedSummary(overlapReadiness),
203+
pairedSavings: roundedSummary(savings),
204+
overlapResidual: roundedSummary(overlapResidual),
205+
},
206+
},
207+
null,
208+
2,
209+
),
210+
);
211+
} finally {
212+
await rm(root, { recursive: true, force: true });
213+
}
214+
}
215+
216+
await main();

‎src/subagent/run-audit-store.test.ts‎

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, test } from "bun:test";
2-
import { mkdtemp, rm, stat } from "node:fs/promises";
2+
import { mkdtemp, rm } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { basename, join } from "node:path";
55
import type { AuditStore, ContextStore } from "@intx/types/runtime";
@@ -59,19 +59,6 @@ function runParams(cwd: string, id: string) {
5959
};
6060
}
6161

62-
async function eventuallyExists(path: string): Promise<boolean> {
63-
for (let attempt = 0; attempt < 50; attempt++) {
64-
if (
65-
await stat(path)
66-
.then(() => true)
67-
.catch(() => false)
68-
)
69-
return true;
70-
await Bun.sleep(1);
71-
}
72-
return false;
73-
}
74-
7562
test("runSubAgent threads the isogit audit store and session id into createAgent", async () => {
7663
const cwd = await mkdtemp(join(tmpdir(), "corbits-run-audit-"));
7764
const store = fakeStore();
@@ -125,61 +112,88 @@ test("runSubAgent threads the isogit audit store and session id into createAgent
125112
test("runSubAgent overlaps store creation with workdir setup", async () => {
126113
const cwd = await mkdtemp(join(tmpdir(), "corbits-run-store-overlap-"));
127114
const store = fakeStore();
115+
let releaseMkdir: (() => void) | undefined;
116+
let signalMkdirStarted: (() => void) | undefined;
117+
let signalMkdirFinished: (() => void) | undefined;
118+
const pendingMkdir = new Promise<void>((resolve) => {
119+
releaseMkdir = resolve;
120+
});
121+
const mkdirStarted = new Promise<void>((resolve) => {
122+
signalMkdirStarted = resolve;
123+
});
124+
const mkdirFinished = new Promise<void>((resolve) => {
125+
signalMkdirFinished = resolve;
126+
});
128127
let resolveStores:
129128
| ((stores: { storage: ContextStore; audit: AuditStore }) => void)
130129
| undefined;
131-
let signalStoreStarted: (() => void) | undefined;
132-
const storeStarted = new Promise<void>((resolve) => {
133-
signalStoreStarted = resolve;
134-
});
135130
const pendingStores = new Promise<{
136131
storage: ContextStore;
137132
audit: AuditStore;
138133
}>((resolve) => {
139134
resolveStores = resolve;
140135
});
136+
let storeStarted = false;
141137
let agentConstructed = false;
142138

143139
try {
144140
await withMockedModuleDuring(
145-
import.meta.resolve("../session/optimized-context-store.js"),
146-
(real: typeof import("../session/optimized-context-store.js")) => ({
141+
import.meta.resolve("node:fs/promises"),
142+
(real: typeof import("node:fs/promises")) => ({
147143
...real,
148-
createSessionStores: () => {
149-
defined(signalStoreStarted, "store start signal")();
150-
return pendingStores;
144+
mkdir: () => {
145+
defined(signalMkdirStarted, "mkdir start signal")();
146+
return pendingMkdir.then(() => {
147+
defined(signalMkdirFinished, "mkdir finish signal")();
148+
return undefined;
149+
});
151150
},
152151
}),
153152
async () => {
154153
await withMockedModuleDuring(
155-
import.meta.resolve("../agent/live-tool-dispatch.js"),
156-
(real: typeof import("../agent/live-tool-dispatch.js")) => ({
154+
import.meta.resolve("../session/optimized-context-store.js"),
155+
(real: typeof import("../session/optimized-context-store.js")) => ({
157156
...real,
158-
createAgentWithLiveToolDispatch: async () => {
159-
agentConstructed = true;
160-
return stubAgent() as unknown as Awaited<
161-
ReturnType<typeof real.createAgentWithLiveToolDispatch>
162-
>;
157+
createSessionStores: () => {
158+
storeStarted = true;
159+
return pendingStores;
163160
},
164161
}),
165162
async () => {
166-
const { runSubAgent } = await import("./run.js");
167-
const run = runSubAgent(runParams(cwd, "overlap-child"));
168-
await storeStarted;
169-
170-
const workdir = join(cwd, ".ctx", "subagents", "overlap-child");
171-
expect(await eventuallyExists(workdir)).toBe(true);
172-
expect(agentConstructed).toBe(false);
173-
174-
defined(
175-
resolveStores,
176-
"store resolver",
177-
)({
178-
storage: store,
179-
audit: store,
180-
});
181-
await run;
182-
expect(agentConstructed).toBe(true);
163+
await withMockedModuleDuring(
164+
import.meta.resolve("../agent/live-tool-dispatch.js"),
165+
(real: typeof import("../agent/live-tool-dispatch.js")) => ({
166+
...real,
167+
createAgentWithLiveToolDispatch: async () => {
168+
agentConstructed = true;
169+
return stubAgent() as unknown as Awaited<
170+
ReturnType<typeof real.createAgentWithLiveToolDispatch>
171+
>;
172+
},
173+
}),
174+
async () => {
175+
const { runSubAgent } = await import("./run.js");
176+
const run = runSubAgent(runParams(cwd, "overlap-child"));
177+
await mkdirStarted;
178+
179+
expect(storeStarted).toBe(true);
180+
expect(agentConstructed).toBe(false);
181+
182+
defined(releaseMkdir, "mkdir resolver")();
183+
await mkdirFinished;
184+
expect(agentConstructed).toBe(false);
185+
186+
defined(
187+
resolveStores,
188+
"store resolver",
189+
)({
190+
storage: store,
191+
audit: store,
192+
});
193+
await run;
194+
expect(agentConstructed).toBe(true);
195+
},
196+
);
183197
},
184198
);
185199
},

0 commit comments

Comments
 (0)