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
133 changes: 130 additions & 3 deletions Dashboard/Controls/QueryPerformanceContent.xaml

Large diffs are not rendered by default.

146 changes: 145 additions & 1 deletion Dashboard/Controls/QueryPerformanceContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ private static (DateTime start, DateTime end) GetSlicerTimeRange(
/// <summary>Raised when a drill-down needs the parent to set custom time pickers. Args: (fromUtc, toUtc)</summary>
public event Action<DateTime, DateTime>? DrillDownTimeRangeRequested;

/// <summary>Fired when the Queries sub-tab changes, so the global Compare dropdown can update.</summary>
public event Action? SubTabChanged;

private CancellationTokenSource? _actualPlanCts;

/// <summary>Cancels the in-flight actual plan execution, if any.</summary>
Expand Down Expand Up @@ -157,7 +160,14 @@ public QueryPerformanceContent()
SetupChartSaveMenus();
Loaded += OnLoaded;
Unloaded += OnUnloaded;
SubTabControl.SelectionChanged += (s, e) => { if (e.Source == SubTabControl) _isDrillDownActive = false; };
SubTabControl.SelectionChanged += (s, e) =>
{
if (e.Source == SubTabControl)
{
_isDrillDownActive = false;
SubTabChanged?.Invoke();
}
};
Helpers.ThemeManager.ThemeChanged += OnThemeChanged;

_queryDurationHover = new Helpers.ChartHoverHelper(QueryPerfTrendsQueryChart, "ms/sec");
Expand Down Expand Up @@ -723,6 +733,140 @@ public void RefreshGridBindings()
/// </summary>
public void SelectSubTab(int index) => SubTabControl.SelectedIndex = index;

private (DateTime From, DateTime To)? _comparisonRange;

public void SetComparisonRange((DateTime From, DateTime To)? range)
{
_comparisonRange = range;
}

public async Task RefreshComparisonAsync()
{
if (_databaseService == null) return;

try
{
var currentEnd = _queryStatsToDate ?? DateTime.UtcNow;
var currentStart = _queryStatsFromDate ?? currentEnd.AddHours(-_queryStatsHoursBack);

await RefreshQueryStatsComparisonAsync(currentStart, currentEnd);
await RefreshProcStatsComparisonAsync(currentStart, currentEnd);
await RefreshQueryStoreComparisonAsync(currentStart, currentEnd);
}
catch (Exception ex)
{
_statusCallback?.Invoke($"Comparison failed: {ex.Message}");
}
}

private void SetQueryStatsComparisonMode(bool active, (DateTime From, DateTime To)? baselineRange = null)
{
QueryStatsDataGrid.Visibility = active ? System.Windows.Visibility.Collapsed : System.Windows.Visibility.Visible;
QueryStatsComparisonGrid.Visibility = active ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;
QueryStatsComparisonBanner.Visibility = active ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;

if (active && baselineRange.HasValue)
{
var from = baselineRange.Value.From.ToString("yyyy-MM-dd HH:mm");
var to = baselineRange.Value.To.ToString("yyyy-MM-dd HH:mm");
QueryStatsComparisonBanner.Text = $"Comparing against baseline: {from} \u2192 {to}";
}
}

private async Task RefreshQueryStatsComparisonAsync(DateTime currentStart, DateTime currentEnd)
{
if (_comparisonRange == null)
{
SetQueryStatsComparisonMode(false);
return;
}

SetQueryStatsComparisonMode(true, _comparisonRange);

var items = await _databaseService!.GetQueryStatsComparisonAsync(
currentStart, currentEnd,
_comparisonRange.Value.From, _comparisonRange.Value.To);

var sorted = items
.OrderBy(x => x.SortGroup)
.ThenByDescending(x => x.SortableDurationDelta)
.ToList();

QueryStatsComparisonGrid.ItemsSource = sorted;
}

private void SetProcStatsComparisonMode(bool active, (DateTime From, DateTime To)? baselineRange = null)
{
ProcStatsDataGrid.Visibility = active ? System.Windows.Visibility.Collapsed : System.Windows.Visibility.Visible;
ProcStatsComparisonGrid.Visibility = active ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;
ProcStatsComparisonBanner.Visibility = active ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;

if (active && baselineRange.HasValue)
{
var from = baselineRange.Value.From.ToString("yyyy-MM-dd HH:mm");
var to = baselineRange.Value.To.ToString("yyyy-MM-dd HH:mm");
ProcStatsComparisonBanner.Text = $"Comparing against baseline: {from} \u2192 {to}";
}
}

private async Task RefreshProcStatsComparisonAsync(DateTime currentStart, DateTime currentEnd)
{
if (_comparisonRange == null)
{
SetProcStatsComparisonMode(false);
return;
}

SetProcStatsComparisonMode(true, _comparisonRange);

var items = await _databaseService!.GetProcedureStatsComparisonAsync(
currentStart, currentEnd,
_comparisonRange.Value.From, _comparisonRange.Value.To);

var sorted = items
.OrderBy(x => x.SortGroup)
.ThenByDescending(x => x.SortableDurationDelta)
.ToList();

ProcStatsComparisonGrid.ItemsSource = sorted;
}

private void SetQueryStoreComparisonMode(bool active, (DateTime From, DateTime To)? baselineRange = null)
{
QueryStoreDataGrid.Visibility = active ? System.Windows.Visibility.Collapsed : System.Windows.Visibility.Visible;
QueryStoreComparisonGrid.Visibility = active ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;
QueryStoreComparisonBanner.Visibility = active ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;

if (active && baselineRange.HasValue)
{
var from = baselineRange.Value.From.ToString("yyyy-MM-dd HH:mm");
var to = baselineRange.Value.To.ToString("yyyy-MM-dd HH:mm");
QueryStoreComparisonBanner.Text = $"Comparing against baseline: {from} \u2192 {to}";
}
}

private async Task RefreshQueryStoreComparisonAsync(DateTime currentStart, DateTime currentEnd)
{
if (_comparisonRange == null)
{
SetQueryStoreComparisonMode(false);
return;
}

SetQueryStoreComparisonMode(true, _comparisonRange);

var items = await _databaseService!.GetQueryStoreComparisonAsync(
currentStart, currentEnd,
_comparisonRange.Value.From, _comparisonRange.Value.To);

var sorted = items
.OrderBy(x => x.SortGroup)
.ThenByDescending(x => x.SortableDurationDelta)
.ToList();

QueryStoreComparisonGrid.ItemsSource = sorted;
}

public void SetTimeRange(int hoursBack, DateTime? fromDate = null, DateTime? toDate = null)
{
_isDrillDownActive = false;
Expand Down
12 changes: 0 additions & 12 deletions Dashboard/Controls/ResourceMetricsContent.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,6 @@
<!-- Server Trends Sub-Tab — Correlated Timeline Lanes -->
<TabItem Header="Server Trends">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="8,4,8,0">
<TextBlock Text="Compare:" VerticalAlignment="Center" Margin="0,0,4,0"
Foreground="{DynamicResource ForegroundBrush}"/>
<ComboBox x:Name="CompareToCombo" SelectedIndex="0" Width="150"
SelectionChanged="CompareToCombo_SelectionChanged"
ToolTip="Overlay a reference period on the timeline charts">
<ComboBoxItem Content="None"/>
<ComboBoxItem Content="Yesterday"/>
<ComboBoxItem Content="Last week"/>
<ComboBoxItem Content="Same day last week"/>
</ComboBox>
</StackPanel>
<local:CorrelatedTimelineLanesControl x:Name="CorrelatedLanes"/>
</DockPanel>
</TabItem>
Expand Down
26 changes: 4 additions & 22 deletions Dashboard/Controls/ResourceMetricsContent.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1025,33 +1025,15 @@ private async Task LoadFileIoThroughputChartsAsync()

#region Server Trends Tab

private async void CompareToCombo_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!IsLoaded) return;
ComparisonRange = GetComparisonRange();
await RefreshServerTrendsAsync();
}

