Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 67 additions & 65 deletions Assets/Tests/Editor/HotReload/HotReloadCallSiteScannerTests.cs

Large diffs are not rendered by default.

18 changes: 7 additions & 11 deletions Assets/Tests/Editor/HotReload/HotReloadCallSiteWarmUpItemTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ public void TearDown()
[Test]
public void LoadCallSites_LoadsTheDllOnce_AndAgainReadsNothing()
{
HotReloadWarmUpTarget[] targets = { CopyTarget() };
string[] dllPaths = { CopyDll() };

int first = HotReloadCallSiteWarmUpItem.LoadCallSites(targets, _cache, CancellationToken.None);
int first = HotReloadCompiledCallers.LoadCallSites(dllPaths, _cache, CancellationToken.None);
int afterFirst = _cache.LoadCount;
HotReloadCallSiteWarmUpItem.LoadCallSites(targets, _cache, CancellationToken.None);
HotReloadCompiledCallers.LoadCallSites(dllPaths, _cache, CancellationToken.None);

Assert.That(first, Is.EqualTo(1), "loaded targets");
Assert.That(afterFirst, Is.EqualTo(1), "reads after the first load");
Expand All @@ -63,27 +63,23 @@ public void LoadCallSites_LoadsTheDllOnce_AndAgainReadsNothing()
[Test]
public void LoadCallSites_WhenCancelled_ThrowsAndLoadsNothing()
{
HotReloadWarmUpTarget[] targets = { CopyTarget() };
string[] dllPaths = { CopyDll() };

Assert.Throws<OperationCanceledException>(
() => HotReloadCallSiteWarmUpItem.LoadCallSites(targets, _cache, new CancellationToken(true)));
() => HotReloadCompiledCallers.LoadCallSites(dllPaths, _cache, new CancellationToken(true)));

Assert.That(_cache.LoadCount, Is.EqualTo(0), "reads");
}

private HotReloadWarmUpTarget CopyTarget()
private string CopyDll()
{
string source = Path.Combine(
Path.GetFullPath(Path.Combine(Application.dataPath, "..")),
HotReloadConstants.ScriptAssembliesRelativeDirectory,
TestAssemblyName + HotReloadConstants.CompiledAssemblyExtension);
string dllPath = Path.Combine(_tempDirectory, Path.GetFileName(source));
File.Copy(source, dllPath);
return new HotReloadWarmUpTarget(
TestAssemblyName,
dllPath,
Path.ChangeExtension(dllPath, ".pdb"),
Array.Empty<string>());
return dllPath;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using NUnit.Framework;

using UnityEngine;
using Stopwatch = System.Diagnostics.Stopwatch;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.ToolContracts;
Expand All @@ -26,6 +27,7 @@ public class HotReloadCallerNoteBackfillE2ETests
private const string CacheFixtureFileName = "HotReloadCallSiteCacheE2EFixture.cs";
private const string CrossAssemblyName = "UnityCLILoop.Tests.Editor.HotReload.CallSiteCrossAssembly";
private const string ReadBody = "return 1;";
private const int HeldReadMilliseconds = 100;

private HotReloadDomainTestScope _scope;

Expand Down Expand Up @@ -79,7 +81,7 @@ public async Task Run_WhoseNoteStepRefusedADll_BackfillsItSoTheNextRunReadsNothi
Is.EqualTo(new[] { CrossAssemblyName }),
"Precondition: the refused dll must be the cross-assembly caller.");

await HotReloadCompositionRoot.Services.CallSiteBackfill.Completion;
await HotReloadCompositionRoot.Services.CompiledCallers.Backfill.Completion;
Assert.That(
HotReloadCompiledCallSiteCache.Shared.LoadCount - before,
Is.EqualTo(2),
Expand Down Expand Up @@ -144,6 +146,54 @@ public async Task Run_WhileTheBackfillOfThePreviousRunIsInFlight_WaitsForThatDll
Assert.That((string)context["cancelledBy"], Is.EqualTo(HotReloadConstants.WarmUpCancelledByRun), "cancelledBy");
}

