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
31 changes: 26 additions & 5 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ private void RenderStatement(PlanStatement statement)
// Update banners
ShowMissingIndexes(statement.MissingIndexes);
ShowParameters(statement);
ShowWaitStats(statement.WaitStats, statement.QueryTimeStats != null);
ShowWaitStats(statement.WaitStats, statement.WaitBenefits, statement.QueryTimeStats != null);
ShowRuntimeSummary(statement);
UpdateInsightsHeader();

Expand Down Expand Up @@ -2635,7 +2635,7 @@ private static long GetChildElapsedMsSum(PlanNode node)
return sum;
}

private void ShowWaitStats(List<WaitStatInfo> waits, bool isActualPlan)
private void ShowWaitStats(List<WaitStatInfo> waits, List<WaitBenefit> benefits, bool isActualPlan)
{
WaitStatsContent.Children.Clear();

Expand All @@ -2651,6 +2651,11 @@ private void ShowWaitStats(List<WaitStatInfo> waits, bool isActualPlan)

WaitStatsEmpty.IsVisible = false;

// Build benefit lookup
var benefitLookup = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
foreach (var wb in benefits)
benefitLookup[wb.WaitType] = wb.MaxBenefitPercent;

var sorted = waits.OrderByDescending(w => w.WaitTimeMs).ToList();
var maxWait = sorted[0].WaitTimeMs;
var totalWait = sorted.Sum(w => w.WaitTimeMs);
Expand All @@ -2659,10 +2664,10 @@ private void ShowWaitStats(List<WaitStatInfo> waits, bool isActualPlan)
WaitStatsHeader.Text = $" Wait Stats \u2014 {totalWait:N0}ms total";

// Build a single Grid for all rows so columns align
// Name and duration auto-size; bar fills remaining space
// Name, bar, duration, and benefit columns
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto")
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto,Auto")
};
for (int i = 0; i < sorted.Count; i++)
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
Expand Down Expand Up @@ -2709,11 +2714,27 @@ private void ShowWaitStats(List<WaitStatInfo> waits, bool isActualPlan)
FontSize = 12,
Foreground = new SolidColorBrush(Color.Parse("#E4E6EB")),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 2, 0, 2)
Margin = new Thickness(0, 2, 8, 2)
};
Grid.SetRow(durationText, i);
Grid.SetColumn(durationText, 2);
grid.Children.Add(durationText);

// Benefit % (if available)
if (benefitLookup.TryGetValue(w.WaitType, out var benefitPct) && benefitPct > 0)
{
var benefitText = new TextBlock
{
Text = $"up to {benefitPct:N0}%",
FontSize = 11,
Foreground = new SolidColorBrush(Color.Parse("#8b949e")),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 2, 0, 2)
};
Grid.SetRow(benefitText, i);
Grid.SetColumn(benefitText, 3);
grid.Children.Add(benefitText);
}
}

WaitStatsContent.Children.Add(grid);
Expand Down
8 changes: 8 additions & 0 deletions src/PlanViewer.Core/Models/PlanModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class PlanStatement
public SetOptionsInfo? SetOptions { get; set; }
public List<PlanParameter> Parameters { get; set; } = new();
public List<WaitStatInfo> WaitStats { get; set; } = new();
public List<WaitBenefit> WaitBenefits { get; set; } = new();
public QueryTimeInfo? QueryTimeStats { get; set; }

// MaxQueryMemory + QueryPlan-level warnings
Expand Down Expand Up @@ -447,6 +448,13 @@ public class PlanParameter
public string? RuntimeValue { get; set; }
}

public class WaitBenefit
{
public string WaitType { get; set; } = "";
public double MaxBenefitPercent { get; set; }
public string Category { get; set; } = "";
}

