From c99b1483289c5c4a57c5911e689bc09b8f5ef470 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:38:26 -0700 Subject: [PATCH 1/2] fix: benchmark scripts handle missing WASM grammars gracefully Add isWasmAvailable() to parser.js that checks if required WASM grammar files exist on disk. All 4 benchmark scripts now check WASM availability before attempting WASM engine benchmarks, mirroring the existing pattern for native engine availability. When neither engine is available, scripts print a helpful error message and exit instead of crashing. Impact: 2 functions changed, 5 affected --- scripts/benchmark.js | 50 ++++++++++++++++++++++---------- scripts/incremental-benchmark.js | 42 +++++++++++++++++++-------- scripts/query-benchmark.js | 47 +++++++++++++++++++++--------- scripts/token-benchmark.js | 23 +++++++++++++-- src/index.js | 2 +- src/parser.js | 9 ++++++ 6 files changed, 129 insertions(+), 44 deletions(-) diff --git a/scripts/benchmark.js b/scripts/benchmark.js index d37b8422..0dff7456 100644 --- a/scripts/benchmark.js +++ b/scripts/benchmark.js @@ -31,6 +31,9 @@ const { fnDepsData, fnImpactData, pathData, rolesData, statsData } = await impor const { isNativeAvailable } = await import( srcImport(srcDir, 'native.js') ); +const { isWasmAvailable } = await import( + srcImport(srcDir, 'parser.js') +); const INCREMENTAL_RUNS = 3; const QUERY_RUNS = 5; @@ -160,10 +163,24 @@ async function benchmarkEngine(engine) { } // ── Run benchmarks ─────────────────────────────────────────────────────── -const wasm = await benchmarkEngine('wasm'); +const hasWasm = isWasmAvailable(); +const hasNative = isNativeAvailable(); + +if (!hasWasm && !hasNative) { + console.error('Error: Neither WASM grammars nor native engine are available.'); + console.error('Run "npm run build:wasm" to build WASM grammars, or install the native platform package.'); + process.exit(1); +} + +let wasm = null; +if (hasWasm) { + wasm = await benchmarkEngine('wasm'); +} else { + console.error('WASM grammars not built — skipping WASM benchmark'); +} let native = null; -if (isNativeAvailable()) { +if (hasNative) { native = await benchmarkEngine('native'); } else { console.error('Native engine not available — skipping native benchmark'); @@ -172,22 +189,25 @@ if (isNativeAvailable()) { // Restore console.log for JSON output console.log = origLog; +const primary = wasm || native; const result = { version, date: new Date().toISOString().slice(0, 10), - files: wasm.files, - wasm: { - buildTimeMs: wasm.buildTimeMs, - queryTimeMs: wasm.queryTimeMs, - nodes: wasm.nodes, - edges: wasm.edges, - dbSizeBytes: wasm.dbSizeBytes, - perFile: wasm.perFile, - noopRebuildMs: wasm.noopRebuildMs, - oneFileRebuildMs: wasm.oneFileRebuildMs, - queries: wasm.queries, - phases: wasm.phases, - }, + files: primary.files, + wasm: wasm + ? { + buildTimeMs: wasm.buildTimeMs, + queryTimeMs: wasm.queryTimeMs, + nodes: wasm.nodes, + edges: wasm.edges, + dbSizeBytes: wasm.dbSizeBytes, + perFile: wasm.perFile, + noopRebuildMs: wasm.noopRebuildMs, + oneFileRebuildMs: wasm.oneFileRebuildMs, + queries: wasm.queries, + phases: wasm.phases, + } + : null, native: native ? { buildTimeMs: native.buildTimeMs, diff --git a/scripts/incremental-benchmark.js b/scripts/incremental-benchmark.js index 426b56fd..76ffad14 100644 --- a/scripts/incremental-benchmark.js +++ b/scripts/incremental-benchmark.js @@ -30,6 +30,9 @@ const { resolveImportPath, resolveImportsBatch, resolveImportPathJS } = await im const { isNativeAvailable } = await import( srcImport(srcDir, 'native.js') ); +const { isWasmAvailable } = await import( + srcImport(srcDir, 'parser.js') +); // Redirect console.log to stderr so only JSON goes to stdout const origLog = console.log; @@ -154,17 +157,26 @@ function benchmarkResolve(inputs) { } // ── Run benchmarks ─────────────────────────────────────────────────────── +const hasWasm = isWasmAvailable(); +const hasNative = isNativeAvailable(); -console.error('Benchmarking WASM engine...'); -const wasm = await benchmarkBuildTiers('wasm'); -console.error(` full=${wasm.fullBuildMs}ms noop=${wasm.noopRebuildMs}ms 1-file=${wasm.oneFileRebuildMs}ms`); +if (!hasWasm && !hasNative) { + console.error('Error: Neither WASM grammars nor native engine are available.'); + console.error('Run "npm run build:wasm" to build WASM grammars, or install the native platform package.'); + process.exit(1); +} -// Get file count from the WASM-built graph -const stats = statsData(dbPath); -const files = stats.files.total; +let wasm = null; +if (hasWasm) { + console.error('Benchmarking WASM engine...'); + wasm = await benchmarkBuildTiers('wasm'); + console.error(` full=${wasm.fullBuildMs}ms noop=${wasm.noopRebuildMs}ms 1-file=${wasm.oneFileRebuildMs}ms`); +} else { + console.error('WASM grammars not built — skipping WASM benchmark'); +} let native = null; -if (isNativeAvailable()) { +if (hasNative) { console.error('Benchmarking native engine...'); native = await benchmarkBuildTiers('native'); console.error(` full=${native.fullBuildMs}ms noop=${native.noopRebuildMs}ms 1-file=${native.oneFileRebuildMs}ms`); @@ -172,6 +184,10 @@ if (isNativeAvailable()) { console.error('Native engine not available — skipping native build benchmark'); } +// Get file count from whichever graph was built last +const stats = statsData(dbPath); +const files = stats.files.total; + // Import resolution benchmark (uses existing graph) console.error('Benchmarking import resolution...'); const inputs = collectImportPairs(); @@ -186,11 +202,13 @@ const result = { version, date: new Date().toISOString().slice(0, 10), files, - wasm: { - fullBuildMs: wasm.fullBuildMs, - noopRebuildMs: wasm.noopRebuildMs, - oneFileRebuildMs: wasm.oneFileRebuildMs, - }, + wasm: wasm + ? { + fullBuildMs: wasm.fullBuildMs, + noopRebuildMs: wasm.noopRebuildMs, + oneFileRebuildMs: wasm.oneFileRebuildMs, + } + : null, native: native ? { fullBuildMs: native.fullBuildMs, diff --git a/scripts/query-benchmark.js b/scripts/query-benchmark.js index 4347c0a9..ea81ea08 100644 --- a/scripts/query-benchmark.js +++ b/scripts/query-benchmark.js @@ -32,6 +32,9 @@ const { fnDepsData, fnImpactData, diffImpactData, statsData } = await import( const { isNativeAvailable } = await import( srcImport(srcDir, 'native.js') ); +const { isWasmAvailable } = await import( + srcImport(srcDir, 'parser.js') +); // Redirect console.log to stderr so only JSON goes to stdout const origLog = console.log; @@ -155,20 +158,36 @@ function benchmarkQueries(targets) { } // ── Run benchmarks ─────────────────────────────────────────────────────── +const hasWasm = isWasmAvailable(); +const hasNative = isNativeAvailable(); -// Build with WASM engine -if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); -await buildGraph(root, { engine: 'wasm', incremental: false }); +if (!hasWasm && !hasNative) { + console.error('Error: Neither WASM grammars nor native engine are available.'); + console.error('Run "npm run build:wasm" to build WASM grammars, or install the native platform package.'); + process.exit(1); +} -const targets = selectTargets(); -console.error(`Targets: hub=${targets.hub}, mid=${targets.mid}, leaf=${targets.leaf}`); +let wasm = null; +if (hasWasm) { + if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); + await buildGraph(root, { engine: 'wasm', incremental: false }); -const wasm = benchmarkQueries(targets); + const targets = selectTargets(); + console.error(`Targets: hub=${targets.hub}, mid=${targets.mid}, leaf=${targets.leaf}`); + wasm = benchmarkQueries(targets); +} else { + console.error('WASM grammars not built — skipping WASM benchmark'); +} let native = null; -if (isNativeAvailable()) { +if (hasNative) { if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); await buildGraph(root, { engine: 'native', incremental: false }); + + const targets = selectTargets(); + if (!hasWasm) { + console.error(`Targets: hub=${targets.hub}, mid=${targets.mid}, leaf=${targets.leaf}`); + } native = benchmarkQueries(targets); } else { console.error('Native engine not available — skipping native benchmark'); @@ -180,12 +199,14 @@ console.log = origLog; const result = { version, date: new Date().toISOString().slice(0, 10), - wasm: { - targets: wasm.targets, - fnDeps: wasm.fnDeps, - fnImpact: wasm.fnImpact, - diffImpact: wasm.diffImpact, - }, + wasm: wasm + ? { + targets: wasm.targets, + fnDeps: wasm.fnDeps, + fnImpact: wasm.fnImpact, + diffImpact: wasm.diffImpact, + } + : null, native: native ? { targets: native.targets, diff --git a/scripts/token-benchmark.js b/scripts/token-benchmark.js index 7c75051c..1598e6dc 100644 --- a/scripts/token-benchmark.js +++ b/scripts/token-benchmark.js @@ -280,14 +280,31 @@ async function runPerfBenchmarks(nextjsDir) { const { isNativeAvailable } = await import( pathToFileURL(path.join(root, 'src', 'native.js')).href ); + const { isWasmAvailable } = await import( + pathToFileURL(path.join(root, 'src', 'parser.js')).href + ); const dbPath = path.join(nextjsDir, '.codegraph', 'graph.db'); console.error('\n── Performance benchmarks ──'); // ── Build benchmarks ────────────────────────────────────────────── + const engines = [ + ...(isWasmAvailable() ? ['wasm'] : []), + ...(isNativeAvailable() ? ['native'] : []), + ]; + if (engines.length === 0) { + console.error(' No engines available — skipping perf benchmarks'); + return null; + } + if (!isWasmAvailable()) { + console.error(' WASM grammars not built — skipping WASM perf benchmark'); + } + if (!isNativeAvailable()) { + console.error(' Native engine not available — skipping native perf benchmark'); + } const buildResults = {}; - for (const engine of ['wasm', ...(isNativeAvailable() ? ['native'] : [])]) { + for (const engine of engines) { console.error(` Full build (${engine})...`); const timings = []; for (let i = 0; i < PERF_RUNS; i++) { @@ -313,9 +330,9 @@ async function runPerfBenchmarks(nextjsDir) { } // ── Stats ───────────────────────────────────────────────────────── - // Ensure we have a graph (rebuild with wasm if needed) + // Ensure we have a graph (rebuild with first available engine if needed) if (!fs.existsSync(dbPath)) { - await buildGraph(nextjsDir, { engine: 'wasm', incremental: false }); + await buildGraph(nextjsDir, { engine: engines[0], incremental: false }); } const stats = statsData(dbPath); const graphStats = { diff --git a/src/index.js b/src/index.js index e58cc643..0cf65498 100644 --- a/src/index.js +++ b/src/index.js @@ -123,7 +123,7 @@ export { matchOwners, owners, ownersData, ownersForFiles, parseCodeowners } from export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js'; // Unified parser API -export { getActiveEngine, parseFileAuto, parseFilesAuto } from './parser.js'; +export { getActiveEngine, isWasmAvailable, parseFileAuto, parseFilesAuto } from './parser.js'; // Query functions (data-returning) export { ALL_SYMBOL_KINDS, diff --git a/src/parser.js b/src/parser.js index 54eb0820..6429920f 100644 --- a/src/parser.js +++ b/src/parser.js @@ -104,6 +104,15 @@ export function getParser(parsers, filePath) { return parsers.get(entry.id) || null; } +/** + * Check whether the required WASM grammar files exist on disk. + */ +export function isWasmAvailable() { + return LANGUAGE_REGISTRY.filter((e) => e.required).every((e) => + fs.existsSync(grammarPath(e.grammarFile)), + ); +} + // ── Unified API ────────────────────────────────────────────────────────────── function resolveEngine(opts = {}) { From 1ac5d28f7fe85fb16a7fd58a9a3be63308a07d04 Mon Sep 17 00:00:00 2001 From: carlos-alm <127798846+carlos-alm@users.noreply.github.com> Date: Tue, 3 Mar 2026 19:49:39 -0700 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20reuse?= =?UTF-8?q?=20targets=20across=20engines,=20add=20isWasmAvailable=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - query-benchmark.js: select targets once from first built graph, reuse for both engines to ensure valid comparisons - Add unit tests for isWasmAvailable() covering all branches: all present, some missing, all missing, and verifying only required grammars are checked --- scripts/query-benchmark.js | 8 +++-- tests/unit/parser.test.js | 61 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 tests/unit/parser.test.js diff --git a/scripts/query-benchmark.js b/scripts/query-benchmark.js index ea81ea08..76dd9151 100644 --- a/scripts/query-benchmark.js +++ b/scripts/query-benchmark.js @@ -167,12 +167,14 @@ if (!hasWasm && !hasNative) { process.exit(1); } +// Build with first available engine to select targets, then reuse for both +let targets = null; let wasm = null; if (hasWasm) { if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); await buildGraph(root, { engine: 'wasm', incremental: false }); - const targets = selectTargets(); + targets = selectTargets(); console.error(`Targets: hub=${targets.hub}, mid=${targets.mid}, leaf=${targets.leaf}`); wasm = benchmarkQueries(targets); } else { @@ -184,8 +186,8 @@ if (hasNative) { if (fs.existsSync(dbPath)) fs.unlinkSync(dbPath); await buildGraph(root, { engine: 'native', incremental: false }); - const targets = selectTargets(); - if (!hasWasm) { + if (!targets) { + targets = selectTargets(); console.error(`Targets: hub=${targets.hub}, mid=${targets.mid}, leaf=${targets.leaf}`); } native = benchmarkQueries(targets); diff --git a/tests/unit/parser.test.js b/tests/unit/parser.test.js new file mode 100644 index 00000000..495b2a96 --- /dev/null +++ b/tests/unit/parser.test.js @@ -0,0 +1,61 @@ +/** + * Unit tests for isWasmAvailable() in src/parser.js + */ + +import fs from 'node:fs'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { isWasmAvailable, LANGUAGE_REGISTRY } from '../../src/parser.js'; + +describe('isWasmAvailable', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('returns a boolean', () => { + expect(typeof isWasmAvailable()).toBe('boolean'); + }); + + it('returns true when all required grammar files exist', () => { + vi.spyOn(fs, 'existsSync').mockReturnValue(true); + expect(isWasmAvailable()).toBe(true); + }); + + it('returns false when any required grammar file is missing', () => { + // First call returns true (JS), second returns false (TS missing) + const mock = vi.spyOn(fs, 'existsSync'); + let callCount = 0; + mock.mockImplementation(() => { + callCount++; + return callCount !== 2; // second required grammar "missing" + }); + expect(isWasmAvailable()).toBe(false); + }); + + it('returns false when all required grammar files are missing', () => { + vi.spyOn(fs, 'existsSync').mockReturnValue(false); + expect(isWasmAvailable()).toBe(false); + }); + + it('only checks required grammars (JS, TS, TSX)', () => { + const spy = vi.spyOn(fs, 'existsSync').mockReturnValue(true); + isWasmAvailable(); + + const requiredEntries = LANGUAGE_REGISTRY.filter((e) => e.required); + expect(requiredEntries.length).toBe(3); + expect(spy).toHaveBeenCalledTimes(3); + + // Verify it checks the correct grammar files + for (const entry of requiredEntries) { + expect(spy).toHaveBeenCalledWith(expect.stringContaining(entry.grammarFile)); + } + }); + + it('checks files in the grammars/ directory', () => { + const spy = vi.spyOn(fs, 'existsSync').mockReturnValue(true); + isWasmAvailable(); + + for (const call of spy.mock.calls) { + expect(call[0]).toContain('grammars'); + } + }); +});