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
11 changes: 6 additions & 5 deletions Darling/Darling.Tests/DarlingMcpConfigHistoryToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,12 +301,12 @@ index rather than the tool list learns the row grew. */
/// <summary>
/// The three grids that render this row grew the same two trailing columns: the web tile's
/// <c>QS_HEALTH_COLUMNS</c> ends with the two payload keys (formatless, so the renderer prints a null as
/// the page's dash), and both WPF Query Store grids end with <c>Capture Mode</c> then <c>Wait Stats
/// Capture</c>, bound to the display properties that render a null as the same glyph — and the two XAML
/// the page's dash), and both WPF Query Store grids end with <c>Capture Mode</c>, <c>Wait Stats
/// Capture</c> then <c>Collected</c> (#4966, bound to <c>CaptureTimeLocal</c>), bound to the display properties that render a null as the same glyph — and the two XAML
/// grids carry the identical binding sequence, the way every twinned grid in the two front ends does.
/// </summary>
[Fact]
public void QueryStoreHealth_EveryGrid_EndsWithTheTwoCaptureModeColumns()
public void QueryStoreHealth_EveryGrid_EndsWithTheTwoCaptureModeColumnsThenCollected()
{
var js = RepoFile.ReadRepoFile("Darling", "PerformanceMonitor.Darling.Service", "wwwroot", "js", "pages", "server-tabs.js");
var marker = "const QS_HEALTH_COLUMNS = [";
Expand All @@ -325,8 +325,9 @@ public void QueryStoreHealth_EveryGrid_EndsWithTheTwoCaptureModeColumns()
var viewerBindings = GridBindings(RepoFile.ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", "ViewerServerTab.xaml"));
var liteBindings = GridBindings(RepoFile.ReadRepoFile("Lite", "Controls", "ServerTab.xaml"));
Assert.Equal(viewerBindings, liteBindings);
Assert.Equal("CaptureModeDisplay", viewerBindings[^2]);
Assert.Equal("WaitStatsCaptureModeDisplay", viewerBindings[^1]);
Assert.Equal("CaptureModeDisplay", viewerBindings[^3]);
Assert.Equal("WaitStatsCaptureModeDisplay", viewerBindings[^2]);
Assert.Equal("CaptureTimeLocal", viewerBindings[^1]);

foreach (var xaml in new[] { RepoFile.ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", "ViewerServerTab.xaml"), RepoFile.ReadRepoFile("Lite", "Controls", "ServerTab.xaml") })
{
Expand Down
80 changes: 80 additions & 0 deletions Darling/Darling.Tests/ViewerConfigGridCollectedColumnTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* 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.Analysis.Baselines;
using PerformanceMonitor.Darling.Viewer;
using PerformanceMonitor.Ui;
using Xunit;
using static Darling.Tests.RepoFile;

namespace Darling.Tests;

/// <summary>
/// #4966: the viewer's five Configuration grids show the newest snapshot, so each ends with a <c>Collected</c> column that says when
/// that snapshot was captured, the same last column Lite's twin grids carry. The column shows the time to the second in the display
/// zone and sorts by the stored UTC instant.
/// </summary>
[Collection("viewer-time-statics")]
public sealed class ViewerConfigGridCollectedColumnTests
{
[Theory]
[InlineData("ServerConfigGrid")]
[InlineData("DatabaseConfigGrid")]
[InlineData("DatabaseScopedConfigGrid")]
[InlineData("QueryStoreHealthGrid")]
[InlineData("TraceFlagsGrid")]
public void EachConfigGrid_EndsWithACollectedColumn_SortingOnTheStoredInstant(string gridName)
{
var xaml = ReadRepoFile("Darling", "PerformanceMonitor.Darling.Viewer", "ViewerServerTab.xaml");
var start = xaml.IndexOf($"x:Name=\"{gridName}\"", StringComparison.Ordinal);
Assert.True(start >= 0, $"no {gridName} in the viewer XAML");
var end = xaml.IndexOf("</DataGrid>", start, StringComparison.Ordinal);
Assert.True(end > start, $"{gridName} is unterminated");
var grid = xaml[start..end];

var columns = Regex.Matches(grid, @"<DataGridTextColumn [^>]*Binding=""\{Binding ([A-Za-z]+)\}""[^>]*>").ToList();
var last = columns[^1];
Assert.Equal("CaptureTimeLocal", last.Groups[1].Value);
Assert.Contains("SortMemberPath=\"CaptureTime\"", last.Value, StringComparison.Ordinal);
Assert.Contains("<TextBlock Text=\"Collected\"", grid[last.Index..], StringComparison.Ordinal);
}

[Fact]
public void CaptureTimeLocal_WordsTheUtcInstant_InTheDisplayZone_ToTheSecond()
{
var savedMode = ViewerTimeHelper.CurrentDisplayMode;
var savedClock = ViewerTimeHelper.ActiveServerClock;
var savedCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
ViewerTimeHelper.CurrentDisplayMode = TimeDisplayMode.ServerTime;
ViewerTimeHelper.ActiveServerClock = ServerClock.Resolve("Eastern Standard Time", -300);
var at = new DateTime(2026, 1, 15, 17, 4, 59, DateTimeKind.Unspecified);

Assert.Equal("2026-01-15 12:04:59", new ServerConfigRow { CaptureTime = at }.CaptureTimeLocal);
Assert.Equal("2026-01-15 12:04:59", new DatabaseConfigRow { CaptureTime = at }.CaptureTimeLocal);
Assert.Equal("2026-01-15 12:04:59", new DatabaseScopedConfigRow { CaptureTime = at }.CaptureTimeLocal);
Assert.Equal("2026-01-15 12:04:59", new QueryStoreHealthRow { CaptureTime = at }.CaptureTimeLocal);
Assert.Equal("2026-01-15 12:04:59", new TraceFlagRow { CaptureTime = at }.CaptureTimeLocal);

ViewerTimeHelper.CurrentDisplayMode = TimeDisplayMode.UTC;
Assert.Equal("2026-01-15 17:04:59", new TraceFlagRow { CaptureTime = at }.CaptureTimeLocal);
}
finally
{
ViewerTimeHelper.CurrentDisplayMode = savedMode;
ViewerTimeHelper.ActiveServerClock = savedClock;
CultureInfo.CurrentCulture = savedCulture;
}
}
}
5 changes: 5 additions & 0 deletions Darling/Darling.Tests/ViewerGridTimeColumnSortMemberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ public sealed class ViewerGridTimeColumnSortMemberTests
{ "ViewerServerTab.xaml", "BlockedProcessReportGrid", "EventTimeLocal", Name(typeof(ViewerBlockedProcessRow)), "EventTime" },
{ "ViewerServerTab.xaml", "DeadlockGrid", "DeadlockTimeLocal", Name(typeof(DeadlockProcessDetail)), "DeadlockTime" },
{ "ViewerServerTab.xaml", "AutomaticTuningGrid", "CollectionTimeLocal", Name(typeof(AutomaticTuningRow)), "CollectionTime" },
{ "ViewerServerTab.xaml", "ServerConfigGrid", "CaptureTimeLocal", Name(typeof(ServerConfigRow)), "CaptureTime" },
{ "ViewerServerTab.xaml", "DatabaseConfigGrid", "CaptureTimeLocal", Name(typeof(DatabaseConfigRow)), "CaptureTime" },
{ "ViewerServerTab.xaml", "DatabaseScopedConfigGrid", "CaptureTimeLocal", Name(typeof(DatabaseScopedConfigRow)), "CaptureTime" },
{ "ViewerServerTab.xaml", "QueryStoreHealthGrid", "CaptureTimeLocal", Name(typeof(QueryStoreHealthRow)), "CaptureTime" },
{ "ViewerServerTab.xaml", "TraceFlagsGrid", "CaptureTimeLocal", Name(typeof(TraceFlagRow)), "CaptureTime" },
{ "ViewerServerTab.xaml", "ServerConfigChangesGrid", "ChangeTimeDisplay", Name(typeof(ServerConfigChangeRow)), "ChangeTime" },
{ "ViewerServerTab.xaml", "DatabaseConfigChangesGrid", "ChangeTimeDisplay", Name(typeof(DatabaseConfigChangeRow)), "ChangeTime" },
{ "ViewerServerTab.xaml", "TraceFlagChangesGrid", "ChangeTimeDisplay", Name(typeof(TraceFlagChangeRow)), "ChangeTime" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ namespace PerformanceMonitor.Darling.Viewer;
public sealed partial class ViewerDataService
{
public const string ServerConfigSql = """
SELECT configuration_name, value_configured, value_in_use, is_dynamic, is_advanced
SELECT configuration_name, value_configured, value_in_use, is_dynamic, is_advanced, capture_time
FROM v_server_config
WHERE server_id = $1
AND capture_time = (SELECT MAX(capture_time) FROM v_server_config WHERE server_id = $1)
ORDER BY configuration_name
""";

/* 28 columns in Lite's exact SELECT order — the database-config reader below maps them by
/* 28 columns, then capture_time (#4966), in Lite's exact SELECT order — the database-config reader below maps them by
incrementing ordinal, so this list's order is load-bearing and must stay byte-identical. */
public const string DatabaseConfigSql = """
SELECT database_name, state_desc, compatibility_level, collation_name, recovery_model,
Expand All @@ -46,7 +46,8 @@ ORDER BY configuration_name
is_query_store_on, is_encrypted, is_trustworthy_on, is_db_chaining_on,
is_broker_enabled, is_cdc_enabled, is_mixed_page_allocation_on,
log_reuse_wait_desc, page_verify_option, target_recovery_time_seconds, delayed_durability,
is_accelerated_database_recovery_on, is_memory_optimized_enabled, is_optimized_locking_on
is_accelerated_database_recovery_on, is_memory_optimized_enabled, is_optimized_locking_on,
capture_time
FROM v_database_config
WHERE server_id = $1
AND capture_time = (SELECT MAX(capture_time) FROM v_database_config WHERE server_id = $1)
Expand All @@ -55,7 +56,7 @@ ORDER BY database_name
""";

public const string DatabaseScopedConfigSql = """
SELECT database_name, configuration_name, value, value_for_secondary
SELECT database_name, configuration_name, value, value_for_secondary, capture_time
FROM v_database_scoped_config
WHERE server_id = $1
AND capture_time = (SELECT MAX(capture_time) FROM v_database_scoped_config WHERE server_id = $1)
Expand All @@ -70,7 +71,7 @@ FROM v_database_scoped_config
connect-time gate in ViewerDataService.cs holds at V137: a store below it has no such column and this
read would throw on the Query Store grid. */
public const string QueryStoreHealthSql = """
SELECT database_name, actual_state, desired_state, readonly_reason, current_storage_size_mb, max_storage_size_mb, size_based_cleanup_mode, stale_query_threshold_days, max_plans_per_query, interval_length_minutes, query_capture_mode, wait_stats_capture_mode
SELECT database_name, actual_state, desired_state, readonly_reason, current_storage_size_mb, max_storage_size_mb, size_based_cleanup_mode, stale_query_threshold_days, max_plans_per_query, interval_length_minutes, query_capture_mode, wait_stats_capture_mode, capture_time
FROM v_query_store_health
WHERE server_id = $1
AND capture_time = (SELECT MAX(capture_time) FROM v_query_store_health WHERE server_id = $1)
Expand All @@ -90,7 +91,7 @@ ORDER BY database_name
/// pre-fix reading. <c>DarlingCurrentConfigReader.TraceFlagsSql</c> and Lite's twin carry the same test.
/// </summary>
public const string TraceFlagsSql = """
SELECT trace_flag, status, is_global, is_session
SELECT trace_flag, status, is_global, is_session, capture_time
FROM v_trace_flags
WHERE server_id = $1
AND capture_time = (SELECT MAX(capture_time) FROM v_trace_flags WHERE server_id = $1)
Expand Down Expand Up @@ -120,7 +121,8 @@ public async Task<List<ServerConfigRow>> GetLatestServerConfigAsync(int serverId
ValueConfigured = reader.IsDBNull(1) ? 0 : reader.GetInt64(1),
ValueInUse = reader.IsDBNull(2) ? 0 : reader.GetInt64(2),
IsDynamic = !reader.IsDBNull(3) && reader.GetBoolean(3),
IsAdvanced = !reader.IsDBNull(4) && reader.GetBoolean(4)
IsAdvanced = !reader.IsDBNull(4) && reader.GetBoolean(4),
CaptureTime = reader.GetDateTime(5)
});
}

Expand Down Expand Up @@ -172,6 +174,7 @@ DatabaseConfigSql binds to the same properties — see the SELECT's byte-identic
IsAcceleratedDatabaseRecoveryOn = !reader.IsDBNull(++ordinal) && reader.GetBoolean(ordinal),
IsMemoryOptimizedEnabled = !reader.IsDBNull(++ordinal) && reader.GetBoolean(ordinal),
IsOptimizedLockingOn = reader.IsDBNull(++ordinal) ? null : reader.GetBoolean(ordinal),
CaptureTime = reader.GetDateTime(++ordinal),
});
}

Expand All @@ -195,7 +198,8 @@ public async Task<List<DatabaseScopedConfigRow>> GetLatestDatabaseScopedConfigAs
DatabaseName = reader.GetString(0),
ConfigurationName = reader.GetString(1),
Value = reader.IsDBNull(2) ? "" : reader.GetString(2),
ValueForSecondary = reader.IsDBNull(3) ? "" : reader.GetString(3)
ValueForSecondary = reader.IsDBNull(3) ? "" : reader.GetString(3),
CaptureTime = reader.GetDateTime(4)
});
}

Expand Down Expand Up @@ -231,6 +235,7 @@ public async Task<List<QueryStoreHealthRow>> GetLatestQueryStoreHealthAsync(int
and the display properties render it as the grid's absence glyph rather than as "". */
QueryCaptureMode = reader.IsDBNull(10) ? null : reader.GetString(10),
WaitStatsCaptureMode = reader.IsDBNull(11) ? null : reader.GetString(11),
CaptureTime = reader.GetDateTime(12),
});
}

Expand All @@ -253,7 +258,8 @@ public async Task<List<TraceFlagRow>> GetLatestTraceFlagsAsync(int serverId, Can
TraceFlag = reader.GetInt32(0),
Status = !reader.IsDBNull(1) && reader.GetBoolean(1),
IsGlobal = !reader.IsDBNull(2) && reader.GetBoolean(2),
IsSession = !reader.IsDBNull(3) && reader.GetBoolean(3)
IsSession = !reader.IsDBNull(3) && reader.GetBoolean(3),
CaptureTime = reader.GetDateTime(4)
});
}

Expand All @@ -266,6 +272,12 @@ public async Task<List<TraceFlagRow>> GetLatestTraceFlagsAsync(int serverId, Can

public class ServerConfigRow
{
/// <summary>#4966: when this row's snapshot was captured (UTC); every row of one snapshot shares it. The grid's Collected column sorts on it.</summary>
public DateTime CaptureTime { get; set; }

/// <summary>#4966: <see cref="CaptureTime"/> to the second, in the display zone (the grid's Collected column).</summary>
public string CaptureTimeLocal => ViewerTimeHelper.FormatForDisplay(CaptureTime, "yyyy-MM-dd HH:mm:ss");

public string ConfigurationName { get; set; } = "";
public long ValueConfigured { get; set; }
public long ValueInUse { get; set; }
Expand All @@ -278,6 +290,12 @@ public class ServerConfigRow

public class DatabaseConfigRow
{
/// <summary>#4966: when this row's snapshot was captured (UTC); every row of one snapshot shares it. The grid's Collected column sorts on it.</summary>
public DateTime CaptureTime { get; set; }

/// <summary>#4966: <see cref="CaptureTime"/> to the second, in the display zone (the grid's Collected column).</summary>
public string CaptureTimeLocal => ViewerTimeHelper.FormatForDisplay(CaptureTime, "yyyy-MM-dd HH:mm:ss");

public string DatabaseName { get; set; } = "";
public string StateDesc { get; set; } = "";
public int CompatibilityLevel { get; set; }
Expand Down Expand Up @@ -350,6 +368,12 @@ public class DatabaseConfigRow
/// </summary>
public class QueryStoreHealthRow
{
/// <summary>#4966: when this row's snapshot was captured (UTC); every row of one snapshot shares it. The grid's Collected column sorts on it.</summary>
public DateTime CaptureTime { get; set; }

/// <summary>#4966: <see cref="CaptureTime"/> to the second, in the display zone (the grid's Collected column).</summary>
public string CaptureTimeLocal => ViewerTimeHelper.FormatForDisplay(CaptureTime, "yyyy-MM-dd HH:mm:ss");

public string DatabaseName { get; set; } = "";
public string ActualState { get; set; } = "";
public string DesiredState { get; set; } = "";
Expand Down Expand Up @@ -391,6 +415,12 @@ public class QueryStoreHealthRow

public class DatabaseScopedConfigRow
{
/// <summary>#4966: when this row's snapshot was captured (UTC); every row of one snapshot shares it. The grid's Collected column sorts on it.</summary>
public DateTime CaptureTime { get; set; }

/// <summary>#4966: <see cref="CaptureTime"/> to the second, in the display zone (the grid's Collected column).</summary>
public string CaptureTimeLocal => ViewerTimeHelper.FormatForDisplay(CaptureTime, "yyyy-MM-dd HH:mm:ss");

public string DatabaseName { get; set; } = "";
public string ConfigurationName { get; set; } = "";
public string Value { get; set; } = "";
Expand All @@ -399,6 +429,12 @@ public class DatabaseScopedConfigRow

public class TraceFlagRow
{
/// <summary>#4966: when this row's snapshot was captured (UTC); every row of one snapshot shares it. The grid's Collected column sorts on it.</summary>
public DateTime CaptureTime { get; set; }

/// <summary>#4966: <see cref="CaptureTime"/> to the second, in the display zone (the grid's Collected column).</summary>
public string CaptureTimeLocal => ViewerTimeHelper.FormatForDisplay(CaptureTime, "yyyy-MM-dd HH:mm:ss");

public int TraceFlag { get; set; }
public bool Status { get; set; }
public bool IsGlobal { get; set; }
Expand Down
Loading
Loading