From e205c9449b043ac4d3144b5db7a175255471c640 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 19:14:57 +0900 Subject: [PATCH 01/12] Move the compiled method identity, the call-site hit, and the caller-note candidate to top-level types The compiled-caller analysis is about to be gathered behind one entry point and then moved into its own assembly. Its contract types have to be nameable on their own for that, so they stop being nested in the scanner and the note enricher. No behaviour changes. --- .../HotReloadCallSiteScannerTests.cs | 86 +++++----- .../HotReload/HotReloadGroupProcessorTests.cs | 12 +- .../HotReloadOneShotCallerClosureTests.cs | 16 +- .../HotReloadOneShotCallerNoteBuilderTests.cs | 156 +++++++++--------- .../HotReload/HotReloadOrchestratorTests.cs | 8 +- ...HotReloadRunStaleSignatureWarningsTests.cs | 16 +- .../HotReloadSignatureChangeCoverageTests.cs | 22 +-- .../HotReload/HotReloadCallSiteHit.cs | 17 ++ .../HotReload/HotReloadCallSiteHit.cs.meta | 11 ++ .../HotReload/HotReloadCallSiteScanner.cs | 96 +++-------- .../HotReloadCompiledMethodIdentity.cs | 35 ++++ .../HotReloadCompiledMethodIdentity.cs.meta | 11 ++ .../HotReload/HotReloadFileEntryApplier.cs | 8 +- .../HotReload/HotReloadFileSinks.cs | 4 +- .../HotReloadOneShotCallerClosure.cs | 80 ++++----- .../HotReloadOneShotCallerNoteCandidate.cs | 20 +++ ...otReloadOneShotCallerNoteCandidate.cs.meta | 11 ++ .../HotReloadOneShotCallerNoteEnricher.cs | 42 ++--- .../HotReload/HotReloadRunAccumulator.cs | 6 +- .../HotReloadRunStaleSignatureWarnings.cs | 4 +- .../HotReloadSignatureChangeCoverage.cs | 26 +-- .../HotReload/HotReloadSignatureChangeGate.cs | 26 +-- .../HotReloadStaleSignatureCallSites.cs | 4 +- 23 files changed, 381 insertions(+), 336 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs.meta diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs index d33a27c78a..269507abaf 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(), @@ -748,16 +748,16 @@ public async Task ApplyOneShotCallerNotes_WithFreshCache_SharesOneLoadBudgetAcro 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( + run.OneShotCallerNoteCandidates.Add(new HotReloadOneShotCallerNoteCandidate( + new HotReloadCompiledMethodIdentity( GetTestAssemblyName(), new HotReloadMetadataTypeName(CrossAssemblyTargetTypeMetadataName), nameof(HotReloadCallSiteScannerCrossAssemblyTarget.Called), Array.Empty(), 0), targetOutcome)); - run.OneShotCallerNoteCandidates.Add(new HotReloadOneShotCallerNoteEnricher.Candidate( - new HotReloadCallSiteScanner.CompiledMethodIdentity( + run.OneShotCallerNoteCandidates.Add(new HotReloadOneShotCallerNoteCandidate( + new HotReloadCompiledMethodIdentity( CrossAssemblyCallerAssemblyName, new HotReloadMetadataTypeName(CrossAssemblyCallerTypeMetadataName), "Call", @@ -798,7 +798,7 @@ private static Task StopInstalledWarmUpAsync() installed.CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope)); } - private static List FindHits( + private static List FindHits( string typeMetadataName, string methodName, string[] parameterTypeFullNames, @@ -813,8 +813,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 +831,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/HotReloadGroupProcessorTests.cs b/Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs index c3bfc866ae..2455fe8df2 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/HotReloadOneShotCallerClosureTests.cs b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerClosureTests.cs index dcd5ba4602..09f0d8a99e 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/HotReloadOneShotCallerNoteBuilderTests.cs b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteBuilderTests.cs index 4d6b179874..66a2b38a67 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); @@ -167,8 +167,8 @@ public void ApplyNotes_MissingScanAssembly_SuppressesNotes() { HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateCandidate("Assembly.One", outcome) }; @@ -178,7 +178,7 @@ public void ApplyNotes_MissingScanAssembly_SuppressesNotes() outcomes, candidates, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List(), + new List(), new List { assemblyName })); Assert.That(outcomes[0].LifecycleNote, Is.Empty); @@ -193,12 +193,12 @@ public void ApplyNotes_UnreadScanAssembly_SuppressesNotes() { HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateCandidate("Assembly.One", outcome) }; - HotReloadCallSiteScanner.CallSiteHit hit = CreateHit(typeof(ValidLifecycleFixture), "Awake"); + HotReloadCallSiteHit hit = CreateHit(typeof(ValidLifecycleFixture), "Awake"); hit.TargetMethodKey = HotReloadMethodKeys.BuildMethodKeyParts("Type", "SetUp", Array.Empty(), 0); HotReloadOneShotCallerNoteEnricher.ApplyNotes( @@ -206,7 +206,7 @@ public void ApplyNotes_UnreadScanAssembly_SuppressesNotes() outcomes, candidates, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { hit }, + new List { hit }, new List(), 0, null, @@ -224,14 +224,14 @@ public void ApplyNotes_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 candidates = + new List { CreateCandidate("Assembly.One", first), CreateCandidate("Assembly.Two", second) }; - List calls = - new List(); + List calls = + new List(); HotReloadOneShotCallerNoteEnricher.ApplyNotes( "project", @@ -241,7 +241,7 @@ public void ApplyNotes_DifferentTargetAssemblies_ScansEachAssemblySeparately() { calls.Add(identities); return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List(), + new List(), new List()); }); @@ -263,8 +263,8 @@ public void ApplyNotes_WorkerLifecycleNoteCandidate_SkipsScanAndKeepsNote() "Assets/Test.cs", workerNote); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateCandidate("Assembly.One", outcome) }; @@ -278,7 +278,7 @@ public void ApplyNotes_WorkerLifecycleNoteCandidate_SkipsScanAndKeepsNote() { scanCount++; return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List(), + new List(), new List()); }); @@ -294,12 +294,12 @@ public void ApplyNotes_OnlyValidAwakeCaller_ReplacesOutcomeWithIndirectLifecycle { HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + 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( @@ -307,7 +307,7 @@ public void ApplyNotes_OnlyValidAwakeCaller_ReplacesOutcomeWithIndirectLifecycle outcomes, candidates, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { hit }, + new List { hit }, new List())); Assert.That( @@ -328,12 +328,12 @@ public void ApplyNotes_FunctionPointerLoadHit_SuppressesIndirectLifecycleNote() { HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + 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()"; hit.IsFunctionPointerLoad = true; @@ -342,7 +342,7 @@ public void ApplyNotes_FunctionPointerLoadHit_SuppressesIndirectLifecycleNote() outcomes, candidates, (assemblyName, identities) => new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List { hit }, + new List { hit }, new List())); Assert.That(outcomes[0].LifecycleNote, Is.Empty); @@ -356,8 +356,8 @@ public void ApplyNotes_CompiledAwakeOnlyCaller_AddsIndirectLifecycleNote() { HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("OneShotCallerScannerFixture.AwakeOnlyTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerFixtureCandidate("AwakeOnlyTarget", outcome) }; @@ -388,8 +388,8 @@ public void ApplyNotes_CompiledOrdinaryCaller_SuppressesIndirectLifecycleNote() { HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("OneShotCallerScannerFixture.MixedTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerFixtureCandidate("MixedTarget", outcome) }; @@ -414,8 +414,8 @@ public void ApplyNotes_CompiledChainedAwakeOnlyCaller_AddsIndirectLifecycleNote( "OneShotCallerScannerFixture.ChainedAwakeOnlyTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerFixtureCandidate("ChainedAwakeOnlyTarget", outcome) }; @@ -448,8 +448,8 @@ public void ApplyNotes_CompiledNonMonoBehaviourChain_AddsIndirectLifecycleNote() "OneShotCallerChainHelper.ConfigureTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerCandidateForType(typeof(OneShotCallerChainHelper), "ConfigureTarget", outcome) }; @@ -482,8 +482,8 @@ public void ApplyNotes_CompiledMixedChain_SuppressesIndirectLifecycleNote() "OneShotCallerScannerFixture.MixedChainTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerFixtureCandidate("MixedChainTarget", outcome) }; @@ -508,8 +508,8 @@ public void ApplyNotes_CompiledDelegateChain_SuppressesIndirectLifecycleNote() "OneShotCallerScannerFixture.DelegateChainTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerFixtureCandidate("DelegateChainTarget", outcome) }; @@ -534,8 +534,8 @@ public void ApplyNotes_CompiledDeadEndChain_SuppressesIndirectLifecycleNote() "OneShotCallerScannerFixture.DeadEndTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerFixtureCandidate("DeadEndTarget", outcome) }; @@ -560,8 +560,8 @@ public void ApplyNotes_CompiledDeepChain_SuppressesIndirectLifecycleNote() "OneShotCallerScannerFixture.DeepTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerFixtureCandidate("DeepTarget", outcome) }; @@ -585,8 +585,8 @@ public void ApplyNotes_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 candidates = + new List { CreateCandidate(assemblyName, outcome) }; @@ -602,17 +602,17 @@ 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", @@ -627,12 +627,12 @@ public void ApplyNotes_CycleThroughIntermediate_AddsIndirectLifecycleNote() 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()); }); @@ -655,8 +655,8 @@ public void ApplyNotes_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 candidates = + new List { CreateCandidate(assemblyName, outcome) }; @@ -667,7 +667,7 @@ public void ApplyNotes_MissingScanAssemblyAtDepthTwo_SuppressesNotes() "SetUp", Array.Empty(), 0); - HotReloadCallSiteScanner.CallSiteHit midToTarget = CreateKeyedHit( + HotReloadCallSiteHit midToTarget = CreateKeyedHit( assemblyName, intermediateType, intermediateName, @@ -682,12 +682,12 @@ public void ApplyNotes_MissingScanAssemblyAtDepthTwo_SuppressesNotes() 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()); }); @@ -703,8 +703,8 @@ public void ApplyNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEa HotReloadMethodOutcome outcome = HotReloadMethodOutcome.Patched("Type.SetUp", "Assets/Test.cs"); List outcomes = new List { outcome }; const string targetAssembly = "TargetAsm"; - List candidates = - new List + List candidates = + new List { CreateCandidate(targetAssembly, outcome) }; @@ -722,18 +722,18 @@ 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", @@ -745,7 +745,7 @@ public void ApplyNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEa if (scannedAssembly == assemblyX) { return new HotReloadCallSiteScanner.HotReloadCallSiteScanResult( - new List + new List { CreateKeyedHit( typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name, @@ -759,12 +759,12 @@ 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()); }); @@ -789,8 +789,8 @@ public void ApplyNotes_CompiledDelegateAssignment_SuppressesIndirectLifecycleNot "OneShotCallerScannerFixture.DelegateAssignedTarget()", "Assets/Test.cs"); List outcomes = new List { outcome }; - List candidates = - new List + List candidates = + new List { CreateScannerFixtureCandidate("DelegateAssignedTarget", outcome) }; @@ -947,14 +947,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,28 +964,28 @@ private static HotReloadCallSiteScanner.CallSiteHit CreateHit(string typeMetadat }; } - private static HotReloadOneShotCallerNoteEnricher.Candidate CreateCandidate( + private static HotReloadOneShotCallerNoteCandidate CreateCandidate( string assemblyName, HotReloadMethodOutcome outcome) { - 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 HotReloadOneShotCallerNoteCandidate(identity, outcome); } - private static HotReloadOneShotCallerNoteEnricher.Candidate CreateScannerFixtureCandidate( + private static HotReloadOneShotCallerNoteCandidate CreateScannerFixtureCandidate( string methodName, HotReloadMethodOutcome outcome) { return CreateScannerCandidateForType(typeof(OneShotCallerScannerFixture), methodName, outcome); } - private static HotReloadOneShotCallerNoteEnricher.Candidate CreateScannerCandidateForType( + private static HotReloadOneShotCallerNoteCandidate CreateScannerCandidateForType( Type type, string methodName, HotReloadMethodOutcome outcome) @@ -993,23 +993,23 @@ private static HotReloadOneShotCallerNoteEnricher.Candidate CreateScannerCandida 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 HotReloadOneShotCallerNoteCandidate(identity, outcome); } - 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 4cd0c17fe9..3c6c1987f6 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/HotReloadRunStaleSignatureWarningsTests.cs b/Assets/Tests/Editor/HotReload/HotReloadRunStaleSignatureWarningsTests.cs index ae106ab652..cb5c0af0d2 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/HotReloadSignatureChangeCoverageTests.cs b/Assets/Tests/Editor/HotReload/HotReloadSignatureChangeCoverageTests.cs index 5bb0bb37ec..19ab7eacf3 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/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs new file mode 100644 index 0000000000..0a0dce1d3d --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs @@ -0,0 +1,17 @@ +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// One compiled instruction that references a target method, reported under its logical owner. + /// + internal sealed class HotReloadCallSiteHit + { + public string CallerAssemblyName; + public HotReloadMetadataTypeName CallerTypeMetadataName; + public string CallerMethodName; + public string[] CallerParameterTypeFullNames; + public int CallerGenericArity; + public string CallerMethodKey; + public string TargetMethodKey; + public bool IsFunctionPointerLoad; + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs.meta new file mode 100644 index 0000000000..8595dd23d2 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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/HotReloadCallSiteScanner.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs index 111287f1d5..62fa7544e1 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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/HotReloadCompiledMethodIdentity.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs new file mode 100644 index 0000000000..65876c820f --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs @@ -0,0 +1,35 @@ +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Identity of a compiled method to search for (assembly + type + name + arity + parameter types). + /// + internal readonly struct HotReloadCompiledMethodIdentity + { + public readonly string AssemblyName; + public readonly HotReloadMetadataTypeName TypeMetadataName; + public readonly string MethodName; + public readonly string[] ParameterTypeFullNames; + public 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/HotReloadCompiledMethodIdentity.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs.meta new file mode 100644 index 0000000000..88930c7d5b --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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/HotReloadFileEntryApplier.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadFileEntryApplier.cs index 84a47f9193..88344e4f5d 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 7e227795a6..9e925dafae 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/HotReloadOneShotCallerClosure.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs index f980395a8d..055fa86412 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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/HotReloadOneShotCallerNoteCandidate.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteCandidate.cs new file mode 100644 index 0000000000..dd3f2ff17b --- /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 0000000000..bc378b1872 --- /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/HotReloadOneShotCallerNoteEnricher.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs index e10fa3dc18..1dc9ba1786 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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)) { @@ -87,30 +73,30 @@ internal static bool IsOneShotLifecycleCaller(HotReloadCallSiteScanner.CallSiteH internal static void ApplyNotes( string projectRoot, List outcomes, - IReadOnlyList candidates, - Func scan) + IReadOnlyList candidates, + Func scan) { - Dictionary> candidatesByAssembly = - new Dictionary>(StringComparer.Ordinal); - foreach (Candidate candidate in candidates) + Dictionary> candidatesByAssembly = + new Dictionary>(StringComparer.Ordinal); + foreach (HotReloadOneShotCallerNoteCandidate candidate in candidates) { if (!string.IsNullOrEmpty(candidate.Outcome.LifecycleNote)) { continue; } - if (!candidatesByAssembly.TryGetValue(candidate.Identity.AssemblyName, out List group)) + if (!candidatesByAssembly.TryGetValue(candidate.Identity.AssemblyName, out List group)) { - group = new List(); + group = new List(); candidatesByAssembly.Add(candidate.Identity.AssemblyName, group); } group.Add(candidate); } - foreach (KeyValuePair> pair in candidatesByAssembly) + foreach (KeyValuePair> pair in candidatesByAssembly) { - HotReloadCallSiteScanner.CompiledMethodIdentity[] identities = pair.Value.ConvertAll( + HotReloadCompiledMethodIdentity[] identities = pair.Value.ConvertAll( candidate => candidate.Identity).ToArray(); HotReloadCallSiteScanner.HotReloadCallSiteScanResult result = scan(pair.Key, identities); if (result.IsIncomplete) @@ -118,16 +104,16 @@ internal static void ApplyNotes( continue; } - foreach (Candidate candidate in pair.Value) + foreach (HotReloadOneShotCallerNoteCandidate candidate in pair.Value) { 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) + List targetHits = + new List(); + foreach (HotReloadCallSiteHit hit in result.Hits) { if (hit.TargetMethodKey != targetKey) { diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs index f1901b0ea3..661d7aafee 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. diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunStaleSignatureWarnings.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunStaleSignatureWarnings.cs index cbf53adaad..aa66676f5a 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/HotReloadSignatureChangeCoverage.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeCoverage.cs index def9dd4a9a..02886879c6 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 012660997a..0311b2a682 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,7 +42,7 @@ internal static async Task TryApplySignatureChangeGat context.WorkerOutput.unchangedMethods ?? Array.Empty(), context.WorkerOutput.skipped ?? Array.Empty(), removedSignatures); - List hits = + List hits = HotReloadCallSiteScanner.FindCallSites(context.ProjectRoot, targets).Hits; Dictionary> uncoveredCallersByTarget = CollectInitialUncoveredCallers(context.AssemblyName, entries, hits, deletedCallerExemptions); @@ -145,7 +145,7 @@ private static List CollectReplacementEntries( internal static Dictionary> CollectInitialUncoveredCallers( string assemblyName, TransformWorkerEntryDto[] entries, - IReadOnlyList hits, + IReadOnlyList hits, IReadOnlyCollection deletedCallerExemptions) { HashSet coveredIdentities = @@ -158,13 +158,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 +194,7 @@ private static HotReloadCallSiteScanner.CompiledMethodIdentity[] CollectScanTarg } private static void TryAddScanTarget( - List targets, + List targets, HashSet seenKeys, string assemblyName, string typeMetadataName, @@ -213,7 +213,7 @@ private static void TryAddScanTarget( } targets.Add( - new HotReloadCallSiteScanner.CompiledMethodIdentity( + new HotReloadCompiledMethodIdentity( assemblyName, new HotReloadMetadataTypeName(typeMetadataName), methodName, @@ -354,7 +354,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 +368,7 @@ private SignatureChangeGateResult( HotReloadShimIsolation.HotReloadShimIsolationResult isolation, List skippedOutcomes, List staleSignatureCallSites, - List hits, + List hits, HashSet deletedCallerExemptions, List gatedReplacementMethodKeys) { @@ -378,7 +378,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 +393,7 @@ public static SignatureChangeGateResult NoWork() public static SignatureChangeGateResult WarningsOnly( List staleSignatureCallSites, - List hits, + List hits, HashSet deletedCallerExemptions) { return new SignatureChangeGateResult( @@ -424,7 +424,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 8a91bb6119..9d8772824f 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; } } } From e90bfe552e4c888997b03c199fe9f16ce7d56d4c Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 19:39:16 +0900 Subject: [PATCH 02/12] Split building one-shot caller notes from attaching them to the run's outcomes The note enricher both built the notes and wrote them back into the run's outcome list, so it had to know HotReloadMethodOutcome, a type of the hot reload application assembly. BuildNotes now takes requests (identity and method name) and returns one note or null per request; a new attacher on the application side decides which outcomes ask for a note and replaces the ones that got one. That keeps the analysis free of outcome types before it moves into its own assembly. The scan order, the budget and the first-note-wins write-back are unchanged; the worker-note test moved to the attacher's tests. --- ...HotReloadOneShotCallerNoteAttacherTests.cs | 118 ++++++ ...loadOneShotCallerNoteAttacherTests.cs.meta | 11 + .../HotReloadOneShotCallerNoteBuilderTests.cs | 382 ++++++------------ .../HotReloadOneShotCallerNoteAttacher.cs | 58 +++ ...HotReloadOneShotCallerNoteAttacher.cs.meta | 11 + .../HotReloadOneShotCallerNoteEnricher.cs | 64 ++- .../HotReloadOneShotCallerNoteRequest.cs | 23 ++ .../HotReloadOneShotCallerNoteRequest.cs.meta | 11 + .../HotReload/HotReloadRunAccumulator.cs | 7 +- 9 files changed, 398 insertions(+), 287 deletions(-) create mode 100644 Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs create mode 100644 Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs.meta diff --git a/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteAttacherTests.cs new file mode 100644 index 0000000000..526cdf35ed --- /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 0000000000..51dbccba3c --- /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 66a2b38a67..eb8841ed2d 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteBuilderTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadOneShotCallerNoteBuilderTests.cs @@ -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 { assemblyName })); - Assert.That(outcomes[0].LifecycleNote, Is.Empty); + Assert.That(notes[0], Is.Null); } /// @@ -189,22 +185,18 @@ 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") }; 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(), @@ -212,31 +204,26 @@ public void ApplyNotes_UnreadScanAssembly_SuppressesNotes() 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(); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (assemblyName, identities) => { calls.Add(identities); @@ -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) - }; - 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) + CreateRequest(typeof(HotReloadOneShotCallerNoteBuilderTests).Assembly.GetName().Name, "Type.SetUp") }; 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())); 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") }; 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())); - 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"; @@ -618,10 +516,8 @@ public void ApplyNotes_CycleThroughIntermediate_AddsIndirectLifecycleNote() "SetUp", intermediateKey); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (scannedAssembly, identities) => { if (identities.Length == 1 && identities[0].MethodName == intermediateName) @@ -637,7 +533,7 @@ public void ApplyNotes_CycleThroughIntermediate_AddsIndirectLifecycleNote() }); 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"; @@ -673,10 +567,8 @@ public void ApplyNotes_MissingScanAssemblyAtDepthTwo_SuppressesNotes() intermediateName, targetKey); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (scannedAssembly, identities) => { if (identities.Length == 1 && identities[0].MethodName == intermediateName) @@ -691,22 +583,20 @@ public void ApplyNotes_MissingScanAssemblyAtDepthTwo_SuppressesNotes() 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"; @@ -735,10 +625,8 @@ public void ApplyNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEa List calls = new List(); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - "project", - outcomes, - candidates, + string[] notes = HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, (scannedAssembly, identities) => { calls.Add(identities); @@ -768,7 +656,7 @@ public void ApplyNotes_SameCallerKeyDifferentAssemblies_SuppressesNoteAndScansEa 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); } /// @@ -964,9 +846,9 @@ private static HotReloadCallSiteHit CreateHit(string typeMetadataName, string me }; } - private static HotReloadOneShotCallerNoteCandidate CreateCandidate( + private static HotReloadOneShotCallerNoteRequest CreateRequest( string assemblyName, - HotReloadMethodOutcome outcome) + string method) { HotReloadCompiledMethodIdentity identity = new HotReloadCompiledMethodIdentity( @@ -975,20 +857,20 @@ private static HotReloadOneShotCallerNoteCandidate CreateCandidate( "SetUp", Array.Empty(), 0); - return new HotReloadOneShotCallerNoteCandidate(identity, outcome); + return new HotReloadOneShotCallerNoteRequest(identity, method); } - private static HotReloadOneShotCallerNoteCandidate 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 HotReloadOneShotCallerNoteCandidate CreateScannerCandidateForType( + private static HotReloadOneShotCallerNoteRequest CreateScannerRequestForType( Type type, string methodName, - HotReloadMethodOutcome outcome) + string method) { string rawAssemblyName = CompilationPipeline.GetAssemblyNameFromScriptPath( "Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerFixture.cs"); @@ -1000,7 +882,7 @@ private static HotReloadOneShotCallerNoteCandidate CreateScannerCandidateForType methodName, Array.Empty(), 0); - return new HotReloadOneShotCallerNoteCandidate(identity, outcome); + return new HotReloadOneShotCallerNoteRequest(identity, method); } private static HotReloadCallSiteHit CreateKeyedHit( diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteAttacher.cs new file mode 100644 index 0000000000..f968ea9214 --- /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 0000000000..912cde48ca --- /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/HotReloadOneShotCallerNoteEnricher.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs index 1dc9ba1786..57813b9b11 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs @@ -70,47 +70,51 @@ internal static bool IsOneShotLifecycleCaller(HotReloadCallSiteHit hit) return false; } - internal static void ApplyNotes( - string projectRoot, - List outcomes, - IReadOnlyList candidates, + /// + /// 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 (HotReloadOneShotCallerNoteCandidate 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) { HotReloadCompiledMethodIdentity[] identities = pair.Value.ConvertAll( - candidate => candidate.Identity).ToArray(); + index => requests[index].Identity).ToArray(); HotReloadCallSiteScanner.HotReloadCallSiteScanResult result = scan(pair.Key, identities); if (result.IsIncomplete) { continue; } - foreach (HotReloadOneShotCallerNoteCandidate 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); + request.Identity.TypeMetadataName.Value, + request.Identity.MethodName, + request.Identity.ParameterTypeFullNames, + request.Identity.GenericArity); List targetHits = new List(); foreach (HotReloadCallSiteHit hit in result.Hits) @@ -132,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/HotReloadOneShotCallerNoteRequest.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs new file mode 100644 index 0000000000..b58b3316e2 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs @@ -0,0 +1,23 @@ +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. + /// + internal 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; + } + + public HotReloadCompiledMethodIdentity Identity { get; } + + public string Method { get; } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs.meta new file mode 100644 index 0000000000..fd45eb9c47 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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/HotReloadRunAccumulator.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs index 661d7aafee..fa5d733ffb 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs @@ -231,11 +231,12 @@ public IReadOnlyList ApplyOneShotCallerNotes(string projectRoot, string // however many candidate assemblies and levels it visits. HotReloadCallSiteLoadBudget budget = new HotReloadCallSiteLoadBudget(HotReloadConstants.CallerNoteUncachedDllLoadBudget); - HotReloadOneShotCallerNoteEnricher.ApplyNotes( - projectRoot, + HotReloadOneShotCallerNoteAttacher.Attach( _outcomes, _oneShotCallerNoteCandidates, - (ignoredAssemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities, budget)); + requests => HotReloadOneShotCallerNoteEnricher.BuildNotes( + requests, + (ignoredAssemblyName, identities) => HotReloadCallSiteScanner.FindCallSites(projectRoot, identities, budget))); if (budget.RefusedAssemblyNames.Count > 0) { HotReloadOrchestratorLog.LogHotReloadCallerNoteLoadBudgetExhausted( From de304050c9faa83c1af6fdff16239c32dcd173d3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 20:24:31 +0900 Subject: [PATCH 03/12] Route hot reload's compiled-caller analysis through one entry point that stops background reads before a run holds the cache The orchestrator, the composition root, the warm-up items, the Editor hooks, the signature-change gate and the run accumulator each reached into the scanner, the shared call-site cache and the backfill directly, so the order a run starts in was theirs to get right, and it was wrong: the orchestrator took the cache hold first, so a run that came during a background read waited for the cache lock and that wait landed in no timing step. HotReloadCompiledCallers is now the one entry point and HotReloadCompiledCallersRun the scope of one run. BeginRunAsync yields to the background read before it takes the hold, and the orchestrator calls it inside warm_up_yield after yielding to the warm-up, so the wait is measured. End releases the hold and then starts the background reads of the dlls the note budget refused; Dispose releases the hold only. The note budget and its log moved into the run scope. Nothing else changes. --- .../HotReloadCallSiteScannerTests.cs | 54 +++---- .../HotReloadCallSiteWarmUpItemTests.cs | 18 +-- .../HotReloadCallerNoteBackfillE2ETests.cs | 2 +- .../HotReloadCompositionRootTests.cs | 4 +- .../HotReload/HotReloadDomainTestScope.cs | 6 +- .../HotReloadEditorTickPumpTestSafety.cs | 2 +- .../HotReloadIntroducedTypeE2ETestBase.cs | 3 +- .../HotReloadPatcherContractTests.cs | 2 +- .../Editor/HotReload/HotReloadPatcherTests.cs | 2 +- ...eloadReferencedMethodSetWarmUpItemTests.cs | 24 +-- .../HotReload/HotReloadServicesTestScope.cs | 4 +- .../HotReload/HotReloadWarmUpE2ETests.cs | 2 +- .../HotReload/HotReloadWarmUpItemsTests.cs | 2 +- .../HotReload/HotReloadCallSiteWarmUpItem.cs | 32 ++-- .../HotReloadCompiledCallSiteCache.cs | 14 ++ .../HotReload/HotReloadCompiledCallers.cs | 139 ++++++++++++++++++ .../HotReloadCompiledCallers.cs.meta | 11 ++ .../HotReload/HotReloadCompiledCallersRun.cs | 122 +++++++++++++++ .../HotReloadCompiledCallersRun.cs.meta | 11 ++ .../HotReload/HotReloadCompositionRoot.cs | 50 +++---- .../HotReloadGroupStageCollaborators.cs | 7 +- .../HotReload/HotReloadOrchestrator.cs | 48 +++--- .../HotReload/HotReloadOrchestratorLog.cs | 16 -- .../HotReloadReferencedMethodSetWarmUpItem.cs | 32 ++-- .../HotReload/HotReloadRunAccumulator.cs | 34 +---- .../HotReload/HotReloadServices.cs | 19 +-- .../HotReload/HotReloadSignatureChangeGate.cs | 3 +- .../HotReload/HotReloadWarmUpEditorHooks.cs | 10 +- .../HotReload/HotReloadWarmUpItems.cs | 6 +- 29 files changed, 450 insertions(+), 229 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs.meta diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs index 269507abaf..20915415e4 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs @@ -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 HotReloadOneShotCallerNoteCandidate( - new HotReloadCompiledMethodIdentity( - GetTestAssemblyName(), - new HotReloadMetadataTypeName(CrossAssemblyTargetTypeMetadataName), - nameof(HotReloadCallSiteScannerCrossAssemblyTarget.Called), - Array.Empty(), - 0), - targetOutcome)); - run.OneShotCallerNoteCandidates.Add(new HotReloadOneShotCallerNoteCandidate( - new HotReloadCompiledMethodIdentity( - 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,7 +797,7 @@ 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( diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallSiteWarmUpItemTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallSiteWarmUpItemTests.cs index 5d4617b0df..3c90a1a761 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 860fa2c7de..7074081a36 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs @@ -79,7 +79,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), diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCompositionRootTests.cs index b631a411fd..73c2a6fe6e 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 711fd72ac4..01338f55fe 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 83b70e68e9..64abbb19c9 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/HotReloadIntroducedTypeE2ETestBase.cs b/Assets/Tests/Editor/HotReload/HotReloadIntroducedTypeE2ETestBase.cs index e96a7887d4..1f4d373775 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/HotReloadPatcherContractTests.cs b/Assets/Tests/Editor/HotReload/HotReloadPatcherContractTests.cs index d4e165808c..18dd89378f 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 836596c1b0..3f9b226e69 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 e0b14f9c37..568578ae57 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadReferencedMethodSetWarmUpItemTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadReferencedMethodSetWarmUpItemTests.cs @@ -44,18 +44,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,17 +61,10 @@ public void PreloadSets_ReadsEachReferencingDllOnce_AndAgainReadsNothing() public void PreloadSets_WhenCancelled_ThrowsAndReadsNothing() { HotReloadReferencedMethodIndex index = new HotReloadReferencedMethodIndex(_persistenceDirectory); - HotReloadWarmUpTarget[] targets = - { - new HotReloadWarmUpTarget( - TestAssemblyName, - DllPath(TestAssemblyName), - Path.ChangeExtension(DllPath(TestAssemblyName), ".pdb"), - new[] { DllPath(CrossAssemblyCallerAssemblyName) }) - }; + string[] dllPaths = { DllPath(CrossAssemblyCallerAssemblyName) }; Assert.Throws( - () => HotReloadReferencedMethodSetWarmUpItem.PreloadSets(targets, index, new CancellationToken(true))); + () => HotReloadCompiledCallers.PreloadReferencedMethodSets(dllPaths, index, new CancellationToken(true))); Assert.That(index.LoadCount, Is.EqualTo(0), "reads"); } diff --git a/Assets/Tests/Editor/HotReload/HotReloadServicesTestScope.cs b/Assets/Tests/Editor/HotReload/HotReloadServicesTestScope.cs index 15ff22efdd..29e0b3a1e5 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/HotReloadWarmUpE2ETests.cs b/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs index 6b6807f47b..d2f634d261 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. diff --git a/Assets/Tests/Editor/HotReload/HotReloadWarmUpItemsTests.cs b/Assets/Tests/Editor/HotReload/HotReloadWarmUpItemsTests.cs index df2ca77bd4..f37a279a4e 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/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteWarmUpItem.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteWarmUpItem.cs index 6cc8610190..eb7bc0b671 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/HotReloadCompiledCallSiteCache.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteCache.cs index 4e0db98dc6..df029e75da 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteCache.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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/HotReloadCompiledCallers.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs new file mode 100644 index 0000000000..05375c5bd8 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs @@ -0,0 +1,139 @@ +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 . + /// + internal 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. + /// + 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/HotReloadCompiledCallers.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs.meta new file mode 100644 index 0000000000..c0ce63bbce --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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/HotReloadCompiledCallersRun.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs new file mode 100644 index 0000000000..0d2083720b --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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. + /// + internal 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/HotReloadCompiledCallersRun.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs.meta new file mode 100644 index 0000000000..48515fbff6 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/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/HotReloadCompositionRoot.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs index 18d949f071..6828ece287 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/HotReloadGroupStageCollaborators.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadGroupStageCollaborators.cs index f4390284bd..a9c5e9aae1 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/HotReloadOrchestrator.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOrchestrator.cs index 84c5a6b48f..97b64015d0 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 a9cfa079db..25ec908d73 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 ad7c62d178..ccb6095811 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,25 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// internal sealed class HotReloadReferencedMethodSetWarmUpItem : IHotReloadWarmUpItem { - public string Name => "referenced_method_sets"; + private readonly HotReloadCompiledCallers _compiledCallers; - public Task RunAsync(HotReloadWarmUpContext context, CancellationToken ct) + internal HotReloadReferencedMethodSetWarmUpItem(HotReloadCompiledCallers compiledCallers) { - // 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)); + Debug.Assert(compiledCallers != null, "compiledCallers must not be null."); + _compiledCallers = compiledCallers; } - /// Preloads each referencing dll's set; throws before the next dll once cancelled. - internal static void PreloadSets( - IReadOnlyList targets, - HotReloadReferencedMethodIndex index, - CancellationToken ct) + public string Name => "referenced_method_sets"; + + public Task RunAsync(HotReloadWarmUpContext context, CancellationToken ct) { - foreach (HotReloadWarmUpTarget target in targets) + List dllPaths = new List(); + foreach (HotReloadWarmUpTarget target in context.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 _compiledCallers.WarmUpReferencedMethodSetsAsync(dllPaths, ct); } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs index fa5d733ffb..c4dff954ce 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadRunAccumulator.cs @@ -216,36 +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); - HotReloadOneShotCallerNoteAttacher.Attach( - _outcomes, - _oneShotCallerNoteCandidates, - requests => HotReloadOneShotCallerNoteEnricher.BuildNotes( - requests, - (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/HotReloadServices.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadServices.cs index 80120dcf9d..61c2479f68 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/HotReloadSignatureChangeGate.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs index 0311b2a682..b3e614afa9 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadSignatureChangeGate.cs @@ -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); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpEditorHooks.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadWarmUpEditorHooks.cs index bffa616ff4..5e67dc9f5e 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 4aa3a63c0d..fab33596ce 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() }; From 5f0ba2800ae826b0ea73f794864adf76fcace1a6 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 20:32:58 +0900 Subject: [PATCH 04/12] Pin when a compiled-caller run takes and releases the cache hold, counts its wait, and starts the background reads The entry point's tests use a cache and a loader of their own: a run begins only after the background read in flight ended, End releases the hold before the refused dlls are read, and a run disposed without End reads none of them. Two orchestrator tests pin what the entry point's tests cannot see: the warm-up yield comes before the hold, and the wait for a background read is counted in warm_up_yield. --- .../HotReloadCallerNoteBackfillE2ETests.cs | 50 ++++++++ .../HotReloadCompiledCallersTests.cs | 108 ++++++++++++++++++ .../HotReloadCompiledCallersTests.cs.meta | 11 ++ .../HotReload/HotReloadWarmUpE2ETests.cs | 31 +++++ 4 files changed, 200 insertions(+) create mode 100644 Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs create mode 100644 Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs.meta diff --git a/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs b/Assets/Tests/Editor/HotReload/HotReloadCallerNoteBackfillE2ETests.cs index 7074081a36..04e547bf95 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; @@ -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 0000000000..3add8991fd --- /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 0000000000..9121c702fb --- /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/HotReloadWarmUpE2ETests.cs b/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs index d2f634d261..653456f21e 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs @@ -181,6 +181,37 @@ 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; + try + { + warmUp.Start(); + run = RunWithReadReturningAsync(6); + + Assert.That(run.IsCompleted, Is.False, "Precondition: the run must be waiting for the item."); + Assert.That( + HotReloadCompiledCallSiteCache.Shared.HoldDepth, + Is.EqualTo(holdDepthBefore), + "cache hold taken while the item is in flight"); + } + finally + { + pending.Release.TrySetResult(true); + } + + await run; + } + /// /// What: a run puts the edited assembly first in the project's ledger and keeps the names /// recorded before it. From 884fa75334f5ca8f53b0259869b51aed099c00f3 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 20:50:21 +0900 Subject: [PATCH 05/12] Pin that the referenced-method set warm-up reads each target's referencing dlls and not its own When the item started handing a flat list to the entry point, the test that used to pass targets moved to the flat list too, so nothing caught the item reading a target's own dll instead of its referencing ones. The flattening is now a method of its own with a test that sees the mix-up. --- ...eloadReferencedMethodSetWarmUpItemTests.cs | 27 +++++++++++++++++++ .../HotReloadReferencedMethodSetWarmUpItem.cs | 11 ++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Assets/Tests/Editor/HotReload/HotReloadReferencedMethodSetWarmUpItemTests.cs b/Assets/Tests/Editor/HotReload/HotReloadReferencedMethodSetWarmUpItemTests.cs index 568578ae57..508a0e83a3 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; @@ -69,6 +70,32 @@ public void PreloadSets_WhenCancelled_ThrowsAndReadsNothing() 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( + "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") }) + }; + + List dllPaths = HotReloadReferencedMethodSetWarmUpItem.CollectDllPaths(targets); + + Assert.That(dllPaths, Is.EqualTo(new[] { DllPath("B"), DllPath("C"), DllPath("E") })); + } + private static string DllPath(string assemblyName) { return Path.Combine( diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodSetWarmUpItem.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodSetWarmUpItem.cs index ccb6095811..4a294b9f4e 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodSetWarmUpItem.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadReferencedMethodSetWarmUpItem.cs @@ -23,14 +23,21 @@ internal HotReloadReferencedMethodSetWarmUpItem(HotReloadCompiledCallers compile public string Name => "referenced_method_sets"; public Task RunAsync(HotReloadWarmUpContext context, CancellationToken ct) + { + List dllPaths = CollectDllPaths(context.Targets); + return _compiledCallers.WarmUpReferencedMethodSetsAsync(dllPaths, 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 context.Targets) + foreach (HotReloadWarmUpTarget target in targets) { dllPaths.AddRange(target.ReferencingDllPaths); } - return _compiledCallers.WarmUpReferencedMethodSetsAsync(dllPaths, ct); + return dllPaths; } } } From d44613b4224d5bf5d53ec2ac57cb6dc8294461ea Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 20:37:05 +0900 Subject: [PATCH 06/12] Let the compiled-caller analysis assembly see the hot-reload shared kernel and the script path helpers The compiled-caller analysis is about to move into its own assembly. Its entry point hands out call-site hits and method identities whose fields are metadata type names, so those two name types become public; the rest of the shared kernel and the script path helpers stay internal and are opened to the new assembly by name. --- .../FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs | 1 + .../Editor/FirstPartyTools/HotReload/Shared/AssemblyInfo.cs | 3 +++ .../FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs | 6 ++++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs b/Packages/src/Editor/FirstPartyTools/Common/ScriptPath/AssemblyInfo.cs index ee1a792cc6..f43d5dc7b6 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/Shared/AssemblyInfo.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/AssemblyInfo.cs index 1cadc4918c..b56310d3af 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 e5d0f5b464..7c828b51ab 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,9 @@ public override string ToString() /// /// A type name in reflection form, the spelling Type.FullName produces, where a nested type /// reads Outer+Inner. + /// Public because the compiled-caller analysis exposes it in the types of its entry point. /// - internal readonly struct HotReloadReflectionTypeName : IEquatable + public readonly struct HotReloadReflectionTypeName : IEquatable { public string Value { get; } From e3edb8e91e66d687da7fd1147a5f87fad80f2818 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 20:39:05 +0900 Subject: [PATCH 07/12] Add the compiled-caller analysis assembly and reference it from hot reload and its tests The assembly is empty for now so the asmdef, its references and the meta files land on their own before the analysis moves in. Only the hot-reload tests see its internals; the application assembly will reach it through the entry point. --- ...UnityCLILoop.Tests.Editor.HotReload.asmdef | 3 ++- .../HotReload/CompiledCallers.meta | 8 +++++++ .../HotReload/CompiledCallers/AssemblyInfo.cs | 5 +++++ .../CompiledCallers/AssemblyInfo.cs.meta | 11 ++++++++++ ...ls.HotReload.CompiledCallers.Editor.asmdef | 22 +++++++++++++++++++ ...tReload.CompiledCallers.Editor.asmdef.meta | 7 ++++++ ...op.FirstPartyTools.HotReload.Editor.asmdef | 3 ++- 7 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers.meta create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/AssemblyInfo.cs create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/AssemblyInfo.cs.meta create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor.asmdef create mode 100644 Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/UnityCLILoop.FirstPartyTools.HotReload.CompiledCallers.Editor.asmdef.meta diff --git a/Assets/Tests/Editor/HotReload/UnityCLILoop.Tests.Editor.HotReload.asmdef b/Assets/Tests/Editor/HotReload/UnityCLILoop.Tests.Editor.HotReload.asmdef index 8d6e832462..8fc81a1e48 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/HotReload/CompiledCallers.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers.meta new file mode 100644 index 0000000000..3d407b4535 --- /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 0000000000..c41f708fb7 --- /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 0000000000..4e286f3dd2 --- /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/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 0000000000..c8892dcb16 --- /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 0000000000..8901d9e05e --- /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/UnityCLILoop.FirstPartyTools.HotReload.Editor.asmdef b/Packages/src/Editor/FirstPartyTools/HotReload/UnityCLILoop.FirstPartyTools.HotReload.Editor.asmdef index 641c99af21..370e791491 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" From 2f94829b3059a3ae9aed89bb281d21b724a2f656 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 20:39:22 +0900 Subject: [PATCH 08/12] Say why the reflection type name is public: the metadata type name returns it --- .../FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs index 7c828b51ab..59dc1ae29b 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadTypeNames.cs @@ -88,7 +88,8 @@ public override string ToString() /// /// A type name in reflection form, the spelling Type.FullName produces, where a nested type /// reads Outer+Inner. - /// Public because the compiled-caller analysis exposes it in the types of its entry point. + /// Public because HotReloadMetadataTypeName, which the compiled-caller analysis exposes, + /// returns it from ToReflectionName. /// public readonly struct HotReloadReflectionTypeName : IEquatable { From 6ecb83de020054938880e101d2b04ffd2c7fd500 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 20:41:51 +0900 Subject: [PATCH 09/12] Move hot reload's compiled-caller analysis into its own assembly behind its entry point The scanner, the call-site cache and index, the referenced-method index, the load budget, the backfill and the one-shot caller note parts now live in the compiled-caller assembly and stay internal there. Only the entry point, the run scope and the types they take and return are public, so the application assembly can no longer reach past the entry point and break the order it keeps (stop background reads before a run holds the cache). --- .../{ => CompiledCallers}/HotReloadCallSiteBackfill.cs | 0 .../{ => CompiledCallers}/HotReloadCallSiteBackfill.cs.meta | 0 .../HotReload/{ => CompiledCallers}/HotReloadCallSiteHit.cs | 2 +- .../{ => CompiledCallers}/HotReloadCallSiteHit.cs.meta | 0 .../{ => CompiledCallers}/HotReloadCallSiteLoadBudget.cs | 0 .../{ => CompiledCallers}/HotReloadCallSiteLoadBudget.cs.meta | 0 .../HotReload/{ => CompiledCallers}/HotReloadCallSiteScanner.cs | 0 .../{ => CompiledCallers}/HotReloadCallSiteScanner.cs.meta | 0 .../{ => CompiledCallers}/HotReloadCompiledCallSiteCache.cs | 0 .../HotReloadCompiledCallSiteCache.cs.meta | 0 .../{ => CompiledCallers}/HotReloadCompiledCallSiteIndex.cs | 0 .../HotReloadCompiledCallSiteIndex.cs.meta | 0 .../HotReload/{ => CompiledCallers}/HotReloadCompiledCallers.cs | 2 +- .../{ => CompiledCallers}/HotReloadCompiledCallers.cs.meta | 0 .../{ => CompiledCallers}/HotReloadCompiledCallersRun.cs | 2 +- .../{ => CompiledCallers}/HotReloadCompiledCallersRun.cs.meta | 0 .../{ => CompiledCallers}/HotReloadCompiledMethodIdentity.cs | 2 +- .../HotReloadCompiledMethodIdentity.cs.meta | 0 .../{ => CompiledCallers}/HotReloadOneShotCallerClosure.cs | 0 .../{ => CompiledCallers}/HotReloadOneShotCallerClosure.cs.meta | 0 .../{ => CompiledCallers}/HotReloadOneShotCallerNoteBuilder.cs | 0 .../HotReloadOneShotCallerNoteBuilder.cs.meta | 0 .../{ => CompiledCallers}/HotReloadOneShotCallerNoteEnricher.cs | 0 .../HotReloadOneShotCallerNoteEnricher.cs.meta | 0 .../{ => CompiledCallers}/HotReloadOneShotCallerNoteRequest.cs | 2 +- .../HotReloadOneShotCallerNoteRequest.cs.meta | 0 .../{ => CompiledCallers}/HotReloadReferencedMethodIndex.cs | 0 .../HotReloadReferencedMethodIndex.cs.meta | 0 28 files changed, 5 insertions(+), 5 deletions(-) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCallSiteBackfill.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCallSiteBackfill.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCallSiteHit.cs (92%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCallSiteHit.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCallSiteLoadBudget.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCallSiteLoadBudget.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCallSiteScanner.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCallSiteScanner.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledCallSiteCache.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledCallSiteCache.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledCallSiteIndex.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledCallSiteIndex.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledCallers.cs (99%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledCallers.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledCallersRun.cs (98%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledCallersRun.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledMethodIdentity.cs (95%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadCompiledMethodIdentity.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadOneShotCallerClosure.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadOneShotCallerClosure.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadOneShotCallerNoteBuilder.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadOneShotCallerNoteBuilder.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadOneShotCallerNoteEnricher.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadOneShotCallerNoteEnricher.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadOneShotCallerNoteRequest.cs (91%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadOneShotCallerNoteRequest.cs.meta (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadReferencedMethodIndex.cs (100%) rename Packages/src/Editor/FirstPartyTools/HotReload/{ => CompiledCallers}/HotReloadReferencedMethodIndex.cs.meta (100%) 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/HotReloadCallSiteHit.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs similarity index 92% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs index 0a0dce1d3d..28ec1bd33b 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs @@ -3,7 +3,7 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// /// One compiled instruction that references a target method, reported under its logical owner. /// - internal sealed class HotReloadCallSiteHit + public sealed class HotReloadCallSiteHit { public string CallerAssemblyName; public HotReloadMetadataTypeName CallerTypeMetadataName; diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteHit.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs.meta 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 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteScanner.cs 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 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallSiteCache.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallSiteCache.cs 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/HotReloadCompiledCallers.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs similarity index 99% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs index 05375c5bd8..73d03e4036 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs @@ -12,7 +12,7 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// assemblies it read cached between runs. What a run could not read is read in the background /// after it. Begin every run with . /// - internal sealed class HotReloadCompiledCallers + public sealed class HotReloadCompiledCallers { private readonly HotReloadCompiledCallSiteCache _cache; private readonly HotReloadCallSiteBackfill _backfill; diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallers.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs similarity index 98% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs index 0d2083720b..3bee28e340 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs @@ -12,7 +12,7 @@ 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. /// - internal sealed class HotReloadCompiledCallersRun : IDisposable + public sealed class HotReloadCompiledCallersRun : IDisposable { private readonly IDisposable _hold; private readonly HotReloadCallSiteBackfill _backfill; diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledCallersRun.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallersRun.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs similarity index 95% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs index 65876c820f..7f56a892d0 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs @@ -5,7 +5,7 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// /// Identity of a compiled method to search for (assembly + type + name + arity + parameter types). /// - internal readonly struct HotReloadCompiledMethodIdentity + public readonly struct HotReloadCompiledMethodIdentity { public readonly string AssemblyName; public readonly HotReloadMetadataTypeName TypeMetadataName; diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompiledMethodIdentity.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs.meta diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerClosure.cs similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerClosure.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerClosure.cs 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 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteEnricher.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteEnricher.cs 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/HotReloadOneShotCallerNoteRequest.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs similarity index 91% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs index b58b3316e2..f1d78d8b0d 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs @@ -6,7 +6,7 @@ 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. /// - internal sealed class HotReloadOneShotCallerNoteRequest + public sealed class HotReloadOneShotCallerNoteRequest { public HotReloadOneShotCallerNoteRequest(HotReloadCompiledMethodIdentity identity, string method) { diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs.meta similarity index 100% rename from Packages/src/Editor/FirstPartyTools/HotReload/HotReloadOneShotCallerNoteRequest.cs.meta rename to Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs.meta 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 From 4b6333483829d1a7b6aeb71b769e3b8f624f535c Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 21:59:58 +0900 Subject: [PATCH 10/12] Assert the warm-up E2E run-order tests only after their run finishes An assertion that threw inside the try left the run going after the test ended, and that run then wrote its timing detail into the next test's logs, so one mutation failed an unrelated test with a wrong count. Both tests now record the in-flight values inside the try and assert them after awaiting the run. --- .../HotReload/HotReloadWarmUpE2ETests.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs b/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs index 653456f21e..0a00841a1c 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadWarmUpE2ETests.cs @@ -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"); @@ -193,23 +197,27 @@ public async Task Run_WhileAnItemIsInFlight_TakesTheCallSiteCacheHoldOnlyAfterTh await BeginScope(warmUp); int holdDepthBefore = HotReloadCompiledCallSiteCache.Shared.HoldDepth; Task run; + bool waitingForItem; + int holdDepthWhileItemRuns; try { warmUp.Start(); run = RunWithReadReturningAsync(6); - Assert.That(run.IsCompleted, Is.False, "Precondition: the run must be waiting for the item."); - Assert.That( - HotReloadCompiledCallSiteCache.Shared.HoldDepth, - Is.EqualTo(holdDepthBefore), - "cache hold taken while the item is in flight"); + 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"); } /// From 9996c275c27ff41b208d1855e081df3001c24eda Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 22:16:46 +0900 Subject: [PATCH 11/12] Keep the contract members hot reload does not read internal to the compiled-caller analysis Hot reload only builds a method identity or a note request and passes it to the entry point, so their fields stay internal and only the constructors are public. Of a call-site hit it reads everything except the caller's generic arity and the function-pointer flag, so those two stay internal. --- .../CompiledCallers/HotReloadCallSiteHit.cs | 5 +++-- .../HotReloadCompiledMethodIdentity.cs | 12 +++++++----- .../HotReloadOneShotCallerNoteRequest.cs | 6 ++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs index 28ec1bd33b..f961dceb08 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCallSiteHit.cs @@ -9,9 +9,10 @@ public sealed class HotReloadCallSiteHit public HotReloadMetadataTypeName CallerTypeMetadataName; public string CallerMethodName; public string[] CallerParameterTypeFullNames; - public int CallerGenericArity; + // Hot-reload code outside the analysis does not read these two. + internal int CallerGenericArity; public string CallerMethodKey; public string TargetMethodKey; - public bool IsFunctionPointerLoad; + internal bool IsFunctionPointerLoad; } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs index 7f56a892d0..068ae56f20 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledMethodIdentity.cs @@ -7,11 +7,13 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// public readonly struct HotReloadCompiledMethodIdentity { - public readonly string AssemblyName; - public readonly HotReloadMetadataTypeName TypeMetadataName; - public readonly string MethodName; - public readonly string[] ParameterTypeFullNames; - public readonly int GenericArity; + // 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, diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs index f1d78d8b0d..115d887ed8 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadOneShotCallerNoteRequest.cs @@ -16,8 +16,10 @@ public HotReloadOneShotCallerNoteRequest(HotReloadCompiledMethodIdentity identit Method = method; } - public HotReloadCompiledMethodIdentity Identity { get; } + // 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; } - public string Method { get; } + internal string Method { get; } } } From de936aa57fff8593f06ca0e9531689a275bcddf7 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 10 Oct 2026 22:31:21 +0900 Subject: [PATCH 12/12] State that the entry point's call-site search must run inside an open run Outside a run nothing holds the call-site cache, so a read on another thread may evict and dispose an assembly the search is still reading. The only caller today runs inside a run; the comment makes that part of the contract. --- .../HotReload/CompiledCallers/HotReloadCompiledCallers.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs index 73d03e4036..3649ac0636 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/CompiledCallers/HotReloadCompiledCallers.cs @@ -43,7 +43,9 @@ public static HotReloadCompiledCallers CreateProduction() /// /// Finds the compiled call and ldftn sites that reference any of , - /// reading every compiled assembly it needs. + /// 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) {