mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 09:11:34 +00:00
improve(gateway): reduce plugin startup memory (#115020)
* perf(gateway): reduce plugin startup memory * test(plugins): cover effective alias order caching
This commit is contained in:
committed by
GitHub
parent
0216fbd4e4
commit
6b255f1f74
@@ -99,6 +99,7 @@ type CliOptions = {
|
||||
cases: GatewayBenchCase[];
|
||||
cpuProfDir?: string;
|
||||
entry: string;
|
||||
heapProfDir?: string;
|
||||
json: boolean;
|
||||
output?: string;
|
||||
runs: number;
|
||||
@@ -115,6 +116,7 @@ const VALUE_FLAGS = new Set([
|
||||
"--case",
|
||||
"--cpu-prof-dir",
|
||||
"--entry",
|
||||
"--heap-prof-dir",
|
||||
"--output",
|
||||
"--runs",
|
||||
"--timeout-ms",
|
||||
@@ -337,6 +339,7 @@ function parseOptions(argv: string[] = process.argv.slice(2)): CliOptions {
|
||||
cases: resolveCases(parseRepeatableFlag(argv, "--case")),
|
||||
cpuProfDir: parseFlagValue(argv, "--cpu-prof-dir"),
|
||||
entry: resolveEntry(parseFlagValue(argv, "--entry")),
|
||||
heapProfDir: parseFlagValue(argv, "--heap-prof-dir"),
|
||||
json: hasFlag(argv, "--json"),
|
||||
output: resolveOutputPath(parseFlagValue(argv, "--output")),
|
||||
runs: parsePositiveInt(parseFlagValue(argv, "--runs"), DEFAULT_RUNS, "--runs"),
|
||||
@@ -363,6 +366,7 @@ Options:
|
||||
--warmup <n> Warmup runs per case (default: ${DEFAULT_WARMUP})
|
||||
--timeout-ms <ms> Per-run timeout (default: ${DEFAULT_TIMEOUT_MS})
|
||||
--cpu-prof-dir <dir> Write one V8 CPU profile per run
|
||||
--heap-prof-dir <dir> Write one V8 heap profile per run
|
||||
--output <path> Write machine-readable JSON to a file
|
||||
--json Emit machine-readable JSON
|
||||
--help, -h Show this text
|
||||
@@ -806,6 +810,7 @@ async function runGatewaySample(options: {
|
||||
benchCase: GatewayBenchCase;
|
||||
cpuProfDir?: string;
|
||||
entry: string;
|
||||
heapProfDir?: string;
|
||||
sampleIndex: number;
|
||||
timeoutMs: number;
|
||||
}): Promise<GatewaySample> {
|
||||
@@ -835,6 +840,7 @@ async function runGatewaySample(options: {
|
||||
`openclaw-gateway-${options.benchCase.id}-${options.sampleIndex}-${Date.now()}.cpuprofile`,
|
||||
]
|
||||
: []),
|
||||
...(options.heapProfDir ? ["--heap-prof", "--heap-prof-dir", options.heapProfDir] : []),
|
||||
options.entry,
|
||||
"gateway",
|
||||
"run",
|
||||
@@ -947,6 +953,7 @@ async function runCase(options: {
|
||||
benchCase: GatewayBenchCase;
|
||||
cpuProfDir?: string;
|
||||
entry: string;
|
||||
heapProfDir?: string;
|
||||
runs: number;
|
||||
timeoutMs: number;
|
||||
warmup: number;
|
||||
@@ -958,6 +965,7 @@ async function runCase(options: {
|
||||
benchCase: options.benchCase,
|
||||
cpuProfDir: options.cpuProfDir,
|
||||
entry: options.entry,
|
||||
heapProfDir: options.heapProfDir,
|
||||
sampleIndex: index + 1,
|
||||
timeoutMs: options.timeoutMs,
|
||||
});
|
||||
@@ -1013,6 +1021,9 @@ async function main() {
|
||||
if (options.cpuProfDir) {
|
||||
mkdirSync(options.cpuProfDir, { recursive: true });
|
||||
}
|
||||
if (options.heapProfDir) {
|
||||
mkdirSync(options.heapProfDir, { recursive: true });
|
||||
}
|
||||
const results: CaseResult[] = [];
|
||||
for (const benchCase of options.cases) {
|
||||
results.push(
|
||||
@@ -1020,6 +1031,7 @@ async function main() {
|
||||
benchCase,
|
||||
cpuProfDir: options.cpuProfDir,
|
||||
entry: options.entry,
|
||||
heapProfDir: options.heapProfDir,
|
||||
runs: options.runs,
|
||||
timeoutMs: options.timeoutMs,
|
||||
warmup: options.warmup,
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { resetGatewayWorkAdmission } from "../process/gateway-work-admission.js";
|
||||
import { scheduleGatewayHandlerPrewarm } from "./server-startup-handler-prewarm.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
resetGatewayWorkAdmission();
|
||||
});
|
||||
|
||||
describe("scheduleGatewayHandlerPrewarm", () => {
|
||||
it("loads every scheduled family in order only after the post-ready timer yields", async () => {
|
||||
vi.useFakeTimers();
|
||||
const familyNames = ["sessions", "chat", "tasks", "cron"];
|
||||
const loaded: string[] = [];
|
||||
const families = familyNames.map((name) => ({
|
||||
name,
|
||||
load: vi.fn(async () => {
|
||||
loaded.push(name);
|
||||
}),
|
||||
}));
|
||||
|
||||
const sidecar = scheduleGatewayHandlerPrewarm({
|
||||
families,
|
||||
log: { warn: vi.fn() },
|
||||
});
|
||||
|
||||
expect(loaded).toEqual([]);
|
||||
await vi.runAllTimersAsync();
|
||||
expect(loaded).toEqual(familyNames);
|
||||
expect(families.every((family) => vi.mocked(family.load).mock.calls.length === 1)).toBe(true);
|
||||
sidecar.stop();
|
||||
});
|
||||
|
||||
it("stops before importing a scheduled family", async () => {
|
||||
vi.useFakeTimers();
|
||||
const load = vi.fn(async () => {});
|
||||
const sidecar = scheduleGatewayHandlerPrewarm({
|
||||
families: [{ name: "sessions", load }],
|
||||
log: { warn: vi.fn() },
|
||||
});
|
||||
|
||||
sidecar.stop();
|
||||
await vi.runAllTimersAsync();
|
||||
expect(load).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
import { runWithGatewayIndependentRootWorkAdmission } from "../process/gateway-work-admission.js";
|
||||
|
||||
type StartupTrace = {
|
||||
measure: <T>(name: string, run: () => T | Promise<T>) => Promise<T>;
|
||||
};
|
||||
|
||||
type GatewayHandlerPrewarmFamily = {
|
||||
name: string;
|
||||
load: () => Promise<unknown>;
|
||||
};
|
||||
|
||||
type GatewayHandlerPrewarmHandle = {
|
||||
stop: () => void;
|
||||
};
|
||||
|
||||
// These are the families requested by the Control UI's first dashboard turn.
|
||||
// Keep the list explicit so adding a cold import is a conscious startup tradeoff.
|
||||
const DASHBOARD_HANDLER_FAMILIES: readonly GatewayHandlerPrewarmFamily[] = [
|
||||
{ name: "sessions", load: () => import("./server-methods/sessions.js") },
|
||||
{ name: "chat", load: () => import("./server-methods/chat.js") },
|
||||
{ name: "tasks", load: () => import("./server-methods/tasks.js") },
|
||||
{ name: "cron", load: () => import("./server-methods/cron.js") },
|
||||
{ name: "models-auth-status", load: () => import("./server-methods/models-auth-status.js") },
|
||||
{ name: "agent-identity", load: () => import("./server-methods/agent-identity.js") },
|
||||
{ name: "board", load: () => import("./server-methods/board.js") },
|
||||
{ name: "channels", load: () => import("./server-methods/channels.js") },
|
||||
];
|
||||
|
||||
export function scheduleGatewayHandlerPrewarm(params: {
|
||||
startupTrace?: StartupTrace;
|
||||
log: { warn: (msg: string) => void };
|
||||
families?: readonly GatewayHandlerPrewarmFamily[];
|
||||
}): GatewayHandlerPrewarmHandle {
|
||||
const families = params.families ?? DASHBOARD_HANDLER_FAMILIES;
|
||||
let stopped = false;
|
||||
let nextIndex = 0;
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const scheduleNext = () => {
|
||||
if (stopped || nextIndex >= families.length) {
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
timer = undefined;
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
const family = families[nextIndex++];
|
||||
if (!family) {
|
||||
return;
|
||||
}
|
||||
const load = () => family.load();
|
||||
void runWithGatewayIndependentRootWorkAdmission(() =>
|
||||
params.startupTrace
|
||||
? params.startupTrace.measure(`post-ready.gateway-handler.${family.name}`, load)
|
||||
: load(),
|
||||
)
|
||||
.catch((err: unknown) => {
|
||||
params.log.warn(
|
||||
`post-ready gateway handler prewarm failed for ${family.name}: ${String(err)}`,
|
||||
);
|
||||
})
|
||||
.finally(scheduleNext);
|
||||
}, 0);
|
||||
timer.unref?.();
|
||||
};
|
||||
|
||||
// One family per event-loop turn keeps this work behind readiness and lets
|
||||
// immediate client traffic run between imports instead of recreating a startup wall.
|
||||
scheduleNext();
|
||||
|
||||
return {
|
||||
stop: () => {
|
||||
stopped = true;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -65,7 +65,6 @@ const hoisted = vi.hoisted(() => {
|
||||
const refreshPreparedModelRuntimeSnapshots = vi.fn(async (_cfg?: unknown) => {});
|
||||
const ensureRuntimePluginsLoaded = vi.fn();
|
||||
const ensureContextWindowCacheLoaded = vi.fn(async () => {});
|
||||
const scheduleGatewayHandlerPrewarm = vi.fn(() => ({ stop: vi.fn() }));
|
||||
const clearCurrentProviderAuthState = vi.fn();
|
||||
const warmCurrentProviderAuthStateOffMainThread = vi.fn(
|
||||
async (_cfg?: unknown, _options?: unknown) => {},
|
||||
@@ -106,7 +105,6 @@ const hoisted = vi.hoisted(() => {
|
||||
refreshPreparedModelRuntimeSnapshots,
|
||||
ensureRuntimePluginsLoaded,
|
||||
ensureContextWindowCacheLoaded,
|
||||
scheduleGatewayHandlerPrewarm,
|
||||
clearCurrentProviderAuthState,
|
||||
warmCurrentProviderAuthStateOffMainThread,
|
||||
setAuthProfileFailureHook,
|
||||
@@ -224,10 +222,6 @@ vi.mock("../agents/model-provider-auth.js", () => ({
|
||||
warmCurrentProviderAuthStateOffMainThread: hoisted.warmCurrentProviderAuthStateOffMainThread,
|
||||
}));
|
||||
|
||||
vi.mock("./server-startup-handler-prewarm.js", () => ({
|
||||
scheduleGatewayHandlerPrewarm: hoisted.scheduleGatewayHandlerPrewarm,
|
||||
}));
|
||||
|
||||
vi.mock("../agents/model-provider-auth-state.js", () => ({
|
||||
clearCurrentProviderAuthState: hoisted.clearCurrentProviderAuthState,
|
||||
}));
|
||||
@@ -1082,7 +1076,7 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(postReadyRequestTurn).toHaveBeenCalledTimes(1);
|
||||
expect(onPostReadySidecars.mock.calls[0]?.[0]).toHaveLength(0);
|
||||
expect(onGatewayLifetimeSidecars.mock.calls[0]?.[0]).toHaveLength(4);
|
||||
expect(onGatewayLifetimeSidecars.mock.calls[0]?.[0]).toHaveLength(3);
|
||||
await vi.dynamicImportSettled();
|
||||
await waitForGatewayTestState(() => {
|
||||
expect(hoisted.setAuthProfileFailureHook).toHaveBeenCalledTimes(1);
|
||||
@@ -1114,7 +1108,7 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
await waitForGatewayTestState(() => {
|
||||
expect(hoisted.setAuthProfileFailureHook).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(onGatewayLifetimeSidecars.mock.calls[0]?.[0]).toHaveLength(4);
|
||||
expect(onGatewayLifetimeSidecars.mock.calls[0]?.[0]).toHaveLength(3);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(10_000);
|
||||
expect(hoisted.warmCurrentProviderAuthStateOffMainThread).not.toHaveBeenCalled();
|
||||
@@ -1238,7 +1232,7 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
| { stop: () => void }[]
|
||||
| undefined;
|
||||
expect(gmailSidecars).toHaveLength(1);
|
||||
expect(lifetimeSidecars).toHaveLength(4);
|
||||
expect(lifetimeSidecars).toHaveLength(3);
|
||||
|
||||
for (const sidecar of gmailSidecars ?? []) {
|
||||
sidecar.stop();
|
||||
@@ -1300,7 +1294,7 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
| Array<{ stop: () => Promise<void> | void }>
|
||||
| undefined;
|
||||
expect(gmailSidecars).toHaveLength(1);
|
||||
expect(lifetimeSidecars).toHaveLength(4);
|
||||
expect(lifetimeSidecars).toHaveLength(3);
|
||||
|
||||
await waitForGatewayTestState(() => {
|
||||
expect(hoisted.transcriptsAutoStartService.start).toHaveBeenCalledTimes(1);
|
||||
@@ -1795,7 +1789,7 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
name: "sidecars.ready",
|
||||
metrics: [
|
||||
["loadedPluginCount", 2],
|
||||
["postReadySidecarCount", 3],
|
||||
["postReadySidecarCount", 2],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,6 @@ import type { GatewayRecoveryRuntime } from "./server-instance-runtime.types.js"
|
||||
import type { refreshLatestUpdateRestartSentinel } from "./server-restart-sentinel.js";
|
||||
import type { GatewaySidecarStartupMode } from "./server-sidecar-startup-mode.js";
|
||||
import { scheduleContextCachePrewarm } from "./server-startup-context-cache-prewarm.js";
|
||||
import { scheduleGatewayHandlerPrewarm } from "./server-startup-handler-prewarm.js";
|
||||
import type { logGatewayStartup } from "./server-startup-log.js";
|
||||
import {
|
||||
createGatewayStartupOutcomeRecorder,
|
||||
@@ -1231,10 +1230,7 @@ export async function startGatewayPostAttachRuntime(
|
||||
reportPluginServices(result.pluginServices);
|
||||
}
|
||||
const postReadySidecars = [...result.postReadySidecars];
|
||||
const gatewayLifetimeSidecars = [
|
||||
scheduleContextCachePrewarm(params),
|
||||
scheduleGatewayHandlerPrewarm(params),
|
||||
];
|
||||
const gatewayLifetimeSidecars = [scheduleContextCachePrewarm(params)];
|
||||
if (workerEnvironmentSidecar) {
|
||||
gatewayLifetimeSidecars.push(workerEnvironmentSidecar);
|
||||
}
|
||||
|
||||
@@ -119,6 +119,10 @@ const INTERNAL_CORE_PACKAGE_ALIASES = [
|
||||
},
|
||||
] as const;
|
||||
const pluginSdkNativeAliases = new Map<string, NativeAliasEntry[]>();
|
||||
const pluginSdkNativeAliasesByMap = new WeakMap<
|
||||
Record<string, string>,
|
||||
Array<readonly [string, string]>
|
||||
>();
|
||||
const internalCorePackageHostRoots = new PluginLruCache<string>(128);
|
||||
const registeredInternalCorePackageHosts = new PluginLruCache<true>(128);
|
||||
let installed = false;
|
||||
@@ -290,17 +294,20 @@ function listPluginSdkNativeAliases(
|
||||
options: InstallOpenClawPluginSdkNativeResolverOptions,
|
||||
): Array<readonly [string, string]> {
|
||||
const modulePath = options.pluginModulePath ?? resolveLoaderModulePath(options);
|
||||
return Object.entries(
|
||||
buildPluginLoaderAliasMap(
|
||||
modulePath,
|
||||
options.argv1 ?? process.argv[1],
|
||||
options.moduleUrl,
|
||||
// Native require hooks must point at JavaScript artifacts, even when the
|
||||
// plugin loader itself is configured to prefer source imports.
|
||||
"dist",
|
||||
options.devSourceRoot,
|
||||
),
|
||||
)
|
||||
const aliasMap = buildPluginLoaderAliasMap(
|
||||
modulePath,
|
||||
options.argv1 ?? process.argv[1],
|
||||
options.moduleUrl,
|
||||
// Native require hooks must point at JavaScript artifacts, even when the
|
||||
// plugin loader itself is configured to prefer source imports.
|
||||
"dist",
|
||||
options.devSourceRoot,
|
||||
);
|
||||
const cached = pluginSdkNativeAliasesByMap.get(aliasMap);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const aliases = Object.entries(aliasMap)
|
||||
.filter(([specifier]) => isPluginSdkAliasSpecifier(specifier))
|
||||
.filter(([, target]) => isNativeLoadableSdkTarget(target))
|
||||
.flatMap(([specifier, target]) => {
|
||||
@@ -312,6 +319,8 @@ function listPluginSdkNativeAliases(
|
||||
[`${specifier}.js`, target],
|
||||
] as Array<readonly [string, string]>;
|
||||
});
|
||||
pluginSdkNativeAliasesByMap.set(aliasMap, aliases);
|
||||
return aliases;
|
||||
}
|
||||
|
||||
function listInternalCorePackageNativeAliases(
|
||||
|
||||
@@ -2238,6 +2238,14 @@ describe("buildPluginLoaderAliasMap memoization", () => {
|
||||
expect(aliasA).not.toBe(aliasB);
|
||||
});
|
||||
|
||||
it("reuses one merged map for plugin entrypoints with the same effective SDK surface", () => {
|
||||
const fixture = createPluginSdkAliasFixture();
|
||||
const entryA = writePluginEntry(fixture.root, bundledPluginFile("a", "src/index.ts"));
|
||||
const entryB = writePluginEntry(fixture.root, bundledPluginFile("b", "src/index.ts"));
|
||||
|
||||
expect(buildPluginLoaderAliasMap(entryB)).toBe(buildPluginLoaderAliasMap(entryA));
|
||||
});
|
||||
|
||||
it("returns different references when pluginSdkResolution differs", () => {
|
||||
const fixture = createPluginSdkAliasFixture();
|
||||
const entry = writePluginEntry(fixture.root, bundledPluginFile("res", "src/index.ts"));
|
||||
@@ -2248,14 +2256,24 @@ describe("buildPluginLoaderAliasMap memoization", () => {
|
||||
expect(auto).not.toBe(dist);
|
||||
});
|
||||
|
||||
it("returns different references when argv1 differs", () => {
|
||||
it("reuses one merged map when resolution modes have the same effective order", () => {
|
||||
const fixture = createPluginSdkAliasFixture();
|
||||
const entry = writePluginEntry(fixture.root, bundledPluginFile("same-order", "src/index.ts"));
|
||||
|
||||
const auto = buildPluginLoaderAliasMap(entry, undefined, undefined, "auto");
|
||||
const source = buildPluginLoaderAliasMap(entry, undefined, undefined, "src");
|
||||
|
||||
expect(source).toBe(auto);
|
||||
});
|
||||
|
||||
it("reuses a merged map when different argv hints resolve the same SDK surface", () => {
|
||||
const fixture = createPluginSdkAliasFixture();
|
||||
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);
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it("returns different references when an explicit dev source root differs", () => {
|
||||
|
||||
@@ -412,6 +412,9 @@ const cachedPluginSdkScopedAliasMaps = new PluginLruCache<Record<string, string>
|
||||
const cachedBundledPluginPublicSurfaceAliasMaps = new PluginLruCache<Record<string, string>>(
|
||||
MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES,
|
||||
);
|
||||
const cachedWorkspacePackageAliasMaps = new PluginLruCache<Record<string, string>>(
|
||||
MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES,
|
||||
);
|
||||
const PLUGIN_SDK_PACKAGE_NAMES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const;
|
||||
const CODEX_MCP_PROJECTION_PLUGIN_SDK_SUBPATH = "codex-mcp-projection";
|
||||
const OLLAMA_CONFIGURED_LOCAL_ORIGIN_RUNTIME_PLUGIN_SDK_SUBPATH = "ssrf-runtime-internal";
|
||||
@@ -896,6 +899,13 @@ function resolveWorkspacePackageAliasMap(params: {
|
||||
isProduction: process.env.NODE_ENV === "production",
|
||||
pluginSdkResolution: params.pluginSdkResolution,
|
||||
});
|
||||
// Raw modes with the same effective preference order resolve identical targets.
|
||||
// Key the process-stable cache by that target-affecting order, not the caller spelling.
|
||||
const cacheKey = `${packageRoot}::${orderedKinds.join(",")}`;
|
||||
const cached = cachedWorkspacePackageAliasMaps.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const aliasMap: Record<string, string> = {};
|
||||
const workspacePackageAliasEntries = [
|
||||
...WORKSPACE_PACKAGE_ALIAS_ENTRIES,
|
||||
@@ -933,6 +943,7 @@ function resolveWorkspacePackageAliasMap(params: {
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedWorkspacePackageAliasMaps.set(cacheKey, aliasMap);
|
||||
return aliasMap;
|
||||
}
|
||||
|
||||
@@ -1262,6 +1273,50 @@ const pluginLoaderModuleConfigCache = new PluginLruCache<{
|
||||
aliasMap: Record<string, string>;
|
||||
cacheKey: string;
|
||||
}>(MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES);
|
||||
const normalizedAliasTargetsByInput = new WeakMap<Record<string, string>, Record<string, string>>();
|
||||
const mergedAliasMapsByComponent = new WeakMap<
|
||||
Record<string, string>,
|
||||
WeakMap<Record<string, string>, WeakMap<Record<string, string>, Record<string, string>>>
|
||||
>();
|
||||
|
||||
function normalizeAliasTargets(aliasMap: Record<string, string>): Record<string, string> {
|
||||
if (process.platform !== "win32") {
|
||||
return aliasMap;
|
||||
}
|
||||
const cached = normalizedAliasTargetsByInput.get(aliasMap);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const normalized = Object.fromEntries(
|
||||
Object.entries(aliasMap).map(([key, value]) => [key, normalizeJitiAliasTargetPath(value)]),
|
||||
);
|
||||
normalizedAliasTargetsByInput.set(aliasMap, normalized);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mergeAliasMaps(
|
||||
bundled: Record<string, string>,
|
||||
workspace: Record<string, string>,
|
||||
pluginSdk: Record<string, string>,
|
||||
): Record<string, string> {
|
||||
let byWorkspace = mergedAliasMapsByComponent.get(bundled);
|
||||
if (!byWorkspace) {
|
||||
byWorkspace = new WeakMap();
|
||||
mergedAliasMapsByComponent.set(bundled, byWorkspace);
|
||||
}
|
||||
let byPluginSdk = byWorkspace.get(workspace);
|
||||
if (!byPluginSdk) {
|
||||
byPluginSdk = new WeakMap();
|
||||
byWorkspace.set(workspace, byPluginSdk);
|
||||
}
|
||||
const cached = byPluginSdk.get(pluginSdk);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const merged = { ...bundled, ...workspace, ...pluginSdk };
|
||||
byPluginSdk.set(pluginSdk, merged);
|
||||
return merged;
|
||||
}
|
||||
|
||||
function hasJitiNormalizedAliasMarker(aliasMap: Record<string, string>) {
|
||||
return Boolean((aliasMap as Record<symbol, unknown>)[JITI_NORMALIZED_ALIAS_SYMBOL]);
|
||||
@@ -1423,33 +1478,32 @@ export function buildPluginLoaderAliasMap(
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result: Record<string, string> = {
|
||||
...resolveBundledPluginPackagePublicSurfaceAliasMap({
|
||||
const bundledAliases = resolveBundledPluginPackagePublicSurfaceAliasMap({
|
||||
modulePath,
|
||||
argv1,
|
||||
moduleUrl,
|
||||
pluginSdkResolution,
|
||||
devSourceRoot,
|
||||
});
|
||||
const workspaceAliases = resolveWorkspacePackageAliasMap({
|
||||
modulePath,
|
||||
argv1,
|
||||
moduleUrl,
|
||||
pluginSdkResolution,
|
||||
devSourceRoot,
|
||||
});
|
||||
const pluginSdkAliases = normalizeAliasTargets(
|
||||
resolvePluginSdkScopedAliasMap({
|
||||
modulePath,
|
||||
argv1,
|
||||
moduleUrl,
|
||||
pluginSdkResolution,
|
||||
devSourceRoot,
|
||||
}),
|
||||
...resolveWorkspacePackageAliasMap({
|
||||
modulePath,
|
||||
argv1,
|
||||
moduleUrl,
|
||||
pluginSdkResolution,
|
||||
devSourceRoot,
|
||||
}),
|
||||
...Object.fromEntries(
|
||||
Object.entries(
|
||||
resolvePluginSdkScopedAliasMap({
|
||||
modulePath,
|
||||
argv1,
|
||||
moduleUrl,
|
||||
pluginSdkResolution,
|
||||
devSourceRoot,
|
||||
}),
|
||||
).map(([key, value]) => [key, normalizeJitiAliasTargetPath(value)]),
|
||||
),
|
||||
};
|
||||
);
|
||||
// Different plugin entrypoints commonly resolve the same process-stable SDK surface.
|
||||
// Reuse one merged map so plugin count does not multiply identical alias objects.
|
||||
const result = mergeAliasMaps(bundledAliases, workspaceAliases, pluginSdkAliases);
|
||||
aliasMapCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ describe("gateway startup benchmark script", () => {
|
||||
expect(helpResult.stdout).toContain("OpenClaw Gateway startup benchmark");
|
||||
expect(helpResult.stdout).toContain("--case <id>");
|
||||
expect(helpResult.stdout).toContain("--cpu-prof-dir <dir>");
|
||||
expect(helpResult.stdout).toContain("--heap-prof-dir <dir>");
|
||||
expect(helpResult.stdout).toContain("default (gateway default)");
|
||||
expect(helpResult.stdout).not.toContain("[gateway-startup-bench]");
|
||||
expect(helpResult.stderr).toBe("");
|
||||
@@ -63,12 +64,15 @@ describe("gateway startup benchmark script", () => {
|
||||
"--output",
|
||||
"startup.json",
|
||||
"--json",
|
||||
"--heap-prof-dir",
|
||||
"profiles",
|
||||
"--runs",
|
||||
"2",
|
||||
]),
|
||||
).toMatchObject({
|
||||
cases: [{ id: "default" }],
|
||||
json: true,
|
||||
heapProfDir: "profiles",
|
||||
output: "startup.json",
|
||||
runs: 2,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user