Skip to content

Commit f8016c6

Browse files
authored
perf: reduce query latency regression from 3.1.4 to 3.3.0 (#528)
* perf: reduce query latency regression from 3.1.4 → 3.3.0 Three targeted fixes for the +28–56% query latency regression: 1. Pin benchmark hub target to stable function names (buildGraph, openDb, loadConfig) instead of auto-selecting the most-connected node. Barrel/type files becoming the hub made version-to-version comparison meaningless. 2. Gate implementors queries in bfsTransitiveCallers — check once whether the graph has any 'implements' edges before doing per-node findNodeById + findImplementors lookups. Skips all implementor overhead for codebases without interface/trait hierarchies. 3. Cache loadConfig() results per cwd. The config file is read from disk on every fnImpactData and diffImpactData call; caching eliminates redundant fs.existsSync + readFileSync + JSON.parse per query invocation. Impact: 5 functions changed, 123 affected * fix: return structuredClone from config cache and guard benchmark db handle Prevent callers from mutating the cached config object by returning a deep clone on cache hits. Add try/finally to selectTargets() so the database handle is closed even if a query throws. Impact: 2 functions changed, 1 affected * fix: install @huggingface/transformers in npm-mode benchmark workers The embedding benchmark's npm mode installs codegraph into a temp dir, but @huggingface/transformers is a devDependency and not included. All 6 model workers crash on import, producing symbols: 0, models: {}. Install it explicitly from the local devDependencies version, matching the existing pattern for native platform packages. Also add a guard in update-embedding-report.js to reject empty results and fail loudly instead of silently overwriting valid benchmark data. * fix: clone config on cache-miss paths to prevent cache corruption (#528) Impact: 1 functions changed, 118 affected
1 parent 3ae7d04 commit f8016c6

5 files changed

Lines changed: 102 additions & 7 deletions

File tree

scripts/lib/bench-config.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,27 @@ export async function resolveBenchmarkSource() {
134134
console.error(`Warning: failed to install native package: ${err.message}`);
135135
}
136136

137+
// @huggingface/transformers is a devDependency (lazy-loaded for embeddings).
138+
// It is not installed as a transitive dep in npm mode, so install it
139+
// explicitly so the embedding benchmark workers can import it.
140+
try {
141+
const localPkg = JSON.parse(
142+
fs.readFileSync(path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '..', '..', 'package.json'), 'utf8'),
143+
);
144+
const hfVersion = localPkg.devDependencies?.['@huggingface/transformers'];
145+
if (hfVersion) {
146+
console.error(`Installing @huggingface/transformers@${hfVersion} for embedding benchmarks...`);
147+
execFileSync('npm', ['install', `@huggingface/transformers@${hfVersion}`, '--no-audit', '--no-fund', '--no-save'], {
148+
cwd: tmpDir,
149+
stdio: 'pipe',
150+
timeout: 120_000,
151+
});
152+
console.error('Installed @huggingface/transformers');
153+
}
154+
} catch (err) {
155+
console.error(`Warning: failed to install @huggingface/transformers: ${err.message}`);
156+
}
157+
137158
const srcDir = path.join(pkgDir, 'src');
138159

