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.
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
/** Scan-scoped existence cache for plugin discovery hot paths.
|
|
*
|
|
* Plugin metadata is process-stable: installs, manifests, and catalogs change
|
|
* only on restart or an explicit owner reload/install/doctor flow (see
|
|
* AGENTS.md). A single cold-start discovery scan still re-probes the same paths
|
|
* many times — `detectBundleManifestFormat` checks `skills/`, `.mcp.json`,
|
|
* `settings.json`, ... and `loadBundleManifest`'s capability builders check
|
|
* them again. Across bundled plugins that is thousands of synchronous
|
|
* `fs.existsSync` calls; the issue reports 25.4s of self-time on Windows cold
|
|
* start.
|
|
*
|
|
* This memoizes existence results for the lifetime of ONE scan pass only. A
|
|
* later install/repair pass runs without an active cache (or under a fresh
|
|
* cache), so marker files that appear mid-process are never served stale — the
|
|
* freshness bug a process-global cache would reintroduce. Outside a scan,
|
|
* `pluginScanExistsSync` falls back to plain `fs.existsSync`, so one-off
|
|
* callers (install, hooks, doctor) stay correct and uncached. */
|
|
import fs from "node:fs";
|
|
|
|
// Stack so nested wrapped scans get isolated caches and always pop on exit.
|
|
// Discovery scans are synchronous, so a single active cache is safe; an async
|
|
// scan would need its own scope rather than sharing this module state.
|
|
const scanExistenceCacheStack: Map<string, boolean>[] = [];
|
|
|
|
/** Runs `fn` with a scan-scoped existence cache active. Sync-only. */
|
|
export function withPluginScanExistenceCache<T>(fn: () => T): T {
|
|
scanExistenceCacheStack.push(new Map());
|
|
try {
|
|
return fn();
|
|
} finally {
|
|
scanExistenceCacheStack.pop();
|
|
}
|
|
}
|
|
|
|
/** `fs.existsSync` memoized for the active scan pass, if any.
|
|
*
|
|
* Outside `withPluginScanExistenceCache` this is plain `fs.existsSync`, so
|
|
* callers that are not part of a scan pay no caching cost or staleness. */
|
|
export function pluginScanExistsSync(targetPath: string): boolean {
|
|
const cache = scanExistenceCacheStack[scanExistenceCacheStack.length - 1];
|
|
if (!cache) {
|
|
return fs.existsSync(targetPath);
|
|
}
|
|
const cached = cache.get(targetPath);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
const result = fs.existsSync(targetPath);
|
|
cache.set(targetPath, result);
|
|
return result;
|
|
}
|