private (DateTime From, DateTime To)? ComparisonRange { get; set; }

/// <summary>
/// Computes the reference time range for the comparison overlay.
/// Returns null if "None" is selected.
/// Sets the comparison range from the global Compare dropdown and refreshes Server Trends.
/// </summary>
private (DateTime From, DateTime To)? GetComparisonRange()
public async Task SetComparisonRangeAsync((DateTime From, DateTime To)? range)
{
if (CompareToCombo == null || CompareToCombo.SelectedIndex <= 0) return null;

var currentEnd = _serverTrendsToDate ?? DateTime.UtcNow;
var currentStart = _serverTrendsFromDate ?? currentEnd.AddHours(-_serverTrendsHoursBack);

return CompareToCombo.SelectedIndex switch
{
1 => (currentStart.AddDays(-1), currentEnd.AddDays(-1)), // Yesterday
2 => (currentStart.AddDays(-7), currentEnd.AddDays(-7)), // Last week
3 => (currentStart.AddDays(-7), currentEnd.AddDays(-7)), // Same day last week
_ => null
};
ComparisonRange = range;
await RefreshServerTrendsAsync();
}

private async Task RefreshServerTrendsAsync()
Expand Down
80 changes: 80 additions & 0 deletions Dashboard/Models/ComparisonItemBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Windows.Media;