/// <summary>
/// What: a run that comes while a backfill read is in flight counts the wait for that read
/// in its warm_up_yield step, so a slow start shows where its time went.
/// </summary>
[Test]
public async Task Run_WhileABackfillReadIsInFlight_CountsTheWaitInWarmUpYield()
{
string fixturePath = FixturePath(CacheFixtureFileName);
string source = File.ReadAllText(fixturePath);
Assert.That(source, Does.Contain(ReadBody), "Precondition: the Read body anchor must exist.");
PendingDllLoader loader = new PendingDllLoader();
loader.PendingFor.Add("a.dll");
HotReloadCallSiteBackfill backfill = new HotReloadCallSiteBackfill(loader.Load);
_scope = HotReloadDomainTestScope.WithCallSiteBackfill(backfill);
await _scope.InstalledWarmUpStopped;
VibeLogger.ClearMemoryLogs();
backfill.Start(new[] { "a.dll" }, "corr");

string editedPath = HotReloadTestSourceWriter.WriteEditedSource(
"CallerNoteBackfillE2E.cs",
source.Replace(ReadBody, "return 4;"));
Task<HotReloadOrchestratorResult> run = HotReloadCompositionRoot.Services.Orchestrator.RunAsync(
new[] { fixturePath },
editedPath,
CancellationToken.None);
Assert.That(run.IsCompleted, Is.False, "Precondition: the run must be waiting for the read.");
// Why measure here: the run's warm_up_yield step started before RunAsync returned and
// ends only after the release below, so it covers at least this span.
Stopwatch held = Stopwatch.StartNew();
await Task.Delay(HeldReadMilliseconds);
long heldMs = held.ElapsedMilliseconds;
loader.Release("a.dll");
HotReloadOrchestratorResult result = await AwaitRunAsync(run);

JArray details = JArray.Parse(VibeLogger.GetLogsForAi(HotReloadConstants.VibeLogTimingDetail));
Assert.That(details.Count, Is.EqualTo(1), "timing details\n" + FormatOutcomes(result));
long warmUpYieldMs = -1;
foreach (JToken step in (JArray)details[0]["context"]["steps"])
{
if ((string)step["step"] == HotReloadConstants.TimingDetailStepWarmUpYield)
{
warmUpYieldMs = (long)step["ms"];
}
}

Assert.That(warmUpYieldMs, Is.GreaterThanOrEqualTo(heldMs), "the wait for the read counts in warm_up_yield");
}

