diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..1e7e508 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2025-02-18 - Repeated Regex Execution Caching + **Learning:** In recursive or looping traversal like graph cycle detection (e.g. invalidation of multiple sheets), running heavy Regex (`matchAll`) repeatedly on the exact same text is extremely slow and compounds exponentially. + **Action:** Introduce text-based short-circuiting caches (e.g., matching the original `text` property against the cached payload string) where text extraction logic like Regex or parsing runs purely on deterministic inputs. diff --git a/src/workspace.ts b/src/workspace.ts index c387051..43b7645 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -26,6 +26,7 @@ function escapeRegExp(s: string): string { export class Workspace { private cache = new Map(); private resolving: string[] = []; + private parsedRefsCache = new Map(); constructor(private engine: SumEngine, private sheets: () => SheetSource[]) {} @@ -47,7 +48,7 @@ export class Workspace { changed = false; for (const s of this.sheets()) { if (dirty.has(s.id)) continue; - for (const title of this.referencedTitles(s.text)) { + for (const title of this.referencedTitles(s.text, s.id)) { const target = this.findSheetByTitle(title); if (target && dirty.has(target.id)) { dirty.add(s.id); @@ -62,6 +63,7 @@ export class Workspace { invalidateAll(): void { this.cache.clear(); + this.parsedRefsCache.clear(); } /** @@ -83,9 +85,16 @@ export class Workspace { return out; } - private referencedTitles(text: string): string[] { + private referencedTitles(text: string, id?: string): string[] { + if (id) { + const cached = this.parsedRefsCache.get(id); + if (cached && cached.text === text) return cached.refs; + } const out: string[] = []; for (const m of text.matchAll(XREF_SCAN_RE)) out.push((m[1] ?? m[2]).trim()); + if (id) { + this.parsedRefsCache.set(id, { text, refs: out }); + } return out; }