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:
co-authored by
Claude Fable 5
parent
ad5300a601
commit
246aee8373
@@ -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)) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user