From 2b64f4bf4b1b8a04409e388bd3bcff56df8f59e6 Mon Sep 17 00:00:00 2001 From: Alex Knight Date: Mon, 20 Apr 2026 22:13:16 +1000 Subject: [PATCH] perf: memoize buildPluginLoaderAliasMap to enable jiti sentinel reuse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildPluginLoaderAliasMap() creates a new alias object via spread on every call. jiti's normalizeAliases() uses a reference-identity sentinel (`if (e[pt]) return e`) to skip its O(N²) normalization work — but fresh object refs defeat the sentinel, causing the full cycle to repeat on every call. This change caches alias maps by their inputs (modulePath, argv1, moduleUrl, pluginSdkResolution) so identical parameters return the same object reference. Subsequent jiti calls hit the sentinel fast-path instead of re-running normalization. Includes 5 new tests covering: - reference identity for identical inputs - cache isolation (different modulePath, pluginSdkResolution, argv1 each produce distinct objects) - content equivalence between cached and freshly-computed results Refs #68983, #63948 --- src/plugins/sdk-alias.test.ts | 92 +++++++++++++++++++++++++++++++++++ src/plugins/sdk-alias.ts | 17 ++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index 77fbaaeccce..34e30633b82 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -1056,3 +1056,95 @@ export const syntheticRuntimeMarker = { expect(resolved).toBe(expected === "dist" ? fixture.distFile : fixture.srcFile); }); }); + +describe("buildPluginLoaderAliasMap memoization", () => { + it("returns the same object reference for identical inputs (jiti sentinel safety)", () => { + const fixture = createPluginSdkAliasFixture(); + const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"); + fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8"); + const sourcePluginEntry = writePluginEntry( + fixture.root, + bundledPluginFile("memo-demo", "src/index.ts"), + ); + + const first = buildPluginLoaderAliasMap(sourcePluginEntry); + const second = buildPluginLoaderAliasMap(sourcePluginEntry); + + // Reference identity is critical: jiti's normalizeAliases uses a + // reference-identity sentinel to skip O(N²) re-processing. + expect(second).toBe(first); + }); + + it("returns different references for different modulePath inputs", () => { + const fixtureA = createPluginSdkAliasFixture(); + const fixtureB = createPluginSdkAliasFixture(); + fs.writeFileSync( + path.join(fixtureA.root, "src", "plugin-sdk", "root-alias.cjs"), + "module.exports = {};\n", + "utf-8", + ); + fs.writeFileSync( + path.join(fixtureB.root, "src", "plugin-sdk", "root-alias.cjs"), + "module.exports = {};\n", + "utf-8", + ); + const entryA = writePluginEntry(fixtureA.root, bundledPluginFile("a", "src/index.ts")); + const entryB = writePluginEntry(fixtureB.root, bundledPluginFile("b", "src/index.ts")); + + const aliasA = buildPluginLoaderAliasMap(entryA); + const aliasB = buildPluginLoaderAliasMap(entryB); + + expect(aliasA).not.toBe(aliasB); + expect(aliasA["openclaw/plugin-sdk"]).not.toBe(aliasB["openclaw/plugin-sdk"]); + }); + + it("returns different references when pluginSdkResolution differs", () => { + const fixture = createPluginSdkAliasFixture(); + fs.writeFileSync( + path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"), + "module.exports = {};\n", + "utf-8", + ); + const entry = writePluginEntry(fixture.root, bundledPluginFile("res", "src/index.ts")); + + const auto = buildPluginLoaderAliasMap(entry, undefined, undefined, "auto"); + const dist = buildPluginLoaderAliasMap(entry, undefined, undefined, "dist"); + + expect(auto).not.toBe(dist); + }); + + it("returns different references when argv1 differs", () => { + const fixture = createPluginSdkAliasFixture(); + fs.writeFileSync( + path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"), + "module.exports = {};\n", + "utf-8", + ); + const entry = writePluginEntry(fixture.root, bundledPluginFile("argv", "src/index.ts")); + + const a = buildPluginLoaderAliasMap(entry, "/path/to/cli-a.mjs"); + const b = buildPluginLoaderAliasMap(entry, "/path/to/cli-b.mjs"); + + expect(a).not.toBe(b); + }); + + it("memoized result has identical content to a freshly computed map", () => { + const fixture = createPluginSdkAliasFixture(); + fs.writeFileSync( + path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs"), + "module.exports = {};\n", + "utf-8", + ); + const entry = writePluginEntry(fixture.root, bundledPluginFile("eq", "src/index.ts")); + + const first = buildPluginLoaderAliasMap(entry); + const second = buildPluginLoaderAliasMap(entry); + + // Same reference (cache hit) + expect(second).toBe(first); + // Same content + expect(second).toEqual(first); + // Same key set + expect(Object.keys(second).toSorted()).toEqual(Object.keys(first).toSorted()); + }); +}); diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 0bf1b02ef37..18bd47d7155 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -430,12 +430,25 @@ export function resolveExtensionApiAlias(params: LoaderModuleResolveParams = {}) return null; } +// Memoize alias maps by inputs so the same object reference is returned for +// identical parameters. jiti's `normalizeAliases` uses a reference-identity +// sentinel (`if (e[pt]) return e`) to skip its O(N²) normalization — returning +// the same object lets the sentinel fire on the 2nd+ call instead of repeating +// the full cycle every time. See #68983. +const aliasMapCache = new Map>(); + export function buildPluginLoaderAliasMap( modulePath: string, argv1: string | undefined = STARTUP_ARGV1, moduleUrl?: string, pluginSdkResolution: PluginSdkResolutionPreference = "auto", ): Record { + const cacheKey = `${modulePath}\0${argv1 ?? ""}\0${moduleUrl ?? ""}\0${pluginSdkResolution}`; + const cached = aliasMapCache.get(cacheKey); + if (cached) { + return cached; + } + const pluginSdkAlias = resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs", @@ -445,7 +458,7 @@ export function buildPluginLoaderAliasMap( pluginSdkResolution, }); const extensionApiAlias = resolveExtensionApiAlias({ modulePath, pluginSdkResolution }); - return { + const result: Record = { ...(extensionApiAlias ? { "openclaw/extension-api": normalizeJitiAliasTargetPath(extensionApiAlias) } : {}), @@ -463,6 +476,8 @@ export function buildPluginLoaderAliasMap( ).map(([key, value]) => [key, normalizeJitiAliasTargetPath(value)]), ), }; + aliasMapCache.set(cacheKey, result); + return result; } export function resolvePluginRuntimeModulePath(