Skip to content

Commit e763623

Browse files
Merge pull request #217 from erikdarlingdata/fix/wait-stats-cleanup
Strip operator-level wait noise, add query classification
2 parents 3439ce6 + c65d86c commit e763623

3 files changed

Lines changed: 39 additions & 335 deletions

File tree

src/PlanViewer.App/Services/AdviceContentBuilder.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,6 +1126,41 @@ private static SolidColorBrush GetWaitCategoryBrush(string waitType)
11261126
items.Add(($"Memory grant: {grantedMB:F1} MB ({usedPct:F0}% used)", memBrush));
11271127
}
11281128

1129+
// Wait profile classification
1130+
if (stmt.WaitStats.Count > 0)
1131+
{
1132+
var totalMs = stmt.WaitStats.Sum(w => w.WaitTimeMs);
1133+
if (totalMs > 0)
1134+
{
1135+
long ioMs = 0, cpuMs = 0, parallelMs = 0, lockMs = 0;
1136+
foreach (var w in stmt.WaitStats)
1137+
{
1138+
var wt = w.WaitType.ToUpperInvariant();
1139+
if (wt.StartsWith("PAGEIOLATCH") || wt.Contains("IO_COMPLETION"))
1140+
ioMs += w.WaitTimeMs;
1141+
else if (wt == "SOS_SCHEDULER_YIELD")
1142+
cpuMs += w.WaitTimeMs;
1143+
else if (wt.StartsWith("CX"))
1144+
parallelMs += w.WaitTimeMs;
1145+
else if (wt.StartsWith("LCK_"))
1146+
lockMs += w.WaitTimeMs;
1147+
}
1148+
1149+
// Pick the dominant category (>= 30% of total)
1150+
var categories = new List<(string label, long ms)>();
1151+
if (ioMs * 100 / totalMs >= 30) categories.Add(("I/O", ioMs));
1152+
if (cpuMs * 100 / totalMs >= 30) categories.Add(("CPU", cpuMs));
1153+
if (parallelMs * 100 / totalMs >= 30) categories.Add(("parallelism", parallelMs));
1154+
if (lockMs * 100 / totalMs >= 30) categories.Add(("lock contention", lockMs));
1155+
1156+
if (categories.Count > 0)
1157+
{
1158+
var label = string.Join(" + ", categories.Select(c => c.label));
1159+
items.Add(($"{label} bound ({totalMs:N0}ms total wait time)", InfoBrush));
1160+
}
1161+
}
1162+
}
1163+
11291164
// Warning counts by severity
11301165
var criticalCount = stmt.Warnings.Count(w =>
11311166
w.Severity.Equals("Critical", StringComparison.OrdinalIgnoreCase));

src/PlanViewer.Core/Services/PlanAnalyzer.cs

Lines changed: 2 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -548,9 +548,9 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt, AnalyzerConfi
548548
{
549549
// Gate: skip trivial filters based on actual stats or estimated cost
550550
bool isTrivial;
551-
long childReads = 0;
552551
if (node.HasActualStats)
553552
{
553+
long childReads = 0;
554554
foreach (var child in node.Children)
555555
childReads += SumSubtreeReads(child);
556556
var childElapsed = node.Children.Max(c => c.ActualElapsedMs);
@@ -571,14 +571,6 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt, AnalyzerConfi
571571
message += $"\n{impact}";
572572
message += $"\nPredicate: {predicate}";
573573

574-
// Wait stats add context — rows burned CPU/I/O/waits just to be discarded
575-
if (childReads >= 1000)
576-
{
577-
var waitContext = GetTopWaitContext(stmt.WaitStats);
578-
if (waitContext != null)
579-
message += $"\n{waitContext}";
580-
}
581-
582574
node.Warnings.Add(new PlanWarning
583575
{
584576
WarningType = "Filter Operator",
@@ -655,16 +647,10 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt, AnalyzerConfi
655647
var actualDisplay = executions > 1
656648
? $"Actual {node.ActualRows:N0} ({actualPerExec:N0} rows x {executions:N0} executions)"
657649
: $"Actual {node.ActualRows:N0}";
658-
var message = $"Estimated {node.EstimateRows:N0} vs {actualDisplay}{factor:F0}x {direction}. {harm}";
659-
660-
var waitContext = GetTopWaitContext(stmt.WaitStats);
661-
if (waitContext != null)
662-
message += $" {waitContext}";
663-
664650
node.Warnings.Add(new PlanWarning
665651
{
666652
WarningType = "Row Estimate Mismatch",
667-
Message = message,
653+
Message = $"Estimated {node.EstimateRows:N0} vs {actualDisplay}{factor:F0}x {direction}. {harm}",
668654
Severity = factor >= 100 ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning
669655
});
670656
}
@@ -848,10 +834,6 @@ _ when nonSargableReason.StartsWith("Function call") =>
848834
message += $" {details.Summary}";
849835
message += " Check that you have appropriate indexes.";
850836

851-
var waitContext = GetTopWaitContext(stmt.WaitStats);
852-
if (waitContext != null)
853-
message += $" {waitContext}";
854-
855837
// I/O waits specifically confirm the scan is hitting disk — elevate
856838
if (HasSignificantIoWaits(stmt.WaitStats) && details.CostPct >= 50
857839
&& severity != PlanWarningSeverity.Critical)
@@ -1047,10 +1029,6 @@ _ when nonSargableReason.StartsWith("Function call") =>
10471029
else
10481030
details.Add("Consider whether a hash or merge join would be more appropriate for this row count.");
10491031

1050-
var waitContext = GetTopWaitContext(stmt.WaitStats);
1051-
if (waitContext != null)
1052-
details.Add(waitContext);
1053-
10541032
node.Warnings.Add(new PlanWarning
10551033
{
10561034
WarningType = "Nested Loops High Executions",
@@ -1839,33 +1817,6 @@ private static bool HasSignificantIoWaits(List<WaitStatInfo> waits)
18391817
return ioMs >= 100 && pct >= 20;
18401818
}
18411819

1842-
/// <summary>
1843-
/// Returns a terse sentence describing the dominant wait type for appending
1844-
/// to an existing warning message, or null if waits are negligible.
1845-
/// Surfaces whatever wait type is dominant — PAGEIOLATCH, SOS_SCHEDULER_YIELD,
1846-
/// CXPACKET, LCK_*, HTBUILD, EXECSYNC, IO_COMPLETION, etc.
1847-
/// Threshold: top wait >= 100ms and >= 20% of total wait time.
1848-
/// </summary>
1849-
private static string? GetTopWaitContext(List<WaitStatInfo> waits)
1850-
{
1851-
if (waits.Count == 0)
1852-
return null;
1853-
1854-
var totalMs = waits.Sum(w => w.WaitTimeMs);
1855-
if (totalMs == 0)
1856-
return null;
1857-
1858-
var top = waits.OrderByDescending(w => w.WaitTimeMs).First();
1859-
if (top.WaitTimeMs < 100)
1860-
return null;
1861-
1862-
var pct = (double)top.WaitTimeMs / totalMs * 100;
1863-
if (pct < 20)
1864-
return null;
1865-
1866-
return $"Dominant wait: {top.WaitType} ({top.WaitTimeMs:N0}ms, {pct:N0}% of total wait time).";
1867-
}
1868-
18691820
private static bool AllocatesResources(PlanNode node)
18701821
{
18711822
// Operators that get memory grants or allocate structures based on row estimates.

0 commit comments

Comments
 (0)