Skip to content

perf: Hot reload asks the compilation pipeline for the assembly list once per domain - #3240

Merged
hatayama merged 4 commits into
feature/hot-reload-large-project-feedback-3from
perf/hot-reload-compilation-assemblies-memo
Oct 8, 2026
Merged

hatayama merged 4 commits into
feature/hot-reload-large-project-feedback-3from
perf/hot-reload-compilation-assemblies-memo

Conversation

@hatayama

@hatayama hatayama commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Hot reload now asks Unity for the compilation assembly list once per domain instead of on every run. On a project with several hundred assemblies this removes about a third of a second from every hot reload.

Why

  • Every run called CompilationPipeline.GetAssemblies(): once per input file while resolving inputs, once more when --files is omitted, and again for new-source membership checks and the call-site reference graph.
  • A trial on a large project (558 compilation assemblies) measured one call at 342-622 ms, still 340 ms when warm. The run's resolve_inputs step (337-366 ms) was almost entirely that one call, and it was the largest share of the run's non-analysis time.
  • The list only changes through an import, a compile, or a domain reload, so asking again on every run repeats the same answer.

Design

  • A memo in the Shared hot reload assembly keeps the first non-empty answer. Callers get it as IReadOnlyList, so they can only read the shared array.
  • An empty answer is never kept: GetAssemblies() returns nothing while a compile is in flight, and keeping that would hide every assembly for the rest of the domain.
  • The memo is dropped on CompilationPipeline.compilationStarted (a failed compile keeps the domain alive) and on EditorApplication.projectChanged (an import does not always compile, for example in Play Mode with "Recompile After Finished Playing"). A domain reload clears it with the other statics.
  • The startup snapshot capture is the first caller after a domain reload, so the first run already finds the list kept.
  • The call-site scanner keeps its own memo of the reference graph built from the list. It is derived data with the same invalidation on compile start; only its GetAssemblies() call now goes through the shared memo.

Changes

  • New HotReloadCompilationAssemblyCache (the memo, testable with an injected fetch) and HotReloadCompilationAssemblies (the per-domain instance and its two event handlers).
  • The five hot reload callers use it: patch-target input resolution, new-source membership validation, changed-file detection, the startup snapshot capture, and the call-site reference graph. The two private FindCompilationAssembly helpers are gone.

Verification

This pull request targets an integration branch, so the pull request CI does not run on it; the checks below were run locally in the Editor.

  • Red first: the new test class failed to compile without the memo (CS0246).
  • uloop run-tests --filter-type regex over HotReloadCompilationAssemblyCacheTests|HotReloadCompilationAssembliesTests|HotReloadPatchTargetSupport|HotReloadNewSourceMembership|HotReloadChangedFileAggregatorTests|HotReloadSnapshotAssemblyEnumerationTests|HotReloadSourceSnapshot|HotReloadCallSiteScannerTests: 120 passed, 0 failed.
  • Mutations, each run against HotReloadCompilationAssemblyCacheTests|HotReloadCompilationAssembliesTests:
Mutation Result
m1: keep an empty answer killed (Current_EmptyAnswer_IsNotMemoized, Current_NonEmptyAfterEmpty_IsMemoized)
m2: Invalidate() does nothing killed (Current_AfterInvalidate_FetchesAgainAndReturnsNewAnswer)
m3: Current() always fetches killed (Current_CalledTwice_FetchesOnceAndReturnsSameInstance, Current_NonEmptyAfterEmpty_IsMemoized, FindByName_KnownName_ReturnsAssemblyAndFetchesOnce)
m4: FindByName matches by prefix (StartsWith) killed (FindByName_SimilarNames_ReturnsExactMatchOnly)
m5: FindByName ignores case killed (FindByName_SimilarNames_ReturnsExactMatchOnly)
m6: FindByName starts at index 1 killed (FindByName_SimilarNames_ReturnsExactMatchOnly)
m7: no compilationStarted subscription killed (StaticConstructor_SubscribesInvalidationToCompilationStartedAndProjectChanged)
m8: no projectChanged subscription killed (StaticConstructor_SubscribesInvalidationToCompilationStartedAndProjectChanged)
m9: a new memo on every Current() call killed (Current_CalledTwice_ReturnsSameInstance)
  • Editor measurements on the development project (111 assemblies), resolve_inputs from hot_reload_timing_detail, three runs in a row on the same file:
Run 1 Run 2 Run 3
Before (base) 115 ms 149 ms 62 ms
After 2 ms 1 ms 1 ms
  • Event behavior, checked in the Editor because tests cannot raise these Unity events (the subscriptions themselves are pinned by a reflection test, m7 and m8 above):
    • compilationStarted: after a compile that failed on a syntax error (domain kept), the next run took 249 ms and the one after 4 ms. Without the handler nothing drops the memo, so that first run would also be fast (not run as a mutation).
    • projectChanged: after importing a non-script asset with AssetDatabase.Refresh() (no compile logged), the next run took 61 ms, then 1 ms. After deleting it and refreshing again: 474 ms, then 1 ms. A control execute-dynamic-code without a refresh left the next run at 0 ms.
    • After a successful compile (domain reload), the first run took 5 ms, so the startup capture fills the memo.

Not covered

  • Tests pin that both handlers are subscribed, but cannot raise compilationStarted or projectChanged; that the events fire when expected (a failed compile, a non-script import) is covered only by the Editor checks above.
  • sibling_detect and other remaining run costs are separate work.

