Skip to content

perf: Hot reload's source snapshot capture rewrites only the sources that changed since the last compile - #3277

Merged
hatayama merged 3 commits into
perf/hot-reload-snapshot-capturefrom
perf/hot-reload-snapshot-incremental-copy
Oct 11, 2026
Merged

hatayama merged 3 commits into
perf/hot-reload-snapshot-capturefrom
perf/hot-reload-snapshot-incremental-copy

Conversation

@hatayama

@hatayama hatayama commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Summary

User Impact

  • Before: each compile rewrote every source of each recompiled assembly into a new directory, while the Editor waited on the main thread. One report measured 781 files and 3.3 MB rewritten for a one-line edit.
  • After: the capture moves the previous build's snapshot directory to the new build's name and keeps the copies whose sources have not been written since. It writes only the edited files again. The debug breakdown now also reports filesReused.

Changes

  • Refactor first (own commit): the copier becomes partial, and the per-assembly tally of unreadable and PDB-checked sources moves into a private nested class, so both capture paths share it.
  • HotReloadSourceSnapshotter reads the stamp of the previous build. If that build's directory still exists and has a manifest, the new HotReloadSourceSnapshotCopier.CaptureIncrementally builds on it. Otherwise the existing full copy runs.
  • CaptureIncrementally:
    1. Deletes a leftover temporary directory, reads the previous manifest, and moves the previous directory to the new temporary name. If that move fails, it falls back to the full copy and leaves the previous directory where it was.

    2. For each listed source, keeps the previous copy when all of these hold:

      • the manifest line is unmarked
      • the source's length and write time still equal the line
      • the write time is before this compile's suspect window
      • the copy exists

      Any other source has its previous copy deleted and is copied, with the same checks as the full copy.

    3. Deletes the copies of sources the assembly no longer lists, writes the manifest, and publishes with one move. The stamp is written only after that, as before.

  • Docs: docs/vibe-logs.md (filesReused, and where the new steps' time is counted) and docs/hot-reload.md (how the snapshot directory is built).

Input space (summary)

Directory-level state Base Head
No stamp, first field not an MVID, MVID without a directory, directory without a usable manifest full copy full copy
Previous directory with a manifest full copy incremental
Current MVID's directory exists / stamp matches unchanged unchanged
Source-level state (incremental) Base Head
Unmarked line, same stat, before the window, copy present read and write reuse, same line
Unmarked line, same stat, before the window, source unreadable no copy, no line, warning reuse (never read), same line
Line marked, no line, stat differs, inside the window, copy missing, new source read and write read and write
Source missing now no copy previous copy deleted, no line
Source unreadable and changed no copy, warning previous copy deleted, no copy, warning
Source no longer listed (not copied) copy deleted
Failure Base Head
Leftover temporary directory deleted first deleted first
Moving the previous directory fails (n/a) full copy; previous directory untouched
Final move fails warning, no stamp, previous kept warning, no stamp, previous gone; next capture copies everything
PDB check throws after the previous directory moved nothing published nothing published; previous sits under the temporary name, deleted by the next capture

Exits × invariants

On every exit:

  • A published unmarked line's copy holds the bytes this compile read. If it does not, the reader's own PDB check rejects it.
  • Only copies the line can vouch for get a line.
  • No copy of an unlisted source is published.
  • The previous directory is either whole under its own name or moved whole to the new temporary name.
  • The stamp is written only after publishing.

Constraint for a later change

Between the two moves, an assembly's snapshot is not published anywhere. Today the capture finishes synchronously before any reader runs: before the command listener opens, or at the start of a hot reload run. So no reader can observe the gap. A later change that moves the capture to a pool thread must do one of two things:

  • make every reader wait on the capture gate, pause points included
  • stop moving the previous directory

Verification

  • Local, Unity 2022.3, uloop compile on the integration branch with this PR: 0 errors. The only warnings are pre-existing ones in test fixtures.
  • New HotReloadIncrementalSnapshotCaptureTests: 18 tests, 22 with test cases, all passed. The two permission tests that root would pass anyway are ignored when the effective uid is 0, and the three permission tests skip Windows.
  • Existing classes, one class at a time, 0 failed:
    • Snapshot classes: HotReloadSnapshotAssemblyEnumerationTests 22, HotReloadSourceSnapshotTests 19, HotReloadSourceSnapshotterTests 9, HotReloadSnapshotEditedDuringCompileTests 4.
    • Readers: HotReloadChangedSiblingSourceDetectorTests 31, HotReloadChangedFileAggregatorTests 5, HotReloadChangedSourceDetectorTests 7, HotReloadDefaultFilesTests 16.
  • Mutations, each reverted after the run:
Mutation Failing tests
Drop the edited-after-compile check SourceMarkedEditedAfterCompile_CopiesTheCurrentBytes
Drop the length / write-time comparison SourceWhoseLengthOrWriteTimeChanged_CopiesTheCurrentBytes (both cases), UnreadableSourceThatChanged_..., CaptureAssemblies_ReportsReusedAndCopiedFiles
Skip deleting unlisted copies SourceRemovedFromTheAssembly_DeletesItsCopy
Skip deleting the previous copy before copying SourceMissingNow_LeavesNoCopyAndNoLine, UnreadableSourceThatChanged_...
Rethrow instead of falling back when the previous directory cannot be moved WhenThePreviousDirectoryCannotBeMoved_FallsBackToAFullCopy (macOS)
  • Live check on the development project. I changed one method body in a sample editor assembly of 6 sources and ran uloop compile, then reverted and compiled again.
    • Both breakdowns: assembliesCaptured 1, filesCopied 1, filesReused 5, filesChecked 0.
    • Only the new MVID's directory remained each time.
  • scripts/check-file-length.sh and scripts/check-code-complexity.sh report no findings.

View guided diff

The incremental capture that follows needs the same counting of
unreadable and PDB-checked sources and the same log and warning, so the
tally becomes a private nested class both paths can use. The copier
becomes partial for the incremental path's file. Behavior is unchanged.
…hanged

Every compile rewrote every source of each recompiled assembly into a new
directory, although a body edit changes one file. The capture now moves
the previous build's directory, named by the stamp, to the new MVID's
temporary name, keeps the copies whose unmarked manifest line still
matches the source's length and write time, rewrites the rest, and
deletes the copies of sources no longer listed. When the previous
directory cannot be moved, it copies every source as before.
@coderabbitai

coderabbitai Bot commented Oct 11, 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: 6eba1131-a12a-42bd-8f1e-36e0dea70327

📥 Commits

Reviewing files that changed from the base of the PR and between c813735 and 23f0d3a.


⛔ Files ignored due to path filters (2)
  • Assets/Tests/Editor/HotReload/HotReloadIncrementalSnapshotCaptureTests.cs.meta is excluded by none and included by none
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.Incremental.cs.meta is excluded by none and included by none

📒 Files selected for processing (8)
  • Assets/Tests/Editor/HotReload/HotReloadIncrementalSnapshotCaptureTests.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotCaptureStats.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.Incremental.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.Stamp.cs
  • Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs
  • docs/hot-reload.md
  • docs/vibe-logs.md

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

Snapshot capture now reuses eligible copies from a prior build, replaces or removes other copies, and publishes the updated snapshot and manifest. The change also adds capture statistics and editor tests for reuse, fallback, publication, and error cases.

Changes

Incremental source snapshots

Layer / File(s) Summary
Prior snapshot selection
Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.Stamp.cs, Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs
The snapshotter validates the previous build stamp and manifest. It uses incremental capture when it finds a reusable prior snapshot and atomic capture otherwise.
Incremental capture and accounting
Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier*, Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotCaptureStats.cs
The copier adopts the prior snapshot and reuses copies only when source stamps, edit status, and copy availability allow reuse. It replaces other copies, removes unlisted copies, publishes the updated manifest, and records reused-file statistics.
Capture tests and documentation
Assets/Tests/Editor/HotReload/HotReloadIncrementalSnapshotCaptureTests.cs, docs/hot-reload.md, docs/vibe-logs.md
Editor tests cover reuse, recopying, omission, publication, fallback, recovery, and capture statistics. Documentation describes rewrite conditions and the updated timing and reuse log fields.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant HotReloadSourceSnapshotter
  participant HotReloadSourceSnapshotterStamp
  participant HotReloadSourceSnapshotCopier
  HotReloadSourceSnapshotter->>HotReloadSourceSnapshotterStamp: FindReusablePreviousSnapshotDirectory
  HotReloadSourceSnapshotterStamp-->>HotReloadSourceSnapshotter: Return previous directory or null
  HotReloadSourceSnapshotter->>HotReloadSourceSnapshotCopier: Capture incrementally or atomically
Loading

Merge Risk | ⚪ Minimal · up to 23f0d

Merge Risk: ⚪ Minimal · up to 23f0d

The incremental capture path has a full-copy fallback for unusable prior snapshots, and no issue requiring a change before merge was established.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 23f0d

The change remains confined to local project snapshots, and checksum verification still protects method-diff inputs. However, reuse based on file length and write time can preserve stale source across builds when a writer preserves that metadata, misleading downstream change detection.

Retained concerns

  • Medium · reliability · inferred: Retained copies can lose their association with the current compiled source. If source A becomes same-length B while its old write time is preserved, the new capture can publish A under B’s build identity. The PDB-verified baseline rejects A, but sibling detection does not universally use that gate. A subsequent ordinary edit back to A can compare equal to the stale snapshot and be reported unchanged despite differing from compiled B. The base’s full capture would have stored B. This conditional regression weakens downstream change and drift detection; it requires metadata-preserving writes and does not establish increased attacker privileges.
Security review details

Security Blast Radius

  • inferred — The inspected exposure is the existing Editor process and project-owned snapshot store across eligible assemblies. Exploiting metadata-based reuse requires influence over local source bytes or snapshot state; the changed branch does not demonstrate new credentials, tenant access, or broader execution authority.

Security Findings and Attack Paths

  • inferred — A metadata-preserving writer can cause prior-generation bytes to be published with the new generation’s identity. The supported consequence is weakened snapshot identity and downstream change detection, not a verified authorization bypass. Sibling detection’s metadata fast path predates this PR; the introduced change is retaining old bytes across builds instead of rereading the current source.

Trust Boundaries and Controls

  • observed — Checksum-verified baseline loading and sibling snapshot comparison are separate controls. The latter can accept matching metadata or matching file bytes without establishing that the retained snapshot belongs to the current compiled generation.

Resilience and Maintainability Implications

  • observed — The existing capture gate executes synchronously on the main-thread lifecycle and records completion only after its delegate returns. Startup captures before serving commands, and the apply entry ensures capture before default file selection. This sequencing is counterevidence to an active reader race during directory adoption; no new concurrent-reader guarantee was established.

Hardening Proposals

  • proposed — Make cross-generation reuse contingent on current-build content identity, such as verification of retained bytes against the current PDB checksum, or decline reuse when that identity cannot be established. Preserve the same assurance for selectors and skip checks, not only method-diff consumers.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 45.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 62 functions across 6 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly summarizes the main change: incremental hot-reload snapshot capture rewrites only sources that changed since the previous compile.
Description check Passed The description directly explains incremental source reuse, fallback behavior, metrics, documentation updates, and verification results.
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 45.16% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 62 functions across 6 files. (2 skipped: 2 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 5fd6da3 into perf/hot-reload-snapshot-capture Oct 11, 2026
5 checks passed
@hatayama
hatayama deleted the perf/hot-reload-snapshot-incremental-copy branch October 11, 2026 05:28
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