Skip to content

Commit 3a40ada

Browse files
Fix operator self-time for parents of Compute Scalar (#215 C5) + one decimal on benefit % (#255)
C5 (operator self-time look-through): GetSerialOwnElapsed subtracted each direct child's ActualElapsedMs, but Compute Scalar operators don't carry runtime stats (ActualElapsedMs = 0). For an operator whose direct children are Compute Scalars — like the node-6 Hash Match in Joe's private plan — that meant "children elapsed = 0" and the parent kept its full elapsed as self-time. Effect: node-6 Hash Spill reported 95% benefit / 8,829ms self-time. With look-through through the Compute Scalars it's 1,609ms self-time / 17.4% — matching Joe's math of 8.829 - 7.121 - 0.099. New GetEffectiveChildElapsedMs helper walks through pass-through operators (Compute Scalar and anything else missing runtime stats) to the first descendant with real stats. Parallelism exchange children keep existing max-child behavior because exchange times are unreliable. Applies to every operator-time-based benefit score (spills, key lookups, bare scans, scan-with-predicate, scan cardinality, eager index spool, etc). C2 (decimal precision on benefit %): Benefit % was rounded to N0 everywhere, so tiny waits showed as "up to 0%". Switched to one decimal except for 100 (shown as "100"), matching the existing pattern in ComparisonFormatter. Applied to web strip, wait-stats card, HTML export (both surfaces), text formatter (both paths), Avalonia Plan Warnings + Node Warnings headers. Version bump 1.7.1 -> 1.7.2. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b945eba commit 3a40ada

6 files changed

Lines changed: 42 additions & 20 deletions

File tree

‎src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,9 @@ private static string FormatBytes(double bytes)
801801
return $"{bytes / (1024L * 1024 * 1024):N1} GB";
802802
}
803803

804+
private static string FormatBenefitPercent(double pct) =>
805+
pct >= 100 ? $"{pct:N0}" : $"{pct:N1}";
806+
804807
#endregion
805808

