Skip to content

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

Merged
hatayama merged 4 commits into
mainfrom
perf/hot-reload-backfill-follows-next-caller-level
Oct 13, 2026
Merged

hatayama merged 4 commits into
mainfrom
perf/hot-reload-backfill-follows-next-caller-level

Conversation

@hatayama

@hatayama hatayama commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

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)

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_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.

View guided diff

The backfill reads only the dlls the run's note step refused at the first incomplete caller level, so the next level's dlls are found only by the next run. Pin the intended behavior before implementing it: after reading, the backfill recomputes the refused dlls (a function passed to Start), reads the new ones, skips ones it already tried, stops at the caller-depth round limit, and honors cancels before and inside the recompute.

Start takes the recompute function now; the stub ignores it, so the new recompute tests fail while the guard tests (empty list, cancel before the recompute) pass.
…vel needs

The note step's caller closure stops at the first level with a refused dll, so the backfill only learned that level's dlls and the next run still read one dll and refused another. After reading, the backfill now calls the run's recompute function, reads the dlls it newly refuses, and repeats until nothing new is refused or the caller-depth limit of rounds is reached.

Tried paths are remembered so a dll that fails to read is not retried, the cancel is checked before each recompute so a waiting run is not held up by it, and the complete entry gains rounds while requested now counts the distinct dlls tried.
…notes

The run remembers the project root and note requests, and on End passes the backfill a function that, on the main thread under a fresh cache hold, rebuilds the notes with a load budget of zero and returns the dlls it refused. Nothing is read by it and it writes no budget-exhausted entry, since no run's note was omitted. A run that built no notes returns an empty list without hopping threads.
…un reads nothing

Add a two-level cross-assembly caller chain (host in the test assembly, relay in the cross-assembly fixture, caller in a new level-2 fixture assembly) and an E2E test: the cold run refuses only the relay's dll, the backfill reads it and then the level-2 dll found by its recompute, writes no extra budget-exhausted entry, and the next run reads no dll.

The test reads the patched host through reflection: a direct call from the test assembly is itself a compiled caller, and the closure would stop at it before walking the relay's level.
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: hatayama/unity-cli-loop/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f6777062-12d2-4f75-821e-70b16ba7d1b5
📥 Commits

Reviewing files that changed from the base of the PR and between e2e9136 and fde83c4.

⛔ Files ignored due to path filters (6)
  • Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2.meta is excluded by none and included by none
  • Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs.meta is excluded by none and included by none
  • Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef is excluded by none and included by none
  • Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef.meta is excluded by none and included by none
📒 Files selected for processing (12)
  • Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillEditorHooksTests.cs
  • Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTestDoubles.cs
  • Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTests.cs
  • Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs
  • Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs
  • Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs
  • Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs
  • Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs
  • docs/vibe-logs.md

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

📝 Walkthrough

Caller-note backfill now recomputes refused assemblies between load rounds and loads newly refused paths up to the caller-depth limit. The caller run supplies the recomputation callback. Unit and end-to-end tests cover repeated rounds, cancellation, and a multi-assembly caller chain.

Changes: Caller-note backfill

Layer / File(s) Summary
Iterative backfill and unit coverage
Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs, Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfill*, Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs
Start accepts a refused-path recomputation callback. Backfill tries distinct paths once, repeats until no new paths remain or the caller-depth limit is reached, and logs the requested path count and round count. Unit tests cover round limits, deduplication, cancellation, shutdown, and exceptions.
Caller-run recomputation wiring
Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs, Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs, docs/vibe-logs.md
The caller run retains note requests and the project root. Its backfill callback holds cached entries and recomputes notes with a zero-load budget. The documentation describes recomputation rounds and completion fields.
Cross-assembly backfill validation
Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfill*, Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs, Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs
The end-to-end test exercises a two-level caller chain across assemblies. It checks that backfill loads the refused assemblies over multiple rounds and that the next run reads no assemblies.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant HotReloadCompiledCallersRun
  participant HotReloadCallSiteBackfill
  participant UnityMainThread
  participant CachedEntries
  HotReloadCompiledCallersRun->>HotReloadCallSiteBackfill: Start with initial paths and recomputation callback
  HotReloadCallSiteBackfill->>HotReloadCompiledCallersRun: Request refused paths
  HotReloadCompiledCallersRun->>UnityMainThread: Switch to main thread
  HotReloadCompiledCallersRun->>CachedEntries: Hold entries
  HotReloadCompiledCallersRun->>HotReloadCompiledCallersRun: Recompute saved notes with zero-load budget
  HotReloadCompiledCallersRun-->>HotReloadCallSiteBackfill: Return refused DLL paths
  HotReloadCallSiteBackfill->>HotReloadCallSiteBackfill: Load newly refused paths in another round
Loading
Merge Risk: ⚪ Minimal · up to fde83

The iterative backfill change is mergeable after normal checks; no concrete outstanding risk was identified.

Security architecture risk: 🔵 Low · up to fde83

The additional background work retains bounded discovery, cancellation, and scoped resource ownership. No concrete new security issue was established in the reviewed flow, but third-party editor usage and broader security coverage remain incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The traced production exposure is the Unity Editor process reading compiled project assemblies selected through caller-reference discovery. This PR advances parsing of later caller levels into backfill; the recomputation cap bounds discovery depth, not the number or total size of DLLs. No broader tenant, service, or credential authority was established by this route.

Trust Boundaries and Controls

  • observed — Production End supplies only the private recomputation method. Refused paths continue to originate from Unity compilation/reference discovery and the project assembly layout, then enter the existing cache reader. The loader does not independently enforce project-root containment; this provenance dependency predates the PR and is not established as a new bypass.

Resilience and Maintainability Implications

  • observed — Cancellation while the main-thread switch is pending causes the awaiter to throw before recomputation acquires its hold. If cancellation races after the switch completes, synchronous recomputation may still finish, but its hold remains scope-bound and backfill terminal cleanup still runs.
Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 45 functions across 11 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly describes the primary change: caller-note backfill reads the next caller level's DLLs so the following run reads none.
Description check Passed The description is directly related to the changeset and explains the backfill recomputation, retry prevention, cancellation handling, logging changes, tests, and performance goal.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 45 functions across 11 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hatayama
hatayama merged commit ab7d1af into main Oct 13, 2026
17 checks passed
@hatayama
hatayama deleted the perf/hot-reload-backfill-follows-next-caller-level branch October 13, 2026 06:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant