Skip to content

Commit 7009393

Browse files
Merge pull request #264 from erikdarlingdata/dev
Release v1.7.6 — C1 memory color + C4 compile time + C8 expensive-op + C9 columnstore
2 parents 48870b0 + e05bc69 commit 7009393

5 files changed

Lines changed: 72 additions & 15 deletions

File tree

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,9 +2800,12 @@ void AddRow(string label, string value, string? color = null)
28002800
rowIndex++;
28012801
}
28022802

2803-
// Efficiency thresholds: white >= 80%, yellow >= 60%, orange >= 40%, red < 40%
2804-
static string EfficiencyColor(double pct) => pct >= 80 ? "#E4E6EB"
2805-
: pct >= 60 ? "#FFD700" : pct >= 40 ? "#FFB347" : "#E57373";
2803+
// Efficiency thresholds: white >= 40%, orange >= 20%, red < 20%.
2804+
// Loosened per Joe's feedback (#215 C1): for memory grants, moderate
2805+
// utilization (e.g. 60%) is fine — operators can spill near their max,
2806+
// so we shouldn't flag anything above a real over-grant threshold.
2807+
static string EfficiencyColor(double pct) => pct >= 40 ? "#E4E6EB"
2808+
: pct >= 20 ? "#FFB347" : "#E57373";
28062809

28072810
// Runtime stats (actual plans)
28082811
if (statement.QueryTimeStats != null)
@@ -2815,6 +2818,11 @@ static string EfficiencyColor(double pct) => pct >= 80 ? "#E4E6EB"
28152818
AddRow("UDF elapsed", $"{statement.QueryUdfElapsedTimeMs:N0}ms");
28162819
}
28172820

2821+
// Compile time — plan-level property (category B). Show regardless of
2822+
// threshold so it's always visible, not just when Rule 19 fires.
2823+
if (statement.CompileTimeMs > 0)
2824+
AddRow("Compile", $"{statement.CompileTimeMs:N0}ms");
2825+
28182826
// Memory grant — color by utilization percentage
28192827
if (statement.MemoryGrant != null)
28202828
{

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.4</Version>
9+
<Version>1.7.6</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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,16 @@ private static void WriteRuntimeCard(StringBuilder sb, StatementResult stmt)
308308
WriteRow(sb, "CPU:Elapsed", ratio.ToString("N2"));
309309
}
310310
}
311+
if (stmt.CompileTimeMs > 0)
312+
WriteRow(sb, "Compile", $"{stmt.CompileTimeMs:N0} ms");
311313
if (stmt.DegreeOfParallelism > 0)
312314
WriteRow(sb, "DOP", stmt.DegreeOfParallelism.ToString());
313315
if (stmt.NonParallelReason != null)
314316
WriteRow(sb, "Serial", Encode(stmt.NonParallelReason));
315317
if (stmt.MemoryGrant != null && stmt.MemoryGrant.GrantedKB > 0)
316318
{
317319
var pctUsed = (double)stmt.MemoryGrant.MaxUsedKB / stmt.MemoryGrant.GrantedKB * 100;
318-
var effClass = pctUsed >= 80 ? "eff-good" : pctUsed >= 40 ? "eff-warn" : "eff-bad";
320+
var effClass = pctUsed >= 40 ? "eff-good" : pctUsed >= 20 ? "eff-warn" : "eff-bad";
319321
WriteRow(sb, "Memory", FormatKB(stmt.MemoryGrant.GrantedKB) + " granted");
320322
sb.AppendLine($"<div class=\"row\"><span class=\"label\">Used</span><span class=\"value {effClass}\">{FormatKB(stmt.MemoryGrant.MaxUsedKB)} ({pctUsed:N0}%)</span></div>");
321323
}

src/PlanViewer.Core/Services/PlanAnalyzer.cs

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -921,21 +921,38 @@ _ when nonSargableReason.StartsWith("Function call") =>
921921
? GetOperatorOwnElapsedMs(node) > 0
922922
: node.CostPercent >= 20;
923923

