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
54 changes: 52 additions & 2 deletions src/PlanViewer.App/Controls/PlanViewerControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -495,13 +495,45 @@ private Border CreateNodeVisual(PlanNode node, int totalWarningCount = -1)
var iconBitmap = IconHelper.LoadIcon(node.IconName);
if (iconBitmap != null)
{
iconRow.Children.Add(new Image
var iconImage = 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 !)
Expand Down Expand Up @@ -3062,6 +3094,24 @@ private void UpdateInsightsHeader()
InsightsHeader.Text = " Plan Insights";
}

/// <summary>
/// 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.
/// </summary>
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") ||
Expand Down
29 changes: 28 additions & 1 deletion src/PlanViewer.Core/Services/PlanIconMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,36 @@ public static class PlanIconMapper

/// <summary>
/// Returns the icon file name (without extension) for a given physical operator.
/// When <paramref name="storageType"/> is "ColumnStore" on a *Index Scan,
/// routes to the columnstore variant.
/// </summary>
public static string GetIconName(string physicalOp)
public static string GetIconName(string physicalOp, string? storageType = null)
{
// 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))
{
switch (physicalOp)
{
case "Clustered Index Scan":
case "Index Scan":
case "Columnstore Index Scan":
return "columnstore_index_scan";
case "Clustered Index Delete":
case "Index Delete":
return "columnstore_index_delete";
case "Clustered Index Insert":
case "Index Insert":
return "columnstore_index_insert";
case "Clustered Index Update":
case "Index Update":
return "columnstore_index_update";
case "Clustered Index Merge":
case "Index Merge":
return "columnstore_index_merge";
}
}

if (IconMap.TryGetValue(physicalOp, out var iconName))
return iconName;

Expand Down
10 changes: 8 additions & 2 deletions src/PlanViewer.Core/Services/ShowPlanParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -667,8 +667,9 @@ private static PlanNode ParseRelOp(XElement relOpEl)
}


// Map to icon
node.IconName = PlanIconMapper.GetIconName(node.PhysicalOp);
// Icon mapping is deferred until after StorageType is parsed below,
// so columnstore scans (which surface as Clustered/Index Scan with
// Storage="ColumnStore") can be routed to the columnstore icon.

// Handle operator-specific element
var physicalOpEl = GetOperatorElement(relOpEl);
Expand Down Expand Up @@ -1368,6 +1369,11 @@ 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);

// Recurse into child RelOps
foreach (var childRelOp in FindChildRelOps(relOpEl))
{
Expand Down
Loading