perf: memoize buildPluginLoaderAliasMap to enable jiti sentinel reuse

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
This commit is contained in:
Alex Knight
2026-04-20 22:13:16 +10:00
committed by Peter Steinberger
parent f27c164e7f
commit 2b64f4bf4b
2 changed files with 108 additions and 1 deletions

View File

@@ -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());
});
});

View File

@@ -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<string, Record<string, string>>();
export function buildPluginLoaderAliasMap(
modulePath: string,
argv1: string | undefined = STARTUP_ARGV1,
moduleUrl?: string,
pluginSdkResolution: PluginSdkResolutionPreference = "auto",
): Record<string, string> {
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<string, string> = {
...(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(