Просмотр исходного кода

perf: Cache import mappings and index fuzzy matches for resolution

Two major optimizations for the ref resolution phase:

1. Cache extractImportMappings() results per file path — previously
   re-read and re-parsed the source file for every single ref from
   that file (e.g. 100 refs from one file = 100 identical file reads)

2. Replace linear scan in matchFuzzy() with a lazily-built
   case-insensitive Map index — O(1) lookup instead of iterating
   all function/method/class nodes for every unresolved ref.
   Also drop low-value prefix matching (confidence 0.3).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Olaf Monien 4 месяцев назад
Родитель
Сommit
ab9d7188c1
3 измененных файлов с 53 добавлено и 33 удалено
  1. 19 6
      src/resolution/import-resolver.ts
  2. 4 2
      src/resolution/index.ts
  3. 30 25
      src/resolution/name-matcher.ts

+ 19 - 6
src/resolution/import-resolver.ts

@@ -425,6 +425,16 @@ function extractPHPImports(content: string): ImportMapping[] {
   return mappings;
 }
 
+// Cache import mappings per file to avoid re-reading and re-parsing
+const importMappingCache = new Map<string, ImportMapping[]>();
+
+/**
+ * Clear the import mapping cache (call between indexing runs)
+ */
+export function clearImportMappingCache(): void {
+  importMappingCache.clear();
+}
+
 /**
  * Resolve a reference using import mappings
  */
@@ -432,14 +442,17 @@ export function resolveViaImport(
   ref: UnresolvedRef,
   context: ResolutionContext
 ): ResolvedRef | null {
-  // Read the source file to extract imports
-  const content = context.readFile(ref.filePath);
-  if (!content) {
-    return null;
+  // Use cached import mappings or extract and cache them
+  let imports = importMappingCache.get(ref.filePath);
+  if (!imports) {
+    const content = context.readFile(ref.filePath);
+    if (!content) {
+      return null;
+    }
+    imports = extractImportMappings(ref.filePath, content, ref.language);
+    importMappingCache.set(ref.filePath, imports);
   }
 
-  const imports = extractImportMappings(ref.filePath, content, ref.language);
-
   // Check if the reference name matches any import
   for (const imp of imports) {
     if (imp.localName === ref.referenceName || ref.referenceName.startsWith(imp.localName + '.')) {

+ 4 - 2
src/resolution/index.ts

@@ -16,8 +16,8 @@ import {
   ResolutionContext,
   FrameworkResolver,
 } from './types';
-import { matchReference } from './name-matcher';
-import { resolveViaImport } from './import-resolver';
+import { matchReference, clearFuzzyIndex } from './name-matcher';
+import { resolveViaImport, clearImportMappingCache } from './import-resolver';
 import { detectFrameworks } from './frameworks';
 import { logDebug } from '../errors';
 
@@ -106,6 +106,8 @@ export class ReferenceResolver {
     this.qualifiedNameCache.clear();
     this.kindCache.clear();
     this.nodeByIdCache.clear();
+    clearImportMappingCache();
+    clearFuzzyIndex();
     this.cachesWarmed = false;
   }
 

+ 30 - 25
src/resolution/name-matcher.ts

@@ -190,6 +190,16 @@ function findBestMatch(
   return bestNode;
 }
 
+// Lazily-built case-insensitive index for fuzzy matching
+let fuzzyIndex: Map<string, Node[]> | null = null;
+
+/**
+ * Clear the fuzzy match index (call between indexing runs)
+ */
+export function clearFuzzyIndex(): void {
+  fuzzyIndex = null;
+}
+
 /**
  * Fuzzy match - last resort with lower confidence
  */
@@ -197,21 +207,29 @@ export function matchFuzzy(
   ref: UnresolvedRef,
   context: ResolutionContext
 ): ResolvedRef | null {
-  // Try case-insensitive match
-  const allNodes = [
-    ...context.getNodesByKind('function'),
-    ...context.getNodesByKind('method'),
-    ...context.getNodesByKind('class'),
-  ];
+  // Build case-insensitive index on first use
+  if (!fuzzyIndex) {
+    fuzzyIndex = new Map();
+    const kinds: Array<Node['kind']> = ['function', 'method', 'class'];
+    for (const kind of kinds) {
+      for (const node of context.getNodesByKind(kind)) {
+        const lower = node.name.toLowerCase();
+        const existing = fuzzyIndex.get(lower);
+        if (existing) {
+          existing.push(node);
+        } else {
+          fuzzyIndex.set(lower, [node]);
+        }
+      }
+    }
+  }
 
   const lowerName = ref.referenceName.toLowerCase();
 
-  // Exact case-insensitive match
-  const caseInsensitive = allNodes.filter(
-    (n) => n.name.toLowerCase() === lowerName
-  );
+  // Exact case-insensitive match via index (O(1) lookup)
+  const caseInsensitive = fuzzyIndex.get(lowerName);
 
-  if (caseInsensitive.length === 1) {
+  if (caseInsensitive && caseInsensitive.length === 1) {
     return {
       original: ref,
       targetNodeId: caseInsensitive[0]!.id,
@@ -220,20 +238,7 @@ export function matchFuzzy(
     };
   }
 
-  // Try prefix match (e.g., "get" matches "getUser")
-  const prefixMatches = allNodes.filter((n) =>
-    n.name.toLowerCase().startsWith(lowerName)
-  );
-
-  if (prefixMatches.length === 1) {
-    return {
-      original: ref,
-      targetNodeId: prefixMatches[0]!.id,
-      confidence: 0.3,
-      resolvedBy: 'fuzzy',
-    };
-  }
-
+  // Skip prefix matching — too expensive and low value (confidence 0.3)
   return null;
 }