diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs index 2db8609..75fb26f 100644 --- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs +++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs @@ -495,45 +495,13 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1) var iconBitmap = IconHelper.LoadIcon(node.IconName); if (iconBitmap != null) { - var iconImage = new Image + iconRow.Children.Add(new Image { Source = iconBitmap, Width = 32, Height = 32, Margin = new Thickness(0, 0, 0, 2) - }; - - // Distinguish Parallelism subtypes (Repartition / Distribute / Gather Streams) - // by overlaying a small letter on the shared parallelism.png icon. - var parallelismGlyph = GetParallelismGlyph(node); - if (parallelismGlyph != null) - { - var iconGrid = new Grid { Margin = new Thickness(0, 0, 0, 2) }; - iconGrid.Children.Add(iconImage); - iconImage.Margin = default; - iconGrid.Children.Add(new Border - { - Width = 14, Height = 14, - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Bottom, - Background = new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x33)), - CornerRadius = new CornerRadius(7), - Child = new TextBlock - { - Text = parallelismGlyph, - FontSize = 9, - FontWeight = FontWeight.Bold, - Foreground = Brushes.White, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center - } - }); - iconRow.Children.Add(iconGrid); - } - else - { - iconRow.Children.Add(iconImage); - } + }); } // Warning indicator badge (orange triangle with !) @@ -3094,24 +3062,6 @@ private void UpdateInsightsHeader() InsightsHeader.Text = " Plan Insights"; } - /// - /// For Parallelism nodes, returns a single-letter glyph distinguishing - /// the three subtypes: R(epartition) / D(istribute) / G(ather) Streams. - /// Returns null for non-parallelism operators. - /// - private static string? GetParallelismGlyph(PlanNode node) - { - if (!string.Equals(node.PhysicalOp, "Parallelism", StringComparison.Ordinal)) - return null; - return node.LogicalOp switch - { - "Repartition Streams" => "R", - "Distribute Streams" => "D", - "Gather Streams" => "G", - _ => null - }; - } - private static string GetWaitCategory(string waitType) { if (waitType.StartsWith("SOS_SCHEDULER_YIELD") || diff --git a/src/PlanViewer.Core/Resources/PlanIcons/parallelism_distribute_streams.png b/src/PlanViewer.Core/Resources/PlanIcons/parallelism_distribute_streams.png new file mode 100644 index 0000000..f85dd59 Binary files /dev/null and b/src/PlanViewer.Core/Resources/PlanIcons/parallelism_distribute_streams.png differ diff --git a/src/PlanViewer.Core/Resources/PlanIcons/parallelism_gather_streams.png b/src/PlanViewer.Core/Resources/PlanIcons/parallelism_gather_streams.png new file mode 100644 index 0000000..14078f7 Binary files /dev/null and b/src/PlanViewer.Core/Resources/PlanIcons/parallelism_gather_streams.png differ diff --git a/src/PlanViewer.Core/Resources/PlanIcons/parallelism_repartition_streams.png b/src/PlanViewer.Core/Resources/PlanIcons/parallelism_repartition_streams.png new file mode 100644 index 0000000..bda974d Binary files /dev/null and b/src/PlanViewer.Core/Resources/PlanIcons/parallelism_repartition_streams.png differ diff --git a/src/PlanViewer.Core/Services/PlanIconMapper.cs b/src/PlanViewer.Core/Services/PlanIconMapper.cs index d329d7d..79f6361 100644 --- a/src/PlanViewer.Core/Services/PlanIconMapper.cs +++ b/src/PlanViewer.Core/Services/PlanIconMapper.cs @@ -179,10 +179,23 @@ public static class PlanIconMapper /// /// Returns the icon file name (without extension) for a given physical operator. /// When is "ColumnStore" on a *Index Scan, - /// routes to the columnstore variant. + /// routes to the columnstore variant. When is + /// "Parallelism" and identifies a stream subtype, + /// routes to the matching subtype icon. /// - public static string GetIconName(string physicalOp, string? storageType = null) + public static string GetIconName(string physicalOp, string? storageType = null, string? logicalOp = null) { + // Parallelism subtypes: PhysicalOp="Parallelism" + LogicalOp identifies which. + if (string.Equals(physicalOp, "Parallelism", StringComparison.Ordinal) && logicalOp != null) + { + switch (logicalOp) + { + case "Repartition Streams": return "parallelism_repartition_streams"; + case "Distribute Streams": return "parallelism_distribute_streams"; + case "Gather Streams": return "parallelism_gather_streams"; + } + } + // Columnstore scans surface as PhysicalOp="Clustered Index Scan" / "Index Scan" // with Storage="ColumnStore" on the Object element. Route to the columnstore icon. if (string.Equals(storageType, "ColumnStore", StringComparison.OrdinalIgnoreCase)) diff --git a/src/PlanViewer.Core/Services/ShowPlanParser.cs b/src/PlanViewer.Core/Services/ShowPlanParser.cs index 35194c5..a0ebebd 100644 --- a/src/PlanViewer.Core/Services/ShowPlanParser.cs +++ b/src/PlanViewer.Core/Services/ShowPlanParser.cs @@ -1369,10 +1369,10 @@ private static PlanNode ParseRelOp(XElement relOpEl) } } - // Map to icon — done here so columnstore scans (which surface as - // Clustered/Index Scan with Storage="ColumnStore") can be routed to - // the columnstore icon. - node.IconName = PlanIconMapper.GetIconName(node.PhysicalOp, node.StorageType); + // Map to icon — done here so columnstore scans (Clustered/Index Scan + // with Storage="ColumnStore") and Parallelism subtypes (which depend on + // LogicalOp) can be routed to their specific icons. + node.IconName = PlanIconMapper.GetIconName(node.PhysicalOp, node.StorageType, node.LogicalOp); // Recurse into child RelOps foreach (var childRelOp in FindChildRelOps(relOpEl))