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
49 changes: 46 additions & 3 deletions Dashboard/Services/PlanAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,26 @@
});
}

// Rule 7: Spill detection — promote severity for large spills
// Rule 7: Spill detection — calculate operator time and set severity
// based on what percentage of statement elapsed time the spill accounts for
foreach (var w in node.Warnings.ToList())
{
if (w.SpillDetails != null && w.SpillDetails.WritesToTempDb > 1000)
w.Severity = PlanWarningSeverity.Critical;
if (w.SpillDetails != null && node.ActualElapsedMs > 0)
{
var operatorMs = GetOperatorOwnElapsedMs(node);
var stmtMs = stmt.QueryTimeStats?.ElapsedTimeMs ?? 0;

if (stmtMs > 0)
{
var pct = (double)operatorMs / stmtMs;
w.Message += $" Operator time: {operatorMs:N0}ms ({pct:P0} of statement).";

if (pct >= 0.5)
w.Severity = PlanWarningSeverity.Critical;
else if (pct >= 0.1)
w.Severity = PlanWarningSeverity.Warning;
}
}
}

// Rule 8: Parallel thread skew (actual plans with per-thread stats)
Expand Down Expand Up @@ -438,7 +453,7 @@

// Rule 22: Table variables (Object name starts with @)
if (!string.IsNullOrEmpty(node.ObjectName) &&
node.ObjectName.Contains("@"))

Check warning on line 456 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)

Check warning on line 456 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)

Check warning on line 456 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)

Check warning on line 456 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)

Check warning on line 456 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)

Check warning on line 456 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)
{
node.Warnings.Add(new PlanWarning
{
Expand Down Expand Up @@ -572,7 +587,7 @@
var refPattern = new Regex(
$@"\b(FROM|JOIN)\s+{Regex.Escape(cteName)}\b",
RegexOptions.IgnoreCase);
var refCount = refPattern.Matches(text).Count;

Check warning on line 590 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'Regex.Count' instead of 'Regex.Matches(...).Count' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)

Check warning on line 590 in Dashboard/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'Regex.Count' instead of 'Regex.Matches(...).Count' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1875)

if (refCount > 1)
{
Expand Down Expand Up @@ -626,6 +641,34 @@
FindMemoryConsumers(child, consumers);
}

/// <summary>
/// Calculates an operator's own elapsed time by subtracting child time.
/// In batch mode, operator times are self-contained. In row mode, times are
/// cumulative (include children), so we subtract the dominant child's time.
/// Parallelism (exchange) operators are skipped because they have timing bugs.
/// </summary>
private static long GetOperatorOwnElapsedMs(PlanNode node)
{
if (node.ActualExecutionMode == "Batch")
return node.ActualElapsedMs;

// Row mode: subtract the dominant child's elapsed time
var maxChildElapsed = 0L;
foreach (var child in node.Children)
{
var childElapsed = child.ActualElapsedMs;

// Exchange operators have timing bugs — skip to their child
if (child.PhysicalOp == "Parallelism" && child.Children.Count > 0)
childElapsed = child.Children.Max(c => c.ActualElapsedMs);

if (childElapsed > maxChildElapsed)
maxChildElapsed = childElapsed;
}

return Math.Max(0, node.ActualElapsedMs - maxChildElapsed);
}

private static string Truncate(string value, int maxLength)
{
return value.Length <= maxLength ? value : value[..maxLength] + "...";
Expand Down
49 changes: 46 additions & 3 deletions Lite/Services/PlanAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,26 @@
});
}

// Rule 7: Spill detection — promote severity for large spills
// Rule 7: Spill detection — calculate operator time and set severity
// based on what percentage of statement elapsed time the spill accounts for
foreach (var w in node.Warnings.ToList())
{
if (w.SpillDetails != null && w.SpillDetails.WritesToTempDb > 1000)
w.Severity = PlanWarningSeverity.Critical;
if (w.SpillDetails != null && node.ActualElapsedMs > 0)
{
var operatorMs = GetOperatorOwnElapsedMs(node);
var stmtMs = stmt.QueryTimeStats?.ElapsedTimeMs ?? 0;

if (stmtMs > 0)
{
var pct = (double)operatorMs / stmtMs;
w.Message += $" Operator time: {operatorMs:N0}ms ({pct:P0} of statement).";

if (pct >= 0.5)
w.Severity = PlanWarningSeverity.Critical;
else if (pct >= 0.1)
w.Severity = PlanWarningSeverity.Warning;
}
}
}

// Rule 8: Parallel thread skew (actual plans with per-thread stats)
Expand Down Expand Up @@ -438,7 +453,7 @@

// Rule 22: Table variables (Object name starts with @)
if (!string.IsNullOrEmpty(node.ObjectName) &&
node.ObjectName.Contains("@"))

Check warning on line 456 in Lite/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)

Check warning on line 456 in Lite/Services/PlanAnalyzer.cs

View workflow job for this annotation

GitHub Actions / build

Use 'string.Contains(char)' instead of 'string.Contains(string)' when searching for a single character (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847)
{
node.Warnings.Add(new PlanWarning
{
Expand Down Expand Up @@ -626,6 +641,34 @@
FindMemoryConsumers(child, consumers);
}

/// <summary>
/// Calculates an operator's own elapsed time by subtracting child time.
/// In batch mode, operator times are self-contained. In row mode, times are
/// cumulative (include children), so we subtract the dominant child's time.
/// Parallelism (exchange) operators are skipped because they have timing bugs.
/// </summary>
private static long GetOperatorOwnElapsedMs(PlanNode node)
{
if (node.ActualExecutionMode == "Batch")
return node.ActualElapsedMs;

// Row mode: subtract the dominant child's elapsed time
var maxChildElapsed = 0L;
foreach (var child in node.Children)
{
var childElapsed = child.ActualElapsedMs;

// Exchange operators have timing bugs — skip to their child
if (child.PhysicalOp == "Parallelism" && child.Children.Count > 0)
childElapsed = child.Children.Max(c => c.ActualElapsedMs);

if (childElapsed > maxChildElapsed)
maxChildElapsed = childElapsed;
}

return Math.Max(0, node.ActualElapsedMs - maxChildElapsed);
}

private static string Truncate(string value, int maxLength)
{
return value.Length <= maxLength ? value : value[..maxLength] + "...";
Expand Down
Loading