diff --git a/src/builder.js b/src/builder.js index 19c8810..c5019b4 100644 --- a/src/builder.js +++ b/src/builder.js @@ -1272,7 +1272,7 @@ export async function buildGraph(rootDir, opts = {}) { } _t.rolesMs = performance.now() - _t.roles0; - // For incremental builds, filter out reverse-dep-only files from AST/complexity + // For incremental builds, filter out reverse-dep-only files from AST/complexity/CFG/dataflow // — their content didn't change, so existing ast_nodes/function_complexity rows are valid. let astComplexitySymbols = allSymbols; if (!isFullBuild) { @@ -1287,13 +1287,12 @@ export async function buildGraph(rootDir, opts = {}) { } } debug( - `AST/complexity: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`, + `AST/complexity/CFG/dataflow: processing ${astComplexitySymbols.size} changed files (skipping ${reverseDepFiles.size} reverse-deps)`, ); } } // AST node extraction (calls, new, string, regex, throw, await) - // Must run before complexity which releases _tree references _t.ast0 = performance.now(); if (opts.ast !== false) { try { @@ -1317,12 +1316,25 @@ export async function buildGraph(rootDir, opts = {}) { } _t.complexityMs = performance.now() - _t.complexity0; + // Pre-parse files missing WASM trees (native builds) so CFG + dataflow + // share a single parse pass instead of each creating parsers independently + if (opts.cfg !== false || opts.dataflow !== false) { + _t.wasmPre0 = performance.now(); + try { + const { ensureWasmTrees } = await import('./parser.js'); + await ensureWasmTrees(astComplexitySymbols, rootDir); + } catch (err) { + debug(`WASM pre-parse failed: ${err.message}`); + } + _t.wasmPreMs = performance.now() - _t.wasmPre0; + } + // CFG analysis (skip with --no-cfg) if (opts.cfg !== false) { _t.cfg0 = performance.now(); try { const { buildCFGData } = await import('./cfg.js'); - await buildCFGData(db, allSymbols, rootDir, engineOpts); + await buildCFGData(db, astComplexitySymbols, rootDir, engineOpts); } catch (err) { debug(`CFG analysis failed: ${err.message}`); } @@ -1334,7 +1346,7 @@ export async function buildGraph(rootDir, opts = {}) { _t.dataflow0 = performance.now(); try { const { buildDataflowEdges } = await import('./dataflow.js'); - await buildDataflowEdges(db, allSymbols, rootDir, engineOpts); + await buildDataflowEdges(db, astComplexitySymbols, rootDir, engineOpts); } catch (err) { debug(`Dataflow analysis failed: ${err.message}`); } @@ -1434,6 +1446,7 @@ export async function buildGraph(rootDir, opts = {}) { rolesMs: +_t.rolesMs.toFixed(1), astMs: +_t.astMs.toFixed(1), complexityMs: +_t.complexityMs.toFixed(1), + ...(_t.wasmPreMs != null && { wasmPreMs: +_t.wasmPreMs.toFixed(1) }), ...(_t.cfgMs != null && { cfgMs: +_t.cfgMs.toFixed(1) }), ...(_t.dataflowMs != null && { dataflowMs: +_t.dataflowMs.toFixed(1) }), }, diff --git a/src/complexity.js b/src/complexity.js index 383b4ed..1425d73 100644 --- a/src/complexity.js +++ b/src/complexity.js @@ -1769,9 +1769,6 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp ); analyzed++; } - - // Release cached tree for GC - symbols._tree = null; } }); diff --git a/src/parser.js b/src/parser.js index 5678e78..e4a4a2e 100644 --- a/src/parser.js +++ b/src/parser.js @@ -38,6 +38,9 @@ function grammarPath(name) { let _initialized = false; +// Memoized parsers — avoids reloading WASM grammars on every createParsers() call +let _cachedParsers = null; + // Query cache for JS/TS/TSX extractors (populated during createParsers) const _queryCache = new Map(); @@ -66,6 +69,8 @@ const TS_EXTRA_PATTERNS = [ ]; export async function createParsers() { + if (_cachedParsers) return _cachedParsers; + if (!_initialized) { await Parser.init(); _initialized = true; @@ -94,6 +99,7 @@ export async function createParsers() { parsers.set(entry.id, null); } } + _cachedParsers = parsers; return parsers; } @@ -104,6 +110,54 @@ export function getParser(parsers, filePath) { return parsers.get(entry.id) || null; } +/** + * Pre-parse files missing `_tree` via WASM so downstream phases (CFG, dataflow) + * don't each need to create parsers and re-parse independently. + * Only parses files whose extension is in SUPPORTED_EXTENSIONS. + * + * @param {Map} fileSymbols - Map + * @param {string} rootDir - absolute project root + */ +export async function ensureWasmTrees(fileSymbols, rootDir) { + // Check if any file needs a tree + let needsParse = false; + for (const [relPath, symbols] of fileSymbols) { + if (!symbols._tree) { + const ext = path.extname(relPath).toLowerCase(); + if (_extToLang.has(ext)) { + needsParse = true; + break; + } + } + } + if (!needsParse) return; + + const parsers = await createParsers(); + + for (const [relPath, symbols] of fileSymbols) { + if (symbols._tree) continue; + const ext = path.extname(relPath).toLowerCase(); + const entry = _extToLang.get(ext); + if (!entry) continue; + const parser = parsers.get(entry.id); + if (!parser) continue; + + const absPath = path.join(rootDir, relPath); + let code; + try { + code = fs.readFileSync(absPath, 'utf-8'); + } catch { + continue; + } + try { + symbols._tree = parser.parse(code); + symbols._langId = entry.id; + } catch { + // skip files that fail to parse + } + } +} + /** * Check whether the required WASM grammar files exist on disk. */