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
8 changes: 5 additions & 3 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@
Foreground="{DynamicResource ForegroundBrush}"
Background="Transparent"
BorderThickness="0"
Padding="0">
Padding="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch">
<Expander.Header>
<TextBlock x:Name="InsightsHeader" Text=" Plan Insights"
FontWeight="SemiBold"
Foreground="{DynamicResource ForegroundBrush}"/>
</Expander.Header>
<Grid MaxHeight="220">
<Grid MaxHeight="220" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="0"/>
<ColumnDefinition Width="Auto" MinWidth="180"/>
Expand Down Expand Up @@ -153,7 +155,7 @@
<Border Grid.Column="4" Padding="10,4,10,8"
Background="#1A2A3D">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
HorizontalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock x:Name="WaitStatsHeader"
Text="Wait Stats"
Expand Down
147 changes: 119 additions & 28 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -447,24 +447,30 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1)
// Actual plan stats: elapsed time, CPU time, and row counts
if (node.HasActualStats)
{
// Elapsed time -- red if >= 1 second
var elapsedSec = node.ActualElapsedMs / 1000.0;
IBrush elapsedBrush = elapsedSec >= 1.0 ? OrangeRedBrush : fgBrush;
// Compute own time (subtract children in row mode)
var ownElapsedMs = GetOwnElapsedMs(node);
var ownCpuMs = GetOwnCpuMs(node);

// Elapsed time -- color based on own time, not cumulative
var ownElapsedSec = ownElapsedMs / 1000.0;
IBrush elapsedBrush = ownElapsedSec >= 1.0 ? OrangeRedBrush
: ownElapsedSec >= 0.1 ? OrangeBrush : fgBrush;
stack.Children.Add(new TextBlock
{
Text = $"{elapsedSec:F3}s",
Text = $"{ownElapsedSec:F3}s",
FontSize = 10,
Foreground = elapsedBrush,
TextAlignment = TextAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
});

// CPU time -- red if >= 1 second
var cpuSec = node.ActualCPUMs / 1000.0;
IBrush cpuBrush = cpuSec >= 1.0 ? OrangeRedBrush : fgBrush;
// CPU time -- color based on own time
var ownCpuSec = ownCpuMs / 1000.0;
IBrush cpuBrush = ownCpuSec >= 1.0 ? OrangeRedBrush
: ownCpuSec >= 0.1 ? OrangeBrush : fgBrush;
stack.Children.Add(new TextBlock
{
Text = $"CPU: {cpuSec:F3}s",
Text = $"CPU: {ownCpuSec:F3}s",
FontSize = 9,
Foreground = cpuBrush,
TextAlignment = TextAlignment.Center,
Expand Down Expand Up @@ -2221,6 +2227,70 @@ private static void CollectWarnings(PlanNode node, List<PlanWarning> warnings)
CollectWarnings(child, warnings);
}

/// <summary>
/// Computes own CPU time for a node by subtracting child times in row mode.
/// Batch mode reports own time directly; row mode is cumulative from leaves up.
/// </summary>
private static long GetOwnCpuMs(PlanNode node)
{
if (node.ActualCPUMs <= 0) return 0;
var mode = node.ActualExecutionMode ?? node.ExecutionMode;
if (mode == "Batch") return node.ActualCPUMs;
var childSum = GetChildCpuMsSum(node);
return Math.Max(0, node.ActualCPUMs - childSum);
}

/// <summary>
/// Computes own elapsed time for a node by subtracting child times in row mode.
/// </summary>
private static long GetOwnElapsedMs(PlanNode node)
{
if (node.ActualElapsedMs <= 0) return 0;
var mode = node.ActualExecutionMode ?? node.ExecutionMode;
if (mode == "Batch") return node.ActualElapsedMs;
var childSum = GetChildElapsedMsSum(node);
return Math.Max(0, node.ActualElapsedMs - childSum);
}

private static long GetChildCpuMsSum(PlanNode node)
{
long sum = 0;
foreach (var child in node.Children)
{
if (child.ActualCPUMs > 0)
sum += child.ActualCPUMs;
else
sum += GetChildCpuMsSum(child); // skip through transparent operators
}
return sum;
}

private static long GetChildElapsedMsSum(PlanNode node)
{
long sum = 0;
foreach (var child in node.Children)
{
if (child.PhysicalOp == "Parallelism" && child.Children.Count > 0)
{
// Exchange: take max of children (parallel branches)
sum += child.Children
.Where(c => c.ActualElapsedMs > 0)
.Select(c => c.ActualElapsedMs)
.DefaultIfEmpty(0)
.Max();
}
else if (child.ActualElapsedMs > 0)
{
sum += child.ActualElapsedMs;
}
else
{
sum += GetChildElapsedMsSum(child); // skip through transparent operators
}
}
return sum;
}

private void ShowWaitStats(List<WaitStatInfo> waits, bool isActualPlan)
{
WaitStatsContent.Children.Clear();
Expand All @@ -2245,14 +2315,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
// Wait type names are nvarchar(60), longest known is 51 chars
// Default UI font is proportional (~6.5px per char at size 12)
var longestName = sorted.Max(w => w.WaitType.Length);
var nameColWidth = longestName * 6.5 + 10;

// Name and duration auto-size; bar fills remaining space
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions($"{nameColWidth},*,Auto")
ColumnDefinitions = new ColumnDefinitions("Auto,*,Auto")
};
for (int i = 0; i < sorted.Count; i++)
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
Expand All @@ -2263,25 +2329,26 @@ private void ShowWaitStats(List<WaitStatInfo> waits, bool isActualPlan)
var barFraction = maxWait > 0 ? (double)w.WaitTimeMs / maxWait : 0;
var color = GetWaitCategoryColor(GetWaitCategory(w.WaitType));

// Wait type name
// Wait type name — colored by category
var nameText = new TextBlock
{
Text = w.WaitType,
FontSize = 12,
Foreground = new SolidColorBrush(Color.Parse("#E4E6EB")),
Foreground = new SolidColorBrush(Color.Parse(color)),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 2, 10, 2)
};
Grid.SetRow(nameText, i);
Grid.SetColumn(nameText, 0);
grid.Children.Add(nameText);

// Bar — fixed max width, proportional to largest wait
// Bar — semi-transparent category color, compact proportional indicator
var barColor = Color.Parse(color);
var colorBar = new Border
{
Width = Math.Max(4, barFraction * 300),
Width = Math.Max(4, barFraction * 60),
Height = 14,
Background = new SolidColorBrush(Color.Parse(color)),
Background = new SolidColorBrush(Color.FromArgb(0x60, barColor.R, barColor.G, barColor.B)),
CornerRadius = new CornerRadius(2),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Expand Down Expand Up @@ -2322,7 +2389,7 @@ private void ShowRuntimeSummary(PlanStatement statement)
};
int rowIndex = 0;

void AddRow(string label, string value)
void AddRow(string label, string value, string? color = null)
{
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));