806809
#region Node Selection & Properties Panel
@@ -1737,7 +1740,7 @@ private void ShowPropertiesPanel(PlanNode node)
17371740
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
17381741
var warnPanel = new StackPanel { Margin = new Thickness(10, 2, 10, 2) };
17391742
var planWarnHeader = w.MaxBenefitPercent.HasValue
1740-
? $"\u26A0 {w.WarningType} \u2014 up to {w.MaxBenefitPercent:N0}% benefit"
1743+
? $"\u26A0 {w.WarningType} \u2014 up to {FormatBenefitPercent(w.MaxBenefitPercent.Value)}% benefit"
17411744
: $"\u26A0 {w.WarningType}";
17421745
warnPanel.Children.Add(new TextBlock
17431746
{
@@ -1819,7 +1822,7 @@ private void ShowPropertiesPanel(PlanNode node)
18191822
: w.Severity == PlanWarningSeverity.Warning ? "#FFB347" : "#6BB5FF";
18201823
var warnPanel = new StackPanel { Margin = new Thickness(10, 2, 10, 2) };
18211824
var nodeWarnHeader = w.MaxBenefitPercent.HasValue
1822-
? $"\u26A0 {w.WarningType} \u2014 up to {w.MaxBenefitPercent:N0}% benefit"
1825+
? $"\u26A0 {w.WarningType} \u2014 up to {FormatBenefitPercent(w.MaxBenefitPercent.Value)}% benefit"
18231826
: $"\u26A0 {w.WarningType}";
18241827
warnPanel.Children.Add(new TextBlock
18251828
{

‎src/PlanViewer.App/PlanViewer.App.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<ApplicationManifest>app.manifest</ApplicationManifest>
77
<ApplicationIcon>EDD.ico</ApplicationIcon>
88
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
9-
<Version>1.7.1</Version>
9+
<Version>1.7.2</Version>
1010
<Authors>Erik Darling</Authors>
1111
<Company>Darling Data LLC</Company>
1212
<Product>Performance Studio</Product>

‎src/PlanViewer.Core/Output/HtmlExporter.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ private static void WriteWaitStatsCard(StringBuilder sb, StatementResult stmt, b
407407
{
408408
var barPct = maxWait > 0 ? (double)w.WaitTimeMs / maxWait * 100 : 0;
409409
var benefitTag = benefitLookup.TryGetValue(w.WaitType, out var pct)
410-
? $" <span class=\"warn-benefit\">up to {pct:N0}%</span>"
410+
? $" <span class=\"warn-benefit\">up to {(pct >= 100 ? pct.ToString("N0") : pct.ToString("N1"))}%</span>"
411411
: "";
412412
sb.AppendLine("<div class=\"wait-row\">");
413413
sb.AppendLine($"<span class=\"wait-type\">{Encode(w.WaitType)}</span>");
@@ -458,7 +458,7 @@ private static void WriteWarnings(StringBuilder sb, StatementResult stmt)
458458
sb.AppendLine($"<span class=\"warn-op\">{Encode(w.Operator)}</span>");
459459
sb.AppendLine($"<span class=\"warn-type\">{Encode(w.Type)}</span>");
460460
if (w.MaxBenefitPercent.HasValue)
461-
sb.AppendLine($"<span class=\"warn-benefit\">up to {w.MaxBenefitPercent:N0}% benefit</span>");
461+
sb.AppendLine($"<span class=\"warn-benefit\">up to {(w.MaxBenefitPercent.Value >= 100 ? w.MaxBenefitPercent.Value.ToString("N0") : w.MaxBenefitPercent.Value.ToString("N1"))}% benefit</span>");
462462
sb.AppendLine($"<span class=\"warn-msg\">{Encode(w.Message)}</span>");
463463
if (!string.IsNullOrEmpty(w.ActionableFix))
464464
sb.AppendLine($"<span class=\"warn-fix\">{Encode(w.ActionableFix)}</span>");

‎src/PlanViewer.Core/Output/TextFormatter.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ public static void WriteText(AnalysisResult result, TextWriter writer)
139139
foreach (var w in stmt.WaitStats.OrderByDescending(w => w.WaitTimeMs))
140140
{
141141
var benefitTag = benefitLookup.TryGetValue(w.WaitType, out var pct)
142-
? $" (up to {pct:N0}% benefit)"
142+
? $" (up to {(pct >= 100 ? pct.ToString("N0") : pct.ToString("N1"))}% benefit)"
143143
: "";
144144
writer.WriteLine($" {w.WaitType}: {w.WaitTimeMs:N0}ms{benefitTag}");
145145
}
@@ -169,7 +169,7 @@ public static void WriteText(AnalysisResult result, TextWriter writer)
169169
foreach (var w in sortedWarnings)
170170
{
171171
var benefitTag = w.MaxBenefitPercent.HasValue
172-
? $" (up to {w.MaxBenefitPercent:N0}% benefit)"
172+
? $" (up to {(w.MaxBenefitPercent.Value >= 100 ? w.MaxBenefitPercent.Value.ToString("N0") : w.MaxBenefitPercent.Value.ToString("N1"))}% benefit)"
173173
: "";
174174
writer.WriteLine($" [{w.Severity}] {w.Type}{benefitTag}: {EscapeNewlines(w.Message)}");
175175
if (!string.IsNullOrEmpty(w.ActionableFix))

‎src/PlanViewer.Core/Services/PlanAnalyzer.cs‎

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,26 +1593,45 @@ private static long GetPerThreadOwnElapsed(PlanNode node)
15931593
}
15941594

15951595
/// <summary>
1596-
/// Serial row mode self-time: subtract all direct children's elapsed.
1597-
/// Exchange children are skipped through to their real child.
1596+
/// Serial row mode self-time: subtract all direct children's effective elapsed.
1597+
/// Pass-through operators (Compute Scalar, etc.) don't carry runtime stats —
1598+
/// look through them to the first descendant that does. Exchange children
1599+
/// use max-child elapsed because exchange times are unreliable.
15981600
/// </summary>
15991601
private static long GetSerialOwnElapsed(PlanNode node)
16001602
{
16011603
var totalChildElapsed = 0L;
16021604
foreach (var child in node.Children)
1603-
{
1604-
var childElapsed = child.ActualElapsedMs;
1605-
1606-
// Exchange operators have unreliable times — skip to their child
1607-
if (child.PhysicalOp == "Parallelism" && child.Children.Count > 0)
1608-
childElapsed = child.Children.Max(c => c.ActualElapsedMs);
1609-
1610-
totalChildElapsed += childElapsed;
1611-
}
1605+
totalChildElapsed += GetEffectiveChildElapsedMs(child);
16121606

16131607
return Math.Max(0, node.ActualElapsedMs - totalChildElapsed);
16141608
}
16151609

1610+
/// <summary>
1611+
/// Returns the elapsed time a child contributes to its parent's subtree.
1612+
/// Looks through pass-through operators (Compute Scalar, Parallelism exchange)
1613+
/// that don't carry reliable runtime stats.
1614+
/// </summary>
1615+
private static long GetEffectiveChildElapsedMs(PlanNode child)
1616+
{
1617+
// Exchange operators: unreliable times, use max child
1618+
if (child.PhysicalOp == "Parallelism" && child.Children.Count > 0)
1619+
return child.Children.Max(GetEffectiveChildElapsedMs);
1620+
1621+
// Child has its own stats: use them
1622+
if (child.ActualElapsedMs > 0)
1623+
return child.ActualElapsedMs;
1624+
1625+
// No stats (Compute Scalar and similar): look through to descendants
1626+
if (child.Children.Count == 0)
1627+
return 0;
1628+
1629+
var sum = 0L;
1630+
foreach (var grandchild in child.Children)
1631+
sum += GetEffectiveChildElapsedMs(grandchild);
1632+
return sum;
1633+
}
1634+
16161635
/// <summary>
16171636
/// Calculates a Parallelism (exchange) operator's own elapsed time.
16181637
/// Exchange times are unreliable — they accumulate wait time caused by

‎src/PlanViewer.Web/Pages/Index.razor‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ else
302302
@w.WaitTimeMs.ToString("N0") ms
303303
@if (benefitPct > 0)
304304
{
305-
<span class="wait-benefit">up to @benefitPct.ToString("N0")%</span>
305+
<span class="wait-benefit">up to @(benefitPct >= 100 ? benefitPct.ToString("N0") : benefitPct.ToString("N1"))%</span>
306306
}
307307
</span>
308308
</div>
@@ -346,7 +346,7 @@ else
346346
<span class="warning-type">@w.Type</span>
347347
@if (w.MaxBenefitPercent.HasValue)
348348
{
349-
<span class="warn-benefit">up to @w.MaxBenefitPercent.Value.ToString("N0")% benefit</span>
349+
<span class="warn-benefit">up to @(w.MaxBenefitPercent.Value >= 100 ? w.MaxBenefitPercent.Value.ToString("N0") : w.MaxBenefitPercent.Value.ToString("N1"))% benefit</span>
350350
}
351351
<span class="warning-msg">@w.Message</span>
352352
@if (!string.IsNullOrEmpty(w.ActionableFix))

0 commit comments

Comments
 (0)