diff --git a/Darling/Darling.Tests/ViewerCpuSchedulerTests.cs b/Darling/Darling.Tests/ViewerCpuSchedulerTests.cs
index 0ecb87195c..2c72d614cf 100644
--- a/Darling/Darling.Tests/ViewerCpuSchedulerTests.cs
+++ b/Darling/Darling.Tests/ViewerCpuSchedulerTests.cs
@@ -119,6 +119,17 @@ public void CpuSchedulerView_IsPinnedInTheAuthoritativeViewList()
///
public sealed class ViewerCpuSchedulerProjectionTests
{
+ [Fact]
+ public void TheGridRows_AllCarryTheSnapshotsCollectionTime_AndANullSnapshotHasNone()
+ {
+ var snapshot = Snapshot();
+ var rows = CpuSchedulerGridRow.Build(snapshot);
+ Assert.NotEmpty(rows);
+ Assert.All(rows, r => Assert.Equal(snapshot.CollectionTime, r.CollectionTime));
+ Assert.Equal(CpuSchedulerMetrics.BuildMetrics(snapshot).Select(m => m.Metric), rows.Select(r => r.Metric));
+ Assert.Empty(CpuSchedulerGridRow.Build(null));
+ }
+
private static CpuSchedulerSnapshot Snapshot(
bool workerExhaustion = false, bool runnableWarn = false, bool blockedWarn = false,
bool queuedWarn = false, bool memPressure = false, bool offlineWarn = false,
diff --git a/Darling/Darling.Tests/ViewerGridTimeColumnSortMemberTests.cs b/Darling/Darling.Tests/ViewerGridTimeColumnSortMemberTests.cs
index b5e560d3a4..d41f17e574 100644
--- a/Darling/Darling.Tests/ViewerGridTimeColumnSortMemberTests.cs
+++ b/Darling/Darling.Tests/ViewerGridTimeColumnSortMemberTests.cs
@@ -132,6 +132,12 @@ public sealed class ViewerGridTimeColumnSortMemberTests
{ "ViewerServerTab.xaml", "ProcedureStatsGrid", "LastExecutionTimeLocal", Name(typeof(ViewerProcedureStatsRow)), "LastExecutionTime" },
{ "ViewerServerTab.xaml", "ProcedureStatsGrid", "CachedTimeFormatted", Name(typeof(ViewerProcedureStatsRow)), "CachedTime" },
{ "ViewerServerTab.xaml", "RunningJobsGrid", "StartTimeLocal", Name(typeof(RunningJobRow)), "StartTime" },
+
+ /* The last "Collected" column of the four newest-snapshot grids (#4966): naive-UTC CollectionTime, shown in the display zone. */
+ { "ViewerServerTab.xaml", "RunningJobsGrid", "CollectionTimeLocal", Name(typeof(RunningJobRow)), "CollectionTime" },
+ { "ViewerServerTab.xaml", "CpuSchedulerGrid", "CollectionTimeLocal", Name(typeof(CpuSchedulerGridRow)), "CollectionTime" },
+ { "ViewerServerTab.xaml", "LatchStatsGrid", "CollectionTimeLocal", Name(typeof(LatchStatsSnapshotRow)), "CollectionTime" },
+ { "ViewerServerTab.xaml", "SpinlockStatsGrid", "CollectionTimeLocal", Name(typeof(SpinlockStatsSnapshotRow)), "CollectionTime" },
{ "ProcedureHistoryWindow.xaml", "HistoryDataGrid", "LastExecutionTimeLocal", Name(typeof(ViewerProcedureStatsHistoryRow)), "LastExecutionTime" },
{ "ProcedureHistoryWindow.xaml", "HistoryDataGrid", "CachedTimeLocal", Name(typeof(ViewerProcedureStatsHistoryRow)), "CachedTime" },
{ "QueryStatsHistoryWindow.xaml", "HistoryDataGrid", "LastExecutionTimeLocal", Name(typeof(ViewerQueryStatsHistoryRow)), "LastExecutionTime" },
diff --git a/Darling/Darling.Tests/ViewerLatchSpinlockTests.cs b/Darling/Darling.Tests/ViewerLatchSpinlockTests.cs
index 16c04b20c3..6a869c4981 100644
--- a/Darling/Darling.Tests/ViewerLatchSpinlockTests.cs
+++ b/Darling/Darling.Tests/ViewerLatchSpinlockTests.cs
@@ -101,21 +101,21 @@ public void SnapshotSql_CarriesTheStoredInterval_SelectsDeltasAsStored(string wh
public void SnapshotRows_NullTheDeltasOnTheMarker_AndSpellTheInterval()
{
var marker = new LatchStatsSnapshotRow("BUFFER", 0, 0, 0,
- DeltaSeriesShaping.ReadableDelta(0, 0), DeltaSeriesShaping.ReadableDelta(0, 0), 0);
+ DeltaSeriesShaping.ReadableDelta(0, 0), DeltaSeriesShaping.ReadableDelta(0, 0), 0, DateTime.UnixEpoch);
Assert.True(marker.IsUnknowable);
Assert.Null(marker.DeltaWaitTimeMs);
Assert.Null(marker.DeltaWaitingRequestsCount);
Assert.Equal("restart / first sample", marker.IntervalDisplay);
var measured = new SpinlockStatsSnapshotRow("LOCK_HASH", 900, 0, 0, 0, 0,
- DeltaSeriesShaping.ReadableDelta(400, 120), DeltaSeriesShaping.ReadableDelta(0, 120), 120);
+ DeltaSeriesShaping.ReadableDelta(400, 120), DeltaSeriesShaping.ReadableDelta(0, 120), 120, DateTime.UnixEpoch);
Assert.False(measured.IsUnknowable);
Assert.Equal(400L, measured.DeltaCollisions);
Assert.Equal(0L, measured.DeltaSpins); // a measured zero stays a zero
Assert.Equal("120", measured.IntervalDisplay);
var preV127 = new LatchStatsSnapshotRow("LOG_MANAGER", 0, 0, 0,
- DeltaSeriesShaping.ReadableDelta(100, null), DeltaSeriesShaping.ReadableDelta(4000, null), null);
+ DeltaSeriesShaping.ReadableDelta(100, null), DeltaSeriesShaping.ReadableDelta(4000, null), null, DateTime.UnixEpoch);
Assert.False(preV127.IsUnknowable);
Assert.Equal(4000L, preV127.DeltaWaitTimeMs);
Assert.Equal("not stored", preV127.IntervalDisplay);
diff --git a/Darling/Darling.Tests/ViewerSnapshotCollectedColumnTests.cs b/Darling/Darling.Tests/ViewerSnapshotCollectedColumnTests.cs
new file mode 100644
index 0000000000..141585b18e
--- /dev/null
+++ b/Darling/Darling.Tests/ViewerSnapshotCollectedColumnTests.cs
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text.RegularExpressions;
+using PerformanceMonitor.Darling.Viewer;
+using PerformanceMonitor.Ui;
+using Xunit;
+using static Darling.Tests.RepoFile;
+
+namespace Darling.Tests;
+
+/// The CPU Scheduler, Latch, Spinlock and Running Jobs grids end in a "Collected" column that says when their snapshot
+/// was collected (#4966): the display-zone time to the second, sorted on the stored DateTime, as the viewer's other Collected columns.
+[Collection("viewer-time-statics")]
+public sealed class ViewerSnapshotCollectedColumnTests
+{
+ private static string Xaml() =>
+ ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", "ViewerServerTab.xaml").ReplaceLineEndings("\n");
+
+ private static string ViewerFile(string file) =>
+ ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", file).ReplaceLineEndings("\n");
+
+ [Theory]
+ [InlineData("CpuSchedulerGrid")]
+ [InlineData("LatchStatsGrid")]
+ [InlineData("SpinlockStatsGrid")]
+ [InlineData("RunningJobsGrid")]
+ public void TheGrid_EndsInACollectedColumn_BoundToTheDisplayTime_SortedOnTheStoredDateTime(string grid)
+ {
+ var xaml = Xaml();
+ var start = Regex.Match(xaml, @"]*?x:Name=""" + grid + @"""").Index;
+ var end = xaml.IndexOf("", start, StringComparison.Ordinal);
+ var last = Regex.Matches(xaml[start..end], @"""])*>").Last().Value;
+ Assert.Contains("Header=\"Collected\"", last, StringComparison.Ordinal);
+ Assert.Contains("Binding=\"{Binding CollectionTimeLocal}\"", last, StringComparison.Ordinal);
+ Assert.Contains("SortMemberPath=\"CollectionTime\"", last, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TheOldSnapshotLabel_IsGone()
+ {
+ Assert.DoesNotContain("SnapshotTime\"", Xaml(), StringComparison.Ordinal);
+ Assert.DoesNotContain("Snapshot at", Xaml(), StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TheCpuSchedulerLoad_WrapsItsRowsWithTheSnapshotsCollectionTime()
+ {
+ var source = ViewerFile("ViewerServerTab.CpuScheduler.cs");
+ Assert.Contains("CpuSchedulerGrid.ItemsSource = CpuSchedulerGridRow.Build(snapshotTask.Result);", source, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TheDisplayText_IsTheDisplayZonesWallTime_ToTheSecond()
+ {
+ var saved = CultureInfo.CurrentCulture;
+ var savedMode = ViewerTimeHelper.CurrentDisplayMode;
+ try
+ {
+ CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
+ ViewerTimeHelper.CurrentDisplayMode = TimeDisplayMode.UTC;
+ var at = new DateTime(2026, 10, 3, 14, 5, 9, DateTimeKind.Unspecified);
+ // A literal, so the format string itself is pinned.
+ var expected = "2026-10-03 14:05:09";
+ Assert.Equal(expected, HistoryTime.CollectionLocal(at));
+ Assert.Equal(expected, new RunningJobRow { CollectionTime = at }.CollectionTimeLocal);
+ Assert.Equal(expected, new LatchStatsSnapshotRow("L", 0, 0, 0, null, null, null, at).CollectionTimeLocal);
+ Assert.Equal(expected, new SpinlockStatsSnapshotRow("S", 0, 0, 0, 0, 0, null, null, null, at).CollectionTimeLocal);
+ Assert.Equal(expected, new CpuSchedulerGridRow { CollectionTime = at }.CollectionTimeLocal);
+ }
+ finally
+ {
+ ViewerTimeHelper.CurrentDisplayMode = savedMode;
+ CultureInfo.CurrentCulture = saved;
+ }
+ }
+}
diff --git a/Darling/Darling.Tests/ViewerSnapshotTimeLiveTests.cs b/Darling/Darling.Tests/ViewerSnapshotTimeLiveTests.cs
new file mode 100644
index 0000000000..aa4430a101
--- /dev/null
+++ b/Darling/Darling.Tests/ViewerSnapshotTimeLiveTests.cs
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Npgsql;
+using PerformanceMonitor.Darling.Storage;
+using PerformanceMonitor.Darling.Viewer;
+using Xunit;
+
+namespace Darling.Tests;
+
+/// The latch and spinlock snapshot reads against a real store return the collection time of the snapshot they rendered (#4966).
+/* #1776 own-store: deliberately NOT [Collection("live-postgres")]. Every test here reaches DARLING_TEST_PG only to CREATE
+ and DROP its own scratch database. */
+[Collection("viewer-time-statics")]
+public sealed class ViewerSnapshotTimeLiveTests
+{
+ private const int ServerId = -496660;
+
+ [Fact]
+ public async Task TheLatchAndSpinlockSnapshots_CarryTheNewestCollectionTime_AgainstDevPostgres()
+ {
+ var ct = TestContext.Current.CancellationToken;
+ await using var scratch = await QueryGridSeed.OpenScratchAsync(ct);
+ var end = QueryGridSeed.NowToTheMinute();
+ var older = end.AddHours(-3);
+ var newest = end.AddHours(-1);
+ await using (var connection = new NpgsqlConnection(scratch.ConnectionString))
+ {
+ await connection.OpenAsync(ct);
+ await PgMigrations.MigrateAsync(connection, ct);
+ await EventGridSeed.AddServerAsync(connection, ServerId, "snapshot-time", end.AddDays(-2), "latch_stats", end, clockOffsetMinutes: null, ct);
+ long id = 1;
+ foreach (var at in new[] { older, newest })
+ {
+ await ExecAsync(connection, $"INSERT INTO latch_stats (collection_id, collection_time, server_id, server_name, latch_class, waiting_requests_count, wait_time_ms, max_wait_time_ms, delta_waiting_requests_count, delta_wait_time_ms, delta_max_wait_time_ms, sample_interval_seconds) VALUES ({id++}, @t, {ServerId}, 'snapshot-time', 'BUFFER', 10, 20, 5, 1, 2, 1, 60)", at, ct);
+ await ExecAsync(connection, $"INSERT INTO spinlock_stats (collection_id, collection_time, server_id, server_name, spinlock_name, collisions, spins, spins_per_collision, sleep_time, backoffs, delta_collisions, delta_spins, delta_sleep_time, delta_backoffs, sample_interval_seconds) VALUES ({id++}, @t, {ServerId}, 'snapshot-time', 'LOCK_HASH', 10, 20, 2, 0, 0, 1, 2, 0, 0, 60)", at, ct);
+ }
+ }
+
+ var viewer = new ViewerDataService(scratch.ConnectionString);
+ var latch = await viewer.GetLatchStatsSnapshotAsync(ServerId, end.AddDays(-7), end, ct);
+ var spinlock = await viewer.GetSpinlockStatsSnapshotAsync(ServerId, end.AddDays(-7), end, ct);
+
+ Assert.Single(latch);
+ Assert.Equal(newest, latch[0].CollectionTime);
+ Assert.Single(spinlock);
+ Assert.Equal(newest, spinlock[0].CollectionTime);
+ Assert.Empty(await viewer.GetLatchStatsSnapshotAsync(ServerId + 1, end.AddDays(-7), end, ct));
+ }
+
+ private static async Task ExecAsync(NpgsqlConnection connection, string sql, DateTime at, CancellationToken ct)
+ {
+ await using var command = new NpgsqlCommand(sql, connection);
+ command.Parameters.AddWithValue("t", DateTime.SpecifyKind(at, DateTimeKind.Unspecified));
+ await command.ExecuteNonQueryAsync(ct);
+ }
+}
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.CpuScheduler.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.CpuScheduler.cs
index 057206438e..723378e782 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.CpuScheduler.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.CpuScheduler.cs
@@ -255,3 +255,36 @@ public async Task> GetCpuSchedulerTrendAsync(
OfflineCpuWarning: !reader.IsDBNull(26) && reader.GetBoolean(26));
}
}
+
+///
+/// One row of the CPU Scheduler latest-snapshot grid (#4966): the shared metric row plus when the snapshot it reads was collected.
+/// The shared leaves the time out on purpose (each app keeps its own time handling), so the
+/// viewer adds it here: every row of one snapshot carries that snapshot's naive-UTC , worded in the
+/// display zone to the second as ; the column sorts by .
+///
+public sealed class CpuSchedulerGridRow
+{
+ public string Metric { get; init; } = "";
+ public string Value { get; init; } = "";
+ public bool IsWarning { get; init; }
+ public DateTime CollectionTime { get; init; }
+
+ public string CollectionTimeLocal => HistoryTime.CollectionLocal(CollectionTime);
+
+ /// The grid's rows for the snapshot; no rows (and so no time) for a window that holds no snapshot.
+ public static List Build(CpuSchedulerSnapshot? snapshot)
+ {
+ var rows = new List();
+ if (snapshot is null)
+ {
+ return rows;
+ }
+
+ foreach (var metric in CpuSchedulerMetrics.BuildMetrics(snapshot))
+ {
+ rows.Add(new CpuSchedulerGridRow { Metric = metric.Metric, Value = metric.Value, IsWarning = metric.IsWarning, CollectionTime = snapshot.CollectionTime });
+ }
+
+ return rows;
+ }
+}
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.LatchSpinlock.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.LatchSpinlock.cs
index 71ca2f50c7..8896ca6a6f 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.LatchSpinlock.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.LatchSpinlock.cs
@@ -12,6 +12,7 @@
using System.Threading.Tasks;
using Npgsql;
using PerformanceMonitor.Common;
+using PerformanceMonitor.Darling.Storage;
namespace PerformanceMonitor.Darling.Viewer;
@@ -36,12 +37,16 @@ public sealed record LatchStatsSnapshotRow(
long MaxWaitTimeMs,
long? DeltaWaitingRequestsCount,
long? DeltaWaitTimeMs,
- int? SampleIntervalSeconds)
+ int? SampleIntervalSeconds,
+ DateTime CollectionTime)
{
/// True when the row's deltas are the unknowable marker — a stored interval of exactly 0 (the
/// FileIoStatsRow / resource-semaphore idiom).
public bool IsUnknowable => SampleIntervalSeconds == 0;
+ /// (naive UTC) in the display zone, to the second; the "Collected" column sorts by .
+ public string CollectionTimeLocal => HistoryTime.CollectionLocal(CollectionTime);
+
/// The grid's Interval (sec) cell — see .
public string IntervalDisplay => DeltaSeriesShaping.IntervalDisplay(SampleIntervalSeconds);
}
@@ -63,11 +68,15 @@ public sealed record SpinlockStatsSnapshotRow(
long Backoffs,
long? DeltaCollisions,
long? DeltaSpins,
- int? SampleIntervalSeconds)
+ int? SampleIntervalSeconds,
+ DateTime CollectionTime)
{
/// True when the row's deltas are the unknowable marker — a stored interval of exactly 0.
public bool IsUnknowable => SampleIntervalSeconds == 0;
+ /// (naive UTC) in the display zone, to the second; the "Collected" column sorts by .
+ public string CollectionTimeLocal => HistoryTime.CollectionLocal(CollectionTime);
+
/// The grid's Interval (sec) cell — see .
public string IntervalDisplay => DeltaSeriesShaping.IntervalDisplay(SampleIntervalSeconds);
}
@@ -144,7 +153,8 @@ FROM v_latch_stats
max_wait_time_ms,
delta_waiting_requests_count,
delta_wait_time_ms,
- sample_interval_seconds
+ sample_interval_seconds,
+ collection_time
FROM v_latch_stats
WHERE server_id = $1
AND collection_time = (SELECT mx FROM latest)
@@ -220,7 +230,8 @@ FROM v_spinlock_stats
backoffs,
delta_collisions,
delta_spins,
- sample_interval_seconds
+ sample_interval_seconds,
+ collection_time
FROM v_spinlock_stats
WHERE server_id = $1
AND collection_time = (SELECT mx FROM latest)
@@ -278,7 +289,8 @@ deltas stand. The interval column is integer in the store (V127), read as such.
reader.IsDBNull(3) ? 0 : reader.GetInt64(3),
DeltaSeriesShaping.ReadableDelta(reader.IsDBNull(4) ? 0 : reader.GetInt64(4), interval),
DeltaSeriesShaping.ReadableDelta(reader.IsDBNull(5) ? 0 : reader.GetInt64(5), interval),
- interval));
+ interval,
+ reader.GetDateTime(7)));
}
return result;
@@ -334,9 +346,21 @@ public async Task> GetSpinlockStatsSnapshotAsync(
reader.IsDBNull(5) ? 0 : reader.GetInt64(5),
DeltaSeriesShaping.ReadableDelta(reader.IsDBNull(6) ? 0 : reader.GetInt64(6), interval),
DeltaSeriesShaping.ReadableDelta(reader.IsDBNull(7) ? 0 : reader.GetInt64(7), interval),
- interval));
+ interval,
+ reader.GetDateTime(9)));
}
return result;
}
+
+ /// Where this server's latch_stats coverage starts for the window (the shared probe), so the trend chart can say so
+ /// when an empty stretch would otherwise draw as a flat zero (#4966).
+ public Task GetLatchStatsDataStartAsync(int serverId, DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken = default) =>
+ DataWindowFloor.GetForServerAsync(_dataSource, DataWindowFloor.Source.ForCollectorTable("latch_stats"), serverId, startUtc, endUtc,
+ ViewerCommandDeadlines.CurrentInteractiveReadSeconds, cancellationToken);
+
+ /// The spinlock_stats twin of .
+ public Task GetSpinlockStatsDataStartAsync(int serverId, DateTime startUtc, DateTime endUtc, CancellationToken cancellationToken = default) =>
+ DataWindowFloor.GetForServerAsync(_dataSource, DataWindowFloor.Source.ForCollectorTable("spinlock_stats"), serverId, startUtc, endUtc,
+ ViewerCommandDeadlines.CurrentInteractiveReadSeconds, cancellationToken);
}
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.RunningJobs.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.RunningJobs.cs
index e8951d4834..270ce13e56 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.RunningJobs.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.RunningJobs.cs
@@ -204,6 +204,9 @@ public class RunningJobRow
public string StartTimeLocal => ViewerDataService.FormatServerClock(StartTime);
+ /// (naive UTC) in the display zone, to the second; the "Collected" column sorts by .
+ public string CollectionTimeLocal => HistoryTime.CollectionLocal(CollectionTime);
+
public string CurrentDurationFormatted => FormatDuration(CurrentDurationSeconds);
public string AvgDurationFormatted => FormatDuration(AvgDurationSeconds);
public string P95DurationFormatted => FormatDuration(P95DurationSeconds);
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.CpuScheduler.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.CpuScheduler.cs
index 3812c27a77..70d15e3620 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.CpuScheduler.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.CpuScheduler.cs
@@ -57,7 +57,7 @@ private async Task LoadCpuSchedulerAsync()
await Task.WhenAll(trendTask, snapshotTask);
RenderCpuSchedulerChart(trendTask.Result);
- CpuSchedulerGrid.ItemsSource = CpuSchedulerMetrics.BuildMetrics(snapshotTask.Result);
+ CpuSchedulerGrid.ItemsSource = CpuSchedulerGridRow.Build(snapshotTask.Result);
var gap = CpuSchedulerGapNote(_server.ServerName, _server.EngineEdition, _server.EngineKind);
CpuSchedulerNoDataMessage.Text = gap ?? "";
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.LatchSpinlock.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.LatchSpinlock.cs
index a8f479d4b6..5e49609259 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.LatchSpinlock.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.LatchSpinlock.cs
@@ -62,6 +62,7 @@ private async Task LoadLatchSpinlockAsync()
{
var (startUtc, endUtc) = GetWindowUtc();
+ /* Four reads in flight for the join below; the two data-start probes run after it under their own scope (#4966). */
using var readFanOut = ViewerReadFanOut.Of(4);
var latchTrendTask = _dataService.GetLatchStatsTrendAsync(_server.ServerId, startUtc, endUtc);
@@ -71,10 +72,23 @@ private async Task LoadLatchSpinlockAsync()
await Task.WhenAll(latchTrendTask, latchSnapshotTask, spinlockTrendTask, spinlockSnapshotTask);
+ /* The join is over: free its width before the probes read again, so they are priced against two reads, not four finished ones. */
+ readFanOut.Release();
+
RenderLatchStatsChart(latchTrendTask.Result);
LatchStatsGrid.ItemsSource = latchSnapshotTask.Result;
RenderSpinlockStatsChart(spinlockTrendTask.Result);
SpinlockStatsGrid.ItemsSource = spinlockSnapshotTask.Result;
+
+ /* #4966: each chart draws a flat zero over an empty stretch, so each says where its collector's coverage starts. The probes stay OUT
+ of the join above: a probe that throws costs the chart's note (DataStartOrNullAsync catches it), never the rows. */
+ using var probeFanOut = ViewerReadFanOut.Of(2);
+ var latchDataStartTask = _dataService.GetLatchStatsDataStartAsync(_server.ServerId, startUtc, endUtc);
+ var spinlockDataStartTask = _dataService.GetSpinlockStatsDataStartAsync(_server.ServerId, startUtc, endUtc);
+ /* Both probes are awaited on one line, each through the catching helper: neither is left unobserved while the other is awaited. */
+ var (latchDataStart, spinlockDataStart) = (await DataStartOrNullAsync(latchDataStartTask, "Latch Stats"), await DataStartOrNullAsync(spinlockDataStartTask, "Spinlock Stats"));
+ UpdateTruncationBanner(LatchStatsTruncationBanner, latchDataStart, startUtc);
+ UpdateTruncationBanner(SpinlockStatsTruncationBanner, spinlockDataStart, startUtc);
}
private void RenderLatchStatsChart(List data)
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml
index 4b49fb7bfe..1b61ac4a67 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml
@@ -2153,6 +2153,7 @@
+
@@ -2894,6 +2895,7 @@
+
@@ -3673,15 +3675,22 @@
+
+
-
-
+
+
+
+
@@ -3694,13 +3703,19 @@
+
-
-
+
+
+
+
@@ -3713,6 +3728,7 @@
+