From bf51a4270283c1f133f42897f6f30ed15c7dbbee Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 13 Oct 2026 12:53:27 +0900 Subject: [PATCH 1/4] test: Pin the caller-note backfill recomputing the dlls it must read 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. --- ...tReloadCallSiteBackfillEditorHooksTests.cs | 2 +- .../HotReloadCallSiteBackfillTestDoubles.cs | 38 ++++ .../HotReloadCallSiteBackfillTests.cs | 190 +++++++++++++++++- .../HotReloadCallerNoteBackfillE2ETests.cs | 4 +- .../HotReloadCompiledCallersTests.cs | 2 +- .../HotReloadCallSiteBackfill.cs | 6 +- .../HotReloadCompiledCallersRun.cs | 5 +- 7 files changed, 231 insertions(+), 16 deletions(-) diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillEditorHooksTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillEditorHooksTests.cs index 16d87dbe4..4fb57f2be 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillEditorHooksTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillEditorHooksTests.cs @@ -64,7 +64,7 @@ public async Task ShutdownForCompile_StopsTheRunningBackfill() private async Task StopWith(Action hook) { - _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + _backfill.Start(new[] { "a.dll", "b.dll" }, HotReloadCallSiteBackfillTestDoubles.FindNothingRefusedAgain, "corr"); Assert.That( _backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Running), diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTestDoubles.cs b/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTestDoubles.cs index 995678a6d..7bce40783 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTestDoubles.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTestDoubles.cs @@ -76,4 +76,42 @@ private TaskCompletionSource GetOrCreate(string dllPath) return source; } } + + /// + /// A stand-in for the backfill's recompute of the refused dlls: it counts its calls and + /// answers each from a function of the call number (1-based) and the backfill's token. + /// + internal sealed class RefusedAgainFinder + { + private readonly Func> _answer; + + internal RefusedAgainFinder(Func> answer) + { + _answer = answer; + } + + internal int Calls { get; private set; } + + /// Answers the given lists in order, then empty lists. + internal static RefusedAgainFinder Answering(params string[][] answers) + { + return new RefusedAgainFinder( + (call, ct) => call <= answers.Length ? answers[call - 1] : Array.Empty()); + } + + internal Task> Find(CancellationToken ct) + { + Calls++; + return Task.FromResult(_answer(Calls, ct)); + } + } + + internal static class HotReloadCallSiteBackfillTestDoubles + { + /// A recompute that finds no refused dll, for tests that do not exercise it. + internal static Task> FindNothingRefusedAgain(CancellationToken ct) + { + return Task.FromResult>(Array.Empty()); + } + } } diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTests.cs index 919900cab..b0c13e792 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallSiteBackfillTests.cs @@ -24,12 +24,14 @@ public sealed class HotReloadCallSiteBackfillTests { private PendingDllLoader _loader; private HotReloadCallSiteBackfill _backfill; + private RefusedAgainFinder _finder; [SetUp] public void SetUp() { _loader = new PendingDllLoader(); _backfill = new HotReloadCallSiteBackfill(_loader.Load); + _finder = RefusedAgainFinder.Answering(); VibeLogger.ClearMemoryLogs(); } @@ -47,7 +49,7 @@ public void TearDown() [Test] public async Task Start_LoadsEachPathInOrder_AndLogsOneCompleteEntry() { - _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + _backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr"); await _backfill.Completion; Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll", "b.dll" }), "paths read"); @@ -60,27 +62,194 @@ public async Task Start_LoadsEachPathInOrder_AndLogsOneCompleteEntry() } /// - /// What: an empty list starts nothing and leaves no entry behind. + /// What: an empty list starts nothing, recomputes nothing, and leaves no entry behind. /// [Test] - public void Start_WithNoPaths_LoadsNothingAndLogsNothing() + public void Start_WithNoDlls_DoesNotRunOrRecomputeAndLogsNothing() { - _backfill.Start(Array.Empty(), "corr"); + _backfill.Start(Array.Empty(), _finder.Find, "corr"); Assert.That(_backfill.Completion.IsCompleted, Is.True, "completion"); Assert.That(_loader.Ran, Is.Empty, "paths read"); + Assert.That(_finder.Calls, Is.EqualTo(0), "recomputes"); + Assert.That(CountCompleteEntries(), Is.EqualTo(0), "complete entries"); + } + + /// + /// What: after the refused dlls are read, the refused dlls are recomputed once; an empty + /// answer ends the backfill and the entry reports one round. + /// + [Test] + public async Task Start_WhenRecomputeFindsNothingNew_StopsAfterOneRound() + { + _backfill.Start(new[] { "a.dll" }, _finder.Find, "corr"); + await _backfill.Completion; + + Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read"); + Assert.That(_finder.Calls, Is.EqualTo(1), "recomputes"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); + Assert.That((int)context["rounds"], Is.EqualTo(1), "rounds"); + } + + /// + /// What: a dll the recompute newly refuses (a caller one level further) is read too, after + /// the first ones, and the backfill recomputes again until nothing new is refused. + /// + [Test] + public async Task Start_WhenRecomputeRefusesANewDll_ReadsItToo() + { + _finder = RefusedAgainFinder.Answering(new[] { "b.dll" }); + + _backfill.Start(new[] { "a.dll" }, _finder.Find, "corr"); + await _backfill.Completion; + + Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll", "b.dll" }), "paths read"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); + Assert.That((int)context["rounds"], Is.EqualTo(2), "rounds"); + Assert.That((int)context["requested"], Is.EqualTo(2), "requested"); + Assert.That((int)context["loaded"], Is.EqualTo(2), "loaded"); + } + + /// + /// What: a dll that failed to read is refused again by the recompute but is not tried a + /// second time, so the backfill ends after one round. + /// + [Test] + public async Task Start_WhenRecomputeRefusesOnlyTriedDlls_DoesNotRetryThem() + { + _loader.ThrowFor.Add("a.dll"); + LogAssert.Expect(LogType.Warning, new Regex("caller-note backfill")); + _finder = new RefusedAgainFinder((call, ct) => new[] { "a.dll" }); + + _backfill.Start(new[] { "a.dll" }, _finder.Find, "corr"); + await _backfill.Completion; + + Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read"); + Assert.That(_finder.Calls, Is.EqualTo(1), "recomputes"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); + Assert.That((int)context["requested"], Is.EqualTo(1), "requested"); + Assert.That((int)context["failed"], Is.EqualTo(1), "failed"); + } + + /// + /// What: a recompute that keeps refusing new dlls is stopped after the caller-depth limit + /// of rounds. The stand-in throws on the round past the limit, so a missing limit fails + /// the test instead of looping forever. + /// + [Test] + public async Task Start_WhenRecomputeKeepsFindingNewDlls_StopsAtTheRoundLimit() + { + int limit = HotReloadOneShotCallerClosure.MaxCallerDepth; + _finder = new RefusedAgainFinder((call, ct) => + { + if (call > limit) + { + throw new InvalidOperationException("test: recomputed past the round limit"); + } + + return new[] { "level" + call + ".dll" }; + }); + + _backfill.Start(new[] { "a.dll" }, _finder.Find, "corr"); + await _backfill.Completion; + + Assert.That(_finder.Calls, Is.EqualTo(limit), "recomputes"); + Assert.That(_loader.Ran.Count, Is.EqualTo(limit + 1), "paths read"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); + Assert.That((int)context["rounds"], Is.EqualTo(limit), "rounds"); + } + + /// + /// What: a cancel that lands while the last dll is read stops the backfill before the + /// recompute, so the run that cancelled waits for that dll only. + /// + [Test] + public async Task Start_WhenCancelledBeforeRecompute_DoesNotRecompute() + { + _loader.PendingFor.Add("a.dll"); + _backfill.Start(new[] { "a.dll" }, _finder.Find, "corr"); + + Task yielded = _backfill.YieldToRunAsync(); + _loader.Release("a.dll"); + await yielded; + + Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read"); + Assert.That(_finder.Calls, Is.EqualTo(0), "recomputes"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); + Assert.That((string)context["cancelledBy"], Is.EqualTo(HotReloadConstants.WarmUpCancelledByRun), "cancelledBy"); + } + + /// + /// What: a recompute cancelled from inside (it throws OperationCanceledException on the + /// backfill's token) ends the backfill quietly: the completion does not fault, an entry names + /// what stopped it, and the backfill is idle again. + /// + [Test] + public async Task Start_WhenRecomputeIsCancelled_StopsQuietly() + { + _finder = new RefusedAgainFinder((call, ct) => + { + _backfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerCompilationStarted); + ct.ThrowIfCancellationRequested(); + return new[] { "b.dll" }; + }); + + _backfill.Start(new[] { "a.dll" }, _finder.Find, "corr"); + await _backfill.Completion; + + Assert.That(_finder.Calls, Is.EqualTo(1), "recomputes"); + Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read"); + Assert.That(_backfill.Completion.IsFaulted, Is.False, "completion faulted"); + Assert.That(_backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Idle), "state"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); + Assert.That((string)context["cancelledBy"], Is.EqualTo("compilationStarted"), "cancelledBy"); + } + + /// + /// What: a recompute answer that repeats a dll or names one already read reads each new + /// dll once. + /// + [Test] + public async Task Start_WhenRecomputeReturnsDuplicates_ReadsEachNewDllOnce() + { + _finder = RefusedAgainFinder.Answering(new[] { "b.dll", "a.dll", "b.dll" }); + + _backfill.Start(new[] { "a.dll" }, _finder.Find, "corr"); + await _backfill.Completion; + + Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll", "b.dll" }), "paths read"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); + Assert.That((int)context["requested"], Is.EqualTo(2), "requested"); + } + + /// + /// What: an unexpected exception from the recompute ends the backfill, is logged, and + /// leaves the completion task unfaulted, the backfill idle, and no complete entry behind. + /// + [Test] + public async Task Start_WhenRecomputeThrowsAnUnexpectedException_LogsItAndCompletionDoesNotFault() + { + _finder = new RefusedAgainFinder((call, ct) => throw new IOException("test: recompute failed")); + LogAssert.Expect(LogType.Exception, new Regex("IOException: test: recompute failed")); + + _backfill.Start(new[] { "a.dll" }, _finder.Find, "corr"); + await _backfill.Completion; + + Assert.That(_finder.Calls, Is.EqualTo(1), "recomputes"); + Assert.That(_backfill.Completion.IsFaulted, Is.False, "completion faulted"); + Assert.That(_backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Idle), "state"); Assert.That(CountCompleteEntries(), Is.EqualTo(0), "complete entries"); } /// /// What: a run that yields while a dll is being read waits for that dll, and no later dll - /// is read; the entry names the run as what stopped the backfill. + /// is read nor recompute run; the entry names the run as what stopped the backfill. /// [Test] public async Task YieldToRunAsync_WhileADllIsInFlight_WaitsForItAndLoadsNoLaterOne() { _loader.PendingFor.Add("a.dll"); - _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + _backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr"); Task yielded = _backfill.YieldToRunAsync(); Assert.That(yielded.IsCompleted, Is.False, "the yield must wait while a.dll is being read"); @@ -88,6 +257,7 @@ public async Task YieldToRunAsync_WhileADllIsInFlight_WaitsForItAndLoadsNoLaterO await yielded; Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read"); + Assert.That(_finder.Calls, Is.EqualTo(0), "recomputes"); JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); Assert.That((int)context["loaded"], Is.EqualTo(1), "loaded"); Assert.That((string)context["cancelledBy"], Is.EqualTo(HotReloadConstants.WarmUpCancelledByRun), "cancelledBy"); @@ -125,7 +295,7 @@ public async Task YieldToRunAsync_WhileTheInFlightDllFails_CompletesAndCountsTha { _loader.PendingFor.Add("a.dll"); LogAssert.Expect(LogType.Warning, new Regex("caller-note backfill")); - _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + _backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr"); Task yielded = _backfill.YieldToRunAsync(); _loader.Fail("a.dll", new IOException("test: cannot read a.dll")); @@ -146,7 +316,7 @@ public async Task YieldToRunAsync_WhileTheInFlightDllFails_CompletesAndCountsTha public async Task Shutdown_WhileADllIsInFlight_StopsBeforeTheNextOneAndRecordsTheTrigger() { _loader.PendingFor.Add("a.dll"); - _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + _backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr"); Task stopped = _backfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerCompilationStarted); _loader.Release("a.dll"); @@ -167,7 +337,7 @@ public async Task Start_WhenALoadThrowsAnIoException_CountsItFailedAndLoadsTheNe _loader.ThrowFor.Add("a.dll"); LogAssert.Expect(LogType.Warning, new Regex("caller-note backfill")); - _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + _backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr"); await _backfill.Completion; Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll", "b.dll" }), "paths read"); @@ -186,7 +356,7 @@ public async Task Start_WhenALoadThrowsAnUnexpectedException_LogsItAndCompletion _loader.ThrowUnexpectedFor.Add("a.dll"); LogAssert.Expect(LogType.Exception, new Regex("InvalidOperationException: unexpected")); - _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + _backfill.Start(new[] { "a.dll", "b.dll" }, _finder.Find, "corr"); await _backfill.Completion; Assert.That(_backfill.Completion.IsFaulted, Is.False, "completion faulted"); diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs index 04e547bf9..55c65ff7a 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs @@ -122,7 +122,7 @@ public async Task Run_WhileTheBackfillOfThePreviousRunIsInFlight_WaitsForThatDll HotReloadCallSiteBackfill backfill = new HotReloadCallSiteBackfill(loader.Load); _scope = HotReloadDomainTestScope.WithCallSiteBackfill(backfill); await _scope.InstalledWarmUpStopped; - backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + backfill.Start(new[] { "a.dll", "b.dll" }, HotReloadCallSiteBackfillTestDoubles.FindNothingRefusedAgain, "corr"); string editedPath = HotReloadTestSourceWriter.WriteEditedSource( "CallerNoteBackfillE2E.cs", @@ -162,7 +162,7 @@ public async Task Run_WhileABackfillReadIsInFlight_CountsTheWaitInWarmUpYield() _scope = HotReloadDomainTestScope.WithCallSiteBackfill(backfill); await _scope.InstalledWarmUpStopped; VibeLogger.ClearMemoryLogs(); - backfill.Start(new[] { "a.dll" }, "corr"); + backfill.Start(new[] { "a.dll" }, HotReloadCallSiteBackfillTestDoubles.FindNothingRefusedAgain, "corr"); string editedPath = HotReloadTestSourceWriter.WriteEditedSource( "CallerNoteBackfillE2E.cs", diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs index 3add8991f..46140d5e7 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs @@ -47,7 +47,7 @@ public void TearDown() public async Task BeginRunAsync_WhileABackgroundReadIsInFlight_TakesTheHoldOnlyAfterTheReadEnds() { _loader.PendingFor.Add("a.dll"); - _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + _backfill.Start(new[] { "a.dll", "b.dll" }, HotReloadCallSiteBackfillTestDoubles.FindNothingRefusedAgain, "corr"); Task begin = _compiledCallers.BeginRunAsync(); Assert.That(begin.IsCompleted, Is.False, "the run waits for the read in flight"); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs index e66fae414..b042695ed 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs @@ -61,9 +61,13 @@ internal HotReloadCallSiteBackfillState State /// is running (the run calling this yielded to the previous one first). Nothing starts and /// nothing is logged for an empty list. /// - internal void Start(IReadOnlyList dllPaths, string correlationId) + internal void Start( + IReadOnlyList dllPaths, + Func>> findRefusedAgain, + string correlationId) { Debug.Assert(dllPaths != null, "dllPaths must not be null."); + Debug.Assert(findRefusedAgain != null, "findRefusedAgain must not be null."); Debug.Assert(!string.IsNullOrEmpty(correlationId), "correlationId must not be null or empty."); if (dllPaths.Count == 0) { diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs index 3bee28e34..837403b91 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs @@ -84,7 +84,10 @@ public void End(string correlationId) // Why the hold ends here, before the backfill starts: its reads belong to no run, so they // must evict as reads outside a run do rather than pile up under this run's hold. _hold.Dispose(); - _backfill.Start(_refusedDllPaths, correlationId); + _backfill.Start( + _refusedDllPaths, + ct => System.Threading.Tasks.Task.FromResult>(Array.Empty()), + correlationId); } /// From 4f3eba3e6e351def1673f70cdafb48fea004ca34 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 13 Oct 2026 12:55:02 +0900 Subject: [PATCH 2/4] perf: The caller-note backfill also reads the dlls the next caller level 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. --- .../HotReloadCallSiteBackfill.cs | 79 +++++++++++++++---- docs/vibe-logs.md | 18 +++-- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs index b042695ed..f5eb3fe45 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -57,9 +58,11 @@ internal HotReloadCallSiteBackfillState State } /// - /// Starts reading on the main thread. Precondition: no backfill - /// is running (the run calling this yielded to the previous one first). Nothing starts and - /// nothing is logged for an empty list. + /// Starts reading on the main thread. Once they are read, + /// recomputes the dlls the run's notes would refuse now + /// and the new ones are read too, until none is new or the caller-depth round limit is + /// reached. Precondition: no backfill is running (the run calling this yielded to the + /// previous one first). Nothing starts and nothing is logged for an empty list. /// internal void Start( IReadOnlyList dllPaths, @@ -90,7 +93,7 @@ internal void Start( _state = HotReloadCallSiteBackfillState.Running; _cancelledBy = null; _cancellation = new CancellationTokenSource(); - _completion = RunAllAsync(dllPaths, correlationId, _cancellation.Token); + _completion = RunAllAsync(dllPaths, findRefusedAgain, correlationId, _cancellation.Token); } } @@ -136,40 +139,83 @@ private Task CancelRunningLocked(string cancelledBy) return _completion; } - private async Task RunAllAsync(IReadOnlyList dllPaths, string correlationId, CancellationToken ct) + private async Task RunAllAsync( + IReadOnlyList dllPaths, + Func>> findRefusedAgain, + string correlationId, + CancellationToken ct) { Stopwatch total = Stopwatch.StartNew(); + int requested = 0; int loaded = 0; int failed = 0; + int rounds = 0; + // Why remember every tried path: a dll that failed to read is refused again by the + // recompute, and trying it again would loop until the round limit. + HashSet attempted = new HashSet(StringComparer.Ordinal); + IReadOnlyList pending = dllPaths; try { try { - foreach (string dllPath in dllPaths) + while (true) { - // Why between dlls: a read cannot be interrupted, so a cancel takes effect - // before the next one, and the run that cancelled waits for this one only. + foreach (string dllPath in pending) + { + // Why between dlls: a read cannot be interrupted, so a cancel takes effect + // before the next one, and the run that cancelled waits for this one only. + if (ct.IsCancellationRequested) + { + break; + } + + if (!attempted.Add(dllPath)) + { + continue; + } + + requested++; + if (await TryLoadAsync(dllPath, ct).ConfigureAwait(false)) + { + loaded++; + } + else + { + failed++; + } + } + + // Why check before recomputing too: the run that cancelled waits for this + // backfill, so it must not also wait for a recompute nobody will use. if (ct.IsCancellationRequested) { break; } - if (await TryLoadAsync(dllPath, ct).ConfigureAwait(false)) + // Why a limit: each round reaches at most one more caller level and the + // notes walk no deeper than this; the attempted set already ends the loop. + if (rounds >= HotReloadOneShotCallerClosure.MaxCallerDepth) { - loaded++; + break; } - else + + rounds++; + // Why recompute rather than walk past the refused level: a refused dll's + // callers are unknown until it is read, so each read can reveal the next level. + IReadOnlyList refusedAgain = await findRefusedAgain(ct).ConfigureAwait(false); + pending = refusedAgain.Where(dllPath => !attempted.Contains(dllPath)).ToList(); + if (pending.Count == 0) { - failed++; + break; } } } catch (OperationCanceledException) when (ct.IsCancellationRequested) { - // A load cancelled before it started read nothing; it counts as neither. + // A load or recompute cancelled before it finished counts as neither. } - LogComplete(dllPaths.Count, loaded, failed, total.ElapsedMilliseconds, correlationId); + LogComplete(requested, loaded, failed, rounds, total.ElapsedMilliseconds, correlationId); } catch (Exception ex) { @@ -209,7 +255,8 @@ private static bool IsSkippableLoadException(Exception ex) return ex is IOException || ex is UnauthorizedAccessException || ex is BadImageFormatException; } - private void LogComplete(int requested, int loaded, int failed, long totalMs, string correlationId) + // requested counts the distinct dlls tried; rounds counts the recomputes. + private void LogComplete(int requested, int loaded, int failed, int rounds, long totalMs, string correlationId) { string cancelledBy; lock (_gate) @@ -220,7 +267,7 @@ private void LogComplete(int requested, int loaded, int failed, long totalMs, st VibeLogger.LogInfo( HotReloadConstants.VibeLogCallerNoteBackfillComplete, "Hot reload caller-note backfill finished.", - new { requested, loaded, failed, totalMs, cancelledBy }, + new { requested, loaded, failed, rounds, totalMs, cancelledBy }, correlationId); } } diff --git a/docs/vibe-logs.md b/docs/vibe-logs.md index 25e7caf46..2cd813563 100644 --- a/docs/vibe-logs.md +++ b/docs/vibe-logs.md @@ -43,13 +43,17 @@ command ran against: only from one-shot lifecycle messages) reads at most one compiled assembly that is not in the call-site cache yet; when it would need more, the note is omitted and the run writes one `hot_reload_caller_note_load_budget_exhausted` with `budgetLoads` and the `refusedAssemblies` - it did not read. Right after the run the refused assemblies are read on a pool thread, and - when that finishes the run's correlation id gets one `hot_reload_caller_note_backfill_complete` - with `requested`, `loaded`, `failed` (dlls that could not be read; the next run reads them - itself), `totalMs` and `cancelledBy` (`run` when the next run came first, - `beforeAssemblyReload`, `compilationStarted`, `services_replaced`, or null). The next run finds - them cached, so a note that needs more than one assembly appears from the second run on; a - run's `warm_up_yield` step also covers the wait for a backfill's dll in flight. + it did not read. Right after the run the refused assemblies are read on a pool thread; then the + notes are recomputed without reading anything, and the assemblies that recompute would still + refuse (callers one level further, found only once the refused ones are read) are read too, + for at most four recomputes. When that finishes the run's correlation id gets one + `hot_reload_caller_note_backfill_complete` with `requested` (distinct dlls tried), `loaded`, + `failed` (dlls that could not be read; the next run reads them itself), `rounds` (recomputes + done), `totalMs` and `cancelledBy` (`run` when the next run came first, + `beforeAssemblyReload`, `compilationStarted`, `services_replaced`, or null). The recompute + writes no `hot_reload_caller_note_load_budget_exhausted`. The next run finds them cached, so a + note that needs more than one assembly appears from the second run on; a run's + `warm_up_yield` step also covers the wait for a backfill's dll in flight. The time inside the analysis and shim-compile phases is split by separate entries, never by `steps`, so `unaccountedMs` keeps its meaning. `hot_reload_worker_request_timing` (one per request to the transform worker, prepare and transform) has the gate wait, launch target From 6e378d16de88ac1b3f4ffb650420197a1c2a4417 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 13 Oct 2026 12:58:32 +0900 Subject: [PATCH 3/4] perf: A run hands the backfill a zero-budget recompute of its caller 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. --- .../HotReloadCompiledCallers.cs | 2 +- .../HotReloadCompiledCallersRun.cs | 46 +++++++++++++++++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs index 3649ac063..e4729bea5 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs @@ -135,7 +135,7 @@ public async Task BeginRunAsync() // Why the whole run: the signature-change gate during analysis and the caller notes at // the end both scan callers through the shared cache, once per method they check, so // one hold keeps every dll they read until the run ends. - return new HotReloadCompiledCallersRun(_cache.HoldEntriesForRun(), _backfill); + return new HotReloadCompiledCallersRun(_cache.HoldEntriesForRun(), () => _cache.HoldEntriesForRun(), _backfill); } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs index 837403b91..dc425c659 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using UnityEngine; @@ -15,16 +17,24 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools public sealed class HotReloadCompiledCallersRun : IDisposable { private readonly IDisposable _hold; + private readonly Func _holdEntries; private readonly HotReloadCallSiteBackfill _backfill; private IReadOnlyList _refusedDllPaths = Array.Empty(); + private string _projectRoot; + private IReadOnlyList _requests; private bool _notesBuilt; private bool _ended; - internal HotReloadCompiledCallersRun(IDisposable hold, HotReloadCallSiteBackfill backfill) + internal HotReloadCompiledCallersRun( + IDisposable hold, + Func holdEntries, + HotReloadCallSiteBackfill backfill) { Debug.Assert(hold != null, "hold must not be null."); + Debug.Assert(holdEntries != null, "holdEntries must not be null."); Debug.Assert(backfill != null, "backfill must not be null."); _hold = hold; + _holdEntries = holdEntries; _backfill = backfill; } @@ -45,6 +55,8 @@ public IReadOnlyList BuildOneShotCallerNotes( Debug.Assert(!string.IsNullOrEmpty(correlationId), "correlationId must not be null or empty."); Debug.Assert(!_notesBuilt, "one budget per run: notes are built once."); _notesBuilt = true; + _projectRoot = projectRoot; + _requests = requests; // Why one budget per run rather than per scan: the direct scan and every level of the // caller closure go through the same lambda, so one budget bounds the whole note step @@ -84,10 +96,7 @@ public void End(string correlationId) // Why the hold ends here, before the backfill starts: its reads belong to no run, so they // must evict as reads outside a run do rather than pile up under this run's hold. _hold.Dispose(); - _backfill.Start( - _refusedDllPaths, - ct => System.Threading.Tasks.Task.FromResult>(Array.Empty()), - correlationId); + _backfill.Start(_refusedDllPaths, FindRefusedAgainAsync, correlationId); } /// @@ -106,6 +115,33 @@ internal void RememberRefusedDllPathsForTesting(IReadOnlyList dllPaths) _refusedDllPaths = dllPaths; } + // Recomputes this run's notes without reading any dll and returns the dlls that recompute + // refuses: the ones the next run would refuse once the backfill read the earlier ones. + private async Task> FindRefusedAgainAsync(CancellationToken ct) + { + // A run that built no notes has nothing to recompute; that is a valid run, not a misuse. + if (_requests == null) + { + return Array.Empty(); + } + + // Why the main thread: the scan resolves compiled assemblies through Editor APIs. + await MainThreadSwitcher.SwitchToMainThread(ct); + // Why a hold only around the recompute: it keeps the dlls it scans from being evicted + // mid-scan, and is released before the backfill reads, which belong to no run. + using (IDisposable hold = _holdEntries()) + { + // Why a zero budget: the recompute only collects the dlls not read yet; the backfill + // reads them off the main thread. Its notes are discarded, and it logs no budget + // exhaustion because no run's note was omitted. + HotReloadCallSiteLoadBudget budget = new HotReloadCallSiteLoadBudget(0); + HotReloadOneShotCallerNoteEnricher.BuildNotes( + _requests, + (ignoredAssemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(_projectRoot, identities, budget)); + return budget.RefusedDllPaths; + } + } + private static void LogLoadBudgetExhausted( int budgetLoads, IReadOnlyList refusedAssemblyNames, From fde83c4ebb290bb2eec0530ece1ed4c0a60319d5 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 13 Oct 2026 13:02:47 +0900 Subject: [PATCH 4/4] test: A cold run's backfill reads the next caller level so the next run 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. --- .../HotReloadCallerNoteBackfillE2ETests.cs | 72 ++++++++++++++++++- ...eloadCallerNoteBackfillLevel2E2EFixture.cs | 18 +++++ ...CallerNoteBackfillLevel2E2EFixture.cs.meta | 11 +++ ...tReloadCallerNoteBackfillLevel2E2ERelay.cs | 14 ++++ ...adCallerNoteBackfillLevel2E2ERelay.cs.meta | 11 +++ .../HotReloadCallSiteCrossAssemblyLevel2.meta | 8 +++ ...ReloadCallerNoteBackfillLevel2E2ECaller.cs | 14 ++++ ...dCallerNoteBackfillLevel2E2ECaller.cs.meta | 11 +++ ...tReload.CallSiteCrossAssemblyLevel2.asmdef | 18 +++++ ...ad.CallSiteCrossAssemblyLevel2.asmdef.meta | 7 ++ 10 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs create mode 100644 Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs.meta create mode 100644 Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs create mode 100644 Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs.meta create mode 100644 Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2.meta create mode 100644 Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs create mode 100644 Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs.meta create mode 100644 Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef create mode 100644 Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef.meta diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs index 55c65ff7a..421a63f1a 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -18,12 +19,14 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload { /// /// End-to-end EditMode coverage for the caller-note backfill around a hot reload run: a run - /// whose note step refused a dll reads it in the background afterwards, so the next run reads - /// nothing, and a run that comes while a backfill reads waits for that dll only. + /// whose note step refused a dll reads it, and the next caller level's dlls, in the background + /// afterwards, so the next run reads nothing, and a run that comes while a backfill reads + /// waits for that dll only. /// public class HotReloadCallerNoteBackfillE2ETests { private const string BackfillFixtureFileName = "HotReloadCallerNoteBackfillE2EFixture.cs"; + private const string Level2BackfillFixtureFileName = "HotReloadCallerNoteBackfillLevel2E2EFixture.cs"; private const string CacheFixtureFileName = "HotReloadCallSiteCacheE2EFixture.cs"; private const string CrossAssemblyName = "UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssembly"; private const string ReadBody = "return 1;"; @@ -107,6 +110,62 @@ public async Task Run_WhoseNoteStepRefusedADll_BackfillsItSoTheNextRunReadsNothi "The second run's budget refuses nothing."); } + /// + /// What: when the refused dll holds a caller whose own caller sits in a further uncached dll, + /// the cold run's backfill reads that dll too, so the next run reads no dll; the backfill's + /// recompute writes no budget-exhausted entry. + /// + [Test] + public async Task Run_WhoseNoteStepRefusedADllWithACallerChain_BackfillsTheNextLevelSoTheNextRunReadsNothing() + { + string fixturePath = FixturePath(Level2BackfillFixtureFileName); + string source = File.ReadAllText(fixturePath); + Assert.That(source, Does.Contain(ReadBody), "Precondition: the Read body anchor must exist."); + _scope = new HotReloadDomainTestScope(); + await _scope.InstalledWarmUpStopped; + // Why clear: entries cached by earlier tests would let the runs refuse nothing. + HotReloadCompiledCallSiteCache.Shared.Clear(); + int before = HotReloadCompiledCallSiteCache.Shared.LoadCount; + + HotReloadOrchestratorResult first = await RunWithReadReturningAsync( + fixturePath, + source, + 2, + ReadLevel2FixtureWithoutACallSite); + JArray exhausted = JArray.Parse(VibeLogger.GetLogsForAi(HotReloadConstants.VibeLogCallerNoteLoadBudgetExhausted)); + Assert.That(exhausted.Count, Is.EqualTo(1), "Precondition: the first run's budget must refuse a dll.\n" + FormatOutcomes(first)); + Assert.That( + exhausted[0]["context"]["refusedAssemblies"].ToObject(), + Is.EqualTo(new[] { CrossAssemblyName }), + "Precondition: the first run must refuse only the relay's dll; the next level is found later."); + + await HotReloadCompositionRoot.Services.CompiledCallers.Backfill.Completion; + int afterBackfill = HotReloadCompiledCallSiteCache.Shared.LoadCount; + Assert.That( + JArray.Parse(VibeLogger.GetLogsForAi(HotReloadConstants.VibeLogCallerNoteLoadBudgetExhausted)).Count, + Is.EqualTo(1), + "The backfill's recompute writes no budget-exhausted entry."); + + HotReloadOrchestratorResult second = await RunWithReadReturningAsync( + fixturePath, + source, + 3, + ReadLevel2FixtureWithoutACallSite); + Assert.That( + HotReloadCompiledCallSiteCache.Shared.LoadCount, + Is.EqualTo(afterBackfill), + "The second run reads no dll: the backfill already read the next caller level.\n" + FormatOutcomes(second)); + Assert.That( + JArray.Parse(VibeLogger.GetLogsForAi(HotReloadConstants.VibeLogCallerNoteLoadBudgetExhausted)).Count, + Is.EqualTo(1), + "The second run's budget refuses nothing."); + + JObject backfill = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogCallerNoteBackfillComplete); + Assert.That((int)backfill["rounds"], Is.GreaterThanOrEqualTo(2), "rounds"); + Assert.That((int)backfill["loaded"], Is.EqualTo(2), "loaded: the relay's dll and the level-2 caller's dll"); + Assert.That(afterBackfill - before, Is.EqualTo(3), "the host's dll in the run, then the two dlls in the backfill"); + } + /// /// What: a run that comes while the previous run's backfill is reading a dll waits for /// that dll, applies, and the backfill reads no later dll; the entry names the run. @@ -215,6 +274,15 @@ private static async Task RunWithReadReturningAsync return result; } + // Why reflection: a direct call here would be a compiled caller of Read in this test + // assembly, and the caller closure would stop at it before walking the relay's level. + private static int ReadLevel2FixtureWithoutACallSite() + { + MethodInfo read = typeof(HotReloadCallerNoteBackfillLevel2E2EFixture).GetMethod( + nameof(HotReloadCallerNoteBackfillLevel2E2EFixture.Read)); + return (int)read.Invoke(new HotReloadCallerNoteBackfillLevel2E2EFixture(), null); + } + private static async Task AwaitRunAsync(Task run) { HotReloadOrchestratorResult result = null; diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs new file mode 100644 index 000000000..d728de873 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs @@ -0,0 +1,18 @@ +using System.Runtime.CompilerServices; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// Compiled host whose one method the level-2 caller-note backfill E2E edits; its caller chain + /// runs through the cross-assembly relay to the level-2 fixture assembly, so a cold run's + /// backfill must read both dlls before the next run reads none. + /// + public class HotReloadCallerNoteBackfillLevel2E2EFixture + { + [MethodImpl(MethodImplOptions.NoInlining)] + public int Read() + { + return 1; + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs.meta new file mode 100644 index 000000000..8f817f245 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillLevel2E2EFixture.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d6dba7ff3e06f407d9f137a32efe323d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs b/Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs new file mode 100644 index 000000000..a0ab1a2a7 --- /dev/null +++ b/Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs @@ -0,0 +1,14 @@ +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// Compiled relay between the level-2 E2E host and its caller in the level-2 fixture assembly, + /// so the caller-note closure walks two cross-assembly levels. + /// + public static class HotReloadCallerNoteBackfillLevel2E2ERelay + { + public static int Call() + { + return new HotReloadCallerNoteBackfillLevel2E2EFixture().Read(); + } + } +} diff --git a/Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs.meta b/Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs.meta new file mode 100644 index 000000000..1ffec0615 --- /dev/null +++ b/Assets/Tests/Editor/HotReloadCallSiteCrossAssembly/HotReloadCallerNoteBackfillLevel2E2ERelay.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1abe54291482248428a5c95e48e91290 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2.meta b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2.meta new file mode 100644 index 000000000..45ae4d2c0 --- /dev/null +++ b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7981b9bdadaf74538bd9ab32c694ed65 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs new file mode 100644 index 000000000..91a777fe2 --- /dev/null +++ b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs @@ -0,0 +1,14 @@ +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// Compiled caller one level above the cross-assembly relay, so the caller-note closure of the + /// level-2 E2E host's method reaches a second dll only once the relay's dll has been read. + /// + public static class HotReloadCallerNoteBackfillLevel2E2ECaller + { + public static int Call() + { + return HotReloadCallerNoteBackfillLevel2E2ERelay.Call(); + } + } +} diff --git a/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs.meta b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs.meta new file mode 100644 index 000000000..89157047a --- /dev/null +++ b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/HotReloadCallerNoteBackfillLevel2E2ECaller.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d1278768d949446a0920598ba578135b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef new file mode 100644 index 000000000..ef86a7542 --- /dev/null +++ b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef @@ -0,0 +1,18 @@ +{ + "name": "UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2", + "rootNamespace": "io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload", + "references": [ + "GUID:fc86d7bfb642b4032b71b989e24c5fe8" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef.meta b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef.meta new file mode 100644 index 000000000..9221e2a50 --- /dev/null +++ b/Assets/Tests/Editor/HotReloadCallSiteCrossAssemblyLevel2/UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssemblyLevel2.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 87f5d615d36a148948da107d69157f70 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: