From 89e6dccc75e220c92d72b0ab0e727992924aec6c Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Sat, 3 Oct 2026 12:05:19 -0400
Subject: [PATCH 1/7] Darling Viewer: the CPU Scheduler, Latch, Spinlock and
Running Jobs grids show their snapshot time, and the latch and spinlock
trends say where their data starts (part of #4966)
---
.../Darling.Tests/ViewerLatchSpinlockTests.cs | 6 +-
.../ViewerSnapshotTimeLabelTests.cs | 43 ++++++++++++
.../ViewerSnapshotTimeLiveTests.cs | 63 +++++++++++++++++
.../ViewerSnapshotTimePinTests.cs | 68 +++++++++++++++++++
.../ViewerDataService.LatchSpinlock.cs | 30 ++++++--
.../ViewerServerTab.CpuScheduler.cs | 1 +
.../ViewerServerTab.LatchSpinlock.cs | 12 +++-
.../ViewerServerTab.RunningJobs.cs | 2 +
.../ViewerServerTab.SnapshotTime.cs | 34 ++++++++++
.../ViewerServerTab.xaml | 58 +++++++++++++---
.../ViewerTimeHelper.cs | 11 +++
11 files changed, 308 insertions(+), 20 deletions(-)
create mode 100644 Darling/Darling.Tests/ViewerSnapshotTimeLabelTests.cs
create mode 100644 Darling/Darling.Tests/ViewerSnapshotTimeLiveTests.cs
create mode 100644 Darling/Darling.Tests/ViewerSnapshotTimePinTests.cs
create mode 100644 Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.SnapshotTime.cs
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/ViewerSnapshotTimeLabelTests.cs b/Darling/Darling.Tests/ViewerSnapshotTimeLabelTests.cs
new file mode 100644
index 0000000000..4830302c1c
--- /dev/null
+++ b/Darling/Darling.Tests/ViewerSnapshotTimeLabelTests.cs
@@ -0,0 +1,43 @@
+/*
+ * 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.Threading;
+using PerformanceMonitor.Darling.Viewer;
+using Xunit;
+
+namespace Darling.Tests;
+
+/// The "Snapshot at <time>" label a newest-snapshot surface shows instead of a data-start note (#4966).
+public sealed class ViewerSnapshotTimeLabelTests
+{
+ [Fact]
+ public void TheLabel_IsTheDisplayZonesWallTime_ToTheSecond()
+ {
+ var zone = TimeZoneInfo.CreateCustomTimeZone("t", TimeSpan.FromHours(-5), "t", "t");
+ var text = ViewerTimeHelper.FormatSnapshotLabel(new DateTime(2026, 3, 4, 15, 6, 7, DateTimeKind.Utc), zone);
+ Assert.Equal("Snapshot at 2026-03-04 10:06:07", text);
+ }
+
+ [Fact]
+ public void TheLabel_IsOnTheInvariantCulture_WhateverTheThreadsCalendar()
+ {
+ var saved = CultureInfo.CurrentCulture;
+ try
+ {
+ CultureInfo.CurrentCulture = new CultureInfo("th-TH");
+ var text = ViewerTimeHelper.FormatSnapshotLabel(new DateTime(2026, 3, 4, 15, 6, 7, DateTimeKind.Utc), TimeZoneInfo.Utc);
+ Assert.Equal("Snapshot at 2026-03-04 15:06:07", text);
+ }
+ finally
+ {
+ CultureInfo.CurrentCulture = saved;
+ }
+ }
+}
diff --git a/Darling/Darling.Tests/ViewerSnapshotTimeLiveTests.cs b/Darling/Darling.Tests/ViewerSnapshotTimeLiveTests.cs
new file mode 100644
index 0000000000..9784d79c7c
--- /dev/null
+++ b/Darling/Darling.Tests/ViewerSnapshotTimeLiveTests.cs
@@ -0,0 +1,63 @@
+/*
+ * 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).
+[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/Darling.Tests/ViewerSnapshotTimePinTests.cs b/Darling/Darling.Tests/ViewerSnapshotTimePinTests.cs
new file mode 100644
index 0000000000..c9779bc6ca
--- /dev/null
+++ b/Darling/Darling.Tests/ViewerSnapshotTimePinTests.cs
@@ -0,0 +1,68 @@
+/*
+ * 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.Text.RegularExpressions;
+using Xunit;
+using static Darling.Tests.RepoFile;
+
+namespace Darling.Tests;
+
+/// How the CPU Scheduler, Latch, Spinlock and Running Jobs grids are wired to show their snapshot's time (#4966). These read
+/// the source, as the other data-start wiring pins do; the label control's behavior is the STA fact in
+/// ViewerSnapshotTimeLabelStaTests.
+public sealed class ViewerSnapshotTimePinTests
+{
+ private static string ViewerFile(string file) =>
+ ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", file).ReplaceLineEndings("\n");
+
+ [Theory]
+ [InlineData("ViewerServerTab.CpuScheduler.cs", "ShowSnapshotTime(CpuSchedulerSnapshotTime, snapshotTask.Result?.CollectionTime)")]
+ [InlineData("ViewerServerTab.RunningJobs.cs", "ShowSnapshotTime(RunningJobsSnapshotTime, jobs.Count == 0 ? null : jobs[0].CollectionTime)")]
+ [InlineData("ViewerServerTab.LatchSpinlock.cs", "ShowSnapshotTime(LatchStatsSnapshotTime, latchSnapshotTask.Result.Count == 0 ? null : latchSnapshotTask.Result[0].CollectionTime)")]
+ [InlineData("ViewerServerTab.LatchSpinlock.cs", "ShowSnapshotTime(SpinlockStatsSnapshotTime, spinlockSnapshotTask.Result.Count == 0 ? null : spinlockSnapshotTask.Result[0].CollectionTime)")]
+ public void EachLoad_SetsItsLabelFromTheSnapshotItRendered(string file, string call)
+ {
+ var source = ViewerFile(file);
+ Assert.Contains(call, source, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void TheLatchLoad_ProbesBothCollectors_OutsideTheJoin_AndCountsThemInTheFanOut()
+ {
+ var source = ViewerFile("ViewerServerTab.LatchSpinlock.cs");
+ Assert.Contains("ViewerReadFanOut.Of(6)", source, StringComparison.Ordinal);
+ var join = Regex.Match(source, @"await Task\.WhenAll\(([^;]*)\);").Groups[1].Value;
+ Assert.DoesNotContain("DataStartTask", join, StringComparison.Ordinal);
+ Assert.True(source.IndexOf("GetLatchStatsDataStartAsync", StringComparison.Ordinal) < source.IndexOf("await Task.WhenAll", StringComparison.Ordinal));
+ Assert.Contains("DataStartOrNullAsync(latchDataStartTask", source, StringComparison.Ordinal);
+ Assert.Contains("DataStartOrNullAsync(spinlockDataStartTask", source, StringComparison.Ordinal);
+ var data = ViewerFile("ViewerDataService.LatchSpinlock.cs");
+ Assert.Contains("ForCollectorTable(\"latch_stats\")", data, StringComparison.Ordinal);
+ Assert.Contains("ForCollectorTable(\"spinlock_stats\")", data, StringComparison.Ordinal);
+ }
+
+ [Theory]
+ [InlineData("LatchSnapshotSql")]
+ [InlineData("SpinlockSnapshotSql")]
+ public void TheSnapshotSql_ReturnsTheCollectionTimeAsItsLastColumn(string constant)
+ {
+ var sql = constant == "LatchSnapshotSql" ? PerformanceMonitor.Darling.Viewer.ViewerDataService.LatchSnapshotSql : PerformanceMonitor.Darling.Viewer.ViewerDataService.SpinlockSnapshotSql;
+ Assert.Matches(@"sample_interval_seconds,\s+collection_time\s+FROM v_", sql);
+ }
+
+ [Fact]
+ public void TheXaml_DeclaresAMutedLabelAboveEachGrid()
+ {
+ var xaml = ViewerFile("ViewerServerTab.xaml");
+ foreach (var name in new[] { "CpuSchedulerSnapshotTime", "RunningJobsSnapshotTime", "LatchStatsSnapshotTime", "SpinlockStatsSnapshotTime" })
+ {
+ Assert.Matches($@"]*FontSize=""11""\s+Foreground=""\{{DynamicResource ForegroundMutedBrush\}}""", xaml);
+ }
+ }
+}
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.LatchSpinlock.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerDataService.LatchSpinlock.cs
index 71ca2f50c7..319734d08a 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,7 +37,8 @@ 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).
@@ -63,7 +65,8 @@ 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;
@@ -144,7 +147,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 +224,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 +283,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 +340,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/ViewerServerTab.CpuScheduler.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.CpuScheduler.cs
index 3812c27a77..13b584aedf 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.CpuScheduler.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.CpuScheduler.cs
@@ -58,6 +58,7 @@ private async Task LoadCpuSchedulerAsync()
RenderCpuSchedulerChart(trendTask.Result);
CpuSchedulerGrid.ItemsSource = CpuSchedulerMetrics.BuildMetrics(snapshotTask.Result);
+ ShowSnapshotTime(CpuSchedulerSnapshotTime, snapshotTask.Result?.CollectionTime);
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..ebe7cc9a66 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.LatchSpinlock.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.LatchSpinlock.cs
@@ -62,19 +62,29 @@ private async Task LoadLatchSpinlockAsync()
{
var (startUtc, endUtc) = GetWindowUtc();
- using var readFanOut = ViewerReadFanOut.Of(4);
+ /* Six reads in flight: the four feeds below and the two charts' data-start probes (#4966). */
+ using var readFanOut = ViewerReadFanOut.Of(6);
var latchTrendTask = _dataService.GetLatchStatsTrendAsync(_server.ServerId, startUtc, endUtc);
var latchSnapshotTask = _dataService.GetLatchStatsSnapshotAsync(_server.ServerId, startUtc, endUtc);
var spinlockTrendTask = _dataService.GetSpinlockStatsTrendAsync(_server.ServerId, startUtc, endUtc);
var spinlockSnapshotTask = _dataService.GetSpinlockStatsSnapshotAsync(_server.ServerId, startUtc, endUtc);
+ /* #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 below: a probe that throws costs the chart's note (DataStartOrNullAsync catches it), never the rows. */
+ var latchDataStartTask = _dataService.GetLatchStatsDataStartAsync(_server.ServerId, startUtc, endUtc);
+ var spinlockDataStartTask = _dataService.GetSpinlockStatsDataStartAsync(_server.ServerId, startUtc, endUtc);
+
await Task.WhenAll(latchTrendTask, latchSnapshotTask, spinlockTrendTask, spinlockSnapshotTask);
RenderLatchStatsChart(latchTrendTask.Result);
LatchStatsGrid.ItemsSource = latchSnapshotTask.Result;
+ ShowSnapshotTime(LatchStatsSnapshotTime, latchSnapshotTask.Result.Count == 0 ? null : latchSnapshotTask.Result[0].CollectionTime);
+ UpdateTruncationBanner(LatchStatsTruncationBanner, await DataStartOrNullAsync(latchDataStartTask, "Latch Stats"), startUtc);
RenderSpinlockStatsChart(spinlockTrendTask.Result);
SpinlockStatsGrid.ItemsSource = spinlockSnapshotTask.Result;
+ ShowSnapshotTime(SpinlockStatsSnapshotTime, spinlockSnapshotTask.Result.Count == 0 ? null : spinlockSnapshotTask.Result[0].CollectionTime);
+ UpdateTruncationBanner(SpinlockStatsTruncationBanner, await DataStartOrNullAsync(spinlockDataStartTask, "Spinlock Stats"), startUtc);
}
private void RenderLatchStatsChart(List data)
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.RunningJobs.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.RunningJobs.cs
index 8b8bb335de..0e8a5d411c 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.RunningJobs.cs
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.RunningJobs.cs
@@ -41,6 +41,8 @@ priced against contention that has already finished. */
readFanOut.Release();
_runningJobsFilterMgr!.UpdateData(jobs);
+ /* The grid is one collection's rows, so any row's CollectionTime (naive UTC, the display-zone frame, not StartTimeLocal's) names it. */
+ ShowSnapshotTime(RunningJobsSnapshotTime, jobs.Count == 0 ? null : jobs[0].CollectionTime);
await ShowEngineGapAsync(RunningJobsNoDataMessage, "running_jobs", jobs.Count);
RunningJobsMsdbWarning.Visibility = ShouldShowMsdbBanner(status) ? Visibility.Visible : Visibility.Collapsed;
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.SnapshotTime.cs b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.SnapshotTime.cs
new file mode 100644
index 0000000000..9d6f3ecc50
--- /dev/null
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.SnapshotTime.cs
@@ -0,0 +1,34 @@
+/*
+ * 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.Windows;
+using System.Windows.Controls;
+
+namespace PerformanceMonitor.Darling.Viewer;
+
+public partial class ViewerServerTab
+{
+ ///
+ /// Sets the muted "Snapshot at <time>" label above a newest-snapshot grid (#4966): the surface shows the collection it
+ /// rendered instead of a data-start note. An empty snapshot ( null) hides the label and leaves the
+ /// grid's own empty text to speak.
+ ///
+ internal static void ShowSnapshotTime(TextBlock label, DateTime? collectionUtc)
+ {
+ if (collectionUtc is not DateTime utc)
+ {
+ label.Text = "";
+ label.Visibility = Visibility.Collapsed;
+ return;
+ }
+
+ label.Text = ViewerTimeHelper.FormatSnapshotLabel(utc);
+ label.Visibility = Visibility.Visible;
+ }
+}
diff --git a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml
index a0e608054f..8b16de845d 100644
--- a/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml
+++ b/Darling/PerformanceMonitor.Darling.Viewer/ViewerServerTab.xaml
@@ -2127,10 +2127,14 @@
+
- ": the collection the metric grid shows (#4966). -->
+
+