mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* fix(scope-resolution): parse def coordinates after file paths Anchor coordinate parsing to the known file path so coordinate-like path fragments and private symbol names cannot corrupt closure attribution. * fix(bench): use production definition ids
241 lines
9.4 KiB
JavaScript
241 lines
9.4 KiB
JavaScript
/**
|
|
* Build-free throughput + identity bench for `buildGraphTargetIndex`, the
|
|
* callable-value-flow target index (issue #2693).
|
|
*
|
|
* #2693 widened this function's gate: before it, only Function/Method/
|
|
* Constructor defs were considered; now VALUE bindings (Const/Property/Static/
|
|
* Variable) are considered too, because a closure bound to a name declares as a
|
|
* value but emits a callable graph node (#2687). Value bindings usually
|
|
* OUTNUMBER callables in real source, so the widening puts the hot loop's cost
|
|
* on a much larger def population — this bench exists to keep that honest.
|
|
*
|
|
* Value bindings are joined to their callable node POSITIONALLY
|
|
* (`file\0line\0name`); they never run the `resolveDefGraphId` key chain,
|
|
* whose label-agnostic `simpleKey` fallback would alias a binding onto any
|
|
* same-named callable in the file.
|
|
*
|
|
* For a synthetic corpus at two scales it reports:
|
|
* - elapsed_ms_small / elapsed_ms_large (fastest of REPS, see `fastest`) + a scaling ratio
|
|
* `(t_large/t_small)/(LARGE/SMALL)`: ~1.0 linear, ~3.x quadratic;
|
|
* - `callable_only_ms_large`, the same corpus with the PRE-#2693 def
|
|
* population, so the cost the widening actually added stays visible as
|
|
* `widening_overhead` rather than being folded into one opaque number;
|
|
* - an order-independent sha256 fingerprint over every (defNodeId → graphId)
|
|
* pair the index resolves, as the correctness gate. A fingerprint change
|
|
* means the set of callable-value targets moved — that is a behaviour
|
|
* change, never a performance one.
|
|
*
|
|
* Build-free: imports the `.ts` hotpaths through tsx
|
|
* (`node --import tsx bench/callable-value-flow/measure.mjs`). Static `.ts`
|
|
* imports work; a top-level `await import()` breaks tsx's lexer.
|
|
*
|
|
* Without args: prints one JSON object per scale plus the summary.
|
|
* With `--check`: asserts the fingerprint == the committed baseline AND both
|
|
* the scaling ratio and the widening overhead are within their recorded
|
|
* budgets; exits non-zero on drift/regression.
|
|
*/
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { createKnowledgeGraph } from '../../src/core/graph/graph.ts';
|
|
import { buildGraphNodeLookup } from '../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts';
|
|
import { buildGraphTargetIndex } from '../../src/core/ingestion/scope-resolution/passes/callable-value-flow.ts';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const BASELINE_PATH = path.resolve(__dirname, 'baselines.json');
|
|
|
|
const SMALL = 250;
|
|
const LARGE = 800;
|
|
const REPS = 15;
|
|
const WARMUP = 5;
|
|
|
|
/**
|
|
* Deterministic synthetic corpus — no randomness, so the fingerprint is stable.
|
|
*
|
|
* Per file: 2 free functions, 1 class with 2 methods, and 8 value bindings. Of
|
|
* those 8, ONE is a closure binding: it declares as a value but its only graph
|
|
* node is a `Function` (exactly what #2687 emits, and the sole case the widened
|
|
* gate is meant to admit). The other 7 keep their own value node, so they must
|
|
* be REJECTED — they are the population whose cost the widening added.
|
|
*
|
|
* The 7:1 reject:admit ratio is the point: the loop must reject seven bindings
|
|
* cheaply for every one it admits. The closure binding's callable node sits at
|
|
* the SAME line as its def, which is what the positional join keys on; the
|
|
* seven others have their own value node at their own line and must not be
|
|
* admitted by any name coincidence.
|
|
*/
|
|
function buildCorpus(fileCount) {
|
|
const graph = createKnowledgeGraph();
|
|
const defs = new Map();
|
|
|
|
// `line` is 1-based (the convention definition ids use); graph nodes store a
|
|
// 0-BASED startLine, and the positional join in buildGraphTargetIndex is what
|
|
// reconciles the two. Modelling that off by one here would silently stop the
|
|
// bench from exercising the value-binding path at all.
|
|
const addNode = (label, filePath, qualifiedName, line) => {
|
|
const id = `${label}:${filePath}:${qualifiedName}`;
|
|
graph.addNode({
|
|
id,
|
|
label,
|
|
properties: {
|
|
filePath,
|
|
name: qualifiedName.split('.').pop(),
|
|
qualifiedName,
|
|
startLine: line - 1,
|
|
},
|
|
});
|
|
return id;
|
|
};
|
|
const addDef = (type, filePath, qualifiedName, line) => {
|
|
const nodeId = `def:${filePath}#${line}:0:${type}:${qualifiedName}`;
|
|
defs.set(nodeId, { nodeId, type, filePath, qualifiedName });
|
|
};
|
|
|
|
for (let f = 0; f < fileCount; f++) {
|
|
const filePath = `src/module${f}/file${f}.ts`;
|
|
let line = 1;
|
|
|
|
for (let i = 0; i < 2; i++, line++) {
|
|
addNode('Function', filePath, `fn${i}`, line);
|
|
addDef('Function', filePath, `fn${i}`, line);
|
|
}
|
|
|
|
addNode('Class', filePath, `Cls`, line);
|
|
for (let i = 0; i < 2; i++, line++) {
|
|
addNode('Method', filePath, `Cls.m${i}`, line);
|
|
addDef('Method', filePath, `Cls.m${i}`, line);
|
|
}
|
|
|
|
// 1 closure binding: value def, callable node, NO value node.
|
|
addNode('Function', filePath, `handler`, line);
|
|
addDef('Const', filePath, `handler`, line);
|
|
line++;
|
|
|
|
// 7 ordinary value bindings: value def AND its own value node → rejected.
|
|
const valueLabels = [
|
|
'Const',
|
|
'Variable',
|
|
'Property',
|
|
'Static',
|
|
'Const',
|
|
'Variable',
|
|
'Property',
|
|
];
|
|
for (let i = 0; i < valueLabels.length; i++, line++) {
|
|
const label = valueLabels[i];
|
|
addNode(label, filePath, `value${i}`, line);
|
|
addDef(label, filePath, `value${i}`, line);
|
|
}
|
|
}
|
|
|
|
return { graph, scopes: { defs: { byId: defs } }, nodeLookup: buildGraphNodeLookup(graph) };
|
|
}
|
|
|
|
/** Only the pre-#2693 def population, for the overhead comparison. */
|
|
function callableOnlyScopes(scopes) {
|
|
const byId = new Map();
|
|
for (const [id, def] of scopes.defs.byId) {
|
|
if (def.type === 'Function' || def.type === 'Method' || def.type === 'Constructor') {
|
|
byId.set(id, def);
|
|
}
|
|
}
|
|
return { defs: { byId } };
|
|
}
|
|
|
|
/**
|
|
* MIN, not median. Both scales are timed in one process, and every source of
|
|
* error here is additive — scheduler preemption, GC, a noisy neighbour on a
|
|
* shared CI runner. The fastest observed run is the closest estimate of the
|
|
* uncontended cost, so the derived ratios stay comparable across machines
|
|
* instead of tracking whatever else the box was doing. (Measured directly: the
|
|
* same build reported an overhead of 1.65 idle and 2.03 while a test shard was
|
|
* running — a median-based gate would have to be loosened until it could no
|
|
* longer detect the regression it exists to catch.)
|
|
*/
|
|
function fastest(values) {
|
|
return Math.min(...values);
|
|
}
|
|
|
|
function timeIndex(scopes, nodeLookup, graph) {
|
|
// Warm up before timing: the first calls carry JIT compilation of the whole
|
|
// resolve chain, and the widened and callable-only runs would otherwise be
|
|
// measured at different optimisation tiers — which alone moved the reported
|
|
// overhead by ~30%.
|
|
for (let w = 0; w < WARMUP; w++) buildGraphTargetIndex(scopes, nodeLookup, undefined, graph);
|
|
const samples = [];
|
|
let last;
|
|
for (let r = 0; r < REPS; r++) {
|
|
const t0 = performance.now();
|
|
last = buildGraphTargetIndex(scopes, nodeLookup, undefined, graph);
|
|
samples.push(performance.now() - t0);
|
|
}
|
|
return { ms: fastest(samples), result: last };
|
|
}
|
|
|
|
function fingerprint(targets) {
|
|
const lines = [...targets.entries()].map(([defId, t]) => `${defId}\u0000${t.id}`).sort();
|
|
return crypto.createHash('sha256').update(lines.join('\n')).digest('hex');
|
|
}
|
|
|
|
const scales = {};
|
|
for (const [name, fileCount] of [
|
|
['small', SMALL],
|
|
['large', LARGE],
|
|
]) {
|
|
const { graph, scopes, nodeLookup } = buildCorpus(fileCount);
|
|
const widened = timeIndex(scopes, nodeLookup, graph);
|
|
const callableOnly = timeIndex(callableOnlyScopes(scopes), nodeLookup, graph);
|
|
scales[name] = {
|
|
files: fileCount,
|
|
defs: scopes.defs.byId.size,
|
|
ms: widened.ms,
|
|
callable_only_ms: callableOnly.ms,
|
|
targets: widened.result.size,
|
|
callable_only_targets: callableOnly.result.size,
|
|
fingerprint: fingerprint(widened.result),
|
|
};
|
|
}
|
|
|
|
const scalingRatio = scales.large.ms / scales.small.ms / (LARGE / SMALL);
|
|
// How much slower the widened gate is than the pre-#2693 one on the same
|
|
// corpus. 1.0 = free; 2.0 = the widening doubled the index build.
|
|
const wideningOverhead = scales.large.ms / scales.large.callable_only_ms;
|
|
|
|
const report = {
|
|
small: scales.small,
|
|
large: scales.large,
|
|
scaling_ratio: Number(scalingRatio.toFixed(3)),
|
|
widening_overhead: Number(wideningOverhead.toFixed(3)),
|
|
fingerprint: scales.large.fingerprint,
|
|
};
|
|
|
|
if (!process.argv.includes('--check')) {
|
|
console.log(JSON.stringify(report, null, 2));
|
|
process.exit(0);
|
|
}
|
|
|
|
const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf-8'));
|
|
const failures = [];
|
|
if (report.fingerprint !== baseline.fingerprint) {
|
|
failures.push(
|
|
`fingerprint drift: ${report.fingerprint} != ${baseline.fingerprint} — the resolved ` +
|
|
`callable-value target set CHANGED. This is a behaviour change, not a perf one.`,
|
|
);
|
|
}
|
|
if (report.scaling_ratio > baseline.scaling_budget) {
|
|
failures.push(`scaling ${report.scaling_ratio} > budget ${baseline.scaling_budget}`);
|
|
}
|
|
if (report.widening_overhead > baseline.widening_overhead_budget) {
|
|
failures.push(
|
|
`widening overhead ${report.widening_overhead} > budget ${baseline.widening_overhead_budget}`,
|
|
);
|
|
}
|
|
|
|
console.log(JSON.stringify(report, null, 2));
|
|
if (failures.length > 0) {
|
|
console.error(`[callable-value-flow --check] FAIL\n - ${failures.join('\n - ')}`);
|
|
process.exit(1);
|
|
}
|
|
console.log('[callable-value-flow --check] PASS');
|