namespace PerformanceMonitorDashboard.Models
{
public abstract class ComparisonItemBase
{
public string DatabaseName { get; set; } = "";

// Current period
public long ExecutionCount { get; set; }
public double AvgDurationMs { get; set; }
public double AvgCpuMs { get; set; }
public double AvgReads { get; set; }

// Baseline period
public long BaselineExecutionCount { get; set; }
public double BaselineAvgDurationMs { get; set; }
public double BaselineAvgCpuMs { get; set; }
public double BaselineAvgReads { get; set; }

// Flags
public bool IsNew => ExecutionCount > 0 && BaselineExecutionCount == 0;
public bool IsGone => ExecutionCount == 0 && BaselineExecutionCount > 0;

// Delta percentages (null when baseline is zero or item is new/gone)
public double? DurationDeltaPct => ComputeDelta(AvgDurationMs, BaselineAvgDurationMs);
public double? CpuDeltaPct => ComputeDelta(AvgCpuMs, BaselineAvgCpuMs);
public double? ReadsDeltaPct => ComputeDelta(AvgReads, BaselineAvgReads);
public double? ExecutionDeltaPct => ComputeDelta(ExecutionCount, BaselineExecutionCount);

// Display helpers for grid binding
public string DurationDeltaDisplay => FormatDelta(DurationDeltaPct);
public string CpuDeltaDisplay => FormatDelta(CpuDeltaPct);
public string ReadsDeltaDisplay => FormatDelta(ReadsDeltaPct);
public string ExecutionDeltaDisplay => FormatDelta(ExecutionDeltaPct);

public string StatusBadge => IsNew ? "NEW" : IsGone ? "GONE" : "";

// Sort key: NEW at top (0), normal by delta (1), GONE at bottom (2)
public int SortGroup => IsNew ? 0 : IsGone ? 2 : 1;
public double SortableDurationDelta => DurationDeltaPct ?? (IsNew ? double.MaxValue : double.MinValue);

// Color brushes for delta columns (red = regression, green = improvement)
public Brush DurationDeltaBrush => GetDeltaBrush(DurationDeltaPct, 25);
public Brush CpuDeltaBrush => GetDeltaBrush(CpuDeltaPct, 25);
public Brush ReadsDeltaBrush => GetDeltaBrush(ReadsDeltaPct, 50, 25);
public Brush ExecutionDeltaBrush => GetDeltaBrush(ExecutionDeltaPct, 100, 50);

private static readonly Brush RedBrush = new SolidColorBrush(Color.FromRgb(0xFF, 0x6B, 0x6B));
private static readonly Brush GreenBrush = new SolidColorBrush(Color.FromRgb(0x4E, 0xC9, 0xB0));
private static readonly Brush NeutralBrush = Brushes.Transparent;

static ComparisonItemBase()
{
RedBrush.Freeze();
GreenBrush.Freeze();
}

private static Brush GetDeltaBrush(double? delta, double redThreshold, double greenThreshold = 25)
{
if (!delta.HasValue) return NeutralBrush;
if (delta.Value > redThreshold) return RedBrush;
if (delta.Value < -greenThreshold) return GreenBrush;
return NeutralBrush;
}

private static double? ComputeDelta(double current, double baseline)
{
if (baseline == 0) return null;
return (current - baseline) / baseline * 100.0;
}

private static string FormatDelta(double? delta)
{
if (!delta.HasValue) return "\u2014";
var sign = delta.Value >= 0 ? "+" : "";
return $"{sign}{delta.Value:N1}%";
}
}
}
9 changes: 9 additions & 0 deletions Dashboard/Models/ProcedureStatsComparisonItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace PerformanceMonitorDashboard.Models
{
public class ProcedureStatsComparisonItem : ComparisonItemBase
{
public string SchemaName { get; set; } = "";
public string ObjectName { get; set; } = "";
public string FullName => string.IsNullOrEmpty(SchemaName) ? ObjectName : $"{SchemaName}.{ObjectName}";
}
}
11 changes: 11 additions & 0 deletions Dashboard/Models/QueryStatsComparisonItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace PerformanceMonitorDashboard.Models
{
public class QueryStatsComparisonItem : ComparisonItemBase
{
public string QueryHash { get; set; } = "";
public string? ObjectName { get; set; }
public string? SchemaName { get; set; }
public string? ObjectType { get; set; }
public string QueryText { get; set; } = "";
}
}
10 changes: 10 additions & 0 deletions Dashboard/ServerTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@
<ToggleButton x:Name="AutoRefreshToggle" Content="Auto-Refresh: Off" Click="AutoRefreshToggle_Click" Margin="8,0,2,0" Padding="12,5" MinWidth="130" ToolTip="Toggle auto-refresh on/off. Configure interval in Settings."/>
<Button Content="Edit Schedules" Click="EditSchedules_Click" Margin="8,0,2,0" Padding="12,5" Style="{DynamicResource AccentButton}" ToolTip="Edit collector schedules (frequency, retention, enabled/disabled)"/>
<TextBlock Text="|" VerticalAlignment="Center" Margin="8,0,4,0" Foreground="{DynamicResource ForegroundBrush}"/>
<TextBlock Text="Compare:" VerticalAlignment="Center" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}"/>
<ComboBox x:Name="CompareToCombo" SelectedIndex="0" Width="130" Margin="0,0,8,0"
SelectionChanged="CompareToCombo_SelectionChanged"
ToolTip="Compare current period against a baseline">
<ComboBoxItem Content="None"/>
<ComboBoxItem Content="Yesterday"/>
<ComboBoxItem Content="Last week"/>
<ComboBoxItem Content="Same day last week"/>
</ComboBox>
<TextBlock Text="|" VerticalAlignment="Center" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}"/>
<TextBlock Text="Times:" VerticalAlignment="Center" Margin="0,0,4,0" Foreground="{DynamicResource ForegroundBrush}"/>
<ComboBox x:Name="TimeDisplayModeBox" Width="120" SelectionChanged="TimeDisplayMode_SelectionChanged" ToolTip="How timestamps are displayed across all tabs">
<ComboBoxItem Content="Server Time" Tag="ServerTime"/>
Expand Down
Loading
Loading