GitNexus/gitnexus/bench/cfg/compare-guard-overhead.mjs
Gergő Magyar d8a256a03a
fix(taint): avoid false positives for guarded Git arguments (#3703)
* fix(taint): refine values after accepted guards

Preserve bounded value restrictions through local flows and function
summaries. Check the complete command against the modeled sink use
before suppressing a finding. Unknown paths and exhausted budgets
retain conservative findings.

Refs #3490

* fix(taint): preserve guard proof identities

Reject destructured/rest formals before they inherit a whole-argument
proof, and include transitive alias uses in bounded guard certificates.

Add worker regressions, argument-heavy scaling coverage and global
refinement fallback diagnostics.

Refs #3703

* perf(cfg): reuse scope and argument facts

Reduce duplicate native AST reads and allocation in TypeScript CFG collection
while preserving argument forwarding and malformed-input recovery.

Add regression cases and a portable paired benchmark with recorded results.

* test(cfg): distinguish validator cases

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
2026-10-24 15:37:26 +01:00

154 lines
6.3 KiB
JavaScript

/**
* Compare guard-free CFG collection with another source checkout.
* Run with --expose-gc --import tsx; both checkouts need installed dependencies.
* Parsing, serialization, and correctness checks stay outside the timed region.
*/
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
import { createRequire } from 'node:module';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { parseArgs } from 'node:util';
const { values } = parseArgs({
options: {
baseline: { type: 'string' },
candidate: { type: 'string' },
workload: { type: 'string', default: 'arguments' },
pairs: { type: 'string', default: '15' },
out: { type: 'string' },
check: { type: 'boolean', default: false },
help: { type: 'boolean', default: false },
},
});
if (values.help) {
console.log(`Usage: node --expose-gc --import tsx bench/cfg/compare-guard-overhead.mjs
--baseline /path/to/reference/gitnexus [--candidate /path/to/current/gitnexus]
[--workload arguments|transitive|direct] [--pairs 15] [--out result.json] [--check]
--check requires at least five pairs, samples >=100ms, and <=5% median overhead.
Exact legacy facts, function coverage, and value forwarding are always checked.`);
process.exit(0);
}
if (!values.baseline || !global.gc) throw new Error('Supply --baseline and run with --expose-gc');
const pairs = Number(values.pairs);
if (!Number.isSafeInteger(pairs) || pairs < 1)
throw new Error('--pairs must be a positive integer');
if (!['arguments', 'transitive', 'direct'].includes(values.workload)) {
throw new Error('Unknown workload');
}
const candidate = path.resolve(
values.candidate ?? path.join(path.dirname(fileURLToPath(import.meta.url)), '../..'),
);
const baseline = path.resolve(values.baseline);
const require = createRequire(path.join(candidate, 'package.json'));
const Parser = require('tree-sitter');
const grammar = require('tree-sitter-typescript').typescript;
const load = (root, relative) =>
import(pathToFileURL(path.join(root, 'src/core/ingestion', relative)).href);
const versions = [];
for (const root of [baseline, candidate]) {
const { collectFunctionCfgs } = await load(root, 'cfg/collect.ts');
const { createTypeScriptCfgVisitor } = await load(root, 'cfg/visitors/typescript.ts');
versions.push({ collect: collectFunctionCfgs, visitor: createTypeScriptCfgVisitor() });
}
const count = 4000;
const source = Array.from({ length: count }, (_, i) => {
if (values.workload === 'arguments') {
return `function f${i}(x:string){ const y="prefix "+x; return consume${i}(y); }`;
}
if (values.workload === 'direct') {
return `function f${i}(x:string){ return consume${i}("prefix "+x); }`;
}
return `function f${i}(x:string){ const a="prefix "+x; const b=a+" suffix"; return consume${i}(b); }`;
}).join('');
const parser = new Parser();
parser.setLanguage(grammar);
const tree = parser.parse(source, undefined, { bufferSize: 1024 * 1024 });
const collect = (version) => version.collect(tree.rootNode, version.visitor, 'perf.ts', 0).cfgs;
for (let rep = 0; rep < 3; rep++) for (const version of versions) collect(version);
const median = (numbers) => [...numbers].sort((a, b) => a - b)[Math.floor(numbers.length / 2)];
const samples = [[], []];
const heaps = [[], []];
const bytes = [];
for (let rep = 0; rep < pairs; rep++) {
for (const index of rep % 2 ? [1, 0] : [0, 1]) {
global.gc();
const before = process.memoryUsage().heapUsed;
const start = process.hrtime.bigint();
const cfgs = collect(versions[index]);
samples[index].push(Number(process.hrtime.bigint() - start) / 1e6);
heaps[index].push(process.memoryUsage().heapUsed - before);
bytes[index] = Buffer.byteLength(JSON.stringify(cfgs));
}
}
// Gather outputs separately; do not retain previous CFG arrays during timing.
const outputs = versions.map(collect);
const legacy = (cfgs) =>
JSON.stringify(cfgs, (key, value) =>
['refinement', 'refinementSafe', 'primitiveBindings'].includes(key) ? undefined : value,
);
const hash = (value) => crypto.createHash('sha256').update(value).digest('hex');
const { createFunctionRefinement } = await load(candidate, 'taint/guard-refinement.ts');
const expectedParts = [
{ kind: 'literal', value: 'prefix ' },
{ kind: 'input' },
...(values.workload === 'transitive' ? [{ kind: 'literal', value: ' suffix' }] : []),
];
const flatten = (effect) => (effect?.kind === 'concat' ? effect.parts.flatMap(flatten) : [effect]);
let effects = 0;
for (const cfg of outputs[1]) {
const refinement = createFunctionRefinement(cfg);
for (const block of cfg.blocks) {
for (
let statementIndex = 0;
statementIndex < (block.statements?.length ?? 0);
statementIndex++
) {
const statement = block.statements[statementIndex];
for (let site = 0; site < (statement.sites?.length ?? 0); site++) {
if (!statement.sites[site].callee?.startsWith('consume')) continue;
const effect = refinement.argument(block.index, statementIndex, site, 0, 0);
if (
effect?.kind === 'concat' &&
JSON.stringify(flatten(effect)) === JSON.stringify(expectedParts) &&
refinement.defaultShell(block.index, statementIndex, site)
)
effects++;
}
}
}
}
const medians = samples.map(median);
const result = {
workload: values.workload,
pairs,
time_overhead_pct: (medians[1] / medians[0] - 1) * 100,
serialized_overhead_pct: (bytes[1] / bytes[0] - 1) * 100,
legacy_equal: hash(legacy(outputs[0])) === hash(legacy(outputs[1])) ? 1 : 0,
functions_complete: outputs[1].length === count ? 1 : 0,
forwarding_complete: effects === count ? 1 : 0,
samples_ms: samples,
medians_ms: medians,
bytes,
// An after-collection heap delta is diagnostic; it is not peak heap.
heap_growth_medians: heaps.map(median),
base_hash: hash(legacy(outputs[0])),
source_hashes: [baseline, candidate].map((root) =>
hash(fs.readFileSync(path.join(root, 'src/core/ingestion/cfg/visitors/typescript-harvest.ts'))),
),
};
const json = JSON.stringify(result, null, 2) + '\n';
if (values.out) fs.writeFileSync(values.out, json);
console.log(json);
if (!result.legacy_equal || !result.functions_complete || !result.forwarding_complete) {
process.exitCode = 1;
}
if (
values.check &&
(pairs < 5 || samples.flat().some((ms) => ms < 100) || result.time_overhead_pct > 5)
) {
process.exitCode = 1;
}