diff --git a/Assets/Tests/Editor/HotReload/HotReloadIncrementalSnapshotCaptureTests.cs b/Assets/Tests/Editor/HotReload/HotReloadIncrementalSnapshotCaptureTests.cs index e054e1806..13135c996 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadIncrementalSnapshotCaptureTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadIncrementalSnapshotCaptureTests.cs @@ -535,10 +535,21 @@ private HotReloadSnapshotCaptureStats CaptureAssemblies(params string[] sources) Array.Empty(), Array.Empty(), AssemblyFlags.EditorAssembly); - return HotReloadSourceSnapshotter.CaptureAssemblies( + HotReloadSnapshotCaptureInputs inputs = new HotReloadSnapshotCaptureInputs( _root, - new[] { assembly }, HotReloadCompileStart.Unknown, + Array.Empty(), + StringComparison.Ordinal, + new[] + { + HotReloadSnapshotCaptureInputs.ToCaptureAssembly( + assembly, + Array.Empty(), + StringComparison.Ordinal) + }, + 0); + return HotReloadSourceSnapshotter.CaptureAssemblies( + inputs, new HotReloadPdbDocumentIndex(Path.Combine(_root, "PdbDocuments"))); } @@ -550,7 +561,9 @@ private HotReloadSnapshotSourceCheck CheckAgainst(string dllPath, string pdbPath dllPath, pdbPath, HotReloadAssemblyMvid.Read(dllPath), - new HotReloadPdbDocumentIndex(Path.Combine(_root, "PdbDocuments"))); + new HotReloadPdbDocumentIndex(Path.Combine(_root, "PdbDocuments")), + HotReloadSnapshotPackageRoots.ReadCurrent(), + HotReloadSnapshotCaptureInputs.CurrentPathComparison()); } // A check whose suspect window never opens, so no copy is checked against a PDB. @@ -561,7 +574,9 @@ private HotReloadSnapshotSourceCheck SuspectsNothing() "unused.dll", "unused.pdb", "unused", - new HotReloadPdbDocumentIndex(Path.Combine(_root, "PdbDocuments"))); + new HotReloadPdbDocumentIndex(Path.Combine(_root, "PdbDocuments")), + Array.Empty(), + StringComparison.Ordinal); } // Copies this test assembly's own DLL and PDB, so the fixture has a real image for Cecil to read. diff --git a/Assets/Tests/Editor/HotReload/HotReloadSnapshotAssemblyEnumerationTests.cs b/Assets/Tests/Editor/HotReload/HotReloadSnapshotAssemblyEnumerationTests.cs index 035bb8f09..116440f3a 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadSnapshotAssemblyEnumerationTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadSnapshotAssemblyEnumerationTests.cs @@ -66,9 +66,7 @@ public void CaptureAssemblies_WithoutStamp_CapturesSourcesRemovesStaleSnapshotAn Directory.CreateDirectory(staleDirectory); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(File.Exists(SnapshotSourcePath("Fixture", mvid)), Is.True); @@ -88,9 +86,7 @@ public void CaptureAssemblies_WhenStampMatches_SkipsCapture() File.WriteAllText(StampPath("Fixture"), stamp); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(Directory.Exists(Path.Combine(_snapshotRoot, "Fixture-" + mvid)), Is.False); @@ -112,9 +108,7 @@ public void CaptureAssemblies_WhenMvidDirectoryExists_KeepsSnapshotsAndWritesSta Directory.CreateDirectory(staleDirectory); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(Directory.GetFiles(currentDirectory), Is.Empty); @@ -154,9 +148,7 @@ public void CaptureAssemblies_WhenStampIsMalformedOrDiffers_CapturesAgain(string File.WriteAllText(StampPath("Fixture"), stamp); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(Directory.Exists(HotReloadSourceSnapshotLayout.AssemblyDirectory(_projectRoot, "Fixture", mvid)), Is.True); @@ -184,9 +176,7 @@ public void CaptureAssemblies_RemovesOnlyThisAssemblysStaleMvidDirectories() Directory.CreateDirectory(nonMvid); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(Directory.Exists(orphanTemporary), Is.False); @@ -206,15 +196,36 @@ public void CaptureAssemblies_WhenAssemblyHasNoSources_WritesNothing() string mvid = HotReloadAssemblyMvid.Read(dllPath); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssemblyWithSources("NoSources", Array.Empty()) }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssemblyWithSources("NoSources", Array.Empty()) }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(Directory.Exists(HotReloadSourceSnapshotLayout.AssemblyDirectory(_projectRoot, "NoSources", mvid)), Is.False); Assert.That(File.Exists(StampPath("NoSources")), Is.False); } + /// + /// Verifies an assembly the inputs classified as an immutable package's is counted and skipped + /// before anything is written, though its DLL and PDB are there: no snapshot directory and no stamp. + /// + [Test] + public void CaptureAssemblies_ImmutablePackageAssembly_CountsItAndWritesNothing() + { + PlantCompiledAssembly("Fixture"); + HotReloadSnapshotCaptureInputs inputs = new HotReloadSnapshotCaptureInputs( + _projectRoot, + HotReloadCompileStart.Unknown, + Array.Empty(), + StringComparison.Ordinal, + new[] { new HotReloadCaptureAssembly("Fixture", new[] { SourceRelativePath }, true) }, + 0); + + HotReloadSnapshotCaptureStats stats = HotReloadSourceSnapshotter.CaptureAssemblies(inputs, CreateDocumentIndex()); + + Assert.That(stats.AssembliesImmutable, Is.EqualTo(1), "immutable"); + Assert.That(Directory.GetDirectories(_snapshotRoot, "Fixture-*"), Is.Empty); + Assert.That(File.Exists(StampPath("Fixture")), Is.False); + } + /// /// Verifies an assembly whose compiled DLL or PDB is missing is skipped without a warning: no snapshot directory and no stamp. /// @@ -226,9 +237,7 @@ public void CaptureAssemblies_WhenCompiledOutputIsMissing_WritesNothingAndDoesNo File.Delete(Path.ChangeExtension(dllPath, missingExtension)); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(Directory.GetDirectories(_snapshotRoot, "Fixture-*"), Is.Empty); @@ -252,9 +261,7 @@ public void CaptureAssemblies_WhenStampMatches_DoesNotOpenTheCompiledAssembly() File.WriteAllText(StampPath("Fixture"), stamp); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(Directory.Exists(HotReloadSourceSnapshotLayout.AssemblyDirectory(_projectRoot, "Fixture", mvid)), Is.False); @@ -280,9 +287,7 @@ public void CaptureAssemblies_WhenOneAssemblyCannotBeRead_WarnsAndCapturesTheRes LogAssert.Expect(LogType.Warning, new Regex("Snapshot capture failed for assembly Broken")); HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Broken"), CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, new[] { CreateAssembly("Broken"), CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(File.Exists(StampPath("Broken")), Is.False); @@ -343,9 +348,7 @@ public void CaptureAssemblies_ForVirtualPlayerRoot_ReadsTheMainProjectsDllAndSna string mvid = HotReloadAssemblyMvid.Read(dllPath); HotReloadSourceSnapshotter.CaptureAssemblies( - playerRoot, - new[] { CreateAssembly("Fixture") }, - HotReloadCompileStart.Unknown, + CreateInputs(playerRoot, new[] { CreateAssembly("Fixture") }, HotReloadCompileStart.Unknown), CreateDocumentIndex()); string playerSnapshotSource = HotReloadSourceSnapshotLayout.SourcePath( @@ -417,14 +420,10 @@ public void CaptureAssemblies_ReturnsWhatItCopiedAndWhatItSkipped() }; HotReloadSnapshotCaptureStats first = HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - assemblies, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, assemblies, HotReloadCompileStart.Unknown), CreateDocumentIndex()); HotReloadSnapshotCaptureStats second = HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - assemblies, - HotReloadCompileStart.Unknown, + CreateInputs(_projectRoot, assemblies, HotReloadCompileStart.Unknown), CreateDocumentIndex()); Assert.That(first.Assemblies, Is.EqualTo(1), "first: assemblies"); @@ -452,9 +451,7 @@ private string CaptureWithSourceSavedBetweenStartAndDll( string mvid = HotReloadAssemblyMvid.Read(dllPath); stats = HotReloadSourceSnapshotter.CaptureAssemblies( - _projectRoot, - new[] { CreateAssembly("Fixture") }, - compileStart, + CreateInputs(_projectRoot, new[] { CreateAssembly("Fixture") }, compileStart), CreateDocumentIndex()); return Path.Combine(_snapshotRoot, "Fixture-" + mvid); @@ -511,6 +508,31 @@ private static UnityCompilationAssembly CreateAssemblyWithSources(string assembl AssemblyFlags.EditorAssembly); } + // The capture's inputs for a temporary root: no package is registered there, so every + // assembly reads as editable. + private static HotReloadSnapshotCaptureInputs CreateInputs( + string projectRoot, + IEnumerable assemblies, + HotReloadCompileStart compileStart) + { + List captureAssemblies = new List(); + foreach (UnityCompilationAssembly assembly in assemblies) + { + captureAssemblies.Add(HotReloadSnapshotCaptureInputs.ToCaptureAssembly( + assembly, + Array.Empty(), + StringComparison.Ordinal)); + } + + return new HotReloadSnapshotCaptureInputs( + projectRoot, + compileStart, + Array.Empty(), + StringComparison.Ordinal, + captureAssemblies, + 0); + } + private string SnapshotSourcePath(string assemblyName, string mvid) { return HotReloadSourceSnapshotLayout.SourcePath( diff --git a/Assets/Tests/Editor/HotReload/HotReloadSnapshotCaptureInputsTests.cs b/Assets/Tests/Editor/HotReload/HotReloadSnapshotCaptureInputsTests.cs new file mode 100644 index 000000000..41f7ac0e8 --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadSnapshotCaptureInputsTests.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using NUnit.Framework; + +using UnityEditor.Compilation; + +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// Covers the values the snapshot capture reads from Unity before it starts: how one of Unity's + /// compilation assemblies is classified from the package roots, and, on this project's real + /// assemblies, that the classification agrees with the Package Manager's answer. + /// + public sealed class HotReloadSnapshotCaptureInputsTests + { + private const int MaxListedMismatches = 5; + + private static readonly HotReloadSnapshotPackageRoot[] Packages = + { + new HotReloadSnapshotPackageRoot("/proj/Packages/src", "Packages/com.example.embedded", false), + new HotReloadSnapshotPackageRoot("/elsewhere/LocalPkg", "Packages/com.example.local", false), + new HotReloadSnapshotPackageRoot("/proj/Library/PackageCache/com.example.reg@1.0.0", "Packages/com.example.reg", true), + }; + + /// + /// Verifies an assembly whose first source lies in an immutable package is classified as + /// immutable, and keeps its name and sources. + /// + [Test] + public void ToCaptureAssembly_FirstSourceInAnImmutablePackage_IsImmutable() + { + HotReloadCaptureAssembly assembly = Classify("Packages/com.example.reg/Runtime/A.cs", "Assets/B.cs"); + + Assert.That(assembly.IsImmutablePackageAssembly, Is.True); + Assert.That(assembly.Name, Is.EqualTo("Fixture")); + Assert.That(assembly.SourceAssetPaths, Is.EqualTo(new[] { "Packages/com.example.reg/Runtime/A.cs", "Assets/B.cs" })); + } + + /// + /// Verifies sources of an embedded package, a local package and Assets are editable. + /// + [TestCase("Packages/com.example.embedded/Editor/A.cs")] + [TestCase("Packages/com.example.local/A.cs")] + [TestCase("Assets/A.cs")] + public void ToCaptureAssembly_FirstSourceEditable_IsNotImmutable(string firstSource) + { + HotReloadCaptureAssembly assembly = Classify(firstSource); + + Assert.That(assembly.IsImmutablePackageAssembly, Is.False); + } + + /// + /// Verifies a package whose name only starts with an immutable package's name is not taken for it. + /// + [Test] + public void ToCaptureAssembly_PackageNameExtendingAnImmutableOne_IsNotImmutable() + { + HotReloadCaptureAssembly assembly = Classify("Packages/com.example.reg2/A.cs"); + + Assert.That(assembly.IsImmutablePackageAssembly, Is.False); + } + + /// + /// Verifies an assembly without sources is classified as editable with an empty source list. + /// + [Test] + public void ToCaptureAssembly_WithoutSources_IsNotImmutableAndHasNoSources() + { + HotReloadCaptureAssembly assembly = Classify(); + + Assert.That(assembly.IsImmutablePackageAssembly, Is.False); + Assert.That(assembly.SourceAssetPaths, Is.Empty); + } + + /// + /// Verifies, for every compilation assembly of this project that has sources, that the + /// classification from the package roots equals the Package Manager's answer the capture + /// asked before. + /// + [Test] + public void ToCaptureAssembly_FromPackageRoots_MatchesThePackageManagerAnswerForEveryAssembly() + { + IReadOnlyList packageRoots = HotReloadSnapshotPackageRoots.ReadCurrent(); + StringComparison comparison = HotReloadSnapshotCaptureInputs.CurrentPathComparison(); + List mismatches = new List(); + int checkedAssemblies = 0; + int immutableAssemblies = 0; + + foreach (UnityCompilationAssembly compilationAssembly in HotReloadCompilationAssemblies.Current()) + { + string[] sourceFiles = compilationAssembly.sourceFiles; + if (sourceFiles == null || sourceFiles.Length == 0) + { + continue; + } + + checkedAssemblies++; + bool expected = HotReloadSourceSnapshotter.ShouldSkipImmutablePackageSources(sourceFiles); + immutableAssemblies += expected ? 1 : 0; + bool actual = HotReloadSnapshotCaptureInputs + .ToCaptureAssembly(compilationAssembly, packageRoots, comparison) + .IsImmutablePackageAssembly; + if (actual != expected) + { + mismatches.Add(compilationAssembly.name + " (" + sourceFiles[0] + "): package roots say " + + actual + ", the Package Manager says " + expected + "."); + } + } + + TestContext.WriteLine( + "Checked " + checkedAssemblies + " assemblies, " + immutableAssemblies + " immutable; " + + mismatches.Count + " mismatches."); + Assert.That(checkedAssemblies, Is.GreaterThan(0), "No compilation assembly with sources was found."); + Assert.That(immutableAssemblies, Is.GreaterThan(0), "No immutable package assembly was found to compare."); + Assert.That( + mismatches, + Is.Empty, + "Checked " + checkedAssemblies + " assemblies, " + immutableAssemblies + " immutable; " + + mismatches.Count + " mismatches:\n" + + string.Join("\n", mismatches.GetRange(0, Math.Min(MaxListedMismatches, mismatches.Count)))); + } + + /// + /// Verifies the inputs read on the main thread name this project's root and carry one entry + /// for each compilation assembly Unity lists. + /// + [Test] + public void ReadOnMainThread_CarriesTheProjectRootAndEveryCompilationAssembly() + { + HotReloadSnapshotCaptureInputs inputs = HotReloadSnapshotCaptureInputs.ReadOnMainThread(); + + Assert.That(inputs.ProjectRoot, Is.EqualTo(Path.GetFullPath(Path.Combine(Application.dataPath, "..")))); + Assert.That(inputs.Assemblies.Count, Is.EqualTo(HotReloadCompilationAssemblies.Current().Count)); + Assert.That(inputs.PackageRoots, Is.Not.Empty); + } + + private static HotReloadCaptureAssembly Classify(params string[] sourceFiles) + { + UnityCompilationAssembly compilationAssembly = new UnityCompilationAssembly( + "Fixture", + "Library/ScriptAssemblies/Fixture.dll", + sourceFiles, + Array.Empty(), + Array.Empty(), + Array.Empty(), + AssemblyFlags.EditorAssembly); + return HotReloadSnapshotCaptureInputs.ToCaptureAssembly(compilationAssembly, Packages, StringComparison.Ordinal); + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadSnapshotCaptureInputsTests.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadSnapshotCaptureInputsTests.cs.meta new file mode 100644 index 000000000..df92ac9fd --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadSnapshotCaptureInputsTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1ee49cc5e8e1b42da9117502651883ab +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReload/HotReloadSnapshotEditedDuringCompileTests.cs b/Assets/Tests/Editor/HotReload/HotReloadSnapshotEditedDuringCompileTests.cs index e0699649e..09abcf338 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadSnapshotEditedDuringCompileTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadSnapshotEditedDuringCompileTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -23,6 +24,12 @@ public sealed class HotReloadSnapshotEditedDuringCompileTests // A file without method bodies, so the PDB has no document for it. private const string BodylessFixtureProjectRelativePath = "Assets/Tests/Editor/HotReload/HotReloadSnapshotBodylessFixture.cs"; + private const string PausePointsRuntimeAssemblyName = "UnityCLILoop.PausePoints.Runtime"; + // A file of this repository's embedded package, whose folder differs from its package name, + // so its asset path and the path its PDB records differ. + private const string PackageSourceAssetPath = + "Packages/io.github.hatayama.uloopmcp/Runtime/PausePoints/UloopPausePoint.cs"; + private const string PackageSourcePhysicalPath = "Packages/src/Runtime/PausePoints/UloopPausePoint.cs"; private static readonly long CompileStartUtcTicks = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks; private static readonly DateTime SinceTheStartUtc = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); @@ -59,13 +66,36 @@ public void CaptureAtomically_ASourceWrittenSinceTheStartThatMatchesThePdb_Recor { PlantSource(FixtureProjectRelativePath, RealBytes(FixtureProjectRelativePath), SinceTheStartUtc); - HotReloadSourceStampManifest manifest = Capture(FixtureProjectRelativePath); + HotReloadSourceStampManifest manifest = Capture(FixtureProjectRelativePath, TestAssemblyName, HotReloadSnapshotPackageRoots.ReadCurrent()); string snapshotFileName = SnapshotFileName(FixtureProjectRelativePath); Assert.That(manifest.IsEditedAfterCompile(snapshotFileName), Is.False); Assert.That(manifest.TryGetStamp(snapshotFileName, out long _, out long _), Is.True); } + /// + /// Verifies that a source of an embedded package written since the compile started whose bytes + /// match the PDB gets an unmarked stamp: the check names it by the folder behind the package + /// path, as the PDB records it, from the package roots it was given. The PDB records the + /// package's sources relative to the compiling project, so the package root is moved under + /// the temporary root, as the real one lies under the real project. + /// + [Test] + public void CaptureAtomically_APackageSourceWrittenSinceTheStartThatMatchesThePdb_RecordsAnUnmarkedStamp() + { + byte[] realBytes = File.ReadAllBytes(Path.Combine(_realProjectRoot, PackageSourcePhysicalPath)); + PlantSource(PackageSourceAssetPath, realBytes, SinceTheStartUtc); + + HotReloadSourceStampManifest manifest = Capture( + PackageSourceAssetPath, + PausePointsRuntimeAssemblyName, + PackageRootsMovedUnderTheTemporaryRoot()); + + string snapshotFileName = SnapshotFileName(PackageSourceAssetPath); + Assert.That(manifest.IsEditedAfterCompile(snapshotFileName), Is.False); + Assert.That(manifest.TryGetStamp(snapshotFileName, out long _, out long _), Is.True); + } + /// /// Verifies that a source written since the compile started whose bytes differ from the PDB is /// marked, and that its copy is still written with the edited bytes. @@ -76,7 +106,7 @@ public void CaptureAtomically_ASourceWrittenSinceTheStartThatDiffersFromThePdb_M byte[] editedBytes = EditedBytes(FixtureProjectRelativePath); PlantSource(FixtureProjectRelativePath, editedBytes, SinceTheStartUtc); - HotReloadSourceStampManifest manifest = Capture(FixtureProjectRelativePath); + HotReloadSourceStampManifest manifest = Capture(FixtureProjectRelativePath, TestAssemblyName, HotReloadSnapshotPackageRoots.ReadCurrent()); string snapshotFileName = SnapshotFileName(FixtureProjectRelativePath); string snapshotPath = Path.Combine(_snapshotDirectory, snapshotFileName); @@ -94,7 +124,7 @@ public void CaptureAtomically_ASourceWrittenBeforeTheStart_IsNotCheckedAgainstTh { PlantSource(FixtureProjectRelativePath, EditedBytes(FixtureProjectRelativePath), BeforeTheStartUtc); - HotReloadSourceStampManifest manifest = Capture(FixtureProjectRelativePath); + HotReloadSourceStampManifest manifest = Capture(FixtureProjectRelativePath, TestAssemblyName, HotReloadSnapshotPackageRoots.ReadCurrent()); Assert.That(manifest.IsEditedAfterCompile(SnapshotFileName(FixtureProjectRelativePath)), Is.False); } @@ -111,35 +141,60 @@ public void CaptureAtomically_ASourceThePdbHasNoDocumentFor_MarksIt() RealBytes(BodylessFixtureProjectRelativePath), SinceTheStartUtc); - HotReloadSourceStampManifest manifest = Capture(BodylessFixtureProjectRelativePath); + HotReloadSourceStampManifest manifest = Capture(BodylessFixtureProjectRelativePath, TestAssemblyName, HotReloadSnapshotPackageRoots.ReadCurrent()); Assert.That(manifest.IsEditedAfterCompile(SnapshotFileName(BodylessFixtureProjectRelativePath)), Is.True); } - private HotReloadSourceStampManifest Capture(string projectRelativePath) + private HotReloadSourceStampManifest Capture( + string projectRelativePath, + string assemblyName, + IReadOnlyList packageRoots) { string dllPath = Path.Combine( _realProjectRoot, HotReloadConstants.ScriptAssembliesRelativeDirectory, - TestAssemblyName + HotReloadConstants.CompiledAssemblyExtension); + assemblyName + HotReloadConstants.CompiledAssemblyExtension); HotReloadSnapshotSourceCheck check = new HotReloadSnapshotSourceCheck( CompileStartUtcTicks, dllPath, Path.ChangeExtension(dllPath, ".pdb"), HotReloadAssemblyMvid.Read(dllPath), - new HotReloadPdbDocumentIndex(Path.Combine(_tempRoot, "PdbDocuments"))); + new HotReloadPdbDocumentIndex(Path.Combine(_tempRoot, "PdbDocuments")), + packageRoots, + HotReloadSnapshotCaptureInputs.CurrentPathComparison()); HotReloadSourceSnapshotCopier.CaptureAtomically( _tempRoot, _snapshotDirectory, new[] { projectRelativePath }, - TestAssemblyName, + assemblyName, check, new HotReloadSnapshotCaptureStats()); return HotReloadSourceStampManifest.Load(_snapshotDirectory); } + // The registered packages, with every folder under the real project moved to the same place + // under the temporary root. + private List PackageRootsMovedUnderTheTemporaryRoot() + { + string realRootPrefix = _realProjectRoot.Replace('\\', '/').TrimEnd('/') + "/"; + List roots = new List(); + foreach (HotReloadSnapshotPackageRoot root in HotReloadSnapshotPackageRoots.ReadCurrent()) + { + string resolvedPath = root.ResolvedPath.Replace('\\', '/'); + if (resolvedPath.StartsWith(realRootPrefix, StringComparison.Ordinal)) + { + resolvedPath = Path.Combine(_tempRoot, resolvedPath.Substring(realRootPrefix.Length)); + } + + roots.Add(new HotReloadSnapshotPackageRoot(resolvedPath, root.AssetPath, root.IsImmutable)); + } + + return roots; + } + private byte[] RealBytes(string projectRelativePath) { return File.ReadAllBytes(Path.Combine(_realProjectRoot, projectRelativePath)); diff --git a/Assets/Tests/Editor/HotReload/HotReloadSnapshotLookupPathTests.cs b/Assets/Tests/Editor/HotReload/HotReloadSnapshotLookupPathTests.cs new file mode 100644 index 000000000..2cd8e835c --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadSnapshotLookupPathTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using NUnit.Framework; + +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload +{ + /// + /// Covers naming a source the way its PDB records it from package roots read beforehand, and, + /// on this project's real sources, that the name equals the one the Package Manager gives. + /// + public sealed class HotReloadSnapshotLookupPathTests + { + private const int MaxListedMismatches = 5; + + private static readonly HotReloadSnapshotPackageRoot[] Packages = + { + new HotReloadSnapshotPackageRoot("/proj/Packages/src", "Packages/com.example.embedded", false), + new HotReloadSnapshotPackageRoot("/elsewhere/LocalPkg", "Packages/com.example.local", false), + }; + + /// + /// Verifies a source of a package inside the project is named relative to the project, one + /// outside it by its absolute path, and an Assets source by its asset path. + /// + [TestCase("Packages/com.example.embedded/Editor/A.cs", "Packages/src/Editor/A.cs")] + [TestCase("Packages/com.example.local/A.cs", "/elsewhere/LocalPkg/A.cs")] + [TestCase("Assets/A.cs", "Assets/A.cs")] + public void ToPdbLookupPath_NamesTheFileTheWayThePdbRecordsIt(string assetPath, string expected) + { + string lookupPath = HotReloadSnapshotLookupPath.ToPdbLookupPath("/proj", assetPath, Packages, StringComparison.Ordinal); + + Assert.That(lookupPath, Is.EqualTo(expected)); + } + + /// + /// Verifies, for every source of every compilation assembly of this project, that the name + /// built from the package roots equals the one ScriptPackageRoots.ToPhysicalPath builds by + /// asking the Package Manager. + /// + [Test] + public void PdbLookupPath_FromPackageRoots_MatchesThePackageManagerAnswerForEverySource() + { + string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + string compiledProjectRoot = CompiledAssemblyLayout.Resolve(projectRoot).MainProjectRoot; + IReadOnlyList packageRoots = HotReloadSnapshotPackageRoots.ReadCurrent(); + StringComparison comparison = HotReloadSnapshotCaptureInputs.CurrentPathComparison(); + List mismatches = new List(); + int checkedSources = 0; + int packageSources = 0; + + foreach (UnityCompilationAssembly compilationAssembly in HotReloadCompilationAssemblies.Current()) + { + foreach (string sourceFile in compilationAssembly.sourceFiles ?? Array.Empty()) + { + string assetPath = sourceFile.Replace('\\', '/'); + string expected = ScriptPackageRoots.ToPhysicalPath(compiledProjectRoot, assetPath); + string actual = HotReloadSnapshotLookupPath.ToPdbLookupPath( + compiledProjectRoot, + assetPath, + packageRoots, + comparison); + checkedSources++; + packageSources += expected == assetPath ? 0 : 1; + if (!string.Equals(actual, expected, StringComparison.Ordinal)) + { + mismatches.Add(assetPath + ": package roots give " + actual + ", the Package Manager gives " + expected + "."); + } + } + } + + TestContext.WriteLine( + "Checked " + checkedSources + " sources, " + packageSources + " renamed by a package; " + + mismatches.Count + " mismatches."); + Assert.That(checkedSources, Is.GreaterThan(0), "No source was found."); + Assert.That(packageSources, Is.GreaterThan(0), "No source behind a package folder was found to compare."); + Assert.That( + mismatches, + Is.Empty, + "Checked " + checkedSources + " sources, " + packageSources + " renamed by a package; " + + mismatches.Count + " mismatches:\n" + + string.Join("\n", mismatches.GetRange(0, Math.Min(MaxListedMismatches, mismatches.Count)))); + } + } +} diff --git a/Assets/Tests/Editor/HotReload/HotReloadSnapshotLookupPathTests.cs.meta b/Assets/Tests/Editor/HotReload/HotReloadSnapshotLookupPathTests.cs.meta new file mode 100644 index 000000000..c628c599e --- /dev/null +++ b/Assets/Tests/Editor/HotReload/HotReloadSnapshotLookupPathTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5847b035ba7a34ddc867e05ea4877d82 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/HotReload/HotReloadSourceSnapshotTests.cs b/Assets/Tests/Editor/HotReload/HotReloadSourceSnapshotTests.cs index 860c5ccd7..52fd7ec15 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadSourceSnapshotTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadSourceSnapshotTests.cs @@ -180,7 +180,7 @@ public void LoadVerifiedSnapshotSource_ForPredefinedEditorAssembly_ReturnsOnDisk [Test] public void LoadVerifiedSnapshotSource_PackageSourceGivenByItsAssetPath_ReturnsTheVerifiedSource() { - HotReloadSourceSnapshotter.CaptureAfterDomainReload(); + HotReloadSourceSnapshotter.CaptureAfterDomainReload(HotReloadSnapshotCaptureInputs.ReadOnMainThread()); string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); string dllPath = Path.Combine( projectRoot, @@ -203,7 +203,7 @@ public void CaptureAfterDomainReload_LogsOneBreakdownWithEveryPhase() { VibeLogger.ClearMemoryLogs(); - HotReloadSourceSnapshotter.CaptureAfterDomainReload(); + HotReloadSourceSnapshotter.CaptureAfterDomainReload(HotReloadSnapshotCaptureInputs.ReadOnMainThread()); JObject context = HotReloadWarmUpTestDoubles.ReadSingleVibeContext(HotReloadConstants.VibeLogSourceSnapshotBreakdown); string[] fields = @@ -509,7 +509,9 @@ private static HotReloadSnapshotSourceCheck SuspectsNothing(string root) "unused.dll", "unused.pdb", "unused", - new HotReloadPdbDocumentIndex(Path.Combine(root, "PdbDocuments"))); + new HotReloadPdbDocumentIndex(Path.Combine(root, "PdbDocuments")), + Array.Empty(), + StringComparison.Ordinal); } // Copies the fixture's real snapshot under a temporary root with its first byte flipped. diff --git a/Assets/Tests/Editor/HotReload/HotReloadSourceSnapshotterTests.cs b/Assets/Tests/Editor/HotReload/HotReloadSourceSnapshotterTests.cs index 49203054f..aa472aaf8 100644 --- a/Assets/Tests/Editor/HotReload/HotReloadSourceSnapshotterTests.cs +++ b/Assets/Tests/Editor/HotReload/HotReloadSourceSnapshotterTests.cs @@ -239,7 +239,9 @@ private static HotReloadSnapshotSourceCheck SuspectsNothing(string root) "unused.dll", "unused.pdb", "unused", - new HotReloadPdbDocumentIndex(Path.Combine(root, "PdbDocuments"))); + new HotReloadPdbDocumentIndex(Path.Combine(root, "PdbDocuments")), + Array.Empty(), + StringComparison.Ordinal); } // Why a write time the test chooses: SetLastWriteTimeUtc stores microseconds while diff --git a/Assets/Tests/Editor/HotReload/PausePointScriptPathFormTests.cs b/Assets/Tests/Editor/HotReload/PausePointScriptPathFormTests.cs index 205d93bd2..e2f9dce99 100644 --- a/Assets/Tests/Editor/HotReload/PausePointScriptPathFormTests.cs +++ b/Assets/Tests/Editor/HotReload/PausePointScriptPathFormTests.cs @@ -55,7 +55,7 @@ public void TearDown() [Test] public void Enable_PackageSourceGivenByItsAssetPath_ArmsTheStatementOnTheEditedFileBasis() { - HotReloadSourceSnapshotter.CaptureAfterDomainReload(); + HotReloadSourceSnapshotter.CaptureAfterDomainReload(HotReloadSnapshotCaptureInputs.ReadOnMainThread()); PausePointResponse response = Enable(FixtureAssetPath, SecondStatementLine); @@ -69,7 +69,7 @@ public void Enable_PackageSourceGivenByItsAssetPath_ArmsTheStatementOnTheEditedF [Test] public void Enable_PackageSourceGivenByItsPhysicalPath_ArmsTheSameMarkerAsTheAssetPath() { - HotReloadSourceSnapshotter.CaptureAfterDomainReload(); + HotReloadSourceSnapshotter.CaptureAfterDomainReload(HotReloadSnapshotCaptureInputs.ReadOnMainThread()); PausePointResponse response = Enable(FixturePhysicalPath, SecondStatementLine); @@ -82,7 +82,7 @@ public void Enable_PackageSourceGivenByItsPhysicalPath_ArmsTheSameMarkerAsTheAss [Test] public void Enable_PackageSourceGivenByItsAbsolutePhysicalPath_ArmsTheSameMarkerAsTheAssetPath() { - HotReloadSourceSnapshotter.CaptureAfterDomainReload(); + HotReloadSourceSnapshotter.CaptureAfterDomainReload(HotReloadSnapshotCaptureInputs.ReadOnMainThread()); PausePointResponse response = Enable(ToAbsolutePath(FixturePhysicalPath), SecondStatementLine); @@ -130,7 +130,7 @@ public void Enable_PackageSourceGivenByItsPhysicalPathWithAShiftedSnapshot_MapsT [Test] public void Enable_AssetsSourceGivenByItsAbsolutePath_ArmsTheSameMarkerAsTheRelativePath() { - HotReloadSourceSnapshotter.CaptureAfterDomainReload(); + HotReloadSourceSnapshotter.CaptureAfterDomainReload(HotReloadSnapshotCaptureInputs.ReadOnMainThread()); int line = FindLineNumber(File.ReadAllText(ToAbsolutePath(AssetsFixturePath)), AssetsFixtureStatementText); Assert.That(line, Is.GreaterThan(0)); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs index 11663b5e3..592292362 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/HotReloadCompositionRoot.cs @@ -72,7 +72,8 @@ internal static HotReloadServices CreateProductionServicesWith( TransformWorkerHost.Shared, HotReloadGroupProcessorDependencies.CreateProduction, new HotReloadApplicationPlayModeQuery(), - new HotReloadSourceSnapshotCapture(HotReloadSourceSnapshotter.CaptureAfterDomainReload), + new HotReloadSourceSnapshotCapture( + () => HotReloadSourceSnapshotter.CaptureAfterDomainReload(HotReloadSnapshotCaptureInputs.ReadOnMainThread())), warmUp, compiledCallers); } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCaptureAssembly.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCaptureAssembly.cs new file mode 100644 index 000000000..8aa1f0701 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCaptureAssembly.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// One compiled assembly as the snapshot capture sees it: its name, the asset paths of its + /// sources, and whether hot reload treats it as a read-only package's, which the capture skips. + /// + internal sealed class HotReloadCaptureAssembly + { + internal HotReloadCaptureAssembly( + string name, + IReadOnlyList sourceAssetPaths, + bool isImmutablePackageAssembly) + { + Debug.Assert(!string.IsNullOrEmpty(name), "name must not be null or empty."); + Debug.Assert(sourceAssetPaths != null, "sourceAssetPaths must not be null."); + + Name = name; + SourceAssetPaths = sourceAssetPaths; + IsImmutablePackageAssembly = isImmutablePackageAssembly; + } + + internal string Name { get; } + + internal IReadOnlyList SourceAssetPaths { get; } + + internal bool IsImmutablePackageAssembly { get; } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCaptureAssembly.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCaptureAssembly.cs.meta new file mode 100644 index 000000000..13d7c95b6 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCaptureAssembly.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f8521cce78a084bdab7ef458bca19dd3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompiledSourceMap.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompiledSourceMap.cs index 19977800f..44f68a964 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompiledSourceMap.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadCompiledSourceMap.cs @@ -97,8 +97,8 @@ internal static HotReloadCompiledSources FromDocuments( pathComparison == StringComparison.Ordinal || pathComparison == StringComparison.OrdinalIgnoreCase, "pathComparison must be Ordinal or OrdinalIgnoreCase."); - string compilingRoot = NormalizeRoot(compilingProjectRoot); - string ownRoot = NormalizeRoot(projectRoot); + string compilingRoot = HotReloadPackageFolderMatch.NormalizeFolder(compilingProjectRoot); + string ownRoot = HotReloadPackageFolderMatch.NormalizeFolder(projectRoot); List packages = NormalizePackages(packageRoots, compilingRoot, ownRoot, pathComparison); List assetPaths = new List(); HashSet seen = new HashSet( @@ -177,8 +177,8 @@ private static bool TryMapProjectRelative( foreach (NormalizedPackageRoot package in packages) { int length = Math.Max( - PrefixLength(relativePath, package.RelativeToCompilingRoot, comparison), - PrefixLength(relativePath, package.RelativeToOwnRoot, comparison)); + HotReloadPackageFolderMatch.PrefixLength(relativePath, package.RelativeToCompilingRoot, comparison), + HotReloadPackageFolderMatch.PrefixLength(relativePath, package.RelativeToOwnRoot, comparison)); if (length > bestLength) { bestLength = length; @@ -196,7 +196,7 @@ private static bool TryMapAbsolute( StringComparison comparison, out MappedSource mapped) { - int rootLength = PrefixLength(absolutePath, compilingRoot, comparison); + int rootLength = HotReloadPackageFolderMatch.PrefixLength(absolutePath, compilingRoot, comparison); if (rootLength >= 0) { return TryMapProjectRelative(absolutePath.Substring(rootLength + 1), packages, comparison, out mapped); @@ -206,7 +206,7 @@ private static bool TryMapAbsolute( NormalizedPackageRoot best = default(NormalizedPackageRoot); foreach (NormalizedPackageRoot package in packages) { - int length = PrefixLength(absolutePath, package.ResolvedPath, comparison); + int length = HotReloadPackageFolderMatch.PrefixLength(absolutePath, package.ResolvedPath, comparison); if (length > bestLength) { bestLength = length; @@ -234,23 +234,6 @@ private static bool TryMapUnderPackage( return true; } - // The length of folder when path is inside it, so that the character after it is "/"; -1 - // otherwise. Why the "/": "/a/LocalPkg2/x.cs" must not count as inside "/a/LocalPkg". - private static int PrefixLength(string path, string folder, StringComparison comparison) - { - if (folder == null || path.Length <= folder.Length + 1) - { - return -1; - } - - if (path[folder.Length] != '/' || !path.StartsWith(folder, comparison)) - { - return -1; - } - - return folder.Length; - } - // Why not Path.IsPathRooted: on macOS and Linux it does not take "C:/proj/..." for rooted, // so a PDB written on Windows would lose its sources there. private static bool IsRooted(string url) @@ -277,12 +260,12 @@ private static List NormalizePackages( List packages = new List(packageRoots.Count); foreach (HotReloadSnapshotPackageRoot root in packageRoots) { - string resolvedPath = NormalizeRoot(root.ResolvedPath); + string resolvedPath = HotReloadPackageFolderMatch.NormalizeFolder(root.ResolvedPath); packages.Add(new NormalizedPackageRoot( resolvedPath, RelativeTo(resolvedPath, compilingRoot, comparison), RelativeTo(resolvedPath, ownRoot, comparison), - NormalizeRoot(root.AssetPath), + HotReloadPackageFolderMatch.NormalizeFolder(root.AssetPath), root.IsImmutable)); } @@ -292,13 +275,8 @@ private static List NormalizePackages( // The rest of path after root and "/", or null when path is not inside root. private static string RelativeTo(string path, string root, StringComparison comparison) { - int length = PrefixLength(path, root, comparison); + int length = HotReloadPackageFolderMatch.PrefixLength(path, root, comparison); return length < 0 ? null : path.Substring(length + 1); } - - private static string NormalizeRoot(string path) - { - return path.Replace('\\', '/').TrimEnd('/'); - } } } diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadPackageFolderMatch.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadPackageFolderMatch.cs new file mode 100644 index 000000000..1ed87b328 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadPackageFolderMatch.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; + +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Tells which package folder a slash-separated path lies in, and whether hot reload treats that + /// package's sources as read-only. Does no IO and asks nothing of the Package Manager, so it can + /// run on any thread. + /// + internal static class HotReloadPackageFolderMatch + { + /// + /// Returns the length of when is inside it, + /// so that the character after it is "/"; -1 otherwise, and -1 for a null folder. Why the + /// "/": "/a/LocalPkg2/x.cs" must not count as inside "/a/LocalPkg". + /// + internal static int PrefixLength(string path, string folder, StringComparison comparison) + { + Debug.Assert(path != null, "path must not be null."); + + if (folder == null || path.Length <= folder.Length + 1) + { + return -1; + } + + if (path[folder.Length] != '/' || !path.StartsWith(folder, comparison)) + { + return -1; + } + + return folder.Length; + } + + /// + /// Returns whether the asset path lies in a package hot reload treats as read-only: the + /// of the package whose asset path + /// contains it, or false when no package does, as for Assets. The same rule the PDB source + /// map applies to the first source of an assembly. + /// + internal static bool IsInImmutablePackage( + string slashNormalizedAssetPath, + IReadOnlyList packageRoots, + StringComparison comparison) + { + Debug.Assert(!string.IsNullOrEmpty(slashNormalizedAssetPath), "slashNormalizedAssetPath must not be null or empty."); + Debug.Assert(packageRoots != null, "packageRoots must not be null."); + + int bestLength = -1; + bool bestIsImmutable = false; + foreach (HotReloadSnapshotPackageRoot root in packageRoots) + { + int length = PrefixLength(slashNormalizedAssetPath, NormalizeFolder(root.AssetPath), comparison); + if (length > bestLength) + { + bestLength = length; + bestIsImmutable = root.IsImmutable; + } + } + + return bestIsImmutable; + } + + /// + /// Returns the folder with forward slashes and without a trailing slash, the form + /// expects. + /// + internal static string NormalizeFolder(string path) + { + return path.Replace('\\', '/').TrimEnd('/'); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadPackageFolderMatch.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadPackageFolderMatch.cs.meta new file mode 100644 index 000000000..2273eb0cf --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadPackageFolderMatch.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2eeca0b7f89a447aca825430aec932e7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotCaptureInputs.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotCaptureInputs.cs new file mode 100644 index 000000000..9fdc288ca --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotCaptureInputs.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.IO; + +using UnityEngine; + +using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Everything the snapshot capture needs from Unity, read once on the main thread and held as + /// values, so that the capture itself asks Unity nothing. + /// + internal sealed class HotReloadSnapshotCaptureInputs + { + internal HotReloadSnapshotCaptureInputs( + string projectRoot, + HotReloadCompileStart compileStart, + IReadOnlyList packageRoots, + StringComparison pathComparison, + IReadOnlyList assemblies, + long assemblyListingMs) + { + Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty."); + Debug.Assert(packageRoots != null, "packageRoots must not be null."); + Debug.Assert( + pathComparison == StringComparison.Ordinal || pathComparison == StringComparison.OrdinalIgnoreCase, + "pathComparison must be Ordinal or OrdinalIgnoreCase."); + Debug.Assert(assemblies != null, "assemblies must not be null."); + + ProjectRoot = projectRoot; + CompileStart = compileStart; + PackageRoots = packageRoots; + PathComparison = pathComparison; + Assemblies = assemblies; + AssemblyListingMs = assemblyListingMs; + } + + /// The root of this Editor's project, which owns the snapshots. + internal string ProjectRoot { get; } + + /// When the compile behind the current assemblies started, as far as this session knows. + internal HotReloadCompileStart CompileStart { get; } + + /// The registered packages, with whether hot reload treats each as read-only. + internal IReadOnlyList PackageRoots { get; } + + /// How paths compare on this platform: case-insensitive on Windows only. + internal StringComparison PathComparison { get; } + + /// The compiled assemblies to capture. + internal IReadOnlyList Assemblies { get; } + + /// How long listing the assemblies took, in milliseconds. + internal long AssemblyListingMs { get; } + + /// + /// Reads the inputs from Unity. Main thread only: the compile start lives in SessionState, the + /// package roots come from the Package Manager, and the assemblies from + /// CompilationPipeline.GetAssemblies, all of which Unity allows on the main thread alone. + /// + internal static HotReloadSnapshotCaptureInputs ReadOnMainThread() + { + string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + HotReloadCompileStart compileStart = HotReloadCompileStartRecord.Read(); + IReadOnlyList packageRoots = HotReloadSnapshotPackageRoots.ReadCurrent(); + StringComparison pathComparison = CurrentPathComparison(); + System.Diagnostics.Stopwatch listingWatch = System.Diagnostics.Stopwatch.StartNew(); + IReadOnlyList compilationAssemblies = HotReloadCompilationAssemblies.Current(); + listingWatch.Stop(); + List assemblies = new List(compilationAssemblies.Count); + foreach (UnityCompilationAssembly compilationAssembly in compilationAssemblies) + { + assemblies.Add(ToCaptureAssembly(compilationAssembly, packageRoots, pathComparison)); + } + + return new HotReloadSnapshotCaptureInputs( + projectRoot, + compileStart, + packageRoots, + pathComparison, + assemblies, + listingWatch.ElapsedMilliseconds); + } + + /// + /// Returns the comparison the package paths use on this platform, the same one + /// ScriptPackageRoots.ToPhysicalPath uses: case-insensitive on Windows only. + /// + internal static StringComparison CurrentPathComparison() + { + return Path.DirectorySeparatorChar == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + } + + /// + /// Turns one of Unity's compilation assemblies into what the capture reads, classifying it by + /// its first source because an asmdef keeps one assembly inside one package. + /// + internal static HotReloadCaptureAssembly ToCaptureAssembly( + UnityCompilationAssembly compilationAssembly, + IReadOnlyList packageRoots, + StringComparison pathComparison) + { + Debug.Assert(compilationAssembly != null, "compilationAssembly must not be null."); + + string[] sourceFiles = compilationAssembly.sourceFiles ?? Array.Empty(); + bool isImmutable = sourceFiles.Length > 0 && HotReloadPackageFolderMatch.IsInImmutablePackage( + sourceFiles[0].Replace('\\', '/'), + packageRoots, + pathComparison); + return new HotReloadCaptureAssembly(compilationAssembly.name, sourceFiles, isImmutable); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotCaptureInputs.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotCaptureInputs.cs.meta new file mode 100644 index 000000000..8416ac3dd --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotCaptureInputs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6e7c8fd81a9e480698a38feff62c9fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotLookupPath.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotLookupPath.cs new file mode 100644 index 000000000..239890c61 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotLookupPath.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +using UnityEngine; + +namespace io.github.hatayama.UnityCliLoop.FirstPartyTools +{ + /// + /// Names a source the way its assembly's PDB records it, from package roots read earlier on the + /// main thread, so that the snapshot capture can check a copy against the PDB without asking the + /// Package Manager. + /// + internal static class HotReloadSnapshotLookupPath + { + /// + /// Returns the same path as ScriptPackageRoots.ToPhysicalPath: project-relative when the file + /// is inside , absolute when its package lives outside + /// it, and the input itself when no package owns the path, as for Assets. Does no IO. + /// + internal static string ToPdbLookupPath( + string compiledProjectRoot, + string slashNormalizedAssetPath, + IReadOnlyList packageRoots, + StringComparison comparison) + { + Debug.Assert(!string.IsNullOrEmpty(compiledProjectRoot), "compiledProjectRoot must not be null or empty."); + Debug.Assert(!string.IsNullOrEmpty(slashNormalizedAssetPath), "slashNormalizedAssetPath must not be null or empty."); + Debug.Assert(packageRoots != null, "packageRoots must not be null."); + + List roots = new List(packageRoots.Count); + foreach (HotReloadSnapshotPackageRoot packageRoot in packageRoots) + { + roots.Add(new ScriptPackageRoot(packageRoot.ResolvedPath, packageRoot.AssetPath)); + } + + return ScriptPathNormalizer.ToPhysicalProjectRelative( + slashNormalizedAssetPath, + compiledProjectRoot, + roots, + comparison); + } + } +} diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotLookupPath.cs.meta b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotLookupPath.cs.meta new file mode 100644 index 000000000..a667a3151 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotLookupPath.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35bdf8ef1cb2a4adf9cf78bac5b907b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotSourceCheck.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotSourceCheck.cs index 63b40820c..f2f9b9660 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotSourceCheck.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSnapshotSourceCheck.cs @@ -1,3 +1,6 @@ +using System; +using System.Collections.Generic; + using UnityEngine; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools @@ -12,24 +15,31 @@ internal sealed class HotReloadSnapshotSourceCheck private readonly string _pdbPath; private readonly string _moduleVersionId; private readonly HotReloadPdbDocumentIndex _documentIndex; + private readonly IReadOnlyList _packageRoots; + private readonly StringComparison _pathComparison; internal HotReloadSnapshotSourceCheck( long suspectWritesFromUtcTicks, string dllPath, string pdbPath, string moduleVersionId, - HotReloadPdbDocumentIndex documentIndex) + HotReloadPdbDocumentIndex documentIndex, + IReadOnlyList packageRoots, + StringComparison pathComparison) { Debug.Assert(!string.IsNullOrEmpty(dllPath), "dllPath must not be null or empty."); Debug.Assert(!string.IsNullOrEmpty(pdbPath), "pdbPath must not be null or empty."); Debug.Assert(!string.IsNullOrEmpty(moduleVersionId), "moduleVersionId must not be null or empty."); Debug.Assert(documentIndex != null, "documentIndex must not be null."); + Debug.Assert(packageRoots != null, "packageRoots must not be null."); SuspectWritesFromUtcTicks = suspectWritesFromUtcTicks; _dllPath = dllPath; _pdbPath = pdbPath; _moduleVersionId = moduleVersionId; _documentIndex = documentIndex; + _packageRoots = packageRoots; + _pathComparison = pathComparison; } /// @@ -40,15 +50,24 @@ internal HotReloadSnapshotSourceCheck( /// /// Returns whether the bytes match the checksum the compiled assembly's PDB recorded for the - /// source. A source the PDB has no usable checksum for is not confirmed. Main thread only. + /// source. A source the PDB has no usable checksum for is not confirmed. Asks nothing of the + /// Package Manager: the package roots are the ones the caller read on the main thread. /// internal bool IsCompiledSource(string projectRoot, string slashNormalizedRelativePath, byte[] sourceBytes) { + // Why the root that compiled the assembly: a Virtual Player reads the main project's + // assemblies, whose PDB names sources relative to the main project (see + // HotReloadSourceBaseline.CompareWithCompiledDocument). + string compiledProjectRoot = CompiledAssemblyLayout.Resolve(projectRoot).MainProjectRoot; + string pdbLookupPath = HotReloadSnapshotLookupPath.ToPdbLookupPath( + compiledProjectRoot, + slashNormalizedRelativePath, + _packageRoots, + _pathComparison); // Why NoDocumentInPdb counts as not compiled: a file without method bodies has no document, // and a copy that cannot be confirmed is safer treated as edited than trusted. - return HotReloadSourceBaseline.CompareWithCompiledDocument( - projectRoot, - slashNormalizedRelativePath, + return HotReloadSourceBaseline.CompareWithCompiledDocumentAtLookupPath( + pdbLookupPath, _dllPath, _pdbPath, _moduleVersionId, diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceBaseline.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceBaseline.cs index 7fe4f34c8..a5c2228ce 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceBaseline.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceBaseline.cs @@ -163,8 +163,9 @@ internal static HotReloadSnapshotMissReason CompareWithCompiledDocument( // Packages/ path. Why the Package Manager is asked rather than the package roots a // run captures: those live in the main hot-reload assembly, which this one cannot see, and // every caller already runs on the Unity main thread the Package Manager requires: a run's - // group step, the pause-point port, which the pause-point tools and a run's patch step call, - // and the snapshot capture after a domain reload. + // group step, and the pause-point port, which the pause-point tools and a run's patch step + // call. The snapshot capture names the source from package roots it was handed instead and + // calls the second stage directly, so that it asks the Package Manager nothing. // Why the root that compiled the assembly rather than the snapshot owner's: the PDB spells a // source inside the compiling project as a path relative to that project's root (behind a // leading "./"), which the lookup matches as the end of the url. A Multiplayer Play Mode @@ -174,6 +175,32 @@ internal static HotReloadSnapshotMissReason CompareWithCompiledDocument( // document ends with. For an ordinary project the two roots are the same. string compiledProjectRoot = CompiledAssemblyLayout.Resolve(projectRoot).MainProjectRoot; string pdbLookupPath = ScriptPackageRoots.ToPhysicalPath(compiledProjectRoot, slashNormalizedRelativePath); + return CompareWithCompiledDocumentAtLookupPath( + pdbLookupPath, + targetDllPath, + pdbPath, + moduleVersionId, + sourceBytes, + documentIndex); + } + + /// + /// Answers the same as for a source already named + /// the way the PDB records it. Asks nothing of the Package Manager, so it can run off the + /// main thread. + /// + internal static HotReloadSnapshotMissReason CompareWithCompiledDocumentAtLookupPath( + string pdbLookupPath, + string targetDllPath, + string pdbPath, + string moduleVersionId, + byte[] sourceBytes, + HotReloadPdbDocumentIndex documentIndex) + { + Debug.Assert(!string.IsNullOrEmpty(pdbLookupPath), "pdbLookupPath must not be null or empty."); + Debug.Assert(sourceBytes != null, "sourceBytes must not be null."); + Debug.Assert(documentIndex != null, "documentIndex must not be null."); + if (!documentIndex.TryFindDocument( targetDllPath, pdbPath, diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.Incremental.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.Incremental.cs index 62db46410..5a20342e7 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.Incremental.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.Incremental.cs @@ -15,7 +15,7 @@ internal static void CaptureIncrementally( string projectRoot, string assemblySnapshotDirectory, string previousSnapshotDirectory, - string[] sourceFiles, + IReadOnlyList sourceFiles, string assemblyName, HotReloadSnapshotSourceCheck check, HotReloadSnapshotCaptureStats stats) @@ -46,7 +46,7 @@ internal static void CaptureIncrementally( HashSet listedNames = new HashSet(StringComparer.Ordinal); SourceCopyTally tally = new SourceCopyTally(); - List manifestLines = new List(sourceFiles.Length); + List manifestLines = new List(sourceFiles.Count); foreach (string projectRelativeSourcePath in sourceFiles) { string normalizedRelativePath = projectRelativeSourcePath.Replace('\\', '/'); diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.cs index 40886d6be..407b3a22b 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotCopier.cs @@ -18,7 +18,7 @@ internal static partial class HotReloadSourceSnapshotCopier internal static void CaptureAtomically( string projectRoot, string assemblySnapshotDirectory, - string[] sourceFiles, + IReadOnlyList sourceFiles, string assemblyName, HotReloadSnapshotSourceCheck check, HotReloadSnapshotCaptureStats stats) @@ -39,7 +39,7 @@ internal static void CaptureAtomically( Directory.CreateDirectory(temporaryDirectory); stats.PublishTicks += prepareWatch.Elapsed.Ticks; SourceCopyTally tally = new SourceCopyTally(); - List manifestLines = new List(sourceFiles.Length); + List manifestLines = new List(sourceFiles.Count); foreach (string projectRelativeSourcePath in sourceFiles) { SourceCopyOutcome outcome = CopySourceFileByteExact( diff --git a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs index 54d3b0e8d..bdae7bfdc 100644 --- a/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs +++ b/Packages/src/Editor/FirstPartyTools/HotReload/Shared/HotReloadSourceSnapshotter.cs @@ -8,7 +8,6 @@ using UnityEngine; -using UnityCompilationAssembly = UnityEditor.Compilation.Assembly; using PackageManagerPackageInfo = UnityEditor.PackageManager.PackageInfo; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools @@ -27,51 +26,47 @@ internal static partial class HotReloadSourceSnapshotter /// selection, the sibling scan and the skip check treat that file as changed. The method-diff /// baseline checks every copy against the PDB when it uses one. /// Logs how long each phase took, even when the list is empty. - /// Returns false when Unity listed no compilation assembly (as while it compiles). + /// Returns false when the inputs list no compilation assembly (as while Unity compiles). + /// Asks Unity nothing beyond what carries. /// - internal static bool CaptureAfterDomainReload() + internal static bool CaptureAfterDomainReload(HotReloadSnapshotCaptureInputs inputs) { - System.Diagnostics.Stopwatch totalWatch = System.Diagnostics.Stopwatch.StartNew(); - string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); - System.Diagnostics.Stopwatch getAssembliesWatch = System.Diagnostics.Stopwatch.StartNew(); - IReadOnlyList assemblies = HotReloadCompilationAssemblies.Current(); - getAssembliesWatch.Stop(); - HotReloadSnapshotCaptureStats stats = CaptureAssemblies( - projectRoot, - assemblies, - HotReloadCompileStartRecord.Read(), - HotReloadPdbDocumentIndex.Shared); - totalWatch.Stop(); + Debug.Assert(inputs != null, "inputs must not be null."); + + System.Diagnostics.Stopwatch captureWatch = System.Diagnostics.Stopwatch.StartNew(); + HotReloadSnapshotCaptureStats stats = CaptureAssemblies(inputs, HotReloadPdbDocumentIndex.Shared); + captureWatch.Stop(); + // Why the listing time is added: the assemblies are now listed while the inputs are read, + // before this method runs, and totalMs has always included that listing. VibeLogger.LogInfo( HotReloadConstants.VibeLogSourceSnapshotBreakdown, "Hot reload source snapshot capture breakdown.", - stats.ToVibeLogContext(getAssembliesWatch.ElapsedMilliseconds, totalWatch.ElapsedMilliseconds)); - return assemblies.Count > 0; + stats.ToVibeLogContext( + inputs.AssemblyListingMs, + inputs.AssemblyListingMs + captureWatch.ElapsedMilliseconds)); + return inputs.Assemblies.Count > 0; } - // Why separate from CaptureAfterDomainReload: the project root and the compilation assemblies come - // from Unity, so taking them as arguments lets tests drive the per-assembly loop in a temporary root. + // Why separate from CaptureAfterDomainReload: taking the document index as an argument lets + // tests drive the per-assembly loop in a temporary root without writing the shared index. // Returns how long each phase took and how much the capture did. internal static HotReloadSnapshotCaptureStats CaptureAssemblies( - string projectRoot, - IEnumerable assemblies, - HotReloadCompileStart compileStart, + HotReloadSnapshotCaptureInputs inputs, HotReloadPdbDocumentIndex documentIndex) { - Debug.Assert(!string.IsNullOrEmpty(projectRoot), "projectRoot must not be null or empty."); - Debug.Assert(assemblies != null, "assemblies must not be null."); + Debug.Assert(inputs != null, "inputs must not be null."); Debug.Assert(documentIndex != null, "documentIndex must not be null."); - string snapshotRoot = HotReloadSourceSnapshotLayout.Root(projectRoot); + string snapshotRoot = HotReloadSourceSnapshotLayout.Root(inputs.ProjectRoot); Directory.CreateDirectory(snapshotRoot); HotReloadSnapshotCaptureStats stats = new HotReloadSnapshotCaptureStats(); - foreach (UnityCompilationAssembly assembly in assemblies) + foreach (HotReloadCaptureAssembly assembly in inputs.Assemblies) { stats.Assemblies++; try { - CaptureAssemblyIfNeeded(projectRoot, snapshotRoot, assembly, compileStart, documentIndex, stats); + CaptureAssemblyIfNeeded(inputs, snapshotRoot, assembly, documentIndex, stats); } catch (Exception ex) when (IsSkippableAssemblyCaptureException(ex)) { @@ -80,7 +75,7 @@ internal static HotReloadSnapshotCaptureStats CaptureAssemblies( // Continuing preserves the fail-closed invariant because the method-diff baseline // rejects missing snapshots by PDB checksum rather than using them for an incorrect diff. UnityEngine.Debug.LogWarning( - $"[UnityCliLoop] Snapshot capture failed for assembly {assembly.name}: {ex.Message}"); + $"[UnityCliLoop] Snapshot capture failed for assembly {assembly.Name}: {ex.Message}"); } } @@ -97,21 +92,23 @@ ex is UnauthorizedAccessException || } private static void CaptureAssemblyIfNeeded( - string projectRoot, + HotReloadSnapshotCaptureInputs inputs, string snapshotRoot, - UnityCompilationAssembly assembly, - HotReloadCompileStart compileStart, + HotReloadCaptureAssembly assembly, HotReloadPdbDocumentIndex documentIndex, HotReloadSnapshotCaptureStats stats) { - string[] sourceFiles = assembly.sourceFiles; - if (sourceFiles == null || sourceFiles.Length == 0) + IReadOnlyList sourceFiles = assembly.SourceAssetPaths; + if (sourceFiles.Count == 0) { return; } + string projectRoot = inputs.ProjectRoot; + // Why only a read: the inputs classified the assembly from the package roots, so the + // capture no longer asks the Package Manager here. System.Diagnostics.Stopwatch packageLookupWatch = System.Diagnostics.Stopwatch.StartNew(); - bool immutable = ShouldSkipImmutablePackageSources(sourceFiles); + bool immutable = assembly.IsImmutablePackageAssembly; stats.PackageLookupTicks += packageLookupWatch.Elapsed.Ticks; if (immutable) { @@ -122,8 +119,8 @@ private static void CaptureAssemblyIfNeeded( // Why projectRoot stays the snapshot owner: only the compiled assemblies move to the main // project for a Virtual Player; its snapshots stay under its own root. CompiledAssemblyLayout layout = CompiledAssemblyLayout.Resolve(projectRoot); - string dllPath = layout.DllPath(assembly.name); - string pdbPath = layout.PdbPath(assembly.name); + string dllPath = layout.DllPath(assembly.Name); + string pdbPath = layout.PdbPath(assembly.Name); if (!File.Exists(dllPath) || !File.Exists(pdbPath)) { return; @@ -132,7 +129,7 @@ private static void CaptureAssemblyIfNeeded( FileInfo dllInfo = new FileInfo(dllPath); long dllMtimeTicks = dllInfo.LastWriteTimeUtc.Ticks; long dllByteLength = dllInfo.Length; - string stampPath = StampPath(snapshotRoot, assembly.name); + string stampPath = StampPath(snapshotRoot, assembly.Name); // Why stamp short-circuits Cecil: a false stamp (identical mtime+length with different // bytes) is vanishingly rare, and even then LoadVerifiedSnapshotSource rejects via PDB @@ -149,20 +146,22 @@ private static void CaptureAssemblyIfNeeded( System.Diagnostics.Stopwatch mvidWatch = System.Diagnostics.Stopwatch.StartNew(); string mvid = HotReloadAssemblyMvid.Read(dllPath); stats.MvidTicks += mvidWatch.Elapsed.Ticks; - string assemblySnapshotDirectory = HotReloadSourceSnapshotLayout.AssemblyDirectory(projectRoot, assembly.name, mvid); + string assemblySnapshotDirectory = HotReloadSourceSnapshotLayout.AssemblyDirectory(projectRoot, assembly.Name, mvid); if (!Directory.Exists(assemblySnapshotDirectory)) { HotReloadSnapshotSourceCheck check = new HotReloadSnapshotSourceCheck( - compileStart.SuspectWritesFrom(dllMtimeTicks), + inputs.CompileStart.SuspectWritesFrom(dllMtimeTicks), dllPath, pdbPath, mvid, - documentIndex); + documentIndex, + inputs.PackageRoots, + inputs.PathComparison); System.Diagnostics.Stopwatch previousWatch = System.Diagnostics.Stopwatch.StartNew(); string previousSnapshotDirectory = FindReusablePreviousSnapshotDirectory( projectRoot, stampPath, - assembly.name, + assembly.Name, assemblySnapshotDirectory); stats.StampTicks += previousWatch.Elapsed.Ticks; if (previousSnapshotDirectory == null) @@ -171,7 +170,7 @@ private static void CaptureAssemblyIfNeeded( projectRoot, assemblySnapshotDirectory, sourceFiles, - assembly.name, + assembly.Name, check, stats); } @@ -182,14 +181,14 @@ private static void CaptureAssemblyIfNeeded( assemblySnapshotDirectory, previousSnapshotDirectory, sourceFiles, - assembly.name, + assembly.Name, check, stats); } stats.AssembliesCaptured++; System.Diagnostics.Stopwatch cleanupWatch = System.Diagnostics.Stopwatch.StartNew(); - DeleteStaleSnapshotDirectories(snapshotRoot, assembly.name, assemblySnapshotDirectory); + DeleteStaleSnapshotDirectories(snapshotRoot, assembly.Name, assemblySnapshotDirectory); stats.CleanupTicks += cleanupWatch.Elapsed.Ticks; } diff --git a/docs/vibe-logs.md b/docs/vibe-logs.md index b750dc519..3ca5cb287 100644 --- a/docs/vibe-logs.md +++ b/docs/vibe-logs.md @@ -103,8 +103,11 @@ command ran against: just before `hot_reload_source_snapshot_captured`. It also appears when Unity listed no compilation assembly, in which case no captured entry follows, and it does not appear when the capture threw. It says where the capture spent its time: - - `getAssembliesMs`: listing the compilation assemblies from Unity. - - `packageLookupMs`: asking Package Manager whether each assembly's sources are immutable. + - `getAssembliesMs`: listing the compilation assemblies from Unity, which happens while the + capture's inputs are read on the main thread, before any copying starts. + - `packageLookupMs`: reading whether each assembly's sources are immutable. The inputs decide + that from the package roots they read from Package Manager once, so this is close to 0; it + no longer asks Package Manager for each assembly. - `stampMs`: reading and writing the stamps beside the snapshots, and reading the previous build's manifest. - `mvidMs`: reading the MVID of each compiled dll whose stamp did not match. @@ -117,7 +120,8 @@ command ran against: previous build's directory there, writing the manifest, and moving the directory into place. - `cleanupMs`: deleting the snapshot directories of earlier builds, and the copies of sources the assembly no longer lists. - - `totalMs`: the whole capture. + - `totalMs`: listing the assemblies plus the whole per-assembly loop. Reading the compile start + and the package roots for the inputs is not counted here. - `assemblies`: the compilation assemblies listed. - `assembliesImmutable`: those skipped because their sources belong to an immutable package. - `assembliesUnchanged`: those skipped because their stamp matched the compiled dll. @@ -126,7 +130,7 @@ command ran against: - `filesCopied` and `bytesCopied`: the sources copied and their total size. - `filesChecked`: the sources checked against the PDB. The phases add up to `totalMs` except for the loop overhead; `captureMs` in the captured entry - also includes writing this breakdown entry. + also includes reading the compile start and the package roots, and writing this breakdown entry. After a domain reload, the warm-up writes one `hot_reload_warm_up_complete` with the target assemblies, each item's `outcome` (`done`, `cancelled` or `failed`) and time, and `cancelledBy` (`run`, `beforeAssemblyReload`, `compilationStarted`, or null), or one