Skip to content

Commit 28d4c74

Browse files
Replace parallelism efficiency warnings with CPU:Elapsed ratio
Removes Rules 25 (Ineffective Parallelism) and 31 (Parallel Wait Bottleneck). Per Joe's feedback: these meta-warnings guess at causes when wait stats already tell the story. Instead, the runtime summary now shows a CPU:Elapsed ratio — users can interpret it directly with DOP context, and the wait stats list speaks for itself. - PlanAnalyzer: remove Rules 25/31 block, remove GetWaitStatsAdvice and DescribeWaitType helpers (only used by those rules) - BenefitScorer: remove Rule 25/31 scoring cases - Index.razor: add CPU:Elapsed row to Runtime insight card - HtmlExporter: add CPU:Elapsed row to runtime card - Tests: remove Rule 25/31 test cases Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c355262 commit 28d4c74

5 files changed

Lines changed: 18 additions & 198 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,11 @@ private static void WriteRuntimeCard(StringBuilder sb, StatementResult stmt)
300300
{
301301
WriteRow(sb, "Elapsed", $"{stmt.QueryTime.ElapsedTimeMs:N0} ms");
302302
WriteRow(sb, "CPU", $"{stmt.QueryTime.CpuTimeMs:N0} ms");
303+
if (stmt.QueryTime.ElapsedTimeMs > 0)
304+
{
305+
var ratio = (double)stmt.QueryTime.CpuTimeMs / stmt.QueryTime.ElapsedTimeMs;
306+
WriteRow(sb, "CPU:Elapsed", ratio.ToString("N2"));
307+
}
303308
}
304309
if (stmt.DegreeOfParallelism > 0)
305310
WriteRow(sb, "DOP", stmt.DegreeOfParallelism.ToString());

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

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,6 @@ private static void ScoreStatementWarnings(PlanStatement stmt)
4949
{
5050
switch (warning.WarningType)
5151
{
52-
case "Ineffective Parallelism": // Rule 25
53-
case "Parallel Wait Bottleneck": // Rule 31
54-
// These are meta-findings about parallelism efficiency.
55-
// The benefit is the gap between actual and ideal elapsed time.
56-
if (elapsedMs > 0 && stmt.QueryTimeStats != null)
57-
{
58-
var cpu = stmt.QueryTimeStats.CpuTimeMs;
59-
var dop = stmt.DegreeOfParallelism;
60-
if (dop > 1 && cpu > 0)
61-
{
62-
// Ideal elapsed = CPU / DOP. Benefit = (actual - ideal) / actual
63-
var idealElapsed = (double)cpu / dop;
64-
var benefit = Math.Max(0, (elapsedMs - idealElapsed) / elapsedMs * 100);
65-
warning.MaxBenefitPercent = Math.Min(100, Math.Round(benefit, 1));
66-
}
67-
}
68-
break;
69-
7052
case "Serial Plan": // Rule 3
7153
// Can't know how fast a parallel plan would be, but estimate:
7254
// CPU-bound: benefit up to (1 - 1/maxDOP) * 100%

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

Lines changed: 3 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -378,53 +378,9 @@ private static void AnalyzeStatement(PlanStatement stmt, AnalyzerConfig cfg)
378378
});
379379
}
380380

