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
2 changes: 1 addition & 1 deletion src/PlanViewer.App/Controls/WaitProfileBarControl.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ private void Redraw()
Canvas.SetTop(rect, 0);
BarCanvas.Children.Add(rect);

ToolTip.SetTip(rect, $"{seg.Category}: {seg.WaitRatio:P2} ({seg.Ratio:P1} of total)");
ToolTip.SetTip(rect, $"{seg.Category}: {WaitRatioFormatter.Format(seg.WaitRatio)} ({seg.Ratio:P1} of total)");

var capturedCategory = seg.Category;
rect.PointerPressed += (_, e) =>
Expand Down
47 changes: 17 additions & 30 deletions src/PlanViewer.App/Controls/WaitStatsProfileControl.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,33 @@
xmlns:local="using:PlanViewer.App.Controls"
x:Class="PlanViewer.App.Controls.WaitStatsProfileControl">
<Grid RowDefinitions="Auto,*">
<!-- Header: toggle chart type -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="6" Margin="4,2">
<Button x:Name="ToggleChartButton" Content=""
<!-- Header row: left controls + right-aligned legend button -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto" Margin="4,2">
<Button x:Name="ToggleChartButton" Grid.Column="0" Content="&#x25A4;"
Width="22" Height="22" Padding="0"
FontSize="12" Background="Transparent" BorderThickness="0"
Foreground="{DynamicResource ForegroundBrush}"
ToolTip.Tip="Toggle bar / ribbon chart"
Click="ToggleChart_Click"/>
<TextBlock x:Name="TitleText" Text="Wait Stats"
<TextBlock x:Name="TitleText" Grid.Column="1" Text="Wait Stats"
FontSize="11" FontWeight="SemiBold"
Foreground="{DynamicResource SlicerToggleBrush}"
VerticalAlignment="Center"/>
<Button x:Name="TableViewButton" Content="▦"
Width="22" Height="22" Padding="0"
FontSize="12" Background="Transparent" BorderThickness="0"
VerticalAlignment="Center" Margin="6,0,0,0"/>
<!-- spacer -->
<Button x:Name="LegendButton" Grid.Column="3" Content="Legend"
Height="20" Padding="8,0"
FontSize="10"
Background="Transparent" BorderThickness="1"
BorderBrush="{DynamicResource ForegroundMutedBrush}"
Foreground="{DynamicResource ForegroundBrush}"
ToolTip.Tip="Show wait stats as table"
Click="TableView_Click"/>
</StackPanel>
ToolTip.Tip="Show color legend"
VerticalAlignment="Center"
Click="Legend_Click"/>
</Grid>
<!-- Content area -->
<Grid Grid.Row="1" x:Name="ContentArea" MinHeight="24">
<local:WaitProfileBarControl x:Name="GlobalBar" PercentMode="True"/>
<local:WaitStatsRibbonControl x:Name="GlobalRibbon" IsVisible="False"/>
<DataGrid x:Name="TableGrid" IsVisible="False"
AutoGenerateColumns="False"
CanUserSortColumns="True"
CanUserResizeColumns="True"
IsReadOnly="True"
SelectionMode="Single"
GridLinesVisibility="Horizontal"
HeadersVisibility="Column"
FontSize="11"
Background="{DynamicResource BackgroundDarkBrush}"
BorderThickness="0">
<DataGrid.Columns>
<DataGridTextColumn Header="Category" Binding="{ReflectionBinding Category}" Width="*"/>
<DataGridTextColumn Header="Wait Ratio" Binding="{ReflectionBinding WaitRatioText}" Width="Auto"/>
<DataGridTextColumn Header="% of Total" Binding="{ReflectionBinding RatioText}" Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
<local:WaitProfileBarControl x:Name="GlobalBar" PercentMode="True" IsVisible="False"/>
<local:WaitStatsRibbonControl x:Name="GlobalRibbon"/>
<!-- Loading overlay -->
<Border x:Name="WaitLoadingOverlay" IsVisible="False"
Background="#80000000" CornerRadius="0"
Expand Down
147 changes: 100 additions & 47 deletions src/PlanViewer.App/Controls/WaitStatsProfileControl.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using PlanViewer.Core.Models;

namespace PlanViewer.App.Controls;

public partial class WaitStatsProfileControl : UserControl
{
private enum ViewMode { Bar, Ribbon, Table }
private enum ViewMode { Bar, Ribbon }

private ViewMode _viewMode = ViewMode.Bar;
private ViewMode _viewMode = ViewMode.Ribbon;
private bool _isCollapsed;
private WaitProfile? _currentProfile;
private readonly ObservableCollection<WaitTableRow> _tableRows = new();
private Popup? _legendPopup;

public event EventHandler<string>? CategoryClicked;
public event EventHandler<string>? CategoryDoubleClicked;
public event EventHandler<bool>? CollapsedChanged;

public bool IsCollapsed => _isCollapsed;

// All known wait categories in the order they appear in the theme
private static readonly string[] AllWaitCategories =
[
"Unknown", "CPU", "Worker Thread", "Lock", "Latch", "Buffer Latch",
"Buffer IO", "Compilation", "SQL CLR", "Mirroring", "Transaction",
"Preemptive", "Service Broker", "Tran Log IO", "Network IO",
"Parallelism", "Memory", "Tracing", "Full Text Search",
"Other Disk IO", "Replication", "Log Rate Governor", "Others"
];

public WaitStatsProfileControl()
{
InitializeComponent();
TableGrid.ItemsSource = _tableRows;
GlobalBar.CategoryClicked += (_, cat) => CategoryClicked?.Invoke(this, cat);
GlobalBar.CategoryDoubleClicked += (_, cat) => CategoryDoubleClicked?.Invoke(this, cat);
GlobalRibbon.CategoryClicked += (_, cat) => CategoryClicked?.Invoke(this, cat);
Expand All @@ -38,7 +50,6 @@ public void SetBarProfile(WaitProfile? profile)
{
_currentProfile = profile;
GlobalBar.SetProfile(profile);
RefreshTableRows();
}

public void SetRibbonData(List<WaitCategoryTimeSlice> data)
Expand All @@ -59,7 +70,7 @@ public void Expand()
ContentArea.IsVisible = true;
TitleText.IsVisible = true;
ToggleChartButton.IsVisible = true;
TableViewButton.IsVisible = true;
LegendButton.IsVisible = true;
CollapsedChanged?.Invoke(this, false);
}

Expand All @@ -70,7 +81,7 @@ public void Collapse()
ContentArea.IsVisible = false;
TitleText.IsVisible = false;
ToggleChartButton.IsVisible = false;
TableViewButton.IsVisible = false;
LegendButton.IsVisible = false;
CollapsedChanged?.Invoke(this, true);
}

