mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-15 23:32:49 +00:00
Introduces `WorkspaceResolutionIndex` — a precomputed bundle of lookup tables built ONCE per resolution run, after `populateOwners` and after finalize, before any pass that needs to find members, exported defs, or class scopes by id. What it replaces (all are pre-existing O(N×D) linear scans of parsedFiles, called inside the receiver-bound MRO chain): - `findOwnedMember(ownerId, name, parsedFiles)` → `Map.get` via `index.memberByOwner.get(ownerId)?.get(name)`. Was the worst offender — receiver-bound dispatcher calls this O(sites × MRO depth) times. - `findExportedDef(filePath, name, parsedFiles)` → `Map.get` via `index.defsByFileAndName`. Hot for namespace-receiver case. - `findExportedDefByName` workspace-wide fallback scan → `Map.get` via `index.callablesBySimpleName`. - `classScopeByDefId` (rebuilt inside `emitReceiverBoundCalls` on every invocation) — moved to one-shot build during finalize, read from `index.classScopeByDefId` everywhere. - `moduleScopeByFile` (rebuilt inside `propagateImportedReturnTypes` on every invocation) — read from `index.moduleScopeByFile`. Findings from a synthetic 100-file Python workload (60 model files each defining 5 classes × 3 methods + 40 user files calling them heavily): scope-resolution wall time: 764ms → 710ms (median, 5 iters) That's a ~7% in-layer win. The smaller-than-expected gain was informative: profiling the synthetic workload shows scope-resolution breakdown is `extract=62% resolve=30% emit=4%`; the index touched the 4% slice (emit + walker calls inside it). Larger O(D) per owner classes will benefit more. Profiling the FULL pipeline (49 fixtures × 3 iters) shows scope-resolution accounts for ~1% of pipeline wall time — the remaining 99% is parse (tree-sitter), heritage, ORM, MRO, processes, and DB writes. So further optimization of this specific layer has marginal pipeline impact; the next-biggest wins live in those phases. Documented as the "double-parse" finding in the audit (captures.ts re-parses each Python file even though the parse phase already produced a tree-sitter Tree) — that's a separate plumbing project across phase boundaries. Bonus: opt-in PROF_SCOPE_RESOLUTION=1 env var prints a per-phase ms breakdown to stderr, so future perf work can measure without extra code changes. Verification: - REGISTRY_PRIMARY_PYTHON=0 (legacy): 191/191. - REGISTRY_PRIMARY_PYTHON=1 (registry): 191/191. - tsc --noEmit clean.
134 lines
4.8 KiB
TypeScript
134 lines
4.8 KiB
TypeScript
/**
|
||
* Synthetic benchmark for scope-resolution. Builds a large in-memory
|
||
* Python workspace and times runScopeResolution against it directly,
|
||
* isolating the resolution cost from parse / heritage / pipeline
|
||
* overhead.
|
||
*
|
||
* Usage: REGISTRY_PRIMARY_PYTHON=1 npx tsx scripts/bench-scope-resolution.ts
|
||
*/
|
||
process.env.REGISTRY_PRIMARY_PYTHON = '1';
|
||
|
||
import { generateId } from '../src/lib/utils.js';
|
||
import { createKnowledgeGraph } from '../src/core/graph/graph.js';
|
||
import { runScopeResolution } from '../src/core/ingestion/scope-resolution/index.js';
|
||
import { pythonScopeResolver } from '../src/core/ingestion/languages/python/scope-resolver.js';
|
||
|
||
const N_CLASSES = Number(process.env.BENCH_CLASSES ?? '60');
|
||
const N_USERS = Number(process.env.BENCH_USERS ?? '40');
|
||
const ITERS = Number(process.env.BENCH_ITERS ?? '5');
|
||
|
||
function buildWorkspace(): { path: string; content: string }[] {
|
||
const files: { path: string; content: string }[] = [];
|
||
|
||
// Build N_CLASSES "model" files, each defining a class with a few methods.
|
||
for (let i = 0; i < N_CLASSES; i++) {
|
||
const lines: string[] = [];
|
||
for (let j = 0; j < 5; j++) {
|
||
lines.push(`class Model${i}_${j}:`);
|
||
lines.push(` name: str`);
|
||
lines.push(` def save(self) -> bool:`);
|
||
lines.push(` return True`);
|
||
lines.push(` def update(self, name: str) -> "Model${i}_${j}":`);
|
||
lines.push(` self.name = name`);
|
||
lines.push(` return self`);
|
||
lines.push(` def get_other(self) -> "Model${i}_${(j + 1) % 5}":`);
|
||
lines.push(` return Model${i}_${(j + 1) % 5}()`);
|
||
lines.push('');
|
||
}
|
||
files.push({ path: `models/m${i}.py`, content: lines.join('\n') });
|
||
}
|
||
|
||
// Build N_USERS "user" files that import from a few model files
|
||
// and exercise the receiver-bound dispatcher heavily.
|
||
for (let u = 0; u < N_USERS; u++) {
|
||
const targets = [u % N_CLASSES, (u + 1) % N_CLASSES, (u + 2) % N_CLASSES];
|
||
const imports = targets
|
||
.map((t) => `from models.m${t} import Model${t}_0, Model${t}_1, Model${t}_2`)
|
||
.join('\n');
|
||
const calls: string[] = [];
|
||
for (let k = 0; k < 30; k++) {
|
||
const t = targets[k % 3]!;
|
||
const j = k % 3;
|
||
calls.push(` m${k} = Model${t}_${j}()`);
|
||
calls.push(` m${k}.save()`);
|
||
calls.push(` m${k}.update("x").save()`);
|
||
calls.push(` m${k}.get_other().save()`);
|
||
}
|
||
const content = `${imports}\n\ndef use_${u}() -> None:\n${calls.join('\n')}\n`;
|
||
files.push({ path: `app/u${u}.py`, content });
|
||
}
|
||
|
||
return files;
|
||
}
|
||
|
||
function buildGraph(files: { path: string; content: string }[]) {
|
||
const graph = createKnowledgeGraph();
|
||
// Pre-populate File / Class / Function nodes the resolver expects.
|
||
for (const f of files) {
|
||
const fileId = generateId('File', f.path);
|
||
graph.addNode({
|
||
id: fileId,
|
||
label: 'File',
|
||
properties: { name: f.path, filePath: f.path },
|
||
});
|
||
|
||
// Lightweight regex-extract class & def names so the lookup index
|
||
// has something to find. Real pipeline builds these via parse phase;
|
||
// for the bench this stand-in is enough to exercise the resolver.
|
||
const classRe = /^class (\w+)/gm;
|
||
const defRe = /^\s*def (\w+)/gm;
|
||
let m: RegExpExecArray | null;
|
||
while ((m = classRe.exec(f.content)) !== null) {
|
||
const name = m[1]!;
|
||
const id = generateId('Class', `${f.path}:${name}`);
|
||
graph.addNode({
|
||
id,
|
||
label: 'Class',
|
||
properties: { name, filePath: f.path, qualifiedName: name },
|
||
});
|
||
}
|
||
while ((m = defRe.exec(f.content)) !== null) {
|
||
const name = m[1]!;
|
||
const id = generateId('Function', `${f.path}:${name}`);
|
||
graph.addNode({
|
||
id,
|
||
label: 'Function',
|
||
properties: { name, filePath: f.path, qualifiedName: name },
|
||
});
|
||
}
|
||
}
|
||
return graph;
|
||
}
|
||
|
||
async function main() {
|
||
const files = buildWorkspace();
|
||
console.log(`bench: ${files.length} files (${N_CLASSES} models × 5 classes + ${N_USERS} users)`);
|
||
console.log(` × ${ITERS} iterations\n`);
|
||
|
||
// Warmup
|
||
for (let i = 0; i < 2; i++) {
|
||
const graph = buildGraph(files);
|
||
runScopeResolution({ graph, files, onWarn: () => {} }, pythonScopeResolver);
|
||
}
|
||
|
||
const samples: number[] = [];
|
||
for (let i = 0; i < ITERS; i++) {
|
||
const graph = buildGraph(files);
|
||
const start = process.hrtime.bigint();
|
||
runScopeResolution({ graph, files, onWarn: () => {} }, pythonScopeResolver);
|
||
const end = process.hrtime.bigint();
|
||
const ms = Number(end - start) / 1_000_000;
|
||
samples.push(ms);
|
||
console.log(` iter ${i + 1}: ${ms.toFixed(0)} ms`);
|
||
}
|
||
|
||
samples.sort((a, b) => a - b);
|
||
const median = samples[Math.floor(samples.length / 2)]!;
|
||
const min = samples[0]!;
|
||
console.log(`\nmin: ${min.toFixed(0)} ms · median: ${median.toFixed(0)} ms`);
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error(err);
|
||
process.exit(1);
|
||
});
|