381-
// Rule 25: Ineffective parallelism — DOP-aware efficiency scoring
382-
// Efficiency = (speedup - 1) / (DOP - 1) * 100
383-
// where speedup = CPU / Elapsed. At DOP 1 speedup=1 (0%), at DOP=speedup (100%).
384-
// Rule 31: Parallel wait bottleneck — elapsed >> CPU means threads waiting, not working.
385-
if (!cfg.IsRuleDisabled(25) && stmt.DegreeOfParallelism > 1 && stmt.QueryTimeStats != null)
386-
{
387-
var cpu = stmt.QueryTimeStats.CpuTimeMs;
388-
var elapsed = stmt.QueryTimeStats.ElapsedTimeMs;
389-
var dop = stmt.DegreeOfParallelism;
390-
391-
if (elapsed >= 1000 && cpu > 0)
392-
{
393-
var speedup = (double)cpu / elapsed;
394-
var efficiency = Math.Max(0.0, Math.Min(100.0, (speedup - 1.0) / (dop - 1.0) * 100.0));
395-
396-
// Build targeted advice from wait stats if available
397-
var waitAdvice = GetWaitStatsAdvice(stmt.WaitStats);
398-
399-
if (speedup < 0.5 && !cfg.IsRuleDisabled(31))
400-
{
401-
// CPU well below Elapsed: threads are waiting, not doing CPU work
402-
var waitPct = (1.0 - speedup) * 100;
403-
var advice = waitAdvice ?? "Common causes include spills to tempdb, physical I/O reads, lock or latch contention, and memory grant waits.";
404-
stmt.PlanWarnings.Add(new PlanWarning
405-
{
406-
WarningType = "Parallel Wait Bottleneck",
407-
Message = $"Parallel plan (DOP {dop}, {efficiency:N0}% efficient) with elapsed time ({elapsed:N0}ms) exceeding CPU time ({cpu:N0}ms). " +
408-
$"Approximately {waitPct:N0}% of elapsed time was spent waiting rather than on CPU. " +
409-
advice,
410-
Severity = PlanWarningSeverity.Warning
411-
});
412-
}
413-
else if (efficiency < 40)
414-
{
415-
// CPU >= Elapsed but well below DOP potential — parallelism is ineffective
416-
var advice = waitAdvice ?? "Look for parallel thread skew, blocking exchanges, or serial zones in the plan that prevent effective parallel execution.";
417-
stmt.PlanWarnings.Add(new PlanWarning
418-
{
419-
WarningType = "Ineffective Parallelism",
420-
Message = $"Parallel plan (DOP {dop}) is only {efficiency:N0}% efficient — CPU time ({cpu:N0}ms) vs elapsed time ({elapsed:N0}ms). " +
421-
$"At DOP {dop}, ideal CPU time would be ~{elapsed * dop:N0}ms. " +
422-
advice,
423-
Severity = efficiency < 20 ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning
424-
});
425-
}
426-
}
427-
}
381+
// Rules 25 (Ineffective Parallelism) and 31 (Parallel Wait Bottleneck) were removed.
382+
// The CPU:Elapsed ratio is now shown in the runtime summary, and wait stats speak
383+
// for themselves — no need for meta-warnings guessing at causes.
428384

429385
// Rule 30: Missing index quality evaluation
430386
if (!cfg.IsRuleDisabled(30))
@@ -1770,94 +1726,6 @@ _ when wt.StartsWith("LCK_") => "lock contention",
17701726
};
17711727
}
17721728

