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