diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs index 73c2a6fe6..2707d8012 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs @@ -4,6 +4,8 @@ using HarmonyLib; +using Newtonsoft.Json.Linq; + using NUnit.Framework; using io.github.hatayama.UnityCliLoop.FirstPartyTools; @@ -101,6 +103,35 @@ public void UninstallInstalledServices_LeavesTheSiblingToolsReadingNoDomainInsta Assert.That(HotReloadPausePointCoordination.HotReloadSide, Is.Not.SameAs(installedPort)); } + /// + /// The pause point side the composition root installs captures the source snapshot + /// through the installed gate, under the pause_point trigger, before it reads the snapshot. + /// + [Test] + public void Install_PausePointSide_CapturesThroughTheInstalledGateWithThePausePointTrigger() + { + VibeLogger.ClearMemoryLogs(); + int captureCount = 0; + HotReloadSourceSnapshotCapture capture = new HotReloadSourceSnapshotCapture(() => + { + captureCount++; + return true; + }); + string source; + + using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture)) + { + source = HotReloadPausePointCoordination.HotReloadSide.GetVerifiedSnapshotSource( + "Assets/NoSuchSource.cs", + "Library/ScriptAssemblies/NoSuchAssembly.dll"); + } + + Assert.That(captureCount, Is.EqualTo(1), "captures"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogSourceSnapshotCaptured); + Assert.That((string)context["trigger"], Is.EqualTo(HotReloadConstants.SourceSnapshotCaptureTriggerPausePoint)); + Assert.That(source, Is.Null, "no snapshot exists for a dll that does not exist"); + } + /// /// The copies that swap one stage keep the same wired-value persistence and restore /// refresh, so a wiring recorded before the swap can still be restored after it and an diff --git a/Assets/Tests/Editor/HotReload/HotReloadDomainTests.cs b/Assets/Tests/Editor/HotReload/HotReloadDomainTests.cs index 8a85bc864..d2a11ac07 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadDomainTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadDomainTests.cs @@ -1004,6 +1004,52 @@ public void FindUnappliedRowForMethod_MethodWithAConstructedGenericParameter_Fin Assert.That(row.Label, Is.EqualTo(workerLabel)); } + /// + /// What: reading a verified snapshot source asks the gate to capture the snapshot first, so + /// a pause point set after both tools were disabled at domain load still has one to read. + /// + [Test] + public void GetVerifiedSnapshotSource_EnsuresTheSnapshotIsCaptured() + { + int ensureCount = 0; + HotReloadPausePointPort port = CreatePortWithSnapshotCapture(() => ensureCount++); + + port.GetVerifiedSnapshotSource(FileOne, typeof(HotReloadDomainTests).Assembly.Location); + + Assert.That(ensureCount, Is.EqualTo(1)); + } + + /// + /// What: an empty path is answered with no source before the gate is asked to capture. + /// + [Test] + public void GetVerifiedSnapshotSource_WithAnEmptyPath_DoesNotEnsureTheCapture() + { + int ensureCount = 0; + HotReloadPausePointPort port = CreatePortWithSnapshotCapture(() => ensureCount++); + + string source = port.GetVerifiedSnapshotSource(string.Empty, typeof(HotReloadDomainTests).Assembly.Location); + + Assert.That(source, Is.Null); + Assert.That(ensureCount, Is.EqualTo(0)); + } + + /// + /// What: a capture that throws reaches the pause point command instead of being turned into + /// a missing snapshot. + /// + [Test] + public void GetVerifiedSnapshotSource_WhenTheCaptureThrows_PassesTheExceptionOn() + { + InvalidOperationException captureFailure = new InvalidOperationException("The capture failed."); + HotReloadPausePointPort port = CreatePortWithSnapshotCapture(() => throw captureFailure); + + InvalidOperationException thrown = Assert.Throws( + () => port.GetVerifiedSnapshotSource(FileOne, typeof(HotReloadDomainTests).Assembly.Location)); + + Assert.That(thrown, Is.SameAs(captureFailure)); + } + // Why a generated name: an artifact assembly is compiled under a name of its own, so a // fixture that reused a project assembly's name would not resolve the way production does. private static Assembly CreateIntroducedTypeAssembly() @@ -1025,7 +1071,12 @@ private static void AssertRow( private HotReloadPausePointPort CreatePort(Func readSourceContentHashOrNull) { - return new HotReloadPausePointPort(_access.Domain, readSourceContentHashOrNull); + return new HotReloadPausePointPort(_access.Domain, readSourceContentHashOrNull, () => { }); + } + + private HotReloadPausePointPort CreatePortWithSnapshotCapture(Action ensureSnapshotCaptured) + { + return new HotReloadPausePointPort(_access.Domain, _ => RecordedSourceHash, ensureSnapshotCaptured); } // Records a partially applied reload of FileOne that read RecordedWorkerSourcePath. diff --git a/Assets/Tests/Editor/HotReload/HotReloadEditorStartupTests.cs b/Assets/Tests/Editor/HotReload/HotReloadEditorStartupTests.cs index 7cb294de6..4a0288dae 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadEditorStartupTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadEditorStartupTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Newtonsoft.Json.Linq; @@ -31,7 +32,7 @@ public void CaptureSourceSnapshotBeforeServingCommands_RunsTheInstalledCaptureOn using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture)) { - HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(); + HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(AllToolsEnabled); } Assert.That(captureCount, Is.EqualTo(1), "captures"); @@ -62,7 +63,7 @@ public void CaptureSourceSnapshotBeforeServingCommands_WhenTheCaptureThrows_Thro using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture)) { Assert.Throws( - () => HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands()); + () => HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(AllToolsEnabled)); HotReloadCompositionRoot.Services.SourceSnapshotCapture.EnsureCaptured( HotReloadConstants.SourceSnapshotCaptureTriggerFirstUpdateTick); @@ -70,5 +71,160 @@ public void CaptureSourceSnapshotBeforeServingCommands_WhenTheCaptureThrows_Thro Assert.That(captureCount, Is.EqualTo(2), "captures"); } + + /// + /// What: with both tools that read the snapshot disabled, the capture before serving + /// commands is skipped and logged, and the skip leaves the capture undone, so the first + /// reader that runs later captures through the gate. + /// + [Test] + public void CaptureSourceSnapshotBeforeServingCommands_WhenNeitherReaderToolIsEnabled_SkipsAndLeavesTheCaptureForAReader() + { + VibeLogger.ClearMemoryLogs(); + int captureCount = 0; + HotReloadSourceSnapshotCapture capture = CountingCapture(() => captureCount++); + + using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture)) + { + HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(NoToolEnabled); + + Assert.That(captureCount, Is.EqualTo(0), "captures while both readers are disabled"); + JObject skipped = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogSourceSnapshotSkipped); + Assert.That((string)skipped["trigger"], Is.EqualTo(HotReloadConstants.SourceSnapshotCaptureTriggerDomainLoad)); + Assert.That((string)skipped["reason"], Is.EqualTo(HotReloadConstants.SourceSnapshotSkipReasonToolsDisabled)); + Assert.That(CountVibeEntries(HotReloadConstants.VibeLogSourceSnapshotCaptured), Is.EqualTo(0), "captured entries"); + + HotReloadCompositionRoot.Services.SourceSnapshotCapture.EnsureCaptured( + HotReloadConstants.SourceSnapshotCaptureTriggerApply); + } + + Assert.That(captureCount, Is.EqualTo(1), "captures once a reader asks the gate"); + } + + /// + /// What: one enabled tool that reads the snapshot is enough for the capture before serving + /// commands to run. + /// + [TestCase(UnityCliLoopConstants.TOOL_NAME_HOT_RELOAD)] + [TestCase(UnityCliLoopConstants.SETTINGS_TOOL_NAME_PAUSE_POINT)] + public void CaptureSourceSnapshotBeforeServingCommands_WhenOnlyOneReaderToolIsEnabled_Captures(string enabledTool) + { + VibeLogger.ClearMemoryLogs(); + int captureCount = 0; + HotReloadSourceSnapshotCapture capture = CountingCapture(() => captureCount++); + + using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture)) + { + HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(name => name == enabledTool); + } + + Assert.That(captureCount, Is.EqualTo(1), "captures"); + JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogSourceSnapshotCaptured); + Assert.That((string)context["trigger"], Is.EqualTo(HotReloadConstants.SourceSnapshotCaptureTriggerDomainLoad)); + } + + /// + /// What: the startup capture asks about both reader tools by the names the tool settings + /// store them under, hot-reload first. + /// + [Test] + public void CaptureAtStartupUnlessReadersAreDisabled_WhenNeitherIsEnabled_AsksBothReaderToolsBySettingsName() + { + VibeLogger.ClearMemoryLogs(); + List askedNames = new List(); + HotReloadSourceSnapshotCapture capture = CountingCapture(() => { }); + + using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture)) + { + HotReloadEditorStartup.CaptureAtStartupUnlessReadersAreDisabled( + HotReloadConstants.SourceSnapshotCaptureTriggerDomainLoad, + name => + { + askedNames.Add(name); + return false; + }); + } + + Assert.That( + askedNames, + Is.EqualTo(new[] { UnityCliLoopConstants.TOOL_NAME_HOT_RELOAD, UnityCliLoopConstants.SETTINGS_TOOL_NAME_PAUSE_POINT })); + } + + /// + /// What: the first update tick's capture is skipped the same way when both reader tools are + /// disabled, and the skip names that trigger. + /// + [Test] + public void CaptureAtStartupUnlessReadersAreDisabled_AtTheFirstUpdateTick_WhenNeitherIsEnabled_SkipsWithThatTrigger() + { + VibeLogger.ClearMemoryLogs(); + int captureCount = 0; + HotReloadSourceSnapshotCapture capture = CountingCapture(() => captureCount++); + + using (HotReloadServicesTestScope.BeginWithSourceSnapshotCapture(capture)) + { + HotReloadEditorStartup.CaptureAtStartupUnlessReadersAreDisabled( + HotReloadConstants.SourceSnapshotCaptureTriggerFirstUpdateTick, + NoToolEnabled); + } + + Assert.That(captureCount, Is.EqualTo(0), "captures"); + JObject skipped = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogSourceSnapshotSkipped); + Assert.That((string)skipped["trigger"], Is.EqualTo(HotReloadConstants.SourceSnapshotCaptureTriggerFirstUpdateTick)); + } + + /// + /// What: with the hot-reload tool disabled the warm-up is not started, and the skip is logged + /// with its reason. + /// + [Test] + public void StartWarmUpUnlessHotReloadIsDisabled_WhenHotReloadIsDisabled_DoesNotStartAndLogsTheReason() + { + VibeLogger.ClearMemoryLogs(); + int startCount = 0; + + HotReloadEditorStartup.StartWarmUpUnlessHotReloadIsDisabled( + name => name != UnityCliLoopConstants.TOOL_NAME_HOT_RELOAD, + () => startCount++); + + Assert.That(startCount, Is.EqualTo(0), "warm-up starts"); + JObject skipped = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogWarmUpSkipped); + Assert.That((string)skipped["reason"], Is.EqualTo(HotReloadConstants.WarmUpSkipReasonToolDisabled)); + } + + /// + /// What: with the hot-reload tool enabled the warm-up is started once and no skip is logged. + /// + [Test] + public void StartWarmUpUnlessHotReloadIsDisabled_WhenHotReloadIsEnabled_StartsOnce() + { + VibeLogger.ClearMemoryLogs(); + int startCount = 0; + + HotReloadEditorStartup.StartWarmUpUnlessHotReloadIsDisabled( + name => name == UnityCliLoopConstants.TOOL_NAME_HOT_RELOAD, + () => startCount++); + + Assert.That(startCount, Is.EqualTo(1), "warm-up starts"); + Assert.That(CountVibeEntries(HotReloadConstants.VibeLogWarmUpSkipped), Is.EqualTo(0), "skipped entries"); + } + + private static bool AllToolsEnabled(string toolName) => true; + + private static bool NoToolEnabled(string toolName) => false; + + private static HotReloadSourceSnapshotCapture CountingCapture(Action onCapture) + { + return new HotReloadSourceSnapshotCapture(() => + { + onCapture(); + return true; + }); + } + + private static int CountVibeEntries(string operation) + { + return JArray.Parse(VibeLogger.GetLogsForAi(operation)).Count; + } } } diff --git a/Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs b/Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs index e811db475..619394bea 100644 --- a/Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs +++ b/Packages/src/Editor/CompositionRoot/UnityCliLoopEditorBootstrapper.cs @@ -1,3 +1,5 @@ +using System; + using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.FirstPartyTools; using io.github.hatayama.UnityCliLoop.Infrastructure; @@ -27,7 +29,8 @@ internal void Initialize() UnityCliLoopApplicationServices applicationServices = _applicationRegistration.Register(); ApplicationEditorStartup.Initialize(applicationServices.DomainReloadDetectionService); EditorRuntimeStateSnapshotSubscriber.InitializeForEditorStartup(); - FirstPartyToolsEditorStartup.Initialize(); + Func isToolEnabled = applicationServices.ToolSettingsUseCase.IsToolEnabled; + FirstPartyToolsEditorStartup.Initialize(isToolEnabled); InfrastructureEditorStartup.Initialize(applicationServices.EditorSettingsPort); PresentationEditorStartup.Initialize( applicationServices.EditorSettingsPort, @@ -45,8 +48,9 @@ internal void Initialize() // afterAssemblyReload, and every command except get-editor-status runs on the main // thread's update or tick, so no command of this domain is answered before this // returns — an edit made after `uloop compile` returns can no longer be captured as - // compiled source. - FirstPartyToolsEditorStartup.PrepareBeforeServingCommands(); + // compiled source. When both the hot-reload and pause-point tools are disabled, the + // capture is skipped here and runs on the first use of either tool instead. + FirstPartyToolsEditorStartup.PrepareBeforeServingCommands(isToolEnabled); } } } diff --git a/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs index 7a279d88b..2bc77b757 100644 --- a/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs +++ b/Packages/src/Editor/FirstPartyTools/FirstPartyToolsEditorStartup.cs @@ -1,3 +1,5 @@ +using System; + using UnityEditor; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools @@ -8,14 +10,18 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// public static class FirstPartyToolsEditorStartup { - public static void Initialize() + /// + /// Initializes every first-party tool's Editor startup work. + /// answers whether the user enabled a tool in the tool settings, by its settings name. + /// + public static void Initialize(Func isToolEnabled) { DomainReloadDisableScopeRecovery.RestoreForEditorStartup(); ExternalSceneChangeTracker.Initialize(); ControlPlayModeEditorStartup.Initialize(); PausePointEditorStartup.Initialize(); CompileEditorStartup.Initialize(); - HotReloadEditorStartup.Initialize(); + HotReloadEditorStartup.Initialize(isToolEnabled); WatchEditorStartup.Initialize(); ExecuteDynamicCodeEditorStartup.Initialize(); GetLogsEditorStartup.Initialize(); @@ -35,9 +41,10 @@ public static void Initialize() /// /// Runs the first-party tool work that has to finish before this domain answers its first - /// uloop command. Called once, at the end of the Editor startup. + /// uloop command. Called once, at the end of the Editor startup. + /// answers whether the user enabled a tool in the tool settings, by its settings name. /// - public static void PrepareBeforeServingCommands() + public static void PrepareBeforeServingCommands(Func isToolEnabled) { // Why not in an import worker: it never serves commands (the server does not start // there) and it shares Library with the main Editor, so a capture there would only @@ -47,7 +54,7 @@ public static void PrepareBeforeServingCommands() return; } - HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(); + HotReloadEditorStartup.CaptureSourceSnapshotBeforeServingCommands(isToolEnabled); } public static void ResetServerScopedServices() diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs index 6828ece28..11663b5e3 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs @@ -271,7 +271,9 @@ private static void Install(HotReloadServices services) HotReloadPausePointCoordination.HotReloadSide = new HotReloadPausePointPort( domain, - path => new HotReloadSourceContentHasher().TryComputeContentHashOfFileOrNull(path)); + path => new HotReloadSourceContentHasher().TryComputeContentHashOfFileOrNull(path), + () => services.SourceSnapshotCapture.EnsureCaptured( + HotReloadConstants.SourceSnapshotCaptureTriggerPausePoint)); // Attaching last keeps the invariant across the gap: the resolver only starts // answering binds once every gateway already points at the domain behind it. domain.IntroducedTypeResolver.Resume(); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadEditorStartup.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadEditorStartup.cs index 881fd704b..b3e05786d 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadEditorStartup.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadEditorStartup.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using UnityEditor; @@ -11,8 +12,10 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools // root only depends on the bundled-tool facade. internal static class HotReloadEditorStartup { - public static void Initialize() + public static void Initialize(Func isToolEnabled) { + Debug.Assert(isToolEnabled != null, "isToolEnabled must not be null."); + // Why not EditorApplication.delayCall: a cold-start session that hits Unity's native // "Scripts have compiler errors" dialog never flushes delayCall again for the rest of // that process's lifetime, even for later registrations — while @@ -23,7 +26,8 @@ public static void Initialize() void CaptureOnFirstUpdateTick() { EditorApplication.update -= CaptureOnFirstUpdateTick; - HotReloadCompositionRoot.Services.SourceSnapshotCapture.EnsureCaptured(HotReloadConstants.SourceSnapshotCaptureTriggerFirstUpdateTick); + CaptureAtStartupUnlessReadersAreDisabled( + HotReloadConstants.SourceSnapshotCaptureTriggerFirstUpdateTick, isToolEnabled); } // Why a callback of its own rather than a line in the capture above: an exception in @@ -42,7 +46,8 @@ void SweepArtifactsOnFirstUpdateTick() void StartWarmUpOnFirstUpdateTick() { EditorApplication.update -= StartWarmUpOnFirstUpdateTick; - HotReloadCompositionRoot.Services.WarmUp.Start(); + StartWarmUpUnlessHotReloadIsDisabled( + isToolEnabled, () => HotReloadCompositionRoot.Services.WarmUp.Start()); } // The services are rebuilt here rather than on first use because the introduced type @@ -91,10 +96,59 @@ void StartWarmUpOnFirstUpdateTick() /// Captures the source snapshot of the current compile before this domain answers any uloop /// command, so an edit made after the compile returned is never captured as compiled source. /// - internal static void CaptureSourceSnapshotBeforeServingCommands() + internal static void CaptureSourceSnapshotBeforeServingCommands(Func isToolEnabled) + { + CaptureAtStartupUnlessReadersAreDisabled( + HotReloadConstants.SourceSnapshotCaptureTriggerDomainLoad, isToolEnabled); + } + + /// + /// Captures the source snapshot at a startup trigger unless both tools that read it are + /// disabled in the tool settings. + /// + internal static void CaptureAtStartupUnlessReadersAreDisabled(string trigger, Func isToolEnabled) { - HotReloadCompositionRoot.Services.SourceSnapshotCapture.EnsureCaptured( - HotReloadConstants.SourceSnapshotCaptureTriggerDomainLoad); + Debug.Assert(!string.IsNullOrEmpty(trigger), "trigger must not be null or empty."); + Debug.Assert(isToolEnabled != null, "isToolEnabled must not be null."); + + // Why only the startup triggers ask: a reader that runs (a hot reload run, a pause point) + // belongs to an enabled tool and captures through the gate itself. A capture that comes + // late stays correct, because a source written after the compile started is checked + // against the PDB. + bool anyReaderEnabled = isToolEnabled(UnityCliLoopConstants.TOOL_NAME_HOT_RELOAD) + || isToolEnabled(UnityCliLoopConstants.SETTINGS_TOOL_NAME_PAUSE_POINT); + if (!anyReaderEnabled) + { + // Why the gate is not marked: a tool enabled later in this domain must still + // capture through it before its first read. + VibeLogger.LogInfo( + HotReloadConstants.VibeLogSourceSnapshotSkipped, + "Hot reload source snapshot capture skipped.", + new { trigger, reason = HotReloadConstants.SourceSnapshotSkipReasonToolsDisabled }); + return; + } + + HotReloadCompositionRoot.Services.SourceSnapshotCapture.EnsureCaptured(trigger); + } + + /// + /// Starts the warm-up unless the hot-reload tool is disabled in the tool settings. + /// + internal static void StartWarmUpUnlessHotReloadIsDisabled(Func isToolEnabled, Action startWarmUp) + { + Debug.Assert(isToolEnabled != null, "isToolEnabled must not be null."); + Debug.Assert(startWarmUp != null, "startWarmUp must not be null."); + + if (!isToolEnabled(UnityCliLoopConstants.TOOL_NAME_HOT_RELOAD)) + { + VibeLogger.LogInfo( + HotReloadConstants.VibeLogWarmUpSkipped, + "Hot reload warm-up skipped.", + new { reason = HotReloadConstants.WarmUpSkipReasonToolDisabled }); + return; + } + + startWarmUp(); } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Patching/HotReloadPausePointPort.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Patching/HotReloadPausePointPort.cs index 75c000929..c6ad53482 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Patching/HotReloadPausePointPort.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Patching/HotReloadPausePointPort.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; @@ -16,13 +17,22 @@ internal sealed class HotReloadPausePointPort : IHotReloadPausePointPort { private readonly HotReloadDomain _domain; private readonly Func _readSourceContentHashOrNull; + private readonly Action _ensureSnapshotCaptured; // Why the hash reader is passed in: the hasher lives in the apply pipeline's assembly, // which this one may not reference, and the check must hash the way the worker did. - public HotReloadPausePointPort(HotReloadDomain domain, Func readSourceContentHashOrNull) + // Why the capture is passed in: the gate that captures the source snapshot belongs to the + // services the composition root installs, which this assembly does not reference. + public HotReloadPausePointPort( + HotReloadDomain domain, + Func readSourceContentHashOrNull, + Action ensureSnapshotCaptured) { + Debug.Assert(ensureSnapshotCaptured != null, "ensureSnapshotCaptured must not be null."); + _domain = domain; _readSourceContentHashOrNull = readSourceContentHashOrNull; + _ensureSnapshotCaptured = ensureSnapshotCaptured; } public MethodBase GetActiveShimForMethod(MethodBase method) @@ -54,6 +64,9 @@ public string GetVerifiedSnapshotSource(string projectRelativeFile, string dllPa return null; } + // Why: with both tools disabled at domain load nothing was captured, and a pause point + // is the first reader once the pause-point tool is enabled. + _ensureSnapshotCaptured(); return HotReloadSourceBaseline.LoadVerifiedSnapshotSource(projectRelativeFile, dllPath); } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadConstants.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadConstants.cs index da47c35f5..7967a25f2 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadConstants.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadConstants.cs @@ -696,6 +696,14 @@ public static bool IsPublicizableProjectAssemblyFileName(string fileNameWithoutE public const string SourceSnapshotCaptureTriggerDomainLoad = "domain_load"; public const string SourceSnapshotCaptureTriggerFirstUpdateTick = "first_update_tick"; public const string SourceSnapshotCaptureTriggerApply = "apply"; + public const string SourceSnapshotCaptureTriggerPausePoint = "pause_point"; + + // Why the capture at a startup trigger did not run, as logged in + // hot_reload_source_snapshot_skipped; and the warm-up's matching reason in + // hot_reload_warm_up_skipped. + public const string VibeLogSourceSnapshotSkipped = "hot_reload_source_snapshot_skipped"; + public const string SourceSnapshotSkipReasonToolsDisabled = "tools_disabled"; + public const string WarmUpSkipReasonToolDisabled = "tool_disabled"; // What stopped a running warm-up or caller-note backfill, as named in the cancelledBy field // of hot_reload_warm_up_complete and hot_reload_caller_note_backfill_complete. diff --git a/docs/hot-reload.md b/docs/hot-reload.md index 36606e3e3..86b0fc72e 100644 --- a/docs/hot-reload.md +++ b/docs/hot-reload.md @@ -80,7 +80,11 @@ whose bytes do not match the PDB checksum carries a mark that it was edited afte and the default selection, the sibling scan and the skip check treat a marked source as changed; a capture moves the previous build's directory to the new MVID's and rewrites only the sources whose length or write time changed, that carry no unmarked manifest line, or that were -written since the compile started; it deletes the copies of sources no longer in the assembly), +written since the compile started; it deletes the copies of sources no longer in the assembly. +When both the hot-reload and pause-point tools are disabled in the tool settings, the capture +after a domain reload is skipped; the first hot reload run or pause point after either tool is +enabled captures it, and any source written since the compile started is then checked against +the PDB), `Library/UloopHotReload/PdbDocuments/fmt1/.txt` (the documents the PDB's sequence points refer to, stamped with the dll's and the PDB's length and write time and the MVID, so the first run after a domain reload does not walk the PDB again while they still match), and diff --git a/docs/vibe-logs.md b/docs/vibe-logs.md index 7aa174458..b750dc519 100644 --- a/docs/vibe-logs.md +++ b/docs/vibe-logs.md @@ -93,7 +93,12 @@ command ran against: `hot_reload_source_snapshot_captured` appears once per domain when the source snapshot capture ran to completion, with its `trigger` (`domain_load` normally; `first_update_tick` or `apply` only when the capture at domain load threw or Unity listed no compilation assembly - yet) and `captureMs`. + yet; `apply` or `pause_point` when the captures at startup were skipped and a hot reload run + or a pause point is the first to read the snapshot) and `captureMs`. When both the hot-reload + and pause-point tools are disabled in the tool settings, the captures at `domain_load` and + `first_update_tick` each write `hot_reload_source_snapshot_skipped` with that `trigger` and + `reason` `tools_disabled` instead, and the first hot reload run or pause point after either + tool is enabled captures it. `hot_reload_source_snapshot_breakdown` appears once each time the capture runs to the end, just before `hot_reload_source_snapshot_captured`. It also appears when Unity listed no compilation assembly, in which case no captured entry follows, and it does not appear when the @@ -126,7 +131,8 @@ command ran against: assemblies, each item's `outcome` (`done`, `cancelled` or `failed`) and time, and `cancelledBy` (`run`, `beforeAssemblyReload`, `compilationStarted`, or null), or one `hot_reload_warm_up_skipped` with its `reason` (`no_targets`, `compiling`, `updating`, - `no_compiled_assembly`, `run_started_first`). `cancelled` covers both an item that never started + `no_compiled_assembly`, `run_started_first`, `tool_disabled` when the hot-reload tool is + disabled in the tool settings). `cancelled` covers both an item that never started (`ms` 0) and one stopped between its units (one dll or one transform worker request each) with units left, with the time it ran. The items run in the order `publicized_targets`, `call_sites`, `referenced_method_sets`, `transform_worker`, `pdb_documents`. The warm-up's transform worker