1773-
private static string? GetWaitStatsAdvice(List<WaitStatInfo> waits)
1774-
{
1775-
if (waits.Count == 0)
1776-
return null;
1777-
1778-
var totalMs = waits.Sum(w => w.WaitTimeMs);
1779-
if (totalMs == 0)
1780-
return null;
1781-
1782-
var top = waits.OrderByDescending(w => w.WaitTimeMs).First();
1783-
var topPct = (double)top.WaitTimeMs / totalMs * 100;
1784-
1785-
// Single dominant wait — give targeted advice
1786-
if (topPct >= 80)
1787-
return DescribeWaitType(top.WaitType, topPct);
1788-
1789-
// Multiple waits — summarize the top contributors instead of guessing
1790-
var topWaits = waits.OrderByDescending(w => w.WaitTimeMs).Take(3)
1791-
.Select(w => $"{w.WaitType} ({(double)w.WaitTimeMs / totalMs * 100:N0}%)")
1792-
.ToList();
1793-
return $"Top waits: {string.Join(", ", topWaits)}.";
1794-
}
1795-
1796-
/// <summary>
1797-
/// Maps a wait type to a human-readable description with percentage context.
1798-
/// Covers all wait types observed in real execution plan files.
1799-
/// </summary>
1800-
private static string DescribeWaitType(string rawWaitType, double topPct)
1801-
{
1802-
var waitType = rawWaitType.ToUpperInvariant();
1803-
return waitType switch
1804-
{
1805-
// I/O: reading/writing data pages from disk
1806-
_ when waitType.StartsWith("PAGEIOLATCH") =>
1807-
$"I/O bound — {topPct:N0}% of wait time is {rawWaitType}. Data is being read from disk rather than memory. Consider adding indexes to reduce I/O, or investigate memory pressure.",
1808-
_ when waitType.Contains("IO_COMPLETION") =>
1809-
$"I/O bound — {topPct:N0}% of wait time is {rawWaitType}. Non-buffer I/O such as sort/hash spills to TempDB or eager writes.",
1810-
1811-
// CPU: thread yielding its scheduler quantum
1812-
_ when waitType == "SOS_SCHEDULER_YIELD" =>
1813-
$"CPU bound — {topPct:N0}% of wait time is {rawWaitType}. The query is consuming significant CPU. Look for expensive operators (scans, sorts, hash builds) that could be eliminated or reduced.",
1814-
1815-
// Parallelism: exchange and synchronization waits
1816-
_ when waitType.StartsWith("CXPACKET") || waitType.StartsWith("CXCONSUMER") =>
1817-
$"Parallel thread skew — {topPct:N0}% of wait time is {rawWaitType}. Work is unevenly distributed across parallel threads.",
1818-
_ when waitType.StartsWith("CXSYNC") =>
1819-
$"Parallel synchronization — {topPct:N0}% of wait time is {rawWaitType}. Threads are waiting at exchange operators to synchronize parallel execution.",
1820-
1821-
// Hash operations
1822-
_ when waitType.StartsWith("HT") =>
1823-
$"Hash operation — {topPct:N0}% of wait time is {rawWaitType}. Time spent building, repartitioning, or cleaning up hash tables. Large hash builds may indicate missing indexes or bad row estimates.",
1824-
1825-
// Sort/bitmap batch operations
1826-
_ when waitType == "BPSORT" =>
1827-
$"Batch sort — {topPct:N0}% of wait time is {rawWaitType}. Time spent in batch-mode sort operations.",
1828-
_ when waitType == "BMPBUILD" =>
1829-
$"Bitmap build — {topPct:N0}% of wait time is {rawWaitType}. Time spent building bitmap filters for hash joins.",
1830-
1831-
// Memory allocation
1832-
_ when waitType.Contains("MEMORY_ALLOCATION_EXT") =>
1833-
$"Memory allocation — {topPct:N0}% of wait time is {rawWaitType}. Frequent memory allocations during query execution.",
1834-
1835-
// Latch contention (non-I/O)
1836-
_ when waitType.StartsWith("PAGELATCH") =>
1837-
$"Page latch contention — {topPct:N0}% of wait time is {rawWaitType}. In-memory page contention, often on TempDB or hot pages.",
1838-
_ when waitType.StartsWith("LATCH_") =>
1839-
$"Latch contention — {topPct:N0}% of wait time is {rawWaitType}.",
1840-
1841-
// Lock contention
1842-
_ when waitType.StartsWith("LCK_") =>
1843-
$"Lock contention — {topPct:N0}% of wait time is {rawWaitType}. Other sessions are holding locks that this query needs.",
1844-
1845-
// Log writes
1846-
_ when waitType == "LOGBUFFER" =>
1847-
$"Log write — {topPct:N0}% of wait time is {rawWaitType}. Waiting for transaction log buffer flushes, typically from data modifications.",
1848-
1849-
// Network
1850-
_ when waitType == "ASYNC_NETWORK_IO" =>
1851-
$"Network bound — {topPct:N0}% of wait time is {rawWaitType}. The client application is not consuming results fast enough.",
1852-
1853-
// Physical page cache
1854-
_ when waitType == "SOS_PHYS_PAGE_CACHE" =>
1855-
$"Physical page cache — {topPct:N0}% of wait time is {rawWaitType}. Contention on the physical memory page allocator.",
1856-
1857-
_ => $"Dominant wait is {rawWaitType} ({topPct:N0}% of wait time)."
1858-
};
1859-
}
1860-
18611729
/// <summary>
18621730
/// Returns true if the statement has significant I/O waits (PAGEIOLATCH_*, IO_COMPLETION).
18631731
/// Used for severity elevation decisions where I/O specifically indicates disk access.

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ else
160160
<span class="insight-label">CPU</span>
161161
<span class="insight-value">@ActiveStmt!.QueryTime.CpuTimeMs.ToString("N0") ms</span>
162162
</div>
163+
@if (ActiveStmt!.QueryTime.ElapsedTimeMs > 0)
164+
{
165+
var ratio = (double)ActiveStmt!.QueryTime.CpuTimeMs / ActiveStmt!.QueryTime.ElapsedTimeMs;
166+
<div class="insight-row">
167+
<span class="insight-label">CPU:Elapsed</span>
168+
<span class="insight-value">@ratio.ToString("N2")</span>
169+
</div>
170+
}
163171
}
164172
@if (ActiveStmt!.DegreeOfParallelism > 0)
165173
{

‎tests/PlanViewer.Core.Tests/PlanAnalyzerTests.cs‎

Lines changed: 2 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -573,32 +573,8 @@ public void Rule29_ImplicitConvertSeekPlan_UpgradedToCritical()
573573
Assert.Contains(warnings, w => w.Message.Contains("prevented an index seek"));
574574
}
575575

