mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
194 lines
7.1 KiB
JavaScript
194 lines
7.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Native identity reconciliation overhead and scaling.
|
|
* node --import tsx bench/incremental-write-integrity/measure.cjs [--check]
|
|
*
|
|
* COPY, graph construction, fingerprints and correctness controls are untimed.
|
|
* Each timed sample includes all single-label scans and exact tuple checks.
|
|
* Counts/fingerprints reject empty output; timing gates use normalized scaling.
|
|
*/
|
|
const assert = require('node:assert/strict');
|
|
const { mkdtemp, rm, readFile } = require('node:fs/promises');
|
|
const { tmpdir, cpus } = require('node:os');
|
|
const { join, resolve } = require('node:path');
|
|
const { pathToFileURL } = require('node:url');
|
|
const { createHash } = require('node:crypto');
|
|
const ROOT = resolve(__dirname, '../..');
|
|
const source = (file) => import(pathToFileURL(join(ROOT, file)).href);
|
|
|
|
(async () => {
|
|
const { createKnowledgeGraph } = await source('src/core/graph/graph.ts');
|
|
const { reconcileGraphNodeIdentities } = await source(
|
|
'src/core/incremental/write-reconciliation.ts',
|
|
);
|
|
const adapter = await source('src/core/lbug/lbug-adapter.ts');
|
|
const baseline = JSON.parse(await readFile(join(__dirname, 'baseline.json'), 'utf8'));
|
|
assert.equal(baseline.version, 1);
|
|
assert.ok(Number.isFinite(baseline.linear_scaling_budget) && baseline.linear_scaling_budget > 0);
|
|
const results = [];
|
|
for (const size of baseline.sizes) {
|
|
assert.ok(Number.isSafeInteger(size.nodes) && size.nodes > 0);
|
|
assert.match(size.fingerprint, /^[a-f0-9]{64}$/);
|
|
const graph = createKnowledgeGraph();
|
|
const hash = createHash('sha256');
|
|
for (let i = 0; i < size.nodes; i++) {
|
|
const id = `Function:src/owner${Math.floor(i / 32)}.ts:fn${i}`;
|
|
const properties = {
|
|
name: `fn${i}`,
|
|
filePath: `src/owner${Math.floor(i / 32)}.ts`,
|
|
startLine: (i % 32) * 4,
|
|
endLine: (i % 32) * 4 + 2,
|
|
};
|
|
graph.addNode({ id, label: 'Function', properties });
|
|
hash.update(
|
|
JSON.stringify([
|
|
id,
|
|
properties.name,
|
|
properties.filePath,
|
|
properties.startLine,
|
|
properties.endLine,
|
|
]) + '\n',
|
|
);
|
|
}
|
|
assert.equal(hash.digest('hex'), size.fingerprint);
|
|
const directory = await mkdtemp(join(tmpdir(), 'gnx-reconciliation-bench-'));
|
|
try {
|
|
await adapter.initLbug(join(directory, 'lbug'), { skipFts: true });
|
|
await adapter.loadGraphToLbug(
|
|
graph,
|
|
directory,
|
|
directory,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
'none',
|
|
);
|
|
await adapter.tryFlushWAL();
|
|
const samples = [];
|
|
let calls = 0;
|
|
const query = async (sql) => {
|
|
calls++;
|
|
return adapter.executeQuery(sql);
|
|
};
|
|
for (let run = 0; run < 9; run++) {
|
|
calls = 0;
|
|
const start = performance.now();
|
|
const receipt = await reconcileGraphNodeIdentities(graph, query, 'benchmark');
|
|
const elapsed = performance.now() - start;
|
|
assert.equal(receipt.nodes, size.nodes);
|
|
assert.equal(calls, receipt.tables);
|
|
if (run >= 2) samples.push(elapsed);
|
|
}
|
|
const first = graph.iterNodes().next().value;
|
|
const alteredQuery = async (sql) => {
|
|
const rows = await adapter.executeQuery(sql);
|
|
if (sql.includes('(n:`Function`)')) rows.find((r) => r.id === first.id).startLine++;
|
|
return rows;
|
|
};
|
|
await assert.rejects(
|
|
reconcileGraphNodeIdentities(graph, alteredQuery, 'negative-control'),
|
|
/startLine/,
|
|
);
|
|
await assert.rejects(
|
|
reconcileGraphNodeIdentities(graph, async () => [], 'negative-control'),
|
|
/missing ID/,
|
|
);
|
|
|
|
// Native 0.18.3 can corrupt scan projections after this real write, even
|
|
// without FTS. Use an independent tuple-set oracle to require rejection
|
|
// whenever the scan is wrong; a future native fix can pass normally.
|
|
const { extractChangedSubgraph } = await source('src/core/incremental/subgraph-extract.ts');
|
|
const files = new Set(['src/owner0.ts', `src/owner${Math.floor((size.nodes - 1) / 32)}.ts`]);
|
|
const wanted = new Set(
|
|
[...graph.iterNodes()].map((node) =>
|
|
JSON.stringify([
|
|
node.id,
|
|
node.properties.name,
|
|
node.properties.filePath,
|
|
node.properties.startLine,
|
|
node.properties.endLine,
|
|
]),
|
|
),
|
|
);
|
|
const nativePhases = [];
|
|
const audit = async (phase) => {
|
|
const rows = await adapter.executeQuery(
|
|
'MATCH (n:Function) RETURN n.id AS id, n.name AS name, ' +
|
|
'n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine',
|
|
);
|
|
const actual = new Set(
|
|
rows.map((r) => JSON.stringify([r.id, r.name, r.filePath, r.startLine, r.endLine])),
|
|
);
|
|
const mismatched = [...wanted].filter((tuple) => !actual.has(tuple)).length;
|
|
const rejected =
|
|
rows.length !== size.nodes || mismatched > 0 || actual.size !== wanted.size;
|
|
if (rejected) {
|
|
await assert.rejects(
|
|
reconcileGraphNodeIdentities(graph, adapter.executeQuery, phase),
|
|
/Graph identity reconciliation/,
|
|
);
|
|
} else {
|
|
assert.equal(
|
|
(await reconcileGraphNodeIdentities(graph, adapter.executeQuery, phase)).nodes,
|
|
size.nodes,
|
|
);
|
|
}
|
|
nativePhases.push({
|
|
phase,
|
|
rows: rows.length,
|
|
missing_tuples: mismatched,
|
|
verdict: rejected ? 'rejected' : 'certified',
|
|
});
|
|
};
|
|
await adapter.deleteNodesForFiles([...files]);
|
|
await adapter.loadGraphToLbug(
|
|
extractChangedSubgraph(graph, files),
|
|
directory,
|
|
directory,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
'none',
|
|
);
|
|
await audit('post-COPY');
|
|
await adapter.tryFlushWAL();
|
|
await audit('post-checkpoint');
|
|
await adapter.closeLbug();
|
|
await adapter.initLbug(join(directory, 'lbug'), { readOnly: true, skipFts: true });
|
|
await audit('reopened');
|
|
samples.sort((a, b) => a - b);
|
|
results.push({
|
|
nodes: size.nodes,
|
|
queries: calls,
|
|
min_ms: +samples[0].toFixed(3),
|
|
median_ms: +samples[Math.floor(samples.length / 2)].toFixed(3),
|
|
fingerprint: size.fingerprint,
|
|
native_phases: nativePhases,
|
|
});
|
|
} finally {
|
|
await adapter.closeLbug();
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
}
|
|
const scaling =
|
|
results[1].median_ms / results[0].median_ms / (results[1].nodes / results[0].nodes);
|
|
const report = {
|
|
node: process.version,
|
|
platform: process.platform,
|
|
cpu: cpus()[0].model,
|
|
results,
|
|
normalized_scaling: +scaling.toFixed(3),
|
|
negative_controls: 'missing/swapped fields rejected',
|
|
native_sequences:
|
|
'selective delete/COPY, checkpoint and read-only reopen checked against independent tuple oracle',
|
|
};
|
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
if (process.argv.includes('--check'))
|
|
assert.ok(
|
|
scaling <= baseline.linear_scaling_budget,
|
|
`normalized scaling ${scaling} exceeds ${baseline.linear_scaling_budget}`,
|
|
);
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|