From c61c108b522de1a2b46e65b59360c9be66fb038e Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Thu, 15 Oct 2026 02:19:38 -0700 Subject: [PATCH] perf(catalog): record default catalog digests at build time (#4041) Every `doctor`, `mcp doctor` and MCP server that reports or compares its catalog identity rebuilt all advertised JSON Schemas and canonicalized them to compute the default digests, about 200 ms of CPU per process. The default (full input and output schema) catalog is a deterministic function of the compiled contracts and pinned schema converter, so compilation now records it once into dist and the runtime reuses it. Compact and output-schema-off identities, and source runs, still compute their digests on first use. Refs #3813 Co-authored-by: Claude Opus 5.5 --- package.json | 4 ++-- scripts/record-catalog-digests.mjs | 19 +++++++++++++++++++ src/catalogIdentity.ts | 15 +++++++++++++-- src/recordedCatalogDigests.ts | 8 ++++++++ turbo.json | 1 + 5 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 scripts/record-catalog-digests.mjs create mode 100644 src/recordedCatalogDigests.ts diff --git a/package.json b/package.json index 330d0d91b7..d637db7fea 100644 --- a/package.json +++ b/package.json @@ -291,8 +291,8 @@ "verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs", "verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs", "mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check", - "compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/bundle-runtime.mjs", - "compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json && node scripts/bundle-runtime.mjs", + "compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs", + "compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs", "artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs", "artifacts:skills": "node scripts/generate-skill-metadata.mjs", "artifacts:product-catalog": "node scripts/generate-product-catalog.mjs", diff --git a/scripts/record-catalog-digests.mjs b/scripts/record-catalog-digests.mjs new file mode 100644 index 0000000000..82807c121f --- /dev/null +++ b/scripts/record-catalog-digests.mjs @@ -0,0 +1,19 @@ +import { writeFile } from "node:fs/promises"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +// Record the default catalog digests into the compiled runtime. Run after +// `tsc` and before bundling, so the digests come from the same compiled +// contracts and pinned schema converter that the runtime serves. +const compiledRoot = resolve( + dirname(fileURLToPath(import.meta.url)), + "../dist", +); +const { createCatalogDigests } = await import( + pathToFileURL(join(compiledRoot, "catalogIdentity.js")).href +); +await writeFile( + join(compiledRoot, "recordedCatalogDigests.js"), + `/** Recorded from the compiled default catalog by scripts/record-catalog-digests.mjs; do not edit. */\nexport const RECORDED_CATALOG_DIGESTS = Object.freeze(${JSON.stringify(createCatalogDigests())});\n`, + "utf8", +); diff --git a/src/catalogIdentity.ts b/src/catalogIdentity.ts index 676251dd02..80425d26ee 100644 --- a/src/catalogIdentity.ts +++ b/src/catalogIdentity.ts @@ -17,6 +17,7 @@ import { type CompactInputSchemaPresentation, } from "./contracts/compactInputSchema.js"; import type { McpOutputSchemaProfile } from "./config/mcpOutputSchemaProfile.js"; +import { RECORDED_CATALOG_DIGESTS } from "./recordedCatalogDigests.js"; const sortByName = ( contracts: readonly ToolContract[], @@ -102,6 +103,8 @@ export const createCatalogDigests = ( } as const; }; +export type CatalogDigests = ReturnType; + export const createCatalogIdentity = ( compactPresentation: CompactInputSchemaPresentation | undefined = undefined, outputSchemaProfile: McpOutputSchemaProfile = "full", @@ -110,7 +113,12 @@ export const createCatalogIdentity = ( compactPresentation === undefined ? undefined : { budgetBytes: compactPresentation.budgetBytes }; - let catalogDigests: ReturnType | undefined; + // Building and canonicalizing every advertised schema dominates doctor and + // server startup, so the compiled runtime records the default digests once. + let catalogDigests: CatalogDigests | undefined = + selectedPresentation === undefined && outputSchemaProfile === "full" + ? RECORDED_CATALOG_DIGESTS + : undefined; return { counts: { cli_commands: CLI_COMMAND_NAMES.length, @@ -128,5 +136,8 @@ export const createCatalogIdentity = ( } as const; }; -/** Stable, schema-sensitive identity; schema digests are computed on first use. */ +/** + * Stable, schema-sensitive identity; schema digests are recorded at build time + * or computed on first use. + */ export const CATALOG_IDENTITY = createCatalogIdentity(); diff --git a/src/recordedCatalogDigests.ts b/src/recordedCatalogDigests.ts new file mode 100644 index 0000000000..fc37c24d50 --- /dev/null +++ b/src/recordedCatalogDigests.ts @@ -0,0 +1,8 @@ +import type { CatalogDigests } from "./catalogIdentity.js"; + +/** + * Digests of the default catalog (full input and output schemas), recorded + * from the compiled contracts by `scripts/record-catalog-digests.mjs` after + * compilation. Source runs leave this undefined and compute them on first use. + */ +export const RECORDED_CATALOG_DIGESTS: CatalogDigests | undefined = undefined; diff --git a/turbo.json b/turbo.json index 8ca759ba1a..37783a4afd 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,7 @@ "scripts/check-dependency-install.mjs", "scripts/clean-build-output.mjs", "scripts/bundle-runtime.mjs", + "scripts/record-catalog-digests.mjs", "scripts/generate-package-metadata.mjs", "src/**/*.{cts,mts,ts,tsx}", "src/**/package.json",