@zuvia-software-solutions/code-mapper 2.3.7 → 2.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -164,26 +164,27 @@ export const analyzeCommand = async (inputPath, options) => {
164
164
  const cpuPct = Math.round(((cpuDelta.user + cpuDelta.system) / 1e3) / wallMs * 100);
165
165
  return `${rssMB}MB | CPU ${cpuPct}%`;
166
166
  };
167
- // Track elapsed time per phase both updateBar and the interval use
168
- // the same format so they don't flicker against each other
169
- let lastPhaseLabel = 'Initializing...';
167
+ // Track elapsed time per BASE phase (without counts) so the timer
168
+ // doesn't reset every time the count updates
169
+ let lastBasePhase = 'Initializing...';
170
+ let lastFullLabel = 'Initializing...';
170
171
  let phaseStart = Date.now();
171
- // Update bar with phase label + elapsed seconds (shown after 3s)
172
- const updateBar = (value, phaseLabel) => {
173
- if (phaseLabel !== lastPhaseLabel) {
174
- lastPhaseLabel = phaseLabel;
172
+ const updateBar = (value, phaseLabel, basePhase) => {
173
+ const base = basePhase ?? phaseLabel;
174
+ if (base !== lastBasePhase) {
175
+ lastBasePhase = base;
175
176
  phaseStart = Date.now();
176
177
  }
178
+ lastFullLabel = phaseLabel;
177
179
  const elapsed = Math.round((Date.now() - phaseStart) / 1000);
178
- const display = elapsed >= 3 ? `${phaseLabel} (${elapsed}s)` : phaseLabel;
180
+ const display = elapsed >= 1 ? `${phaseLabel} (${elapsed}s)` : phaseLabel;
179
181
  bar.update(value, { phase: display, resources: getResourceStats() });
180
182
  };
181
183
  // Tick elapsed seconds for phases with infrequent progress callbacks
182
- // (e.g. CSV streaming, FTS indexing) — uses the same display format as updateBar
183
184
  const elapsedTimer = setInterval(() => {
184
185
  const elapsed = Math.round((Date.now() - phaseStart) / 1000);
185
- if (elapsed >= 3) {
186
- bar.update({ phase: `${lastPhaseLabel} (${elapsed}s)`, resources: getResourceStats() });
186
+ if (elapsed >= 1) {
187
+ bar.update({ phase: `${lastFullLabel} (${elapsed}s)`, resources: getResourceStats() });
187
188
  }
188
189
  }, 1000);
189
190
  // Cache embeddings from existing index before rebuild
@@ -218,13 +219,13 @@ export const analyzeCommand = async (inputPath, options) => {
218
219
  recordPhase(progress.phase);
219
220
  lastPipelinePhase = progress.phase;
220
221
  }
221
- let phaseLabel = PHASE_LABELS[progress.phase] || progress.phase;
222
- if (progress.stats && progress.stats.totalFiles > 0 &&
223
- (progress.phase === 'parsing' || progress.phase === 'extracting' || progress.phase === 'calls')) {
222
+ const baseLabel = PHASE_LABELS[progress.phase] || progress.phase;
223
+ let phaseLabel = baseLabel;
224
+ if (progress.stats && progress.stats.totalFiles > 0) {
224
225
  phaseLabel += ` (${progress.stats.filesProcessed.toLocaleString()}/${progress.stats.totalFiles.toLocaleString()})`;
225
226
  }
226
227
  const scaled = Math.round(progress.percent * 0.6);
227
- updateBar(scaled, phaseLabel);
228
+ updateBar(scaled, phaseLabel, baseLabel);
228
229
  }, options?.tsgo === false ? { tsgo: false } : {});
229
230
  // Phase 2: SQLite (60-85%)
230
231
  recordPhase('sqlite');
@@ -3,7 +3,7 @@ import type { KnowledgeGraph } from '../graph/types.js';
3
3
  import type { ASTCache } from './ast-cache.js';
4
4
  import type { ResolutionContext } from './resolution-context.js';
5
5
  import type { ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
6
- import type { TsgoService } from '../semantic/tsgo-service.js';
6
+ import { TsgoService } from '../semantic/tsgo-service.js';
7
7
  export declare const processCalls: (graph: KnowledgeGraph, files: {
8
8
  path: string;
9
9
  content: string;
@@ -10,6 +10,7 @@ import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, F
10
10
  import { buildTypeEnv } from './type-env.js';
11
11
  import { getTreeSitterBufferSize } from './constants.js';
12
12
  import { callRouters } from './call-routing.js';
13
+ import { TsgoService } from '../semantic/tsgo-service.js';
13
14
  import path from 'node:path';
14
15
  /** Walk up the AST to find the enclosing function/method, or null for top-level code */
15
16
  const findEnclosingFunction = (node, filePath, ctx) => {
@@ -809,80 +810,160 @@ async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPat
809
810
  }
810
811
  list.push(call);
811
812
  }
812
- let resolved = 0;
813
- let failed = 0;
814
813
  const t0 = Date.now();
815
814
  const skippedTotal = skippedUnambiguous + skippedKnownType + skippedBuiltin;
816
- console.error(`Code Mapper: tsgo resolving ${tsgoEligible.length} calls across ${tsgoByFile.size} files (skipped ${skippedTotal}: ${skippedUnambiguous} unambiguous, ${skippedKnownType} known-type, ${skippedBuiltin} builtin)...`);
817
- let tsgoFilesProcessed = 0;
815
+ // Adaptive parallelism based on three constraints:
816
+ // 1. CPU: 75% of cores — parsing workers are done, leave 25% for Node.js event loop + OS
817
+ // 2. Memory: each tsgo loads the full project (~500MB estimate) — cap by free system memory
818
+ // 3. Workload: at least 50 files per process to amortize ~0.5s startup cost
819
+ const osModule = await import('os');
820
+ const cpuCount = osModule.cpus().length;
821
+ const freeMemGB = osModule.freemem() / (1024 * 1024 * 1024);
822
+ const maxByCpu = Math.max(1, Math.floor(cpuCount * 0.75));
823
+ const maxByMemory = Math.max(1, Math.floor(freeMemGB / 0.5));
824
+ const maxByWorkload = Math.max(1, Math.floor(tsgoByFile.size / 50));
825
+ const actualWorkers = Math.min(maxByCpu, maxByMemory, maxByWorkload);
826
+ console.error(`Code Mapper: tsgo resolving ${tsgoEligible.length} calls across ${tsgoByFile.size} files with ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''} (skipped ${skippedTotal}: ${skippedUnambiguous} unambiguous, ${skippedKnownType} known-type, ${skippedBuiltin} builtin)...`);
827
+ // Split files round-robin across workers for balanced distribution
828
+ const fileEntries = [...tsgoByFile.entries()];
829
+ const workerSlices = Array.from({ length: actualWorkers }, () => []);
830
+ for (let i = 0; i < fileEntries.length; i++) {
831
+ workerSlices[i % actualWorkers].push(fileEntries[i]);
832
+ }
833
+ // Shared progress counter
834
+ let totalFilesProcessed = 0;
818
835
  const tsgoTotalFiles = tsgoByFile.size;
819
- for (const [filePath, calls] of tsgoByFile) {
820
- tsgoFilesProcessed++;
821
- if (tsgoFilesProcessed % 25 === 0) {
822
- onProgress?.(tsgoFilesProcessed, tsgoTotalFiles);
823
- await yieldToEventLoop();
824
- }
825
- const absFilePath = path.resolve(repoPath, filePath);
826
- // Sequential LSP requests tsgo processes over stdio, concurrent floods cause hangs
827
- for (const call of calls) {
828
- try {
829
- const def = await tsgoService.resolveDefinition(absFilePath, call.callLine - 1, call.callColumn);
830
- if (!def) {
831
- failed++;
832
- continue;
833
- }
834
- const targetSymbols = ctx.symbols.lookupAllInFile(def.filePath);
835
- if (targetSymbols.length === 0) {
836
- failed++;
837
- continue;
838
- }
839
- // Match by exact startLine, then by range containment
840
- let bestMatch;
841
- for (const sym of targetSymbols) {
842
- const node = graph.getNode(toNodeId(sym.nodeId));
843
- if (node && node.properties.startLine === def.line) {
844
- bestMatch = sym;
845
- break;
836
+ /** Resolve a slice of files using a single tsgo service */
837
+ const resolveSlice = async (service, slice) => {
838
+ const sliceResults = new Map();
839
+ let sliceResolved = 0;
840
+ let sliceFailed = 0;
841
+ for (const [filePath, calls] of slice) {
842
+ totalFilesProcessed++;
843
+ if (totalFilesProcessed % 25 === 0) {
844
+ onProgress?.(totalFilesProcessed, tsgoTotalFiles);
845
+ }
846
+ const absFilePath = path.resolve(repoPath, filePath);
847
+ for (const call of calls) {
848
+ try {
849
+ const def = await service.resolveDefinition(absFilePath, call.callLine - 1, call.callColumn);
850
+ if (!def) {
851
+ sliceFailed++;
852
+ continue;
846
853
  }
847
- }
848
- if (!bestMatch) {
854
+ const targetSymbols = ctx.symbols.lookupAllInFile(def.filePath);
855
+ if (targetSymbols.length === 0) {
856
+ sliceFailed++;
857
+ continue;
858
+ }
859
+ // Match by exact startLine, then by range containment
860
+ let bestMatch;
849
861
  for (const sym of targetSymbols) {
850
862
  const node = graph.getNode(toNodeId(sym.nodeId));
851
- if (node) {
852
- const sl = node.properties.startLine;
853
- const el = node.properties.endLine;
854
- if (sl !== undefined && el !== undefined && def.line >= sl && def.line <= el) {
855
- bestMatch = sym;
856
- break;
863
+ if (node && node.properties.startLine === def.line) {
864
+ bestMatch = sym;
865
+ break;
866
+ }
867
+ }
868
+ if (!bestMatch) {
869
+ for (const sym of targetSymbols) {
870
+ const node = graph.getNode(toNodeId(sym.nodeId));
871
+ if (node) {
872
+ const sl = node.properties.startLine;
873
+ const el = node.properties.endLine;
874
+ if (sl !== undefined && el !== undefined && def.line >= sl && def.line <= el) {
875
+ bestMatch = sym;
876
+ break;
877
+ }
857
878
  }
858
879
  }
859
880
  }
860
- }
861
- if (bestMatch) {
862
- // Drop self-referencing tsgo edges
863
- if (bestMatch.nodeId === call.sourceId) {
864
- failed++;
865
- continue;
881
+ if (bestMatch) {
882
+ if (bestMatch.nodeId === call.sourceId) {
883
+ sliceFailed++;
884
+ continue;
885
+ }
886
+ const callKey = `${call.sourceId}\0${call.calledName}\0${call.callLine}`;
887
+ sliceResults.set(callKey, {
888
+ nodeId: bestMatch.nodeId,
889
+ confidence: TIER_CONFIDENCE['tsgo-resolved'],
890
+ reason: 'tsgo-lsp',
891
+ });
892
+ sliceResolved++;
893
+ }
894
+ else {
895
+ sliceFailed++;
866
896
  }
867
- const callKey = `${call.sourceId}\0${call.calledName}\0${call.callLine}`;
868
- results.set(callKey, {
869
- nodeId: bestMatch.nodeId,
870
- confidence: TIER_CONFIDENCE['tsgo-resolved'],
871
- reason: 'tsgo-lsp',
872
- });
873
- resolved++;
874
897
  }
875
- else {
876
- failed++;
898
+ catch {
899
+ sliceFailed++;
900
+ }
901
+ }
902
+ }
903
+ return { resolved: sliceResolved, failed: sliceFailed, results: sliceResults };
904
+ };
905
+ let resolved = 0;
906
+ let failed = 0;
907
+ if (actualWorkers === 1) {
908
+ // Single process — use the existing service (already started)
909
+ const outcome = await resolveSlice(tsgoService, fileEntries);
910
+ resolved = outcome.resolved;
911
+ failed = outcome.failed;
912
+ for (const [k, v] of outcome.results)
913
+ results.set(k, v);
914
+ }
915
+ else {
916
+ // Parallel — spawn extra services, keep the original for slice 0
917
+ const extraServices = [];
918
+ try {
919
+ // Start extra tsgo processes in parallel
920
+ const startPromises = [];
921
+ for (let i = 1; i < actualWorkers; i++) {
922
+ startPromises.push((async () => {
923
+ const svc = new TsgoService(repoPath);
924
+ if (await svc.start())
925
+ return svc;
926
+ return null;
927
+ })());
928
+ }
929
+ const started = await Promise.all(startPromises);
930
+ for (const svc of started) {
931
+ if (svc)
932
+ extraServices.push(svc);
933
+ }
934
+ // Build final service list: original + extras that started successfully
935
+ const services = [tsgoService, ...extraServices];
936
+ const activeSlices = workerSlices.slice(0, services.length);
937
+ // If some services failed to start, redistribute their slices
938
+ if (services.length < actualWorkers) {
939
+ for (let i = services.length; i < actualWorkers; i++) {
940
+ const orphanSlice = workerSlices[i];
941
+ if (orphanSlice) {
942
+ // Distribute orphan files round-robin across active services
943
+ for (let j = 0; j < orphanSlice.length; j++) {
944
+ activeSlices[j % services.length].push(orphanSlice[j]);
945
+ }
946
+ }
877
947
  }
878
948
  }
879
- catch {
880
- failed++;
949
+ console.error(`Code Mapper: ${services.length} tsgo processes ready, resolving in parallel...`);
950
+ // Resolve all slices in parallel
951
+ const outcomes = await Promise.all(activeSlices.map((slice, i) => resolveSlice(services[i], slice)));
952
+ for (const outcome of outcomes) {
953
+ resolved += outcome.resolved;
954
+ failed += outcome.failed;
955
+ for (const [k, v] of outcome.results)
956
+ results.set(k, v);
881
957
  }
882
958
  }
959
+ finally {
960
+ // Stop extra services (the original is stopped by the caller)
961
+ for (const svc of extraServices)
962
+ svc.stop();
963
+ }
883
964
  }
884
965
  const elapsed = Date.now() - t0;
885
- console.error(`Code Mapper: tsgo resolved ${resolved}/${eligible.length} calls in ${elapsed}ms (${failed} unresolvable)`);
966
+ console.error(`Code Mapper: tsgo resolved ${resolved}/${eligible.length} calls in ${elapsed}ms (${failed} unresolvable, ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''})`);
886
967
  return results;
887
968
  }
888
969
  /** Generic method names that produce false edges when receiver type is unknown (worker-extracted path) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuvia-software-solutions/code-mapper",
3
- "version": "2.3.7",
3
+ "version": "2.3.8",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",