fix(resolution): stop per-call config-key scan that made large Java/Kotlin (Spring) indexes take ~1h (#1180) (#1210)
On a large Java/Kotlin Spring monorepo, reference resolution — not extraction —
dominated a full index (Spring Boot's ~9,650-file tree: extraction 62s,
resolution ~26min). The Spring framework resolver ran an uncached
getNodesByKind('constant') full scan + canonicalConfigKey() filter for EVERY
dotted `calls` ref (every list.add(), builder.build(), receiver.method()),
because the config-key branch gated only on "dotted java/kotlin", not on ref
kind. With ~1,100 constant nodes × ~200k dotted calls that is ~200M wasted
row-fetches/allocations.
Fixes, one theme — config-key constants bind config `references`, never `calls`:
- frameworks/java.ts: gate the Spring config-key branch on
referenceKind === 'references' (what @Value/@ConfigurationProperties emit) so
the `calls` flood skips the scan.
- name-matcher.ts: a `calls` ref no longer resolves to a yaml/properties config
node via matchByQualifiedName (service.process() vs the yaml key
service.process) — a wrong edge that also hid the real callee; it now falls
through to method resolution.
- resolution/index.ts: cache getNodesByKind in the resolver context (same
lifetime as nameCache). Fixes the same uncached-per-ref scan in the Drupal
hook_ resolver and is defense-in-depth for the Spring :prefix branch.
Measured (Spring Boot): resolution 269s→16.5s on a 4.3k-file module (16×) and
~26min→44.7s on the full 9.6k-file tree (~35×); graph byte-identical, full suite
passes. Adds a regression test (same key, two ref kinds, opposite outcomes).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e65a39746c
commit
625b4fe921
+15
-1
@@ -229,6 +229,15 @@ export class ReferenceResolver {
|
||||
private qualifiedNameCache: LRUCache<string, Node[]>; // qualified_name → nodes cache
|
||||
private fileLinesCache: LRUCache<string, string[] | null>; // file → split lines cache
|
||||
private methodMatchCache: LRUCache<string, Node[]>; // lang\0Type::method → matching method nodes
|
||||
// Node kinds are a small fixed set (~24), so this is a plain Map, not an LRU.
|
||||
// getNodesByKind returns the FULL node list for a kind; it was previously
|
||||
// uncached — a per-ref `SELECT * FROM nodes WHERE kind=?` + row-mapping. Called
|
||||
// for every dotted call ref by the Spring resolver (constants) and every
|
||||
// `hook_` ref by the Drupal resolver (functions), that scan dominated
|
||||
// resolution on large repos (#1180). The node set is stable within a
|
||||
// resolution pass (same lifetime assumption as nameCache); clearCaches() resets
|
||||
// it between passes. Callers must treat the returned array as read-only.
|
||||
private nodesByKindCache = new Map<Node['kind'], Node[]>();
|
||||
private knownNames: Set<string> | null = null; // all known symbol names for fast pre-filtering
|
||||
private knownFiles: Set<string> | null = null;
|
||||
private cachesWarmed = false;
|
||||
@@ -332,6 +341,7 @@ export class ReferenceResolver {
|
||||
this.qualifiedNameCache.clear();
|
||||
this.fileLinesCache.clear();
|
||||
this.methodMatchCache.clear();
|
||||
this.nodesByKindCache.clear();
|
||||
this.knownNames = null;
|
||||
this.knownFiles = null;
|
||||
this.cachesWarmed = false;
|
||||
@@ -404,7 +414,11 @@ export class ReferenceResolver {
|
||||
},
|
||||
|
||||
getNodesByKind: (kind: Node['kind']) => {
|
||||
return this.queries.getNodesByKind(kind);
|
||||
const cached = this.nodesByKindCache.get(kind);
|
||||
if (cached !== undefined) return cached;
|
||||
const result = this.queries.getNodesByKind(kind);
|
||||
this.nodesByKindCache.set(kind, result);
|
||||
return result;
|
||||
},
|
||||
|
||||
fileExists: (filePath: string) => {
|
||||
|
||||
Reference in New Issue
Block a user