Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
f24d8a2
Web time-series charts break their line at collection gaps (#5644 W7)
erikdarlingdata Oct 12, 2026
efc0b7b
FinOps top-by-average ranks on the unrounded CPU average; Version Sto…
erikdarlingdata Oct 12, 2026
3139747
Web audit labels W9, W14, W16, W19: peak_granted_at, Triage/Mute firs…
erikdarlingdata Oct 12, 2026
6dd88f6
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
43f5435
CPU collector no longer stores the newest ring-buffer record twice wh…
erikdarlingdata Oct 12, 2026
ab9d879
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
de985f1
Blocking events count waits, not blocked-process reports (#5644 W1, W2)
erikdarlingdata Oct 12, 2026
cbc3f22
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
de6cf7a
Web wording: caller-only clauses, latch/spinlock rate labels, plural,…
erikdarlingdata Oct 12, 2026
6292c5c
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
58c25c3
Long text cells clip to one line, capped panels say so, ISO times pri…
erikdarlingdata Oct 12, 2026
f84f5f1
Pin the blocking wait count: one event per wait, SQL and C# agree (#5…
erikdarlingdata Oct 12, 2026
7a7ac3d
Web UI audit lane F: strict integer query params, changed-only restar…
erikdarlingdata Oct 12, 2026
4380d02
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
69bd320
Default Trace: stop re-storing the newest event and keep the Commit r…
erikdarlingdata Oct 12, 2026
632a91d
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
f3bfd36
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into lane…
erikdarlingdata Oct 12, 2026
ee284d7
Merge origin/fix/5644-web-ui-audit-lane-e into the web UI audit branc…
erikdarlingdata Oct 12, 2026
3bdd4ff
G1a: update pins the lane E merge made stale; test plainText and loca…
erikdarlingdata Oct 12, 2026
1b7c51f
Pin the CPU ring-buffer dedup with its tolerance; run the DuckDB bloc…
erikdarlingdata Oct 12, 2026
7062063
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
342f058
Lite tests: seeded blocked-process rows are separate waits (#5644 W1)
erikdarlingdata Oct 12, 2026
ede1df9
Fix the doubled comma in the Version Store collector's on-prem query …
erikdarlingdata Oct 12, 2026
87fec7a
G1a: relabel plan cache (all cache stores); name the server clock in …
erikdarlingdata Oct 12, 2026
ebf6ada
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
c36ef94
Guard the Version Store collector's query text against syntax errors:…
erikdarlingdata Oct 12, 2026
74dc733
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
5382d82
Choose XE or DMV blocking waits before the excluded-database filter; …
erikdarlingdata Oct 12, 2026
02ab9fd
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
0e2e946
Default Trace: read at-or-after the watermark tick and drop only the …
erikdarlingdata Oct 12, 2026
baaf0aa
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
01ba2b3
Blocking waits: epoch_us for DuckDB, NULL times are their own wait wi…
erikdarlingdata Oct 12, 2026
402924c
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
aa19b70
Describe the blocking count as waits in the README and the alert sett…
erikdarlingdata Oct 12, 2026
7c363d9
Merge remote-tracking branch 'origin/dev' into fix/5644-g2-deploy
erikdarlingdata Oct 12, 2026
637385c
BlockingWaitCountTests joins the live-postgres collection: its two li…
erikdarlingdata Oct 12, 2026
4c0b819
Merge remote-tracking branch 'origin/dev' into fix/5644-g2-deploy
erikdarlingdata Oct 12, 2026
9993bf0
Lite tests: the Blocked helper sets BlockedSpid once, explicit spid w…
erikdarlingdata Oct 12, 2026
edd7c3b
Web grids localize ISO instants only in opted-in message columns, nev…
erikdarlingdata Oct 12, 2026
ff012eb
The three Default Trace readers dedup on the same full column set, fi…
erikdarlingdata Oct 12, 2026
4360ce2
Chart gap threshold is computed per series, so a coarse overlay is on…
erikdarlingdata Oct 12, 2026
77a525a
Default Trace watermark rides on the trace's EventSequence, keyed to …
erikdarlingdata Oct 12, 2026
cf0232b
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
f2fbc89
Collection Health run-time column pin follows the opted-in localizeTe…
erikdarlingdata Oct 12, 2026
b10cce7
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 12, 2026
0112c46
Merge remote-tracking branch 'origin/dev' into fix/5644-g5
erikdarlingdata Oct 12, 2026
2b22139
Tag FinOpsTopAvgRankingTests with the Reads=Lite trait the guard requ…
erikdarlingdata Oct 12, 2026
fcb1e39
Web: show the alert text, sweep notes and regression text in local ti…
erikdarlingdata Oct 13, 2026
e4c156f
WIP #5644 H1: live-check wording fixes (uncommitted work of a hung la…
erikdarlingdata Oct 13, 2026
f494262
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into work…
erikdarlingdata Oct 13, 2026
dcdafef
Default Trace readers keep two real events with identical columns; ev…
erikdarlingdata Oct 13, 2026
2cb8a93
Web UI: finish the live-check wording fixes (#5644)
erikdarlingdata Oct 13, 2026
c51c727
Count blocking waits, not report rows, on Lite's server tab badge and…
erikdarlingdata Oct 13, 2026
9bb9198
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 13, 2026
f4f713d
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 13, 2026
fa89a49
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 13, 2026
5cb482a
Merge origin/dev into fix/5644-web-ui-audit
erikdarlingdata Oct 13, 2026
5ba497c
Starter dashboard blocking trend panel is titled for reports or snaps…
erikdarlingdata Oct 13, 2026
b69234e
Overview tab Deadlocks chart uses the same empty sentence as the Bloc…
erikdarlingdata Oct 13, 2026
3c4fc70
Tests follow the web UI audit: Alert History harness finds Metric by …
erikdarlingdata Oct 13, 2026
168ffe3
Update four test pins the #5644 web UI audit outdated: wait-counting …
erikdarlingdata Oct 13, 2026
aaa10d2
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 13, 2026
54909f3
CI fixes: four pins follow the #5644 wording and panel changes; darli…
erikdarlingdata Oct 13, 2026
93d5cbb
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 13, 2026
0785efa
Band blocking on report counts again; /api/fleet refuses out-of-range…
erikdarlingdata Oct 13, 2026
03cdb22
Merge remote-tracking branch 'origin/fix/5644-web-ui-audit' into fix/…
erikdarlingdata Oct 13, 2026
d47ce50
Band the daily summary and the fleet sweep on blocked-process report …
erikdarlingdata Oct 13, 2026
87ec9c1
Merge origin/dev into fix/5644-web-ui-audit; keep #5732's favicon
erikdarlingdata Oct 13, 2026
6e8fa8a
The fleet bounded-read reference query returns the band's report coun…
erikdarlingdata Oct 13, 2026
3fa2d6d
Merge origin/dev into fix/5644-web-ui-audit; keep the skip-collector …
erikdarlingdata Oct 13, 2026
3f6f8ae
Pin that the daily summary SQL carries the skip collector's name (#5644)
erikdarlingdata Oct 13, 2026
cdf79d3
Merge origin/dev into fix/5644-web-ui-audit
erikdarlingdata Oct 13, 2026
f1f1f59
Merge origin/dev into fix/5644-web-ui-audit (#5756 CI fixes)
erikdarlingdata Oct 13, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,8 @@ jobs:
- 'Lite.Tests/LiteTimeRangeGapTests.cs'
# #5641: Darling.Tests compiles this Lite.Tests file in (Linked\AlertLegendParityTests.cs), so an edit to it has to reach the Darling suite.
- 'Lite.Tests/AlertLegendParityTests.cs'
# #5644: Darling.Tests compiles this Lite.Tests file in (Linked\DefaultTraceEventSequenceWatermarkTests.cs), so an edit to it has to reach the Darling suite.
- 'Lite.Tests/DefaultTraceEventSequenceWatermarkTests.cs'
# #5596: Darling.Tests' WpfExitSwitchPinTests reads this project file for the WPF exit switch, so an edit to it has to reach the Darling suite.
- 'Lite.Tests/Lite.Tests.csproj'
# #5616: the two shard scripts are run by the shard jobs and parsed by Darling.Tests pins.
Expand Down
4 changes: 3 additions & 1 deletion Darling/Darling.Tests/AlertNotebookRenderClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ public void Views_AlertHeaderCell_RendersValueThresholdAndDetailText_ThroughElsT
Assert.Contains("a.detail_text", views, StringComparison.Ordinal);

// The detail_text line must go through el()'s { text: ... } prop (textContent), never innerHTML.
Assert.Contains("el(\"pre\", { class: \"code\", text: a.detail_text })", views, StringComparison.Ordinal);
// W20 (#5644) shows the stored text with its UTC lines converted to the viewer's zone (localizeAlertDetail returns
// a string), so the text prop now holds that call; it is still el()'s text path, not markup.
Assert.Contains("el(\"pre\", { class: \"code\", text: localizeAlertDetail(a.detail_text, a.newest_sample) })", views, StringComparison.Ordinal);

// Scope the innerHTML ban to renderAlertHeaderCell's own body — the file's header comment (R4)
// says "never innerHTML" in prose, which the whole-file assertion this replaces was tripping on.
Expand Down
165 changes: 165 additions & 0 deletions Darling/Darling.Tests/BlockingBandReportUnitLiveTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Npgsql;
using PerformanceMonitor.Collectors;
using PerformanceMonitor.Common;
using PerformanceMonitor.Darling.Service.Mcp;
using PerformanceMonitor.Darling.Storage;
using PerformanceMonitor.Darling.Viewer;
using Xunit;

namespace Darling.Tests;

/// <summary>
/// #5644 review of W1: the displayed blocking count is WAITS, but the health band's per-hour count tiers
/// (<c>ServerHealthThresholds.BlockingWarnPerHour</c> 5 / <c>BlockingCriticalPerHour</c> 20) were measured in
/// blocked-process REPORTS per server-hour, so the band reads the report count. One wait that SQL Server reported
/// 22 times in the last ten minutes (short waits, so the 10 s / 60 s wait arms stay out of it) is ONE wait on the
/// card and 22 reports for the band: Critical by count, as it was before W1. Read through the fleet reader and the
/// viewer's per-server summary. Gated on DARLING_TEST_PG.
/// </summary>
/* #1776 own-store: every fact plants its rows under one dedicated server id and deletes them in cleanup. */
[Collection("live-postgres")]
public sealed class BlockingBandReportUnitLiveTests
{
private const string ServerName = "darling-blocking-band-report-unit";
private static readonly int ServerId = ServerIdHelper.GetDeterministicHashCode(ServerName);

private const int Reports = 22;

private static string? ConnectionString => Environment.GetEnvironmentVariable("DARLING_TEST_PG");

private static async Task ExecAsync(NpgsqlConnection c, string sql, CancellationToken ct, params object[] p)
{
using var cmd = new NpgsqlCommand(sql, c);
foreach (var v in p)
cmd.Parameters.AddWithValue(v);
await cmd.ExecuteNonQueryAsync(ct);
}

/* One wait reported 22 times, 100 ms apart: the k-th report carries wait_time_ms = k * 100 and is written at
start + wait, so every report derives the same wait start (the rule BlockingWaits collapses on). The longest
wait is 2.2 s: under the 10 s wait arm, so only the count arm can band it. */
private static async Task PlantAsync(NpgsqlConnection c, bool dmv, CancellationToken ct)
{
var start = DateTime.SpecifyKind(DateTime.UtcNow.AddMinutes(-10), DateTimeKind.Unspecified);
for (var k = 1; k <= Reports; k++)
{
var at = start.AddMilliseconds(100 * k);
if (dmv)
await ExecAsync(c, "INSERT INTO dmv_blocking_snapshots (collection_id, collection_time, server_id, server_name, event_time, database_name, blocked_spid, blocking_spid, wait_time_ms, blocking_status) VALUES ($1,$2,$3,$4,$2,'db',52,51,$5,'suspended')",
ct, CollectionIdGenerator.Next(), at, ServerId, ServerName, (long)(100 * k));
else
await ExecAsync(c, "INSERT INTO blocked_process_reports (blocked_report_id, collection_time, server_id, server_name, event_time, wait_time_ms, blocking_spid, blocked_spid, blocking_status, database_name) VALUES ($1,$2,$3,$4,$2,$5,51,52,'suspended','db')",
ct, CollectionIdGenerator.Next(), at, ServerId, ServerName, (long)(100 * k));
}
}

private static async Task<NpgsqlConnection> OpenSeededAsync(bool dmv, CancellationToken ct)
{
var connection = new NpgsqlConnection(ConnectionString);
await connection.OpenAsync(ct);
await PgMigrations.MigrateAsync(connection, ct);
await DeleteRowsAsync(connection, ct);
await ExecAsync(connection, @"
INSERT INTO servers (server_id, server_name, display_name, is_enabled, sql_major_version, created_date, modified_date)
VALUES ($1, $2, $2, TRUE, 16, now()::timestamp, now()::timestamp)
ON CONFLICT (server_id) DO UPDATE SET is_enabled = TRUE", ct, ServerId, ServerName);
await PlantAsync(connection, dmv, ct);
return connection;
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task TheFleetCard_ShowsOneWait_AndBandsOnTheReports(bool dmv)
{
Assert.SkipWhen(string.IsNullOrEmpty(ConnectionString), "Set DARLING_TEST_PG to a Postgres connection string to run the live test.");
var ct = TestContext.Current.CancellationToken;
var bodySucceeded = false;
try
{
await using var connection = await OpenSeededAsync(dmv, ct);
await using var postgres = NpgsqlDataSource.Create(ConnectionString!);
var now = DateTime.UtcNow;

var result = await DarlingFleetReader.GetFleetOverviewAsync(
postgres, now.AddHours(-1), now, now, cancellationToken: ct);

var card = result.Cards.Single(c => c.ServerId == ServerId);
Assert.Equal(1, card.BlockingCount); // displayed: ONE wait
Assert.Equal(Reports, card.BlockingReportCount); // the band's unit
Assert.Equal((double)Reports, card.BlockingRatePerHour); // reports per hour
Assert.Equal(HealthSeverity.Critical, card.BlockingSeverity);
Assert.True(card.MaxBlockingWaitMs < 10_000, "the wait arm must not be what bands this card");
/* The re-band the fleet ranking runs reads the same report count. */
Assert.Equal(Reports, card.ToHealthMetrics().BlockingCount);
bodySucceeded = true;
}
finally
{
await LiveStoreCleanup.RunAsync(ConnectionString!, bodySucceeded, async (cleanup, cleanupCt) =>
await DeleteRowsAsync(cleanup, cleanupCt));
}
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task TheViewerSummary_ShowsOneWait_AndBandsOnTheReports(bool dmv)
{
Assert.SkipWhen(string.IsNullOrEmpty(ConnectionString), "Set DARLING_TEST_PG to a Postgres connection string to run the live test.");
var ct = TestContext.Current.CancellationToken;
var bodySucceeded = false;
try
{
await using var connection = await OpenSeededAsync(dmv, ct);
await using var viewer = new ViewerDataService(ConnectionString!);

var summary = await viewer.GetServerSummaryAsync(ServerId, ServerName, null, ct);

Assert.Equal(1, summary.BlockingCount);
Assert.Equal(Reports, summary.BlockingReportCount);
Assert.Equal(Reports, summary.BlockingCountForBand);
Assert.Equal((double)Reports, summary.BlockingRatePerHour);
Assert.Equal(HealthSeverity.Critical, summary.BlockingSeverity);
bodySucceeded = true;
}
finally
{
await LiveStoreCleanup.RunAsync(ConnectionString!, bodySucceeded, async (cleanup, cleanupCt) =>
await DeleteRowsAsync(cleanup, cleanupCt));
}
}

/// <summary>A card or summary built without a report count (the pre-W1 shape) bands on its own count.</summary>
[Fact]
public void ACardBuiltWithoutAReportCount_BandsOnItsCount()
{
var summary = new ServerSummaryItem { BlockingCount = 7, BlockingWindow = TimeSpan.FromHours(1) };
Assert.Equal(7, summary.BlockingCountForBand);

var withReports = new ServerSummaryItem { BlockingCount = 1, BlockingReportCount = 30, BlockingWindow = TimeSpan.FromHours(1) };
Assert.Equal(30, withReports.BlockingCountForBand);
Assert.Equal(HealthSeverity.Critical, withReports.BlockingSeverity);
}

private static async Task DeleteRowsAsync(NpgsqlConnection connection, CancellationToken ct)
{
using var cleanup = new NpgsqlCommand(
$"DELETE FROM blocked_process_reports WHERE server_id = {ServerId}; " +
$"DELETE FROM dmv_blocking_snapshots WHERE server_id = {ServerId}; " +
$"DELETE FROM servers WHERE server_id = {ServerId};", connection);
await cleanup.ExecuteNonQueryAsync(ct);
}
}
Loading
Loading