Hot reload asks CompilationPipeline.GetAssemblies() on every run, which takes
hundreds of milliseconds on a project with several hundred assemblies. These
tests describe a memo that asks once, keeps a non-empty answer until it is
invalidated, and never keeps the empty answer Unity gives during a compile.
It asks Unity once and keeps the first non-empty answer until it is
invalidated. An empty answer is not kept because GetAssemblies() returns
nothing while a compile is in flight.
Input resolution, new-source membership checks, changed-file detection,
the startup snapshot capture, and the call-site reference graph each called
CompilationPipeline.GetAssemblies(), which costs 340-620 ms on a project
with several hundred assemblies. They now share one memo that is dropped
when a compile starts (a failed compile keeps the domain alive) and when the
project changes (an import does not always compile).
@coderabbitai

coderabbitai Bot commented Oct 8, 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: 0a766aaf-d73e-4c19-aa37-81330da8fc94
📥 Commits

Reviewing files that changed from the base of the PR and between 00b4a89 and 0814b30.

⛔ Files ignored due to path filters (3)
  • Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs.meta is excluded by none and included by none
📒 Files selected for processing (8)
  • Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/HotReloadChangedFileAggregator.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/HotReloadNewSourceMembershipValidator.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs

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


📝 Walkthrough

Walkthrough

Hot reload code now obtains compilation assemblies through a shared cache. The cache reuses non-empty results, retries empty results, supports name lookup, and invalidates on compilation start or project change. EditMode tests cover cache and lookup behavior.

Changes

Compilation assembly access

Layer / File(s) Summary
Cache behavior and tests
Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs, Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs
The cache reuses non-empty results, does not memoize empty results, supports invalidation and name lookup, and has tests for these behaviors.
Shared accessor and invalidation
Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs
The shared accessor uses the cache and invalidates it on compilation start and project change.
Hot reload caller migration
Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs, Packages/src/Editor/FirstPartyTools/HotReload/HotReloadChangedFileAggregator.cs, Packages/src/Editor/FirstPartyTools/HotReload/HotReloadNewSourceMembershipValidator.cs, Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs, Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs
These callers now retrieve compilation assemblies or find assemblies by name through the shared accessor.

Priority: ➖ Normal

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

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant HotReloadCallSiteScanner
  participant HotReloadCompilationAssemblies
  participant HotReloadCompilationAssemblyCache
  participant CompilationPipeline
  HotReloadCallSiteScanner->>HotReloadCompilationAssemblies: Current()
  HotReloadCompilationAssemblies->>HotReloadCompilationAssemblyCache: Current()
  HotReloadCompilationAssemblyCache->>CompilationPipeline: Fetch compilation assemblies
  CompilationPipeline-->>HotReloadCompilationAssemblyCache: Compilation assemblies
  HotReloadCompilationAssemblyCache-->>HotReloadCompilationAssemblies: Cached assembly list
  HotReloadCompilationAssemblies-->>HotReloadCallSiteScanner: Assembly list
Loading

Merge Risk: ⚪ Minimal · up to 0814b

No actionable merge-blocking issue is established; the change is mergeable after normal checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 0814b

The change preserves existing assembly-identity and membership controls without adding a new external entrypoint. The main uncertainty is whether shared assembly data remains fresh after compilation failures and imports, especially when a lookup occurs during compilation.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The new shared-state failure domain is the current Unity Editor project's assembly metadata: stale data could propagate into target lookup, changed-file selection, membership checks, and reference-graph construction. The inspected changes do not introduce additional externally reachable entrypoints or broader execution authority.

Trust Boundaries and Controls

  • observed — Caller-provided script paths still pass through Unity's assembly-name resolution before exact-name lookup. The PR changes metadata acquisition, while preserving target-image checks and live new-source membership validation. A stale cache alone has not been shown to bypass these controls.

Resilience and Maintainability Implications

  • inferred — If Unity returns non-empty transitional metadata during compilation, a lookup before busy refusal can retain it until another invalidation. Recovery after a failed compile therefore remains a runtime proof gap, not a demonstrated failure or vulnerability. Empty-result retry and independent target-identity checks provide counterevidence against unconditional cache poisoning or control bypass.
  • observed — The scanner's derived reference graph invalidates on compilation start, but not project change. That policy already existed at the PR base; the PR replaces only its assembly-fetch source. The mismatch is not evidence of an introduced security regression.
🚥 Pre-merge checks | ✅ 3 | ❌ 1 | ❓ 1

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The description identifies PR #3240, but no separate linked issue or issue-tracking requirement is provided for verification. Provide the linked issue reference or confirm that no separate issue link is required.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the performance change, cache behavior, invalidation rules, affected callers, and verification results.
Out of Scope Changes check ✅ Passed The changes stay within the stated objective: cache compilation assemblies, invalidate the cache at the required events, route hot reload callers through it, and add focused tests.
Title check ✅ Passed The title concisely and accurately describes the main performance change: hot reload now requests the compilation assembly list once per domain.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · 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.

The lookup tests passed for a prefix match, a case-insensitive match, and a
loop that skips the first assembly. Nothing pinned that the per-domain memo
is shared or that its invalidation is subscribed to compile start and
project change, so dropping either subscription went unnoticed.
@hatayama
hatayama merged commit 731c2f7 into feature/hot-reload-large-project-feedback-3 Oct 8, 2026
4 of 5 checks passed
@hatayama
hatayama deleted the perf/hot-reload-compilation-assemblies-memo branch October 8, 2026 01:45
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