139160
if (!fs.existsSync(srcDir)) {

scripts/query-benchmark.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,32 @@ function round1(n) {
111111
return Math.round(n * 10) / 10;
112112
}
113113

114+
// Pinned hub targets — stable function names that exist across versions.
115+
// Auto-selecting the most-connected node makes version-to-version comparison
116+
// meaningless when barrel/type files get added or removed.
117+
const PINNED_HUB_CANDIDATES = ['buildGraph', 'openDb', 'loadConfig'];
118+
114119
function selectTargets() {
115120
const db = new Database(dbPath, { readonly: true });
121+
try {
122+
123+
// Try pinned candidates first for a stable hub across versions
124+
let hub = null;
125+
for (const candidate of PINNED_HUB_CANDIDATES) {
126+
const row = db
127+
.prepare(
128+
`SELECT n.name FROM nodes n
129+
JOIN edges e ON e.source_id = n.id OR e.target_id = n.id
130+
WHERE n.name = ? AND n.file NOT LIKE '%test%' AND n.file NOT LIKE '%spec%'
131+
LIMIT 1`,
132+
)
133+
.get(candidate);
134+
if (row) {
135+
hub = row.name;
136+
break;
137+
}
138+
}
139+
116140
const rows = db
117141
.prepare(
118142
`SELECT n.name, COUNT(e.id) AS cnt
@@ -123,14 +147,19 @@ function selectTargets() {
123147
ORDER BY cnt DESC`,
124148
)
125149
.all();
126-
db.close();
127150

128151
if (rows.length === 0) throw new Error('No nodes with edges found in graph');
129152

130-
const hub = rows[0].name;
153+
// Fall back to most-connected if no pinned candidate found
154+
if (!hub) hub = rows[0].name;
155+
131156
const mid = rows[Math.floor(rows.length / 2)].name;
132157
const leaf = rows[rows.length - 1].name;
133158
return { hub, mid, leaf };
159+
160+
} finally {
161+
db.close();
162+
}
134163
}
135164

136165
function benchDepths(fn, name, depths) {

scripts/update-embedding-report.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ if (arg) {
2626
}
2727
const entry = JSON.parse(jsonText);
2828

29+
// Guard: reject empty benchmark results (all workers crashed or no symbols indexed)
30+
if (!entry.symbols || !entry.models || Object.keys(entry.models).length === 0) {
31+
console.error(
32+
`Embedding benchmark produced empty results (symbols=${entry.symbols}, models=${Object.keys(entry.models || {}).length}). ` +
33+
'Skipping report update to avoid overwriting valid data. Check benchmark worker logs.',
34+
);
35+
process.exit(1);
36+
}
37+
2938
// ── Paths ────────────────────────────────────────────────────────────────
3039
const reportPath = path.join(root, 'generated', 'benchmarks', 'EMBEDDING-BENCHMARKS.md');
3140

src/domain/analysis/impact.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ import { findMatchingNodes } from './symbol-lookup.js';
2424

2525
const INTERFACE_LIKE_KINDS = new Set(['interface', 'trait']);
2626

27+
/**
28+
* Check whether the graph contains any 'implements' edges.
29+
* Cached per db handle so the query runs at most once per connection.
30+
*/
31+
const _hasImplementsCache = new WeakMap();
32+
function hasImplementsEdges(db) {
33+
if (_hasImplementsCache.has(db)) return _hasImplementsCache.get(db);
34+
const row = db.prepare("SELECT 1 FROM edges WHERE kind = 'implements' LIMIT 1").get();
35+
const result = !!row;
36+
_hasImplementsCache.set(db, result);
37+
return result;
38+
}
39+
2740
/**
2841
* BFS traversal to find transitive callers of a node.
2942
* When an interface/trait node is encountered (either as the start node or
@@ -40,14 +53,17 @@ export function bfsTransitiveCallers(
4053
startId,
4154
{ noTests = false, maxDepth = 3, includeImplementors = true, onVisit } = {},
4255
) {
56+
// Skip all implementor lookups when the graph has no implements edges
57+
const resolveImplementors = includeImplementors && hasImplementsEdges(db);
58+
4359
const visited = new Set([startId]);
4460
const levels = {};
4561
let frontier = [startId];
4662

4763
// Seed: if start node is an interface/trait, include its implementors at depth 1.
4864
// Implementors go into a separate list so their callers appear at depth 2, not depth 1.
4965
const implNextFrontier = [];
50-
if (includeImplementors) {
66+
if (resolveImplementors) {
5167
const startNode = findNodeById(db, startId);
5268
if (startNode && INTERFACE_LIKE_KINDS.has(startNode.kind)) {
5369
const impls = findImplementors(db, startId);
@@ -88,7 +104,7 @@ export function bfsTransitiveCallers(
88104

89105
// If a caller is an interface/trait, also pull in its implementors
90106
// Implementors are one extra hop away, so record at d+1
91-
if (includeImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
107+
if (resolveImplementors && INTERFACE_LIKE_KINDS.has(c.kind)) {
92108
const impls = findImplementors(db, c.id);
93109
for (const impl of impls) {
94110
if (!visited.has(impl.id) && (!noTests || !isTestFile(impl.file))) {

src/infrastructure/config.js

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,19 @@ export const DEFAULTS = {
130130
},
131131
};
132132

133+
// Per-cwd config cache — avoids re-reading the config file on every query call.
134+
// The config file rarely changes within a single process lifetime.
135+
const _configCache = new Map();
136+
133137
/**
134138
* Load project configuration from a .codegraphrc.json or similar file.
135-
* Returns merged config with defaults.
139+
* Returns merged config with defaults. Results are cached per cwd.
136140
*/
137141
export function loadConfig(cwd) {
138142
cwd = cwd || process.cwd();
143+
const cached = _configCache.get(cwd);
144+
if (cached) return structuredClone(cached);
145+
139146
for (const name of CONFIG_FILES) {
140147
const filePath = path.join(cwd, name);
141148
if (fs.existsSync(filePath)) {
@@ -148,13 +155,26 @@ export function loadConfig(cwd) {
148155
merged.query.excludeTests = Boolean(config.excludeTests);
149156
}
150157
delete merged.excludeTests;
151-
return resolveSecrets(applyEnvOverrides(merged));
158+
const result = resolveSecrets(applyEnvOverrides(merged));
159+
_configCache.set(cwd, structuredClone(result));
160+
return result;
152161
} catch (err) {
153162
debug(`Failed to parse config ${filePath}: ${err.message}`);
154163
}
155164
}
156165
}
157-
return resolveSecrets(applyEnvOverrides({ ...DEFAULTS }));
166+
const defaults = resolveSecrets(applyEnvOverrides({ ...DEFAULTS }));
167+
_configCache.set(cwd, structuredClone(defaults));
168+
return defaults;
169+
}
170+
171+
/**
172+
* Clear the config cache. Intended for long-running processes that need to
173+
* pick up on-disk config changes, and for test isolation when tests share
174+
* the same cwd.
175+
*/
176+
export function clearConfigCache() {
177+
_configCache.clear();
158178
}
159179

160180
const ENV_LLM_MAP = {

0 commit comments

Comments
 (0)