diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
index 854b987..bd45cc4 100644
--- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
+++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
@@ -801,6 +801,9 @@ private static string FormatBytes(double bytes)
return $"{bytes / (1024L * 1024 * 1024):N1} GB";
}
+ private static string FormatBenefitPercent(double pct) =>
+ pct >= 100 ? $"{pct:N0}" : $"{pct:N1}";
+
#endregion
#region Node Selection & Properties Panel
@@ -1737,7 +1740,7 @@ private void ShowPropertiesPanel(PlanNode node)
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
var warnPanel = new StackPanel { Margin = new Thickness(10, 2, 10, 2) };
var planWarnHeader = w.MaxBenefitPercent.HasValue
- ? $"\u26A0 {w.WarningType} \u2014 up to {w.MaxBenefitPercent:N0}% benefit"
+ ? $"\u26A0 {w.WarningType} \u2014 up to {FormatBenefitPercent(w.MaxBenefitPercent.Value)}% benefit"
: $"\u26A0 {w.WarningType}";
warnPanel.Children.Add(new TextBlock
{
@@ -1819,7 +1822,7 @@ private void ShowPropertiesPanel(PlanNode node)
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
var warnPanel = new StackPanel { Margin = new Thickness(10, 2, 10, 2) };
var nodeWarnHeader = w.MaxBenefitPercent.HasValue
- ? $"\u26A0 {w.WarningType} \u2014 up to {w.MaxBenefitPercent:N0}% benefit"
+ ? $"\u26A0 {w.WarningType} \u2014 up to {FormatBenefitPercent(w.MaxBenefitPercent.Value)}% benefit"
: $"\u26A0 {w.WarningType}";
warnPanel.Children.Add(new TextBlock
{
diff --git a/src/PlanViewer.App/PlanViewer.App.csproj b/src/PlanViewer.App/PlanViewer.App.csproj
index c00ff0c..5334a5c 100644
--- a/src/PlanViewer.App/PlanViewer.App.csproj
+++ b/src/PlanViewer.App/PlanViewer.App.csproj
@@ -6,7 +6,7 @@
app.manifest
EDD.ico
true
- 1.7.1
+ 1.7.2
Erik Darling
Darling Data LLC
Performance Studio
diff --git a/src/PlanViewer.Core/Output/HtmlExporter.cs b/src/PlanViewer.Core/Output/HtmlExporter.cs
index 897598c..7877137 100644
--- a/src/PlanViewer.Core/Output/HtmlExporter.cs
+++ b/src/PlanViewer.Core/Output/HtmlExporter.cs
@@ -407,7 +407,7 @@ private static void WriteWaitStatsCard(StringBuilder sb, StatementResult stmt, b
{
var barPct = maxWait > 0 ? (double)w.WaitTimeMs / maxWait * 100 : 0;
var benefitTag = benefitLookup.TryGetValue(w.WaitType, out var pct)
- ? $" up to {pct:N0}%"
+ ? $" up to {(pct >= 100 ? pct.ToString("N0") : pct.ToString("N1"))}%"
: "";
sb.AppendLine("
");
sb.AppendLine($"{Encode(w.WaitType)}");
@@ -458,7 +458,7 @@ private static void WriteWarnings(StringBuilder sb, StatementResult stmt)
sb.AppendLine($"{Encode(w.Operator)}");
sb.AppendLine($"{Encode(w.Type)}");
if (w.MaxBenefitPercent.HasValue)
- sb.AppendLine($"up to {w.MaxBenefitPercent:N0}% benefit");
+ sb.AppendLine($"up to {(w.MaxBenefitPercent.Value >= 100 ? w.MaxBenefitPercent.Value.ToString("N0") : w.MaxBenefitPercent.Value.ToString("N1"))}% benefit");
sb.AppendLine($"{Encode(w.Message)}");
if (!string.IsNullOrEmpty(w.ActionableFix))
sb.AppendLine($"{Encode(w.ActionableFix)}");
diff --git a/src/PlanViewer.Core/Output/TextFormatter.cs b/src/PlanViewer.Core/Output/TextFormatter.cs
index d5ec7d1..954af41 100644
--- a/src/PlanViewer.Core/Output/TextFormatter.cs
+++ b/src/PlanViewer.Core/Output/TextFormatter.cs
@@ -139,7 +139,7 @@ public static void WriteText(AnalysisResult result, TextWriter writer)
foreach (var w in stmt.WaitStats.OrderByDescending(w => w.WaitTimeMs))
{
var benefitTag = benefitLookup.TryGetValue(w.WaitType, out var pct)
- ? $" (up to {pct:N0}% benefit)"
+ ? $" (up to {(pct >= 100 ? pct.ToString("N0") : pct.ToString("N1"))}% benefit)"
: "";
writer.WriteLine($" {w.WaitType}: {w.WaitTimeMs:N0}ms{benefitTag}");
}
@@ -169,7 +169,7 @@ public static void WriteText(AnalysisResult result, TextWriter writer)
foreach (var w in sortedWarnings)
{
var benefitTag = w.MaxBenefitPercent.HasValue
- ? $" (up to {w.MaxBenefitPercent:N0}% benefit)"
+ ? $" (up to {(w.MaxBenefitPercent.Value >= 100 ? w.MaxBenefitPercent.Value.ToString("N0") : w.MaxBenefitPercent.Value.ToString("N1"))}% benefit)"
: "";
writer.WriteLine($" [{w.Severity}] {w.Type}{benefitTag}: {EscapeNewlines(w.Message)}");
if (!string.IsNullOrEmpty(w.ActionableFix))
diff --git a/src/PlanViewer.Core/Services/PlanAnalyzer.cs b/src/PlanViewer.Core/Services/PlanAnalyzer.cs
index 01229ea..a66cf67 100644
--- a/src/PlanViewer.Core/Services/PlanAnalyzer.cs
+++ b/src/PlanViewer.Core/Services/PlanAnalyzer.cs
@@ -1593,26 +1593,45 @@ private static long GetPerThreadOwnElapsed(PlanNode node)
}
///
- /// Serial row mode self-time: subtract all direct children's elapsed.
- /// Exchange children are skipped through to their real child.
+ /// Serial row mode self-time: subtract all direct children's effective elapsed.
+ /// Pass-through operators (Compute Scalar, etc.) don't carry runtime stats —
+ /// look through them to the first descendant that does. Exchange children
+ /// use max-child elapsed because exchange times are unreliable.
///
private static long GetSerialOwnElapsed(PlanNode node)
{
var totalChildElapsed = 0L;
foreach (var child in node.Children)
- {
- var childElapsed = child.ActualElapsedMs;
-
- // Exchange operators have unreliable times — skip to their child
- if (child.PhysicalOp == "Parallelism" && child.Children.Count > 0)
- childElapsed = child.Children.Max(c => c.ActualElapsedMs);
-
- totalChildElapsed += childElapsed;
- }
+ totalChildElapsed += GetEffectiveChildElapsedMs(child);
return Math.Max(0, node.ActualElapsedMs - totalChildElapsed);
}
+ ///
+ /// Returns the elapsed time a child contributes to its parent's subtree.
+ /// Looks through pass-through operators (Compute Scalar, Parallelism exchange)
+ /// that don't carry reliable runtime stats.
+ ///
+ private static long GetEffectiveChildElapsedMs(PlanNode child)
+ {
+ // Exchange operators: unreliable times, use max child
+ if (child.PhysicalOp == "Parallelism" && child.Children.Count > 0)
+ return child.Children.Max(GetEffectiveChildElapsedMs);
+
+ // Child has its own stats: use them
+ if (child.ActualElapsedMs > 0)
+ return child.ActualElapsedMs;
+
+ // No stats (Compute Scalar and similar): look through to descendants
+ if (child.Children.Count == 0)
+ return 0;
+
+ var sum = 0L;
+ foreach (var grandchild in child.Children)
+ sum += GetEffectiveChildElapsedMs(grandchild);
+ return sum;
+ }
+
///
/// Calculates a Parallelism (exchange) operator's own elapsed time.
/// Exchange times are unreliable — they accumulate wait time caused by
diff --git a/src/PlanViewer.Web/Pages/Index.razor b/src/PlanViewer.Web/Pages/Index.razor
index 0fc82d5..3fd618b 100644
--- a/src/PlanViewer.Web/Pages/Index.razor
+++ b/src/PlanViewer.Web/Pages/Index.razor
@@ -302,7 +302,7 @@ else
@w.WaitTimeMs.ToString("N0") ms
@if (benefitPct > 0)
{
- up to @benefitPct.ToString("N0")%
+ up to @(benefitPct >= 100 ? benefitPct.ToString("N0") : benefitPct.ToString("N1"))%
}
@@ -346,7 +346,7 @@ else
@w.Type
@if (w.MaxBenefitPercent.HasValue)
{
- up to @w.MaxBenefitPercent.Value.ToString("N0")% benefit
+ up to @(w.MaxBenefitPercent.Value >= 100 ? w.MaxBenefitPercent.Value.ToString("N0") : w.MaxBenefitPercent.Value.ToString("N1"))% benefit
}
@w.Message
@if (!string.IsNullOrEmpty(w.ActionableFix))