gitnexus 1.6.6-rc.39 → 1.6.6-rc.40
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.
- package/dist/cli/analyze.js +2 -0
- package/dist/core/ingestion/call-processor.js +55 -1
- package/dist/core/ingestion/model/heritage-map.js +34 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +40 -5
- package/dist/core/ingestion/registry-primary-flag.js +2 -5
- package/dist/core/ingestion/utils/deferred-resolution-profile.d.ts +55 -0
- package/dist/core/ingestion/utils/deferred-resolution-profile.js +107 -0
- package/dist/core/ingestion/utils/env.d.ts +13 -0
- package/dist/core/ingestion/utils/env.js +18 -0
- package/dist/core/ingestion/utils/verbose.js +2 -7
- package/package.json +1 -1
package/dist/cli/analyze.js
CHANGED
|
@@ -374,6 +374,8 @@ async function ensureHeap() {
|
|
|
374
374
|
*/
|
|
375
375
|
const ANALYZE_CLI_ENV_KEYS = [
|
|
376
376
|
'GITNEXUS_VERBOSE',
|
|
377
|
+
'GITNEXUS_PROFILE_DEFERRED',
|
|
378
|
+
'GITNEXUS_PROFILE_DEFERRED_SLOW_MS',
|
|
377
379
|
'GITNEXUS_MAX_FILE_SIZE',
|
|
378
380
|
'GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS',
|
|
379
381
|
'GITNEXUS_EMBEDDING_THREADS',
|
|
@@ -28,6 +28,7 @@ import { generateId } from '../../lib/utils.js';
|
|
|
28
28
|
import { getLanguageFromFilename, SupportedLanguages } from '../../_shared/index.js';
|
|
29
29
|
import { isRegistryPrimary } from './registry-primary-flag.js';
|
|
30
30
|
import { isVerboseIngestionEnabled } from './utils/verbose.js';
|
|
31
|
+
import { deferredCallFileSlowMs, deferredCallLogEveryN, getDeferredProfileDroppedCount, isDeferredResolutionProfileEnabled, logDeferredProfile, profileElapsedMs, resetDeferredProfileDroppedCount, startTimer, } from './utils/deferred-resolution-profile.js';
|
|
31
32
|
import { yieldToEventLoop } from './utils/event-loop.js';
|
|
32
33
|
import { parseSourceSafe } from '../tree-sitter/safe-parse.js';
|
|
33
34
|
import { CLASS_CONTAINER_TYPES, FUNCTION_NODE_TYPES, findEnclosingClassInfo, genericFuncName, inferFunctionLabel, } from './utils/ast-helpers.js';
|
|
@@ -2292,6 +2293,39 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
2292
2293
|
}
|
|
2293
2294
|
const totalFiles = byFile.size;
|
|
2294
2295
|
let filesProcessed = 0;
|
|
2296
|
+
// Counts only files that survived the registry-primary skip — what the user
|
|
2297
|
+
// is actually waiting on. Keyed by this counter, the first per-file progress
|
|
2298
|
+
// log fires on the first *resolved* file rather than file #1 of byFile,
|
|
2299
|
+
// which would silently land inside the skip block on mixed Python+JVM repos
|
|
2300
|
+
// where the skipped language sorts first.
|
|
2301
|
+
let resolvedFiles = 0;
|
|
2302
|
+
const profileCalls = isDeferredResolutionProfileEnabled();
|
|
2303
|
+
const slowFileMs = profileCalls ? deferredCallFileSlowMs() : 0;
|
|
2304
|
+
const logEveryN = profileCalls ? deferredCallLogEveryN() : 0;
|
|
2305
|
+
let skippedRegistryPrimaryFiles = 0;
|
|
2306
|
+
// Fresh dropped-log counter per analyze run — the module-private counter
|
|
2307
|
+
// in deferred-resolution-profile.ts is process-lived, so without a reset
|
|
2308
|
+
// here it would accumulate across consecutive analyze invocations in the
|
|
2309
|
+
// same Node process (e.g., the MCP server, eval harness, integration
|
|
2310
|
+
// tests).
|
|
2311
|
+
if (profileCalls)
|
|
2312
|
+
resetDeferredProfileDroppedCount();
|
|
2313
|
+
// One-pass pre-count of the eventual non-skipped total so the live progress
|
|
2314
|
+
// denominator stays stable as the loop iterates. Otherwise `${totalFiles -
|
|
2315
|
+
// skippedRegistryPrimaryFiles}` drifts upward — files iterated before later
|
|
2316
|
+
// registry-primary skips have been seen carry an inflated denominator, and
|
|
2317
|
+
// the ratio only self-corrects after every file has been classified. Pre-
|
|
2318
|
+
// count runs only on the enabled path so the disabled path stays free of
|
|
2319
|
+
// the extra Map iteration. Defaults to 0 on the disabled path; the live log
|
|
2320
|
+
// gate is also disabled there, so the value is never read.
|
|
2321
|
+
let resolvedTotal = 0;
|
|
2322
|
+
if (profileCalls) {
|
|
2323
|
+
for (const filePath of byFile.keys()) {
|
|
2324
|
+
const lang = getLanguageFromFilename(filePath);
|
|
2325
|
+
if (!lang || !isRegistryPrimary(lang))
|
|
2326
|
+
resolvedTotal++;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2295
2329
|
for (const [filePath, calls] of byFile) {
|
|
2296
2330
|
filesProcessed++;
|
|
2297
2331
|
if (filesProcessed % 100 === 0) {
|
|
@@ -2301,8 +2335,15 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
2301
2335
|
// Registry-primary gate: skip Python (etc.) entirely when the
|
|
2302
2336
|
// scope-based phase owns CALLS for this language.
|
|
2303
2337
|
const fileLanguage = getLanguageFromFilename(filePath);
|
|
2304
|
-
if (fileLanguage && isRegistryPrimary(fileLanguage))
|
|
2338
|
+
if (fileLanguage && isRegistryPrimary(fileLanguage)) {
|
|
2339
|
+
skippedRegistryPrimaryFiles++;
|
|
2305
2340
|
continue;
|
|
2341
|
+
}
|
|
2342
|
+
resolvedFiles++;
|
|
2343
|
+
const tFile = startTimer(profileCalls);
|
|
2344
|
+
if (profileCalls && (resolvedFiles === 1 || resolvedFiles % logEveryN === 0)) {
|
|
2345
|
+
logDeferredProfile(`calls ${resolvedFiles}/${resolvedTotal} file=${filePath} sites=${calls.length}`);
|
|
2346
|
+
}
|
|
2306
2347
|
ctx.enableCache(filePath);
|
|
2307
2348
|
const widenCache = new Map();
|
|
2308
2349
|
const receiverMap = fileReceiverTypes.get(filePath);
|
|
@@ -2408,6 +2449,19 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
2408
2449
|
}
|
|
2409
2450
|
}
|
|
2410
2451
|
ctx.clearCache();
|
|
2452
|
+
if (tFile !== null) {
|
|
2453
|
+
const elapsed = profileElapsedMs(tFile);
|
|
2454
|
+
if (elapsed >= slowFileMs) {
|
|
2455
|
+
logDeferredProfile(`slow file ${elapsed.toFixed(0)}ms path=${filePath} calls=${calls.length} lang=${fileLanguage ?? 'unknown'}`);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (profileCalls) {
|
|
2460
|
+
logDeferredProfile(`processCallsFromExtracted done: ${totalFiles} files, ${extractedCalls.length} call sites, skipped registry-primary files=${skippedRegistryPrimaryFiles}`);
|
|
2461
|
+
const droppedCount = getDeferredProfileDroppedCount();
|
|
2462
|
+
if (droppedCount > 0) {
|
|
2463
|
+
logDeferredProfile(`note: ${droppedCount} profile log lines dropped (logger errors)`);
|
|
2464
|
+
}
|
|
2411
2465
|
}
|
|
2412
2466
|
onProgress?.(totalFiles, totalFiles);
|
|
2413
2467
|
};
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* classes implementing a given interface)
|
|
14
14
|
*/
|
|
15
15
|
import { getLanguageFromFilename } from '../../../_shared/index.js';
|
|
16
|
+
import { isDeferredResolutionProfileEnabled, logDeferredProfile, } from '../utils/deferred-resolution-profile.js';
|
|
16
17
|
/**
|
|
17
18
|
* Determine whether a heritage.extends capture is actually an IMPLEMENTS
|
|
18
19
|
* relationship. Consults the symbol table first (authoritative — Tier 1 /
|
|
@@ -72,10 +73,35 @@ export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
|
|
|
72
73
|
const seenParents = new Map();
|
|
73
74
|
// interfaceName → Set<filePath> (implementor lookup for interface dispatch)
|
|
74
75
|
const implementorFiles = new Map();
|
|
76
|
+
const profileHeritage = isDeferredResolutionProfileEnabled();
|
|
77
|
+
let maxNameCartesian = 0;
|
|
78
|
+
let ambiguousHeritageRecords = 0;
|
|
79
|
+
let unresolvedChildLookups = 0;
|
|
80
|
+
let unresolvedParentLookups = 0;
|
|
75
81
|
for (const h of heritage) {
|
|
76
82
|
// ── Parent lookup (nodeId-based) ────────────────────────────────
|
|
77
83
|
const childDefs = ctx.model.types.lookupClassByName(h.className);
|
|
78
84
|
const parentDefs = ctx.model.types.lookupClassByName(h.parentName);
|
|
85
|
+
// Unresolved-side counters live in a separate guard so they observe
|
|
86
|
+
// records the ambiguity block below skips. On JVM monorepos the
|
|
87
|
+
// pathological fan-out case is precisely "many same-named children
|
|
88
|
+
// with an unresolved external supertype" (or the inverse) — both
|
|
89
|
+
// sides non-empty is the case `ambiguousHeritageRecords` already
|
|
90
|
+
// covers; the unresolved cases were silently dropped from the
|
|
91
|
+
// metric before this counter.
|
|
92
|
+
if (profileHeritage) {
|
|
93
|
+
if (childDefs.length === 0)
|
|
94
|
+
unresolvedChildLookups++;
|
|
95
|
+
if (parentDefs.length === 0)
|
|
96
|
+
unresolvedParentLookups++;
|
|
97
|
+
}
|
|
98
|
+
if (profileHeritage && childDefs.length > 0 && parentDefs.length > 0) {
|
|
99
|
+
const product = childDefs.length * parentDefs.length;
|
|
100
|
+
if (product > 1)
|
|
101
|
+
ambiguousHeritageRecords++;
|
|
102
|
+
if (product > maxNameCartesian)
|
|
103
|
+
maxNameCartesian = product;
|
|
104
|
+
}
|
|
79
105
|
if (childDefs.length > 0 && parentDefs.length > 0) {
|
|
80
106
|
for (const child of childDefs) {
|
|
81
107
|
for (const parent of parentDefs) {
|
|
@@ -249,6 +275,14 @@ export const buildHeritageMap = (heritage, ctx, getHeritageStrategy) => {
|
|
|
249
275
|
const getImplementorFiles = (interfaceName) => {
|
|
250
276
|
return implementorFiles.get(interfaceName) ?? EMPTY_SET;
|
|
251
277
|
};
|
|
278
|
+
if (profileHeritage) {
|
|
279
|
+
logDeferredProfile(`buildHeritageMap: ${heritage.length} heritage records, ` +
|
|
280
|
+
`${ambiguousHeritageRecords} with child×parent lookup product >1, ` +
|
|
281
|
+
`max product ${maxNameCartesian}, ` +
|
|
282
|
+
`${unresolvedChildLookups} unresolved child lookups, ` +
|
|
283
|
+
`${unresolvedParentLookups} unresolved parent lookups, ` +
|
|
284
|
+
`${implementorFiles.size} interface implementor keys`);
|
|
285
|
+
}
|
|
252
286
|
return {
|
|
253
287
|
getParents,
|
|
254
288
|
getAncestors,
|
|
@@ -31,6 +31,7 @@ import path from 'node:path';
|
|
|
31
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
32
|
import { isDev } from '../utils/env.js';
|
|
33
33
|
import { isVerboseIngestionEnabled } from '../utils/verbose.js';
|
|
34
|
+
import { endTimer, isDeferredResolutionProfileEnabled, logDeferredProfile, startTimer, } from '../utils/deferred-resolution-profile.js';
|
|
34
35
|
import { synthesizeWildcardImportBindings, needsSynthesis } from './wildcard-synthesis.js';
|
|
35
36
|
import { extractORMQueriesInline } from './orm-extraction.js';
|
|
36
37
|
import { logger } from '../../logger.js';
|
|
@@ -564,7 +565,13 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
564
565
|
// heritage: 75 -> 80 (5)
|
|
565
566
|
// routes: 80 -> 85 (5)
|
|
566
567
|
// calls: 85 -> 95 (10)
|
|
568
|
+
const deferredProfile = isDeferredResolutionProfileEnabled();
|
|
569
|
+
if (deferredProfile) {
|
|
570
|
+
logDeferredProfile(`deferred band start: imports=${deferredWorkerImports.length} heritage=${deferredWorkerHeritage.length} ` +
|
|
571
|
+
`calls=${deferredWorkerCalls.length} routes=${allExtractedRoutes.length}`);
|
|
572
|
+
}
|
|
567
573
|
if (deferredWorkerImports.length > 0) {
|
|
574
|
+
const tImports = startTimer(deferredProfile);
|
|
568
575
|
await processImportsFromExtracted(graph, allPathObjects, deferredWorkerImports, ctx, (current, total) => {
|
|
569
576
|
const ratio = total > 0 ? current / total : 1;
|
|
570
577
|
onProgress({
|
|
@@ -579,6 +586,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
579
586
|
},
|
|
580
587
|
});
|
|
581
588
|
}, repoPath, importCtx);
|
|
589
|
+
endTimer(tImports, (ms) => `processImportsFromExtracted: ${ms.toFixed(0)}ms (${deferredWorkerImports.length} import batches before drain)`);
|
|
582
590
|
// U15 (lightweight M1): processImportsFromExtracted is the sole
|
|
583
591
|
// consumer of `deferredWorkerImports`. Free the array now so the
|
|
584
592
|
// GC can reclaim the per-file ExtractedImport records before the
|
|
@@ -590,8 +598,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
590
598
|
deferredWorkerImports.length = 0;
|
|
591
599
|
}
|
|
592
600
|
if (anyChunkNeedsWildcardSynth) {
|
|
601
|
+
const tWildcard = startTimer(deferredProfile);
|
|
593
602
|
synthesizeWildcardImportBindings(graph, ctx);
|
|
594
603
|
hasSynthesized = true;
|
|
604
|
+
endTimer(tWildcard, (ms) => `synthesizeWildcardImportBindings: ${ms.toFixed(0)}ms`);
|
|
595
605
|
}
|
|
596
606
|
// L5 from PR #1693 review: populate `exportedTypeMap` from the in-progress
|
|
597
607
|
// graph BEFORE `seedCrossFileReceiverTypes` runs. Previously the seeding
|
|
@@ -609,11 +619,22 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
609
619
|
}
|
|
610
620
|
if (exportedTypeMap.size > 0 && ctx.namedImportMap.size > 0 && deferredWorkerCalls.length > 0) {
|
|
611
621
|
const { enrichedCount } = seedCrossFileReceiverTypes(deferredWorkerCalls, ctx.namedImportMap, exportedTypeMap);
|
|
612
|
-
if (
|
|
613
|
-
|
|
622
|
+
if (enrichedCount > 0) {
|
|
623
|
+
// Two independent gates, not else-if: when both isDev AND
|
|
624
|
+
// deferredProfile are active, BOTH lines fire — log scrapers keyed
|
|
625
|
+
// on the original "🔗 E1" emoji marker keep matching, AND operators
|
|
626
|
+
// grepping the [deferred-profile] prefix see no gap between the
|
|
627
|
+
// wildcard-synth and heritage timings.
|
|
628
|
+
if (isDev) {
|
|
629
|
+
logger.info(`🔗 E1: Seeded ${enrichedCount} cross-file receiver types (all chunks)`);
|
|
630
|
+
}
|
|
631
|
+
if (deferredProfile) {
|
|
632
|
+
logDeferredProfile(`E1: seeded ${enrichedCount} cross-file receiver types (all chunks)`);
|
|
633
|
+
}
|
|
614
634
|
}
|
|
615
635
|
}
|
|
616
636
|
if (deferredWorkerHeritage.length > 0) {
|
|
637
|
+
const tHeritage = startTimer(deferredProfile);
|
|
617
638
|
await processHeritageFromExtracted(graph, deferredWorkerHeritage, ctx, (current, total) => {
|
|
618
639
|
const ratio = total > 0 ? current / total : 1;
|
|
619
640
|
onProgress({
|
|
@@ -628,8 +649,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
628
649
|
},
|
|
629
650
|
});
|
|
630
651
|
});
|
|
652
|
+
endTimer(tHeritage, (ms) => `processHeritageFromExtracted: ${ms.toFixed(0)}ms (${deferredWorkerHeritage.length} records)`);
|
|
631
653
|
}
|
|
632
654
|
if (allExtractedRoutes.length > 0) {
|
|
655
|
+
const tRoutes = startTimer(deferredProfile);
|
|
633
656
|
await processRoutesFromExtracted(graph, allExtractedRoutes, ctx, (current, total) => {
|
|
634
657
|
const ratio = total > 0 ? current / total : 1;
|
|
635
658
|
onProgress({
|
|
@@ -644,10 +667,17 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
644
667
|
},
|
|
645
668
|
});
|
|
646
669
|
});
|
|
670
|
+
endTimer(tRoutes, (ms) => `processRoutesFromExtracted: ${ms.toFixed(0)}ms (${allExtractedRoutes.length} routes)`);
|
|
671
|
+
}
|
|
672
|
+
let fullWorkerHeritageMap;
|
|
673
|
+
if (deferredWorkerHeritage.length > 0) {
|
|
674
|
+
const tBuildHeritage = startTimer(deferredProfile);
|
|
675
|
+
fullWorkerHeritageMap = buildHeritageMap(deferredWorkerHeritage, ctx, getHeritageStrategyForLanguage);
|
|
676
|
+
endTimer(tBuildHeritage, (ms) => `buildHeritageMap wall: ${ms.toFixed(0)}ms`);
|
|
677
|
+
}
|
|
678
|
+
else if (deferredProfile) {
|
|
679
|
+
logDeferredProfile('buildHeritageMap: skipped (no heritage records)');
|
|
647
680
|
}
|
|
648
|
-
const fullWorkerHeritageMap = deferredWorkerHeritage.length > 0
|
|
649
|
-
? buildHeritageMap(deferredWorkerHeritage, ctx, getHeritageStrategyForLanguage)
|
|
650
|
-
: undefined;
|
|
651
681
|
// U15 (lightweight M1): buildHeritageMap is the LAST consumer of the
|
|
652
682
|
// raw `deferredWorkerHeritage` records — processCallsFromExtracted
|
|
653
683
|
// below reads from the derived `fullWorkerHeritageMap` instead. Free
|
|
@@ -656,6 +686,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
656
686
|
// earlier was a read-only consumer (pushed to graph, didn't drain).
|
|
657
687
|
deferredWorkerHeritage.length = 0;
|
|
658
688
|
if (deferredWorkerCalls.length > 0) {
|
|
689
|
+
if (deferredProfile) {
|
|
690
|
+
logDeferredProfile(`processCallsFromExtracted: starting (${deferredWorkerCalls.length} call sites, heritageMap=${fullWorkerHeritageMap !== undefined})`);
|
|
691
|
+
}
|
|
692
|
+
const tCalls = startTimer(deferredProfile);
|
|
659
693
|
await processCallsFromExtracted(graph, deferredWorkerCalls, ctx, (current, total) => {
|
|
660
694
|
const ratio = total > 0 ? current / total : 1;
|
|
661
695
|
onProgress({
|
|
@@ -673,6 +707,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
673
707
|
},
|
|
674
708
|
});
|
|
675
709
|
}, deferredConstructorBindings.length > 0 ? deferredConstructorBindings : undefined, fullWorkerHeritageMap, bindingAccumulator);
|
|
710
|
+
endTimer(tCalls, (ms) => `processCallsFromExtracted: ${ms.toFixed(0)}ms total`);
|
|
676
711
|
}
|
|
677
712
|
if (deferredAssignments.length > 0) {
|
|
678
713
|
processAssignmentsFromExtracted(graph, deferredAssignments, ctx, deferredConstructorBindings.length > 0 ? deferredConstructorBindings : undefined, bindingAccumulator);
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
* lives in `shadow-harness.ts` (#923), not here.
|
|
37
37
|
*/
|
|
38
38
|
import { SupportedLanguages } from '../../_shared/index.js';
|
|
39
|
+
import { parseTruthyEnv } from './utils/env.js';
|
|
39
40
|
/**
|
|
40
41
|
* Languages whose RFC #909 Ring 3 scope-resolution migration is complete.
|
|
41
42
|
*
|
|
@@ -110,10 +111,6 @@ export function primaryLanguages() {
|
|
|
110
111
|
return out;
|
|
111
112
|
}
|
|
112
113
|
// ─── Internal ───────────────────────────────────────────────────────────────
|
|
113
|
-
/** Accepted truthy strings (case-insensitive, trimmed). */
|
|
114
|
-
const TRUTHY_VALUES = new Set(['true', '1', 'yes']);
|
|
115
114
|
function parseFlag(raw) {
|
|
116
|
-
|
|
117
|
-
return false;
|
|
118
|
-
return TRUTHY_VALUES.has(raw.trim().toLowerCase());
|
|
115
|
+
return parseTruthyEnv(raw);
|
|
119
116
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall-clock logging for the post-chunk deferred resolution band
|
|
3
|
+
* (imports → heritage → heritage map → legacy call resolution).
|
|
4
|
+
*
|
|
5
|
+
* Enabled when either:
|
|
6
|
+
* - `GITNEXUS_VERBOSE=1` / `gitnexus analyze -v` (primary path for #1741), or
|
|
7
|
+
* - `GITNEXUS_PROFILE_DEFERRED=1` (force on without full verbose ingestion noise)
|
|
8
|
+
*
|
|
9
|
+
* Issue #1741: large Java/Kotlin repos appear stuck at "Resolving calls"
|
|
10
|
+
* because the UI progress bar updates every 100 files and intermediate
|
|
11
|
+
* stages emit little to the log.
|
|
12
|
+
*/
|
|
13
|
+
/** True when deferred-stage timing / progress logs should emit. */
|
|
14
|
+
export declare const isDeferredResolutionProfileEnabled: () => boolean;
|
|
15
|
+
/** Log a call-resolution progress line every N files (finer when verbose). */
|
|
16
|
+
export declare const deferredCallLogEveryN: () => number;
|
|
17
|
+
/** Per-file call-resolution log threshold (ms). Lower default when verbose. */
|
|
18
|
+
export declare const deferredCallFileSlowMs: () => number;
|
|
19
|
+
export declare const profileNow: () => bigint;
|
|
20
|
+
export declare const profileElapsedMs: (start: bigint) => number;
|
|
21
|
+
/**
|
|
22
|
+
* Number of `logDeferredProfile` calls whose underlying `logger.info` threw.
|
|
23
|
+
* Surfaced in the deferred-band done-summary when greater than zero.
|
|
24
|
+
*/
|
|
25
|
+
export declare const getDeferredProfileDroppedCount: () => number;
|
|
26
|
+
/**
|
|
27
|
+
* Reset the dropped-line counter. Call from test `afterEach` to keep the
|
|
28
|
+
* module-private state from leaking across tests. Also used inside
|
|
29
|
+
* `processCallsFromExtracted` at function entry so each analyze run gets
|
|
30
|
+
* a fresh count rather than accumulating across the process lifetime.
|
|
31
|
+
*/
|
|
32
|
+
export declare const resetDeferredProfileDroppedCount: () => void;
|
|
33
|
+
export declare const logDeferredProfile: (message: string) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Capture a monotonic timestamp when profiling is enabled; otherwise return null.
|
|
36
|
+
* Pair with `endTimer` so the type system narrows correctly — using `null` instead
|
|
37
|
+
* of a `0n` sentinel makes "profiling disabled" structurally distinct from
|
|
38
|
+
* "zero elapsed time" and lets TypeScript catch missing guards.
|
|
39
|
+
*/
|
|
40
|
+
export declare const startTimer: (enabled: boolean) => bigint | null;
|
|
41
|
+
/**
|
|
42
|
+
* Emit a `[deferred-profile]` log line for a captured timer. No-op when the
|
|
43
|
+
* timer is `null` (profiling was disabled at capture time). The formatter
|
|
44
|
+
* receives elapsed ms so the call sites stay readable.
|
|
45
|
+
*
|
|
46
|
+
* The format callback runs inside a try/catch so a throwing formatter
|
|
47
|
+
* (custom toString, JSON.stringify on a circular object) cannot abort the
|
|
48
|
+
* deferred resolution band — observability code must never escalate to a
|
|
49
|
+
* load-bearing failure. On catch we emit a single `formatter error: …`
|
|
50
|
+
* line via logDeferredProfile and return; the caller's stage continues
|
|
51
|
+
* as if profiling had no-op'd for this timer. DoD §2.8 ("no silent
|
|
52
|
+
* catches that swallow diagnostics") is satisfied by surfacing the
|
|
53
|
+
* failure message rather than dropping it.
|
|
54
|
+
*/
|
|
55
|
+
export declare const endTimer: (start: bigint | null, format: (elapsedMs: number) => string) => void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall-clock logging for the post-chunk deferred resolution band
|
|
3
|
+
* (imports → heritage → heritage map → legacy call resolution).
|
|
4
|
+
*
|
|
5
|
+
* Enabled when either:
|
|
6
|
+
* - `GITNEXUS_VERBOSE=1` / `gitnexus analyze -v` (primary path for #1741), or
|
|
7
|
+
* - `GITNEXUS_PROFILE_DEFERRED=1` (force on without full verbose ingestion noise)
|
|
8
|
+
*
|
|
9
|
+
* Issue #1741: large Java/Kotlin repos appear stuck at "Resolving calls"
|
|
10
|
+
* because the UI progress bar updates every 100 files and intermediate
|
|
11
|
+
* stages emit little to the log.
|
|
12
|
+
*/
|
|
13
|
+
import { logger } from '../../logger.js';
|
|
14
|
+
import { parseTruthyEnv } from './env.js';
|
|
15
|
+
import { isVerboseIngestionEnabled } from './verbose.js';
|
|
16
|
+
// Module-private tuning constants for the gates below. Not exported — these
|
|
17
|
+
// are internal knobs, not part of the module's API surface.
|
|
18
|
+
const LOG_EVERY_N_VERBOSE = 10;
|
|
19
|
+
const LOG_EVERY_N_PROFILE = 100;
|
|
20
|
+
const DEFAULT_SLOW_MS_VERBOSE = 3_000;
|
|
21
|
+
const DEFAULT_SLOW_MS = 5_000;
|
|
22
|
+
/** True when deferred-stage timing / progress logs should emit. */
|
|
23
|
+
export const isDeferredResolutionProfileEnabled = () => isVerboseIngestionEnabled() || parseTruthyEnv(process.env.GITNEXUS_PROFILE_DEFERRED);
|
|
24
|
+
/** Log a call-resolution progress line every N files (finer when verbose). */
|
|
25
|
+
export const deferredCallLogEveryN = () => isVerboseIngestionEnabled() ? LOG_EVERY_N_VERBOSE : LOG_EVERY_N_PROFILE;
|
|
26
|
+
/** Per-file call-resolution log threshold (ms). Lower default when verbose. */
|
|
27
|
+
export const deferredCallFileSlowMs = () => {
|
|
28
|
+
const raw = process.env.GITNEXUS_PROFILE_DEFERRED_SLOW_MS;
|
|
29
|
+
if (raw) {
|
|
30
|
+
// Use Number() not parseInt: parseInt('1e9', 10) === 1 (prefix-parses, drops the exponent),
|
|
31
|
+
// which would turn a user-intended "effectively disabled" threshold into a 1 ms log storm.
|
|
32
|
+
const n = Number(raw);
|
|
33
|
+
if (Number.isFinite(n) && n > 0)
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
36
|
+
return isVerboseIngestionEnabled() ? DEFAULT_SLOW_MS_VERBOSE : DEFAULT_SLOW_MS;
|
|
37
|
+
};
|
|
38
|
+
export const profileNow = () => process.hrtime.bigint();
|
|
39
|
+
export const profileElapsedMs = (start) => Number(process.hrtime.bigint() - start) / 1e6;
|
|
40
|
+
// Module-private counter for `[deferred-profile]` log lines the underlying
|
|
41
|
+
// logger refused to accept. Pino's SonicBoom transport is sync:false today,
|
|
42
|
+
// so steady-state `logger.info(string)` calls don't throw — but first-use
|
|
43
|
+
// construction paths (pino-pretty resolve, level validation) and any future
|
|
44
|
+
// transport reconfiguration could. The wrap below catches and counts so a
|
|
45
|
+
// failing logger cannot abort the deferred band, and the count surfaces in
|
|
46
|
+
// the deferred-band done-summary (see processCallsFromExtracted) so the
|
|
47
|
+
// failure is visible rather than silently swallowed (DoD §2.8).
|
|
48
|
+
let droppedLogLines = 0;
|
|
49
|
+
/**
|
|
50
|
+
* Number of `logDeferredProfile` calls whose underlying `logger.info` threw.
|
|
51
|
+
* Surfaced in the deferred-band done-summary when greater than zero.
|
|
52
|
+
*/
|
|
53
|
+
export const getDeferredProfileDroppedCount = () => droppedLogLines;
|
|
54
|
+
/**
|
|
55
|
+
* Reset the dropped-line counter. Call from test `afterEach` to keep the
|
|
56
|
+
* module-private state from leaking across tests. Also used inside
|
|
57
|
+
* `processCallsFromExtracted` at function entry so each analyze run gets
|
|
58
|
+
* a fresh count rather than accumulating across the process lifetime.
|
|
59
|
+
*/
|
|
60
|
+
export const resetDeferredProfileDroppedCount = () => {
|
|
61
|
+
droppedLogLines = 0;
|
|
62
|
+
};
|
|
63
|
+
export const logDeferredProfile = (message) => {
|
|
64
|
+
try {
|
|
65
|
+
logger.info(`[deferred-profile] ${message}`);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Do not call the failing logger from the handler — that would risk
|
|
69
|
+
// an infinite loop if the failure mode is steady-state. Just count.
|
|
70
|
+
droppedLogLines++;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Capture a monotonic timestamp when profiling is enabled; otherwise return null.
|
|
75
|
+
* Pair with `endTimer` so the type system narrows correctly — using `null` instead
|
|
76
|
+
* of a `0n` sentinel makes "profiling disabled" structurally distinct from
|
|
77
|
+
* "zero elapsed time" and lets TypeScript catch missing guards.
|
|
78
|
+
*/
|
|
79
|
+
export const startTimer = (enabled) => enabled ? process.hrtime.bigint() : null;
|
|
80
|
+
/**
|
|
81
|
+
* Emit a `[deferred-profile]` log line for a captured timer. No-op when the
|
|
82
|
+
* timer is `null` (profiling was disabled at capture time). The formatter
|
|
83
|
+
* receives elapsed ms so the call sites stay readable.
|
|
84
|
+
*
|
|
85
|
+
* The format callback runs inside a try/catch so a throwing formatter
|
|
86
|
+
* (custom toString, JSON.stringify on a circular object) cannot abort the
|
|
87
|
+
* deferred resolution band — observability code must never escalate to a
|
|
88
|
+
* load-bearing failure. On catch we emit a single `formatter error: …`
|
|
89
|
+
* line via logDeferredProfile and return; the caller's stage continues
|
|
90
|
+
* as if profiling had no-op'd for this timer. DoD §2.8 ("no silent
|
|
91
|
+
* catches that swallow diagnostics") is satisfied by surfacing the
|
|
92
|
+
* failure message rather than dropping it.
|
|
93
|
+
*/
|
|
94
|
+
export const endTimer = (start, format) => {
|
|
95
|
+
if (start === null)
|
|
96
|
+
return;
|
|
97
|
+
const elapsedMs = profileElapsedMs(start);
|
|
98
|
+
let message;
|
|
99
|
+
try {
|
|
100
|
+
message = format(elapsedMs);
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
logDeferredProfile(`formatter error: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
logDeferredProfile(message);
|
|
107
|
+
};
|
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** Whether we're running in development mode (enables verbose console logging). */
|
|
10
10
|
export declare const isDev: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Parse a narrow-form truthy env-var value. Accepts `'1'`, `'true'`, `'yes'`
|
|
13
|
+
* (case-insensitive, whitespace-trimmed). Anything else — including
|
|
14
|
+
* `undefined`, empty string, `'0'`, `'false'`, `'no'`, or unknown tokens —
|
|
15
|
+
* returns `false`.
|
|
16
|
+
*
|
|
17
|
+
* This is the shared helper for narrow-form truthy parsing across the
|
|
18
|
+
* ingestion module. `logger.ts` uses a broader negative-list form
|
|
19
|
+
* (`isTruthyEnv`) that intentionally accepts anything except a small set of
|
|
20
|
+
* falsy tokens — that lives separately because it follows pino-debug
|
|
21
|
+
* conventions and serves a different purpose.
|
|
22
|
+
*/
|
|
23
|
+
export declare const parseTruthyEnv: (raw: string | undefined) => boolean;
|
|
11
24
|
/**
|
|
12
25
|
* Whether scope-resolution dev validators (e.g. `validateBindingsImmutability`)
|
|
13
26
|
* should run AND emit warnings. Off by default in CLI runs to avoid silent
|
|
@@ -8,6 +8,24 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** Whether we're running in development mode (enables verbose console logging). */
|
|
10
10
|
export const isDev = process.env.NODE_ENV === 'development';
|
|
11
|
+
/**
|
|
12
|
+
* Parse a narrow-form truthy env-var value. Accepts `'1'`, `'true'`, `'yes'`
|
|
13
|
+
* (case-insensitive, whitespace-trimmed). Anything else — including
|
|
14
|
+
* `undefined`, empty string, `'0'`, `'false'`, `'no'`, or unknown tokens —
|
|
15
|
+
* returns `false`.
|
|
16
|
+
*
|
|
17
|
+
* This is the shared helper for narrow-form truthy parsing across the
|
|
18
|
+
* ingestion module. `logger.ts` uses a broader negative-list form
|
|
19
|
+
* (`isTruthyEnv`) that intentionally accepts anything except a small set of
|
|
20
|
+
* falsy tokens — that lives separately because it follows pino-debug
|
|
21
|
+
* conventions and serves a different purpose.
|
|
22
|
+
*/
|
|
23
|
+
export const parseTruthyEnv = (raw) => {
|
|
24
|
+
if (raw === undefined)
|
|
25
|
+
return false;
|
|
26
|
+
const value = raw.trim().toLowerCase();
|
|
27
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
28
|
+
};
|
|
11
29
|
/**
|
|
12
30
|
* Whether scope-resolution dev validators (e.g. `validateBindingsImmutability`)
|
|
13
31
|
* should run AND emit warnings. Off by default in CLI runs to avoid silent
|
|
@@ -1,7 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
if (!raw)
|
|
4
|
-
return false;
|
|
5
|
-
const value = raw.toLowerCase();
|
|
6
|
-
return value === '1' || value === 'true' || value === 'yes';
|
|
7
|
-
};
|
|
1
|
+
import { parseTruthyEnv } from './env.js';
|
|
2
|
+
export const isVerboseIngestionEnabled = () => parseTruthyEnv(process.env.GITNEXUS_VERBOSE);
|
package/package.json
CHANGED