diff --git a/gitnexus/bench/cfg/baselines.json b/gitnexus/bench/cfg/baselines.json index 606ccd19e..7676e8919 100644 --- a/gitnexus/bench/cfg/baselines.json +++ b/gitnexus/bench/cfg/baselines.json @@ -29,8 +29,16 @@ "scaling_budget": 1.8, "disk_bytes_budget": 1.2, "heap_budget": 1.3, - "rd_scaling_budget": 10.0, - "_note": "#2082 M2: N bindings live across ~N blocks in one loop -- bindings x blocks scale JOINTLY (the solver-lattice stressor). The overlay design measures rd ~5.2 normalized: the OUT spine copy on genning blocks is O(V) per block, which is quadratic when V scales with B (bounded in prod by maxFunctionLines; real functions have V~10-40). Budget 10 deliberately tolerates that known shape and exists to catch the repo's recurring per-item-rescan class (a per-use scan over all defs is O(n^3) here, ratio >=16). If rd drops well below 5, tighten." + "rd_scaling_budget": 2.0, + "_note": "#2082 M2 / #2201 SSA: N bindings live across ~N blocks in one loop -- bindings x blocks scale JOINTLY (the solver-lattice stressor). The dense GEN/KILL worklist measured rd ~5.2 normalized here (the OUT spine copy is O(V) per block, quadratic when V scales with B). The #2201 SSA-sparse solver answers each use's reaching set from the def-use graph WITHOUT a per-block dense lattice, dropping rd to ~0.86 (linear; measured 5-23x faster absolute). Budget tightened 10->2: still absorbs noise + catches a regression to the per-item-rescan class (a per-use scan over all defs is O(n^3) here, ratio >=16), but now also catches a fall-back to the dense quadratic. Fingerprint unchanged -- CFG construction is untouched." + }, + "deep-nest": { + "fingerprint": "c0ca870487abc6ff379304c3162003e9e4f9b44aeb2fc29adfcf8d2179c7613a", + "scaling_budget": 1.8, + "disk_bytes_budget": 1.2, + "rd_scaling_budget": 2.0, + "facts_large_min": 100, + "_note": "#2201: N nested loops carrying ONE variable end-to-end (depth 40->160) -- the pathology the dense worklist is superlinear on and whose block-visit total drives it past the blocks×64 ceiling (it would TRUNCATE to empty). rd is measured under the PRODUCTION blocks×64 budget (rdProductionBudget) to prove the ceiling stops firing: the depth-INDEPENDENT SSA solver (phi-nodes capture loop merges statically; no fixpoint iteration) computes the full facts (164 at large; floor 100 catches a regression to truncation/zero) with rd_scaling ~0.90 (linear in depth; measured ~0.42ms at depth 160). rd_scaling_budget 2.0 catches a regression back to superlinear. No heap_budget -- the deep-nest CFG payload is tiny and the retained-heap delta is GC-noise-dominated. Re-baseline the fingerprint only on an intentional CFG/visitor change." }, "fact-fanout": { "fingerprint": "83a8243a8aff117f69aeecb39d02a483e6cca70439d75f63e433f4e4ac85578f", diff --git a/gitnexus/bench/cfg/measure.mjs b/gitnexus/bench/cfg/measure.mjs index efc0661a8..598b8a73e 100644 --- a/gitnexus/bench/cfg/measure.mjs +++ b/gitnexus/bench/cfg/measure.mjs @@ -44,7 +44,10 @@ import { fileURLToPath } from 'node:url'; import Parser from 'tree-sitter'; import { collectFunctionCfgs } from '../../src/core/ingestion/cfg/collect.ts'; import { computeReachingDefs } from '../../src/core/ingestion/cfg/reaching-defs.ts'; -import { DEFAULT_PDG_MAX_REACHING_DEF_FACTS_PER_FUNCTION } from '../../src/core/ingestion/cfg/emit.ts'; +import { + DEFAULT_PDG_MAX_REACHING_DEF_FACTS_PER_FUNCTION, + DEFAULT_PDG_MAX_REACHING_DEF_BLOCK_REVISITS, +} from '../../src/core/ingestion/cfg/emit.ts'; import { getTreeSitterBufferSize } from '../../src/core/ingestion/constants.ts'; import { getLanguageGrammar } from '../../src/core/tree-sitter/parser-loader.ts'; import { getProvider } from '../../src/core/ingestion/languages/index.ts'; @@ -172,6 +175,29 @@ const SCENARIOS = [ return s + ' c = c - 1;\n }\n return v0;\n}\n'; }, }, + { + name: 'deep-nest', + // #2201: N nested loops carrying one variable end-to-end — the pathology the + // dense GEN/KILL worklist is superlinear on and that drives its block-visit + // total past the blocks×64 ceiling (it would truncate to an empty result). + // The production SSA solver is depth-INDEPENDENT (φ-nodes capture the loop + // merges statically; no fixpoint iteration), so rd time scales ~linearly + // with depth and the ceiling never fires. Two gates: rd_scaling_budget + // catches a regression back to superlinear, and facts_large_min asserts the + // solver still COMPUTES full facts under the PRODUCTION blocks×64 budget + // (rdProductionBudget) — a dense worklist would report zero facts here. + small: 40, + large: 160, // 4×, well under the visitor's recursive-nesting depth guard + rdMaxFacts: 0, // measure the algorithm, not the cap + rdProductionBudget: true, // pass blocks×64 — the SSA solver must still compute + gen: (n) => { + let s = 'function f(c: number) {\n let x = 0;\n'; + for (let i = 0; i < n; i++) s += ' '.repeat(i + 1) + `while (c > ${i}) {\n`; + s += ' '.repeat(n + 1) + 'x = x + 1;\n'; + for (let i = n - 1; i >= 0; i--) s += ' '.repeat(i + 1) + '}\n'; + return s + ' return x;\n}\n'; + }, + }, { name: 'fact-fanout', // #2082 M2: N parallel case-arm defs of one variable + N later uses — @@ -296,17 +322,32 @@ function measureCollect(tk, src, file, reps) { // the scope-resolution emit loop adds per file on a --pdg run). `maxFacts` // mirrors the per-scenario production posture: 0 (unlimited) measures the // algorithm; the production default exercises the boundedness contract. -function measureReachingDefs(cfgs, reps, maxFacts) { - for (const c of cfgs) computeReachingDefs(c, { maxFacts }); // warm JIT +// When `blockVisitsMul` > 0 each call also passes the PRODUCTION per-function +// maxBlockVisits budget (blocks × mul). On the deep-nest scenario this is how +// "the ceiling stops firing" (#2201) is measured: the dense worklist would +// truncate to an empty result under this budget, whereas the production SSA +// solver computes the full facts — so a nonzero `facts` under the budget is the +// gate (see facts_large_min in baselines.json). +function measureReachingDefs(cfgs, reps, maxFacts, blockVisitsMul = 0) { + const limitsFor = (c) => + blockVisitsMul > 0 + ? { maxFacts, maxBlockVisits: c.blocks.length * blockVisitsMul } + : { maxFacts }; + for (const c of cfgs) computeReachingDefs(c, limitsFor(c)); // warm JIT const samples = []; let facts = 0; + let allComputed = true; for (let i = 0; i < reps; i++) { const start = process.hrtime.bigint(); facts = 0; - for (const c of cfgs) facts += computeReachingDefs(c, { maxFacts }).facts.length; + for (const c of cfgs) { + const r = computeReachingDefs(c, limitsFor(c)); + facts += r.facts.length; + if (r.status !== 'computed') allComputed = false; + } samples.push(Number(process.hrtime.bigint() - start) / 1e6); } - return { ms: median(samples), facts }; + return { ms: median(samples), facts, allComputed }; } // ---- taint pass cost (#2083 M3 U7) ---- @@ -441,10 +482,14 @@ function measureScenario(scenario) { ? heapLarge / heapSmall / sizeRatio : null; - // #2082 M2: reaching-defs solve cost over the same CFGs. + // #2082 M2: reaching-defs solve cost over the same CFGs. #2201: scenarios + // marked `rdProductionBudget` also pass the per-function blocks×64 ceiling, to + // prove the production SSA solver still COMPUTES where the dense worklist would + // truncate (the deep-nest ceiling-stops-firing acceptance). const rdMaxFacts = scenario.rdMaxFacts ?? 0; - const rdSmall = measureReachingDefs(small.cfgs, REPS, rdMaxFacts); - const rdLarge = measureReachingDefs(large.cfgs, REPS, rdMaxFacts); + const rdBudgetMul = scenario.rdProductionBudget ? DEFAULT_PDG_MAX_REACHING_DEF_BLOCK_REVISITS : 0; + const rdSmall = measureReachingDefs(small.cfgs, REPS, rdMaxFacts, rdBudgetMul); + const rdLarge = measureReachingDefs(large.cfgs, REPS, rdMaxFacts, rdBudgetMul); // Clamp the denominator: a 0.000ms small-N median would otherwise yield // ratio 0 and the gate would self-disable exactly when the solver is fast. const rdRatio = rdLarge.ms / Math.max(rdSmall.ms, 0.001) / sizeRatio; @@ -506,6 +551,7 @@ function measureScenario(scenario) { rd_scaling_ratio: Number(rdRatio.toFixed(3)), facts_small: rdSmall.facts, facts_large: rdLarge.facts, + rd_all_computed: rdLarge.allComputed, ...fingerprint(tk, scenario), }; } @@ -570,6 +616,16 @@ if (!CHECK) { `(the maxFacts early-stop is the boundedness contract)`, ); } + // #2201 deep-nest: under the PRODUCTION blocks×64 budget the SSA solver must + // still COMPUTE full facts (a nonzero floor) where the dense worklist would + // truncate to empty — "the ceiling stops firing". + if (base.facts_large_min !== undefined && r.facts_large < base.facts_large_min) { + failures.push( + `${r.scenario}: only ${r.facts_large} facts < floor ${base.facts_large_min} under the ` + + `production block-visit budget — the ceiling fired (SSA should not truncate here)` + + (r.rd_all_computed ? '' : ` [status != computed]`), + ); + } if (base.disk_bytes_large_max !== undefined && r.disk_bytes_large > base.disk_bytes_large_max) { failures.push( `${r.scenario}: cfgSideChannel absolute size ${r.disk_bytes_large} > ceiling ` +