diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTestRuns.cs b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTestRuns.cs new file mode 100644 index 000000000..40b592607 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTestRuns.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; + +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// Compiled-caller runs for tests that build an apply context by hand, outside any hot reload run. + /// + internal static class HotReloadCompiledCallersTestRuns + { + /// + /// Opens a run on compiled callers of its own: a fresh cache and a backfill that reads + /// nothing. Its hold is taken on that fresh cache, never on the shared one, so it keeps no + /// shared entry alive and needs no Dispose; the caller scans it does still read the shared + /// cache without a hold, as gate tests did before scans moved onto the run. The backfill is + /// idle, so the run begins synchronously and is read without blocking. + /// + internal static HotReloadCompiledCallersRun OpenIsolated() + { + HotReloadCompiledCallers compiledCallers = new HotReloadCompiledCallers( + new HotReloadCompiledCallSiteCache(HotReloadCompiledCallSiteCache.DefaultBudgetBytes), + new HotReloadCallSiteBackfill((dllPath, ct) => Task.CompletedTask)); + Task begin = compiledCallers.BeginRunAsync(); + Assert.That(begin.IsCompleted, Is.True, "Precondition: a run on an idle backfill begins synchronously."); + return begin.Result; + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTestRuns.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTestRuns.cs.meta new file mode 100644 index 000000000..c59a245e6 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTestRuns.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43582fde9e890428e9bef0a545db5708 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs index 46140d5e7..5c04a7bfa 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs @@ -1,9 +1,14 @@ +using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + using io.github.hatayama.UnityCliLoop.FirstPartyTools; using io.github.hatayama.UnityCliLoop.ToolContracts; @@ -104,5 +109,35 @@ public async Task Dispose_WithoutEnd_ReleasesTheHoldAndStartsNoBackgroundRead() Assert.That(_loader.Ran, Is.Empty, "no background read starts"); Assert.That(_backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Idle)); } + + /// + /// What: a caller scan asked of a run that already ended is reported, since its reads are + /// no longer held against eviction. + /// + [Test] + public async Task FindCallSites_AfterTheRunEnded_Asserts() + { + HotReloadCompiledCallersRun run = await _compiledCallers.BeginRunAsync(); + run.End("corr"); + LogAssert.Expect(LogType.Assert, "FindCallSites is called only while the run is open."); + + run.FindCallSites(Path.GetFullPath(Path.Combine(Application.dataPath, "..")), Array.Empty()); + + run.Dispose(); + } + + /// + /// What: a caller scan asked of a run that was disposed without End (a run that threw) is + /// reported too, since its hold is released as well. + /// + [Test] + public async Task FindCallSites_AfterTheRunWasDisposedWithoutEnd_Asserts() + { + HotReloadCompiledCallersRun run = await _compiledCallers.BeginRunAsync(); + run.Dispose(); + LogAssert.Expect(LogType.Assert, "FindCallSites is called only while the run is open."); + + run.FindCallSites(Path.GetFullPath(Path.Combine(Application.dataPath, "..")), Array.Empty()); + } } } diff --git a/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorLeaveOutTests.cs b/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorLeaveOutTests.cs index 07e0f1dac..df72056bc 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorLeaveOutTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorLeaveOutTests.cs @@ -289,11 +289,16 @@ private async Task> RunGroupAsync( collaborators, context, compileResult, entriesToPatch), collaborators.EntryApplier.ApplyPreparedEntries))) { - return await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync( - files, - "leave-out-test", - new HotReloadRunTiming(), - CancellationToken.None); + using (HotReloadCompiledCallersRun compiledCallersRun = + await HotReloadCompositionRoot.Services.CompiledCallers.BeginRunAsync()) + { + return await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync( + files, + compiledCallersRun, + "leave-out-test", + new HotReloadRunTiming(), + CancellationToken.None); + } } } diff --git a/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs b/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs index 2455fe8df..befbc164a 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs @@ -639,11 +639,16 @@ public async Task ProcessGroupAsync_WhenTheWorkerFails_BuildsUnappliedResultsWit collaborators, context, compileResult, entriesToPatch), collaborators.EntryApplier.ApplyPreparedEntries))) { - results = await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync( - new[] { file }, - "worker-failure-test", - new HotReloadRunTiming(), - CancellationToken.None); + using (HotReloadCompiledCallersRun compiledCallersRun = + await HotReloadCompositionRoot.Services.CompiledCallers.BeginRunAsync()) + { + results = await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync( + new[] { file }, + compiledCallersRun, + "worker-failure-test", + new HotReloadRunTiming(), + CancellationToken.None); + } } Assert.That(results, Has.Count.EqualTo(1)); @@ -749,13 +754,18 @@ private static async Task RunGroupWithPhaseStubsAsync( { try { - total.Start(); - await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync( - new[] { stubs.GroupFile }, - "phase-timing-test", - timing, - CancellationToken.None); - total.Stop(); + using (HotReloadCompiledCallersRun compiledCallersRun = + await HotReloadCompositionRoot.Services.CompiledCallers.BeginRunAsync()) + { + total.Start(); + await HotReloadCompositionRoot.Services.GroupProcessor.ProcessGroupAsync( + new[] { stubs.GroupFile }, + compiledCallersRun, + "phase-timing-test", + timing, + CancellationToken.None); + total.Stop(); + } } catch (OperationCanceledException exception) { @@ -1821,7 +1831,8 @@ private static HotReloadApplyContext CreateContext( workerOutput, new[] { callerFile, targetFile }, null, - new HotReloadCompileFailureNoteSources(workerOutput.skipped, Array.Empty())); + new HotReloadCompileFailureNoteSources(workerOutput.skipped, Array.Empty()), + HotReloadCompiledCallersTestRuns.OpenIsolated()); } private static HotReloadApplyContext CreateEmptyEntriesContext( @@ -1848,7 +1859,8 @@ private static HotReloadApplyContext CreateEmptyEntriesContext( emptyWorkerOutput, context.Files, null, - new HotReloadCompileFailureNoteSources(emptyWorkerOutput.skipped, Array.Empty())); + new HotReloadCompileFailureNoteSources(emptyWorkerOutput.skipped, Array.Empty()), + context.CompiledCallersRun); } private static Assembly FindCompilationAssembly() diff --git a/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs index 1f4d37377..e96a7887d 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs @@ -102,8 +102,7 @@ private protected static HotReloadGroupStageCollaborators WithInternalAccessGran collaborators.EditorStateSnapshotCapture, collaborators.CommitPolicy, collaborators.PlayMode, - internalAccessGrant, - collaborators.CompiledCallers); + internalAccessGrant); } private protected static Task RunReloadAsync( diff --git a/Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs b/Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs index 3c6c1987f..f9c8cf418 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs @@ -3710,7 +3710,8 @@ private static HotReloadApplyContext CreateApplyContext( workerOutput, new[] { file }, null, - new HotReloadCompileFailureNoteSources(workerOutput.skipped, Array.Empty())); + new HotReloadCompileFailureNoteSources(workerOutput.skipped, Array.Empty()), + HotReloadCompiledCallersTestRuns.OpenIsolated()); } private static HotReloadOrchestratorResult ToOrchestratorResult( diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs index e4729bea5..7785aacae 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs @@ -41,17 +41,6 @@ public static HotReloadCompiledCallers CreateProduction() return new HotReloadCompiledCallers(cache, backfill); } - /// - /// Finds the compiled call and ldftn sites that reference any of , - /// reading every compiled assembly it needs. Call it only while a run begun with - /// is open: outside one, a read on another thread may evict and - /// dispose a cached assembly this is still reading. - /// - public List FindCallSites(string projectRoot, HotReloadCompiledMethodIdentity[] targets) - { - return HotReloadCallSiteScanner.FindCallSites(projectRoot, targets).Hits; - } - /// The compiled dlls of the assemblies that reference . public IReadOnlyList CollectReferencingDllPaths(string projectRoot, string assemblyName) { diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs index dc425c659..7a306c6cc 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs @@ -24,6 +24,7 @@ public sealed class HotReloadCompiledCallersRun : IDisposable private IReadOnlyList _requests; private bool _notesBuilt; private bool _ended; + private bool _disposed; internal HotReloadCompiledCallersRun( IDisposable hold, @@ -38,6 +39,18 @@ internal HotReloadCompiledCallersRun( _backfill = backfill; } + /// + /// Finds the compiled call and ldftn sites that reference any of , + /// reading every compiled assembly it needs. Call before the run ends: the assemblies it + /// reads stay cached only while the run is open, so another thread cannot evict and + /// dispose one this is still reading. + /// + public List FindCallSites(string projectRoot, HotReloadCompiledMethodIdentity[] targets) + { + Debug.Assert(!_ended && !_disposed, "FindCallSites is called only while the run is open."); + return HotReloadCallSiteScanner.FindCallSites(projectRoot, targets).Hits; + } + /// /// Builds the one-shot lifecycle note of each request: one entry per request, in the same /// order, null where no note can be proven. Reads at most the configured number of compiled @@ -105,6 +118,7 @@ public void End(string correlationId) /// public void Dispose() { + _disposed = true; _hold.Dispose(); } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadApplyContext.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadApplyContext.cs index 9913911cc..80ceb1b94 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadApplyContext.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadApplyContext.cs @@ -28,7 +28,8 @@ internal HotReloadApplyContext( TransformWorkerOutputDto workerOutput, IReadOnlyList files, HotReloadPreparedIntroducedTypes preparedIntroducedTypes, - HotReloadCompileFailureNoteSources compileFailureNoteSources) + HotReloadCompileFailureNoteSources compileFailureNoteSources, + HotReloadCompiledCallersRun compiledCallersRun) { Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be empty."); Debug.Assert(!string.IsNullOrEmpty(assemblyName), "assemblyName must not be empty."); @@ -39,6 +40,7 @@ internal HotReloadApplyContext( Debug.Assert(workerOutput != null, "workerOutput must not be null."); Debug.Assert(files != null && files.Count > 0, "A group must hold a file."); Debug.Assert(compileFailureNoteSources != null, "compileFailureNoteSources must not be null."); + Debug.Assert(compiledCallersRun != null, "compiledCallersRun must not be null."); ProjectRoot = projectRoot; AssemblyName = assemblyName; @@ -51,6 +53,7 @@ internal HotReloadApplyContext( Files = files; PreparedIntroducedTypes = preparedIntroducedTypes; CompileFailureNoteSources = compileFailureNoteSources; + CompiledCallersRun = compiledCallersRun; List<(string ProjectRelativePath, string AssemblyResolvePath)> filePaths = new List<(string ProjectRelativePath, string AssemblyResolvePath)>(files.Count); @@ -101,6 +104,10 @@ internal HotReloadApplyContext( // What a shim compile failure of this group can be explained with. internal HotReloadCompileFailureNoteSources CompileFailureNoteSources { get; } + // The compiled-caller analysis of the run this group belongs to; caller scans go through it + // so they read only while the run keeps the cached assemblies alive. + internal HotReloadCompiledCallersRun CompiledCallersRun { get; } + // The edited files of this group, in the order they were sent to the worker. internal IReadOnlyList Files { get; } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs index 0297501ec..10676d4b2 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs @@ -152,8 +152,7 @@ internal static HotReloadServices CreateServices( editorStateSnapshotCapture, new HotReloadGroupCommitPolicy(domain, fileEntryApplier), playMode, - HotReloadMonoInternalAccessGrant.ForThisProcess(), - compiledCallers); + HotReloadMonoInternalAccessGrant.ForThisProcess()); // A factory, not a built value: the stages are bound to the collaborators built here, // and a caller that built them from the installed services would bind a replacement's // group run back to the domain that was installed when it called. diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupProcessor.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupProcessor.cs index 9bd7eb7e6..bd52a5172 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupProcessor.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupProcessor.cs @@ -48,11 +48,13 @@ internal HotReloadGroupProcessor( internal async Task> ProcessGroupAsync( IReadOnlyList files, + HotReloadCompiledCallersRun compiledCallersRun, string correlationId, HotReloadRunTiming timing, CancellationToken ct) { Debug.Assert(files != null && files.Count > 0, "A group must hold a file."); + Debug.Assert(compiledCallersRun != null, "compiledCallersRun must not be null."); Debug.Assert(timing != null, "timing must not be null."); HotReloadGroupFile firstFile = files[0]; @@ -72,7 +74,7 @@ internal async Task> ProcessGroupAsync using (timing.MeasureDetail(HotReloadConstants.TimingDetailStepSnapshotGroupState)) { - SnapshotGroupState(files); + HotReloadGroupStateSnapshot.Capture(_domain, _collaborators.PlayMode, files); } // Why here and not at the leave-out decision: the ledgers need the main thread, which @@ -159,7 +161,7 @@ internal async Task> ProcessGroupAsync if (preparation.Prepared == null) { - return await TransformAndApplyGroupAsync(files, workerInput, null, refusedTypes, activePaths, correlationId, timing, ct) + return await TransformAndApplyGroupAsync(files, workerInput, null, refusedTypes, activePaths, compiledCallersRun, correlationId, timing, ct) .ConfigureAwait(false); } @@ -169,6 +171,7 @@ internal async Task> ProcessGroupAsync preparation.Prepared, refusedTypes, activePaths, + compiledCallersRun, correlationId, timing, ct).ConfigureAwait(false); @@ -194,6 +197,7 @@ private async Task> TransformAndApplyP HotReloadPreparedIntroducedTypes prepared, IReadOnlyList refusedTypes, HashSet activePaths, + HotReloadCompiledCallersRun compiledCallersRun, string correlationId, HotReloadRunTiming timing, CancellationToken ct) @@ -227,6 +231,7 @@ private async Task> TransformAndApplyP prepared, refusedTypes, activePaths, + compiledCallersRun, correlationId, timing, ct).ConfigureAwait(false); @@ -255,6 +260,7 @@ private async Task> TransformAndApplyG HotReloadPreparedIntroducedTypes prepared, IReadOnlyList refusedTypes, HashSet activePaths, + HotReloadCompiledCallersRun compiledCallersRun, string correlationId, HotReloadRunTiming timing, CancellationToken ct) @@ -281,7 +287,7 @@ private async Task> TransformAndApplyG if (leftOutPaths.Count == 0) { - return await ApplyTransformedGroupAsync(files, workerInput, workerOutput, prepared, refusedTypes, correlationId, timing, ct) + return await ApplyTransformedGroupAsync(files, workerInput, workerOutput, prepared, refusedTypes, compiledCallersRun, correlationId, timing, ct) .ConfigureAwait(false); } @@ -306,6 +312,7 @@ private async Task> TransformAndApplyG retryResult.Output, prepared, refusedTypes, + compiledCallersRun, correlationId, timing, ct).ConfigureAwait(false) @@ -366,6 +373,7 @@ private async Task> ApplyTransformedGr TransformWorkerOutputDto workerOutput, HotReloadPreparedIntroducedTypes prepared, IReadOnlyList refusedTypes, + HotReloadCompiledCallersRun compiledCallersRun, string correlationId, HotReloadRunTiming timing, CancellationToken ct) @@ -424,7 +432,8 @@ private async Task> ApplyTransformedGr workerOutput, files, prepared, - new HotReloadCompileFailureNoteSources(workerOutput.skipped, refusedTypes)); + new HotReloadCompileFailureNoteSources(workerOutput.skipped, refusedTypes), + compiledCallersRun); } Stopwatch gateWatch = Stopwatch.StartNew(); @@ -610,37 +619,5 @@ internal async Task RevalidateBeforeRevertAsync( revertUnchangedPatches(); return true; } - - private void SnapshotGroupState(IReadOnlyList files) - { - foreach (HotReloadGroupFile file in files) - { - // Why snapshot at the group's apply entry: runs process groups sequentially, and - // RevertUnchangedPatches / BeginFileGeneration mutate ledgers after the worker. - // The worker itself does not. - file.SnapshotLabels = HotReloadAppliedSourceLifecycle.CollectActiveLabelsForFile( - _domain, - file.ProjectRelativePath); - file.SnapshotAddedLabels = new HashSet( - _domain.ListActiveAddedMethodKeys(file.ProjectRelativePath), - StringComparer.Ordinal); - file.SnapshotForwardedUnityMessageLabels = _collaborators.PlayMode.IsPlaying - ? HotReloadDeactivatedUnityMessageNote.CollectForwardedLabels( - _domain.DescribeAddedMembersOfFile(file.ProjectRelativePath)) - : new HashSet(StringComparer.Ordinal); - // Why projectRelativePath (not workerSourcePath): contentPathOverride E2E copies - // live under Library/UloopHotReload/TestSources/ and are absent from the PDB - // document list. Assembly resolution already computed the on-disk path. - file.SnapshotSource = HotReloadSourceBaseline.LoadVerifiedSnapshotSource( - file.ProjectRelativePath, - file.TargetDllPath); - file.SnapshotMissReason = file.SnapshotSource == null - ? HotReloadSourceBaseline.DescribeSnapshotMiss( - file.ProjectRelativePath, - file.TargetDllPath, - file.NewSourceMembershipEvidence) - : HotReloadSnapshotMissReason.None; - } - } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs index a9c5e9aae..f4390284b 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs @@ -24,8 +24,7 @@ internal HotReloadGroupStageCollaborators( IHotReloadEditorStateSnapshotCapture editorStateSnapshotCapture, HotReloadGroupCommitPolicy commitPolicy, IHotReloadPlayModeQuery playMode, - IHotReloadInternalAccessGrant internalAccessGrant, - HotReloadCompiledCallers compiledCallers) + IHotReloadInternalAccessGrant internalAccessGrant) { Debug.Assert(domain != null, "domain must not be null."); Debug.Assert(patcher != null, "patcher must not be null."); @@ -38,7 +37,6 @@ internal HotReloadGroupStageCollaborators( Debug.Assert(commitPolicy != null, "commitPolicy must not be null."); Debug.Assert(playMode != null, "playMode must not be null."); Debug.Assert(internalAccessGrant != null, "internalAccessGrant must not be null."); - Debug.Assert(compiledCallers != null, "compiledCallers must not be null."); Domain = domain; Patcher = patcher; FileEntryApplier = fileEntryApplier; @@ -49,7 +47,6 @@ internal HotReloadGroupStageCollaborators( CommitPolicy = commitPolicy; PlayMode = playMode; InternalAccessGrant = internalAccessGrant; - CompiledCallers = compiledCallers; } internal HotReloadDomain Domain { get; } @@ -73,7 +70,5 @@ internal HotReloadGroupStageCollaborators( // Why one grant per services graph: its availability is probed once when it is built, and // every artifact of the domain these collaborators serve is granted through that answer. internal IHotReloadInternalAccessGrant InternalAccessGrant { get; } - - internal HotReloadCompiledCallers CompiledCallers { get; } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStateSnapshot.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStateSnapshot.cs new file mode 100644 index 000000000..5f66c5aa7 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStateSnapshot.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Takes the per-file snapshot a group compares its apply against. + /// + internal static class HotReloadGroupStateSnapshot + { + /// + /// Records on each file of the group what the domain holds for it before the group + /// applies: active labels, added methods, forwarded Unity messages, and the verified + /// source baseline (or why there is none). Call on the Unity main thread. + /// + internal static void Capture( + HotReloadDomain domain, + IHotReloadPlayModeQuery playMode, + IReadOnlyList files) + { + foreach (HotReloadGroupFile file in files) + { + // Why snapshot at the group's apply entry: runs process groups sequentially, and + // RevertUnchangedPatches / BeginFileGeneration mutate ledgers after the worker. + // The worker itself does not. + file.SnapshotLabels = HotReloadAppliedSourceLifecycle.CollectActiveLabelsForFile( + domain, + file.ProjectRelativePath); + file.SnapshotAddedLabels = new HashSet( + domain.ListActiveAddedMethodKeys(file.ProjectRelativePath), + StringComparer.Ordinal); + file.SnapshotForwardedUnityMessageLabels = playMode.IsPlaying + ? HotReloadDeactivatedUnityMessageNote.CollectForwardedLabels( + domain.DescribeAddedMembersOfFile(file.ProjectRelativePath)) + : new HashSet(StringComparer.Ordinal); + // Why projectRelativePath (not workerSourcePath): contentPathOverride E2E copies + // live under Library/UloopHotReload/TestSources/ and are absent from the PDB + // document list. Assembly resolution already computed the on-disk path. + file.SnapshotSource = HotReloadSourceBaseline.LoadVerifiedSnapshotSource( + file.ProjectRelativePath, + file.TargetDllPath); + file.SnapshotMissReason = file.SnapshotSource == null + ? HotReloadSourceBaseline.DescribeSnapshotMiss( + file.ProjectRelativePath, + file.TargetDllPath, + file.NewSourceMembershipEvidence) + : HotReloadSnapshotMissReason.None; + } + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStateSnapshot.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStateSnapshot.cs.meta new file mode 100644 index 000000000..8b345cf26 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStateSnapshot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b7d746a1cec3742b4a908fa41e333abc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestrator.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestrator.cs index 97b64015d..4072ad520 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestrator.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestrator.cs @@ -206,7 +206,8 @@ await ProcessPlannedGroupAsync( contentPathOverrideByFile, isLastChangedGroup, extraResults, - run) + run, + compiledCallersRun) .ConfigureAwait(false); } @@ -322,7 +323,8 @@ private async Task ProcessPlannedGroupAsync( IReadOnlyDictionary contentPathOverrideByFile, bool isLastGroupOfAssembly, List<(string Path, HotReloadFileProcessResult Result)> extraResults, - HotReloadRunAccumulator run) + HotReloadRunAccumulator run, + HotReloadCompiledCallersRun compiledCallersRun) { Debug.Assert(inputIndexes != null && inputIndexes.Count > 0, "A group must hold a file."); List filesOfGroup = new List(inputIndexes.Count); @@ -351,7 +353,7 @@ private async Task ProcessPlannedGroupAsync( // ProcessGroupAsync switches back via MainThreadSwitcher (EditorApplication.update // queue) before any main-thread-only editor API or Harmony patch. IReadOnlyList groupResults = - await _groupProcessor.ProcessGroupAsync(filesOfGroup, correlationId, timing, ct) + await _groupProcessor.ProcessGroupAsync(filesOfGroup, compiledCallersRun, correlationId, timing, ct) .ConfigureAwait(false); Debug.Assert( groupResults.Count == filesOfGroup.Count, diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs index b3e614afa..8905fac86 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs @@ -42,7 +42,7 @@ internal static async Task TryApplySignatureChangeGat context.WorkerOutput.unchangedMethods ?? Array.Empty(), context.WorkerOutput.skipped ?? Array.Empty(), removedSignatures); - List hits = collaborators.CompiledCallers.FindCallSites(context.ProjectRoot, targets); + List hits = context.CompiledCallersRun.FindCallSites(context.ProjectRoot, targets); Dictionary> uncoveredCallersByTarget = CollectInitialUncoveredCallers(context.AssemblyName, entries, hits, deletedCallerExemptions);