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 {