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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- User's locale used for date/time formatting in WPF bindings ([#459])
- XML processing instructions stripped from sql_command/sql_text display
- Parameterized queries in blocking/deadlock alert filtering
- **Lite UI responsiveness overhaul** — visible-tab-only refresh, sub-tab awareness, Query Store collector optimization (NULL plan XML + LOOP JOIN hint), and DuckDB write reduction ([#510])

Timer tick improvements measured under TPC-C load on SQL2022:

| Scenario | Before | After | Improvement |
|---|---|---|---|
| Lite idle | 6-13s | 546-750ms | ~90% |
| Lite under TPC-C | 6-13s | ~3s | ~70% |
| Dashboard idle | 5.6s | 0.6-0.8s | 86% |
| Dashboard under TPC-C | 5.6s | 1.8-2.0s | 64% |

Query Store collector specifically:

| Metric | Before | After |
|---|---|---|
| query_store collector total | 6-18s | ~600ms |
| query_store SQL time | 374-1,104ms | ~300ms (LOOP JOIN hint) |
| query_store DuckDB write | 6-16s | ~75-230ms (NULL plan XML) |

### Fixed

Expand All @@ -62,6 +80,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Deadlock filter** using wrong column reference in `GetFilteredDeadlockCountAsync`
- **RESTORING database** filter added to waiting_tasks collector ([#430])
- Custom TrayToolTip crash — replaced with plain ToolTipText ([#422])
- **Lite tab switch freeze** — added `_isRefreshing` guard to prevent tab switch handler from competing with timer ticks for DuckDB connection, eliminating "not responding" hangs ([#510])
- DuckDB read lock acquisition resilience
- Formatted duration columns sorting alphabetically instead of numerically
- Settings window staying open on validation errors
Expand Down Expand Up @@ -463,3 +482,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#482]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/482
[#488]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/488
[#489]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/489
[#510]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/510
2 changes: 1 addition & 1 deletion Dashboard/ServerTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@
</Border>

<!-- Tab Control -->
<TabControl Grid.Row="1" x:Name="DataTabControl" Margin="10,0,10,0">
<TabControl Grid.Row="1" x:Name="DataTabControl" Margin="10,0,10,0" SelectionChanged="DataTabControl_SelectionChanged">
<!-- Overview Tab - At-a-glance health and status -->
<TabItem Header="Overview">
<TabControl>
Expand Down
242 changes: 209 additions & 33 deletions Dashboard/ServerTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ private void SetupAutoRefresh()

try
{
await LoadDataAsync();
await LoadDataAsync(fullRefresh: false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -396,7 +396,7 @@ public void RefreshAutoRefreshSettings()

try
{
await LoadDataAsync();
await LoadDataAsync(fullRefresh: false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -445,7 +445,7 @@ private void AutoRefreshToggle_Click(object sender, RoutedEventArgs e)

try
{
await LoadDataAsync();
await LoadDataAsync(fullRefresh: false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -1081,7 +1081,12 @@ private async Task ApplyAndRefreshCurrentTabAsync()
}
}

private async Task LoadDataAsync()
/// <summary>
/// Loads data for the Dashboard. When fullRefresh is true (first load, manual refresh,
/// Apply to All), all tabs are refreshed in parallel. When false (auto-refresh timer tick),
/// only the currently visible tab is refreshed to reduce SQL Server load.
/// </summary>
private async Task LoadDataAsync(bool fullRefresh = true)
{
using var _ = Helpers.MethodProfiler.StartTiming("ServerTab");
try
Expand All @@ -1104,42 +1109,185 @@ private async Task LoadDataAsync()

StatusText.Text = GetLoadingMessage();

// Fetch all data in parallel — overview queries + all tab refreshes
if (fullRefresh)
{
// Full refresh: query all tabs in parallel (first load, manual refresh, Apply to All)
await RefreshAllTabsAsync();
}
else
{
// Timer tick: only refresh the currently visible tab
await RefreshVisibleTabAsync();
}

StatusText.Text = "Ready";
FooterText.Text = $"Last refresh: {DateTime.Now:yyyy-MM-dd HH:mm:ss} | Server: {_serverConnection.DisplayName}";
}
catch (Exception ex)
{
StatusText.Text = "Error loading data";
MessageBox.Show(
$"Error loading data:\n\n{ex.Message}",
"Error",
MessageBoxButton.OK,
MessageBoxImage.Error
);
}
finally
{
RefreshButton.IsEnabled = true;
}
}

// ====================================================================
// Per-Tab Refresh Methods
// ====================================================================

/// <summary>
/// Refreshes all tabs in parallel — used on first load, manual refresh, and Apply to All.
/// </summary>
private async Task RefreshAllTabsAsync()
{
var overviewTask = RefreshOverviewTabAsync();
var queriesTask = RefreshQueriesTabAsync();
var resourceMetricsTask = RefreshResourceMetricsTabAsync();
var memoryTask = RefreshMemoryTabAsync();
var lockingTask = RefreshLockingTabAsync();
var systemEventsTask = RefreshSystemEventsTabAsync();

await Task.WhenAll(overviewTask, queriesTask, resourceMetricsTask, memoryTask, lockingTask, systemEventsTask);
}

/// <summary>
/// Refreshes only the currently visible tab — used on auto-refresh timer tick.
/// </summary>
private async Task RefreshVisibleTabAsync()
{
var selectedTab = DataTabControl.SelectedItem as TabItem;
if (selectedTab == null) return;

var tabHeader = GetTabHeaderText(selectedTab);

switch (tabHeader)
{
case "Overview":
await RefreshOverviewTabAsync();
break;
case "Queries":
await RefreshQueriesTabAsync();
break;
case "Resource Metrics":
await RefreshResourceMetricsTabAsync();
break;
case "Memory":
await RefreshMemoryTabAsync();
break;
case "Locking":
await RefreshLockingTabAsync();
break;
case "System Events":
await RefreshSystemEventsTabAsync();
break;
// Plan Viewer has no data to refresh
}
}

/// <summary>
/// Refreshes the Overview tab: Collection Health, Duration Trends, Daily Summary,
/// Critical Issues, Default Trace, Current Config, Config Changes, Resource Overview, Running Jobs.
/// </summary>
private async Task RefreshOverviewTabAsync()
{
try
{
var healthTask = _databaseService.GetCollectionHealthAsync();
var durationLogsTask = _databaseService.GetCollectionDurationLogsAsync();
var blockingEventsTask = _databaseService.GetBlockingEventsAsync();
var deadlocksTask = _databaseService.GetDeadlocksAsync();
var blockingStatsTask = _databaseService.GetBlockingDeadlockStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var lockWaitStatsTask = _databaseService.GetLockWaitStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var currentWaitsDurationTask = _databaseService.GetWaitingTaskTrendAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var currentWaitsBlockedTask = _databaseService.GetBlockedSessionTrendAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);

var performanceTask = PerformanceTab.RefreshAllDataAsync();
var memoryTask = MemoryTab.RefreshAllDataAsync();
var resourceOverviewTask = RefreshResourceOverviewAsync();
var runningJobsTask = RefreshRunningJobsAsync();
var resourceMetricsTask = ResourceMetricsContent.RefreshAllDataAsync();
var dailySummaryTask = DailySummaryTab.RefreshDataAsync();
var criticalIssuesTask = CriticalIssuesTab.RefreshDataAsync();
var defaultTraceTask = DefaultTraceTab.RefreshAllDataAsync();
var currentConfigTask = CurrentConfigTab.RefreshAllDataAsync();
var configChangesTask = ConfigChangesTab.RefreshAllDataAsync();
var systemEventsTask = SystemEventsContent.RefreshAllDataAsync();

// Wait for everything to complete before _isRefreshing resets
await Task.WhenAll(
healthTask, durationLogsTask, blockingEventsTask, deadlocksTask, blockingStatsTask, lockWaitStatsTask, currentWaitsDurationTask, currentWaitsBlockedTask,
performanceTask, memoryTask, resourceOverviewTask, runningJobsTask,
resourceMetricsTask, dailySummaryTask, criticalIssuesTask, defaultTraceTask, currentConfigTask, configChangesTask, systemEventsTask);
await Task.WhenAll(healthTask, durationLogsTask, resourceOverviewTask, runningJobsTask,
dailySummaryTask, criticalIssuesTask, defaultTraceTask, currentConfigTask, configChangesTask);

// Populate grids with fetched data
var healthData = await healthTask;
HealthDataGrid.ItemsSource = healthData;
UpdateDataGridFilterButtonStyles(HealthDataGrid, _collectionHealthFilters);
HealthNoDataMessage.Visibility = healthData.Count == 0 ? Visibility.Visible : Visibility.Collapsed;

var durationLogs = await durationLogsTask;
UpdateCollectorDurationChart(durationLogs);
}
catch (Exception ex)
{
Logger.Error($"Error refreshing Overview tab: {ex.Message}", ex);
}
}

/// <summary>
/// Refreshes the Queries tab (delegated to QueryPerformanceContent UserControl).
/// </summary>
private async Task RefreshQueriesTabAsync()
{
try
{
await PerformanceTab.RefreshAllDataAsync();
}
catch (Exception ex)
{
Logger.Error($"Error refreshing Queries tab: {ex.Message}", ex);
}
}

/// <summary>
/// Refreshes the Resource Metrics tab (delegated to ResourceMetricsContent UserControl).
/// </summary>
private async Task RefreshResourceMetricsTabAsync()
{
try
{
await ResourceMetricsContent.RefreshAllDataAsync();
}
catch (Exception ex)
{
Logger.Error($"Error refreshing Resource Metrics tab: {ex.Message}", ex);
}
}

/// <summary>
/// Refreshes the Memory tab (delegated to MemoryContent UserControl).
/// </summary>
private async Task RefreshMemoryTabAsync()
{
try
{
await MemoryTab.RefreshAllDataAsync();
}
catch (Exception ex)
{
Logger.Error($"Error refreshing Memory tab: {ex.Message}", ex);
}
}

/// <summary>
/// Refreshes the Locking tab: Blocking events, deadlocks, blocking/deadlock stats,
/// lock wait stats, current waits duration, and current waits blocked sessions.
/// </summary>
private async Task RefreshLockingTabAsync()
{
try
{
var blockingEventsTask = _databaseService.GetBlockingEventsAsync();
var deadlocksTask = _databaseService.GetDeadlocksAsync();
var blockingStatsTask = _databaseService.GetBlockingDeadlockStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var lockWaitStatsTask = _databaseService.GetLockWaitStatsAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var currentWaitsDurationTask = _databaseService.GetWaitingTaskTrendAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);
var currentWaitsBlockedTask = _databaseService.GetBlockedSessionTrendAsync(_blockingStatsHoursBack, _blockingStatsFromDate, _blockingStatsToDate);

await Task.WhenAll(blockingEventsTask, deadlocksTask, blockingStatsTask, lockWaitStatsTask, currentWaitsDurationTask, currentWaitsBlockedTask);

try
{
Expand Down Expand Up @@ -1180,27 +1328,55 @@ await Task.WhenAll(
{
Logger.Warning($"Could not load blocking/deadlock stats: {blockingStatsEx.Message}");
}
}
catch (Exception ex)
{
Logger.Error($"Error refreshing Locking tab: {ex.Message}", ex);
}
}

/// <summary>
/// Refreshes the System Events tab (delegated to SystemEventsContent UserControl).
/// </summary>
private async Task RefreshSystemEventsTabAsync()
{
try
{
await SystemEventsContent.RefreshAllDataAsync();
}
catch (Exception ex)
{
Logger.Error($"Error refreshing System Events tab: {ex.Message}", ex);
}
}

int failing = healthData.Count(h => h.HealthStatus == "FAILING");
int stale = healthData.Count(h => h.HealthStatus == "STALE");
int healthy = healthData.Count(h => h.HealthStatus == "HEALTHY");
/// <summary>
/// Handles the main TabControl's SelectionChanged event to refresh the newly
/// visible tab with current data. Guards against bubbling from nested TabControls.
/// </summary>
private async void DataTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Only handle events from the main DataTabControl, not from nested sub-tab controls
if (e.Source != DataTabControl) return;

// Don't refresh during initial load or if already refreshing
if (_isRefreshing || !IsLoaded) return;

_isRefreshing = true;
try
{
await RefreshVisibleTabAsync();
StatusText.Text = "Ready";
FooterText.Text = $"Last refresh: {DateTime.Now:yyyy-MM-dd HH:mm:ss} | Server: {_serverConnection.DisplayName}";
}
catch (Exception ex)
{
StatusText.Text = "Error loading data";
MessageBox.Show(
$"Error loading data:\n\n{ex.Message}",
"Error",
MessageBoxButton.OK,
MessageBoxImage.Error
);
Logger.Error($"Error refreshing on tab switch: {ex.Message}", ex);
StatusText.Text = "Error refreshing data";
}
finally
{
RefreshButton.IsEnabled = true;
_isRefreshing = false;
}
}

Expand Down
8 changes: 4 additions & 4 deletions Lite/Controls/ServerTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@

<!-- Main Tab Control -->
<TabControl Grid.Row="1" x:Name="MainTabControl" Background="{DynamicResource BackgroundBrush}"
BorderThickness="0" Padding="0">
BorderThickness="0" Padding="0" SelectionChanged="MainTabControl_SelectionChanged">

<!-- Wait Stats Tab -->
<TabItem Header="Wait Stats">
Expand Down Expand Up @@ -179,7 +179,7 @@
<!-- Query Performance Tab -->
<TabItem Header="Queries">
<Grid Margin="8">
<TabControl Background="Transparent" BorderThickness="0">
<TabControl x:Name="QueriesSubTabControl" Background="Transparent" BorderThickness="0">
<TabItem Header="Performance Trends">
<Grid>
<Grid.RowDefinitions>
Expand Down Expand Up @@ -697,7 +697,7 @@

<!-- Memory Tab -->
<TabItem Header="Memory">
<TabControl Background="Transparent" BorderThickness="0">
<TabControl x:Name="MemorySubTabControl" Background="Transparent" BorderThickness="0">
<!-- Memory Overview Sub-Tab -->
<TabItem Header="Overview">
<Grid Margin="8">
Expand Down Expand Up @@ -890,7 +890,7 @@
<!-- Blocking Tab -->
<TabItem Header="Blocking">
<Grid Margin="8">
<TabControl Background="Transparent" BorderThickness="0">
<TabControl x:Name="BlockingSubTabControl" Background="Transparent" BorderThickness="0">
<TabItem Header="Trends">
<Grid>
<Grid.RowDefinitions>
Expand Down
Loading
Loading