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))