perf: The caller-note backfill also reads the next caller level's dlls, so the run after a cold run reads none

Merged
HA
3d ago
+525-43

Summary

The caller-note step of a hot reload run reads at most one uncached compiled dll; the background backfill then reads the dlls the budget refused. But the caller closure stops at the first caller level with a refused dll, so the backfill only learned that level's dlls. The dll one caller level further was found by the next run, which read one dll and refused another (measured: the first warm run after a cold run spent 185–237 ms in caller notes vs. 110–120 ms fully warm).

Now, after reading the refused dlls, the backfill asks the run to recompute its notes with a load budget of zero (on the main thread, under a fresh cache hold, discarding the notes) and reads the dlls that recompute refuses. It repeats until nothing new is refused, capped at MaxCallerDepth (4) recomputes. After a cold run's backfill finishes, the next run reads no dll.

  • HotReloadCallSiteBackfill.Start takes the recompute function (findRefusedAgain); RunAllAsync loops read → recompute.
  • Tried paths are remembered, so a dll that fails to read is not retried (no loop on a broken dll).
  • Cancel is checked before each recompute too, so a run that yields is not held up by a recompute; a recompute cancelled from inside ends quietly via the existing OperationCanceledException filter.
  • The recompute writes no hot_reload_caller_note_load_budget_exhausted (no run's note was omitted).
  • HotReloadCompiledCallersRun remembers the project root and requests and passes FindRefusedAgainAsync on End; a run that built no notes returns an empty list without hopping threads.
  • Log change: hot_reload_caller_note_backfill_complete gains rounds (recomputes done), and requested now means the distinct dlls actually tried (previously the length of the initial list). docs/vibe-logs.md updated.

The closure's early stop on an incomplete level is unchanged (it is what makes a note safe).

Input space covered (HotReloadCallSiteBackfillTests)

CaseExpected
empty initial listnothing runs, no recompute, no entry
recompute returns nothingone round, stops
recompute refuses a new dllreads it, rounds=2, requested=2
failed dll refused againnot retried, one round
recompute always returns new dllsstops at 4 rounds
cancel while a dll is read (2 dlls / last dll)no recompute
recompute cancelled from insidestops quietly, entry names the trigger, completion not faulted
recompute returns duplicates / tried dllseach new dll read once
recompute throws an unexpected exceptionlogged by the existing boundary catch, no entry, idle, completion not faulted

E2E: Run_WhoseNoteStepRefusedADllWithACallerChain_BackfillsTheNextLevelSoTheNextRunReadsNothing uses a new two-level chain (host in the test assembly → relay in the cross-assembly fixture → caller in a new CallSiteCrossAssemblyLevel2 fixture assembly). The host is read through reflection in the test, because a direct call from the test assembly would itself be a compiled caller at which the closure stops.

Mutation checks

Each run against the committed head, then reverted:

  • Remove the cancel check before the recompute → Start_WhenCancelledBeforeRecompute_DoesNotRecompute and YieldToRunAsync_WhileADllIsInFlight_... fail.
  • Remove the tried-path exclusion → ..._DoesNotRetryThem and ..._ReadsEachNewDllOnce fail.
  • Remove the round limit → ..._StopsAtTheRoundLimit fails.
  • Never recompute → the E2E fails at "the second run reads no dll" (the symptom this PR fixes).

Verification

  • Red: with Start taking the new argument but ignoring it, exactly the recompute tests failed (7) and the guard tests passed.
  • Class-filtered EditMode runs, all passing: HotReloadCallSiteBackfillTests (17), HotReloadCallerNoteBackfillE2ETests (4), HotReloadCompiledCallersTests (3), HotReloadCallSiteBackfillEditorHooksTests (2), HotReloadCallSiteScannerTests (30), HotReloadOneShotCallerClosureTests (2), OnionAssemblyDependencyTests (83).
  • File length and CA1502 complexity checkers: no findings.