public class WaitStatInfo
{
public string WaitType { get; set; } = "";
Expand Down
16 changes: 16 additions & 0 deletions src/PlanViewer.Core/Output/AnalysisResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ public class StatementResult
[JsonPropertyName("wait_stats")]
public List<WaitStatResult> WaitStats { get; set; } = new();

// Wait stats benefit analysis
[JsonPropertyName("wait_benefits")]
public List<WaitBenefitResult> WaitBenefits { get; set; } = new();

// Cursor metadata
[JsonPropertyName("cursor")]
public CursorResult? Cursor { get; set; }
Expand Down Expand Up @@ -353,6 +357,18 @@ public class WaitStatResult
public long WaitCount { get; set; }
}

public class WaitBenefitResult
{
[JsonPropertyName("wait_type")]
public string WaitType { get; set; } = "";

[JsonPropertyName("max_benefit_percent")]
public double MaxBenefitPercent { get; set; }

[JsonPropertyName("category")]
public string Category { get; set; } = "";
}

public class CursorResult
{
[JsonPropertyName("name")]
Expand Down
10 changes: 9 additions & 1 deletion src/PlanViewer.Core/Output/HtmlExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,14 +391,22 @@ private static void WriteWaitStatsCard(StringBuilder sb, StatementResult stmt, b
sb.AppendLine("<div class=\"card-body\">");
if (stmt.WaitStats.Count > 0)
{
// Build benefit lookup
var benefitLookup = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
foreach (var wb in stmt.WaitBenefits)
benefitLookup[wb.WaitType] = wb.MaxBenefitPercent;

var maxWait = stmt.WaitStats.Max(w => w.WaitTimeMs);
foreach (var w in stmt.WaitStats.OrderByDescending(w => w.WaitTimeMs))
{
var barPct = maxWait > 0 ? (double)w.WaitTimeMs / maxWait * 100 : 0;
var benefitTag = benefitLookup.TryGetValue(w.WaitType, out var pct)
? $" <span class=\"warn-benefit\">up to {pct:N0}%</span>"
: "";
sb.AppendLine("<div class=\"wait-row\">");
sb.AppendLine($"<span class=\"wait-type\">{Encode(w.WaitType)}</span>");
sb.AppendLine($"<div class=\"wait-bar-container\"><div class=\"wait-bar\" style=\"width:{barPct:F0}%\"></div></div>");
sb.AppendLine($"<span class=\"wait-ms\">{w.WaitTimeMs:N0} ms</span>");
sb.AppendLine($"<span class=\"wait-ms\">{w.WaitTimeMs:N0} ms{benefitTag}</span>");
sb.AppendLine("</div>");
}
}
Expand Down
11 changes: 11 additions & 0 deletions src/PlanViewer.Core/Output/ResultMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ private static StatementResult MapStatement(PlanStatement stmt)
});
}

// Wait stat benefits
foreach (var wb in stmt.WaitBenefits)
{
result.WaitBenefits.Add(new WaitBenefitResult
{
WaitType = wb.WaitType,
MaxBenefitPercent = wb.MaxBenefitPercent,
Category = wb.Category
});
}

// Parameters — flag potential sniffing issues
foreach (var p in stmt.Parameters)
{
Expand Down
12 changes: 11 additions & 1 deletion src/PlanViewer.Core/Output/TextFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,18 @@ public static void WriteText(AnalysisResult result, TextWriter writer)
if (stmt.WaitStats.Count > 0)
{
writer.WriteLine("Wait stats:");
// Build a lookup from wait type to benefit %
var benefitLookup = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
foreach (var wb in stmt.WaitBenefits)
benefitLookup[wb.WaitType] = wb.MaxBenefitPercent;

foreach (var w in stmt.WaitStats.OrderByDescending(w => w.WaitTimeMs))
writer.WriteLine($" {w.WaitType}: {w.WaitTimeMs:N0}ms");
{
var benefitTag = benefitLookup.TryGetValue(w.WaitType, out var pct)
? $" (up to {pct:N0}% benefit)"
: "";
writer.WriteLine($" {w.WaitType}: {w.WaitTimeMs:N0}ms{benefitTag}");
}
}

if (stmt.Parameters.Count > 0)
Expand Down
Loading
Loading