diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs index d33a27c78..20915415e 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs @@ -53,7 +53,7 @@ public class HotReloadCallSiteScannerTests [Test] public void FindCallSites_OrdinaryCaller_ReportsCallerMethodKey() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.CalledFromOrdinaryMethod), Array.Empty(), @@ -74,7 +74,7 @@ public void FindCallSites_OrdinaryCaller_ReportsCallerMethodKey() [Test] public void FindCallSites_NeverCalled_ReturnsEmpty() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.NeverCalled), Array.Empty(), @@ -91,8 +91,8 @@ public void FindCallSites_MissingSelectedAssembly_ReportsAssemblyName() { const string missingAssemblyName = "MissingCompiledAssembly"; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadCallSiteScanner.CompiledMethodIdentity target = - new HotReloadCallSiteScanner.CompiledMethodIdentity( + HotReloadCompiledMethodIdentity target = + new HotReloadCompiledMethodIdentity( missingAssemblyName, new HotReloadMetadataTypeName(FixtureTypeMetadataName), nameof(HotReloadCallSiteScannerFixture.NeverCalled), @@ -111,7 +111,7 @@ public void FindCallSites_MissingSelectedAssembly_ReportsAssemblyName() [Test] public void FindCallSites_DelegateAssignment_ReportsLdftnCaller() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.CalledOnlyViaDelegate), Array.Empty(), @@ -131,7 +131,7 @@ public void FindCallSites_DelegateAssignment_ReportsLdftnCaller() [Test] public void FindCallSites_AsyncCaller_ReportsLogicalOwnerMethodKey() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.CalledFromAsyncMethod), Array.Empty(), @@ -152,7 +152,7 @@ public void FindCallSites_AsyncCaller_ReportsLogicalOwnerMethodKey() [Test] public void FindCallSites_GenericTypeInstantiation_ReportsCaller() { - List hits = FindHits( + List hits = FindHits( GenericHostTypeMetadataName, nameof(GenericHost.Target), Array.Empty(), @@ -170,7 +170,7 @@ public void FindCallSites_GenericTypeInstantiation_ReportsCaller() [Test] public void FindCallSites_GenericMethodInstantiation_ReportsCallAndLdftn() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.GenericMethodTarget), Array.Empty(), @@ -188,7 +188,7 @@ public void FindCallSites_GenericMethodInstantiation_ReportsCallAndLdftn() [Test] public void FindCallSites_OrdinarySelfRecursion_ReturnsEmpty() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.SelfRecursive), new[] { "System.Int32" }, @@ -210,17 +210,17 @@ public void FindCallSites_TwoTargets_AttributesEachHitToMatchingTargetKey() string ordinaryTargetKey = FixtureTypeMetadataName + "::CalledFromOrdinaryMethod()"; string delegateTargetKey = FixtureTypeMetadataName + "::CalledOnlyViaDelegate()"; - List hits = HotReloadCallSiteScanner.FindCallSites( + List hits = HotReloadCallSiteScanner.FindCallSites( projectRoot, new[] { - new HotReloadCallSiteScanner.CompiledMethodIdentity( + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName(FixtureTypeMetadataName), nameof(HotReloadCallSiteScannerFixture.CalledFromOrdinaryMethod), Array.Empty(), 0), - new HotReloadCallSiteScanner.CompiledMethodIdentity( + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName(FixtureTypeMetadataName), nameof(HotReloadCallSiteScannerFixture.CalledOnlyViaDelegate), @@ -229,9 +229,9 @@ public void FindCallSites_TwoTargets_AttributesEachHitToMatchingTargetKey() }).Hits; Assert.That(hits.Count, Is.EqualTo(2)); - HotReloadCallSiteScanner.CallSiteHit ordinaryHit = null; - HotReloadCallSiteScanner.CallSiteHit delegateHit = null; - foreach (HotReloadCallSiteScanner.CallSiteHit hit in hits) + HotReloadCallSiteHit ordinaryHit = null; + HotReloadCallSiteHit delegateHit = null; + foreach (HotReloadCallSiteHit hit in hits) { if (hit.TargetMethodKey == ordinaryTargetKey) { @@ -261,7 +261,7 @@ public void FindCallSites_TwoTargets_AttributesEachHitToMatchingTargetKey() [Test] public void FindCallSites_CrossAssemblyCaller_ReportsReferencedAssemblyHit() { - List hits = FindHits( + List hits = FindHits( CrossAssemblyTargetTypeMetadataName, nameof(HotReloadCallSiteScannerCrossAssemblyTarget.Called), Array.Empty(), @@ -284,13 +284,13 @@ public void FindCallSites_CrossAssemblyCaller_ReportsReferencedAssemblyHit() [Test] public void FindCallSites_SameKeyCallersAcrossAssemblies_ReportsBothCallerAssemblies() { - List hits = FindHits( + List hits = FindHits( QualifiedCallerIdentityTargetTypeMetadataName, nameof(HotReloadQualifiedCallerIdentityTarget.Called), Array.Empty(), 0); List callerAssemblyNames = new List(); - foreach (HotReloadCallSiteScanner.CallSiteHit hit in hits) + foreach (HotReloadCallSiteHit hit in hits) { callerAssemblyNames.Add(hit.CallerAssemblyName); Assert.That( @@ -315,7 +315,7 @@ public void FindCallSites_SameKeyCallersAcrossAssemblies_ReportsBothCallerAssemb [Test] public void FindCallSites_SameFullNameForeignAssemblyTarget_ExcludesForeignCallSite() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.CalledFromCrossAssembly), Array.Empty(), @@ -331,7 +331,7 @@ public void FindCallSites_SameFullNameForeignAssemblyTarget_ExcludesForeignCallS [Test] public void FindCallSites_GenericArityCaller_KeyDiffersFromNonGenericCaller() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.CalledFromGenericArityCaller), Array.Empty(), @@ -352,7 +352,7 @@ public void FindCallSites_GenericArityCaller_KeyDiffersFromNonGenericCaller() [Test] public void FindCallSites_AsyncSelfRecursion_ReturnsEmpty() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.AsyncSelfRecursive), new[] { "System.Int32" }, @@ -368,7 +368,7 @@ public void FindCallSites_AsyncSelfRecursion_ReturnsEmpty() [Test] public void FindCallSites_NestedCaller_KeepsTheMetadataSeparator() { - List hits = FindHits( + List hits = FindHits( FixtureTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.CalledFromNestedType), Array.Empty(), @@ -390,7 +390,7 @@ public void FindCallSites_NestedCaller_KeepsTheMetadataSeparator() [Test] public void FindCallSites_NestedTarget_ReportsTheCaller() { - List hits = FindHits( + List hits = FindHits( NestedCallerHostTypeMetadataName, nameof(HotReloadCallSiteScannerFixture.NestedCallerHost.CalledFromOuterType), Array.Empty(), @@ -445,25 +445,25 @@ public void FindCallSites_TwoTargetsMatchingOneCallSite_ReportsOneHitForTheFirst string rawAssemblyName = CompilationPipeline.GetAssemblyNameFromScriptPath( TestScriptProjectRelativePath); string assemblyName = Path.GetFileNameWithoutExtension(rawAssemblyName); - HotReloadCallSiteScanner.CompiledMethodIdentity typeParameterTarget = - new HotReloadCallSiteScanner.CompiledMethodIdentity( + HotReloadCompiledMethodIdentity typeParameterTarget = + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName(FixtureTypeMetadataName), nameof(HotReloadCallSiteScannerFixture.GenericParameterTarget), new[] { "T" }, 1); - HotReloadCallSiteScanner.CompiledMethodIdentity int32Target = - new HotReloadCallSiteScanner.CompiledMethodIdentity( + HotReloadCompiledMethodIdentity int32Target = + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName(FixtureTypeMetadataName), nameof(HotReloadCallSiteScannerFixture.GenericParameterTarget), new[] { "System.Int32" }, 1); - List hits = HotReloadCallSiteScanner.FindCallSites( + List hits = HotReloadCallSiteScanner.FindCallSites( projectRoot, new[] { typeParameterTarget, int32Target }).Hits; - List reversedHits = HotReloadCallSiteScanner.FindCallSites( + List reversedHits = HotReloadCallSiteScanner.FindCallSites( projectRoot, new[] { int32Target, typeParameterTarget }).Hits; @@ -487,7 +487,7 @@ public void FindCallSites_TwoTargetsMatchingOneCallSite_ReportsOneHitForTheFirst [Test] public void FindCallSites_CrossAssemblyGenericTypeInstantiation_ReportsCaller() { - List hits = FindHits( + List hits = FindHits( CrossAssemblyGenericHostTypeMetadataName, nameof(HotReloadCrossAssemblyGenericHost.Target), Array.Empty(), @@ -507,15 +507,15 @@ public void FindCallSites_CrossAssemblyGenericTypeInstantiation_ReportsCaller() [Test] public void FindCallSites_CrossAssemblyGenericMethodInstantiation_ReportsCallAndLdftn() { - List hits = FindHits( + List hits = FindHits( CrossAssemblyTargetTypeMetadataName, nameof(HotReloadCallSiteScannerCrossAssemblyTarget.GenericMethod), Array.Empty(), 1); Assert.That(hits.Count, Is.EqualTo(2)); - HotReloadCallSiteScanner.CallSiteHit call = hits.Find(hit => !hit.IsFunctionPointerLoad); - HotReloadCallSiteScanner.CallSiteHit load = hits.Find(hit => hit.IsFunctionPointerLoad); + HotReloadCallSiteHit call = hits.Find(hit => !hit.IsFunctionPointerLoad); + HotReloadCallSiteHit load = hits.Find(hit => hit.IsFunctionPointerLoad); Assert.That(call.CallerAssemblyName, Is.EqualTo(CrossAssemblyCallerAssemblyName)); Assert.That(load.CallerAssemblyName, Is.EqualTo(CrossAssemblyCallerAssemblyName)); Assert.That( @@ -533,7 +533,7 @@ public void FindCallSites_CrossAssemblyGenericMethodInstantiation_ReportsCallAnd [Test] public void FindCallSites_CrossAssemblyNestedTarget_ReportsCaller() { - List hits = FindHits( + List hits = FindHits( CrossAssemblyNestedTargetTypeMetadataName, nameof(HotReloadCallSiteScannerCrossAssemblyTarget.Nested.CalledFromOtherAssembly), Array.Empty(), @@ -736,36 +736,38 @@ public async Task FindCallSites_CachedAssembliesDoNotConsumeTheLoadBudget() /// per scan call would read two. /// [Test] - public async Task ApplyOneShotCallerNotes_WithFreshCache_SharesOneLoadBudgetAcrossCandidateAssemblies() + public async Task BuildOneShotCallerNotes_WithFreshCache_SharesOneLoadBudgetAcrossCandidateAssemblies() { await StopInstalledWarmUpAsync(); HotReloadCompiledCallSiteCache.Shared.Clear(); int before = HotReloadCompiledCallSiteCache.Shared.LoadCount; - HotReloadRunAccumulator run = new HotReloadRunAccumulator( - HotReloadCompositionRoot.Services.Domain, - HotReloadCompositionRoot.Services.Patcher, - HotReloadCompositionRoot.Services.UnityMessageForwarding, - false); - HotReloadMethodOutcome targetOutcome = HotReloadMethodOutcome.Patched("Type.Called", "Assets/Test.cs"); - HotReloadMethodOutcome callerOutcome = HotReloadMethodOutcome.Patched("Type.Call", "Assets/Test2.cs"); - run.OneShotCallerNoteCandidates.Add(new HotReloadOneShotCallerNoteEnricher.Candidate( - new HotReloadCallSiteScanner.CompiledMethodIdentity( - GetTestAssemblyName(), - new HotReloadMetadataTypeName(CrossAssemblyTargetTypeMetadataName), - nameof(HotReloadCallSiteScannerCrossAssemblyTarget.Called), - Array.Empty(), - 0), - targetOutcome)); - run.OneShotCallerNoteCandidates.Add(new HotReloadOneShotCallerNoteEnricher.Candidate( - new HotReloadCallSiteScanner.CompiledMethodIdentity( - CrossAssemblyCallerAssemblyName, - new HotReloadMetadataTypeName(CrossAssemblyCallerTypeMetadataName), - "Call", - Array.Empty(), - 0), - callerOutcome)); + HotReloadCompiledCallers compiledCallers = new HotReloadCompiledCallers( + HotReloadCompiledCallSiteCache.Shared, + new HotReloadCallSiteBackfill((dllPath, ct) => Task.CompletedTask)); + HotReloadOneShotCallerNoteRequest[] requests = + { + new HotReloadOneShotCallerNoteRequest( + new HotReloadCompiledMethodIdentity( + GetTestAssemblyName(), + new HotReloadMetadataTypeName(CrossAssemblyTargetTypeMetadataName), + nameof(HotReloadCallSiteScannerCrossAssemblyTarget.Called), + Array.Empty(), + 0), + "Type.Called"), + new HotReloadOneShotCallerNoteRequest( + new HotReloadCompiledMethodIdentity( + CrossAssemblyCallerAssemblyName, + new HotReloadMetadataTypeName(CrossAssemblyCallerTypeMetadataName), + "Call", + Array.Empty(), + 0), + "Type.Call") + }; - run.ApplyOneShotCallerNotes(GetProjectRoot(), "test-correlation"); + using (HotReloadCompiledCallersRun compiledCallersRun = await compiledCallers.BeginRunAsync()) + { + compiledCallersRun.BuildOneShotCallerNotes(GetProjectRoot(), requests, "test-correlation"); + } Assert.That(HotReloadCompiledCallSiteCache.Shared.LoadCount - before, Is.EqualTo(1)); } @@ -795,10 +797,10 @@ private static Task StopInstalledWarmUpAsync() HotReloadServices installed = HotReloadCompositionRoot.Services; return Task.WhenAll( installed.WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope), - installed.CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope)); + installed.CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerTestScope)); } - private static List FindHits( + private static List FindHits( string typeMetadataName, string methodName, string[] parameterTypeFullNames, @@ -813,8 +815,8 @@ private static HotReloadCallSiteScanner.HotReloadCallSiteScanResult Scan( string[] parameterTypeFullNames, int genericArity) { - HotReloadCallSiteScanner.CompiledMethodIdentity target = - new HotReloadCallSiteScanner.CompiledMethodIdentity( + HotReloadCompiledMethodIdentity target = + new HotReloadCompiledMethodIdentity( GetTestAssemblyName(), new HotReloadMetadataTypeName(typeMetadataName), methodName, @@ -831,8 +833,8 @@ private static HotReloadCallSiteScanner.HotReloadCallSiteScanResult ScanWithBudg string methodName, HotReloadCallSiteLoadBudget loadBudget) { - HotReloadCallSiteScanner.CompiledMethodIdentity target = - new HotReloadCallSiteScanner.CompiledMethodIdentity( + HotReloadCompiledMethodIdentity target = + new HotReloadCompiledMethodIdentity( GetTestAssemblyName(), new HotReloadMetadataTypeName(typeMetadataName), methodName, diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallSiteWarmUpItemTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallSiteWarmUpItemTests.cs index 5d4617b0d..3c90a1a76 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallSiteWarmUpItemTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallSiteWarmUpItemTests.cs @@ -46,11 +46,11 @@ public void TearDown() [Test] public void LoadCallSites_LoadsTheDllOnce_AndAgainReadsNothing() { - HotReloadWarmUpTarget[] targets = { CopyTarget() }; + string[] dllPaths = { CopyDll() }; - int first = HotReloadCallSiteWarmUpItem.LoadCallSites(targets, _cache, CancellationToken.None); + int first = HotReloadCompiledCallers.LoadCallSites(dllPaths, _cache, CancellationToken.None); int afterFirst = _cache.LoadCount; - HotReloadCallSiteWarmUpItem.LoadCallSites(targets, _cache, CancellationToken.None); + HotReloadCompiledCallers.LoadCallSites(dllPaths, _cache, CancellationToken.None); Assert.That(first, Is.EqualTo(1), "loaded targets"); Assert.That(afterFirst, Is.EqualTo(1), "reads after the first load"); @@ -63,15 +63,15 @@ public void LoadCallSites_LoadsTheDllOnce_AndAgainReadsNothing() [Test] public void LoadCallSites_WhenCancelled_ThrowsAndLoadsNothing() { - HotReloadWarmUpTarget[] targets = { CopyTarget() }; + string[] dllPaths = { CopyDll() }; Assert.Throws( - () => HotReloadCallSiteWarmUpItem.LoadCallSites(targets, _cache, new CancellationToken(true))); + () => HotReloadCompiledCallers.LoadCallSites(dllPaths, _cache, new CancellationToken(true))); Assert.That(_cache.LoadCount, Is.EqualTo(0), "reads"); } - private HotReloadWarmUpTarget CopyTarget() + private string CopyDll() { string source = Path.Combine( Path.GetFullPath(Path.Combine(Application.dataPath, "..")), @@ -79,11 +79,7 @@ private HotReloadWarmUpTarget CopyTarget() TestAssemblyName + HotReloadConstants.CompiledAssemblyExtension); string dllPath = Path.Combine(_tempDirectory, Path.GetFileName(source)); File.Copy(source, dllPath); - return new HotReloadWarmUpTarget( - TestAssemblyName, - dllPath, - Path.ChangeExtension(dllPath, ".pdb"), - Array.Empty()); + return dllPath; } } } diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs index 860fa2c7d..04e547bf9 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs @@ -9,6 +9,7 @@ using NUnit.Framework; using UnityEngine; +using Stopwatch = System.Diagnostics.Stopwatch; using io.github.hatayama.UnityCliLoop.FirstPartyTools; using io.github.hatayama.UnityCliLoop.ToolContracts; @@ -26,6 +27,7 @@ public class HotReloadCallerNoteBackfillE2ETests private const string CacheFixtureFileName = "HotReloadCallSiteCacheE2EFixture.cs"; private const string CrossAssemblyName = "UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssembly"; private const string ReadBody = "return 1;"; + private const int HeldReadMilliseconds = 100; private HotReloadDomainTestScope _scope; @@ -79,7 +81,7 @@ public async Task Run_WhoseNoteStepRefusedADll_BackfillsItSoTheNextRunReadsNothi Is.EqualTo(new[] { CrossAssemblyName }), "Precondition: the refused dll must be the cross-assembly caller."); - await HotReloadCompositionRoot.Services.CallSiteBackfill.Completion; + await HotReloadCompositionRoot.Services.CompiledCallers.Backfill.Completion; Assert.That( HotReloadCompiledCallSiteCache.Shared.LoadCount - before, Is.EqualTo(2), @@ -144,6 +146,54 @@ public async Task Run_WhileTheBackfillOfThePreviousRunIsInFlight_WaitsForThatDll Assert.That((string)context["cancelledBy"], Is.EqualTo(HotReloadConstants.WarmUpCancelledByRun), "cancelledBy"); } + /// + /// What: a run that comes while a backfill read is in flight counts the wait for that read + /// in its warm_up_yield step, so a slow start shows where its time went. + /// + [Test] + public async Task Run_WhileABackfillReadIsInFlight_CountsTheWaitInWarmUpYield() + { + string fixturePath = FixturePath(CacheFixtureFileName); + string source = File.ReadAllText(fixturePath); + Assert.That(source, Does.Contain(ReadBody), "Precondition: the Read body anchor must exist."); + PendingDllLoader loader = new PendingDllLoader(); + loader.PendingFor.Add("a.dll"); + HotReloadCallSiteBackfill backfill = new HotReloadCallSiteBackfill(loader.Load); + _scope = HotReloadDomainTestScope.WithCallSiteBackfill(backfill); + await _scope.InstalledWarmUpStopped; + VibeLogger.ClearMemoryLogs(); + backfill.Start(new[] { "a.dll" }, "corr"); + + string editedPath = HotReloadTestSourceWriter.WriteEditedSource( + "CallerNoteBackfillE2E.cs", + source.Replace(ReadBody, "return 4;")); + Task run = HotReloadCompositionRoot.Services.Orchestrator.RunAsync( + new[] { fixturePath }, + editedPath, + CancellationToken.None); + Assert.That(run.IsCompleted, Is.False, "Precondition: the run must be waiting for the read."); + // Why measure here: the run's warm_up_yield step started before RunAsync returned and + // ends only after the release below, so it covers at least this span. + Stopwatch held = Stopwatch.StartNew(); + await Task.Delay(HeldReadMilliseconds); + long heldMs = held.ElapsedMilliseconds; + loader.Release("a.dll"); + HotReloadOrchestratorResult result = await AwaitRunAsync(run); + + JArray details = JArray.Parse(VibeLogger.GetLogsForAi(HotReloadConstants.VibeLogTimingDetail)); + Assert.That(details.Count, Is.EqualTo(1), "timing details\n" + FormatOutcomes(result)); + long warmUpYieldMs = -1; + foreach (JToken step in (JArray)details[0]["context"]["steps"]) + { + if ((string)step["step"] == HotReloadConstants.TimingDetailStepWarmUpYield) + { + warmUpYieldMs = (long)step["ms"]; + } + } + + Assert.That(warmUpYieldMs, Is.GreaterThanOrEqualTo(heldMs), "the wait for the read counts in warm_up_yield"); + } + private static async Task RunWithReadReturningAsync( string fixturePath, string source, diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs new file mode 100644 index 000000000..3add8991f --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// Contract of the compiled-caller analysis at its entry point: when a run takes the call-site + /// cache hold relative to a background read, and when ending or disposing the run releases it + /// and starts the background reads. Each test uses a cache and a loader of its own and touches + /// neither the installed services nor the shared cache. + /// + public sealed class HotReloadCompiledCallersTests + { + private PendingDllLoader _loader; + private HotReloadCompiledCallSiteCache _cache; + private HotReloadCallSiteBackfill _backfill; + private HotReloadCompiledCallers _compiledCallers; + + [SetUp] + public void SetUp() + { + _loader = new PendingDllLoader(); + _cache = new HotReloadCompiledCallSiteCache(HotReloadCompiledCallSiteCache.DefaultBudgetBytes); + _backfill = new HotReloadCallSiteBackfill(_loader.Load); + _compiledCallers = new HotReloadCompiledCallers(_cache, _backfill); + VibeLogger.ClearMemoryLogs(); + } + + [TearDown] + public void TearDown() + { + _loader.ReleaseAll(); + VibeLogger.ClearMemoryLogs(); + } + + /// + /// What: a run begins only after the background read in flight ended, so the hold is not + /// taken while the read holds the cache, and the backfill stops before its next dll. + /// + [Test] + public async Task BeginRunAsync_WhileABackgroundReadIsInFlight_TakesTheHoldOnlyAfterTheReadEnds() + { + _loader.PendingFor.Add("a.dll"); + _backfill.Start(new[] { "a.dll", "b.dll" }, "corr"); + + Task begin = _compiledCallers.BeginRunAsync(); + Assert.That(begin.IsCompleted, Is.False, "the run waits for the read in flight"); + Assert.That(_cache.HoldDepth, Is.EqualTo(0), "no hold while the read is in flight"); + + _loader.Release("a.dll"); + using (HotReloadCompiledCallersRun run = await begin) + { + Assert.That(_cache.HoldDepth, Is.EqualTo(1), "the run holds the entries"); + Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read"); + } + + Assert.That(_cache.HoldDepth, Is.EqualTo(0), "disposing the run releases the hold"); + } + + /// + /// What: ending a run releases its hold before the background read of the dlls its notes + /// could not read starts, so those reads evict as reads outside a run do. + /// + [Test] + public async Task End_ReleasesTheHoldBeforeTheBackgroundReadOfTheRefusedDllsStarts() + { + List holdDepthAtLoad = new List(); + HotReloadCallSiteBackfill backfill = new HotReloadCallSiteBackfill((dllPath, ct) => + { + holdDepthAtLoad.Add(_cache.HoldDepth); + return Task.CompletedTask; + }); + HotReloadCompiledCallers compiledCallers = new HotReloadCompiledCallers(_cache, backfill); + HotReloadCompiledCallersRun run = await compiledCallers.BeginRunAsync(); + run.RememberRefusedDllPathsForTesting(new[] { "refused.dll" }); + + run.End("corr"); + await backfill.Completion; + run.Dispose(); + + Assert.That(holdDepthAtLoad, Is.EqualTo(new[] { 0 }), "the refused dll is read once, with no hold open"); + Assert.That(_cache.HoldDepth, Is.EqualTo(0)); + } + + /// + /// What: a run that ends without End (an exception) releases its hold and reads none of the + /// dlls its notes could not read. + /// + [Test] + public async Task Dispose_WithoutEnd_ReleasesTheHoldAndStartsNoBackgroundRead() + { + HotReloadCompiledCallersRun run = await _compiledCallers.BeginRunAsync(); + run.RememberRefusedDllPathsForTesting(new[] { "refused.dll" }); + + run.Dispose(); + + Assert.That(_cache.HoldDepth, Is.EqualTo(0), "the hold is released"); + Assert.That(_loader.Ran, Is.Empty, "no background read starts"); + Assert.That(_backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Idle)); + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs.meta new file mode 100644 index 000000000..9121c702f --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5429193b8e65245dfa68b4224aa8480f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs index b631a411f..73c2a6fe6 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs @@ -184,7 +184,7 @@ private static HotReloadServices CreateServicesSharing(HotReloadDomain domain) new HotReloadApplicationPlayModeQuery(), new HotReloadSourceSnapshotCapture(() => true), installed.WarmUp, - installed.CallSiteBackfill); + installed.CompiledCallers); } private static HotReloadServices CreateServicesOwning(HotReloadDomain domain) @@ -199,7 +199,7 @@ private static HotReloadServices CreateServicesOwning(HotReloadDomain domain) new HotReloadApplicationPlayModeQuery(), new HotReloadSourceSnapshotCapture(() => true), HotReloadWarmUpTestDoubles.CreateInert(), - HotReloadCompositionRoot.CreateProductionCallSiteBackfill()); + HotReloadCompiledCallers.CreateProduction()); } private static HotReloadIntroducedTypeArtifact CreateArtifact(string metadataName) diff --git a/Assets/Tests/Editor/HotReload/HotReloadDomainTestScope.cs b/Assets/Tests/Editor/HotReload/HotReloadDomainTestScope.cs index 711fd72ac..01338f55f 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadDomainTestScope.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadDomainTestScope.cs @@ -35,7 +35,7 @@ private HotReloadDomainTestScope(HotReloadServices services) HotReloadServices installed = HotReloadCompositionRoot.Services; InstalledWarmUpStopped = Task.WhenAll( installed.WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope), - installed.CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope)); + installed.CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerTestScope)); // The capture is main-thread only, and a run inside the scope normalizes script paths // against these roots on the background threads it switches to. @@ -57,7 +57,7 @@ internal static HotReloadDomainTestScope WithWarmUp(HotReloadWarmUp warmUp) return new HotReloadDomainTestScope( HotReloadCompositionRoot.CreateProductionServicesWith( warmUp, - HotReloadCompositionRoot.CreateProductionCallSiteBackfill())); + HotReloadCompiledCallers.CreateProduction())); } /// @@ -69,7 +69,7 @@ internal static HotReloadDomainTestScope WithCallSiteBackfill(HotReloadCallSiteB return new HotReloadDomainTestScope( HotReloadCompositionRoot.CreateProductionServicesWith( HotReloadWarmUpTestDoubles.CreateInert(), - callSiteBackfill)); + new HotReloadCompiledCallers(HotReloadCompiledCallSiteCache.Shared, callSiteBackfill))); } public void Dispose() diff --git a/Assets/Tests/Editor/HotReload/HotReloadEditorTickPumpTestSafety.cs b/Assets/Tests/Editor/HotReload/HotReloadEditorTickPumpTestSafety.cs index 83b70e68e..64abbb19c 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadEditorTickPumpTestSafety.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadEditorTickPumpTestSafety.cs @@ -28,7 +28,7 @@ public void StartPump() // they read the shared caches on a pool thread (after a reload, or after a hot reload // run made just before the tests), and the tests that count those reads must not race them. _ = HotReloadCompositionRoot.Services.WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestRun); - _ = HotReloadCompositionRoot.Services.CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestRun); + _ = HotReloadCompositionRoot.Services.CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerTestRun); StopPump(); // EditorApplication.SignalTick is [ThreadSafe] in the Editor bindings, which is what // allows the timer thread to call it. diff --git a/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs b/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs index c3bfc866a..2455fe8df 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs @@ -1321,9 +1321,9 @@ private static HotReloadSignatureChangeGate.SignatureChangeGateResult CreateGate { return HotReloadSignatureChangeGate.SignatureChangeGateResult.WarningsOnly( new List(), - new List + new List { - new HotReloadCallSiteScanner.CallSiteHit + new HotReloadCallSiteHit { CallerAssemblyName = AssemblyName, CallerMethodKey = CallerKey, @@ -1340,7 +1340,7 @@ private static HotReloadSignatureChangeGate.SignatureChangeGateResult CreateEmpt { return HotReloadSignatureChangeGate.SignatureChangeGateResult.WarningsOnly( new List(), - new List(), + new List(), new HashSet()); } @@ -1353,9 +1353,9 @@ private static HotReloadSignatureChangeGate.SignatureChangeGateResult CreateGate }; return HotReloadSignatureChangeGate.SignatureChangeGateResult.WarningsOnly( new List(), - new List + new List { - new HotReloadCallSiteScanner.CallSiteHit + new HotReloadCallSiteHit { CallerAssemblyName = AssemblyName, CallerMethodKey = CallerKey, @@ -2037,7 +2037,7 @@ public void SignatureChangeGateResult_Retried_ReportsTheWorkerRetryWithoutFailin null, new List(), new List(), - new List(), + new List(), new HashSet(), new List()); diff --git a/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs index e96a7887d..1f4d37377 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs @@ -102,7 +102,8 @@ private protected static HotReloadGroupStageCollaborators WithInternalAccessGran collaborators.EditorStateSnapshotCapture, collaborators.CommitPolicy, collaborators.PlayMode, - internalAccessGrant); + internalAccessGrant, + collaborators.CompiledCallers); } private protected static Task RunReloadAsync( diff --git a/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerClosureTests.cs b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerClosureTests.cs index dcd5ba460..09f0d8a99 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerClosureTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerClosureTests.cs @@ -25,7 +25,7 @@ public void Resolve_LevelTwoCallerIsAwake_ReturnsThatRoot() List roots = HotReloadOneShotCallerClosure.Resolve( new[] { CreateDirectHelperHit() }, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { CreateAwakeCallerOfHelperHit() }, + new List { CreateAwakeCallerOfHelperHit() }, new List()), hit => hit.CallerMethodName == "Awake"); @@ -41,8 +41,8 @@ public void Resolve_LevelTwoCallerIsAwake_ReturnsThatRoot() [Test] public void Resolve_LevelTwoScanIsIncompleteBecauseOfUnreadAssemblies_ReturnsNull() { - List scannedIdentities = - new List(); + List scannedIdentities = + new List(); List roots = HotReloadOneShotCallerClosure.Resolve( new[] { CreateDirectHelperHit() }, @@ -50,7 +50,7 @@ public void Resolve_LevelTwoScanIsIncompleteBecauseOfUnreadAssemblies_ReturnsNul { scannedIdentities.Add(identities); return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { CreateAwakeCallerOfHelperHit() }, + new List { CreateAwakeCallerOfHelperHit() }, new List(), 0, null, @@ -65,9 +65,9 @@ public void Resolve_LevelTwoScanIsIncompleteBecauseOfUnreadAssemblies_ReturnsNul Assert.That(scannedIdentities[0][0].MethodName, Is.EqualTo(HelperMethodName)); } - private static HotReloadCallSiteScanner.CallSiteHit CreateDirectHelperHit() + private static HotReloadCallSiteHit CreateDirectHelperHit() { - return new HotReloadCallSiteScanner.CallSiteHit + return new HotReloadCallSiteHit { CallerAssemblyName = "Assembly.Callers", CallerTypeMetadataName = new HotReloadMetadataTypeName(HelperTypeMetadataName), @@ -80,9 +80,9 @@ private static HotReloadCallSiteScanner.CallSiteHit CreateDirectHelperHit() }; } - private static HotReloadCallSiteScanner.CallSiteHit CreateAwakeCallerOfHelperHit() + private static HotReloadCallSiteHit CreateAwakeCallerOfHelperHit() { - return new HotReloadCallSiteScanner.CallSiteHit + return new HotReloadCallSiteHit { CallerAssemblyName = "Assembly.Callers", CallerTypeMetadataName = new HotReloadMetadataTypeName("Callers.Mono"), diff --git a/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs new file mode 100644 index 000000000..526cdf35e --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// EditMode coverage for attaching one-shot lifecycle notes to a run's method outcomes: which + /// candidates are asked for a note and which outcomes a returned note replaces. + /// + public sealed class HotReloadOneShotCallerNoteAttacherTests + { + /// + /// What: an outcome that already has a note (the transform worker's) is not asked for one + /// and keeps its note. + /// + [Test] + public void Attach_OutcomeThatAlreadyHasANote_IsNotRequestedAndKeepsItsNote() + { + const string workerNote = "Worker lifecycle note."; + HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs", workerNote); + List outcomes = new List { outcome }; + List candidates = new List + { + CreateCandidate("Assembly.One", outcome) + }; + List received = new List(); + + HotReloadOneShotCallerNoteAttacher.Attach( + outcomes, + candidates, + requests => + { + received.AddRange(requests); + return new string[requests.Count]; + }); + + Assert.That(received, Is.Empty); + Assert.That(outcomes[0].LifecycleNote, Is.EqualTo(workerNote)); + } + + /// + /// What: each candidate becomes a request with its method and identity, and a returned note + /// replaces that candidate's outcome while a null leaves its outcome untouched. + /// + [Test] + public void Attach_NoteForARequest_ReplacesItsOutcome() + { + const string note = "One-shot note."; + HotReloadMethodOutcome first = HotReloadMethodOutcome.Patched("Type.First", "Assets/First.cs"); + HotReloadMethodOutcome second = HotReloadMethodOutcome.Patched("Type.Second", "Assets/Second.cs"); + List outcomes = new List { first, second }; + List candidates = new List + { + CreateCandidate("Assembly.One", first), + CreateCandidate("Assembly.Two", second) + }; + List received = new List(); + + HotReloadOneShotCallerNoteAttacher.Attach( + outcomes, + candidates, + requests => + { + received.AddRange(requests); + return new[] { note, null }; + }); + + Assert.That(received.Count, Is.EqualTo(2)); + Assert.That(received[0].Method, Is.EqualTo("Type.First")); + Assert.That(received[0].Identity.AssemblyName, Is.EqualTo("Assembly.One")); + Assert.That(received[1].Method, Is.EqualTo("Type.Second")); + Assert.That(received[1].Identity.AssemblyName, Is.EqualTo("Assembly.Two")); + Assert.That(outcomes[0].LifecycleNote, Is.EqualTo(note)); + Assert.That(outcomes[1], Is.SameAs(second)); + } + + /// + /// What: when two candidates point at the same outcome, the first note wins. + /// + [Test] + public void Attach_TwoCandidatesForOneOutcome_KeepsTheFirstNote() + { + HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); + List outcomes = new List { outcome }; + List candidates = new List + { + CreateCandidate("Assembly.One", outcome), + CreateCandidate("Assembly.Two", outcome) + }; + + HotReloadOneShotCallerNoteAttacher.Attach( + outcomes, + candidates, + requests => new[] { "first", "second" }); + + Assert.That(outcomes.Count, Is.EqualTo(1)); + Assert.That(outcomes[0].LifecycleNote, Is.EqualTo("first")); + } + + private static HotReloadOneShotCallerNoteCandidate CreateCandidate( + string assemblyName, + HotReloadMethodOutcome outcome) + { + HotReloadCompiledMethodIdentity identity = + new HotReloadCompiledMethodIdentity( + assemblyName, + new HotReloadMetadataTypeName("Type"), + "SetUp", + Array.Empty(), + 0); + return new HotReloadOneShotCallerNoteCandidate(identity, outcome); + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs.meta new file mode 100644 index 000000000..51dbccba3 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4967734cb4bcf48a4b3ad6174a5ed173 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteBuilderTests.cs b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteBuilderTests.cs index 4d6b17987..eb8841ed2 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteBuilderTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteBuilderTests.cs @@ -62,7 +62,7 @@ public void IsOneShotLifecycleCaller_NestedCallerInMetadataForm_ReturnsTrue() [Test] public void IsOneShotLifecycleCaller_ParameterizedCallerWithMatchingName_ReturnsFalse() { - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(typeof(OverloadedLifecycleFixture), "Awake"); + HotReloadCallSiteHit hit = CreateHit(typeof(OverloadedLifecycleFixture), "Awake"); hit.CallerParameterTypeFullNames = new[] { "System.Int32" }; bool result = HotReloadOneShotCallerNoteEnricher.IsOneShotLifecycleCaller(hit); @@ -76,7 +76,7 @@ public void IsOneShotLifecycleCaller_ParameterizedCallerWithMatchingName_Returns [Test] public void IsOneShotLifecycleCaller_GenericCallerWithMatchingName_ReturnsFalse() { - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(typeof(OverloadedLifecycleFixture), "Awake"); + HotReloadCallSiteHit hit = CreateHit(typeof(OverloadedLifecycleFixture), "Awake"); hit.CallerGenericArity = 1; bool result = HotReloadOneShotCallerNoteEnricher.IsOneShotLifecycleCaller(hit); @@ -160,28 +160,24 @@ public void IsOneShotLifecycleCaller_MultipleGenericMethods_ReturnsFalseWithoutT } /// - /// What: an incomplete scan does not add caller-aware notes to its candidates. + /// What: an incomplete scan returns no caller-aware note for its requests. /// [Test] - public void ApplyNotes_MissingScanAssembly_SuppressesNotes() + public void BuildNotes_MissingScanAssembly_SuppressesNotes() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateCandidate("Assembly.One", outcome) + CreateRequest("Assembly.One", "Type.SetUp") }; - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List(), + new List(), new List { assemblyName })); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// @@ -189,59 +185,50 @@ public void ApplyNotes_MissingScanAssembly_SuppressesNotes() /// did find would have proven an Awake-only caller. /// [Test] - public void ApplyNotes_UnreadScanAssembly_SuppressesNotes() + public void BuildNotes_UnreadScanAssembly_SuppressesNotes() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateCandidate("Assembly.One", outcome) + CreateRequest("Assembly.One", "Type.SetUp") }; - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(typeof(ValidLifecycleFixture), "Awake"); + HotReloadCallSiteHit hit = CreateHit(typeof(ValidLifecycleFixture), "Awake"); hit.TargetMethodKey = HotReloadMethodKeys.BuildMethodKeyParts("Type", "SetUp", Array.Empty(), 0); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { hit }, + new List { hit }, new List(), 0, null, new List { assemblyName })); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// - /// What: candidates with separate target assemblies are scanned in separate fake calls. + /// What: requests with separate target assemblies are scanned in separate fake calls. /// [Test] - public void ApplyNotes_DifferentTargetAssemblies_ScansEachAssemblySeparately() + public void BuildNotes_DifferentTargetAssemblies_ScansEachAssemblySeparately() { - HotReloadMethodOutcome first = HotReloadMethodOutcome.Patched("Type.First", "Assets/First.cs"); - HotReloadMethodOutcome second = HotReloadMethodOutcome.Patched("Type.Second", "Assets/Second.cs"); - List outcomes = new List { first, second }; - List candidates = - new List + List requests = + new List { - CreateCandidate("Assembly.One", first), - CreateCandidate("Assembly.Two", second) + CreateRequest("Assembly.One", "Type.First"), + CreateRequest("Assembly.Two", "Type.Second") }; - List calls = - new List(); + List calls = + new List(); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => { calls.Add(identities); return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List(), + new List(), new List()); }); @@ -252,66 +239,27 @@ public void ApplyNotes_DifferentTargetAssemblies_ScansEachAssemblySeparately() } /// - /// What: a worker lifecycle note remains authoritative and skips the caller scan. + /// What: a proven Awake-only caller returns the full indirect note for the request. /// [Test] - public void ApplyNotes_WorkerLifecycleNoteCandidate_SkipsScanAndKeepsNote() + public void BuildNotes_OnlyValidAwakeCaller_ReturnsIndirectLifecycleNote() { - const string workerNote = "Worker lifecycle note."; - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched( - "Type.SetUp", - "Assets/Test.cs", - workerNote); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateCandidate("Assembly.One", outcome) + CreateRequest(typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name, "Type.SetUp") }; - int scanCount = 0; - - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, - (assemblyName, identities) => - { - scanCount++; - return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List(), - new List()); - }); - - Assert.That(scanCount, Is.EqualTo(0)); - Assert.That(outcomes[0].LifecycleNote, Is.EqualTo(workerNote)); - } - - /// - /// What: a proven Awake-only caller writes the full indirect note into the response outcome list. - /// - [Test] - public void ApplyNotes_OnlyValidAwakeCaller_ReplacesOutcomeWithIndirectLifecycleNote() - { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List - { - CreateCandidate(typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name, outcome) - }; - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(typeof(ValidLifecycleFixture), "Awake"); + HotReloadCallSiteHit hit = CreateHit(typeof(ValidLifecycleFixture), "Awake"); hit.TargetMethodKey = "Type::SetUp()"; - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { hit }, + new List { hit }, new List())); Assert.That( - outcomes[0].LifecycleNote, + notes[0], Is.EqualTo( "Type.SetUp is called only from one-shot lifecycle method(s) (Awake) in the compiled " + "assemblies; objects that already ran them will not run the patched body. It takes " @@ -324,53 +272,45 @@ public void ApplyNotes_OnlyValidAwakeCaller_ReplacesOutcomeWithIndirectLifecycle /// What: a function-pointer load suppresses the note before its Awake caller is classified. /// [Test] - public void ApplyNotes_FunctionPointerLoadHit_SuppressesIndirectLifecycleNote() + public void BuildNotes_FunctionPointerLoadHit_SuppressesIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateCandidate(typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name, outcome) + CreateRequest(typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name, "Type.SetUp") }; - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(typeof(ValidLifecycleFixture), "Awake"); + HotReloadCallSiteHit hit = CreateHit(typeof(ValidLifecycleFixture), "Awake"); hit.TargetMethodKey = "Type::SetUp()"; hit.IsFunctionPointerLoad = true; - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { hit }, + new List { hit }, new List())); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// - /// What: compiled Awake-only callers add an indirect lifecycle note to a patched outcome. + /// What: compiled Awake-only callers return an indirect lifecycle note for the patched method. /// [Test] - public void ApplyNotes_CompiledAwakeOnlyCaller_AddsIndirectLifecycleNote() + public void BuildNotes_CompiledAwakeOnlyCaller_AddsIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("OneShotCallerScannerFixture.AwakeOnlyTarget()", "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerFixtureCandidate("AwakeOnlyTarget", outcome) + CreateScannerFixtureRequest("AwakeOnlyTarget", "OneShotCallerScannerFixture.AwakeOnlyTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); Assert.That( - outcomes[0].LifecycleNote, + notes[0], Is.EqualTo( "OneShotCallerScannerFixture.AwakeOnlyTarget() is called only from one-shot lifecycle " + "method(s) (Awake) in the compiled assemblies; objects that already ran them will not run " @@ -384,51 +324,41 @@ public void ApplyNotes_CompiledAwakeOnlyCaller_AddsIndirectLifecycleNote() /// What: a compiled ordinary caller suppresses the indirect lifecycle note. /// [Test] - public void ApplyNotes_CompiledOrdinaryCaller_SuppressesIndirectLifecycleNote() + public void BuildNotes_CompiledOrdinaryCaller_SuppressesIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("OneShotCallerScannerFixture.MixedTarget()", "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerFixtureCandidate("MixedTarget", outcome) + CreateScannerFixtureRequest("MixedTarget", "OneShotCallerScannerFixture.MixedTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// /// What: a compiled two-step Awake chain still produces the indirect lifecycle note. /// [Test] - public void ApplyNotes_CompiledChainedAwakeOnlyCaller_AddsIndirectLifecycleNote() + public void BuildNotes_CompiledChainedAwakeOnlyCaller_AddsIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched( - "OneShotCallerScannerFixture.ChainedAwakeOnlyTarget()", - "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerFixtureCandidate("ChainedAwakeOnlyTarget", outcome) + CreateScannerFixtureRequest("ChainedAwakeOnlyTarget", "OneShotCallerScannerFixture.ChainedAwakeOnlyTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); Assert.That( - outcomes[0].LifecycleNote, + notes[0], Is.EqualTo( "OneShotCallerScannerFixture.ChainedAwakeOnlyTarget() is called only from one-shot " + "lifecycle method(s) (Awake) in the compiled assemblies; objects that already ran them " @@ -442,27 +372,21 @@ public void ApplyNotes_CompiledChainedAwakeOnlyCaller_AddsIndirectLifecycleNote( /// What: a compiled Awake chain through a non-MonoBehaviour instance still produces the note. /// [Test] - public void ApplyNotes_CompiledNonMonoBehaviourChain_AddsIndirectLifecycleNote() + public void BuildNotes_CompiledNonMonoBehaviourChain_AddsIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched( - "OneShotCallerChainHelper.ConfigureTarget()", - "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerCandidateForType(typeof(OneShotCallerChainHelper), "ConfigureTarget", outcome) + CreateScannerRequestForType(typeof(OneShotCallerChainHelper), "ConfigureTarget", "OneShotCallerChainHelper.ConfigureTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); Assert.That( - outcomes[0].LifecycleNote, + notes[0], Is.EqualTo( "OneShotCallerChainHelper.ConfigureTarget() is called only from one-shot lifecycle " + "method(s) (Awake) in the compiled assemblies; objects that already ran them will not run " @@ -476,119 +400,93 @@ public void ApplyNotes_CompiledNonMonoBehaviourChain_AddsIndirectLifecycleNote() /// What: a compiled chain that also has a non-lifecycle root suppresses the note. /// [Test] - public void ApplyNotes_CompiledMixedChain_SuppressesIndirectLifecycleNote() + public void BuildNotes_CompiledMixedChain_SuppressesIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched( - "OneShotCallerScannerFixture.MixedChainTarget()", - "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerFixtureCandidate("MixedChainTarget", outcome) + CreateScannerFixtureRequest("MixedChainTarget", "OneShotCallerScannerFixture.MixedChainTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// /// What: a compiled chain whose intermediate is loaded as a function pointer suppresses the note. /// [Test] - public void ApplyNotes_CompiledDelegateChain_SuppressesIndirectLifecycleNote() + public void BuildNotes_CompiledDelegateChain_SuppressesIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched( - "OneShotCallerScannerFixture.DelegateChainTarget()", - "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerFixtureCandidate("DelegateChainTarget", outcome) + CreateScannerFixtureRequest("DelegateChainTarget", "OneShotCallerScannerFixture.DelegateChainTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// /// What: a compiled chain that stops at an uncalled intermediate suppresses the note. /// [Test] - public void ApplyNotes_CompiledDeadEndChain_SuppressesIndirectLifecycleNote() + public void BuildNotes_CompiledDeadEndChain_SuppressesIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched( - "OneShotCallerScannerFixture.DeadEndTarget()", - "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerFixtureCandidate("DeadEndTarget", outcome) + CreateScannerFixtureRequest("DeadEndTarget", "OneShotCallerScannerFixture.DeadEndTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// /// What: a compiled chain deeper than MaxCallerDepth suppresses the note. /// [Test] - public void ApplyNotes_CompiledDeepChain_SuppressesIndirectLifecycleNote() + public void BuildNotes_CompiledDeepChain_SuppressesIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched( - "OneShotCallerScannerFixture.DeepTarget()", - "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerFixtureCandidate("DeepTarget", outcome) + CreateScannerFixtureRequest("DeepTarget", "OneShotCallerScannerFixture.DeepTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// /// What: a cycle between the target and an intermediate still keeps a proven Awake root. /// [Test] - public void ApplyNotes_CycleThroughIntermediate_AddsIndirectLifecycleNote() + public void BuildNotes_CycleThroughIntermediate_AddsIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); - List outcomes = new List { outcome }; string assemblyName = typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name; - List candidates = - new List + List requests = + new List { - CreateCandidate(assemblyName, outcome) + CreateRequest(assemblyName, "Type.SetUp") }; const string intermediateType = "Ns.Mid"; const string intermediateName = "Run"; @@ -602,42 +500,40 @@ public void ApplyNotes_CycleThroughIntermediate_AddsIndirectLifecycleNote() intermediateName, Array.Empty(), 0); - HotReloadCallSiteScanner.CallSiteHit midToTarget = CreateKeyedHit( + HotReloadCallSiteHit midToTarget = CreateKeyedHit( assemblyName, intermediateType, intermediateName, targetKey); - HotReloadCallSiteScanner.CallSiteHit awakeToMid = CreateKeyedHit( + HotReloadCallSiteHit awakeToMid = CreateKeyedHit( assemblyName, typeof(ValidLifecycleFixture).FullName, "Awake", intermediateKey); - HotReloadCallSiteScanner.CallSiteHit targetToMid = CreateKeyedHit( + HotReloadCallSiteHit targetToMid = CreateKeyedHit( assemblyName, "Type", "SetUp", intermediateKey); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (scannedAssembly, identities) => { if (identities.Length == 1 && identities[0].MethodName == intermediateName) { return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { awakeToMid, targetToMid }, + new List { awakeToMid, targetToMid }, new List()); } return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { midToTarget }, + new List { midToTarget }, new List()); }); Assert.That( - outcomes[0].LifecycleNote, + notes[0], Is.EqualTo( "Type.SetUp is called only from one-shot lifecycle method(s) (Awake) in the compiled " + "assemblies; objects that already ran them will not run the patched body. It takes " @@ -650,15 +546,13 @@ public void ApplyNotes_CycleThroughIntermediate_AddsIndirectLifecycleNote() /// What: a missing scan assembly at depth two suppresses the note. /// [Test] - public void ApplyNotes_MissingScanAssemblyAtDepthTwo_SuppressesNotes() + public void BuildNotes_MissingScanAssemblyAtDepthTwo_SuppressesNotes() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); - List outcomes = new List { outcome }; string assemblyName = typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name; - List candidates = - new List + List requests = + new List { - CreateCandidate(assemblyName, outcome) + CreateRequest(assemblyName, "Type.SetUp") }; const string intermediateType = "Ns.Mid"; const string intermediateName = "Run"; @@ -667,46 +561,42 @@ public void ApplyNotes_MissingScanAssemblyAtDepthTwo_SuppressesNotes() "SetUp", Array.Empty(), 0); - HotReloadCallSiteScanner.CallSiteHit midToTarget = CreateKeyedHit( + HotReloadCallSiteHit midToTarget = CreateKeyedHit( assemblyName, intermediateType, intermediateName, targetKey); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (scannedAssembly, identities) => { if (identities.Length == 1 && identities[0].MethodName == intermediateName) { return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List(), + new List(), new List { scannedAssembly }); } return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { midToTarget }, + new List { midToTarget }, new List()); }); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// /// What: identical caller keys in different assemblies stay distinct so a dead end is not dropped. /// [Test] - public void ApplyNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEachAssembly() + public void BuildNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEachAssembly() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); - List outcomes = new List { outcome }; const string targetAssembly = "TargetAsm"; - List candidates = - new List + List requests = + new List { - CreateCandidate(targetAssembly, outcome) + CreateRequest(targetAssembly, "Type.SetUp") }; const string helperType = "Ns.Helper"; const string helperMethod = "Build"; @@ -722,30 +612,28 @@ public void ApplyNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEa "SetUp", Array.Empty(), 0); - HotReloadCallSiteScanner.CallSiteHit assemblyXHit = CreateKeyedHit( + HotReloadCallSiteHit assemblyXHit = CreateKeyedHit( assemblyX, helperType, helperMethod, targetKey); - HotReloadCallSiteScanner.CallSiteHit assemblyYHit = CreateKeyedHit( + HotReloadCallSiteHit assemblyYHit = CreateKeyedHit( assemblyY, helperType, helperMethod, targetKey); - List calls = - new List(); + List calls = + new List(); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (scannedAssembly, identities) => { calls.Add(identities); if (scannedAssembly == assemblyX) { return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List + new List { CreateKeyedHit( typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name, @@ -759,16 +647,16 @@ public void ApplyNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEa if (scannedAssembly == assemblyY) { return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List(), + new List(), new List()); } return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { assemblyXHit, assemblyYHit }, + new List { assemblyXHit, assemblyYHit }, new List()); }); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); Assert.That(calls.Count, Is.EqualTo(3)); Assert.That(calls[1].Length, Is.EqualTo(1)); Assert.That(calls[2].Length, Is.EqualTo(1)); @@ -783,26 +671,20 @@ public void ApplyNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEa /// event-driven calls are not proven to be one-shot. /// [Test] - public void ApplyNotes_CompiledDelegateAssignment_SuppressesIndirectLifecycleNote() + public void BuildNotes_CompiledDelegateAssignment_SuppressesIndirectLifecycleNote() { - HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched( - "OneShotCallerScannerFixture.DelegateAssignedTarget()", - "Assets/Test.cs"); - List outcomes = new List { outcome }; - List candidates = - new List + List requests = + new List { - CreateScannerFixtureCandidate("DelegateAssignedTarget", outcome) + CreateScannerFixtureRequest("DelegateAssignedTarget", "OneShotCallerScannerFixture.DelegateAssignedTarget()") }; string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities)); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// @@ -947,14 +829,14 @@ public void Build_NoCallers_ReturnsNull() Assert.That(note, Is.Null); } - private static HotReloadCallSiteScanner.CallSiteHit CreateHit(Type type, string methodName) + private static HotReloadCallSiteHit CreateHit(Type type, string methodName) { return CreateHit(type.FullName, methodName); } - private static HotReloadCallSiteScanner.CallSiteHit CreateHit(string typeMetadataName, string methodName) + private static HotReloadCallSiteHit CreateHit(string typeMetadataName, string methodName) { - return new HotReloadCallSiteScanner.CallSiteHit + return new HotReloadCallSiteHit { CallerAssemblyName = typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name, CallerTypeMetadataName = new HotReloadMetadataTypeName(typeMetadataName), @@ -964,52 +846,52 @@ private static HotReloadCallSiteScanner.CallSiteHit CreateHit(string typeMetadat }; } - private static HotReloadOneShotCallerNoteEnricher.Candidate CreateCandidate( + private static HotReloadOneShotCallerNoteRequest CreateRequest( string assemblyName, - HotReloadMethodOutcome outcome) + string method) { - HotReloadCallSiteScanner.CompiledMethodIdentity identity = - new HotReloadCallSiteScanner.CompiledMethodIdentity( + HotReloadCompiledMethodIdentity identity = + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName("Type"), "SetUp", Array.Empty(), 0); - return new HotReloadOneShotCallerNoteEnricher.Candidate(identity, outcome); + return new HotReloadOneShotCallerNoteRequest(identity, method); } - private static HotReloadOneShotCallerNoteEnricher.Candidate CreateScannerFixtureCandidate( + private static HotReloadOneShotCallerNoteRequest CreateScannerFixtureRequest( string methodName, - HotReloadMethodOutcome outcome) + string method) { - return CreateScannerCandidateForType(typeof(OneShotCallerScannerFixture), methodName, outcome); + return CreateScannerRequestForType(typeof(OneShotCallerScannerFixture), methodName, method); } - private static HotReloadOneShotCallerNoteEnricher.Candidate CreateScannerCandidateForType( + private static HotReloadOneShotCallerNoteRequest CreateScannerRequestForType( Type type, string methodName, - HotReloadMethodOutcome outcome) + string method) { string rawAssemblyName = CompilationPipeline.GetAssemblyNameFromScriptPath( "Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerFixture.cs"); string assemblyName = Path.GetFileNameWithoutExtension(rawAssemblyName); - HotReloadCallSiteScanner.CompiledMethodIdentity identity = - new HotReloadCallSiteScanner.CompiledMethodIdentity( + HotReloadCompiledMethodIdentity identity = + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName(type.FullName), methodName, Array.Empty(), 0); - return new HotReloadOneShotCallerNoteEnricher.Candidate(identity, outcome); + return new HotReloadOneShotCallerNoteRequest(identity, method); } - private static HotReloadCallSiteScanner.CallSiteHit CreateKeyedHit( + private static HotReloadCallSiteHit CreateKeyedHit( string assemblyName, string typeMetadataName, string methodName, string targetMethodKey) { - return new HotReloadCallSiteScanner.CallSiteHit + return new HotReloadCallSiteHit { CallerAssemblyName = assemblyName, CallerTypeMetadataName = new HotReloadMetadataTypeName(typeMetadataName), diff --git a/Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs b/Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs index 4cd0c17fe..3c6c1987f 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadOrchestratorTests.cs @@ -6352,7 +6352,7 @@ public void FindSignatureChangeCoverageLosses_CoveredCallerRemains_ReturnsEmpty( { TransformWorkerEntryDto replacement = CreateReplacementEntry("Host", "Target"); TransformWorkerEntryDto caller = CreateOrdinaryEntry("Host", "Caller"); - List hits = new List + List hits = new List { CreateCallSiteHit("Host::Caller(System.Int32)", "Host::Target(System.Int32)") }; @@ -6374,7 +6374,7 @@ public void FindSignatureChangeCoverageLosses_CoveredCallerRemains_ReturnsEmpty( public void FindSignatureChangeCoverageLosses_CallerKeyDropped_ReturnsReplacementKey() { TransformWorkerEntryDto replacement = CreateReplacementEntry("Host", "Target"); - List hits = new List + List hits = new List { CreateCallSiteHit("Host::Caller(System.Int32)", "Host::Target(System.Int32)") }; @@ -8421,11 +8421,11 @@ private static TransformWorkerEntryDto CreateOrdinaryEntry(string typeName, stri }; } - private static HotReloadCallSiteScanner.CallSiteHit CreateCallSiteHit( + private static HotReloadCallSiteHit CreateCallSiteHit( string callerMethodKey, string targetMethodKey) { - return new HotReloadCallSiteScanner.CallSiteHit + return new HotReloadCallSiteHit { CallerAssemblyName = "TestAssembly", CallerMethodKey = callerMethodKey, diff --git a/Assets/Tests/Editor/HotReload/HotReloadPatcherContractTests.cs b/Assets/Tests/Editor/HotReload/HotReloadPatcherContractTests.cs index d4e165808..18dd89378 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadPatcherContractTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadPatcherContractTests.cs @@ -218,7 +218,7 @@ private static IDisposable BeginReplacementWith(IHotReloadHarmony harmony) new HotReloadApplicationPlayModeQuery(), new HotReloadSourceSnapshotCapture(() => true), HotReloadWarmUpTestDoubles.CreateInert(), - HotReloadCompositionRoot.CreateProductionCallSiteBackfill())); + HotReloadCompiledCallers.CreateProduction())); } private static MethodInfo GetPatchTarget() diff --git a/Assets/Tests/Editor/HotReload/HotReloadPatcherTests.cs b/Assets/Tests/Editor/HotReload/HotReloadPatcherTests.cs index 836596c1b..3f9b226e6 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadPatcherTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadPatcherTests.cs @@ -92,7 +92,7 @@ private static IDisposable BeginReplacementWith(IHotReloadHarmony harmony) new HotReloadApplicationPlayModeQuery(), new HotReloadSourceSnapshotCapture(() => true), HotReloadWarmUpTestDoubles.CreateInert(), - HotReloadCompositionRoot.CreateProductionCallSiteBackfill())); + HotReloadCompiledCallers.CreateProduction())); } /// diff --git a/Assets/Tests/Editor/HotReload/HotReloadReferencedMethodSetWarmUpItemTests.cs b/Assets/Tests/Editor/HotReload/HotReloadReferencedMethodSetWarmUpItemTests.cs index e0b14f9c3..508a0e83a 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadReferencedMethodSetWarmUpItemTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadReferencedMethodSetWarmUpItemTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Threading; @@ -44,18 +45,11 @@ public void TearDown() public void PreloadSets_ReadsEachReferencingDllOnce_AndAgainReadsNothing() { HotReloadReferencedMethodIndex index = new HotReloadReferencedMethodIndex(_persistenceDirectory); - HotReloadWarmUpTarget[] targets = - { - new HotReloadWarmUpTarget( - TestAssemblyName, - DllPath(TestAssemblyName), - Path.ChangeExtension(DllPath(TestAssemblyName), ".pdb"), - new[] { DllPath(CrossAssemblyCallerAssemblyName), DllPath(TestAssemblyName) }) - }; + string[] dllPaths = { DllPath(CrossAssemblyCallerAssemblyName), DllPath(TestAssemblyName) }; - HotReloadReferencedMethodSetWarmUpItem.PreloadSets(targets, index, CancellationToken.None); + HotReloadCompiledCallers.PreloadReferencedMethodSets(dllPaths, index, CancellationToken.None); int afterFirst = index.LoadCount; - HotReloadReferencedMethodSetWarmUpItem.PreloadSets(targets, index, CancellationToken.None); + HotReloadCompiledCallers.PreloadReferencedMethodSets(dllPaths, index, CancellationToken.None); Assert.That(afterFirst, Is.EqualTo(2), "reads after the first preload"); Assert.That(index.LoadCount, Is.EqualTo(2), "reads after the second preload"); @@ -68,19 +62,38 @@ public void PreloadSets_ReadsEachReferencingDllOnce_AndAgainReadsNothing() public void PreloadSets_WhenCancelled_ThrowsAndReadsNothing() { HotReloadReferencedMethodIndex index = new HotReloadReferencedMethodIndex(_persistenceDirectory); + string[] dllPaths = { DllPath(CrossAssemblyCallerAssemblyName) }; + + Assert.Throws( + () => HotReloadCompiledCallers.PreloadReferencedMethodSets(dllPaths, index, new CancellationToken(true))); + + Assert.That(index.LoadCount, Is.EqualTo(0), "reads"); + } + + /// + /// What: the item reads the referencing dlls of every target in target order and never a + /// target's own dll. + /// + [Test] + public void CollectDllPaths_ReturnsEachTargetsReferencingDllsInOrder_AndNotItsOwnDll() + { HotReloadWarmUpTarget[] targets = { new HotReloadWarmUpTarget( - TestAssemblyName, - DllPath(TestAssemblyName), - Path.ChangeExtension(DllPath(TestAssemblyName), ".pdb"), - new[] { DllPath(CrossAssemblyCallerAssemblyName) }) + "A", + DllPath("A"), + Path.ChangeExtension(DllPath("A"), ".pdb"), + new[] { DllPath("B"), DllPath("C") }), + new HotReloadWarmUpTarget( + "D", + DllPath("D"), + Path.ChangeExtension(DllPath("D"), ".pdb"), + new[] { DllPath("E") }) }; - Assert.Throws( - () => HotReloadReferencedMethodSetWarmUpItem.PreloadSets(targets, index, new CancellationToken(true))); + List dllPaths = HotReloadReferencedMethodSetWarmUpItem.CollectDllPaths(targets); - Assert.That(index.LoadCount, Is.EqualTo(0), "reads"); + Assert.That(dllPaths, Is.EqualTo(new[] { DllPath("B"), DllPath("C"), DllPath("E") })); } private static string DllPath(string assemblyName) diff --git a/Assets/Tests/Editor/HotReload/HotReloadRunStaleSignatureWarningsTests.cs b/Assets/Tests/Editor/HotReload/HotReloadRunStaleSignatureWarningsTests.cs index ae106ab65..cb5c0af0d 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadRunStaleSignatureWarningsTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadRunStaleSignatureWarningsTests.cs @@ -158,8 +158,8 @@ public void AppendTo_SameLabelActiveInOtherAssembly_KeepsCaller() [TestCase(false)] public void AppendTo_SameWireKeyInTwoAssemblies_OneActive_ListsItOnce(bool activeCallerRecordedFirst) { - HotReloadCallSiteScanner.CallSiteHit activeCaller = CreateHit(ExternalAssemblyName, CallerType, "Call"); - HotReloadCallSiteScanner.CallSiteHit compiledCaller = CreateHit(EditedAssemblyName, CallerType, "Call"); + HotReloadCallSiteHit activeCaller = CreateHit(ExternalAssemblyName, CallerType, "Call"); + HotReloadCallSiteHit compiledCaller = CreateHit(EditedAssemblyName, CallerType, "Call"); HotReloadRunStaleSignatureWarnings staleWarnings = activeCallerRecordedFirst ? Record(RemovedKey, activeCaller, compiledCaller) : Record(RemovedKey, compiledCaller, activeCaller); @@ -245,7 +245,7 @@ public void AppendTo_NestedTypeCallerActive_IsOmitted() [Test] public void AppendTo_FunctionPointerLoadOfActiveCaller_IsOmitted() { - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(ExternalAssemblyName, CallerType, "Call"); + HotReloadCallSiteHit hit = CreateHit(ExternalAssemblyName, CallerType, "Call"); hit.IsFunctionPointerLoad = true; HotReloadRunStaleSignatureWarnings staleWarnings = Record(RemovedKey, hit); List warnings = new List(); @@ -300,7 +300,7 @@ public void AppendTo_TwoSignatures_FiltersEachIndependently() { new HotReloadStaleSignatureCallSites( OtherRemovedKey, - new List + new List { CreateHit(ExternalAssemblyName, OtherCallerType, "Call") }) @@ -319,7 +319,7 @@ public void AppendTo_TwoSignatures_FiltersEachIndependently() private static HotReloadRunStaleSignatureWarnings Record( string removedKey, - params HotReloadCallSiteScanner.CallSiteHit[] callers) + params HotReloadCallSiteHit[] callers) { HotReloadRunStaleSignatureWarnings staleWarnings = new HotReloadRunStaleSignatureWarnings(); staleWarnings.AddRange( @@ -327,17 +327,17 @@ private static HotReloadRunStaleSignatureWarnings Record( { new HotReloadStaleSignatureCallSites( removedKey, - new List(callers)) + new List(callers)) }); return staleWarnings; } - private static HotReloadCallSiteScanner.CallSiteHit CreateHit( + private static HotReloadCallSiteHit CreateHit( string assemblyName, string typeMetadataName, string methodName) { - return new HotReloadCallSiteScanner.CallSiteHit + return new HotReloadCallSiteHit { CallerAssemblyName = assemblyName, CallerTypeMetadataName = new HotReloadMetadataTypeName(typeMetadataName), diff --git a/Assets/Tests/Editor/HotReload/HotReloadServicesTestScope.cs b/Assets/Tests/Editor/HotReload/HotReloadServicesTestScope.cs index 15ff22efd..29e0b3a1e 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadServicesTestScope.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadServicesTestScope.cs @@ -106,7 +106,7 @@ internal static IDisposable BeginWith( // Why stop the installed warm-up: the scope's run yields to that same warm-up, but a // test that counts cache reads must not race one already running on a pool thread. _ = installed.WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope); - _ = installed.CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope); + _ = installed.CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerTestScope); return HotReloadCompositionRoot.BeginReplacement( HotReloadCompositionRoot.CreateServices( installed.Domain, @@ -118,7 +118,7 @@ internal static IDisposable BeginWith( playModeQuery, installed.SourceSnapshotCapture, installed.WarmUp, - installed.CallSiteBackfill)); + installed.CompiledCallers)); } } diff --git a/Assets/Tests/Editor/HotReload/HotReloadSignatureChangeCoverageTests.cs b/Assets/Tests/Editor/HotReload/HotReloadSignatureChangeCoverageTests.cs index 5bb0bb37e..19ab7eacf 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadSignatureChangeCoverageTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadSignatureChangeCoverageTests.cs @@ -24,7 +24,7 @@ public class HotReloadSignatureChangeCoverageTests public void CollectUncoveredCallersByTarget_ExternalSameKeyCaller_StaysUncovered() { TransformWorkerEntryDto localCaller = CreateOrdinaryEntry(); - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(ExternalAssemblyName); + HotReloadCallSiteHit hit = CreateHit(ExternalAssemblyName); Dictionary> uncovered = HotReloadSignatureChangeGate.CollectInitialUncoveredCallers( @@ -46,7 +46,7 @@ public void CollectUncoveredCallersByTarget_ExternalSameKeyCaller_StaysUncovered public void CollectInitialUncoveredCallers_SameAssemblySameKeyCaller_IsCovered() { TransformWorkerEntryDto localCaller = CreateOrdinaryEntry(); - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(EditedAssemblyName); + HotReloadCallSiteHit hit = CreateHit(EditedAssemblyName); Dictionary> uncovered = HotReloadSignatureChangeGate.CollectInitialUncoveredCallers( @@ -156,7 +156,7 @@ public void FindSignatureChangeCoverageLosses_ExternalSameKeyCallerRemains_Retur { TransformWorkerEntryDto replacement = CreateReplacementEntry(); TransformWorkerEntryDto localCaller = CreateOrdinaryEntry(); - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(ExternalAssemblyName); + HotReloadCallSiteHit hit = CreateHit(ExternalAssemblyName); List losses = HotReloadSignatureChangeCoverage.FindSignatureChangeCoverageLosses( EditedAssemblyName, @@ -343,7 +343,7 @@ public void AppendSignatureChangeCallersRepatchedWarnings_ExternalSameKeyCaller_ { TransformWorkerEntryDto replacement = CreateReplacementEntry(); TransformWorkerEntryDto localCaller = CreateOrdinaryEntry(); - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(ExternalAssemblyName); + HotReloadCallSiteHit hit = CreateHit(ExternalAssemblyName); List warnings = new List(); HashSet snapshotLabels = new HashSet(StringComparer.Ordinal) { @@ -372,11 +372,11 @@ public void AppendSignatureChangeCallersRepatchedWarnings_ExternalSameKeyCaller_ [Test] public void CollectStaleSignatureCallSites_CrossAssemblySameKey_KeepsFirstHitPerIdentity() { - HotReloadCallSiteScanner.CallSiteHit editedHit = CreateHit(EditedAssemblyName); - HotReloadCallSiteScanner.CallSiteHit repeatedEditedHit = CreateHit(EditedAssemblyName); - HotReloadCallSiteScanner.CallSiteHit externalHit = CreateHit(ExternalAssemblyName); - List hits = - new List { editedHit, repeatedEditedHit, externalHit }; + HotReloadCallSiteHit editedHit = CreateHit(EditedAssemblyName); + HotReloadCallSiteHit repeatedEditedHit = CreateHit(EditedAssemblyName); + HotReloadCallSiteHit externalHit = CreateHit(ExternalAssemblyName); + List hits = + new List { editedHit, repeatedEditedHit, externalHit }; Dictionary> callersByTarget = HotReloadSignatureChangeCoverage.CollectUncoveredCallersByTarget( hits, @@ -452,9 +452,9 @@ private static TransformWorkerEntryDto CreateOrdinaryEntry() }; } - private static HotReloadCallSiteScanner.CallSiteHit CreateHit(string callerAssemblyName) + private static HotReloadCallSiteHit CreateHit(string callerAssemblyName) { - return new HotReloadCallSiteScanner.CallSiteHit + return new HotReloadCallSiteHit { CallerAssemblyName = callerAssemblyName, CallerTypeMetadataName = new HotReloadMetadataTypeName("Example.Caller"), diff --git a/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs b/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs index 6b6807f47..0a00841a1 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs @@ -62,7 +62,7 @@ public async Task Run_AfterTheWarmUpFinished_ReadsNoCompiledAssemblyForTheEdited string testDll = TestAssemblyDllPath(); HotReloadWarmUp warmUp = new HotReloadWarmUp( new FixedContextSource(CreateTestAssemblyCapture(testDll)), - HotReloadWarmUpItems.CreateProduction()); + HotReloadWarmUpItems.CreateProduction(HotReloadCompiledCallers.CreateProduction())); await BeginScope(warmUp); // Why clear again once the installed warm-up stopped: its unit in flight can log a copy // written and its completion after the clear in SetUp. @@ -153,6 +153,7 @@ public async Task Run_WhileAnItemIsInFlight_WaitsForItAndStartsOnlyAfterward() await BeginScope(warmUp); VibeLogger.ClearMemoryLogs(); Task run; + int fileStartsBeforeRelease; try { warmUp.Start(); @@ -160,15 +161,18 @@ public async Task Run_WhileAnItemIsInFlight_WaitsForItAndStartsOnlyAfterward() // Why this shows the wait: without it the run passes its first main-thread switch // synchronously here and writes its file start before the task comes back. - Assert.That(ReadEntries(HotReloadConstants.VibeLogFileStart), Is.Empty, "file start before the item finished"); + fileStartsBeforeRelease = ReadEntries(HotReloadConstants.VibeLogFileStart).Count; } finally { pending.Release.TrySetResult(true); } + // Assert only after the run finishes: an assertion that throws first leaves the run + // going into the next test, which then counts that run's log entries. await run; + Assert.That(fileStartsBeforeRelease, Is.EqualTo(0), "file start before the item finished"); Assert.That(ReadEntries(HotReloadConstants.VibeLogFileStart), Is.Not.Empty, "file start after the item finished"); JArray timingDetails = ReadEntries(HotReloadConstants.VibeLogTimingDetail); Assert.That(timingDetails.Count, Is.EqualTo(1), "timing details"); @@ -181,6 +185,41 @@ public async Task Run_WhileAnItemIsInFlight_WaitsForItAndStartsOnlyAfterward() Assert.That(steps, Does.Contain(HotReloadConstants.TimingDetailStepWarmUpYield)); } + /// + /// What: a run that comes while a warm-up item is in flight takes the call-site cache hold + /// only after it yielded to the warm-up, so the hold is not open while the item runs. + /// + [Test] + public async Task Run_WhileAnItemIsInFlight_TakesTheCallSiteCacheHoldOnlyAfterTheItemFinishes() + { + PendingWarmUpItem pending = new PendingWarmUpItem("a", _ran); + HotReloadWarmUp warmUp = CreateWarmUp(pending); + await BeginScope(warmUp); + int holdDepthBefore = HotReloadCompiledCallSiteCache.Shared.HoldDepth; + Task run; + bool waitingForItem; + int holdDepthWhileItemRuns; + try + { + warmUp.Start(); + run = RunWithReadReturningAsync(6); + + waitingForItem = !run.IsCompleted; + holdDepthWhileItemRuns = HotReloadCompiledCallSiteCache.Shared.HoldDepth; + } + finally + { + pending.Release.TrySetResult(true); + } + + // Assert only after the run finishes: an assertion that throws first leaves the run + // going into the next test, which then counts that run's log entries. + await run; + + Assert.That(waitingForItem, Is.True, "Precondition: the run must be waiting for the item."); + Assert.That(holdDepthWhileItemRuns, Is.EqualTo(holdDepthBefore), "cache hold taken while the item is in flight"); + } + /// /// What: a run puts the edited assembly first in the project's ledger and keeps the names /// recorded before it. diff --git a/Assets/Tests/Editor/HotReload/HotReloadWarmUpItemsTests.cs b/Assets/Tests/Editor/HotReload/HotReloadWarmUpItemsTests.cs index df2ca77bd..f37a279a4 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadWarmUpItemsTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadWarmUpItemsTests.cs @@ -20,7 +20,7 @@ public sealed class HotReloadWarmUpItemsTests public void CreateProduction_ReturnsPublicizedTargetsThenCallSitesThenReferencedMethodSetsThenTransformWorkerThenPdbDocuments() { List names = new List(); - foreach (IHotReloadWarmUpItem item in HotReloadWarmUpItems.CreateProduction()) + foreach (IHotReloadWarmUpItem item in HotReloadWarmUpItems.CreateProduction(HotReloadCompiledCallers.CreateProduction())) { names.Add(item.Name); } diff --git a/Assets/Tests/Editor/HotReload/UnityCLILoop.Tests.Editor.HotReload.asmdef b/Assets/Tests/Editor/HotReload/UnityCLILoop.Tests.Editor.HotReload.asmdef index 8d6e83246..8fc81a1e4 100644 --- a/Assets/Tests/Editor/HotReload/UnityCLILoop.Tests.Editor.HotReload.asmdef +++ b/Assets/Tests/Editor/HotReload/UnityCLILoop.Tests.Editor.HotReload.asmdef @@ -16,7 +16,8 @@ "GUID:384dc39e13234d8aae3b1b1c44c9507e", "GUID:1e3293e9639242278dcb27724b34beb4", "GUID:5390a0826a36841e7a977485f863d05f", - "GUID:24d448abf7c10486d9578018b3a3c54b" + "GUID:24d448abf7c10486d9578018b3a3c54b", + "GUID:b6ba53c1f946045358f72c7981f7b425" ], "includePlatforms": [ "Editor" diff --git a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs index ee1a792cc..f43d5dc7b 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.Shared.Editor")] +[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor")] [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.Editor")] [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.Patching.Editor")] [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.PausePoint.Editor")] diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers.meta new file mode 100644 index 000000000..3d407b453 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: da68a32195687424d8a0795ef42575ca +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/AssemblyInfo.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/AssemblyInfo.cs new file mode 100644 index 000000000..c41f708fb --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +// Only the tests see these internals: the hot-reload application assembly uses this module +// through its entry point alone, and the compiler keeps it away from the parts behind it. +[assembly: InternalsVisibleTo("UnityCLILoop.Tests.Editor.HotReload")] diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/AssemblyInfo.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/AssemblyInfo.cs.meta new file mode 100644 index 000000000..4e286f3dd --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63cf750119fad4af2aac29cf7eacf8c5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteBackfill.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteBackfill.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteBackfill.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteBackfill.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteBackfill.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs new file mode 100644 index 000000000..f961dceb0 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs @@ -0,0 +1,18 @@ +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// One compiled instruction that references a target method, reported under its logical owner. + /// + public sealed class HotReloadCallSiteHit + { + public string CallerAssemblyName; + public HotReloadMetadataTypeName CallerTypeMetadataName; + public string CallerMethodName; + public string[] CallerParameterTypeFullNames; + // Hot-reload code outside the analysis does not read these two. + internal int CallerGenericArity; + public string CallerMethodKey; + public string TargetMethodKey; + internal bool IsFunctionPointerLoad; + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs.meta new file mode 100644 index 000000000..8595dd23d --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 24198ccc010c942bf82ca41a60e1aed2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteLoadBudget.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteLoadBudget.cs similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteLoadBudget.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteLoadBudget.cs diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteLoadBudget.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteLoadBudget.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteLoadBudget.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteLoadBudget.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteScanner.cs similarity index 87% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteScanner.cs index 111287f1d..62fa7544e 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteScanner.cs @@ -21,7 +21,7 @@ internal static class HotReloadCallSiteScanner /// internal sealed class HotReloadCallSiteScanResult { - public List Hits; + public List Hits; public List MissingScanAssemblyNames; /// @@ -47,7 +47,7 @@ internal sealed class HotReloadCallSiteScanResult public bool IsIncomplete => MissingScanAssemblyNames.Count > 0 || UnreadScanAssemblyNames.Count > 0; public HotReloadCallSiteScanResult( - List hits, + List hits, List missingScanAssemblyNames, int examinedCallSiteCount = 0, List skippedScanAssemblyNames = null, @@ -61,52 +61,6 @@ public HotReloadCallSiteScanResult( } } - /// - /// Identity of a compiled method to search for (assembly + type + name + arity + parameter types). - /// - public readonly struct CompiledMethodIdentity - { - public readonly string AssemblyName; - public readonly HotReloadMetadataTypeName TypeMetadataName; - public readonly string MethodName; - public readonly string[] ParameterTypeFullNames; - public readonly int GenericArity; - - public CompiledMethodIdentity( - string assemblyName, - HotReloadMetadataTypeName typeMetadataName, - string methodName, - string[] parameterTypeFullNames, - int genericArity) - { - Debug.Assert(!string.IsNullOrEmpty(assemblyName), "assemblyName must not be null or empty."); - Debug.Assert(!string.IsNullOrEmpty(methodName), "methodName must not be null or empty."); - Debug.Assert(parameterTypeFullNames != null, "parameterTypeFullNames must not be null."); - Debug.Assert(genericArity >= 0, "genericArity must not be negative."); - - AssemblyName = assemblyName; - TypeMetadataName = typeMetadataName; - MethodName = methodName; - ParameterTypeFullNames = parameterTypeFullNames; - GenericArity = genericArity; - } - } - - /// - /// One compiled instruction that references a target method, reported under its logical owner. - /// - public sealed class CallSiteHit - { - public string CallerAssemblyName; - public HotReloadMetadataTypeName CallerTypeMetadataName; - public string CallerMethodName; - public string[] CallerParameterTypeFullNames; - public int CallerGenericArity; - public string CallerMethodKey; - public string TargetMethodKey; - public bool IsFunctionPointerLoad; - } - /// /// Finds compiled call / ldftn sites that reference any of . /// With a , assemblies that are not in the call-site cache are @@ -115,13 +69,13 @@ public sealed class CallSiteHit /// public static HotReloadCallSiteScanResult FindCallSites( string projectRoot, - CompiledMethodIdentity[] targets, + HotReloadCompiledMethodIdentity[] targets, HotReloadCallSiteLoadBudget loadBudget = null) { Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty."); Debug.Assert(targets != null, "targets must not be null."); - List hits = new List(); + List hits = new List(); List missingScanAssemblyNames = new List(); if (targets.Length == 0) { @@ -180,10 +134,10 @@ public static HotReloadCallSiteScanResult FindCallSites( unreadScanAssemblyNames); } - private static HashSet CollectTargetAssemblyNames(CompiledMethodIdentity[] targets) + private static HashSet CollectTargetAssemblyNames(HotReloadCompiledMethodIdentity[] targets) { HashSet targetAssemblyNames = new HashSet(StringComparer.Ordinal); - foreach (CompiledMethodIdentity target in targets) + foreach (HotReloadCompiledMethodIdentity target in targets) { targetAssemblyNames.Add(target.AssemblyName); } @@ -193,10 +147,10 @@ private static HashSet CollectTargetAssemblyNames(CompiledMethodIdentity // Keys the referenced-method index files a MemberRef row under. The type name is the // metadata name (nested types joined with '/'), the spelling Cecil's FullName uses. - private static HashSet CollectTargetKeys(CompiledMethodIdentity[] targets) + private static HashSet CollectTargetKeys(HotReloadCompiledMethodIdentity[] targets) { HashSet targetKeys = new HashSet(StringComparer.Ordinal); - foreach (CompiledMethodIdentity target in targets) + foreach (HotReloadCompiledMethodIdentity target in targets) { targetKeys.Add(HotReloadReferencedMethodIndex.BuildKey( target.AssemblyName, @@ -208,11 +162,11 @@ private static HashSet CollectTargetKeys(CompiledMethodIdentity[] target } private static HashSet CollectScanAssemblyNames( - CompiledMethodIdentity[] targets, + HotReloadCompiledMethodIdentity[] targets, HashSet targetAssemblyNames) { HashSet targetDllFileNames = new HashSet(StringComparer.Ordinal); - foreach (CompiledMethodIdentity target in targets) + foreach (HotReloadCompiledMethodIdentity target in targets) { targetDllFileNames.Add(target.AssemblyName + HotReloadConstants.CompiledAssemblyExtension); } @@ -345,9 +299,9 @@ private static bool ReferencesAnyTargetDll( private static bool TryCollectHitsFromAssembly( string assemblyName, string dllPath, - CompiledMethodIdentity[] targets, + HotReloadCompiledMethodIdentity[] targets, HotReloadCallSiteLoadBudget loadBudget, - List hits, + List hits, out int examinedCallSiteCount) { // Why cache: the dll only changes on a compile, which also reloads the domain, so @@ -381,13 +335,13 @@ private static bool TryCollectHitsFromAssembly( // full identity match still decides each of them. private static List CollectCandidatePositions( HotReloadCompiledCallSiteCache.Entry compiled, - CompiledMethodIdentity[] targets) + HotReloadCompiledMethodIdentity[] targets) { // Why skip a repeated key: targets that share a type and a name (overloads, arities) // share one bucket, and visiting it twice would report its call sites twice. HashSet<(string TypeName, string MethodName)> visitedKeys = new HashSet<(string TypeName, string MethodName)>(); List positions = new List(); - foreach (CompiledMethodIdentity target in targets) + foreach (HotReloadCompiledMethodIdentity target in targets) { string typeName = target.TypeMetadataName.Value; if (!visitedKeys.Add((typeName, target.MethodName))) @@ -408,10 +362,10 @@ private static void CollectHitFromCallSite( string assemblyName, HotReloadCompiledCallSiteCache.Entry compiled, HotReloadCompiledCallSiteCache.CompiledCallSite callSite, - CompiledMethodIdentity[] targets, - List hits) + HotReloadCompiledMethodIdentity[] targets, + List hits) { - (bool matched, CompiledMethodIdentity target) = FindMatchingTarget( + (bool matched, HotReloadCompiledMethodIdentity target) = FindMatchingTarget( callSite.Operand, assemblyName, compiled.Module, @@ -439,13 +393,13 @@ private static void CollectHitFromCallSite( callSite.IsFunctionPointerLoad)); } - private static (bool matched, CompiledMethodIdentity target) FindMatchingTarget( + private static (bool matched, HotReloadCompiledMethodIdentity target) FindMatchingTarget( MethodReference methodReference, string scannedAssemblyName, ModuleDefinition scannedModule, - CompiledMethodIdentity[] targets) + HotReloadCompiledMethodIdentity[] targets) { - foreach (CompiledMethodIdentity target in targets) + foreach (HotReloadCompiledMethodIdentity target in targets) { if (MatchesIdentity( methodReference, @@ -464,7 +418,7 @@ private static bool IsSelfCall( string callerAssemblyName, ModuleDefinition callerModule, MethodDefinition caller, - CompiledMethodIdentity target) + HotReloadCompiledMethodIdentity target) { if (callerAssemblyName != target.AssemblyName) { @@ -476,7 +430,7 @@ private static bool IsSelfCall( private static bool MatchesIdentity( MethodReference methodReference, - CompiledMethodIdentity target, + HotReloadCompiledMethodIdentity target, string scannedAssemblyName, ModuleDefinition scannedModule) { @@ -615,10 +569,10 @@ private static bool IsCompilerGeneratedType(TypeDefinition type) return false; } - private static CallSiteHit CreateHit( + private static HotReloadCallSiteHit CreateHit( string assemblyName, MethodDefinition caller, - CompiledMethodIdentity target, + HotReloadCompiledMethodIdentity target, bool isFunctionPointerLoad) { string[] parameterTypeFullNames = new string[caller.Parameters.Count]; @@ -629,7 +583,7 @@ private static CallSiteHit CreateHit( HotReloadMetadataTypeName typeMetadataName = new HotReloadMetadataTypeName(caller.DeclaringType.FullName); - return new CallSiteHit + return new HotReloadCallSiteHit { CallerAssemblyName = assemblyName, CallerTypeMetadataName = typeMetadataName, diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteScanner.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteScanner.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteCache.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteCache.cs similarity index 98% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteCache.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteCache.cs index 4e0db98dc..df029e75d 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteCache.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteCache.cs @@ -254,6 +254,20 @@ public long LastHoldReleaseEvictedBytes } } + /// + /// Number of holds now open; how a test sees whether a run holds the entries. + /// + public int HoldDepth + { + get + { + lock (_gate) + { + return _holdDepth; + } + } + } + /// /// Returns the cached view of while the file's length, last /// write time, and module version id (MVID) all match the cached entry; otherwise reads the diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteCache.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteCache.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteCache.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteCache.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteIndex.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteIndex.cs similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteIndex.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteIndex.cs diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteIndex.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteIndex.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteIndex.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteIndex.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs new file mode 100644 index 000000000..3649ac063 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs @@ -0,0 +1,141 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// The compiled-caller analysis of hot reload: finds the compiled methods that call a method, + /// tells whether those callers are one-shot lifecycle messages, and keeps the compiled + /// assemblies it read cached between runs. What a run could not read is read in the background + /// after it. Begin every run with . + /// + public sealed class HotReloadCompiledCallers + { + private readonly HotReloadCompiledCallSiteCache _cache; + private readonly HotReloadCallSiteBackfill _backfill; + + /// The cache a run holds and the call-site warm-up fills; production passes the shared cache the caller scan reads. + /// The background reader of the dlls a run's notes could not read. + internal HotReloadCompiledCallers(HotReloadCompiledCallSiteCache cache, HotReloadCallSiteBackfill backfill) + { + Debug.Assert(cache != null, "cache must not be null."); + Debug.Assert(backfill != null, "backfill must not be null."); + _cache = cache; + _backfill = backfill; + } + + /// The background reader, for tests that await or inspect it. + internal HotReloadCallSiteBackfill Backfill => _backfill; + + /// Builds the instance production runs on. Call on the Unity main thread. + public static HotReloadCompiledCallers CreateProduction() + { + // Why the cache is taken here: this runs on the main thread while the services are built, + // and no pool thread may be the first to touch a Shared instance. + HotReloadCompiledCallSiteCache cache = HotReloadCompiledCallSiteCache.Shared; + HotReloadCallSiteBackfill backfill = new HotReloadCallSiteBackfill( + (dllPath, ct) => Task.Run(() => cache.GetOrLoad(dllPath), ct)); + 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) + { + return HotReloadCallSiteScanner.CollectReferencingDllPaths(projectRoot, assemblyName); + } + + /// + /// Reads the call sites of into the cache on a pool thread; throws + /// before the next dll once is cancelled. + /// + public Task WarmUpCallSitesAsync(IReadOnlyList dllPaths, CancellationToken ct) + { + HotReloadCompiledCallSiteCache cache = _cache; + return Task.Run(() => LoadCallSites(dllPaths, cache, ct)); + } + + /// Loads each dll and returns how many it loaded; throws before the next dll once cancelled. + internal static int LoadCallSites( + IReadOnlyList dllPaths, + HotReloadCompiledCallSiteCache cache, + CancellationToken ct) + { + int count = 0; + foreach (string dllPath in dllPaths) + { + // Why throw rather than stop: an item that returns normally is reported done, and + // one that skipped targets has not loaded them. + ct.ThrowIfCancellationRequested(); + cache.GetOrLoad(dllPath); + count++; + } + + return count; + } + + /// + /// Reads the referenced-method set of each of on a pool thread; + /// throws before the next dll once is cancelled. + /// + public Task WarmUpReferencedMethodSetsAsync(IReadOnlyList dllPaths, CancellationToken ct) + { + // Why taken here: the Shared instance reads Application.dataPath when it is first + // touched, which only the main thread may do. + HotReloadReferencedMethodIndex index = HotReloadReferencedMethodIndex.Shared; + return Task.Run(() => PreloadReferencedMethodSets(dllPaths, index, ct)); + } + + /// Preloads each dll's set; throws before the next dll once cancelled. + internal static void PreloadReferencedMethodSets( + IReadOnlyList dllPaths, + HotReloadReferencedMethodIndex index, + CancellationToken ct) + { + foreach (string dllPath in dllPaths) + { + // Why throw rather than stop: an item that returns normally is reported done. + ct.ThrowIfCancellationRequested(); + index.Preload(dllPath); + } + } + + /// + /// Stops the background reads for ; the task completes once the + /// dll in flight has been read, and never faults. + /// + public Task StopBackgroundReadsAsync(string trigger) + { + return _backfill.Shutdown(trigger); + } + + /// + /// Begins a run: waits for a background read in flight to end, then keeps every cached + /// compiled assembly cached until the returned run ends. Never faults. + /// + public async Task BeginRunAsync() + { + // Why the background reads stop before the hold: a read holds the cache lock for the + // whole read, so a hold taken first would wait for it unmeasured; waiting here is measured + // with the run's other yields, and the run reads nothing the background read is reading. + await _backfill.YieldToRunAsync().ConfigureAwait(false); + // 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); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs.meta new file mode 100644 index 000000000..c0ce63bbc --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3a655c25197384934b3c936405257a32 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs new file mode 100644 index 000000000..3bee28e34 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// One run of the compiled-caller analysis: the cached compiled assemblies stay cached until it + /// ends. End it with once the run finished normally; dispose it on every exit. + /// + public sealed class HotReloadCompiledCallersRun : IDisposable + { + private readonly IDisposable _hold; + private readonly HotReloadCallSiteBackfill _backfill; + private IReadOnlyList _refusedDllPaths = Array.Empty(); + private bool _notesBuilt; + private bool _ended; + + internal HotReloadCompiledCallersRun(IDisposable hold, HotReloadCallSiteBackfill backfill) + { + Debug.Assert(hold != null, "hold must not be null."); + Debug.Assert(backfill != null, "backfill must not be null."); + _hold = hold; + _backfill = backfill; + } + + /// + /// 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 + /// assemblies that are not cached yet; the ones it could not read are read in the + /// background after . Call once per run, on the Unity main thread (the + /// scan resolves compiled assemblies through Editor APIs). + /// + public IReadOnlyList BuildOneShotCallerNotes( + string projectRoot, + IReadOnlyList requests, + string correlationId) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty."); + Debug.Assert(requests != null, "requests must not be null."); + 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; + + // 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 + // however many candidate assemblies and levels it visits. + HotReloadCallSiteLoadBudget budget = + new HotReloadCallSiteLoadBudget(HotReloadConstants.CallerNoteUncachedDllLoadBudget); + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, + (ignoredAssemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities, budget)); + if (budget.RefusedAssemblyNames.Count > 0) + { + LogLoadBudgetExhausted( + HotReloadConstants.CallerNoteUncachedDllLoadBudget, + budget.RefusedAssemblyNames, + correlationId); + } + + _refusedDllPaths = budget.RefusedDllPaths; + return notes; + } + + /// + /// Ends the run after it finished normally: stops holding the cached assemblies, then starts + /// reading in the background the ones the notes could not read. Call once, after the run's + /// result is built. + /// + public void End(string correlationId) + { + Debug.Assert(!string.IsNullOrEmpty(correlationId), "correlationId must not be null or empty."); + Debug.Assert(!_ended, "End is called once per run."); + if (_ended) + { + return; + } + + _ended = true; + // 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); + } + + /// + /// Stops holding the cached assemblies without starting background reads, which is what a + /// run that ends with an exception gets. Safe after : the hold is released once. + /// + public void Dispose() + { + _hold.Dispose(); + } + + /// Sets the dlls reads, for tests that cannot exhaust a real budget. + internal void RememberRefusedDllPathsForTesting(IReadOnlyList dllPaths) + { + Debug.Assert(dllPaths != null, "dllPaths must not be null."); + _refusedDllPaths = dllPaths; + } + + private static void LogLoadBudgetExhausted( + int budgetLoads, + IReadOnlyList refusedAssemblyNames, + string correlationId) + { + VibeLogger.LogInfo( + HotReloadConstants.VibeLogCallerNoteLoadBudgetExhausted, + "Caller-note resolution hit its uncached dll load budget; the note was omitted.", + new + { + budgetLoads, + refusedAssemblies = refusedAssemblyNames.ToArray() + }, + correlationId); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs.meta new file mode 100644 index 000000000..48515fbff --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: faafb6fbf30714982a1092a551e9d850 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs new file mode 100644 index 000000000..068ae56f2 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs @@ -0,0 +1,37 @@ +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Identity of a compiled method to search for (assembly + type + name + arity + parameter types). + /// + public readonly struct HotReloadCompiledMethodIdentity + { + // Only the analysis behind the entry point reads these; hot-reload code outside it only + // builds this value and passes it in. + internal readonly string AssemblyName; + internal readonly HotReloadMetadataTypeName TypeMetadataName; + internal readonly string MethodName; + internal readonly string[] ParameterTypeFullNames; + internal readonly int GenericArity; + + public HotReloadCompiledMethodIdentity( + string assemblyName, + HotReloadMetadataTypeName typeMetadataName, + string methodName, + string[] parameterTypeFullNames, + int genericArity) + { + Debug.Assert(!string.IsNullOrEmpty(assemblyName), "assemblyName must not be null or empty."); + Debug.Assert(!string.IsNullOrEmpty(methodName), "methodName must not be null or empty."); + Debug.Assert(parameterTypeFullNames != null, "parameterTypeFullNames must not be null."); + Debug.Assert(genericArity >= 0, "genericArity must not be negative."); + + AssemblyName = assemblyName; + TypeMetadataName = typeMetadataName; + MethodName = methodName; + ParameterTypeFullNames = parameterTypeFullNames; + GenericArity = genericArity; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs.meta new file mode 100644 index 000000000..88930c7d5 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 92ce7351b28b5498d861d5fefef80c47 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerClosure.cs similarity index 63% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerClosure.cs index f980395a8..055fa8641 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerClosure.cs @@ -17,17 +17,17 @@ internal static class HotReloadOneShotCallerClosure /// Returns proven lifecycle roots, or null when any abort condition makes the note unsafe. /// internal static List Resolve( - IReadOnlyList directHits, - Func scan, - Func isOneShotLifecycleCaller) + IReadOnlyList directHits, + Func scan, + Func isOneShotLifecycleCaller) { Debug.Assert(directHits != null, "directHits must not be null."); Debug.Assert(scan != null, "scan must not be null."); Debug.Assert(isOneShotLifecycleCaller != null, "isOneShotLifecycleCaller must not be null."); List roots = new List(); - List frontier = - new List(); + List frontier = + new List(); HashSet visited = new HashSet(StringComparer.Ordinal); if (!ClassifyHits(directHits, isOneShotLifecycleCaller, roots, frontier, visited)) { @@ -43,11 +43,11 @@ internal static List Resolve( } private static List WalkFrontier( - List frontier, + List frontier, List roots, HashSet visited, - Func scan, - Func isOneShotLifecycleCaller) + Func scan, + Func isOneShotLifecycleCaller) { int depth = 1; while (frontier.Count > 0) @@ -57,8 +57,8 @@ private static List WalkFrontier( return null; } - List nextFrontier = - new List(); + List nextFrontier = + new List(); if (!ScanFrontierLevel(frontier, nextFrontier, roots, visited, scan, isOneShotLifecycleCaller)) { return null; @@ -72,18 +72,18 @@ private static List WalkFrontier( } private static bool ScanFrontierLevel( - List frontier, - List nextFrontier, + List frontier, + List nextFrontier, List roots, HashSet visited, - Func scan, - Func isOneShotLifecycleCaller) + Func scan, + Func isOneShotLifecycleCaller) { - Dictionary> groups = + Dictionary> groups = GroupFrontierByAssembly(frontier); - foreach (KeyValuePair> pair in groups) + foreach (KeyValuePair> pair in groups) { - HotReloadCallSiteScanner.CompiledMethodIdentity[] identities = pair.Value.ToArray(); + HotReloadCompiledMethodIdentity[] identities = pair.Value.ToArray(); HotReloadCallSiteScanner.HotReloadCallSiteScanResult result = scan(pair.Key, identities); if (result.IsIncomplete) { @@ -100,23 +100,23 @@ private static bool ScanFrontierLevel( } private static bool AssignHitsForGroup( - List identities, - IReadOnlyList hits, - Func isOneShotLifecycleCaller, + List identities, + IReadOnlyList hits, + Func isOneShotLifecycleCaller, List roots, - List nextFrontier, + List nextFrontier, HashSet visited) { - foreach (HotReloadCallSiteScanner.CompiledMethodIdentity identity in identities) + foreach (HotReloadCompiledMethodIdentity identity in identities) { string targetKey = HotReloadMethodKeys.BuildMethodKeyParts( identity.TypeMetadataName.Value, identity.MethodName, identity.ParameterTypeFullNames, identity.GenericArity); - List hitsForIdentity = - new List(); - foreach (HotReloadCallSiteScanner.CallSiteHit hit in hits) + List hitsForIdentity = + new List(); + foreach (HotReloadCallSiteHit hit in hits) { if (hit.TargetMethodKey == targetKey) { @@ -139,13 +139,13 @@ private static bool AssignHitsForGroup( } private static bool ClassifyHits( - IReadOnlyList hits, - Func isOneShotLifecycleCaller, + IReadOnlyList hits, + Func isOneShotLifecycleCaller, List roots, - List frontier, + List frontier, HashSet visited) { - foreach (HotReloadCallSiteScanner.CallSiteHit hit in hits) + foreach (HotReloadCallSiteHit hit in hits) { // A delegate target can run after its Awake registration, so a function-pointer // load cannot prove the target is called only from one-shot lifecycle methods. @@ -171,16 +171,16 @@ private static bool ClassifyHits( return true; } - private static Dictionary> GroupFrontierByAssembly( - List frontier) + private static Dictionary> GroupFrontierByAssembly( + List frontier) { - Dictionary> groups = - new Dictionary>(StringComparer.Ordinal); - foreach (HotReloadCallSiteScanner.CompiledMethodIdentity identity in frontier) + Dictionary> groups = + new Dictionary>(StringComparer.Ordinal); + foreach (HotReloadCompiledMethodIdentity identity in frontier) { - if (!groups.TryGetValue(identity.AssemblyName, out List group)) + if (!groups.TryGetValue(identity.AssemblyName, out List group)) { - group = new List(); + group = new List(); groups.Add(identity.AssemblyName, group); } @@ -190,7 +190,7 @@ private static bool ClassifyHits( return groups; } - private static string BuildVisitKey(HotReloadCallSiteScanner.CallSiteHit hit) + private static string BuildVisitKey(HotReloadCallSiteHit hit) { string callerKey = hit.CallerMethodKey; if (string.IsNullOrEmpty(callerKey)) @@ -205,11 +205,11 @@ private static string BuildVisitKey(HotReloadCallSiteScanner.CallSiteHit hit) return hit.CallerAssemblyName + VisitKeySeparator + callerKey; } - private static HotReloadCallSiteScanner.CompiledMethodIdentity ToIdentity( - HotReloadCallSiteScanner.CallSiteHit hit) + private static HotReloadCompiledMethodIdentity ToIdentity( + HotReloadCallSiteHit hit) { string[] parameterTypeFullNames = hit.CallerParameterTypeFullNames ?? Array.Empty(); - return new HotReloadCallSiteScanner.CompiledMethodIdentity( + return new HotReloadCompiledMethodIdentity( hit.CallerAssemblyName, hit.CallerTypeMetadataName, hit.CallerMethodName, diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerClosure.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerClosure.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteBuilder.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteBuilder.cs similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteBuilder.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteBuilder.cs diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteBuilder.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteBuilder.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteBuilder.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteBuilder.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteEnricher.cs similarity index 60% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteEnricher.cs index e10fa3dc1..57813b9b1 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteEnricher.cs @@ -13,20 +13,6 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// internal static class HotReloadOneShotCallerNoteEnricher { - internal sealed class Candidate - { - public HotReloadCallSiteScanner.CompiledMethodIdentity Identity; - public HotReloadMethodOutcome Outcome; - - public Candidate( - HotReloadCallSiteScanner.CompiledMethodIdentity identity, - HotReloadMethodOutcome outcome) - { - Identity = identity; - Outcome = outcome; - } - } - // Keep in sync with LifecycleNotes.OneShotLifecycleMethodNames in the transform worker. private static readonly string[] OneShotLifecycleMethodNames = { @@ -40,7 +26,7 @@ public Candidate( /// /// Determines whether a compiled caller can be proven to be a one-shot lifecycle message. /// - internal static bool IsOneShotLifecycleCaller(HotReloadCallSiteScanner.CallSiteHit hit) + internal static bool IsOneShotLifecycleCaller(HotReloadCallSiteHit hit) { if (hit == null || !IsOneShotLifecycleMethodName(hit.CallerMethodName)) { @@ -84,50 +70,54 @@ internal static bool IsOneShotLifecycleCaller(HotReloadCallSiteScanner.CallSiteH return false; } - internal static void ApplyNotes( - string projectRoot, - List outcomes, - IReadOnlyList candidates, - Func scan) + /// + /// Builds the one-shot lifecycle note of each request; the result has one entry per request, + /// in the same order, null where no note can be proven. + /// + internal static string[] BuildNotes( + IReadOnlyList requests, + Func scan) { - Dictionary> candidatesByAssembly = - new Dictionary>(StringComparer.Ordinal); - foreach (Candidate candidate in candidates) + Debug.Assert(requests != null, "requests must not be null."); + Debug.Assert(scan != null, "scan must not be null."); + + string[] notes = new string[requests.Count]; + // Why grouped in request order: the scans run in this order, and the load budget is spent in it. + Dictionary> indexesByAssembly = + new Dictionary>(StringComparer.Ordinal); + for (int index = 0; index < requests.Count; index++) { - if (!string.IsNullOrEmpty(candidate.Outcome.LifecycleNote)) + string assemblyName = requests[index].Identity.AssemblyName; + if (!indexesByAssembly.TryGetValue(assemblyName, out List group)) { - continue; + group = new List(); + indexesByAssembly.Add(assemblyName, group); } - if (!candidatesByAssembly.TryGetValue(candidate.Identity.AssemblyName, out List group)) - { - group = new List(); - candidatesByAssembly.Add(candidate.Identity.AssemblyName, group); - } - - group.Add(candidate); + group.Add(index); } - foreach (KeyValuePair> pair in candidatesByAssembly) + foreach (KeyValuePair> pair in indexesByAssembly) { - HotReloadCallSiteScanner.CompiledMethodIdentity[] identities = pair.Value.ConvertAll( - candidate => candidate.Identity).ToArray(); + HotReloadCompiledMethodIdentity[] identities = pair.Value.ConvertAll( + index => requests[index].Identity).ToArray(); HotReloadCallSiteScanner.HotReloadCallSiteScanResult result = scan(pair.Key, identities); if (result.IsIncomplete) { continue; } - foreach (Candidate candidate in pair.Value) + foreach (int index in pair.Value) { + HotReloadOneShotCallerNoteRequest request = requests[index]; string targetKey = HotReloadMethodKeys.BuildMethodKeyParts( - candidate.Identity.TypeMetadataName.Value, - candidate.Identity.MethodName, - candidate.Identity.ParameterTypeFullNames, - candidate.Identity.GenericArity); - List targetHits = - new List(); - foreach (HotReloadCallSiteScanner.CallSiteHit hit in result.Hits) + request.Identity.TypeMetadataName.Value, + request.Identity.MethodName, + request.Identity.ParameterTypeFullNames, + request.Identity.GenericArity); + List targetHits = + new List(); + foreach (HotReloadCallSiteHit hit in result.Hits) { if (hit.TargetMethodKey != targetKey) { @@ -146,19 +136,11 @@ internal static void ApplyNotes( continue; } - string note = HotReloadOneShotCallerNoteBuilder.Build(candidate.Outcome.Method, callers); - if (note == null) - { - continue; - } - - int index = outcomes.IndexOf(candidate.Outcome); - if (index >= 0) - { - outcomes[index] = candidate.Outcome.WithLifecycleNote(note); - } + notes[index] = HotReloadOneShotCallerNoteBuilder.Build(request.Method, callers); } } + + return notes; } private static bool IsOneShotLifecycleMethodName(string methodName) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteEnricher.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteEnricher.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs new file mode 100644 index 000000000..115d887ed --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs @@ -0,0 +1,25 @@ +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// One method a one-shot lifecycle note is asked for: the compiled identity its callers are + /// searched by and the name the note shows. + /// + public sealed class HotReloadOneShotCallerNoteRequest + { + public HotReloadOneShotCallerNoteRequest(HotReloadCompiledMethodIdentity identity, string method) + { + Debug.Assert(!string.IsNullOrEmpty(method), "method must not be null or empty."); + + Identity = identity; + Method = method; + } + + // Only the analysis behind the entry point reads these; hot-reload code outside it only + // builds this value and passes it in. + internal HotReloadCompiledMethodIdentity Identity { get; } + + internal string Method { get; } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs.meta new file mode 100644 index 000000000..fd45eb9c4 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 36887725528654e08ba1755a7b965018 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodIndex.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadReferencedMethodIndex.cs similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodIndex.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadReferencedMethodIndex.cs diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodIndex.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadReferencedMethodIndex.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodIndex.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadReferencedMethodIndex.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor.asmdef b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor.asmdef new file mode 100644 index 000000000..c8892dcb1 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor.asmdef @@ -0,0 +1,22 @@ +{ + "name": "UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor", + "rootNamespace": "io.github.hatayama.UnityCliLoop.FirstPartyTools", + "references": [ + "GUID:1e3293e9639242278dcb27724b34beb4", + "GUID:fc3fd32eddbee40e39c2d76dc184957b", + "GUID:24d448abf7c10486d9578018b3a3c54b" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "Mono.Cecil.dll" + ], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor.asmdef.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor.asmdef.meta new file mode 100644 index 000000000..8901d9e05 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b6ba53c1f946045358f72c7981f7b425 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteWarmUpItem.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteWarmUpItem.cs index 6cc861019..eb7bc0b67 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteWarmUpItem.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteWarmUpItem.cs @@ -2,6 +2,8 @@ using System.Threading; using System.Threading.Tasks; +using UnityEngine; + namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { /// @@ -10,33 +12,25 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// internal sealed class HotReloadCallSiteWarmUpItem : IHotReloadWarmUpItem { - public string Name => "call_sites"; + private readonly HotReloadCompiledCallers _compiledCallers; - public Task RunAsync(HotReloadWarmUpContext context, CancellationToken ct) + internal HotReloadCallSiteWarmUpItem(HotReloadCompiledCallers compiledCallers) { - // Why taken here: the item is entered on the main thread, and the pool thread below - // must not be the first to touch a Shared instance. - HotReloadCompiledCallSiteCache cache = HotReloadCompiledCallSiteCache.Shared; - return Task.Run(() => LoadCallSites(context.Targets, cache, ct)); + Debug.Assert(compiledCallers != null, "compiledCallers must not be null."); + _compiledCallers = compiledCallers; } - /// Loads each target's dll and returns how many it loaded; throws before the next dll once cancelled. - internal static int LoadCallSites( - IReadOnlyList targets, - HotReloadCompiledCallSiteCache cache, - CancellationToken ct) + public string Name => "call_sites"; + + public Task RunAsync(HotReloadWarmUpContext context, CancellationToken ct) { - int count = 0; - foreach (HotReloadWarmUpTarget target in targets) + List dllPaths = new List(); + foreach (HotReloadWarmUpTarget target in context.Targets) { - // Why throw rather than stop: an item that returns normally is reported done, and - // one that skipped targets has not loaded them. - ct.ThrowIfCancellationRequested(); - cache.GetOrLoad(target.DllPath); - count++; + dllPaths.Add(target.DllPath); } - return count; + return _compiledCallers.WarmUpCallSitesAsync(dllPaths, ct); } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs index 18d949f07..6828ece28 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading.Tasks; using HarmonyLib; @@ -50,17 +49,18 @@ public static void Initialize() /// internal static HotReloadServices CreateProductionServices() { - return CreateProductionServicesWith(CreateProductionWarmUp(), CreateProductionCallSiteBackfill()); + HotReloadCompiledCallers compiledCallers = HotReloadCompiledCallers.CreateProduction(); + return CreateProductionServicesWith(CreateProductionWarmUp(compiledCallers), compiledCallers); } /// /// Builds the production services around and - /// , without installing them. A test that drives the - /// warm-up or the backfill itself passes its own. + /// (the compiled-caller analysis), without installing + /// them. A test that drives the warm-up or the background reads itself passes its own. /// internal static HotReloadServices CreateProductionServicesWith( HotReloadWarmUp warmUp, - HotReloadCallSiteBackfill callSiteBackfill) + HotReloadCompiledCallers compiledCallers) { HotReloadHarmonyGateway harmony = new HotReloadHarmonyGateway(new Harmony(HotReloadConstants.HarmonyId)); @@ -74,33 +74,18 @@ internal static HotReloadServices CreateProductionServicesWith( new HotReloadApplicationPlayModeQuery(), new HotReloadSourceSnapshotCapture(HotReloadSourceSnapshotter.CaptureAfterDomainReload), warmUp, - callSiteBackfill); + compiledCallers); } - /// - /// Builds the backfill production runs on: it reads a dll into the shared call-site cache. - /// - internal static HotReloadCallSiteBackfill CreateProductionCallSiteBackfill() - { - // Why the cache is taken before the pool thread starts: Start runs on the main thread at - // the end of a run, and the pool thread must not be the first to touch a Shared instance - // (the call-site warm-up item takes it the same way). - return new HotReloadCallSiteBackfill((dllPath, ct) => - { - HotReloadCompiledCallSiteCache cache = HotReloadCompiledCallSiteCache.Shared; - return Task.Run(() => cache.GetOrLoad(dllPath), ct); - }); - } - - private static HotReloadWarmUp CreateProductionWarmUp() + private static HotReloadWarmUp CreateProductionWarmUp(HotReloadCompiledCallers compiledCallers) { string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); return new HotReloadWarmUp( new HotReloadWarmUpContextSource( projectRoot, new HotReloadEditorStateSnapshotCapture(), - assemblyName => HotReloadCallSiteScanner.CollectReferencingDllPaths(projectRoot, assemblyName)), - HotReloadWarmUpItems.CreateProduction()); + assemblyName => compiledCallers.CollectReferencingDllPaths(projectRoot, assemblyName)), + HotReloadWarmUpItems.CreateProduction(compiledCallers)); } /// @@ -133,7 +118,7 @@ internal static HotReloadServices CreateServices( IHotReloadPlayModeQuery playMode, HotReloadSourceSnapshotCapture sourceSnapshotCapture, HotReloadWarmUp warmUp, - HotReloadCallSiteBackfill callSiteBackfill) + HotReloadCompiledCallers compiledCallers) { // Built in dependency order, and every collaborator takes what it needs here: nothing // below may read the installed services, or a replacement scope would leave it bound @@ -154,7 +139,8 @@ internal static HotReloadServices CreateServices( editorStateSnapshotCapture, new HotReloadGroupCommitPolicy(domain, fileEntryApplier), playMode, - HotReloadMonoInternalAccessGrant.ForThisProcess()); + HotReloadMonoInternalAccessGrant.ForThisProcess(), + compiledCallers); // 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. @@ -219,7 +205,7 @@ internal static HotReloadServices CreateServices( packageRootCapture, unityMessageForwarding, warmUp, - callSiteBackfill), + compiledCallers), new HotReloadStatusExecutor( domain, patcher, @@ -234,7 +220,7 @@ internal static HotReloadServices CreateServices( wiredValueRestoreRefresh, sourceSnapshotCapture, warmUp, - callSiteBackfill); + compiledCallers); } /// @@ -325,11 +311,11 @@ internal static void UninstallInstalledServices() private static void UninstallServices(HotReloadServices services) { ClearWiring(); - // Why stop the warm-up and the backfill: services replaced within a domain would - // otherwise leave the previous warm-up or backfill reading the shared caches on a pool - // thread. Not awaited: the caller is synchronous, and the dll in flight ends on its own. + // Why stop the warm-up and the background reads: services replaced within a domain would + // otherwise leave the previous warm-up or background reads reading the shared caches on a + // pool thread. Not awaited: the caller is synchronous, and the dll in flight ends on its own. _ = services?.WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerServicesReplaced); - _ = services?.CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerServicesReplaced); + _ = services?.CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerServicesReplaced); // Why the domain is disposed after the wiring is dropped: disposing unsubscribes its // resolver, and a bind arriving through a still-installed gateway would otherwise // reach a domain that can no longer answer. diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileEntryApplier.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileEntryApplier.cs index 84a47f919..88344e4f5 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileEntryApplier.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileEntryApplier.cs @@ -385,7 +385,7 @@ private void AppendOneShotCallerNoteCandidate( HotReloadEntryResolution.ResolvedEntry resolved, HotReloadMethodOutcome outcome, string assemblyName, - List candidates) + List candidates) { if (candidates == null) { @@ -399,14 +399,14 @@ private void AppendOneShotCallerNoteCandidate( return; } - HotReloadCallSiteScanner.CompiledMethodIdentity identity = - new HotReloadCallSiteScanner.CompiledMethodIdentity( + HotReloadCompiledMethodIdentity identity = + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName(resolved.Entry.typeMetadataName), resolved.Entry.methodName, resolved.Entry.parameterTypeFullNames ?? Array.Empty(), resolved.Entry.genericArity); - candidates.Add(new HotReloadOneShotCallerNoteEnricher.Candidate(identity, outcome)); + candidates.Add(new HotReloadOneShotCallerNoteCandidate(identity, outcome)); } private HotReloadMethodOutcome ApplyResolvedEntry( diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileSinks.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileSinks.cs index 7e227795a..9e925dafa 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileSinks.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileSinks.cs @@ -16,7 +16,7 @@ internal sealed class HotReloadFileSinks { internal HotReloadFileSinks( List siblingDerivedWarnings, - List oneShotCallerNoteCandidates, + List oneShotCallerNoteCandidates, HotReloadRunStaleSignatureWarnings staleSignatureWarnings, HotReloadRunDisplayedRemovedMembers displayedRemovedMembers = null) { @@ -62,7 +62,7 @@ internal HotReloadFileSinks( internal List SiblingDerivedWarnings { get; } // Shared across the whole run; null when the caller collects no one-shot caller notes. - internal List OneShotCallerNoteCandidates { get; } + internal List OneShotCallerNoteCandidates { get; } // Shared across the whole run so the warning reflects the patches active when it ends. internal HotReloadRunStaleSignatureWarnings StaleSignatureWarnings { get; } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs index f4390284b..a9c5e9aae 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs @@ -24,7 +24,8 @@ internal HotReloadGroupStageCollaborators( IHotReloadEditorStateSnapshotCapture editorStateSnapshotCapture, HotReloadGroupCommitPolicy commitPolicy, IHotReloadPlayModeQuery playMode, - IHotReloadInternalAccessGrant internalAccessGrant) + IHotReloadInternalAccessGrant internalAccessGrant, + HotReloadCompiledCallers compiledCallers) { Debug.Assert(domain != null, "domain must not be null."); Debug.Assert(patcher != null, "patcher must not be null."); @@ -37,6 +38,7 @@ 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; @@ -47,6 +49,7 @@ internal HotReloadGroupStageCollaborators( CommitPolicy = commitPolicy; PlayMode = playMode; InternalAccessGrant = internalAccessGrant; + CompiledCallers = compiledCallers; } internal HotReloadDomain Domain { get; } @@ -70,5 +73,7 @@ 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/HotReloadOneShotCallerNoteAttacher.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs new file mode 100644 index 000000000..f968ea921 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; + +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Attaches one-shot lifecycle notes to a run's method outcomes: asks for a note for each + /// candidate whose outcome has none yet, and replaces each outcome that got one. + /// + internal static class HotReloadOneShotCallerNoteAttacher + { + internal static void Attach( + List outcomes, + IReadOnlyList candidates, + Func, IReadOnlyList> buildNotes) + { + Debug.Assert(outcomes != null, "outcomes must not be null."); + Debug.Assert(candidates != null, "candidates must not be null."); + Debug.Assert(buildNotes != null, "buildNotes must not be null."); + + List pending = new List(); + List requests = new List(); + foreach (HotReloadOneShotCallerNoteCandidate candidate in candidates) + { + // Why skipped: a note the transform worker already wrote stays authoritative. + if (!string.IsNullOrEmpty(candidate.Outcome.LifecycleNote)) + { + continue; + } + + pending.Add(candidate); + requests.Add(new HotReloadOneShotCallerNoteRequest(candidate.Identity, candidate.Outcome.Method)); + } + + IReadOnlyList notes = buildNotes(requests); + Debug.Assert(notes != null && notes.Count == requests.Count, "buildNotes returns one entry per request."); + for (int index = 0; index < pending.Count; index++) + { + if (notes[index] == null) + { + continue; + } + + // Why by reference: an outcome an earlier candidate already replaced is no longer in + // the list, so the first note for one outcome wins. + int outcomeIndex = outcomes.IndexOf(pending[index].Outcome); + if (outcomeIndex < 0) + { + continue; + } + + outcomes[outcomeIndex] = pending[index].Outcome.WithLifecycleNote(notes[index]); + } + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs.meta new file mode 100644 index 000000000..912cde48c --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 17aa8618eebe84d7c99aa20235fe3b46 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs new file mode 100644 index 000000000..dd3f2ff17 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs @@ -0,0 +1,20 @@ +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// A patched or added method whose outcome may get a one-shot lifecycle note, with the + /// compiled identity its callers are searched by. + /// + internal sealed class HotReloadOneShotCallerNoteCandidate + { + public HotReloadCompiledMethodIdentity Identity; + public HotReloadMethodOutcome Outcome; + + public HotReloadOneShotCallerNoteCandidate( + HotReloadCompiledMethodIdentity identity, + HotReloadMethodOutcome outcome) + { + Identity = identity; + Outcome = outcome; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs.meta new file mode 100644 index 000000000..bc378b187 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7a5a3974e2e6f4ebfb8c3b383dad52a9 +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 84c5a6b48..97b64015d 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestrator.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestrator.cs @@ -26,7 +26,7 @@ internal sealed class HotReloadOrchestrator : IHotReloadOrchestrator private readonly HotReloadPatcher _patcher; private readonly HotReloadUnityMessageForwarding _unityMessageForwarding; private readonly HotReloadWarmUp _warmUp; - private readonly HotReloadCallSiteBackfill _callSiteBackfill; + private readonly HotReloadCompiledCallers _compiledCallers; internal HotReloadOrchestrator( HotReloadDomain domain, @@ -38,7 +38,7 @@ internal HotReloadOrchestrator( IHotReloadPackageRootCapture packageRootCapture, HotReloadUnityMessageForwarding unityMessageForwarding, HotReloadWarmUp warmUp, - HotReloadCallSiteBackfill callSiteBackfill) + HotReloadCompiledCallers compiledCallers) { Debug.Assert(domain != null, "domain must not be null."); Debug.Assert(patcher != null, "patcher must not be null."); @@ -50,7 +50,7 @@ internal HotReloadOrchestrator( Debug.Assert( unityMessageForwarding != null, "unityMessageForwarding must not be null."); Debug.Assert(warmUp != null, "warmUp must not be null."); - Debug.Assert(callSiteBackfill != null, "callSiteBackfill must not be null."); + Debug.Assert(compiledCallers != null, "compiledCallers must not be null."); _domain = domain; _patcher = patcher; _groupProcessor = groupProcessor; @@ -60,7 +60,7 @@ internal HotReloadOrchestrator( _packageRootCapture = packageRootCapture; _unityMessageForwarding = unityMessageForwarding; _warmUp = warmUp; - _callSiteBackfill = callSiteBackfill; + _compiledCallers = compiledCallers; } /// @@ -85,22 +85,9 @@ public async Task RunAsync( string correlationId = VibeLogger.GenerateCorrelationId(); Stopwatch total = Stopwatch.StartNew(); - // 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. - using IDisposable callSiteCacheHold = HotReloadCompiledCallSiteCache.Shared.HoldEntriesForRun(); HotReloadRunTiming timing = new HotReloadRunTiming(); - - using (timing.MeasureDetail(HotReloadConstants.TimingDetailStepWarmUpYield)) - { - // Why first: the warm-up may be reading a dll this run is about to read; waiting for - // that one item here is cheaper than contending for the cache lock inside the run. - await _warmUp.YieldToRunAsync().ConfigureAwait(false); - // Why here too: the backfill of the previous run may still be reading a dll this run is - // about to read, and waiting for that one read here is cheaper than contending for the - // cache lock inside the run. - await _callSiteBackfill.YieldToRunAsync().ConfigureAwait(false); - } + using HotReloadCompiledCallersRun compiledCallersRun = + await BeginCompiledCallersRunAsync(timing).ConfigureAwait(false); using (timing.MeasureDetail(HotReloadConstants.TimingDetailStepMainThreadSwitch)) { @@ -261,11 +248,11 @@ await ProcessPlannedGroupAsync( await MainThreadSwitcher.SwitchToMainThread(ct); } - IReadOnlyList refusedDllPaths; using (timing.MeasureDetail(HotReloadConstants.TimingDetailStepCallerNotes)) { string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - refusedDllPaths = run.ApplyOneShotCallerNotes(projectRoot, correlationId); + run.ApplyOneShotCallerNotes( + requests => compiledCallersRun.BuildOneShotCallerNotes(projectRoot, requests, correlationId)); } using (timing.MeasureDetail(HotReloadConstants.TimingDetailStepMainThreadSwitch)) @@ -278,14 +265,23 @@ await ProcessPlannedGroupAsync( HotReloadTimingDetailPayload.Build(breakdown, timing.Details, groupCount), correlationId); HotReloadOrchestratorResult result = run.BuildResult(correlationId, breakdown); - // 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. Disposing - // the hold again at the end of the method releases nothing. - callSiteCacheHold.Dispose(); - _callSiteBackfill.Start(refusedDllPaths, correlationId); + compiledCallersRun.End(correlationId); return result; } + // Yields to the background work that may be reading a dll this run is about to read, and + // begins the compiled-caller run once nothing reads any more. + private async Task BeginCompiledCallersRunAsync(HotReloadRunTiming timing) + { + using (timing.MeasureDetail(HotReloadConstants.TimingDetailStepWarmUpYield)) + { + // Why first: the warm-up may be reading a dll this run is about to read; waiting for + // that one item here is cheaper than contending for the cache lock inside the run. + await _warmUp.YieldToRunAsync().ConfigureAwait(false); + return await _compiledCallers.BeginRunAsync().ConfigureAwait(false); + } + } + // Why on the inputs only: a re-applied sibling joins a group later and was never selected, // so it keeps the flag unset even in a default-selection run. private static List CollectDistinctAssemblyNames( diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestratorLog.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestratorLog.cs index a9cfa079d..25ec908d7 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestratorLog.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestratorLog.cs @@ -177,21 +177,5 @@ internal static void LogHotReloadTimingDetail(HotReloadTimingDetailPayload paylo }, correlationId); } - - internal static void LogHotReloadCallerNoteLoadBudgetExhausted( - int budgetLoads, - IReadOnlyList refusedAssemblyNames, - string correlationId) - { - VibeLogger.LogInfo( - HotReloadConstants.VibeLogCallerNoteLoadBudgetExhausted, - "Caller-note resolution hit its uncached dll load budget; the note was omitted.", - new - { - budgetLoads, - refusedAssemblies = refusedAssemblyNames.ToArray() - }, - correlationId); - } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodSetWarmUpItem.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodSetWarmUpItem.cs index ad7c62d17..4a294b9f4 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodSetWarmUpItem.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodSetWarmUpItem.cs @@ -2,6 +2,8 @@ using System.Threading; using System.Threading.Tasks; +using UnityEngine; + namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { /// @@ -10,31 +12,32 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// internal sealed class HotReloadReferencedMethodSetWarmUpItem : IHotReloadWarmUpItem { + private readonly HotReloadCompiledCallers _compiledCallers; + + internal HotReloadReferencedMethodSetWarmUpItem(HotReloadCompiledCallers compiledCallers) + { + Debug.Assert(compiledCallers != null, "compiledCallers must not be null."); + _compiledCallers = compiledCallers; + } + public string Name => "referenced_method_sets"; public Task RunAsync(HotReloadWarmUpContext context, CancellationToken ct) { - // Why taken here: the Shared instance reads Application.dataPath when it is first - // touched, which only the main thread may do. - HotReloadReferencedMethodIndex index = HotReloadReferencedMethodIndex.Shared; - return Task.Run(() => PreloadSets(context.Targets, index, ct)); + List dllPaths = CollectDllPaths(context.Targets); + return _compiledCallers.WarmUpReferencedMethodSetsAsync(dllPaths, ct); } - /// Preloads each referencing dll's set; throws before the next dll once cancelled. - internal static void PreloadSets( - IReadOnlyList targets, - HotReloadReferencedMethodIndex index, - CancellationToken ct) + /// The referencing dlls of every target, in target order; never a target's own dll. + internal static List CollectDllPaths(IReadOnlyList targets) { + List dllPaths = new List(); foreach (HotReloadWarmUpTarget target in targets) { - foreach (string dllPath in target.ReferencingDllPaths) - { - // Why throw rather than stop: an item that returns normally is reported done. - ct.ThrowIfCancellationRequested(); - index.Preload(dllPath); - } + dllPaths.AddRange(target.ReferencingDllPaths); } + + return dllPaths; } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs index f1901b0ea..c4dff954c 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs @@ -35,8 +35,8 @@ internal sealed class HotReloadRunAccumulator // and two files declaring the same type is a mistake the run has to report against both. private readonly List _introducedTypes = new List(); - private readonly List _oneShotCallerNoteCandidates = - new List(); + private readonly List _oneShotCallerNoteCandidates = + new List(); // Why staged (not recorded per file): duplicate paths in one run must still apply // twice; recording mid-run would short-circuit the second copy. // Why the last occurrence wins: only what the last copy landed is what the next run has @@ -94,7 +94,7 @@ public HotReloadRunAccumulator( public HotReloadSiblingBaselineNotices SiblingBaselineNotices => _siblingBaselineNotices; /// Candidate sink shared with the per-file stage for one-shot lifecycle notes. - public List OneShotCallerNoteCandidates => + public List OneShotCallerNoteCandidates => _oneShotCallerNoteCandidates; /// Remembers why a sibling came back, for its report and its ledger updates. @@ -216,35 +216,14 @@ public void RecordAppliedSourceHashes() } /// - /// Attaches one-shot lifecycle notes to the merged outcomes. Requires the Unity main thread - /// because the call-site scan reads compiled assemblies through Editor APIs. - /// Returns the compiled dlls the load budget refused, in scan order; empty when every scan - /// was complete. + /// Attaches one-shot lifecycle notes to the merged outcomes; gets + /// one request per candidate outcome that has no note yet and returns one note or null per request. /// - public IReadOnlyList ApplyOneShotCallerNotes(string projectRoot, string correlationId) + public void ApplyOneShotCallerNotes( + Func, IReadOnlyList> buildNotes) { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty."); - Debug.Assert(!string.IsNullOrEmpty(correlationId), "correlationId must not be null or empty."); - - // 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 - // however many candidate assemblies and levels it visits. - HotReloadCallSiteLoadBudget budget = - new HotReloadCallSiteLoadBudget(HotReloadConstants.CallerNoteUncachedDllLoadBudget); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, - _outcomes, - _oneShotCallerNoteCandidates, - (ignoredAssemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities, budget)); - if (budget.RefusedAssemblyNames.Count > 0) - { - HotReloadOrchestratorLog.LogHotReloadCallerNoteLoadBudgetExhausted( - HotReloadConstants.CallerNoteUncachedDllLoadBudget, - budget.RefusedAssemblyNames, - correlationId); - } - - return budget.RefusedDllPaths; + Debug.Assert(buildNotes != null, "buildNotes must not be null."); + HotReloadOneShotCallerNoteAttacher.Attach(_outcomes, _oneShotCallerNoteCandidates, buildNotes); } /// diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunStaleSignatureWarnings.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunStaleSignatureWarnings.cs index cbf53adaa..aa66676f5 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunStaleSignatureWarnings.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunStaleSignatureWarnings.cs @@ -81,7 +81,7 @@ private static List CollectCompiledCallerKeys( { List callerKeys = new List(); HashSet seenCallerKeys = new HashSet(StringComparer.Ordinal); - foreach (HotReloadCallSiteScanner.CallSiteHit caller in callSites.Callers) + foreach (HotReloadCallSiteHit caller in callSites.Callers) { if (IsReplacedByActivePatch(caller, activeLabelsByAssembly)) { @@ -100,7 +100,7 @@ private static List CollectCompiledCallerKeys( // Why the label rather than the wire key: an active patch is described by the label of its // resolved method, which the hit's label spells the same way, nested types included. private static bool IsReplacedByActivePatch( - HotReloadCallSiteScanner.CallSiteHit caller, + HotReloadCallSiteHit caller, Dictionary> activeLabelsByAssembly) { return activeLabelsByAssembly.TryGetValue(caller.CallerAssemblyName, out HashSet labels) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadServices.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadServices.cs index 80120dcf9..61c2479f6 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadServices.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadServices.cs @@ -28,7 +28,7 @@ internal HotReloadServices( HotReloadWiredValueRestoreRefresh wiredValueRestoreRefresh, HotReloadSourceSnapshotCapture sourceSnapshotCapture, HotReloadWarmUp warmUp, - HotReloadCallSiteBackfill callSiteBackfill) + HotReloadCompiledCallers compiledCallers) { Debug.Assert(domain != null, "domain must not be null."); Debug.Assert(harmony != null, "harmony must not be null."); @@ -53,7 +53,7 @@ internal HotReloadServices( wiredValueRestoreRefresh != null, "wiredValueRestoreRefresh must not be null."); Debug.Assert(sourceSnapshotCapture != null, "sourceSnapshotCapture must not be null."); Debug.Assert(warmUp != null, "warmUp must not be null."); - Debug.Assert(callSiteBackfill != null, "callSiteBackfill must not be null."); + Debug.Assert(compiledCallers != null, "compiledCallers must not be null."); Domain = domain; Harmony = harmony; Patcher = patcher; @@ -73,7 +73,7 @@ internal HotReloadServices( WiredValueRestoreRefresh = wiredValueRestoreRefresh; SourceSnapshotCapture = sourceSnapshotCapture; WarmUp = warmUp; - CallSiteBackfill = callSiteBackfill; + CompiledCallers = compiledCallers; } internal HotReloadDomain Domain { get; } @@ -136,10 +136,11 @@ internal HotReloadServices( internal HotReloadWarmUp WarmUp { get; } /// - /// Reads, after a run, the assemblies the run's caller-note budget refused. Shared by every - /// copy below, so the run that yields to it and the Editor hooks that stop it see one backfill. + /// The compiled-caller analysis, which also reads after a run the assemblies the run's + /// caller-note budget refused. Shared by every copy below, so the run that yields to those + /// background reads and the Editor hooks that stop them see one instance. /// - internal HotReloadCallSiteBackfill CallSiteBackfill { get; } + internal HotReloadCompiledCallers CompiledCallers { get; } /// /// A copy that runs instead of this one, sharing every @@ -168,7 +169,7 @@ internal HotReloadServices WithOrchestrator(IHotReloadOrchestrator orchestrator) WiredValueRestoreRefresh, SourceSnapshotCapture, WarmUp, - CallSiteBackfill); + CompiledCallers); } /// @@ -197,7 +198,7 @@ internal HotReloadServices WithChangeDetector(IHotReloadChangeDetector changeDet WiredValueRestoreRefresh, SourceSnapshotCapture, WarmUp, - CallSiteBackfill); + CompiledCallers); } /// @@ -227,7 +228,7 @@ internal HotReloadServices WithSourceSnapshotCapture( WiredValueRestoreRefresh, sourceSnapshotCapture, WarmUp, - CallSiteBackfill); + CompiledCallers); } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeCoverage.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeCoverage.cs index def9dd4a9..02886879c 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeCoverage.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeCoverage.cs @@ -26,12 +26,12 @@ internal static HashSet CollectCoveredMethodId } internal static Dictionary> CollectUncoveredCallersByTarget( - IReadOnlyList hits, + IReadOnlyList hits, HashSet coveredIdentities) { Dictionary> uncoveredCallersByTarget = new Dictionary>(StringComparer.Ordinal); - foreach (HotReloadCallSiteScanner.CallSiteHit hit in hits) + foreach (HotReloadCallSiteHit hit in hits) { HotReloadQualifiedMethodIdentity callerIdentity = CreateCallerIdentity(hit); if (coveredIdentities.Contains(callerIdentity)) @@ -62,7 +62,7 @@ internal static Dictionary> Colle /// internal static List CollectStaleSignatureCallSites( TransformWorkerRemovedMethodSignatureDto[] removedSignatures, - IReadOnlyList hits, + IReadOnlyList hits, Dictionary> uncoveredCallersByTarget) { List staleCallSites = new List(); @@ -92,16 +92,16 @@ internal static List CollectStaleSignatureCall // Why one hit per caller: a caller that calls the removed method twice is still one caller // to name, and whether it still runs its compiled body is decided per caller. - private static List CollectFirstHitPerCaller( + private static List CollectFirstHitPerCaller( string targetMethodKey, - IReadOnlyList hits, + IReadOnlyList hits, List uncoveredCallers) { HashSet callersWithoutHit = new HashSet(uncoveredCallers); - List callerHits = - new List(uncoveredCallers.Count); - foreach (HotReloadCallSiteScanner.CallSiteHit hit in hits) + List callerHits = + new List(uncoveredCallers.Count); + foreach (HotReloadCallSiteHit hit in hits) { if (string.Equals(hit.TargetMethodKey, targetMethodKey, StringComparison.Ordinal) && callersWithoutHit.Remove(CreateCallerIdentity(hit))) @@ -128,7 +128,7 @@ internal static void AppendSignatureChangeCallersRepatchedWarnings( List warnings, string assemblyName, TransformWorkerEntryDto[] entriesToPatch, - IReadOnlyList hits, + IReadOnlyList hits, HashSet snapshotLabels) { Debug.Assert(warnings != null, "warnings must not be null."); @@ -151,7 +151,7 @@ internal static void AppendSignatureChangeCallersRepatchedWarnings( Dictionary> callerLabelsByOldSignature = new Dictionary>(StringComparer.Ordinal); - foreach (HotReloadCallSiteScanner.CallSiteHit hit in hits) + foreach (HotReloadCallSiteHit hit in hits) { if (hit == null || !appliedReplacementKeys.Contains(hit.TargetMethodKey) @@ -190,7 +190,7 @@ internal static void AppendSignatureChangeCallersRepatchedWarnings( } } - internal static string FormatCallSiteCallerLabel(HotReloadCallSiteScanner.CallSiteHit hit) + internal static string FormatCallSiteCallerLabel(HotReloadCallSiteHit hit) { Debug.Assert(hit != null, "hit must not be null."); return HotReloadMethodKeys.FormatMethodLabelParts( @@ -247,7 +247,7 @@ internal static bool AreUncoveredCallersInEditedFile( internal static List FindSignatureChangeCoverageLosses( string assemblyName, TransformWorkerEntryDto[] entriesToPatch, - IReadOnlyList hits, + IReadOnlyList hits, IReadOnlyCollection deletedCallerExemptions) { Debug.Assert(entriesToPatch != null, "entriesToPatch must not be null."); @@ -442,7 +442,7 @@ private static HotReloadQualifiedMethodIdentity CreateUnchangedMethodIdentity( } private static HotReloadQualifiedMethodIdentity CreateCallerIdentity( - HotReloadCallSiteScanner.CallSiteHit hit) + HotReloadCallSiteHit hit) { Debug.Assert(hit != null, "hit must not be null."); return new HotReloadQualifiedMethodIdentity(hit.CallerAssemblyName, hit.CallerMethodKey); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs index 012660997..b3e614afa 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs @@ -31,7 +31,7 @@ internal static async Task TryApplySignatureChangeGat return SignatureChangeGateResult.NoWork(); } - HotReloadCallSiteScanner.CompiledMethodIdentity[] targets = CollectScanTargets( + HotReloadCompiledMethodIdentity[] targets = CollectScanTargets( context.AssemblyName, replacementEntries, removedSignatures); @@ -42,8 +42,7 @@ internal static async Task TryApplySignatureChangeGat context.WorkerOutput.unchangedMethods ?? Array.Empty(), context.WorkerOutput.skipped ?? Array.Empty(), removedSignatures); - List hits = - HotReloadCallSiteScanner.FindCallSites(context.ProjectRoot, targets).Hits; + List hits = collaborators.CompiledCallers.FindCallSites(context.ProjectRoot, targets); Dictionary> uncoveredCallersByTarget = CollectInitialUncoveredCallers(context.AssemblyName, entries, hits, deletedCallerExemptions); @@ -145,7 +144,7 @@ private static List CollectReplacementEntries( internal static Dictionary> CollectInitialUncoveredCallers( string assemblyName, TransformWorkerEntryDto[] entries, - IReadOnlyList hits, + IReadOnlyList hits, IReadOnlyCollection deletedCallerExemptions) { HashSet coveredIdentities = @@ -158,13 +157,13 @@ internal static Dictionary> Colle coveredIdentities); } - private static HotReloadCallSiteScanner.CompiledMethodIdentity[] CollectScanTargets( + private static HotReloadCompiledMethodIdentity[] CollectScanTargets( string assemblyName, IReadOnlyList replacementEntries, TransformWorkerRemovedMethodSignatureDto[] removedSignatures) { - List targets = - new List(); + List targets = + new List(); HashSet seenKeys = new HashSet(StringComparer.Ordinal); foreach (TransformWorkerEntryDto entry in replacementEntries) { @@ -194,7 +193,7 @@ private static HotReloadCallSiteScanner.CompiledMethodIdentity[] CollectScanTarg } private static void TryAddScanTarget( - List targets, + List targets, HashSet seenKeys, string assemblyName, string typeMetadataName, @@ -213,7 +212,7 @@ private static void TryAddScanTarget( } targets.Add( - new HotReloadCallSiteScanner.CompiledMethodIdentity( + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName(typeMetadataName), methodName, @@ -354,7 +353,7 @@ internal sealed class SignatureChangeGateResult public HotReloadShimIsolation.HotReloadShimIsolationResult Isolation { get; } public List SkippedOutcomes { get; } public List StaleSignatureCallSites { get; } - public List Hits { get; } + public List Hits { get; } public HashSet DeletedCallerExemptions { get; } public List GatedReplacementMethodKeys { get; } @@ -368,7 +367,7 @@ private SignatureChangeGateResult( HotReloadShimIsolation.HotReloadShimIsolationResult isolation, List skippedOutcomes, List staleSignatureCallSites, - List hits, + List hits, HashSet deletedCallerExemptions, List gatedReplacementMethodKeys) { @@ -378,7 +377,7 @@ private SignatureChangeGateResult( Isolation = isolation; SkippedOutcomes = skippedOutcomes ?? new List(); StaleSignatureCallSites = staleSignatureCallSites ?? new List(); - Hits = hits ?? new List(); + Hits = hits ?? new List(); DeletedCallerExemptions = deletedCallerExemptions ?? new HashSet(); GatedReplacementMethodKeys = gatedReplacementMethodKeys ?? new List(); @@ -393,7 +392,7 @@ public static SignatureChangeGateResult NoWork() public static SignatureChangeGateResult WarningsOnly( List staleSignatureCallSites, - List hits, + List hits, HashSet deletedCallerExemptions) { return new SignatureChangeGateResult( @@ -424,7 +423,7 @@ public static SignatureChangeGateResult Retried( HotReloadShimIsolation.HotReloadShimIsolationResult isolation, List skippedOutcomes, List staleSignatureCallSites, - List hits, + List hits, HashSet deletedCallerExemptions, List gatedReplacementMethodKeys) { diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadStaleSignatureCallSites.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadStaleSignatureCallSites.cs index 8a91bb611..9d8772824 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadStaleSignatureCallSites.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadStaleSignatureCallSites.cs @@ -17,7 +17,7 @@ internal sealed class HotReloadStaleSignatureCallSites { internal HotReloadStaleSignatureCallSites( string removedMethodKey, - IReadOnlyList callers) + IReadOnlyList callers) { Debug.Assert(!string.IsNullOrEmpty(removedMethodKey), "removedMethodKey must not be null or empty."); Debug.Assert(callers != null && callers.Count > 0, "callers must hold at least one hit."); @@ -28,6 +28,6 @@ internal HotReloadStaleSignatureCallSites( // The wire key of the removed signature (Type::Method(params)), as the warning names it. internal string RemovedMethodKey { get; } - internal IReadOnlyList Callers { get; } + internal IReadOnlyList Callers { get; } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpEditorHooks.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpEditorHooks.cs index bffa616ff..5e67dc9f5 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpEditorHooks.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpEditorHooks.cs @@ -6,9 +6,9 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { /// - /// Stops the installed warm-up and caller-note backfill when the Editor is about to reload the - /// domain or starts a compile: they would read dlls that are being rewritten, or work for a - /// domain that is going away. + /// Stops the installed warm-up and the background reads of the compiled-caller analysis when + /// the Editor is about to reload the domain or starts a compile: they would read dlls that are + /// being rewritten, or work for a domain that is going away. /// internal static class HotReloadWarmUpEditorHooks { @@ -34,13 +34,13 @@ internal static void Initialize() internal static void ShutdownForReload() { _ = GetServices().WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerBeforeAssemblyReload); - _ = GetServices().CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerBeforeAssemblyReload); + _ = GetServices().CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerBeforeAssemblyReload); } internal static void ShutdownForCompile(object context) { _ = GetServices().WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerCompilationStarted); - _ = GetServices().CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerCompilationStarted); + _ = GetServices().CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerCompilationStarted); } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpItems.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpItems.cs index 4aa3a63c0..fab33596c 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpItems.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpItems.cs @@ -20,13 +20,13 @@ internal static class HotReloadWarmUpItems /// and do not stop the items after it; any other exception does, but a run with the same /// input makes the same call and fails the same way. /// - internal static IReadOnlyList CreateProduction() + internal static IReadOnlyList CreateProduction(HotReloadCompiledCallers compiledCallers) { return new IHotReloadWarmUpItem[] { new HotReloadPublicizedTargetWarmUpItem(), - new HotReloadCallSiteWarmUpItem(), - new HotReloadReferencedMethodSetWarmUpItem(), + new HotReloadCallSiteWarmUpItem(compiledCallers), + new HotReloadReferencedMethodSetWarmUpItem(compiledCallers), new HotReloadTransformWorkerWarmUpItem(TransformWorkerHost.Shared.RunAsync), new HotReloadPdbDocumentWarmUpItem() }; diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/AssemblyInfo.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/AssemblyInfo.cs index 1cadc4918..b56310d3a 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/AssemblyInfo.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/AssemblyInfo.cs @@ -6,6 +6,9 @@ // The introduced-type aggregate builds on this shared kernel. [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.IntroducedType.Editor")] +// The compiled-caller analysis builds on this shared kernel. +[assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor")] + // The application assembly composes this part; the split is internal to the hot-reload tool. [assembly: InternalsVisibleTo("UnityCLILoop.FirstPartyTools.HotReload.Editor")] diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs index e5d0f5b46..59dc1ae29 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs @@ -12,8 +12,9 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// Why a type and not a string: metadata, reflection and display names differ only in the /// separator of a nested type, so a value that crosses from one world to the other silently /// stops matching. Holding the world in the type makes the crossing a call, not a habit. + /// Public because the compiled-caller analysis exposes it in the types of its entry point. /// - internal readonly struct HotReloadMetadataTypeName : IEquatable + public readonly struct HotReloadMetadataTypeName : IEquatable { public string Value { get; } @@ -87,8 +88,10 @@ public override string ToString() /// /// A type name in reflection form, the spelling Type.FullName produces, where a nested type /// reads Outer+Inner. + /// Public because HotReloadMetadataTypeName, which the compiled-caller analysis exposes, + /// returns it from ToReflectionName. /// - internal readonly struct HotReloadReflectionTypeName : IEquatable + public readonly struct HotReloadReflectionTypeName : IEquatable { public string Value { get; } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/UnityCLILoop.FirstPartyTools.HotReload.Editor.asmdef b/Packages/src/Editor/FirstPartyTools/HotReload/UnityCLILoop.FirstPartyTools.HotReload.Editor.asmdef index 641c99af2..370e79149 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/UnityCLILoop.FirstPartyTools.HotReload.Editor.asmdef +++ b/Packages/src/Editor/FirstPartyTools/HotReload/UnityCLILoop.FirstPartyTools.HotReload.Editor.asmdef @@ -8,7 +8,8 @@ "GUID:0b83f8a313f54fdea6fcf945d7fd0615", "GUID:384dc39e13234d8aae3b1b1c44c9507e", "GUID:1e3293e9639242278dcb27724b34beb4", - "GUID:24d448abf7c10486d9578018b3a3c54b" + "GUID:24d448abf7c10486d9578018b3a3c54b", + "GUID:b6ba53c1f946045358f72c7981f7b425" ], "includePlatforms": [ "Editor"