mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-01 11:13:33 +00:00
* perf(plugins): cache existence probes within bundle manifest scan Bundle plugin discovery re-probes the same marker paths (skills/, commands/, agents/, .mcp.json, .lsp.json, settings.json, hooks/hooks.json) once in detectBundleManifestFormat and again in loadBundleManifest's capability builders. Across the bundled plugin tree this is thousands of redundant synchronous fs.existsSync calls; #76209 reports 25.4s of self-time on a Windows cold start. Add a scan-scoped existence cache (plugin-scan-existence-cache.ts) entered only around discoverBundleInRoot. pluginScanExistsSync memoizes inside the active scan and falls back to plain fs.existsSync outside it, so install, hooks, and doctor flows stay uncached. The cache is push/pop per discoverBundleInRoot call (try/finally), so a later install/repair pass re-reads the filesystem — no process-global staleness. Measured on Windows over a 25-plugin fixture: 550 -> 325 fs.existsSync calls (41% fewer), 294.75ms -> 208.49ms. Discovery results unchanged. Closes #76209 * fix(plugins): drop unused test reset helper and satisfy oxlint Remove __resetPluginScanExistenceCacheForTest: the scan cache is push/pop balanced by try/finally in withPluginScanExistenceCache, so the stack never leaks between tests and the helper was dead code. It also tripped oxlint no-underscore-dangle. Refactor the integration test to count existsSync calls via a const-returning helper so there is no useless assignment.
161 lines
5.9 KiB
TypeScript
161 lines
5.9 KiB
TypeScript
// Verifies the scan-scoped plugin existence cache: within a scan pass repeated
|
|
// probes hit the filesystem once, across scans the filesystem is re-read (no
|
|
// process-global staleness), and outside a scan it is a plain passthrough.
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { loadBundleManifest, detectBundleManifestFormat } from "./bundle-manifest.js";
|
|
import {
|
|
pluginScanExistsSync,
|
|
withPluginScanExistenceCache,
|
|
} from "./plugin-scan-existence-cache.js";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function makeTempDir(prefix: string): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
tempDirs.push(dir);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
tempDirs.length = 0;
|
|
});
|
|
|
|
describe("pluginScanExistsSync", () => {
|
|
it("is a plain passthrough outside a scan (no caching)", () => {
|
|
const dir = makeTempDir("exists-passthrough-");
|
|
const target = path.join(dir, "marker.json");
|
|
fs.writeFileSync(target, "{}");
|
|
const spy = vi.spyOn(fs, "existsSync");
|
|
try {
|
|
expect(pluginScanExistsSync(target)).toBe(true);
|
|
expect(pluginScanExistsSync(target)).toBe(true);
|
|
expect(pluginScanExistsSync(target)).toBe(true);
|
|
// No active scan cache: every probe reaches the filesystem.
|
|
expect(spy).toHaveBeenCalledTimes(3);
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("memoizes repeated probes within a single scan pass", () => {
|
|
const dir = makeTempDir("exists-memoize-");
|
|
const target = path.join(dir, "marker.json");
|
|
fs.writeFileSync(target, "{}");
|
|
const spy = vi.spyOn(fs, "existsSync");
|
|
try {
|
|
const result = withPluginScanExistenceCache(() => {
|
|
let hit = false;
|
|
for (let i = 0; i < 5; i += 1) {
|
|
if (pluginScanExistsSync(target)) {
|
|
hit = true;
|
|
}
|
|
}
|
|
return hit;
|
|
});
|
|
expect(result).toBe(true);
|
|
// Five probes of the same path, one filesystem call inside the scan.
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("does not conflate different paths inside a scan", () => {
|
|
const dir = makeTempDir("exists-distinct-");
|
|
const present = path.join(dir, "present.json");
|
|
const absent = path.join(dir, "absent.json");
|
|
fs.writeFileSync(present, "{}");
|
|
const spy = vi.spyOn(fs, "existsSync");
|
|
try {
|
|
withPluginScanExistenceCache(() => {
|
|
expect(pluginScanExistsSync(present)).toBe(true);
|
|
expect(pluginScanExistsSync(absent)).toBe(false);
|
|
// Re-probe both: served from cache, no new filesystem calls.
|
|
expect(pluginScanExistsSync(present)).toBe(true);
|
|
expect(pluginScanExistsSync(absent)).toBe(false);
|
|
});
|
|
expect(spy).toHaveBeenCalledTimes(2);
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("re-reads the filesystem across separate scans (no process-global staleness)", () => {
|
|
const dir = makeTempDir("exists-freshness-");
|
|
const target = path.join(dir, "marker.json");
|
|
// First scan sees the path absent.
|
|
withPluginScanExistenceCache(() => {
|
|
expect(pluginScanExistsSync(target)).toBe(false);
|
|
});
|
|
// Marker appears between scans (e.g. an install/repair pass).
|
|
fs.writeFileSync(target, "{}");
|
|
// A later scan must observe the new state, not a stale cached false.
|
|
withPluginScanExistenceCache(() => {
|
|
expect(pluginScanExistsSync(target)).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("bundle manifest scan uses the existence cache", () => {
|
|
function buildClaudeBundlePlugin(root: string): void {
|
|
fs.mkdirSync(path.join(root, ".claude-plugin"), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, ".claude-plugin", "plugin.json"),
|
|
JSON.stringify({ name: "demo-bundle" }),
|
|
);
|
|
// Capability marker dirs/files that both detectBundleManifestFormat and
|
|
// loadBundleManifest probe for a claude-format bundle.
|
|
fs.mkdirSync(path.join(root, "skills"), { recursive: true });
|
|
fs.mkdirSync(path.join(root, "commands"), { recursive: true });
|
|
fs.mkdirSync(path.join(root, "agents"), { recursive: true });
|
|
fs.writeFileSync(path.join(root, ".mcp.json"), "{}");
|
|
fs.writeFileSync(path.join(root, ".lsp.json"), "{}");
|
|
fs.writeFileSync(path.join(root, "settings.json"), "{}");
|
|
}
|
|
|
|
it("reduces fs.existsSync calls when detect + load run under one scan cache", () => {
|
|
const root = makeTempDir("claude-bundle-");
|
|
buildClaudeBundlePlugin(root);
|
|
|
|
// Run a detect + load pair and count fs.existsSync calls under a spy.
|
|
const detectAndLoad = (): void => {
|
|
const format = detectBundleManifestFormat(root);
|
|
if (format) {
|
|
loadBundleManifest({ rootDir: root, bundleFormat: format });
|
|
}
|
|
};
|
|
const countExistsSyncCalls = (run: () => void): number => {
|
|
const spy = vi.spyOn(fs, "existsSync");
|
|
try {
|
|
run();
|
|
return spy.mock.calls.length;
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
};
|
|
|
|
// Baseline: no scan cache → detect and load re-probe the same marker paths.
|
|
const uncachedCalls = countExistsSyncCalls(detectAndLoad);
|
|
// Same workload, but the detect + load pair shares one scan cache.
|
|
const cachedCalls = countExistsSyncCalls(() => withPluginScanExistenceCache(detectAndLoad));
|
|
|
|
// Both paths must still produce a valid claude bundle manifest.
|
|
const manifest = withPluginScanExistenceCache(() => {
|
|
const format = detectBundleManifestFormat(root);
|
|
return format ? loadBundleManifest({ rootDir: root, bundleFormat: format }) : null;
|
|
});
|
|
expect(manifest?.ok).toBe(true);
|
|
|
|
// The cache must eliminate the redundant probes; assert a real reduction
|
|
// rather than an exact count so the test is robust to marker-set changes.
|
|
expect(cachedCalls).toBeLessThan(uncachedCalls);
|
|
expect(cachedCalls).toBeGreaterThan(0);
|
|
});
|
|
});
|