diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompilationAssembliesTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCompilationAssembliesTests.cs new file mode 100644 index 000000000..753ae17ec --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCompilationAssembliesTests.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +using NUnit.Framework; + +using UnityEditor; +using UnityEditor.Compilation; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// EditMode coverage for the per-domain compilation assembly list: one shared memo, dropped by the + /// Unity events that can change the list. + /// + public sealed class HotReloadCompilationAssembliesTests + { + // Why string names: the handlers are private to the production class; a rename fails these tests + // instead of silently passing them. + private const string CompilationStartedHandlerName = "OnCompilationStarted"; + private const string ProjectChangedHandlerName = "OnProjectChanged"; + + /// + /// Verifies that the static constructor subscribed the memo's invalidation to compile start and + /// project change. + /// + [Test] + public void StaticConstructor_SubscribesInvalidationToCompilationStartedAndProjectChanged() + { + HotReloadCompilationAssemblies.Current(); + + Assert.That( + StaticEventHasHandler(typeof(CompilationPipeline), CompilationStartedHandlerName), + Is.True, + "OnCompilationStarted must be subscribed on CompilationPipeline."); + Assert.That( + StaticEventHasHandler(typeof(EditorApplication), ProjectChangedHandlerName), + Is.True, + "OnProjectChanged must be subscribed on EditorApplication."); + } + + /// + /// Verifies that two calls share one kept list instead of asking Unity each time. + /// + [Test] + public void Current_CalledTwice_ReturnsSameInstance() + { + IReadOnlyList first = HotReloadCompilationAssemblies.Current(); + IReadOnlyList second = HotReloadCompilationAssemblies.Current(); + + Assert.That(first, Is.Not.Empty); + Assert.That(second, Is.SameAs(first)); + } + + // Why: CompilationPipeline stores handlers on Delegate fields, but EditorApplication.projectChanged + // lives on EventWithPerformanceTracker. A Delegate-only scan cannot see it. + private static bool StaticEventHasHandler(Type eventOwner, string handlerName) + { + FieldInfo[] fields = eventOwner.GetFields( + BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + for (int index = 0; index < fields.Length; index++) + { + object value = fields[index].GetValue(null); + if (ContainsProductionHandler(value, handlerName)) + { + return true; + } + } + + return false; + } + + private static bool ContainsProductionHandler(object source, string handlerName) + { + if (source == null) + { + return false; + } + + Delegate current = source as Delegate; + if (current != null) + { + return InvocationListContains(current, handlerName); + } + + return EnumeratorContainsHandler(source, handlerName); + } + + private static bool InvocationListContains(Delegate current, string handlerName) + { + Delegate[] listeners = current.GetInvocationList(); + for (int listenerIndex = 0; listenerIndex < listeners.Length; listenerIndex++) + { + if (IsProductionHandler(listeners[listenerIndex], handlerName)) + { + return true; + } + } + + return false; + } + + private static bool EnumeratorContainsHandler(object source, string handlerName) + { + string typeName = source.GetType().Name; + if (typeName.IndexOf("EventWithPerformanceTracker", StringComparison.Ordinal) < 0) + { + return false; + } + + MethodInfo getEnumerator = source.GetType().GetMethod( + "GetEnumerator", + BindingFlags.Instance | BindingFlags.Public); + if (getEnumerator == null || getEnumerator.GetParameters().Length != 0) + { + return false; + } + + object enumerator = getEnumerator.Invoke(source, null); + if (enumerator == null) + { + return false; + } + + MethodInfo moveNext = enumerator.GetType().GetMethod("MoveNext"); + PropertyInfo currentProperty = enumerator.GetType().GetProperty("Current"); + if (moveNext == null || currentProperty == null) + { + return false; + } + + while ((bool)moveNext.Invoke(enumerator, null)) + { + Delegate listener = currentProperty.GetValue(enumerator) as Delegate; + if (IsProductionHandler(listener, handlerName)) + { + return true; + } + } + + return false; + } + + private static bool IsProductionHandler(Delegate listener, string handlerName) + { + if (listener == null) + { + return false; + } + + MethodInfo listenerMethod = listener.Method; + return listenerMethod.DeclaringType == typeof(HotReloadCompilationAssemblies) + && listenerMethod.Name == handlerName; + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompilationAssembliesTests.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadCompilationAssembliesTests.cs.meta new file mode 100644 index 000000000..0abc5271e --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCompilationAssembliesTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 585d8186a697a4e21b54c6109d15ba69 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs b/Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs new file mode 100644 index 000000000..b26b95ea2 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; + +using NUnit.Framework; + +using UnityEditor.Compilation; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// EditMode coverage for the per-domain memo of the compilation assembly list: when it asks Unity, + /// when it reuses the last answer, and why an empty answer is never kept. + /// + public sealed class HotReloadCompilationAssemblyCacheTests + { + private int _fetchCount; + private Queue _answers; + + [SetUp] + public void SetUp() + { + _fetchCount = 0; + _answers = new Queue(); + } + + /// + /// Verifies that a second Current() reuses the first answer without asking Unity again. + /// + [Test] + public void Current_CalledTwice_FetchesOnceAndReturnsSameInstance() + { + _answers.Enqueue(new[] { CreateAssembly("First") }); + HotReloadCompilationAssemblyCache cache = CreateCache(); + + IReadOnlyList first = cache.Current(); + IReadOnlyList second = cache.Current(); + + Assert.That(_fetchCount, Is.EqualTo(1)); + Assert.That(second, Is.SameAs(first)); + Assert.That(cache.IsFilled, Is.True); + } + + /// + /// Verifies that Invalidate() drops the memo so the next Current() asks Unity for a new list. + /// + [Test] + public void Current_AfterInvalidate_FetchesAgainAndReturnsNewAnswer() + { + UnityCompilationAssembly[] firstAnswer = { CreateAssembly("First") }; + UnityCompilationAssembly[] secondAnswer = { CreateAssembly("Second") }; + _answers.Enqueue(firstAnswer); + _answers.Enqueue(secondAnswer); + HotReloadCompilationAssemblyCache cache = CreateCache(); + cache.Current(); + + cache.Invalidate(); + IReadOnlyList afterInvalidate = cache.Current(); + + Assert.That(_fetchCount, Is.EqualTo(2)); + Assert.That(afterInvalidate, Is.SameAs(secondAnswer)); + } + + /// + /// Verifies that an empty answer, which Unity gives while a compile is in flight, is not kept. + /// + [Test] + public void Current_EmptyAnswer_IsNotMemoized() + { + _answers.Enqueue(Array.Empty()); + _answers.Enqueue(Array.Empty()); + HotReloadCompilationAssemblyCache cache = CreateCache(); + + IReadOnlyList first = cache.Current(); + bool filledAfterEmpty = cache.IsFilled; + cache.Current(); + + Assert.That(first, Is.Empty); + Assert.That(filledAfterEmpty, Is.False); + Assert.That(_fetchCount, Is.EqualTo(2)); + } + + /// + /// Verifies that the first non-empty answer after an empty one is kept. + /// + [Test] + public void Current_NonEmptyAfterEmpty_IsMemoized() + { + UnityCompilationAssembly[] nonEmpty = { CreateAssembly("First") }; + _answers.Enqueue(Array.Empty()); + _answers.Enqueue(nonEmpty); + HotReloadCompilationAssemblyCache cache = CreateCache(); + + cache.Current(); + cache.Current(); + IReadOnlyList third = cache.Current(); + + Assert.That(_fetchCount, Is.EqualTo(2)); + Assert.That(third, Is.SameAs(nonEmpty)); + } + + /// + /// Verifies that FindByName returns the assembly with that name and asks Unity only once across lookups. + /// + [Test] + public void FindByName_KnownName_ReturnsAssemblyAndFetchesOnce() + { + UnityCompilationAssembly target = CreateAssembly("Second"); + _answers.Enqueue(new[] { CreateAssembly("First"), target }); + HotReloadCompilationAssemblyCache cache = CreateCache(); + + UnityCompilationAssembly first = cache.FindByName("Second"); + UnityCompilationAssembly second = cache.FindByName("Second"); + + Assert.That(first, Is.SameAs(target)); + Assert.That(second, Is.SameAs(target)); + Assert.That(_fetchCount, Is.EqualTo(1)); + } + + /// + /// Verifies that FindByName matches the whole name with case, not a prefix or a case-folded name, + /// and that it looks at the first assembly too. + /// + [Test] + public void FindByName_SimilarNames_ReturnsExactMatchOnly() + { + UnityCompilationAssembly lowerCase = CreateAssembly("foo"); + UnityCompilationAssembly prefixed = CreateAssembly("Foo.Editor"); + UnityCompilationAssembly exact = CreateAssembly("Foo"); + _answers.Enqueue(new[] { lowerCase, prefixed, exact }); + HotReloadCompilationAssemblyCache cache = CreateCache(); + + UnityCompilationAssembly foundUpper = cache.FindByName("Foo"); + UnityCompilationAssembly foundLower = cache.FindByName("foo"); + + Assert.That(foundUpper, Is.SameAs(exact)); + Assert.That(foundLower, Is.SameAs(lowerCase)); + } + + /// + /// Verifies that FindByName returns null for a name no compilation assembly has. + /// + [Test] + public void FindByName_UnknownName_ReturnsNull() + { + _answers.Enqueue(new[] { CreateAssembly("First") }); + HotReloadCompilationAssemblyCache cache = CreateCache(); + + UnityCompilationAssembly found = cache.FindByName("Missing"); + + Assert.That(found, Is.Null); + } + + private HotReloadCompilationAssemblyCache CreateCache() + { + return new HotReloadCompilationAssemblyCache(Fetch); + } + + private UnityCompilationAssembly[] Fetch() + { + _fetchCount++; + return _answers.Dequeue(); + } + + private static UnityCompilationAssembly CreateAssembly(string assemblyName) + { + return new UnityCompilationAssembly( + assemblyName, + HotReloadConstants.ScriptAssembliesRelativeDirectory + "/" + assemblyName + HotReloadConstants.CompiledAssemblyExtension, + Array.Empty(), + Array.Empty(), + Array.Empty(), + Array.Empty(), + AssemblyFlags.EditorAssembly); + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs.meta new file mode 100644 index 000000000..b20a4b48a --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadCompilationAssemblyCacheTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 23e6c40d20f324b7891035e1cd3ec444 +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 030713d35..24d6e9100 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCallSiteScanner.cs @@ -174,7 +174,7 @@ private static Dictionary GetReferencedDllFileNamesByAssembly( } Dictionary graph = new Dictionary(StringComparer.Ordinal); - foreach (UnityCompilationAssembly assembly in CompilationPipeline.GetAssemblies()) + foreach (UnityCompilationAssembly assembly in HotReloadCompilationAssemblies.Current()) { graph[assembly.name] = CollectReferenceFileNames(assembly.allReferences); } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadChangedFileAggregator.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadChangedFileAggregator.cs index e3bb7d24a..d6c368e2d 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadChangedFileAggregator.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadChangedFileAggregator.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.IO; -using UnityEditor.Compilation; - using UnityEngine; using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; @@ -64,7 +62,7 @@ internal static HotReloadChangedFileAggregationResult Detect() string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); List snapshotAssemblies = CollectSnapshotAssemblies( projectRoot, - CompilationPipeline.GetAssemblies()); + HotReloadCompilationAssemblies.Current()); return DetectFromSnapshotDirectories(projectRoot, snapshotAssemblies); } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadNewSourceMembershipValidator.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadNewSourceMembershipValidator.cs index 72fe13b7e..c99bca4b6 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadNewSourceMembershipValidator.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadNewSourceMembershipValidator.cs @@ -105,7 +105,7 @@ private static HotReloadFailureDescription DescribeMembershipChange( HotReloadNewSourceMembershipEvidence evidence) { string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - UnityCompilationAssembly compilationAssembly = FindCompilationAssembly(evidence.AssemblyName); + UnityCompilationAssembly compilationAssembly = HotReloadCompilationAssemblies.FindByName(evidence.AssemblyName); if (compilationAssembly == null) { return HotReloadFailureDescription.Declaration( @@ -422,20 +422,6 @@ internal static bool EvidenceMatches( return true; } - private static UnityCompilationAssembly FindCompilationAssembly(string assemblyName) - { - UnityCompilationAssembly[] assemblies = CompilationPipeline.GetAssemblies(); - for (int index = 0; index < assemblies.Length; index++) - { - if (assemblies[index].name == assemblyName) - { - return assemblies[index]; - } - } - - return null; - } - private static string NormalizeProjectRelativePath(string path) { return string.IsNullOrEmpty(path) ? null : path.Replace('\\', '/'); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs index 1ab778337..f688f1b75 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadPatchTargetSupport.cs @@ -57,7 +57,7 @@ internal static HotReloadPatchTargetResolution ResolvePatchTarget( } string assemblyName = Path.GetFileNameWithoutExtension(rawAssemblyName); - UnityCompilationAssembly compilationAssembly = FindCompilationAssembly(assemblyName); + UnityCompilationAssembly compilationAssembly = HotReloadCompilationAssemblies.FindByName(assemblyName); // Why gate on compilationAssembly == null: the unimported-asmdef flag is only // consumed on that branch, and walking ancestor directories on every successful // resolve (including loose Assembly-CSharp scripts) is wasted disk I/O. @@ -208,19 +208,6 @@ private static HotReloadFailureDescription DescribeBusyRefusal( return editorStateSnapshotCapture.CaptureCurrent().GetBusyFailure(); } - private static UnityCompilationAssembly FindCompilationAssembly(string assemblyName) - { - foreach (UnityCompilationAssembly assembly in CompilationPipeline.GetAssemblies()) - { - if (assembly.name == assemblyName) - { - return assembly; - } - } - - return null; - } - // Why Path.Combine then GetFullPath: Unity Assembly.sourceFiles are project-relative // (slash-separated). The worker cwd is Library/UloopHotReload/Worker//, so it // can only open absolute paths. Normalization matches HotReloadSourceSnapshotter. diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs new file mode 100644 index 000000000..842fe71c1 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; + +using UnityEditor; +using UnityEditor.Compilation; + +using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// The compilation assembly list for hot reload, asked of Unity once per domain. + /// Why: on a project with several hundred assemblies one CompilationPipeline.GetAssemblies() call + /// takes 340-620 ms, while the list only changes through an import or a compile. The startup + /// snapshot capture is the first caller after a domain reload, so runs find the list already kept. + /// + internal static class HotReloadCompilationAssemblies + { + private static readonly HotReloadCompilationAssemblyCache Cache = + new HotReloadCompilationAssemblyCache(CompilationPipeline.GetAssemblies); + + static HotReloadCompilationAssemblies() + { + // Why: a compile that fails keeps the domain alive, so without this the list would keep + // the asmdefs and source files from before that compile until the next reload. + CompilationPipeline.compilationStarted += OnCompilationStarted; + // Why: an import does not always compile (Play Mode with Recompile After Finished Playing + // postpones it), yet it can change the source files of an assembly. + EditorApplication.projectChanged += OnProjectChanged; + } + + /// + /// Returns the compilation assemblies. The list is shared; callers only read it. + /// + internal static IReadOnlyList Current() + { + return Cache.Current(); + } + + /// + /// Returns the first compilation assembly with this name, or null when none has it. + /// + internal static UnityCompilationAssembly FindByName(string assemblyName) + { + return Cache.FindByName(assemblyName); + } + + private static void OnCompilationStarted(object context) + { + Cache.Invalidate(); + } + + private static void OnProjectChanged() + { + Cache.Invalidate(); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs.meta new file mode 100644 index 000000000..34d698d2d --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblies.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 74af2888c9ea646518605a0fe11f27cf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs new file mode 100644 index 000000000..5eac8cd97 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; + +using UnityEngine; + +using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Keeps the last non-empty compilation assembly list so repeated lookups ask Unity only once + /// until the owner invalidates it. Main thread only, like the Unity call it wraps. + /// + internal sealed class HotReloadCompilationAssemblyCache + { + private readonly Func _fetch; + private UnityCompilationAssembly[] _assemblies; + + internal HotReloadCompilationAssemblyCache(Func fetch) + { + Debug.Assert(fetch != null, "fetch must not be null."); + _fetch = fetch; + } + + internal bool IsFilled => _assemblies != null; + + /// + /// Returns the memoized list, asking Unity when nothing is kept. The list is shared; callers only read it. + /// + internal IReadOnlyList Current() + { + if (_assemblies != null) + { + return _assemblies; + } + + UnityCompilationAssembly[] fetched = _fetch(); + Debug.Assert(fetched != null, "fetch must not return null."); + + // Why not memoize an empty result: GetAssemblies() answers nothing while a compile is in + // flight, and keeping that would hide every assembly for the rest of the domain's life. + if (fetched.Length > 0) + { + _assemblies = fetched; + } + + return fetched; + } + + /// + /// Returns the first compilation assembly with this name, or null when none has it. + /// + internal UnityCompilationAssembly FindByName(string assemblyName) + { + Debug.Assert(!string.IsNullOrEmpty(assemblyName), "assemblyName must not be null or empty."); + + IReadOnlyList assemblies = Current(); + for (int index = 0; index < assemblies.Count; index++) + { + if (assemblies[index].name == assemblyName) + { + return assemblies[index]; + } + } + + return null; + } + + internal void Invalidate() + { + _assemblies = null; + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs.meta new file mode 100644 index 000000000..7f7ac07f1 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompilationAssemblyCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d7365883bccb142a7b7e503e8080d096 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs index 6f0e49fa0..6064b1ff9 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs @@ -6,7 +6,6 @@ using Mono.Cecil; -using UnityEditor.Compilation; using UnityEditor.PackageManager; using UnityEngine; @@ -33,7 +32,7 @@ internal static class HotReloadSourceSnapshotter internal static void CaptureAfterDomainReload() { string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - CaptureAssemblies(projectRoot, CompilationPipeline.GetAssemblies()); + CaptureAssemblies(projectRoot, HotReloadCompilationAssemblies.Current()); } // Why separate from CaptureAfterDomainReload: the project root and the compilation assemblies come