Expand All @@ -2342,7 +2409,7 @@ void AddRow(string label, string value)
{
Text = value,
FontSize = 11,
Foreground = new SolidColorBrush(Color.Parse(valueColor)),
Foreground = new SolidColorBrush(Color.Parse(color ?? valueColor)),
Margin = new Thickness(0, 1, 0, 1)
};
Grid.SetRow(valueText, rowIndex);
Expand All @@ -2352,6 +2419,10 @@ void AddRow(string label, string value)
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";

// Runtime stats (actual plans)
if (statement.QueryTimeStats != null)
{
Expand All @@ -2363,33 +2434,53 @@ void AddRow(string label, string value)
AddRow("UDF elapsed", $"{statement.QueryUdfElapsedTimeMs:N0}ms");
}

// Memory grant
// Memory grant — color by utilization percentage
if (statement.MemoryGrant != null)
{
var mg = statement.MemoryGrant;
AddRow("Memory grant", $"{mg.GrantedMemoryKB:N0} KB granted, {mg.MaxUsedMemoryKB:N0} KB used");
var grantPct = mg.GrantedMemoryKB > 0
? (double)mg.MaxUsedMemoryKB / mg.GrantedMemoryKB * 100 : 100;
var grantColor = EfficiencyColor(grantPct);
AddRow("Memory grant",
$"{mg.GrantedMemoryKB:N0} KB granted, {mg.MaxUsedMemoryKB:N0} KB used ({grantPct:N0}%)",
grantColor);
if (mg.GrantWaitTimeMs > 0)
AddRow("Grant wait", $"{mg.GrantWaitTimeMs:N0}ms");
AddRow("Grant wait", $"{mg.GrantWaitTimeMs:N0}ms", "#E57373");
}

// DOP
// DOP + parallelism efficiency — color by efficiency
if (statement.DegreeOfParallelism > 0)
AddRow("DOP", statement.DegreeOfParallelism.ToString());
{
var dopText = statement.DegreeOfParallelism.ToString();
string? dopColor = null;
if (statement.QueryTimeStats != null &&
statement.QueryTimeStats.ElapsedTimeMs > 0 &&
statement.QueryTimeStats.CpuTimeMs > 0)
{
var idealCpu = statement.QueryTimeStats.ElapsedTimeMs * statement.DegreeOfParallelism;
var efficiency = Math.Min(100.0, statement.QueryTimeStats.CpuTimeMs * 100.0 / idealCpu);
dopText += $" ({efficiency:N0}% efficient)";
dopColor = EfficiencyColor(efficiency);
}
AddRow("DOP", dopText, dopColor);
}
else if (statement.NonParallelPlanReason != null)
AddRow("Serial", statement.NonParallelPlanReason);

// Thread stats
// Thread stats — color by utilization
if (statement.ThreadStats != null)
{
var ts = statement.ThreadStats;
AddRow("Branches", ts.Branches.ToString());
var totalReserved = ts.Reservations.Sum(r => r.ReservedThreads);
if (totalReserved > 0)
{
var threadPct = (double)ts.UsedThreads / totalReserved * 100;
var threadColor = EfficiencyColor(threadPct);
var threadText = ts.UsedThreads == totalReserved
? $"{ts.UsedThreads} used ({totalReserved} reserved)"
: $"{ts.UsedThreads} used of {totalReserved} reserved ({totalReserved - ts.UsedThreads} inactive)";
AddRow("Threads", threadText);
AddRow("Threads", threadText, threadColor);
}
else
{
Expand Down
Loading