private static async Task<HotReloadOrchestratorResult> RunWithReadReturningAsync(
string fixturePath,
string source,
Expand Down
108 changes: 108 additions & 0 deletions Assets/Tests/Editor/HotReload/HotReloadCompiledCallersTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.HotReload
{
/// <summary>
/// Contract of the compiled-caller analysis at its entry point: when a run takes the call-site
/// cache hold relative to a background read, and when ending or disposing the run releases it
/// and starts the background reads. Each test uses a cache and a loader of its own and touches
/// neither the installed services nor the shared cache.
/// </summary>
public sealed class HotReloadCompiledCallersTests
{
private PendingDllLoader _loader;
private HotReloadCompiledCallSiteCache _cache;
private HotReloadCallSiteBackfill _backfill;
private HotReloadCompiledCallers _compiledCallers;

[SetUp]
public void SetUp()
{
_loader = new PendingDllLoader();
_cache = new HotReloadCompiledCallSiteCache(HotReloadCompiledCallSiteCache.DefaultBudgetBytes);
_backfill = new HotReloadCallSiteBackfill(_loader.Load);
_compiledCallers = new HotReloadCompiledCallers(_cache, _backfill);
VibeLogger.ClearMemoryLogs();
}

[TearDown]
public void TearDown()
{
_loader.ReleaseAll();
VibeLogger.ClearMemoryLogs();
}

/// <summary>
/// What: a run begins only after the background read in flight ended, so the hold is not
/// taken while the read holds the cache, and the backfill stops before its next dll.
/// </summary>
[Test]
public async Task BeginRunAsync_WhileABackgroundReadIsInFlight_TakesTheHoldOnlyAfterTheReadEnds()
{
_loader.PendingFor.Add("a.dll");
_backfill.Start(new[] { "a.dll", "b.dll" }, "corr");

Task<HotReloadCompiledCallersRun> begin = _compiledCallers.BeginRunAsync();
Assert.That(begin.IsCompleted, Is.False, "the run waits for the read in flight");
Assert.That(_cache.HoldDepth, Is.EqualTo(0), "no hold while the read is in flight");

_loader.Release("a.dll");
using (HotReloadCompiledCallersRun run = await begin)
{
Assert.That(_cache.HoldDepth, Is.EqualTo(1), "the run holds the entries");
Assert.That(_loader.Ran, Is.EqualTo(new[] { "a.dll" }), "paths read");
}

Assert.That(_cache.HoldDepth, Is.EqualTo(0), "disposing the run releases the hold");
}

/// <summary>
/// What: ending a run releases its hold before the background read of the dlls its notes
/// could not read starts, so those reads evict as reads outside a run do.
/// </summary>
[Test]
public async Task End_ReleasesTheHoldBeforeTheBackgroundReadOfTheRefusedDllsStarts()
{
List<int> holdDepthAtLoad = new List<int>();
HotReloadCallSiteBackfill backfill = new HotReloadCallSiteBackfill((dllPath, ct) =>
{
holdDepthAtLoad.Add(_cache.HoldDepth);
return Task.CompletedTask;
});
HotReloadCompiledCallers compiledCallers = new HotReloadCompiledCallers(_cache, backfill);
HotReloadCompiledCallersRun run = await compiledCallers.BeginRunAsync();
run.RememberRefusedDllPathsForTesting(new[] { "refused.dll" });

run.End("corr");
await backfill.Completion;
run.Dispose();

Assert.That(holdDepthAtLoad, Is.EqualTo(new[] { 0 }), "the refused dll is read once, with no hold open");
Assert.That(_cache.HoldDepth, Is.EqualTo(0));
}

/// <summary>
/// What: a run that ends without End (an exception) releases its hold and reads none of the
/// dlls its notes could not read.
/// </summary>
[Test]
public async Task Dispose_WithoutEnd_ReleasesTheHoldAndStartsNoBackgroundRead()
{
HotReloadCompiledCallersRun run = await _compiledCallers.BeginRunAsync();
run.RememberRefusedDllPathsForTesting(new[] { "refused.dll" });

run.Dispose();

Assert.That(_cache.HoldDepth, Is.EqualTo(0), "the hold is released");
Assert.That(_loader.Ran, Is.Empty, "no background read starts");
Assert.That(_backfill.State, Is.EqualTo(HotReloadCallSiteBackfillState.Idle));
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ private static HotReloadServices CreateServicesSharing(HotReloadDomain domain)
new HotReloadApplicationPlayModeQuery(),
new HotReloadSourceSnapshotCapture(() => true),
installed.WarmUp,
installed.CallSiteBackfill);
installed.CompiledCallers);
}

private static HotReloadServices CreateServicesOwning(HotReloadDomain domain)
Expand All @@ -199,7 +199,7 @@ private static HotReloadServices CreateServicesOwning(HotReloadDomain domain)
new HotReloadApplicationPlayModeQuery(),
new HotReloadSourceSnapshotCapture(() => true),
HotReloadWarmUpTestDoubles.CreateInert(),
HotReloadCompositionRoot.CreateProductionCallSiteBackfill());
HotReloadCompiledCallers.CreateProduction());
}

private static HotReloadIntroducedTypeArtifact CreateArtifact(string metadataName)
Expand Down
6 changes: 3 additions & 3 deletions Assets/Tests/Editor/HotReload/HotReloadDomainTestScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ private HotReloadDomainTestScope(HotReloadServices services)
HotReloadServices installed = HotReloadCompositionRoot.Services;
InstalledWarmUpStopped = Task.WhenAll(
installed.WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope),
installed.CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestScope));
installed.CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerTestScope));

