Skip to content

Commit 94ae918

Browse files
perf(plugins): reuse installed manifest realpaths (#96710)
Co-authored-by: sheyanmin <[email protected]>
1 parent af90622 commit 94ae918

2 files changed

Lines changed: 133 additions & 3 deletions

File tree

src/plugins/manifest-registry-installed.test.ts

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
readPersistedInstalledPluginIndex,
77
writePersistedInstalledPluginIndex,
88
} from "./installed-plugin-index-store.js";
9-
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
9+
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
1010
import {
1111
loadPluginManifestRegistryForInstalledIndex,
1212
resolveInstalledManifestRegistryIndexFingerprint,
@@ -151,6 +151,24 @@ function createIndexWithPackageJson(rootDir: string): InstalledPluginIndex {
151151
};
152152
}
153153

154+
function createIndexWithUnhashedPackageJson(rootDir: string): InstalledPluginIndex {
155+
const index = createIndexWithFileSignatures(rootDir);
156+
const packageJsonPath = writePackageManifest(rootDir, "Installed");
157+
const record = index.plugins[0];
158+
if (!record) {
159+
throw new Error("expected index record");
160+
}
161+
record.packageJson = {
162+
path: "package.json",
163+
hash: "",
164+
fileSignature: fileSignature(packageJsonPath),
165+
};
166+
return {
167+
...index,
168+
plugins: [record],
169+
};
170+
}
171+
154172
describe("loadPluginManifestRegistryForInstalledIndex", () => {
155173
it("reuses frozen installed-index fingerprints when file signatures are persisted", () => {
156174
const rootDir = makeTempDir();
@@ -180,6 +198,97 @@ describe("loadPluginManifestRegistryForInstalledIndex", () => {
180198
expect(second).not.toBe(first);
181199
});
182200

201+
it("reuses package realpaths across mutable installed-index fingerprint builds", () => {
202+
const rootDir = makeTempDir();
203+
writePlugin(rootDir, "installed", "installed-");
204+
const index = createIndexWithUnhashedPackageJson(rootDir);
205+
const packageJsonPath = path.join(fs.realpathSync(rootDir), "package.json");
206+
const realpathSpy = vi.spyOn(fs, "realpathSync");
207+
let rootPathCalls: unknown[][];
208+
let packageJsonPathCalls: unknown[][];
209+
try {
210+
resolveInstalledManifestRegistryIndexFingerprint(index);
211+
resolveInstalledManifestRegistryIndexFingerprint(index);
212+
rootPathCalls = realpathSpy.mock.calls.filter(([filePath]) => filePath === rootDir);
213+
packageJsonPathCalls = realpathSpy.mock.calls.filter(
214+
([filePath]) => filePath === packageJsonPath,
215+
);
216+
} finally {
217+
realpathSpy.mockRestore();
218+
}
219+
220+
expect(rootPathCalls).toHaveLength(1);
221+
expect(packageJsonPathCalls).toHaveLength(1);
222+
});
223+
224+
it("clears package realpath memoization with plugin metadata lifecycle caches", () => {
225+
const rootDir = makeTempDir();
226+
writePlugin(rootDir, "installed", "installed-");
227+
const index = createIndexWithUnhashedPackageJson(rootDir);
228+
const packageJsonPath = path.join(fs.realpathSync(rootDir), "package.json");
229+
const realpathSpy = vi.spyOn(fs, "realpathSync");
230+
let rootPathCalls: unknown[][];
231+
let packageJsonPathCalls: unknown[][];
232+
try {
233+
resolveInstalledManifestRegistryIndexFingerprint(index);
234+
clearPluginMetadataLifecycleCaches();
235+
resolveInstalledManifestRegistryIndexFingerprint(index);
236+
rootPathCalls = realpathSpy.mock.calls.filter(([filePath]) => filePath === rootDir);
237+
packageJsonPathCalls = realpathSpy.mock.calls.filter(
238+
([filePath]) => filePath === packageJsonPath,
239+
);
240+
} finally {
241+
realpathSpy.mockRestore();
242+
}
243+
244+
expect(rootPathCalls).toHaveLength(2);
245+
expect(packageJsonPathCalls).toHaveLength(2);
246+
});
247+
248+
it("bounds package realpath memoization across many fingerprint roots", () => {
249+
const firstRootDir = makeTempDir();
250+
writePlugin(firstRootDir, "installed", "installed-");
251+
const firstIndex = createIndexWithUnhashedPackageJson(firstRootDir);
252+
resolveInstalledManifestRegistryIndexFingerprint(firstIndex);
253+
254+
const records: InstalledPluginIndexRecord[] = [];
255+
for (let index = 0; index < 300; index += 1) {
256+
const rootDir = makeTempDir();
257+
const pluginId = `installed-${index}`;
258+
writePlugin(rootDir, pluginId, `${pluginId}-`);
259+
const record = createIndexWithUnhashedPackageJson(rootDir).plugins[0];
260+
if (!record) {
261+
throw new Error("expected index record");
262+
}
263+
records.push({
264+
...record,
265+
pluginId,
266+
manifestHash: `manifest-hash-${index}`,
267+
});
268+
}
269+
resolveInstalledManifestRegistryIndexFingerprint({
270+
...firstIndex,
271+
plugins: records,
272+
});
273+
274+
const packageJsonPath = path.join(fs.realpathSync(firstRootDir), "package.json");
275+
const realpathSpy = vi.spyOn(fs, "realpathSync");
276+
let rootPathCalls: unknown[][];
277+
let packageJsonPathCalls: unknown[][];
278+
try {
279+
resolveInstalledManifestRegistryIndexFingerprint(firstIndex);
280+
rootPathCalls = realpathSpy.mock.calls.filter(([filePath]) => filePath === firstRootDir);
281+
packageJsonPathCalls = realpathSpy.mock.calls.filter(
282+
([filePath]) => filePath === packageJsonPath,
283+
);
284+
} finally {
285+
realpathSpy.mockRestore();
286+
}
287+
288+
expect(rootPathCalls).toHaveLength(1);
289+
expect(packageJsonPathCalls).toHaveLength(1);
290+
});
291+
183292
it("does not cache shallow-frozen installed-index fingerprints with mutable nested records", () => {
184293
const rootDir = makeTempDir();
185294
writePlugin(rootDir, "installed", "installed-");

src/plugins/manifest-registry-installed.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ import {
3232
const installedManifestRegistryIndexFingerprintCache = new WeakMap<InstalledPluginIndex, string>();
3333
const installedPackageJsonPathCache = new Map<string, string | null>();
3434
const installedPackageMetadataCache = new Map<string, InstalledPackageMetadata>();
35+
// Installed plugin metadata is process-stable between explicit lifecycle clears.
36+
// Share realpaths across fingerprint builds to avoid repeated package boundary IO.
37+
const installedManifestRegistryRealpathCache = new Map<string, string>();
3538
const MAX_INSTALLED_PACKAGE_JSON_PATH_CACHE_ENTRIES = 256;
3639
const MAX_INSTALLED_PACKAGE_METADATA_CACHE_ENTRIES = 256;
40+
const MAX_INSTALLED_MANIFEST_REGISTRY_REALPATH_CACHE_ENTRIES = 512;
3741

3842
type InstalledPackageMetadata = {
3943
packageManifest?: OpenClawPackageManifest;
@@ -44,6 +48,7 @@ type InstalledPackageMetadata = {
4448
export function clearInstalledManifestRegistryProcessCaches(): void {
4549
installedPackageJsonPathCache.clear();
4650
installedPackageMetadataCache.clear();
51+
installedManifestRegistryRealpathCache.clear();
4752
}
4853

4954
registerPluginMetadataProcessMemoLifecycleClear(clearInstalledManifestRegistryProcessCaches);
@@ -169,6 +174,19 @@ function rememberInstalledPackageJsonPath(
169174
return packageJsonPath;
170175
}
171176

177+
function trimInstalledManifestRegistryRealpathCache(): void {
178+
while (
179+
installedManifestRegistryRealpathCache.size >
180+
MAX_INSTALLED_MANIFEST_REGISTRY_REALPATH_CACHE_ENTRIES
181+
) {
182+
const oldest = installedManifestRegistryRealpathCache.keys().next().value;
183+
if (oldest === undefined) {
184+
break;
185+
}
186+
installedManifestRegistryRealpathCache.delete(oldest);
187+
}
188+
}
189+
172190
function buildInstalledPackageJsonPathCacheKey(
173191
record: InstalledPluginIndexRecord,
174192
): string | undefined {
@@ -196,7 +214,6 @@ function buildInstalledPackageMetadataCacheKey(params: {
196214
}
197215

198216
function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
199-
const realpathCache = new Map<string, string>();
200217
return {
201218
version: index.version,
202219
hostContractVersion: index.hostContractVersion,
@@ -206,7 +223,11 @@ function buildInstalledManifestRegistryIndexKey(index: InstalledPluginIndex) {
206223
installRecords: index.installRecords,
207224
diagnostics: index.diagnostics,
208225
plugins: index.plugins.map((record) => {
209-
const packageJsonPath = resolvePackageJsonPath(record, realpathCache);
226+
const packageJsonPath = resolvePackageJsonPath(
227+
record,
228+
installedManifestRegistryRealpathCache,
229+
);
230+
trimInstalledManifestRegistryRealpathCache();
210231
const packageJsonFile = record.packageJson?.fileSignature
211232
? packageJsonPath
212233
? formatFileSignature(packageJsonPath, record.packageJson.fileSignature)

0 commit comments

Comments
 (0)