924-
if (colCount <= 3 && isSignificant)
924+
if (isSignificant)
925925
{
926926
var scanKind = node.PhysicalOp == "Clustered Index Scan"
927927
? "Clustered index scan"
928928
: "Heap table scan";
929-
var indexAdvice = node.PhysicalOp == "Clustered Index Scan"
930-
? "Consider a nonclustered index on the output columns (as key or INCLUDE) so SQL Server can read a narrower structure."
931-
: "Consider a clustered or nonclustered index on the output columns so SQL Server can read a narrower structure.";
932929

933-
node.Warnings.Add(new PlanWarning
930+
if (colCount <= 3)
934931
{
935-
WarningType = "Bare Scan",
936-
Message = $"{scanKind} reads the full table with no predicate, outputting {colCount} column(s): {Truncate(node.OutputColumns, 200)}. {indexAdvice} For analytical workloads, a columnstore index may be a better fit.",
937-
Severity = PlanWarningSeverity.Warning
938-
});
932+
// Narrow output: a nonclustered rowstore index can cover this cheaply.
933+
var indexAdvice = node.PhysicalOp == "Clustered Index Scan"
934+
? "Consider a nonclustered index on the output columns (as key or INCLUDE) so SQL Server can read a narrower structure."
935+
: "Consider a clustered or nonclustered index on the output columns so SQL Server can read a narrower structure.";
936+
937+
node.Warnings.Add(new PlanWarning
938+
{
939+
WarningType = "Bare Scan",
940+
Message = $"{scanKind} reads the full table with no predicate, outputting {colCount} column(s): {Truncate(node.OutputColumns, 200)}. {indexAdvice} For analytical workloads, a columnstore index may be a better fit.",
941+
Severity = PlanWarningSeverity.Warning
942+
});
943+
}
944+
else
945+
{
946+
// Wider output: rowstore NC index isn't a great fit (would have to
947+
// carry too many columns), but columnstore doesn't care about column
948+
// count. Suggest it for analytical / aggregate-style workloads.
949+
node.Warnings.Add(new PlanWarning
950+
{
951+
WarningType = "Bare Scan",
952+
Message = $"{scanKind} reads the full table with no predicate, outputting {colCount} columns. A nonclustered rowstore index isn't a great fit for wide outputs, but if this is an analytical or aggregate-style query, a columnstore index (CCI or NCCI) can scan the same data far more cheaply — column count doesn't penalize columnstore the way it does rowstore indexes.",
953+
Severity = PlanWarningSeverity.Warning
954+
});
955+
}
939956
}
940957
}
941958

@@ -1229,6 +1246,29 @@ _ when nonSargableReason.StartsWith("Function call") =>
12291246
w.Message = $"Implicit conversion prevented an index seek, forcing a scan instead. Fix the data type mismatch: ensure the parameter or variable type matches the column type exactly. {w.Message}";
12301247
}
12311248
}
1249+
1250+
// Rule 35: Expensive Operator — always show operators that take a significant
1251+
// share of statement time even when no other rule has something to say. Joe
1252+
// (#215 C8) wanted expensive scans that the tool had nothing to suggest on
1253+
// to still surface as top items. Threshold: self-time >= 20% of statement
1254+
// elapsed. Only emits if no other warning is already on the node to avoid
1255+
// doubling up. The benefit % is just the self-time share.
1256+
if (!cfg.IsRuleDisabled(35) && node.HasActualStats && node.Warnings.Count == 0
1257+
&& stmt.QueryTimeStats != null && stmt.QueryTimeStats.ElapsedTimeMs > 0)
1258+
{
1259+
var selfMs = GetOperatorOwnElapsedMs(node);
1260+
var pct = (double)selfMs / stmt.QueryTimeStats.ElapsedTimeMs * 100;
1261+
if (pct >= 20.0)
1262+
{
1263+
node.Warnings.Add(new PlanWarning
1264+
{
1265+
WarningType = "Expensive Operator",
1266+
Message = $"{node.PhysicalOp} took {selfMs:N0}ms ({pct:N1}% of statement elapsed) but no specific rule identified a fix. Worth investigating: is the row volume necessary? Are upstream estimates driving this operator harder than it should be?",
1267+
Severity = pct >= 50 ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning,
1268+
MaxBenefitPercent = Math.Round(Math.Min(100.0, pct), 1)
1269+
});
1270+
}
1271+
}
12321272
}
12331273

12341274
/// <summary>

src/PlanViewer.Web/Pages/Index.razor

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,13 @@ else
171171
</div>
172172
}
173173
}
174+
@if (ActiveStmt!.CompileTimeMs > 0)
175+
{
176+
<div class="insight-row">
177+
<span class="insight-label">Compile</span>
178+
<span class="insight-value">@ActiveStmt!.CompileTimeMs.ToString("N0") ms</span>
179+
</div>
180+
}
174181
@if (ActiveStmt!.DegreeOfParallelism > 0)
175182
{
176183
<div class="insight-row">
@@ -188,7 +195,7 @@ else
188195
@if (ActiveStmt!.MemoryGrant != null && ActiveStmt!.MemoryGrant.GrantedKB > 0)
189196
{
190197
var pctUsed = (double)ActiveStmt!.MemoryGrant.MaxUsedKB / ActiveStmt!.MemoryGrant.GrantedKB * 100;
191-
var effClass = pctUsed >= 80 ? "eff-good" : pctUsed >= 60 ? "eff-ok" : pctUsed >= 40 ? "eff-warn" : "eff-bad";
198+
var effClass = pctUsed >= 40 ? "eff-good" : pctUsed >= 20 ? "eff-warn" : "eff-bad";
192199
<div class="insight-row">
193200
<span class="insight-label">Memory</span>
194201
<span class="insight-value">@FormatKB(ActiveStmt!.MemoryGrant.GrantedKB) granted</span>

0 commit comments

Comments
 (0)