// The capture is main-thread only, and a run inside the scope normalizes script paths
// against these roots on the background threads it switches to.
Expand All @@ -57,7 +57,7 @@ internal static HotReloadDomainTestScope WithWarmUp(HotReloadWarmUp warmUp)
return new HotReloadDomainTestScope(
HotReloadCompositionRoot.CreateProductionServicesWith(
warmUp,
HotReloadCompositionRoot.CreateProductionCallSiteBackfill()));
HotReloadCompiledCallers.CreateProduction()));
}

/// <summary>
Expand All @@ -69,7 +69,7 @@ internal static HotReloadDomainTestScope WithCallSiteBackfill(HotReloadCallSiteB
return new HotReloadDomainTestScope(
HotReloadCompositionRoot.CreateProductionServicesWith(
HotReloadWarmUpTestDoubles.CreateInert(),
callSiteBackfill));
new HotReloadCompiledCallers(HotReloadCompiledCallSiteCache.Shared, callSiteBackfill)));
}

public void Dispose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void StartPump()
// they read the shared caches on a pool thread (after a reload, or after a hot reload
// run made just before the tests), and the tests that count those reads must not race them.
_ = HotReloadCompositionRoot.Services.WarmUp.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestRun);
_ = HotReloadCompositionRoot.Services.CallSiteBackfill.Shutdown(HotReloadConstants.WarmUpShutdownTriggerTestRun);
_ = HotReloadCompositionRoot.Services.CompiledCallers.StopBackgroundReadsAsync(HotReloadConstants.WarmUpShutdownTriggerTestRun);
StopPump();
// EditorApplication.SignalTick is [ThreadSafe] in the Editor bindings, which is what
// allows the timer thread to call it.
Expand Down
12 changes: 6 additions & 6 deletions Assets/Tests/Editor/HotReload/HotReloadGroupProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1321,9 +1321,9 @@ private static HotReloadSignatureChangeGate.SignatureChangeGateResult CreateGate
{
return HotReloadSignatureChangeGate.SignatureChangeGateResult.WarningsOnly(
new List<HotReloadStaleSignatureCallSites>(),
new List<HotReloadCallSiteScanner.CallSiteHit>
new List<HotReloadCallSiteHit>
{
new HotReloadCallSiteScanner.CallSiteHit
new HotReloadCallSiteHit
{
CallerAssemblyName = AssemblyName,
CallerMethodKey = CallerKey,
Expand All @@ -1340,7 +1340,7 @@ private static HotReloadSignatureChangeGate.SignatureChangeGateResult CreateEmpt
{
return HotReloadSignatureChangeGate.SignatureChangeGateResult.WarningsOnly(
new List<HotReloadStaleSignatureCallSites>(),
new List<HotReloadCallSiteScanner.CallSiteHit>(),
new List<HotReloadCallSiteHit>(),
new HashSet<HotReloadQualifiedMethodIdentity>());
}

Expand All @@ -1353,9 +1353,9 @@ private static HotReloadSignatureChangeGate.SignatureChangeGateResult CreateGate
};
return HotReloadSignatureChangeGate.SignatureChangeGateResult.WarningsOnly(
new List<HotReloadStaleSignatureCallSites>(),
new List<HotReloadCallSiteScanner.CallSiteHit>
new List<HotReloadCallSiteHit>
{
new HotReloadCallSiteScanner.CallSiteHit
new HotReloadCallSiteHit
{
CallerAssemblyName = AssemblyName,
CallerMethodKey = CallerKey,
Expand Down Expand Up @@ -2037,7 +2037,7 @@ public void SignatureChangeGateResult_Retried_ReportsTheWorkerRetryWithoutFailin
null,
new List<HotReloadMethodOutcome>(),
new List<HotReloadStaleSignatureCallSites>(),
new List<HotReloadCallSiteScanner.CallSiteHit>(),
new List<HotReloadCallSiteHit>(),
new HashSet<HotReloadQualifiedMethodIdentity>(),
new List<string>());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ private protected static HotReloadGroupStageCollaborators WithInternalAccessGran
collaborators.EditorStateSnapshotCapture,
collaborators.CommitPolicy,
collaborators.PlayMode,
internalAccessGrant);
internalAccessGrant,
collaborators.CompiledCallers);
}

private protected static Task<HotReloadOrchestratorResult> RunReloadAsync(
Expand Down
Loading
Loading