Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion src/PlanViewer.App/PlanViewer.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<ApplicationManifest>app.manifest</ApplicationManifest>
<ApplicationIcon>EDD.ico</ApplicationIcon>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>1.6.0</Version>
<Version>1.7.0</Version>
<Authors>Erik Darling</Authors>
<Company>Darling Data LLC</Company>
<Product>Performance Studio</Product>
Expand Down
63 changes: 63 additions & 0 deletions src/PlanViewer.Core/Services/BenefitScorer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,73 @@ public static void Score(ParsedPlan plan)

if (stmt.WaitStats.Count > 0 && stmt.QueryTimeStats != null)
ScoreWaitStats(stmt);

if (stmt.WaitStats.Count > 0)
EmitWaitStatWarnings(stmt);
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guard asymmetry worth confirming: ScoreWaitStats on line 40 only runs when stmt.QueryTimeStats != null, but EmitWaitStatWarnings on line 43 runs whenever WaitStats.Count > 0.

When QueryTimeStats is null (e.g. estimated-only plans with recorded wait stats), stmt.WaitBenefits stays empty, so benefitByType.TryGetValue always misses and every wait is emitted at Info severity with MaxBenefitPercent = null. The plan may have a real 99%-elapsed CXPACKET problem that silently shows up as Info.

Intentional degrade? If so, fine — but it's worth a comment at line 42 saying so. Otherwise consider matching the QueryTimeStats != null guard.


Generated by Claude Code

}
}
}

/// <summary>
/// Emits a PlanWarning per wait stat entry, merging the per-wait benefit %
/// from ScoreWaitStats with the descriptive content from WaitStatsKnowledge.
/// The existing wait-stats chart/card stays as a complementary view.
/// </summary>
private static void EmitWaitStatWarnings(PlanStatement stmt)
{
// Lookup benefit % by wait type (populated by ScoreWaitStats)
var benefitByType = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
foreach (var wb in stmt.WaitBenefits)
benefitByType[wb.WaitType] = wb.MaxBenefitPercent;

foreach (var wait in stmt.WaitStats)
{
if (wait.WaitTimeMs <= 0) continue;

var entry = WaitStatsKnowledge.Lookup(wait.WaitType);
double? benefitPct = benefitByType.TryGetValue(wait.WaitType, out var b) ? b : null;

var msg = new System.Text.StringBuilder();
msg.Append(wait.WaitType).Append(": ").Append(entry.Description);
msg.Append(" Observed ").Append(wait.WaitTimeMs.ToString("N0")).Append(" ms");
if (wait.WaitCount > 0)
msg.Append(" across ").Append(wait.WaitCount.ToString("N0")).Append(" wait").Append(wait.WaitCount == 1 ? "" : "s");
msg.Append('.');

if (entry.ShowEffectiveLatency && wait.WaitCount > 0)
{
var effLatency = (double)wait.WaitTimeMs / wait.WaitCount;
msg.Append(" Effective latency: ")
.Append(FormatLatency(effLatency))
.Append(" per wait.");
}

var severity = benefitPct switch
{
>= 50 => PlanWarningSeverity.Critical,
>= 10 => PlanWarningSeverity.Warning,
_ => PlanWarningSeverity.Info,
};

stmt.PlanWarnings.Add(new PlanWarning
{
WarningType = "Wait: " + wait.WaitType,
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WarningType = "Wait: " + wait.WaitType creates one unique WarningType per wait type (e.g. "Wait: CXPACKET", "Wait: PAGEIOLATCH_SH"). Any downstream code keying on WarningType — filters, grouping in the web viewer / HTML export / Avalonia Plan Warnings expander, rule-count metrics — now sees an unbounded set of warning types instead of a single "Wait Statistics" bucket. Worth a quick scan of the ResultMapper / exporters / Avalonia warnings view to confirm none of them switch/dictionary on WarningType.


Generated by Claude Code

Message = msg.ToString(),
Severity = severity,
MaxBenefitPercent = benefitPct,
ActionableFix = entry.HowToFix
});
}
}

private static string FormatLatency(double ms)
{
if (ms >= 1000) return $"{ms / 1000:N2} s";
if (ms >= 10) return $"{ms:N0} ms";
if (ms >= 1) return $"{ms:N1} ms";
return $"{ms * 1000:N0} µs";
}

private static void ScoreStatementWarnings(PlanStatement stmt)
{
var elapsedMs = stmt.QueryTimeStats?.ElapsedTimeMs ?? 0;
Expand Down
Loading
Loading