fix(ui): within-pass progress for the C fn-pointer linking pass (#1300)

Follow-up to #1299: the per-pass bar still parked on one number while a
single long pass ran — on C-heavy repos that's the fn-pointer dispatch
pass, which sweeps every C/C++ file four times (typedefs, registrations,
field propagation, dispatch sites) and dominates the linking phase.

The pass now reports a real fraction of its dominant work
(scannedFiles / files×4, at the same per-16-files cadence as its
cooperative yield), and the orchestrator surfaces instrumented passes'
fractions as fractional steps, throttled to whole-percent movement so
the UI message volume stays bounded. The mechanism is opt-in per pass —
any synthesizer that a real repo shows parking the bar can adopt the
same callback.

Verified on the 1,342-file C repo from the report: the linking bar now
moves through 88→89→90 where it previously sat at 88 for the whole
pass; graph byte-identical (50,520 nodes / 148,232 edges).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-15 20:05:35 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent ad5300a601
commit 246aee8373
4 changed files with 71 additions and 9 deletions
+23 -5
View File
@@ -308,11 +308,29 @@ const INCLUDE_RE = /#[ \t]*include[ \t]+"([^"\n]+)"/g;
/** Included files worth scanning for registration tables (e.g. a generated `.def`). */
const INCLUDABLE_EXT = /\.(def|inc|h|hh|hpp|hxx|c|cc|cpp|cxx|ipp|tcc|tbl)$/i;
export async function cFnPointerDispatchEdges(_queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
export async function cFnPointerDispatchEdges(
_queries: QueryBuilder,
ctx: ResolutionContext,
onYield: MaybeYield,
onFraction?: (fraction: number) => void
): Promise<Edge[]> {
let scannedFiles = 0;
const files = ctx.getAllFiles().filter((f) => C_CPP_EXT.test(f));
if (files.length === 0) return [];
// Within-pass progress: this is the pass that parks the "Linking dynamic
// dispatch" bar on C-heavy repos, so it reports a real fraction of its
// dominant work. `files` is swept once per file loop below (passes A, C, D,
// E — pass B is node-bound and comparatively brief), reported at the same
// per-16-files cadence as the cooperative yield.
const FILE_SWEEPS = 4;
const tick = async (): Promise<void> => {
if ((++scannedFiles & 15) === 0) {
onFraction?.(scannedFiles / (files.length * FILE_SWEEPS));
await onYield();
}
};
// Cache raw + stripped source per file, LRU-BOUNDED. The old unbounded Maps
// retained every C/C++ file's raw AND stripped text for the whole pass —
// multiple GB on the Linux kernel, one of the two OOM culprits in #1212.
@@ -358,7 +376,7 @@ export async function cFnPointerDispatchEdges(_queries: QueryBuilder, ctx: Resol
const fnPtrTypedefs = new Set<string>();
const fnTypeTypedefs = new Set<string>();
for (const file of files) {
if ((++scannedFiles & 15) === 0) await onYield();
await tick();
const s = src(file);
if (!s || !s.includes('typedef')) continue;
FNPTR_TYPEDEF_RE.lastIndex = 0;
@@ -764,7 +782,7 @@ export async function cFnPointerDispatchEdges(_queries: QueryBuilder, ctx: Resol
// ---- Pass C: registrations — stream each file (and its qualifying local
// includes) through processUnit, one at a time.
for (const file of files) {
if ((++scannedFiles & 15) === 0) await onYield();
await tick();
const env = new Map<string, MacroDef>();
const objEnv = new Map<string, string>();
const defined = new Set<string>();
@@ -846,7 +864,7 @@ export async function cFnPointerDispatchEdges(_queries: QueryBuilder, ctx: Resol
const FIELD_ASSIGN_RE = /(\w+)\s*(?:->|\.)\s*(\w+)\s*=\s*(\w+)\s*(?:->|\.)\s*(\w+)/g;
const propagations: { to: string; from: string }[] = [];
for (const file of files) {
if ((++scannedFiles & 15) === 0) await onYield();
await tick();
const s = src(file);
if (!s || !s.includes('=')) continue;
for (const fn of ctx.getNodesInFile(file)) {
@@ -897,7 +915,7 @@ export async function cFnPointerDispatchEdges(_queries: QueryBuilder, ctx: Resol
const edges: Edge[] = [];
const seen = new Set<string>();
for (const file of files) {
if ((++scannedFiles & 15) === 0) await onYield();
await tick();
const s = src(file);
if (!s) continue;
for (const fn of ctx.getNodesInFile(file)) {
+19 -3
View File
@@ -3474,8 +3474,24 @@ export async function synthesizeCallbackEdges(
// tail — long enough on big repos that users conclude the index hung and
// kill it. Report each completed pass; the caller surfaces it as its own
// progress phase. Emit 0/total up front so the phase flips immediately.
// Emissions are throttled to whole-percent movement (each consumes a UI
// message); values may be fractional steps from within-pass reporting.
let passesDone = 0;
onProgress?.(0, SYNTH_PROGRESS_STEPS);
let lastPct = -1;
const emit = (value: number): void => {
if (!onProgress) return;
const v = Math.min(value, SYNTH_PROGRESS_STEPS);
const pct = Math.floor((v / SYNTH_PROGRESS_STEPS) * 100);
if (pct === lastPct) return;
lastPct = pct;
onProgress(v, SYNTH_PROGRESS_STEPS);
};
// A single long pass otherwise parks the bar between steps; a pass that
// takes this callback reports a 0..1 fraction of its own work, surfaced
// here as fractional progress within its step.
const subProgress = (fraction: number): void =>
emit(passesDone + Math.max(0, Math.min(fraction, 1)));
emit(0);
// Per-pass wall-clock timing to stderr, opt-in via CODEGRAPH_SYNTH_TIMINGS
// (=1: passes over 250ms; =all: every pass). This is the diagnostic that
@@ -3489,7 +3505,7 @@ export async function synthesizeCallbackEdges(
console.error(`[synth-timing] ${label}: ${dt}ms`);
}
passesDone++;
onProgress?.(Math.min(passesDone, SYNTH_PROGRESS_STEPS), SYNTH_PROGRESS_STEPS);
emit(passesDone);
};
// Language gating: one indexed DISTINCT over the files table lets a pass
@@ -3560,7 +3576,7 @@ export async function synthesizeCallbackEdges(
const sidekiqEdges = has('ruby') ? await sidekiqDispatchEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('sidekiqEdges');
const erlangBehaviourEdges = has('erlang') ? await erlangBehaviourDispatchEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('erlangBehaviourEdges');
const laravelEdges = has('php') ? await laravelEventEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('laravelEdges');
const cFnPtrEdges = has('c', 'cpp') ? await cFnPointerDispatchEdges(queries, ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('cFnPtrEdges');
const cFnPtrEdges = has('c', 'cpp') ? await cFnPointerDispatchEdges(queries, ctx, yieldToLoop, subProgress) : NONE; await yieldToLoop(); __mark('cFnPtrEdges');
const goframeEdges = has('go') ? await goframeRouteEdges(ctx, yieldToLoop) : NONE; await yieldToLoop(); __mark('goframeEdges');
const nixOptionEdges = has('nix') ? await nixOptionPathEdges(queries, yieldToLoop) : NONE; await yieldToLoop(); __mark('nixOptionEdges');