From 7f7bafd7996ca5d563669a3a33cb61f748c194f2 Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Mon, 28 Sep 2026 10:01:49 -0400
Subject: [PATCH 1/4] Plan viewer: make Wait Stats card collapsible, closed by
default
Ports erikdarlingdata/PerformanceStudio@9b8252e to the shared WPF plan
viewer (PerformanceMonitor.Ui/PlanViewerControl.xaml, hosted by both
the Darling Viewer and Lite). The Wait Stats card gets its own nested
Expander, closed by default, instead of always showing alongside
Runtime Summary and Missing Index Suggestions inside the outer
Insights expander.
The header text moves to a pure helper, PlanDisplayText.WaitStatsHeader,
matching PS's format: "Wait Stats" alone when there's nothing to show,
or "Wait Stats N ms" with the total wait time when there is.
---
Darling/Darling.Tests/Viewer4571Tests.cs | 24 ++++++++++++++
.../PlanDisplayText.cs | 8 +++++
.../PlanViewerControl.Properties.cs | 4 +--
PerformanceMonitor.Ui/PlanViewerControl.xaml | 33 +++++++++++--------
4 files changed, 54 insertions(+), 15 deletions(-)
create mode 100644 Darling/Darling.Tests/Viewer4571Tests.cs
diff --git a/Darling/Darling.Tests/Viewer4571Tests.cs b/Darling/Darling.Tests/Viewer4571Tests.cs
new file mode 100644
index 0000000000..576cf79070
--- /dev/null
+++ b/Darling/Darling.Tests/Viewer4571Tests.cs
@@ -0,0 +1,24 @@
+using PerformanceMonitor.PlanAnalysis;
+using Xunit;
+
+namespace Darling.Tests;
+
+///
+/// Pins the Wait Stats card's collapsible header text (#4571), matching
+/// erikdarlingdata/PerformanceStudio@9b8252e: "Wait Stats" alone when empty, "Wait Stats N ms"
+/// when there's a total wait time to show.
+///
+public class Viewer4571Tests
+{
+ [Fact]
+ public void WaitStatsHeader_NoWaits_IsPlainLabel()
+ {
+ Assert.Equal("Wait Stats", PlanDisplayText.WaitStatsHeader(0, 0));
+ }
+
+ [Fact]
+ public void WaitStatsHeader_WithWaits_AppendsTotalMs()
+ {
+ Assert.Equal("Wait Stats 1,234 ms", PlanDisplayText.WaitStatsHeader(3, 1234));
+ }
+}
diff --git a/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs b/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
index 1641c18ec8..b853cfd37f 100644
--- a/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
+++ b/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
@@ -17,6 +17,14 @@ public static class PlanDisplayText
public static string? ParseErrorMessage(ParsedPlan plan) =>
plan.ParseError == null ? null : $"This plan couldn't be parsed: {plan.ParseError}";
+ ///
+ /// The Wait Stats card's collapsible header text, matching erikdarlingdata/PerformanceStudio@9b8252e:
+ /// "Wait Stats" alone when there's nothing to show, or "Wait Stats" plus the total wait time in
+ /// milliseconds when there is. is the sum of every wait's WaitTimeMs.
+ ///
+ public static string WaitStatsHeader(int waitCount, long totalWaitMs) =>
+ waitCount > 0 ? $"Wait Stats {totalWaitMs:N0} ms" : "Wait Stats";
+
///
/// The CPU:Elapsed ratio shown in the runtime summary, matching
/// erikdarlingdata/PerformanceStudio@28d4c74's row: CPU time with external-wait time
diff --git a/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs b/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
index 3e4fdcbec9..d0affc446e 100644
--- a/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
+++ b/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
@@ -1044,7 +1044,7 @@ private void ShowWaitStats(List waits, bool isActualPlan)
if (waits.Count == 0)
{
- WaitStatsHeader.Text = "Wait Stats";
+ WaitStatsHeader.Text = PlanDisplayText.WaitStatsHeader(0, 0);
WaitStatsEmpty.Text = isActualPlan
? "No wait stats recorded"
: "No wait stats (estimated plan)";
@@ -1058,7 +1058,7 @@ private void ShowWaitStats(List waits, bool isActualPlan)
var maxWait = sorted[0].WaitTimeMs;
var totalWait = sorted.Sum(w => w.WaitTimeMs);
- WaitStatsHeader.Text = $" Wait Stats \u2014 {totalWait:N0}ms total";
+ WaitStatsHeader.Text = PlanDisplayText.WaitStatsHeader(sorted.Count, totalWait);
var longestName = sorted.Max(w => w.WaitType.Length);
var nameColWidth = longestName * 6.5 + 10;
diff --git a/PerformanceMonitor.Ui/PlanViewerControl.xaml b/PerformanceMonitor.Ui/PlanViewerControl.xaml
index 4899baa688..a465948c13 100644
--- a/PerformanceMonitor.Ui/PlanViewerControl.xaml
+++ b/PerformanceMonitor.Ui/PlanViewerControl.xaml
@@ -96,24 +96,31 @@
-
+
-
-
+
+
-
-
-
-
+ FontWeight="SemiBold" Foreground="{DynamicResource PlanWaitStatsHeaderBrush}"/>
+
+
+
+
+
+
+
+
From 3f916bf87c95ebdf95adc173a98f6e68bcb55abd Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Mon, 28 Sep 2026 10:18:25 -0400
Subject: [PATCH 2/4] Plan viewer: match PerformanceStudio desktop's Wait Stats
header text (#4571)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The collapsible Wait Stats card's header now reads "Wait Stats" when
empty, or "Wait Stats — N ms total" when there is a total wait time,
matching PlanViewerControl.WaitStats.cs in the desktop viewer.
---
Darling/Darling.Tests/Viewer4571Tests.cs | 6 +++---
PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs | 6 +++---
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/Darling/Darling.Tests/Viewer4571Tests.cs b/Darling/Darling.Tests/Viewer4571Tests.cs
index 576cf79070..219b1f50ab 100644
--- a/Darling/Darling.Tests/Viewer4571Tests.cs
+++ b/Darling/Darling.Tests/Viewer4571Tests.cs
@@ -5,8 +5,8 @@ namespace Darling.Tests;
///
/// Pins the Wait Stats card's collapsible header text (#4571), matching
-/// erikdarlingdata/PerformanceStudio@9b8252e: "Wait Stats" alone when empty, "Wait Stats N ms"
-/// when there's a total wait time to show.
+/// erikdarlingdata/PerformanceStudio@9b8252e: "Wait Stats" alone when empty, "Wait Stats \u2014 Nms total"
+/// when there's a total wait time to show (matching the desktop viewer's PlanViewerControl.WaitStats.cs).
///
public class Viewer4571Tests
{
@@ -19,6 +19,6 @@ public void WaitStatsHeader_NoWaits_IsPlainLabel()
[Fact]
public void WaitStatsHeader_WithWaits_AppendsTotalMs()
{
- Assert.Equal("Wait Stats 1,234 ms", PlanDisplayText.WaitStatsHeader(3, 1234));
+ Assert.Equal("Wait Stats \u2014 1,234ms total", PlanDisplayText.WaitStatsHeader(3, 1234));
}
}
diff --git a/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs b/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
index b853cfd37f..04d7c8f791 100644
--- a/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
+++ b/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
@@ -19,11 +19,11 @@ public static class PlanDisplayText
///
/// The Wait Stats card's collapsible header text, matching erikdarlingdata/PerformanceStudio@9b8252e:
- /// "Wait Stats" alone when there's nothing to show, or "Wait Stats" plus the total wait time in
- /// milliseconds when there is. is the sum of every wait's WaitTimeMs.
+ /// "Wait Stats" alone when there's nothing to show, or "Wait Stats \u2014 Nms total" when there
+ /// is. is the sum of every wait's WaitTimeMs.
///
public static string WaitStatsHeader(int waitCount, long totalWaitMs) =>
- waitCount > 0 ? $"Wait Stats {totalWaitMs:N0} ms" : "Wait Stats";
+ waitCount > 0 ? $"Wait Stats \u2014 {totalWaitMs:N0}ms total" : "Wait Stats";
///
/// The CPU:Elapsed ratio shown in the runtime summary, matching
From 6ddcb249d723bb1cc4a5d15c546e411410909bc2 Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Mon, 28 Sep 2026 10:33:51 -0400
Subject: [PATCH 3/4] Fix a stray \u2014 escape in a doc comment and add the
Wait Stats header tooltip
Matches PerformanceStudio desktop's Wait Stats header tooltip, shown only
when there are waits to describe.
---
Darling/Darling.Tests/Viewer4571Tests.cs | 12 ++++++++++++
PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs | 11 +++++++++--
.../PlanViewerControl.Properties.cs | 2 ++
3 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/Darling/Darling.Tests/Viewer4571Tests.cs b/Darling/Darling.Tests/Viewer4571Tests.cs
index 219b1f50ab..c1acdf1a4e 100644
--- a/Darling/Darling.Tests/Viewer4571Tests.cs
+++ b/Darling/Darling.Tests/Viewer4571Tests.cs
@@ -21,4 +21,16 @@ public void WaitStatsHeader_WithWaits_AppendsTotalMs()
{
Assert.Equal("Wait Stats \u2014 1,234ms total", PlanDisplayText.WaitStatsHeader(3, 1234));
}
+
+ [Fact]
+ public void WaitStatsHeaderTooltip_NoWaits_IsNull()
+ {
+ Assert.Null(PlanDisplayText.WaitStatsHeaderTooltip(0, 0));
+ }
+
+ [Fact]
+ public void WaitStatsHeaderTooltip_WithWaits_DescribesCountAndTotal()
+ {
+ Assert.Equal("1,234 ms of waits across 3 wait types", PlanDisplayText.WaitStatsHeaderTooltip(3, 1234));
+ }
}
diff --git a/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs b/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
index 04d7c8f791..9106df43bf 100644
--- a/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
+++ b/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
@@ -19,12 +19,19 @@ public static class PlanDisplayText
///
/// The Wait Stats card's collapsible header text, matching erikdarlingdata/PerformanceStudio@9b8252e:
- /// "Wait Stats" alone when there's nothing to show, or "Wait Stats \u2014 Nms total" when there
- /// is. is the sum of every wait's WaitTimeMs.
+ /// "Wait Stats" alone when there's nothing to show, or Wait Stats, an em dash, then the total
+ /// in milliseconds when there is. is the sum of every wait's WaitTimeMs.
///
public static string WaitStatsHeader(int waitCount, long totalWaitMs) =>
waitCount > 0 ? $"Wait Stats \u2014 {totalWaitMs:N0}ms total" : "Wait Stats";
+ ///
+ /// The Wait Stats card's header tooltip, matching erikdarlingdata/PerformanceStudio's desktop viewer:
+ /// shown only when there are waits to describe.
+ ///
+ public static string? WaitStatsHeaderTooltip(int waitCount, long totalWaitMs) =>
+ waitCount > 0 ? $"{totalWaitMs:N0} ms of waits across {waitCount} wait types" : null;
+
///
/// The CPU:Elapsed ratio shown in the runtime summary, matching
/// erikdarlingdata/PerformanceStudio@28d4c74's row: CPU time with external-wait time
diff --git a/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs b/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
index d0affc446e..1d72a831b0 100644
--- a/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
+++ b/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
@@ -1045,6 +1045,7 @@ private void ShowWaitStats(List waits, bool isActualPlan)
if (waits.Count == 0)
{
WaitStatsHeader.Text = PlanDisplayText.WaitStatsHeader(0, 0);
+ WaitStatsHeader.ToolTip = PlanDisplayText.WaitStatsHeaderTooltip(0, 0);
WaitStatsEmpty.Text = isActualPlan
? "No wait stats recorded"
: "No wait stats (estimated plan)";
@@ -1059,6 +1060,7 @@ private void ShowWaitStats(List waits, bool isActualPlan)
var totalWait = sorted.Sum(w => w.WaitTimeMs);
WaitStatsHeader.Text = PlanDisplayText.WaitStatsHeader(sorted.Count, totalWait);
+ WaitStatsHeader.ToolTip = PlanDisplayText.WaitStatsHeaderTooltip(sorted.Count, totalWait);
var longestName = sorted.Max(w => w.WaitType.Length);
var nameColWidth = longestName * 6.5 + 10;
From 8f048c116883c94946365a817bb96df6a4f79850 Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Mon, 28 Sep 2026 10:49:04 -0400
Subject: [PATCH 4/4] Cut #4571 down to PS desktop parity: tooltip only, no
per-card expander
---
Darling/Darling.Tests/Viewer4571Tests.cs | 17 ++--------
.../PlanDisplayText.cs | 8 -----
.../PlanViewerControl.Properties.cs | 4 +--
PerformanceMonitor.Ui/PlanViewerControl.xaml | 33 ++++++++-----------
4 files changed, 17 insertions(+), 45 deletions(-)
diff --git a/Darling/Darling.Tests/Viewer4571Tests.cs b/Darling/Darling.Tests/Viewer4571Tests.cs
index c1acdf1a4e..6a8516d6c2 100644
--- a/Darling/Darling.Tests/Viewer4571Tests.cs
+++ b/Darling/Darling.Tests/Viewer4571Tests.cs
@@ -4,24 +4,11 @@
namespace Darling.Tests;
///
-/// Pins the Wait Stats card's collapsible header text (#4571), matching
-/// erikdarlingdata/PerformanceStudio@9b8252e: "Wait Stats" alone when empty, "Wait Stats \u2014 Nms total"
-/// when there's a total wait time to show (matching the desktop viewer's PlanViewerControl.WaitStats.cs).
+/// Pins the Wait Stats card header's tooltip (#4571), matching erikdarlingdata/PerformanceStudio's
+/// desktop viewer: null when there are no waits, otherwise the total wait time and wait-type count.
///
public class Viewer4571Tests
{
- [Fact]
- public void WaitStatsHeader_NoWaits_IsPlainLabel()
- {
- Assert.Equal("Wait Stats", PlanDisplayText.WaitStatsHeader(0, 0));
- }
-
- [Fact]
- public void WaitStatsHeader_WithWaits_AppendsTotalMs()
- {
- Assert.Equal("Wait Stats \u2014 1,234ms total", PlanDisplayText.WaitStatsHeader(3, 1234));
- }
-
[Fact]
public void WaitStatsHeaderTooltip_NoWaits_IsNull()
{
diff --git a/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs b/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
index 9106df43bf..a8b9eab941 100644
--- a/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
+++ b/PerformanceMonitor.PlanAnalysis/PlanDisplayText.cs
@@ -17,14 +17,6 @@ public static class PlanDisplayText
public static string? ParseErrorMessage(ParsedPlan plan) =>
plan.ParseError == null ? null : $"This plan couldn't be parsed: {plan.ParseError}";
- ///
- /// The Wait Stats card's collapsible header text, matching erikdarlingdata/PerformanceStudio@9b8252e:
- /// "Wait Stats" alone when there's nothing to show, or Wait Stats, an em dash, then the total
- /// in milliseconds when there is. is the sum of every wait's WaitTimeMs.
- ///
- public static string WaitStatsHeader(int waitCount, long totalWaitMs) =>
- waitCount > 0 ? $"Wait Stats \u2014 {totalWaitMs:N0}ms total" : "Wait Stats";
-
///
/// The Wait Stats card's header tooltip, matching erikdarlingdata/PerformanceStudio's desktop viewer:
/// shown only when there are waits to describe.
diff --git a/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs b/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
index 21cee18582..fdec547814 100644
--- a/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
+++ b/PerformanceMonitor.Ui/PlanViewerControl.Properties.cs
@@ -1066,7 +1066,7 @@ private void ShowWaitStats(List waits, bool isActualPlan)
if (waits.Count == 0)
{
- WaitStatsHeader.Text = PlanDisplayText.WaitStatsHeader(0, 0);
+ WaitStatsHeader.Text = "Wait Stats";
WaitStatsHeader.ToolTip = PlanDisplayText.WaitStatsHeaderTooltip(0, 0);
WaitStatsEmpty.Text = isActualPlan
? "No wait stats recorded"
@@ -1081,7 +1081,7 @@ private void ShowWaitStats(List waits, bool isActualPlan)
var maxWait = sorted[0].WaitTimeMs;
var totalWait = sorted.Sum(w => w.WaitTimeMs);
- WaitStatsHeader.Text = PlanDisplayText.WaitStatsHeader(sorted.Count, totalWait);
+ WaitStatsHeader.Text = $" Wait Stats \u2014 {totalWait:N0}ms total";
WaitStatsHeader.ToolTip = PlanDisplayText.WaitStatsHeaderTooltip(sorted.Count, totalWait);
var longestName = sorted.Max(w => w.WaitType.Length);
diff --git a/PerformanceMonitor.Ui/PlanViewerControl.xaml b/PerformanceMonitor.Ui/PlanViewerControl.xaml
index a465948c13..4899baa688 100644
--- a/PerformanceMonitor.Ui/PlanViewerControl.xaml
+++ b/PerformanceMonitor.Ui/PlanViewerControl.xaml
@@ -96,31 +96,24 @@
-
+
-
-
+
+
-
-
-
-
-
-
-
-
+ FontWeight="SemiBold" Foreground="{DynamicResource PlanWaitStatsHeaderBrush}"
+ Margin="0,0,0,6"/>
+
+
+
+