diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml b/src/PlanViewer.App/Controls/PlanViewerControl.axaml
index f632500..503228f 100644
--- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml
+++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml
@@ -52,13 +52,15 @@
Foreground="{DynamicResource ForegroundBrush}"
Background="Transparent"
BorderThickness="0"
- Padding="0">
+ Padding="0"
+ HorizontalAlignment="Stretch"
+ HorizontalContentAlignment="Stretch">
-
+
@@ -153,7 +155,7 @@
+ HorizontalScrollBarVisibility="Auto">
= 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,
@@ -2221,6 +2227,70 @@ private static void CollectWarnings(PlanNode node, List warnings)
CollectWarnings(child, warnings);
}
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+
+ ///
+ /// Computes own elapsed time for a node by subtracting child times in row mode.
+ ///
+ 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 waits, bool isActualPlan)
{
WaitStatsContent.Children.Clear();
@@ -2245,14 +2315,10 @@ private void ShowWaitStats(List 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));
@@ -2263,12 +2329,12 @@ private void ShowWaitStats(List 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)
};
@@ -2276,12 +2342,13 @@ private void ShowWaitStats(List waits, bool isActualPlan)
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,
@@ -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));
@@ -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);
@@ -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)
{
@@ -2363,22 +2434,40 @@ 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;
@@ -2386,10 +2475,12 @@ void AddRow(string label, string value)
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
{