measure-index.cjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. // Standalone benchmark harness; see docs/benchmarks/regression-audit-2026-09.md.
  2. // Arguments: built engine directory, fixture root, existing output directory, native|wasm.
  3. const fs = require('node:fs');
  4. const path = require('node:path');
  5. const { performance, monitorEventLoopDelay } = require('node:perf_hooks');
  6. const { DatabaseSync } = require('node:sqlite');
  7. const [engine, root, out, backend] = process.argv.slice(2);
  8. if (!engine || !root || !out || !['native', 'wasm'].includes(backend)) {
  9. throw new Error('Usage: node measure-index.cjs BUILT_ENGINE FRESH_FIXTURE EXISTING_OUTPUT native|wasm');
  10. }
  11. if (fs.existsSync(path.join(root, '.codegraph'))) {
  12. throw new Error('Refusing an existing fixture index; preserve it and use a fresh fixture.');
  13. }
  14. const begin = performance.now();
  15. const tracePath = path.join(out, 'progress.ndjson');
  16. // Exclusive creation prevents accidental reuse of a previous run's evidence.
  17. const traceFd = fs.openSync(tracePath, 'wx');
  18. let phase = 'opening';
  19. function progress(event, details = {}) {
  20. fs.writeSync(traceFd, JSON.stringify({ event, phase, epochMs: Date.now(),
  21. wallMs: performance.now() - begin, cpu: process.cpuUsage(), rss: process.memoryUsage().rss,
  22. ...details }) + '\n');
  23. }
  24. const loopDelay = monitorEventLoopDelay({ resolution: 20 });
  25. loopDelay.enable();
  26. let previousLoop = performance.eventLoopUtilization();
  27. const heartbeat = setInterval(() => {
  28. const current = performance.eventLoopUtilization();
  29. progress('heartbeat', { eventLoop: performance.eventLoopUtilization(current, previousLoop),
  30. delayMaxMs: loopDelay.max / 1e6 });
  31. previousLoop = current;
  32. loopDelay.reset();
  33. }, 5000);
  34. heartbeat.unref();
  35. progress('start');
  36. const { CodeGraph } = require(path.join(engine, 'dist/index.js'));
  37. const { DatabaseConnection } = require(path.join(engine, 'dist/db/index.js'));
  38. const loader = require(path.join(engine, 'dist/extraction/kernel/loader.js'));
  39. const result = { engine, root, backend, node: process.version, stages: [] };
  40. let cg;
  41. const orig = CodeGraph.prototype.resolveReferencesBatched;
  42. CodeGraph.prototype.resolveReferencesBatched = async function (...args) {
  43. const stage = { name: 'resolution-and-synthesis', startEpochMs: Date.now(), memoryBefore: process.memoryUsage(), refsBefore: this.db.getDb().prepare('SELECT count(*) AS n FROM unresolved_refs').get().n };
  44. const start = performance.now(), cpu = process.cpuUsage();
  45. progress('stage-start', { name: stage.name });
  46. try { const value = await orig.apply(this, args); stage.stats = value.stats; return value; }
  47. finally {
  48. Object.assign(stage, { endEpochMs: Date.now(), wallMs: performance.now() - start, cpu: process.cpuUsage(cpu), memoryAfter: process.memoryUsage() });
  49. result.stages.push(stage);
  50. progress('stage-complete', { stage });
  51. phase = 'finalizing';
  52. }
  53. };
  54. const origMaintenance = DatabaseConnection.prototype.runMaintenance;
  55. DatabaseConnection.prototype.runMaintenance = async function (...args) {
  56. phase = 'maintenance';
  57. const start = performance.now(), cpu = process.cpuUsage();
  58. progress('stage-start', { name: 'database-maintenance' });
  59. try { return await origMaintenance.apply(this, args); }
  60. finally {
  61. const stage = { name: 'database-maintenance', wallMs: performance.now() - start,
  62. cpu: process.cpuUsage(cpu) };
  63. result.stages.push(stage);
  64. progress('stage-complete', { stage });
  65. phase = 'finalizing';
  66. }
  67. };
  68. (async () => {
  69. try {
  70. // getKernel() deliberately ignores CODEGRAPH_KERNEL=0; kernelSupports()
  71. // is the actual per-call routing predicate used by extraction.
  72. result.nativeLoaded = loader.kernelSupports('typescript');
  73. if (result.nativeLoaded !== (backend === 'native')) throw new Error('Wrong extraction backend');
  74. cg = CodeGraph.initSync(root);
  75. result.openMs = performance.now() - begin;
  76. const start = performance.now(), cpu = process.cpuUsage();
  77. result.indexStartEpochMs = Date.now();
  78. let lastProgress = 0;
  79. phase = 'indexing';
  80. progress('index-start');
  81. result.index = await cg.indexAll({ onProgress: p => {
  82. const now = performance.now();
  83. // Preserve every resolution/synthesis batch, but avoid one disk write per
  84. // scanned/parsed file. Heartbeats continue while asynchronous work waits.
  85. if (phase !== p.phase || p.phase === 'resolving' || p.phase === 'linking' ||
  86. now - lastProgress >= 1000 || (p.total > 0 && p.current === p.total)) {
  87. phase = p.phase;
  88. progress('progress', { progress: p });
  89. lastProgress = now;
  90. }
  91. } });
  92. result.indexEndEpochMs = Date.now();
  93. result.indexMs = performance.now() - start;
  94. result.indexCpu = process.cpuUsage(cpu);
  95. progress('index-complete', { indexMs: result.indexMs, indexCpu: result.indexCpu, index: result.index });
  96. // Save the completed index measurement BEFORE potentially expensive full DB
  97. // checks. A stopped validation must not look like an indexing timeout.
  98. fs.writeFileSync(path.join(out, 'index-result.json'), JSON.stringify(result, null, 2));
  99. phase = 'verification';
  100. progress('verification-start');
  101. if (!result.index.success || result.index.filesErrored) throw new Error('Index did not finish cleanly');
  102. const db = new DatabaseSync(path.join(root, '.codegraph/codegraph.db'), { readOnly: true });
  103. result.counts = Object.fromEntries(['files','nodes','edges','unresolved_refs'].map(table => [table, db.prepare(`SELECT count(*) AS n FROM ${table}`).get().n]));
  104. result.languages = db.prepare('SELECT language,count(*) AS n FROM files GROUP BY language').all();
  105. result.integrity = db.prepare('PRAGMA integrity_check').all();
  106. result.foreignKeys = db.prepare('PRAGMA foreign_key_check').all();
  107. result.orphans = db.prepare('SELECT count(*) AS n FROM edges e LEFT JOIN nodes s ON s.id=e.source LEFT JOIN nodes t ON t.id=e.target WHERE s.id IS NULL OR t.id IS NULL').get().n;
  108. db.close();
  109. if (result.integrity.length !== 1 || result.integrity[0].integrity_check !== 'ok' ||
  110. result.foreignKeys.length || result.orphans) throw new Error('Database verification failed');
  111. progress('verification-complete');
  112. } catch (e) { result.error = e.stack; process.exitCode = 1; }
  113. finally {
  114. phase = 'closing';
  115. progress('closing');
  116. try { cg?.close(); }
  117. catch (e) { result.closeError = e.stack; process.exitCode = 1; }
  118. result.totalInsideProcessMs = performance.now() - begin;
  119. result.maxRSSKiB = process.resourceUsage().maxRSS;
  120. result.finalMemory = process.memoryUsage();
  121. fs.writeFileSync(path.join(out, 'result.json'), JSON.stringify(result, null, 2));
  122. progress('complete', { error: result.error, closeError: result.closeError });
  123. clearInterval(heartbeat);
  124. loopDelay.disable();
  125. fs.closeSync(traceFd);
  126. console.log(JSON.stringify({ indexMs: result.indexMs, stages: result.stages.map(x => ({ wallMs: x.wallMs, stats: x.stats })), error: result.error }));
  127. }
  128. })();