perf(gateway): reuse stable turn metadata

This commit is contained in:
Peter Steinberger
2026-05-30 17:30:34 +01:00
parent 02ca283716
commit 18e7d28b21
10 changed files with 248 additions and 34 deletions

View File

@@ -244,6 +244,30 @@ describe("loadPluginMetadataSnapshot process memo", () => {
expect(second.byPluginId.get("demo")).toBe(second.plugins[0]);
});
it("skips persisted registry filesystem fingerprints after a process memo hit", () => {
const stateDir = tempStateDir();
touchPersistedIndex(stateDir);
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
source: "persisted",
snapshot: makeIndex(),
diagnostics: [],
});
const first = loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
const statSpy = vi.spyOn(fs, "statSync");
const readdirSpy = vi.spyOn(fs, "readdirSync");
try {
const second = loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
expect(second).toBe(first);
expect(statSpy).not.toHaveBeenCalled();
expect(readdirSpy).not.toHaveBeenCalled();
} finally {
statSpy.mockRestore();
readdirSpy.mockRestore();
}
});
it("clears the process memo at plugin metadata lifecycle boundaries", () => {
const stateDir = tempStateDir();
touchPersistedIndex(stateDir);
@@ -481,7 +505,7 @@ describe("loadPluginMetadataSnapshot process memo", () => {
expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledOnce();
});
it("refreshes when the persisted registry file changes", () => {
it("keeps persisted registry snapshots process-stable until lifecycle clear", () => {
const stateDir = tempStateDir();
touchPersistedIndex(stateDir, 1);
loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
@@ -494,6 +518,11 @@ describe("loadPluginMetadataSnapshot process memo", () => {
touchPersistedIndex(stateDir, 22);
loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledOnce();
clearPluginMetadataLifecycleCaches();
loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledTimes(2);
});

View File

@@ -37,6 +37,7 @@ import {
type PluginMetadataSnapshotMemo = {
key: string;
lookupContextHash: string;
registryState?: PersistedRegistryMemoState;
snapshot: PluginMetadataSnapshot;
};
@@ -231,6 +232,18 @@ function resolvePersistedRegistryMemoContextHash(params: {
});
}
function resolvePersistedRegistryMemoLookupContextHash(params: {
env: NodeJS.ProcessEnv;
preferPersisted?: boolean;
stateDir?: string;
}): string {
return hashJson({
env: pickMemoRelevantEnv(params.env),
preferPersisted: params.preferPersisted ?? null,
stateDir: params.stateDir ?? null,
});
}
function resolvePersistedRegistryMemoState(params: {
env: NodeJS.ProcessEnv;
index?: InstalledPluginIndex;
@@ -273,6 +286,15 @@ function resolvePersistedRegistryMemoStateForLookup(
},
memos: readonly PluginMetadataSnapshotMemo[],
): PersistedRegistryMemoState {
const lookupContextHash = resolvePersistedRegistryMemoLookupContextHash(params);
for (const memo of memos) {
if (memo.lookupContextHash === lookupContextHash && memo.registryState) {
// Gateway runtime metadata is process-stable. Installs/reloads clear the
// memo lifecycle explicitly, so hot lookups can reuse the prepared
// registry stamp instead of re-statting plugin roots on every turn.
return memo.registryState;
}
}
const fastFingerprint = resolvePersistedRegistryFastMemoFingerprint(params);
const fastHash = hashJson(fastFingerprint);
const contextHash = resolvePersistedRegistryMemoContextHash({
@@ -581,6 +603,13 @@ export function loadPluginMetadataSnapshot(
: registryState;
rememberPluginMetadataSnapshotMemo({
key: computePluginMetadataSnapshotMemoKey({ params, registryState: cachedRegistryState }),
lookupContextHash: resolvePersistedRegistryMemoLookupContextHash({
env,
...(params.stateDir ? { stateDir: resolveUserPath(params.stateDir, env) } : {}),
...(params.preferPersisted !== undefined
? { preferPersisted: params.preferPersisted }
: {}),
}),
registryState: cachedRegistryState,
snapshot,
});