From 7b230efeac324ed70d24fafa3d1f2beeb69bfce6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 10 Sep 2026 16:20:24 -0700 Subject: [PATCH] fix(sqlite): speed up read-only snapshot startup (#144448) * perf(sqlite): bundle read-only snapshot child independently * test(build): retain standalone worker declaration coverage --- .../runtime-process-core-build-entries.mts | 13 ++++ scripts/lib/vitest-worker-compiler.mts | 11 +++- test/scripts/tsdown-config.test.ts | 25 +++++++- test/scripts/tsdown-runtime-config.test.ts | 60 ++++++++++++++----- tsdown.config.ts | 16 ++++- 5 files changed, 108 insertions(+), 17 deletions(-) diff --git a/scripts/lib/runtime-process-core-build-entries.mts b/scripts/lib/runtime-process-core-build-entries.mts index 6d7d501c9376..59645b343e68 100644 --- a/scripts/lib/runtime-process-core-build-entries.mts +++ b/scripts/lib/runtime-process-core-build-entries.mts @@ -19,3 +19,16 @@ export function createRuntimeProcessBuildEntries( export const runtimeProcessCoreBuildEntries = createRuntimeProcessBuildEntries( Object.values(runtimeProcessEntrypoints), ); + +// Short-lived snapshot children own a separate bundle; parents retain shared runtime identity. +export const standaloneRuntimeProcessBuildEntries = createRuntimeProcessBuildEntries([ + runtimeProcessEntrypoints.sqliteReadOnly, +]); + +export function sharedRuntimeProcessBuildEntries(entries: Record) { + return Object.fromEntries( + Object.entries(entries).filter( + ([name]) => !Object.hasOwn(standaloneRuntimeProcessBuildEntries, name), + ), + ); +} diff --git a/scripts/lib/vitest-worker-compiler.mts b/scripts/lib/vitest-worker-compiler.mts index 554ed4a1317a..609247f7f0eb 100644 --- a/scripts/lib/vitest-worker-compiler.mts +++ b/scripts/lib/vitest-worker-compiler.mts @@ -4,6 +4,10 @@ import { createRequire } from "node:module"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { createManagedHandoffBuildConfig } from "./managed-handoff-build-config.mts"; +import { + sharedRuntimeProcessBuildEntries, + standaloneRuntimeProcessBuildEntries, +} from "./runtime-process-core-build-entries.mts"; import { createStateSchemaInlinePlugin } from "./state-schema-inline-plugin.mts"; import { hashVitestWorkerArtifact, @@ -64,7 +68,7 @@ async function compileVitestWorkerArtifacts(directory: string): Promise { const config: NonNullable[0]> = { config: false, cwd: root, - entry, + entry: sharedRuntimeProcessBuildEntries(entry), outDir, format: "esm", platform: "node", @@ -141,6 +145,11 @@ async function compileVitestWorkerArtifacts(directory: string): Promise { ], }; await build(config); + await build({ + ...config, + entry: standaloneRuntimeProcessBuildEntries, + outputOptions: { codeSplitting: false }, + }); await build({ ...createManagedHandoffBuildConfig(), config: false, diff --git a/test/scripts/tsdown-config.test.ts b/test/scripts/tsdown-config.test.ts index 94fa0a6148ba..4f09e2547c66 100644 --- a/test/scripts/tsdown-config.test.ts +++ b/test/scripts/tsdown-config.test.ts @@ -608,6 +608,16 @@ describe("tsdown config", () => { const unifiedRuntimeConfig = configs.find( (entry) => entry.name === TSDOWN_UNIFIED_CONFIG_GROUP, ); + const standaloneRuntimeConfig = configs.find( + (entry) => + entry.name === TSDOWN_UNIFIED_CONFIG_GROUP && + entry.dts === false && + hasWorkerEntry( + entry, + "infra/sqlite-readonly-location.worker", + path.resolve("src/infra/sqlite-readonly-location.worker.ts"), + ), + ); const unifiedDeclarationConfigs = TSDOWN_UNIFIED_DTS_CONFIG_GROUPS.map((name) => configs.find((entry) => entry.name === name), ); @@ -615,15 +625,28 @@ describe("tsdown config", () => { expect(packageConfigs).not.toHaveLength(0); expect(packageConfigs.map((entry) => entry.dts)).toEqual(packageConfigs.map(() => true)); expect(unifiedRuntimeConfig?.dts).toBe(false); + expect(standaloneRuntimeConfig?.dts).toBe(false); expect(unifiedDeclarationConfigs.every(Boolean)).toBe(true); const runtimeEntryNames = Object.keys(unifiedRuntimeConfig?.entry ?? {}); expect(runtimeEntryNames).toContain("native-hook-relay/entry"); const declarationEntryNames = runtimeEntryNames.filter( (name) => name !== "native-hook-relay/entry", ); + const standaloneEntries = Object.entries(standaloneRuntimeConfig?.entry ?? {}); + const standaloneNames = new Set(standaloneEntries.map(([name]) => name)); + const declarationInputs = Object.fromEntries([ + ...Object.entries(unifiedRuntimeConfig?.entry ?? {}).filter( + ([name]) => name !== "native-hook-relay/entry", + ), + ...standaloneEntries, + ]); for (const declarationConfig of unifiedDeclarationConfigs) { expect(declarationConfig?.dts).toMatchObject({ emitDtsOnly: true }); - expect(Object.keys(declarationConfig?.entry ?? {})).toEqual(declarationEntryNames); + // Splitting executable graphs keeps all declaration aliases and the shared input order. + expect(declarationConfig?.entry).toEqual(declarationInputs); + expect( + Object.keys(declarationConfig?.entry ?? {}).filter((name) => !standaloneNames.has(name)), + ).toEqual(declarationEntryNames); } }); diff --git a/test/scripts/tsdown-runtime-config.test.ts b/test/scripts/tsdown-runtime-config.test.ts index ea25ec5cbca3..5d0f54371e7e 100644 --- a/test/scripts/tsdown-runtime-config.test.ts +++ b/test/scripts/tsdown-runtime-config.test.ts @@ -1,6 +1,7 @@ // Covers bundling rules encoded in the root tsdown config. import { readFileSync } from "node:fs"; import path from "node:path"; +import { expectDefined } from "@openclaw/normalization-core/expect"; import { bundledPluginRoot } from "openclaw/plugin-sdk/test-fixtures"; import { describe, expect, it } from "vitest"; import { OPENCLAW_AGENT_SCHEMA_SQL } from "../../src/state/openclaw-agent-schema.js"; @@ -18,6 +19,10 @@ type TsdownConfigEntry = { entry?: Record | string[]; inputOptions?: TsdownInputOptions; minify?: unknown; + dts?: boolean | { emitDtsOnly?: boolean }; + define?: Record; + outputOptions?: { codeSplitting?: boolean }; + outExtensions?: () => { js: string }; outDir?: string; plugins?: Array<{ name?: string }>; }; @@ -68,6 +73,16 @@ function entrySources(config: TsdownConfigEntry): Record { return config.entry; } +function requireSqliteReadOnlyChildGraph(): TsdownConfigEntry { + const graphs = asConfigArray(tsdownConfig).filter( + (config) => + !(typeof config.dts === "object" && config.dts.emitDtsOnly) && + entryKeys(config).includes("infra/sqlite-readonly-location.worker"), + ); + expect(graphs).toHaveLength(1); + return expectDefined(graphs[0], "read-only snapshot child graph"); +} + function bundledEntry(pluginId: string): string { return `${bundledPluginRoot(pluginId)}/index`; } @@ -191,7 +206,10 @@ describe("tsdown config", () => { expect(entrySources(unifiedGraph)["native-hook-relay/entry"]).toBe( "src/cli/native-hook-relay-entry.ts", ); - expect(inlinePlugins).toHaveLength(3); + expect(requireSqliteReadOnlyChildGraph().plugins).toContainEqual( + expect.objectContaining({ name: STATE_SCHEMA_INLINE_PLUGIN_NAME }), + ); + expect(inlinePlugins).toHaveLength(4); }); it("keeps core, plugin runtime, plugin-sdk, bundled root plugins, and bundled hooks in one dist graph", () => { @@ -207,7 +225,7 @@ describe("tsdown config", () => { "agents/compaction-planning.worker", "agents/model-provider-auth.worker", "config/sessions/session-accessor.sqlite-archive.worker", - "infra/sqlite-readonly-location.worker", + "plugin-sdk/sqlite-runtime", "state/openclaw-database-verify.worker", "plugins/memory-state", "subagent-registry.runtime", @@ -233,6 +251,18 @@ describe("tsdown config", () => { } }); + it("emits the read-only snapshot child once without sealing its package loaders", () => { + const child = requireSqliteReadOnlyChildGraph(); + expect(entrySources(child)).toEqual({ + "infra/sqlite-readonly-location.worker": path.resolve( + "src/infra/sqlite-readonly-location.worker.ts", + ), + }); + expect(child.outputOptions).toEqual({ codeSplitting: false }); + expect(child.outExtensions?.().js).toBe(".js"); + expect(child.define?.SEALED_RUNTIME_BUILD).toBeUndefined(); + }); + it("builds the Docker healthcheck as a stable dist entry", () => { const distGraph = requireUnifiedDistGraph(); @@ -340,20 +370,22 @@ describe("tsdown config", () => { }); it("bundles SDK-owned helpers while retaining fs-safe package ownership", () => { - const unifiedGraph = requireUnifiedDistGraph(); - const alwaysBundle = unifiedGraph.deps?.alwaysBundle; + for (const graph of [requireUnifiedDistGraph(), requireSqliteReadOnlyChildGraph()]) { + const alwaysBundle = graph.deps?.alwaysBundle; + const external = graph.inputOptions?.({})?.external; + if (typeof alwaysBundle !== "function" || typeof external !== "function") { + throw new Error("expected runtime graph dependency predicates"); + } - if (typeof alwaysBundle !== "function") { - throw new Error("expected unified graph alwaysBundle predicate"); + expect(alwaysBundle("@openclaw/fs-safe")).toBe(false); + expect(alwaysBundle("@openclaw/fs-safe/path")).toBe(false); + expect(external("@openclaw/fs-safe/path", undefined, false)).toBe(true); + expect(alwaysBundle("openclaw/plugin-sdk/ssrf-runtime-internal")).toBe(true); + expect(alwaysBundle("openclaw/plugin-sdk/ssrf-runtime")).toBe(false); + expect(alwaysBundle("zod")).toBe(true); + expect(alwaysBundle("zod/v4/core")).toBe(true); + expect(alwaysBundle("not-a-runtime-dependency")).toBe(false); } - - expect(alwaysBundle("@openclaw/fs-safe")).toBe(false); - expect(alwaysBundle("@openclaw/fs-safe/path")).toBe(false); - expect(alwaysBundle("openclaw/plugin-sdk/ssrf-runtime-internal")).toBe(true); - expect(alwaysBundle("openclaw/plugin-sdk/ssrf-runtime")).toBe(false); - expect(alwaysBundle("zod")).toBe(true); - expect(alwaysBundle("zod/v4/core")).toBe(true); - expect(alwaysBundle("not-a-runtime-dependency")).toBe(false); }); it("suppresses unresolved imports from extension source", () => { diff --git a/tsdown.config.ts b/tsdown.config.ts index 638a4db02ae9..326ff1d78337 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -19,6 +19,10 @@ import { } from "./scripts/lib/plugin-sdk-entries.mts"; import { createRuntimeDependencyOwnershipBuildPlugin } from "./scripts/lib/runtime-dependency-ownership-build-plugin.mts"; import { runtimeProcessBuildEntries } from "./scripts/lib/runtime-process-build-entries.mts"; +import { + sharedRuntimeProcessBuildEntries, + standaloneRuntimeProcessBuildEntries, +} from "./scripts/lib/runtime-process-core-build-entries.mts"; import { createStateSchemaInlinePlugin, STATE_SCHEMA_INLINE_PLUGIN_NAME, @@ -813,7 +817,7 @@ const configs: UserConfig[] = [ // Build core entrypoints, plugin-sdk subpaths, bundled plugin entrypoints, // and bundled hooks in one graph so runtime singletons are emitted once. entry: { - ...unifiedDistEntries, + ...sharedRuntimeProcessBuildEntries(unifiedDistEntries), "native-hook-relay/entry": "src/cli/native-hook-relay-entry.ts", }, deps: unifiedDeps, @@ -828,6 +832,16 @@ const configs: UserConfig[] = [ }, false, ), + nodeBuildConfig( + { + name: TSDOWN_UNIFIED_CONFIG_GROUP, + entry: standaloneRuntimeProcessBuildEntries, + deps: unifiedDeps, + outputOptions: { codeSplitting: false }, + plugins: [createStateSchemaInlinePlugin()], + }, + false, + ), workerDeployBuildConfig(), { ...createManagedHandoffBuildConfig(), name: TSDOWN_UNIFIED_CONFIG_GROUP, env }, nodeBuildConfig(