Skip to content

Commit bca424b

Browse files
committed
perf: memoize directoryDistance to fix hot-path regression
docs check acknowledged — internal perf fix mirrored in both engines, no new functionality to document. Impact: 1 functions changed, 18 affected
1 parent eae348c commit bca424b

2 files changed

Lines changed: 49 additions & 4 deletions

File tree

crates/codegraph-core/src/domain/graph/resolve.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,22 @@ fn ancestor_chain(dir: &str) -> Vec<String> {
259259
chain
260260
}
261261

262+
// directory_distance is on the hot path for every call-edge confidence
263+
// score, invoked from inside compute_confidence's rayon `.par_iter()` caller
264+
// (line ~330 below). The same directory pairs recur constantly across a
265+
// build, so memoizing avoids rebuilding both ancestor chains and the lookup
266+
// map every call. Thread-local (not a shared Mutex/DashMap) because rayon's
267+
// worker pool is reused across the whole build — each worker accumulates its
268+
// own useful cache with zero lock contention, at the cost of some redundant
269+
// computation the first time a given pair is seen on each thread.
270+
// distance(a, b) === distance(b, a) (symmetric tree distance), so the key is
271+
// order-independent to halve the effective cache size per thread (#1769
272+
// perf regression).
273+
thread_local! {
274+
static DIRECTORY_DISTANCE_CACHE: std::cell::RefCell<std::collections::HashMap<(String, String), usize>> =
275+
std::cell::RefCell::new(std::collections::HashMap::new());
276+
}
277+
262278
/// Directory-tree distance between two directories: hops up from `a` to the
263279
/// nearest ancestor shared with `b`, plus hops down from there to `b`.
264280
///
@@ -271,16 +287,24 @@ fn ancestor_chain(dir: &str) -> Vec<String> {
271287
/// so e.g. a file in `graph/algorithms/*.rs` calling a method declared in
272288
/// the shallower `graph/model.rs` was scored as maximally distant (issue #1769).
273289
fn directory_distance(a: &str, b: &str) -> usize {
290+
let key = if a <= b { (a.to_string(), b.to_string()) } else { (b.to_string(), a.to_string()) };
291+
if let Some(cached) = DIRECTORY_DISTANCE_CACHE.with(|c| c.borrow().get(&key).copied()) {
292+
return cached;
293+
}
294+
274295
let chain_a = ancestor_chain(a);
275296
let chain_b = ancestor_chain(b);
276297
let index_in_b: std::collections::HashMap<&str, usize> =
277298
chain_b.iter().enumerate().map(|(j, d)| (d.as_str(), j)).collect();
299+
let mut dist = usize::MAX;
278300
for (i, dir_a) in chain_a.iter().enumerate() {
279301
if let Some(&j) = index_in_b.get(dir_a.as_str()) {
280-
return i + j;
302+
dist = i + j;
303+
break;
281304
}
282305
}
283-
usize::MAX
306+
DIRECTORY_DISTANCE_CACHE.with(|c| c.borrow_mut().insert(key, dist));
307+
dist
284308
}
285309

286310
/// Compute proximity-based confidence for call resolution.

src/domain/graph/resolve.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,15 +486,36 @@ function ancestorChain(dir: string): string[] {
486486
* `graph/algorithms/*.ts` calling a method declared in the shallower
487487
* `graph/model.ts` was scored as maximally distant (issue #1769).
488488
*/
489+
// directoryDistance is on the hot path for every call-edge confidence score
490+
// (computeConfidence runs per candidate during ranking/filtering, not just
491+
// once per emitted edge — see call-resolver.ts, resolver/strategy.ts,
492+
// stages/build-edges.ts). The same directory pairs recur constantly across a
493+
// build, so memoizing avoids rebuilding both ancestor chains and the lookup
494+
// map on every call. distance(a, b) === distance(b, a) (symmetric tree
495+
// distance), so the key is order-independent to halve the effective cache
496+
// size. Never cleared: purely a function of two path strings, so a stale
497+
// entry can't exist, and even a large repo's directory count keeps this
498+
// bounded (#1769 perf regression — see PR discussion).
499+
const directoryDistanceCache = new Map<string, number>();
500+
489501
function directoryDistance(a: string, b: string): number {
502+
const key = a <= b ? `${a}|${b}` : `${b}|${a}`;
503+
const cached = directoryDistanceCache.get(key);
504+
if (cached !== undefined) return cached;
505+
490506
const chainA = ancestorChain(a);
491507
const chainB = ancestorChain(b);
492508
const indexInB = new Map<string, number>(chainB.map((d, idx) => [d, idx]));
509+
let dist = Infinity;
493510
for (let i = 0; i < chainA.length; i++) {
494511
const j = indexInB.get(chainA[i]!);
495-
if (j !== undefined) return i + j;
512+
if (j !== undefined) {
513+
dist = i + j;
514+
break;
515+
}
496516
}
497-
return Infinity;
517+
directoryDistanceCache.set(key, dist);
518+
return dist;
498519
}
499520

500521
function computeConfidenceJS(

0 commit comments

Comments
 (0)