perf: The caller-note backfill also reads the next caller level's dlls, so the run after a cold run reads none
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.Starttakes the recompute function (findRefusedAgain);RunAllAsyncloops 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
OperationCanceledExceptionfilter. - The recompute writes no
hot_reload_caller_note_load_budget_exhausted(no run's note was omitted). HotReloadCompiledCallersRunremembers the project root and requests and passesFindRefusedAgainAsynconEnd; a run that built no notes returns an empty list without hopping threads.- Log change:
hot_reload_caller_note_backfill_completegainsrounds(recomputes done), andrequestednow means the distinct dlls actually tried (previously the length of the initial list).docs/vibe-logs.mdupdated.
The closure's early stop on an incomplete level is unchanged (it is what makes a note safe).
Input space covered (HotReloadCallSiteBackfillTests)
| Case | Expected |
|---|---|
| empty initial list | nothing runs, no recompute, no entry |
| recompute returns nothing | one round, stops |
| recompute refuses a new dll | reads it, rounds=2, requested=2 |
| failed dll refused again | not retried, one round |
| recompute always returns new dlls | stops at 4 rounds |
| cancel while a dll is read (2 dlls / last dll) | no recompute |
| recompute cancelled from inside | stops quietly, entry names the trigger, completion not faulted |
| recompute returns duplicates / tried dlls | each new dll read once |
| recompute throws an unexpected exception | logged 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_DoesNotRecomputeandYieldToRunAsync_WhileADllIsInFlight_...fail. - Remove the tried-path exclusion →
..._DoesNotRetryThemand..._ReadsEachNewDllOncefail. - Remove the round limit →
..._StopsAtTheRoundLimitfails. - Never recompute → the E2E fails at "the second run reads no dll" (the symptom this PR fixes).
Verification
- Red: with
Starttaking 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.