mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
fix: add time-based deadline to cross-file type propagation to prevent stalling on large repos
Adds a 2-minute wall-clock time limit (DEFAULT_CROSS_FILE_ELAPSED_MS) to runCrossFileBindingPropagation. When exceeded, the phase gracefully stops and logs a warning. Users can override via GITNEXUS_CROSS_FILE_TIMEOUT_MS env var. This prevents the analyze command from stalling for hours on very large repositories where per-file re-resolution is expensive. Fixes the reported issue where gitnexus analyze stalls at "Cross-file type propagation" for several hours on repos with 15000+ files. Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b8341947-557c-4111-a3a8-991ba455ab01 Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
This commit is contained in:
parent
375d98145f
commit
c576c68503
2 changed files with 92 additions and 0 deletions
|
|
@ -32,6 +32,19 @@ const CROSS_FILE_SKIP_THRESHOLD = 0.03;
|
|||
/** Hard cap on files re-processed during cross-file propagation. */
|
||||
const MAX_CROSS_FILE_REPROCESS = 2000;
|
||||
|
||||
/**
|
||||
* Default wall-clock time limit (ms) for cross-file propagation.
|
||||
* Override via `GITNEXUS_CROSS_FILE_TIMEOUT_MS` env var.
|
||||
* Prevents the phase from stalling for hours on very large repositories
|
||||
* where per-file re-resolution is expensive.
|
||||
*/
|
||||
const DEFAULT_CROSS_FILE_ELAPSED_MS = 120_000; // 2 minutes
|
||||
|
||||
function getCrossFileTimeoutMs(): number {
|
||||
const raw = Number(process.env.GITNEXUS_CROSS_FILE_TIMEOUT_MS);
|
||||
return raw > 0 && Number.isFinite(raw) ? raw : DEFAULT_CROSS_FILE_ELAPSED_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-file binding propagation.
|
||||
* Returns the number of files re-processed.
|
||||
|
|
@ -113,9 +126,13 @@ export async function runCrossFileBindingPropagation(
|
|||
|
||||
let crossFileResolved = 0;
|
||||
const crossFileStart = Date.now();
|
||||
const crossFileDeadlineMs = getCrossFileTimeoutMs();
|
||||
const astCache = createASTCache(AST_CACHE_CAP);
|
||||
let timedOut = false;
|
||||
|
||||
for (const level of levels) {
|
||||
if (timedOut) break;
|
||||
|
||||
const levelCandidates: {
|
||||
filePath: string;
|
||||
seeded: Map<string, string>;
|
||||
|
|
@ -161,6 +178,15 @@ export async function runCrossFileBindingPropagation(
|
|||
const contentMap = await readFileContents(repoPath, levelPaths);
|
||||
|
||||
for (const { filePath, seeded, importedReturns, importedRawReturns } of levelCandidates) {
|
||||
if (Date.now() - crossFileStart >= crossFileDeadlineMs) {
|
||||
timedOut = true;
|
||||
logger.warn(
|
||||
`⏱️ Cross-file type propagation timed out after ${crossFileDeadlineMs}ms` +
|
||||
` (${crossFileResolved} files processed). Override with GITNEXUS_CROSS_FILE_TIMEOUT_MS env var.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const content = contentMap.get(filePath);
|
||||
if (!content) continue;
|
||||
|
||||
|
|
|
|||
|
|
@ -162,6 +162,72 @@ describe('runCrossFileBindingPropagation', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('stops processing when time deadline is exceeded', async () => {
|
||||
const graph = createKnowledgeGraph();
|
||||
const ctx = createResolutionContext();
|
||||
|
||||
const exportedTypeMap: ExportedTypeMap = new Map([
|
||||
['upstream.ts', new Map([['User', 'User']])],
|
||||
]);
|
||||
|
||||
const allPaths: string[] = ['upstream.ts'];
|
||||
// Create 50 downstream importers
|
||||
ctx.importMap.set('upstream.ts', new Set());
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const file = `downstream${i}.ts`;
|
||||
allPaths.push(file);
|
||||
const bindings = new Map();
|
||||
bindings.set('User', { sourcePath: 'upstream.ts', exportedName: 'User' });
|
||||
ctx.namedImportMap.set(file, bindings);
|
||||
ctx.importMap.set(file, new Set(['upstream.ts']));
|
||||
}
|
||||
|
||||
// Set a very short timeout via env var. The processCalls mock below
|
||||
// advances Date.now past this deadline after the first invocation.
|
||||
const origEnv = process.env.GITNEXUS_CROSS_FILE_TIMEOUT_MS;
|
||||
process.env.GITNEXUS_CROSS_FILE_TIMEOUT_MS = '50';
|
||||
|
||||
let callCount = 0;
|
||||
processCallsMock.mockImplementation(async () => {
|
||||
callCount++;
|
||||
// After the first call, busy-wait past the 50ms deadline so the
|
||||
// time check before the next candidate triggers.
|
||||
if (callCount === 1) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < 60) {
|
||||
/* spin */
|
||||
}
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await runCrossFileBindingPropagation(
|
||||
graph,
|
||||
ctx,
|
||||
exportedTypeMap,
|
||||
new Set(allPaths),
|
||||
allPaths.length,
|
||||
'/repo',
|
||||
Date.now(),
|
||||
() => {},
|
||||
);
|
||||
|
||||
// Should have processed at least 1 file (the first one goes through
|
||||
// before the deadline check), but NOT all 50.
|
||||
expect(result).toBeGreaterThan(0);
|
||||
expect(result).toBeLessThan(50);
|
||||
expect(processCallsMock.mock.calls.length).toBeLessThan(50);
|
||||
} finally {
|
||||
if (origEnv === undefined) {
|
||||
delete process.env.GITNEXUS_CROSS_FILE_TIMEOUT_MS;
|
||||
} else {
|
||||
process.env.GITNEXUS_CROSS_FILE_TIMEOUT_MS = origEnv;
|
||||
}
|
||||
processCallsMock.mockImplementation(async () => []);
|
||||
}
|
||||
});
|
||||
|
||||
it('caps processing at MAX_CROSS_FILE_REPROCESS (2000)', async () => {
|
||||
const graph = createKnowledgeGraph();
|
||||
const ctx = createResolutionContext();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue