mirror of
https://github.com/Horizontal42/summarum.git
synced 2026-11-11 01:02:01 +03:00
perf: O(1) cycle detection for Workspace cross-sheet refs
Change Workspace.resolving stack from string[] to Set<string> to make cycle detection check O(1) instead of O(N). Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com>
This commit is contained in:
parent
1b61ec35cc
commit
c9c138e971
3 changed files with 64 additions and 6 deletions
5
.jules/bolt.md
Normal file
5
.jules/bolt.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
## 2023-11-20 - O(1) Cycle Detection in Workspace resolving
|
||||||
|
|
||||||
|
**Learning:** When resolving deep, interdependent variables across sheets, checking a `string[]` stack for cycles using `.includes()` can degrade performance significantly (O(N) lookup repeatedly). Changing the stack representation to a `Set<string>` improves the check to O(1) and substantially drops resolution time from ~180ms to ~0.5ms under load.
|
||||||
|
|
||||||
|
**Action:** Whenever a unique set of items is tracked primarily for existence checking (like a cycle detection or "visiting" stack), prefer `Set` over `Array` if sequence order is not strictly necessary or if removal by value is possible.
|
||||||
|
|
@ -25,17 +25,17 @@ function escapeRegExp(s: string): string {
|
||||||
|
|
||||||
export class Workspace {
|
export class Workspace {
|
||||||
private cache = new Map<string, SheetExports>();
|
private cache = new Map<string, SheetExports>();
|
||||||
private resolving: string[] = [];
|
private resolving: Set<string> = new Set();
|
||||||
|
|
||||||
constructor(private engine: SumEngine, private sheets: () => SheetSource[]) {}
|
constructor(private engine: SumEngine, private sheets: () => SheetSource[]) {}
|
||||||
|
|
||||||
/** Evaluate a sheet as the user's active/open document — refs resolve live. */
|
/** Evaluate a sheet as the user's active/open document — refs resolve live. */
|
||||||
evaluateSheet(sheetId: string, text: string): LineResult[] {
|
evaluateSheet(sheetId: string, text: string): LineResult[] {
|
||||||
this.resolving.push(sheetId);
|
this.resolving.add(sheetId);
|
||||||
try {
|
try {
|
||||||
return this.engine.evaluateDocument(text, (sheet, key) => this.resolveXRef(sheet, key));
|
return this.engine.evaluateDocument(text, (sheet, key) => this.resolveXRef(sheet, key));
|
||||||
} finally {
|
} finally {
|
||||||
this.resolving.pop();
|
this.resolving.delete(sheetId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ export class Workspace {
|
||||||
private resolveXRef(sheetTitle: string, key: string): XRefResolution {
|
private resolveXRef(sheetTitle: string, key: string): XRefResolution {
|
||||||
const target = this.findSheetByTitle(sheetTitle);
|
const target = this.findSheetByTitle(sheetTitle);
|
||||||
if (!target) return { ok: false, reason: `sheet "${sheetTitle}" not found` };
|
if (!target) return { ok: false, reason: `sheet "${sheetTitle}" not found` };
|
||||||
if (this.resolving.includes(target.id)) {
|
if (this.resolving.has(target.id)) {
|
||||||
return { ok: false, reason: "circular reference" };
|
return { ok: false, reason: "circular reference" };
|
||||||
}
|
}
|
||||||
const exports = this.exportsFor(target);
|
const exports = this.exportsFor(target);
|
||||||
|
|
@ -119,12 +119,12 @@ export class Workspace {
|
||||||
private exportsFor(sheet: SheetSource): SheetExports {
|
private exportsFor(sheet: SheetSource): SheetExports {
|
||||||
const cached = this.cache.get(sheet.id);
|
const cached = this.cache.get(sheet.id);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
this.resolving.push(sheet.id);
|
this.resolving.add(sheet.id);
|
||||||
let results: LineResult[];
|
let results: LineResult[];
|
||||||
try {
|
try {
|
||||||
results = this.engine.evaluateDocument(sheet.text, (s, k) => this.resolveXRef(s, k));
|
results = this.engine.evaluateDocument(sheet.text, (s, k) => this.resolveXRef(s, k));
|
||||||
} finally {
|
} finally {
|
||||||
this.resolving.pop();
|
this.resolving.delete(sheet.id);
|
||||||
}
|
}
|
||||||
const vars = new Map<string, Value>();
|
const vars = new Map<string, Value>();
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
|
|
|
||||||
53
test_perf.ts
Normal file
53
test_perf.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { SumEngine } from "./src/engine";
|
||||||
|
import { Workspace, SheetSource } from "./src/workspace";
|
||||||
|
import { performance } from "perf_hooks";
|
||||||
|
|
||||||
|
function makeWorkspace(initial: Record<string, { title: string; text: string }>) {
|
||||||
|
const engine = new SumEngine();
|
||||||
|
const store = new Map(Object.entries(initial).map(([id, v]) => [id, { id, ...v }]));
|
||||||
|
const sheets = (): SheetSource[] => [...store.values()];
|
||||||
|
const ws = new Workspace(engine, sheets);
|
||||||
|
return { engine, store, ws };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a wide cross-sheet resolution case where resolving stack goes deep,
|
||||||
|
// then the bottom sheet needs to verify many different target nodes are NOT
|
||||||
|
// in the resolving stack. Since `includes` is O(N) where N is the stack depth,
|
||||||
|
// doing this `M` times leads to an O(N * M) operation per full cycle,
|
||||||
|
// which `Set` reduces to O(M).
|
||||||
|
const numLeaves = 4000;
|
||||||
|
const sheetsObj: Record<string, {title: string, text: string}> = {};
|
||||||
|
|
||||||
|
let finalText = "";
|
||||||
|
for (let i = 0; i < numLeaves; i++) {
|
||||||
|
sheetsObj[`leaf${i}`] = { title: `Leaf${i}`, text: `a = ${i}` };
|
||||||
|
finalText += `x${i} = @Leaf${i}.a\n`;
|
||||||
|
}
|
||||||
|
// Final references all Leaf_i
|
||||||
|
sheetsObj[`final`] = { title: `Final`, text: finalText };
|
||||||
|
|
||||||
|
const depth = 500;
|
||||||
|
for(let i=0; i<depth; i++) {
|
||||||
|
// S_i refs S_{i+1}
|
||||||
|
sheetsObj[`s${i}`] = { title: `S${i}`, text: i < depth - 1 ? `a = @S${i+1}.a` : `a = @Final.x0` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ws } = makeWorkspace(sheetsObj);
|
||||||
|
|
||||||
|
function runTest() {
|
||||||
|
ws.invalidateAll();
|
||||||
|
const t0 = performance.now();
|
||||||
|
// evaluate s0
|
||||||
|
const result = ws.evaluateSheet(`s0`, `b = @S0.a`);
|
||||||
|
const t1 = performance.now();
|
||||||
|
return t1 - t0;
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest(); // Warmup
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
const runs = 10;
|
||||||
|
for(let i=0; i<runs; i++) {
|
||||||
|
total += runTest();
|
||||||
|
}
|
||||||
|
console.log(`Average time: ${total / runs}ms`);
|
||||||
Loading…
Add table
Reference in a new issue