perf(ui): topology page — staleness off the live loop + parallelized one-shot

This commit is contained in:
Joseph Doherty
2026-06-26 16:53:44 -04:00
parent eb59c4244f
commit 99254b71de
2 changed files with 210 additions and 15 deletions
@@ -387,6 +387,93 @@ public class TopologyPageTests : BunitContext
Assert.Contains("Changed", markup);
}
/// <summary>
/// Seeds one site with one deployed (Enabled) instance whose deployment
/// comparison resolves: a deployed snapshot plus a current flatten. Used by
/// the staleness-lifecycle tests below, which assert on how OFTEN the
/// comparison runs (via the snapshot repository call count), not on the
/// resulting Stale/Current value.
/// </summary>
private void SeedOneDeployedInstanceWithComparison()
{
SeedRepos(
sites: new[] { new Site("Plant-A", "plant-a") { Id = 1 } },
instances: new[]
{
new Instance("Pump-001") { Id = 100, SiteId = 1, State = InstanceState.Enabled }
});
var deployedConfig = new FlattenedConfiguration { InstanceUniqueName = "Pump-001" };
_deployRepo.GetDeployedSnapshotByInstanceIdAsync(100, Arg.Any<CancellationToken>())
.Returns(Task.FromResult<DeployedConfigSnapshot?>(
new DeployedConfigSnapshot("dep-1", "hash-old",
JsonSerializer.Serialize(deployedConfig))));
var currentConfig = new FlattenedConfiguration { InstanceUniqueName = "Pump-001" };
_pipeline.FlattenAndValidateAsync(100, Arg.Any<CancellationToken>())
.Returns(Task.FromResult(Result<FlatteningPipelineResult>.Success(
new FlatteningPipelineResult(currentConfig, "hash-new", ValidationResult.Success()))));
}
[Fact]
public async Task LiveUpdate_DoesNotRecomputeStaleness()
{
// Performance regression guard: staleness (the expensive per-instance
// re-flatten via DeploymentService.GetDeploymentComparisonAsync) must NOT
// run on the 15s live-update poll. The live-update path refreshes only the
// cheap deployed state (LoadDeployedStateAsync), which the timer invokes
// directly. The comparison reaches into the snapshot repository, so its
// call count is the proxy for "did staleness recompute".
SeedOneDeployedInstanceWithComparison();
var cut = Render<TopologyPage>();
// Initial load computed staleness exactly once.
await _deployRepo.Received(1)
.GetDeployedSnapshotByInstanceIdAsync(100, Arg.Any<CancellationToken>());
// Drive the EXACT code path the live-update timer runs.
await cut.InvokeAsync(() => cut.Instance.LoadDeployedStateAsync());
// Staleness was NOT recomputed by the live-update refresh — still once.
await _deployRepo.Received(1)
.GetDeployedSnapshotByInstanceIdAsync(100, Arg.Any<CancellationToken>());
}
[Fact]
public void DeployedState_Renders_AfterSplit()
{
// After splitting the load into fast-state vs expensive-staleness, the
// cheap deployed state (hierarchy + State badge) must still render.
SeedOneDeployedInstanceWithComparison();
var cut = Render<TopologyPage>();
FindToggleForLabel(cut, "Plant-A")!.Click();
Assert.Contains("Pump-001", cut.Markup);
Assert.Contains("Enabled", cut.Markup);
// The deployed instance shows a staleness badge (Stale or Current).
Assert.Matches("Stale|Current", cut.Markup);
}
[Fact]
public void ManualRecheck_RecomputesStaleness()
{
// The "Re-check staleness" button lets operators refresh staleness on
// demand (since it's off the live loop). Clicking it runs the comparison
// again — a second snapshot-repository call.
SeedOneDeployedInstanceWithComparison();
var cut = Render<TopologyPage>();
_deployRepo.Received(1)
.GetDeployedSnapshotByInstanceIdAsync(100, Arg.Any<CancellationToken>());
cut.Find("button[aria-label='Re-check staleness']").Click();
_deployRepo.Received(2)
.GetDeployedSnapshotByInstanceIdAsync(100, Arg.Any<CancellationToken>());
}
[Fact]
public void LegacyInstancesRoute_IsDeclaredOnTopologyPage()
{