فهرست منبع

Optimize reference resolution with in-memory caches

The resolving refs phase stalled on large projects (3400+ files, 38k+ nodes)
because matchFuzzy loaded ALL functions/methods/classes per ref, import
mappings were re-extracted per ref, and fileExists hit disk every call.

Add kindCache, lowerNameCache, importMappingCache, and knownFiles set to
warmCaches(). Rewrite matchFuzzy to use O(1) lowercase index lookup instead
of 3x getNodesByKind scans. Cache import mappings per file. Pre-build file
existence set from the index for O(1) fileExists checks.
Colby McHenry 4 ماه پیش
والد
کامیت
acd9713632
4فایلهای تغییر یافته به همراه82 افزوده شده و 33 حذف شده
  1. 3 5
      src/resolution/import-resolver.ts
  2. 67 1
      src/resolution/index.ts
  3. 8 27
      src/resolution/name-matcher.ts
  4. 4 0
      src/resolution/types.ts

+ 3 - 5
src/resolution/import-resolver.ts

@@ -432,14 +432,12 @@ export function resolveViaImport(
   ref: UnresolvedRef,
   context: ResolutionContext
 ): ResolvedRef | null {
-  // Read the source file to extract imports
-  const content = context.readFile(ref.filePath);
-  if (!content) {
+  // Use cached import mappings (avoids re-reading and re-parsing per ref)
+  const imports = context.getImportMappings(ref.filePath, ref.language);
+  if (imports.length === 0 && !context.readFile(ref.filePath)) {
     return null;
   }
 
-  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 + '.')) {

+ 67 - 1
src/resolution/index.ts

@@ -15,9 +15,10 @@ import {
   ResolutionResult,
   ResolutionContext,
   FrameworkResolver,
+  ImportMapping,
 } from './types';
 import { matchReference } from './name-matcher';
-import { resolveViaImport } from './import-resolver';
+import { resolveViaImport, extractImportMappings } from './import-resolver';
 import { detectFrameworks } from './frameworks';
 import { logDebug } from '../errors';
 
@@ -39,6 +40,10 @@ export class ReferenceResolver {
   private nameCache: Map<string, Node[]> = new Map();
   private qualifiedNameCache: Map<string, Node[]> = new Map();
   private nodeByIdCache: Map<string, Node> = new Map();
+  private kindCache: Map<string, Node[]> = new Map();
+  private lowerNameCache: Map<string, Node[]> = new Map();
+  private importMappingCache: Map<string, ImportMapping[]> = new Map();
+  private knownFiles: Set<string> | null = null;
   private cachesWarmed = false;
 
   constructor(projectRoot: string, queries: QueryBuilder) {
@@ -82,8 +87,28 @@ export class ReferenceResolver {
 
       // Index by ID
       this.nodeByIdCache.set(node.id, node);
+
+      // Index by kind
+      const byKind = this.kindCache.get(node.kind);
+      if (byKind) {
+        byKind.push(node);
+      } else {
+        this.kindCache.set(node.kind, [node]);
+      }
+
+      // Index by lowercase name (for fuzzy matching)
+      const lowerName = node.name.toLowerCase();
+      const byLower = this.lowerNameCache.get(lowerName);
+      if (byLower) {
+        byLower.push(node);
+      } else {
+        this.lowerNameCache.set(lowerName, [node]);
+      }
     }
 
+    // Pre-build known files set from index
+    this.knownFiles = new Set(this.queries.getAllFiles().map((f) => f.path));
+
     this.cachesWarmed = true;
   }
 
@@ -96,6 +121,10 @@ export class ReferenceResolver {
     this.nameCache.clear();
     this.qualifiedNameCache.clear();
     this.nodeByIdCache.clear();
+    this.kindCache.clear();
+    this.lowerNameCache.clear();
+    this.importMappingCache.clear();
+    this.knownFiles = null;
     this.cachesWarmed = false;
   }
 
@@ -131,10 +160,21 @@ export class ReferenceResolver {
       },
 
       getNodesByKind: (kind: Node['kind']) => {
+        if (this.cachesWarmed) {
+          return this.kindCache.get(kind) ?? [];
+        }
         return this.queries.getNodesByKind(kind);
       },
 
       fileExists: (filePath: string) => {
+        // Check pre-built known files set first (O(1))
+        if (this.knownFiles) {
+          const normalized = filePath.replace(/\\/g, '/');
+          if (this.knownFiles.has(filePath) || this.knownFiles.has(normalized)) {
+            return true;
+          }
+        }
+        // Fall back to filesystem for files not yet indexed
         const fullPath = path.join(this.projectRoot, filePath);
         try {
           return fs.existsSync(fullPath);
@@ -168,6 +208,32 @@ export class ReferenceResolver {
       getAllFiles: () => {
         return this.queries.getAllFiles().map((f) => f.path);
       },
+
+      getNodesByLowerName: (lowerName: string) => {
+        if (this.cachesWarmed) {
+          return this.lowerNameCache.get(lowerName) ?? [];
+        }
+        // Fallback: scan all nodes (expensive, but only used if cache not warm)
+        return this.queries.getAllNodes().filter(
+          (n) => n.name.toLowerCase() === lowerName
+        );
+      },
+
+      getImportMappings: (filePath: string, language) => {
+        const cacheKey = filePath;
+        const cached = this.importMappingCache.get(cacheKey);
+        if (cached) return cached;
+
+        const content = this.context.readFile(filePath);
+        if (!content) {
+          this.importMappingCache.set(cacheKey, []);
+          return [];
+        }
+
+        const mappings = extractImportMappings(filePath, content, language);
+        this.importMappingCache.set(cacheKey, mappings);
+        return mappings;
+      },
     };
   }
 

+ 8 - 27
src/resolution/name-matcher.ts

@@ -197,39 +197,20 @@ export function matchFuzzy(
   ref: UnresolvedRef,
   context: ResolutionContext
 ): ResolvedRef | null {
-  // Try case-insensitive match
-  const allNodes = [
-    ...context.getNodesByKind('function'),
-    ...context.getNodesByKind('method'),
-    ...context.getNodesByKind('class'),
-  ];
-
   const lowerName = ref.referenceName.toLowerCase();
 
-  // Exact case-insensitive match
-  const caseInsensitive = allNodes.filter(
-    (n) => n.name.toLowerCase() === lowerName
-  );
+  // Use pre-built lowercase index for O(1) lookup instead of scanning all nodes
+  const candidates = context.getNodesByLowerName(lowerName);
 
-  if (caseInsensitive.length === 1) {
-    return {
-      original: ref,
-      targetNodeId: caseInsensitive[0]!.id,
-      confidence: 0.5,
-      resolvedBy: 'fuzzy',
-    };
-  }
+  // Filter to callable kinds only (function, method, class)
+  const callableKinds = new Set(['function', 'method', 'class']);
+  const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind));
 
-  // Try prefix match (e.g., "get" matches "getUser")
-  const prefixMatches = allNodes.filter((n) =>
-    n.name.toLowerCase().startsWith(lowerName)
-  );
-
-  if (prefixMatches.length === 1) {
+  if (callableCandidates.length === 1) {
     return {
       original: ref,
-      targetNodeId: prefixMatches[0]!.id,
-      confidence: 0.3,
+      targetNodeId: callableCandidates[0]!.id,
+      confidence: 0.5,
       resolvedBy: 'fuzzy',
     };
   }

+ 4 - 0
src/resolution/types.ts

@@ -79,6 +79,10 @@ export interface ResolutionContext {
   getProjectRoot(): string;
   /** Get all files */
   getAllFiles(): string[];
+  /** Get nodes by lowercase name (O(1) lookup for fuzzy matching) */
+  getNodesByLowerName(lowerName: string): Node[];
+  /** Get cached import mappings for a file */
+  getImportMappings(filePath: string, language: Language): ImportMapping[];
 }
 
 /**