576-
// ---------------------------------------------------------------
577-
// Rule 25: Ineffective Parallelism
578-
// ---------------------------------------------------------------
579-
580-
[Fact]
581-
public void Rule25_IneffectiveParallelism_DetectedWhenCpuEqualsElapsed()
582-
{
583-
// serially-parallel: DOP 8 but CPU 17,110ms ≈ elapsed 17,112ms (efficiency ~0%)
584-
var plan = PlanTestHelper.LoadAndAnalyze("serially-parallel.sqlplan");
585-
var warnings = PlanTestHelper.WarningsOfType(plan, "Ineffective Parallelism");
586-
587-
Assert.Single(warnings);
588-
Assert.Contains("DOP 8", warnings[0].Message);
589-
Assert.Contains("% efficient", warnings[0].Message);
590-
}
591-
592-
[Fact]
593-
public void Rule25_IneffectiveParallelism_NotFiredOnEffectiveParallelPlan()
594-
{
595-
// parallel-skew: DOP 4, CPU 28,634ms vs elapsed 9,417ms (ratio ~3.0)
596-
// This is effective parallelism — Rule 25 should NOT fire
597-
var plan = PlanTestHelper.LoadAndAnalyze("parallel-skew.sqlplan");
598-
var warnings = PlanTestHelper.WarningsOfType(plan, "Ineffective Parallelism");
599-
600-
Assert.Empty(warnings);
601-
}
576+
// Rules 25 and 31 were removed — CPU:Elapsed ratio is shown in the runtime
577+
// summary instead, and wait stats speak for themselves.
602578

603579
// ---------------------------------------------------------------
604580
// Rule 28: NOT IN with Nullable Column (Row Count Spool)
@@ -642,25 +618,6 @@ public void Rule30_MissingIndexQuality_DetectsWideOrLowImpact()
642618
}
643619
}
644620

645-
// ---------------------------------------------------------------
646-
// Rule 31: Parallel Wait Bottleneck
647-
// ---------------------------------------------------------------
648-
649-
[Fact]
650-
public void Rule31_ParallelWaitBottleneck_DetectedWhenElapsedExceedsCpu()
651-
{
652-
// excellent-parallel-spill: DOP 4, CPU 172,222ms vs elapsed 225,870ms
653-
// speedup ~0.76 — CPU < Elapsed but >= 0.5, so fires as Ineffective Parallelism
654-
// (wait bottleneck only fires when speedup < 0.5 — extreme waiting)
655-
var plan = PlanTestHelper.LoadAndAnalyze("excellent-parallel-spill.sqlplan");
656-
657-
// At DOP 4 with speedup 0.76, efficiency ≈ 0% — fires Ineffective Parallelism
658-
var warnings = PlanTestHelper.WarningsOfType(plan, "Ineffective Parallelism");
659-
Assert.NotEmpty(warnings);
660-
Assert.Contains("DOP 4", warnings[0].Message);
661-
Assert.Contains("% efficient", warnings[0].Message);
662-
}
663-
664621
// ---------------------------------------------------------------
665622
// Seek Predicate Parsing
666623
// ---------------------------------------------------------------

0 commit comments

Comments
 (0)