Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions scripts/benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
Expand All @@ -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,
Expand Down
42 changes: 30 additions & 12 deletions scripts/incremental-benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -154,24 +157,37 @@ 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`);
} else {
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();
Expand All @@ -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,
Expand Down
49 changes: 36 additions & 13 deletions scripts/query-benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -155,20 +158,38 @@ 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}`);
// 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 wasm = benchmarkQueries(targets);
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 });

if (!targets) {
targets = selectTargets();
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');
Expand All @@ -180,12 +201,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,
Expand Down
23 changes: 20 additions & 3 deletions scripts/token-benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}) {
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/parser.test.js
Original file line number Diff line number Diff line change
@@ -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');
}
});
});
Loading