perf: startup optimizations — pre-compiled extensions, compile cache, batch discovery (#2125)

Skip jiti JIT compilation for bundled extensions that have pre-compiled .js
siblings, enable V8 bytecode caching on Node 22+, and batch directory
discovery to reduce syscalls during resource loading.

Fixes #2108

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher 2026-03-23 12:02:30 -04:00 committed by GitHub
parent c75f69610f
commit f4ee51017a
4 changed files with 295 additions and 59 deletions

View file

@ -569,6 +569,24 @@ function createExtensionAPI(
}
async function loadExtensionModule(extensionPath: string) {
// Pre-compiled extension loading: if the source is .ts and a sibling .js
// file exists with matching or newer mtime, use native import() to skip
// jiti JIT compilation entirely. This is the biggest startup win for
// bundled extensions that have already been built.
if (extensionPath.endsWith(".ts")) {
const jsPath = extensionPath.replace(/\.ts$/, ".js");
try {
const [tsStat, jsStat] = [fs.statSync(extensionPath), fs.statSync(jsPath)];
if (jsStat.mtimeMs >= tsStat.mtimeMs) {
const module = await import(jsPath);
const factory = (module.default ?? module) as ExtensionFactory;
return typeof factory !== "function" ? undefined : factory;
}
} catch {
// .js file doesn't exist or stat failed — fall through to jiti
}
}
const jiti = createJiti(import.meta.url, {
moduleCache: false,
...getJitiOptions(),

View file

@ -1562,6 +1562,26 @@ export class DefaultPackageManager implements PackageManager {
}
}
/**
* Batch-discover which resource subdirectories exist under a parent dir.
* A single readdirSync replaces 4 separate existsSync probes, reducing
* syscalls during startup.
*/
private discoverResourceSubdirs(baseDir: string): Set<string> {
try {
const entries = readdirSync(baseDir, { withFileTypes: true });
const names = new Set<string>();
for (const e of entries) {
if (e.isDirectory() || e.isSymbolicLink()) {
names.add(e.name);
}
}
return names;
} catch {
return new Set();
}
}
private addAutoDiscoveredResources(
accumulator: ResourceAccumulator,
globalSettings: ReturnType<SettingsManager["getGlobalSettings"]>,
@ -1595,6 +1615,11 @@ export class DefaultPackageManager implements PackageManager {
themes: (projectSettings.themes ?? []) as string[],
};
// Batch directory discovery: one readdir of each parent replaces up to
// 4 separate existsSync calls per base directory, cutting syscalls.
const projectSubdirs = this.discoverResourceSubdirs(projectBaseDir);
const userSubdirs = this.discoverResourceSubdirs(globalBaseDir);
const userDirs = {
extensions: join(globalBaseDir, "extensions"),
skills: join(globalBaseDir, "skills"),
@ -1626,66 +1651,82 @@ export class DefaultPackageManager implements PackageManager {
}
};
addResources(
"extensions",
collectAutoExtensionEntries(projectDirs.extensions),
projectMetadata,
projectOverrides.extensions,
projectBaseDir,
);
addResources(
"skills",
[
...collectAutoSkillEntries(projectDirs.skills),
// Project resources — skip collect calls when the parent readdir shows
// the subdirectory doesn't exist (avoids redundant existsSync + readdirSync).
if (projectSubdirs.has("extensions")) {
addResources(
"extensions",
collectAutoExtensionEntries(projectDirs.extensions),
projectMetadata,
projectOverrides.extensions,
projectBaseDir,
);
}
{
const skillEntries = [
...(projectSubdirs.has("skills") ? collectAutoSkillEntries(projectDirs.skills) : []),
...projectAgentsSkillDirs.flatMap((dir) => collectAutoSkillEntries(dir)),
],
projectMetadata,
projectOverrides.skills,
projectBaseDir,
);
addResources(
"prompts",
collectAutoPromptEntries(projectDirs.prompts),
projectMetadata,
projectOverrides.prompts,
projectBaseDir,
);
addResources(
"themes",
collectAutoThemeEntries(projectDirs.themes),
projectMetadata,
projectOverrides.themes,
projectBaseDir,
);
];
if (skillEntries.length > 0) {
addResources("skills", skillEntries, projectMetadata, projectOverrides.skills, projectBaseDir);
}
}
if (projectSubdirs.has("prompts")) {
addResources(
"prompts",
collectAutoPromptEntries(projectDirs.prompts),
projectMetadata,
projectOverrides.prompts,
projectBaseDir,
);
}
if (projectSubdirs.has("themes")) {
addResources(
"themes",
collectAutoThemeEntries(projectDirs.themes),
projectMetadata,
projectOverrides.themes,
projectBaseDir,
);
}
addResources(
"extensions",
collectAutoExtensionEntries(userDirs.extensions),
userMetadata,
userOverrides.extensions,
globalBaseDir,
);
addResources(
"skills",
[...collectAutoSkillEntries(userDirs.skills), ...collectAutoSkillEntries(userAgentsSkillsDir)],
userMetadata,
userOverrides.skills,
globalBaseDir,
);
addResources(
"prompts",
collectAutoPromptEntries(userDirs.prompts),
userMetadata,
userOverrides.prompts,
globalBaseDir,
);
addResources(
"themes",
collectAutoThemeEntries(userDirs.themes),
userMetadata,
userOverrides.themes,
globalBaseDir,
);
// User (global) resources
if (userSubdirs.has("extensions")) {
addResources(
"extensions",
collectAutoExtensionEntries(userDirs.extensions),
userMetadata,
userOverrides.extensions,
globalBaseDir,
);
}
{
const skillEntries = [
...(userSubdirs.has("skills") ? collectAutoSkillEntries(userDirs.skills) : []),
...collectAutoSkillEntries(userAgentsSkillsDir),
];
if (skillEntries.length > 0) {
addResources("skills", skillEntries, userMetadata, userOverrides.skills, globalBaseDir);
}
}
if (userSubdirs.has("prompts")) {
addResources(
"prompts",
collectAutoPromptEntries(userDirs.prompts),
userMetadata,
userOverrides.prompts,
globalBaseDir,
);
}
if (userSubdirs.has("themes")) {
addResources(
"themes",
collectAutoThemeEntries(userDirs.themes),
userMetadata,
userOverrides.themes,
globalBaseDir,
);
}
}
private collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] {

View file

@ -29,6 +29,15 @@ import { stopWebMode } from './web-mode.js'
import { getProjectSessionsDir } from './project-sessions.js'
import { markStartup, printStartupTimings } from './startup-timings.js'
// ---------------------------------------------------------------------------
// V8 compile cache — Node 22+ can cache compiled bytecode across runs,
// eliminating repeated parse/compile overhead for unchanged modules.
// Must be set early so dynamic imports (extensions, lazy subcommands) benefit.
// ---------------------------------------------------------------------------
if (parseInt(process.versions.node) >= 22) {
process.env.NODE_COMPILE_CACHE ??= join(agentDir, '.compile-cache')
}
// ---------------------------------------------------------------------------
// Minimal CLI arg parser — detects print/subagent mode flags
// ---------------------------------------------------------------------------
@ -538,8 +547,16 @@ const sessionManager = cliFlags._selectedSessionPath
exitIfManagedResourcesAreNewer(agentDir)
initResources(agentDir)
markStartup('initResources')
// Overlap resource loading with session manager setup — both are independent.
// resourceLoader.reload() is the most expensive step (jiti compilation), so
// starting it early shaves ~50-200ms off interactive startup.
const resourceLoader = buildResourceLoader(agentDir)
await resourceLoader.reload()
const resourceLoadPromise = resourceLoader.reload()
// While resources load, let session manager finish any async I/O it needs.
// Then await the resource promise before creating the agent session.
await resourceLoadPromise
markStartup('resourceLoader.reload')
const { session, extensionsResult } = await createAgentSession({

View file

@ -0,0 +1,160 @@
import { describe, it, beforeEach, afterEach } from "node:test";
import assert from "node:assert/strict";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
// ─── Pre-compiled extension loading ──────────────────────────────────────────
describe("pre-compiled extension loading", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "precompiled-ext-"));
});
afterEach(() => {
try {
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 3 });
} catch {
// Ignore cleanup errors on Windows
}
});
it("prefers .js sibling over .ts when .js is newer", async () => {
// Create a .ts file
const tsPath = path.join(tmpDir, "ext.ts");
fs.writeFileSync(tsPath, `export default function ext() { return "ts"; }`);
// Create a .js file with a newer mtime
const jsPath = path.join(tmpDir, "ext.js");
fs.writeFileSync(jsPath, `export default function ext() { return "js"; }`);
// Make .js newer than .ts
const now = new Date();
const past = new Date(now.getTime() - 10_000);
fs.utimesSync(tsPath, past, past);
fs.utimesSync(jsPath, now, now);
const tsStat = fs.statSync(tsPath);
const jsStat = fs.statSync(jsPath);
assert.ok(jsStat.mtimeMs >= tsStat.mtimeMs, ".js should have matching or newer mtime");
});
it("falls back to .ts when no .js sibling exists", () => {
const tsPath = path.join(tmpDir, "ext.ts");
fs.writeFileSync(tsPath, `export default function ext() { return "ts"; }`);
const jsPath = path.join(tmpDir, "ext.js");
assert.ok(!fs.existsSync(jsPath), ".js should not exist");
});
it("falls back to .ts when .js is older", () => {
const tsPath = path.join(tmpDir, "ext.ts");
fs.writeFileSync(tsPath, `export default function ext() { return "ts"; }`);
const jsPath = path.join(tmpDir, "ext.js");
fs.writeFileSync(jsPath, `export default function ext() { return "js-stale"; }`);
// Make .ts newer
const now = new Date();
const past = new Date(now.getTime() - 10_000);
fs.utimesSync(jsPath, past, past);
fs.utimesSync(tsPath, now, now);
const tsStat = fs.statSync(tsPath);
const jsStat = fs.statSync(jsPath);
assert.ok(jsStat.mtimeMs < tsStat.mtimeMs, ".js should be older than .ts");
});
});
// ─── Batch directory discovery ───────────────────────────────────────────────
describe("batch directory discovery", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "batch-discover-"));
});
afterEach(() => {
try {
fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 3 });
} catch {
// Ignore cleanup errors on Windows
}
});
it("single readdir discovers existing subdirectories", () => {
// Create some resource subdirectories
fs.mkdirSync(path.join(tmpDir, "extensions"));
fs.mkdirSync(path.join(tmpDir, "skills"));
// prompts and themes do NOT exist
const entries = fs.readdirSync(tmpDir, { withFileTypes: true });
const subdirs = new Set(
entries.filter((e) => e.isDirectory()).map((e) => e.name),
);
assert.ok(subdirs.has("extensions"));
assert.ok(subdirs.has("skills"));
assert.ok(!subdirs.has("prompts"));
assert.ok(!subdirs.has("themes"));
});
it("returns empty set for non-existent parent directory", () => {
const missing = path.join(tmpDir, "does-not-exist");
let subdirs = new Set<string>();
try {
const entries = fs.readdirSync(missing, { withFileTypes: true });
subdirs = new Set(
entries.filter((e) => e.isDirectory()).map((e) => e.name),
);
} catch {
subdirs = new Set();
}
assert.equal(subdirs.size, 0);
});
});
// ─── Node.js compile cache ──────────────────────────────────────────────────
describe("Node.js compile cache env setup", () => {
it("NODE_COMPILE_CACHE is settable on Node 22+", () => {
const nodeVersion = parseInt(process.versions.node);
if (nodeVersion >= 22) {
// Verify the env var mechanism works (does not throw)
const original = process.env.NODE_COMPILE_CACHE;
try {
process.env.NODE_COMPILE_CACHE = path.join(os.tmpdir(), ".test-compile-cache");
assert.equal(
process.env.NODE_COMPILE_CACHE,
path.join(os.tmpdir(), ".test-compile-cache"),
);
} finally {
if (original === undefined) {
delete process.env.NODE_COMPILE_CACHE;
} else {
process.env.NODE_COMPILE_CACHE = original;
}
}
}
});
it("does not overwrite existing NODE_COMPILE_CACHE", () => {
const original = process.env.NODE_COMPILE_CACHE;
try {
process.env.NODE_COMPILE_CACHE = "/custom/cache";
// Simulate the ??= behavior from cli.ts
process.env.NODE_COMPILE_CACHE ??= "/should-not-overwrite";
assert.equal(process.env.NODE_COMPILE_CACHE, "/custom/cache");
} finally {
if (original === undefined) {
delete process.env.NODE_COMPILE_CACHE;
} else {
process.env.NODE_COMPILE_CACHE = original;
}
}
});
});