Skip to content

Commit 24d6fc9

Browse files
committed
perf(boot): load isogit on session open and span boot to mount
1 parent 422fee1 commit 24d6fc9

8 files changed

Lines changed: 135 additions & 4 deletions

File tree

‎src/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +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";
1617
import { createTelemetry, telemetryDisabledByEnv } from "./telemetry/index.js";
1718
import { getTelemetry, setTelemetry } from "./telemetry/singleton.js";
1819
import {
@@ -36,6 +37,7 @@ export async function mainWithRunners(
3637
argv: readonly string[],
3738
runners: Runners,
3839
): Promise<number> {
40+
startBootSpans();
3941
// Must run first: @intx/log installs a console sink on import, and
4042
// loadConfig can log (e.g. healed settings). This replaces that default so
4143
// nothing — including vendored loggers — reaches the terminal the TUI owns.

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { EventEmitter } from "node:events";
2+
import { expect, test } from "bun:test";
3+
4+
import { main } from "../index.js";
5+
import { clear, snapshot } from "./index.js";
6+
import { endBootSpans, startBootSpans } from "./boot-spans.js";
7+
import { createHarness } from "../tui/harness.js";
8+
import { mountRunnerHost } from "../tui/runner/host.js";
9+
10+
test("boot starts boot and first-paint spans before config exits", async () => {
11+
clear();
12+
endBootSpans();
13+
await expect(main(["--help"])).rejects.toThrow();
14+
const open = snapshot().filter((span) => span.endNs === undefined);
15+
expect(open.some((span) => span.name === "boot")).toBe(true);
16+
expect(open.some((span) => span.name === "first-paint")).toBe(true);
17+
endBootSpans();
18+
clear();
19+
});
20+
21+
test("mount ends the boot and first-paint spans", async () => {
22+
clear();
23+
endBootSpans();
24+
startBootSpans();
25+
const harness = await createHarness({ width: 80, height: 24 });
26+
const host = await mountRunnerHost({
27+
title: "test",
28+
eventEmitter: new EventEmitter(),
29+
send: () => undefined,
30+
interrupt: () => undefined,
31+
deliver: () => undefined,
32+
providers: {},
33+
onModelSelect: () => undefined,
34+
commands: [],
35+
onCommand: () => undefined,
36+
chrome: () => ({ agents: [] }),
37+
subscribeChrome: () => () => undefined,
38+
subAgentSessions: () => [],
39+
createRenderer: async () => harness.renderer,
40+
});
41+
try {
42+
const done = snapshot().filter((span) => span.endNs !== undefined);
43+
expect(done.some((span) => span.name === "boot")).toBe(true);
44+
expect(done.some((span) => span.name === "first-paint")).toBe(true);
45+
} finally {
46+
host.dispose();
47+
harness.destroy();
48+
clear();
49+
}
50+
});

‎src/perf/boot-spans.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { end, start } from "./index.js";
2+
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+
*/
8+
let bootId: string | undefined;
9+
let firstPaintId: string | undefined;
10+
11+
export function startBootSpans(): void {
12+
if (bootId !== undefined) return;
13+
bootId = start("boot");
14+
firstPaintId = start("first-paint");
15+
}
16+
17+
export function endBootSpans(): void {
18+
if (bootId !== undefined) {
19+
end(bootId);
20+
bootId = undefined;
21+
}
22+
if (firstPaintId !== undefined) {
23+
end(firstPaintId);
24+
firstPaintId = undefined;
25+
}
26+
}

‎src/perf/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export const SPAN_NAMES = [
5252
"adapter.request_build",
5353
"adapter.first_byte",
5454
"adapter.transport",
55+
"boot",
56+
"first-paint",
5557
] as const;
5658

5759
export type SpanName = (typeof SPAN_NAMES)[number];

‎src/session/isogit-boot.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { mkdtemp, rm } from "node:fs/promises";
2+
import { tmpdir } from "node:os";
3+
import { join } from "node:path";
4+
import { expect, mock, test } from "bun:test";
5+
6+
let nodeEvaluated = false;
7+
8+
mock.module("@intx/storage-isogit/node", () => {
9+
nodeEvaluated = true;
10+
return {
11+
createIsogitStore: async () => {
12+
throw new Error("session open reached isogit");
13+
},
14+
};
15+
});
16+
17+
test("importing the session store does not construct isogit storage", async () => {
18+
await import("./optimized-context-store.js");
19+
expect(nodeEvaluated).toBe(false);
20+
});
21+
22+
test("opening a session constructs isogit storage", async () => {
23+
const { createSessionStores } = await import("./optimized-context-store.js");
24+
const dir = await mkdtemp(join(tmpdir(), "isogit-boot-"));
25+
try {
26+
await expect(createSessionStores(dir)).rejects.toThrow(
27+
"session open reached isogit",
28+
);
29+
expect(nodeEvaluated).toBe(true);
30+
} finally {
31+
await rm(dir, { recursive: true, force: true });
32+
}
33+
});

‎src/session/isogit-node.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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+
6+
export type IsogitNodeModule = typeof import("@intx/storage-isogit/node");
7+
8+
let loading: Promise<IsogitNodeModule> | undefined;
9+
10+
export function isogitNodeLoadStarted(): boolean {
11+
return loading !== undefined;
12+
}
13+
14+
export function loadIsogitNode(): Promise<IsogitNodeModule> {
15+
loading ??= import("@intx/storage-isogit/node");
16+
return loading;
17+
}

‎src/session/optimized-context-store.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import fs from "node:fs";
22
import path from "node:path";
33
import { type } from "arktype";
44
import git from "isomorphic-git";
5-
import {
6-
createIsogitStore,
7-
type CommitSigner,
8-
} from "@intx/storage-isogit/node";
5+
import type { CommitSigner } from "@intx/storage-isogit/node";
6+
import { loadIsogitNode } from "./isogit-node.js";
97
import {
108
ContentBlock,
119
ConnectorThreadState,
@@ -1478,6 +1476,7 @@ export async function createSessionStores(
14781476
opts?: SessionStoreOptions,
14791477
): Promise<SessionStores> {
14801478
const signer = opts?.signer ?? (await loadOrCreateCommitSigner(dir));
1479+
const { createIsogitStore } = await loadIsogitNode();
14811480
const base = await createIsogitStore(dir, signer);
14821481
const pendingBlobFilepaths = new Set<string>();
14831482
const pendingSegmentPaths = new Set<string>();

‎src/tui/runner/host.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import type { QueueKind } from "../delivery-queue.js";
6161
import type { ShellOutputFeed } from "../../session/shell-output-feed.js";
6262
import { openModelPickerOverlay } from "../overlays.js";
6363
import type { ReconnectScope } from "../connect-scope.js";
64+
import { endBootSpans } from "../../perf/boot-spans.js";
6465
import {
6566
reconnectRecoveryItemId,
6667
reconnectRecoveryItemLabel,
@@ -387,6 +388,7 @@ export async function mountRunnerHost(
387388
? { telemetryNotice: deps.telemetryNotice }
388389
: {}),
389390
});
391+
endBootSpans();
390392

391393
const pushChrome = (): void => {
392394
host.setChrome(chromeFromSession(deps.chrome()));

0 commit comments

Comments
 (0)