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
14 changes: 11 additions & 3 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2800,9 +2800,12 @@ void AddRow(string label, string value, string? color = null)
rowIndex++;
}

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

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

// Compile time — plan-level property (category B). Show regardless of
// threshold so it's always visible, not just when Rule 19 fires.
if (statement.CompileTimeMs > 0)
AddRow("Compile", $"{statement.CompileTimeMs:N0}ms");

// Memory grant — color by utilization percentage
if (statement.MemoryGrant != null)
{
Expand Down
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.7.4</Version>
<Version>1.7.6</Version>
<Authors>Erik Darling</Authors>
<Company>Darling Data LLC</Company>
<Product>Performance Studio</Product>
Expand Down
4 changes: 3 additions & 1 deletion src/PlanViewer.Core/Output/HtmlExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,14 +308,16 @@ private static void WriteRuntimeCard(StringBuilder sb, StatementResult stmt)
WriteRow(sb, "CPU:Elapsed", ratio.ToString("N2"));
}
}
if (stmt.CompileTimeMs > 0)
WriteRow(sb, "Compile", $"{stmt.CompileTimeMs:N0} ms");
if (stmt.DegreeOfParallelism > 0)
WriteRow(sb, "DOP", stmt.DegreeOfParallelism.ToString());
if (stmt.NonParallelReason != null)
WriteRow(sb, "Serial", Encode(stmt.NonParallelReason));
if (stmt.MemoryGrant != null && stmt.MemoryGrant.GrantedKB > 0)
{
var pctUsed = (double)stmt.MemoryGrant.MaxUsedKB / stmt.MemoryGrant.GrantedKB * 100;
var effClass = pctUsed >= 80 ? "eff-good" : pctUsed >= 40 ? "eff-warn" : "eff-bad";
var effClass = pctUsed >= 40 ? "eff-good" : pctUsed >= 20 ? "eff-warn" : "eff-bad";
WriteRow(sb, "Memory", FormatKB(stmt.MemoryGrant.GrantedKB) + " granted");
sb.AppendLine($"<div class=\"row\"><span class=\"label\">Used</span><span class=\"value {effClass}\">{FormatKB(stmt.MemoryGrant.MaxUsedKB)} ({pctUsed:N0}%)</span></div>");
}
Expand Down
58 changes: 49 additions & 9 deletions src/PlanViewer.Core/Services/PlanAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@

if (unsnifffedParams.Count > 0)
{
var hasRecompile = stmt.StatementText.Contains("RECOMPILE", StringComparison.OrdinalIgnoreCase);

Check warning on line 356 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Dereference of a possibly null reference.

Check warning on line 356 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Dereference of a possibly null reference.

Check warning on line 356 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / release

Dereference of a possibly null reference.

Check warning on line 356 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / release

Dereference of a possibly null reference.
if (!hasRecompile)
{
var names = string.Join(", ", unsnifffedParams.Select(p => p.Name));
Expand Down Expand Up @@ -921,21 +921,38 @@
? GetOperatorOwnElapsedMs(node) > 0
: node.CostPercent >= 20;

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

node.Warnings.Add(new PlanWarning
if (colCount <= 3)
{
WarningType = "Bare Scan",
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.",
Severity = PlanWarningSeverity.Warning
});
// Narrow output: a nonclustered rowstore index can cover this cheaply.
var indexAdvice = node.PhysicalOp == "Clustered Index Scan"
? "Consider a nonclustered index on the output columns (as key or INCLUDE) so SQL Server can read a narrower structure."
: "Consider a clustered or nonclustered index on the output columns so SQL Server can read a narrower structure.";

node.Warnings.Add(new PlanWarning
{
WarningType = "Bare Scan",
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.",
Severity = PlanWarningSeverity.Warning
});
}
else
{
// Wider output: rowstore NC index isn't a great fit (would have to
// carry too many columns), but columnstore doesn't care about column
// count. Suggest it for analytical / aggregate-style workloads.
node.Warnings.Add(new PlanWarning
{
WarningType = "Bare Scan",
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.",
Severity = PlanWarningSeverity.Warning
});
}
}
}

Expand Down Expand Up @@ -1204,7 +1221,7 @@
// Rule 28: Row Count Spool — NOT IN with nullable column
// Pattern: Row Count Spool with high rewinds, child scan has IS NULL predicate,
// and statement text contains NOT IN
if (!cfg.IsRuleDisabled(28) && node.PhysicalOp.Contains("Row Count Spool"))

Check warning on line 1224 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Dereference of a possibly null reference.

Check warning on line 1224 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Dereference of a possibly null reference.

Check warning on line 1224 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / release

Dereference of a possibly null reference.

Check warning on line 1224 in src/PlanViewer.Core/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / release

Dereference of a possibly null reference.
{
var rewinds = node.HasActualStats ? (double)node.ActualRewinds : node.EstimateRewinds;
if (rewinds > 10000 && HasNotInPattern(node, stmt))
Expand All @@ -1229,6 +1246,29 @@
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}";
}
}

// Rule 35: Expensive Operator — always show operators that take a significant
// share of statement time even when no other rule has something to say. Joe
// (#215 C8) wanted expensive scans that the tool had nothing to suggest on
// to still surface as top items. Threshold: self-time >= 20% of statement
// elapsed. Only emits if no other warning is already on the node to avoid
// doubling up. The benefit % is just the self-time share.
if (!cfg.IsRuleDisabled(35) && node.HasActualStats && node.Warnings.Count == 0
&& stmt.QueryTimeStats != null && stmt.QueryTimeStats.ElapsedTimeMs > 0)
{
var selfMs = GetOperatorOwnElapsedMs(node);
var pct = (double)selfMs / stmt.QueryTimeStats.ElapsedTimeMs * 100;
if (pct >= 20.0)
{
node.Warnings.Add(new PlanWarning
{
WarningType = "Expensive Operator",
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?",
Severity = pct >= 50 ? PlanWarningSeverity.Critical : PlanWarningSeverity.Warning,
MaxBenefitPercent = Math.Round(Math.Min(100.0, pct), 1)
});
}
}
}

/// <summary>
Expand Down
9 changes: 8 additions & 1 deletion src/PlanViewer.Web/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ else
</div>
}
}
@if (ActiveStmt!.CompileTimeMs > 0)
{
<div class="insight-row">
<span class="insight-label">Compile</span>
<span class="insight-value">@ActiveStmt!.CompileTimeMs.ToString("N0") ms</span>
</div>
}
@if (ActiveStmt!.DegreeOfParallelism > 0)
{
<div class="insight-row">
Expand All @@ -188,7 +195,7 @@ else
@if (ActiveStmt!.MemoryGrant != null && ActiveStmt!.MemoryGrant.GrantedKB > 0)
{
var pctUsed = (double)ActiveStmt!.MemoryGrant.MaxUsedKB / ActiveStmt!.MemoryGrant.GrantedKB * 100;
var effClass = pctUsed >= 80 ? "eff-good" : pctUsed >= 60 ? "eff-ok" : pctUsed >= 40 ? "eff-warn" : "eff-bad";
var effClass = pctUsed >= 40 ? "eff-good" : pctUsed >= 20 ? "eff-warn" : "eff-bad";
<div class="insight-row">
<span class="insight-label">Memory</span>
<span class="insight-value">@FormatKB(ActiveStmt!.MemoryGrant.GrantedKB) granted</span>
Expand Down
Loading