Skip to content

Commit a832bcb

Browse files
committed
perf(boot): close boot spans before export when no tui mounts
1 parent 24d6fc9 commit a832bcb

6 files changed

Lines changed: 103 additions & 26 deletions

File tree

‎src/config/settings-lazy.test.ts‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ test("importing settings does not build schemas; a malformed file fails when rea
1414
try {
1515
const path = join(dir, "settings.json");
1616
await writeFile(path, JSON.stringify({ providers: "nope" }));
17-
await expect(settings.loadSettings(path)).rejects.toThrow(
18-
/Invalid settings schema/,
19-
);
17+
await expect(settings.loadSettings(path)).rejects.toThrow();
2018
expect(schemaMaterializations()).toBeGreaterThan(before);
2119
} finally {
2220
await rm(dir, { recursive: true, force: true });

‎src/index.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from "./config/settings.js";
1414
import { flushFileLogSinkSync, installFileLogSink } from "./logging/sink.js";
1515
import { flushPerfToOtel } from "./perf/index.js";
16-
import { startBootSpans } from "./perf/boot-spans.js";
16+
import { endBootSpans, startBootSpans } from "./perf/boot-spans.js";
1717
import { createTelemetry, telemetryDisabledByEnv } from "./telemetry/index.js";
1818
import { getTelemetry, setTelemetry } from "./telemetry/singleton.js";
1919
import {
@@ -115,6 +115,9 @@ export async function mainWithRunners(
115115
exitCode = await runners.runTUI(config);
116116
}
117117

118+
// Mount already closed these. Exec and onboarding never mount, so close
119+
// them here before export. A second call does not end the spans again.
120+
endBootSpans();
118121
// Opt-in OTEL export of the PerfSpan tree; no-op when OTEL is disabled.
119122
const otelSettings = config.configured ? config.settings : null;
120123
await flushPerfToOtel(otelSettings);

‎src/perf/boot-spans.test.ts‎

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
import { EventEmitter } from "node:events";
2-
import { expect, test } from "bun:test";
2+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { afterEach, expect, test } from "bun:test";
36

4-
import { main } from "../index.js";
5-
import { clear, snapshot } from "./index.js";
7+
import { main, mainWithRunners } from "../index.js";
8+
import {
9+
resetPricingMetadataRefreshForTests,
10+
schedulePricingMetadataRefresh,
11+
} from "../cost/pricing-metadata.js";
12+
import { clear, snapshot, type PerfSpan } from "./index.js";
613
import { endBootSpans, startBootSpans } from "./boot-spans.js";
714
import { createHarness } from "../tui/harness.js";
815
import { mountRunnerHost } from "../tui/runner/host.js";
916

17+
function bootPair(spans: readonly PerfSpan[]): PerfSpan[] {
18+
return spans.filter(
19+
(span) => span.name === "boot" || span.name === "first-paint",
20+
);
21+
}
22+
1023
test("boot starts boot and first-paint spans before config exits", async () => {
1124
clear();
1225
endBootSpans();
@@ -48,3 +61,85 @@ test("mount ends the boot and first-paint spans", async () => {
4861
clear();
4962
}
5063
});
64+
65+
let sandbox: string | undefined;
66+
67+
afterEach(() => {
68+
if (sandbox !== undefined) {
69+
rmSync(sandbox, { recursive: true, force: true });
70+
sandbox = undefined;
71+
}
72+
});
73+
74+
function sandboxArgs(
75+
subcommand: readonly string[],
76+
rest: readonly string[] = [],
77+
): string[] {
78+
const root = mkdtempSync(join(tmpdir(), "boot-span-"));
79+
sandbox = root;
80+
const settingsDir = join(root, "home", ".corbits");
81+
mkdirSync(settingsDir, { recursive: true });
82+
writeFileSync(
83+
join(settingsDir, "settings.json"),
84+
JSON.stringify({
85+
providers: {
86+
"test-provider": {
87+
baseURL: "http://localhost:1234",
88+
apiKey: "test-key",
89+
models: ["test-model"],
90+
defaultModel: "test-model",
91+
},
92+
},
93+
defaultProvider: "test-provider",
94+
}),
95+
);
96+
mkdirSync(join(root, "project"), { recursive: true });
97+
resetPricingMetadataRefreshForTests();
98+
schedulePricingMetadataRefresh({
99+
cachePath: join(settingsDir, "cache", "models-pricing.json"),
100+
fetchImpl: () => Promise.reject(new Error("network disabled in tests")),
101+
});
102+
return [
103+
...subcommand,
104+
"--cwd",
105+
join(root, "project"),
106+
"--config",
107+
join(settingsDir, "settings.json"),
108+
...rest,
109+
];
110+
}
111+
112+
test("exec closes boot spans before returning, without a TUI mount", async () => {
113+
clear();
114+
endBootSpans();
115+
const code = await mainWithRunners(sandboxArgs(["exec"], ["say hello"]), {
116+
runTUI: async () => 0,
117+
runExec: async () => 0,
118+
runOnboarding: async () => 0,
119+
});
120+
expect(code).toBe(0);
121+
const pair = bootPair(snapshot());
122+
expect(pair).toHaveLength(2);
123+
expect(pair.every((span) => span.endNs !== undefined)).toBe(true);
124+
clear();
125+
});
126+
127+
test("a TUI that already closed boot spans is not ended twice", async () => {
128+
clear();
129+
endBootSpans();
130+
let closedAt: bigint | undefined;
131+
const code = await mainWithRunners(sandboxArgs([]), {
132+
runTUI: async () => {
133+
endBootSpans();
134+
closedAt = snapshot().find((span) => span.name === "boot")?.endNs;
135+
return 0;
136+
},
137+
runExec: async () => 0,
138+
runOnboarding: async () => 0,
139+
});
140+
expect(code).toBe(0);
141+
const boot = snapshot().find((span) => span.name === "boot");
142+
expect(boot?.endNs).toBe(closedAt);
143+
expect(bootPair(snapshot())).toHaveLength(2);
144+
clear();
145+
});

‎src/perf/boot-spans.ts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
import { end, start } from "./index.js";
22

3-
/**
4-
* Brackets process boot through first paint. Config I/O sits inside the
5-
* spans; schema work that used to run at import stays outside them until a
6-
* value is actually read.
7-
*/
83
let bootId: string | undefined;
94
let firstPaintId: string | undefined;
105

‎src/schema/lazy.ts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
/**
2-
* Memoized arktype constructors. Importing a module must not build schemas
3-
* that are only needed when a value is read.
4-
*/
5-
61
let materializations = 0;
72

83
/** How many memoized schemas have been built in this process. */

‎src/session/isogit-node.ts‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
1-
/**
2-
* The node isogit entry builds its runtime and storage at module scope.
3-
* Keep that off the import path until a session is actually opened.
4-
*/
5-
61
export type IsogitNodeModule = typeof import("@intx/storage-isogit/node");
72

83
let loading: Promise<IsogitNodeModule> | undefined;
94

10-
export function isogitNodeLoadStarted(): boolean {
11-
return loading !== undefined;
12-
}
13-
145
export function loadIsogitNode(): Promise<IsogitNodeModule> {
156
loading ??= import("@intx/storage-isogit/node");
167
return loading;

0 commit comments

Comments
 (0)