perf(rules-injector): cache full candidates and memoize ancestor scans
The rule scan cache stored a path[] keyed by (projectRoot|startDir| skipClaudeUserRules), so two issues stacked up on every tracked tool call: - Cache hits still ran safeRealpathSync(realpathSync) and re-derived isGlobal / distance / isSingleFile for every cached path. That is a per-candidate sync syscall plus repeated string-prefix walks. - Sibling files in the same project landed under different startDir keys, so the entire walk-and-recursive-scan chain repeated even though every ancestor rule directory was identical. Store the full RuleFileCandidate[] in the per-call cache so a cache hit returns immediately with no realpath syscall. Add a separate per- directory scan cache (getDirScan/setDirScan) keyed by absolute rule directory path, so two sibling files reuse the same readdir + realpath work for every shared ancestor. Microbench (200 files / 20 modules / cached session): - single sweep: 41.8ms -> 2.5ms (16x) - 3-pass replay: 88.6ms -> 3.2ms (28x) Pin the new invariants with two new tests: - 'does not re-resolve symlinked rule path on cache hit' via a retargeted directory symlink. - 'reuses ancestor directory scan for sibling files in the same project' by deleting the source rule file between the two calls.
This commit is contained in:
@@ -1,21 +1,38 @@
|
||||
import type { RuleFileCandidate } from "./types";
|
||||
|
||||
export type DirectoryScanEntry = {
|
||||
path: string;
|
||||
realPath: string;
|
||||
};
|
||||
|
||||
export type RuleScanCache = {
|
||||
get: (key: string) => string[] | undefined;
|
||||
set: (key: string, value: string[]) => void;
|
||||
get: (key: string) => RuleFileCandidate[] | undefined;
|
||||
set: (key: string, value: RuleFileCandidate[]) => void;
|
||||
getDirScan: (dir: string) => DirectoryScanEntry[] | undefined;
|
||||
setDirScan: (dir: string, entries: DirectoryScanEntry[]) => void;
|
||||
clear: () => void;
|
||||
};
|
||||
|
||||
export function createRuleScanCache(): RuleScanCache {
|
||||
const cache = new Map<string, string[]>();
|
||||
const finalResultCache = new Map<string, RuleFileCandidate[]>();
|
||||
const directoryScanCache = new Map<string, DirectoryScanEntry[]>();
|
||||
|
||||
return {
|
||||
get(key: string): string[] | undefined {
|
||||
return cache.get(key);
|
||||
get(key: string): RuleFileCandidate[] | undefined {
|
||||
return finalResultCache.get(key);
|
||||
},
|
||||
set(key: string, value: string[]): void {
|
||||
cache.set(key, value);
|
||||
set(key: string, value: RuleFileCandidate[]): void {
|
||||
finalResultCache.set(key, value);
|
||||
},
|
||||
getDirScan(dir: string): DirectoryScanEntry[] | undefined {
|
||||
return directoryScanCache.get(dir);
|
||||
},
|
||||
setDirScan(dir: string, entries: DirectoryScanEntry[]): void {
|
||||
directoryScanCache.set(dir, entries);
|
||||
},
|
||||
clear(): void {
|
||||
cache.clear();
|
||||
finalResultCache.clear();
|
||||
directoryScanCache.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user