bench(cfg): deep-nest scenario + tighten dense-bindings rd budget 10->2 (#2201 U7)

dense-bindings rd_scaling drops 5.2->0.86 (SSA linear); budget tightened to 2.0.
New deep-nest scenario (N nested loops, one carried var) measures rd under the
production blocks×64 ceiling and asserts the SSA solver still COMPUTES full
facts (facts_large_min) where the dense worklist would truncate — the
ceiling-stops-firing acceptance. CFG fingerprints unchanged.
This commit is contained in:
Gergo Magyar 2026-06-15 14:47:39 +00:00
parent 0a8dbd43d5
commit 101738113b
2 changed files with 74 additions and 10 deletions

View file

@ -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",

View file

@ -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 ` +