Expand All @@ -81,57 +92,99 @@ public void SetLoading(bool isLoading)

private void ToggleChart_Click(object? sender, RoutedEventArgs e)
{
// Cycle: Bar -> Ribbon -> Bar (skip table; table has its own button)
if (_viewMode == ViewMode.Table)
{
// If in table mode, toggle goes back to bar
_viewMode = ViewMode.Bar;
}
else
{
_viewMode = _viewMode == ViewMode.Bar ? ViewMode.Ribbon : ViewMode.Bar;
}
ApplyViewMode();
}

private void TableView_Click(object? sender, RoutedEventArgs e)
{
_viewMode = _viewMode == ViewMode.Table ? ViewMode.Bar : ViewMode.Table;
_viewMode = _viewMode == ViewMode.Bar ? ViewMode.Ribbon : ViewMode.Bar;
ApplyViewMode();
}

private void ApplyViewMode()
{
GlobalBar.IsVisible = _viewMode == ViewMode.Bar;
GlobalRibbon.IsVisible = _viewMode == ViewMode.Ribbon;
TableGrid.IsVisible = _viewMode == ViewMode.Table;
ToggleChartButton.Content = _viewMode == ViewMode.Ribbon ? "▤" : "☰";

// The ContentArea lives inside an Auto-sized parent row, so a *-row
// DataGrid would collapse to zero height. Give an explicit height
// when in table mode; reset to NaN (auto) for chart modes.
ContentArea.Height = _viewMode == ViewMode.Table ? 120 : double.NaN;
ToggleChartButton.Content = _viewMode == ViewMode.Ribbon ? "☰" : "▤";
}

private void RefreshTableRows()
private void Legend_Click(object? sender, RoutedEventArgs e)
{
_tableRows.Clear();
if (_currentProfile == null) { return; }
foreach (var seg in _currentProfile.Segments.OrderByDescending(s => s.Ratio))
if (_legendPopup != null)
{
_tableRows.Add(new WaitTableRow
_legendPopup.IsOpen = !_legendPopup.IsOpen;
return;
}

var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,Auto") };
for (int i = 0; i < AllWaitCategories.Length; i++)
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));

for (int i = 0; i < AllWaitCategories.Length; i++)
{
var cat = AllWaitCategories[i];
var brush = TryFindBrush($"WaitCategory.{cat}", new SolidColorBrush(Color.Parse("#555D66")));

var swatch = new Border
{
Category = seg.Category,
WaitRatioText = seg.WaitRatio.ToString("P2"),
RatioText = seg.Ratio.ToString("P1")
});
Width = 14,
Height = 14,
Background = brush,
CornerRadius = new CornerRadius(2),
Margin = new Thickness(4, 2),
VerticalAlignment = VerticalAlignment.Center,
};
Grid.SetRow(swatch, i);
Grid.SetColumn(swatch, 0);
grid.Children.Add(swatch);

var label = new TextBlock
{
Text = cat,
FontSize = 11,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(4, 2, 8, 2),
};
if (this.TryFindResource("ForegroundBrush", this.ActualThemeVariant, out var fg) && fg is IBrush fgBrush)
label.Foreground = fgBrush;
Grid.SetRow(label, i);
Grid.SetColumn(label, 1);
grid.Children.Add(label);
}

var scroll = new ScrollViewer
{
Content = grid,
MaxHeight = 300,
VerticalScrollBarVisibility = Avalonia.Controls.Primitives.ScrollBarVisibility.Auto,
};

var bgBrush = TryFindBrush("BackgroundLightBrush", new SolidColorBrush(Color.Parse("#22252D")));
var borderBrush = TryFindBrush("BorderBrush", new SolidColorBrush(Color.Parse("#3A3D45")));
var container = new Border
{
Background = bgBrush,
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(6),
Padding = new Thickness(4),
Child = scroll,
};

_legendPopup = new Popup
{
Child = container,
IsLightDismissEnabled = true,
Placement = PlacementMode.Bottom,
PlacementTarget = LegendButton,
};

// Add to visual tree so DynamicResources resolve
if (this.Content is Grid rootGrid)
rootGrid.Children.Add(_legendPopup);

_legendPopup.IsOpen = true;
}
}

public class WaitTableRow
{
public string Category { get; set; } = "";
public string WaitRatioText { get; set; } = "";
public string RatioText { get; set; } = "";
private IBrush TryFindBrush(string key, IBrush fallback)
{
if (this.TryFindResource(key, this.ActualThemeVariant, out var resource) && resource is IBrush brush)
return brush;
return fallback;
}
}
Loading
Loading