diff --git a/CHANGELOG.md b/CHANGELOG.md index f73ee6d8..b78dab5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 @@ -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 diff --git a/Dashboard/ServerTab.xaml b/Dashboard/ServerTab.xaml index a7ccd608..0c630634 100644 --- a/Dashboard/ServerTab.xaml +++ b/Dashboard/ServerTab.xaml @@ -143,7 +143,7 @@ - + diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index d1dd898d..27eb14c8 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -325,7 +325,7 @@ private void SetupAutoRefresh() try { - await LoadDataAsync(); + await LoadDataAsync(fullRefresh: false); } catch (Exception ex) { @@ -396,7 +396,7 @@ public void RefreshAutoRefreshSettings() try { - await LoadDataAsync(); + await LoadDataAsync(fullRefresh: false); } catch (Exception ex) { @@ -445,7 +445,7 @@ private void AutoRefreshToggle_Click(object sender, RoutedEventArgs e) try { - await LoadDataAsync(); + await LoadDataAsync(fullRefresh: false); } catch (Exception ex) { @@ -1081,7 +1081,12 @@ private async Task ApplyAndRefreshCurrentTabAsync() } } - private async Task LoadDataAsync() + /// + /// 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. + /// + private async Task LoadDataAsync(bool fullRefresh = true) { using var _ = Helpers.MethodProfiler.StartTiming("ServerTab"); try @@ -1104,35 +1109,110 @@ 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 + // ==================================================================== + + /// + /// Refreshes all tabs in parallel — used on first load, manual refresh, and Apply to All. + /// + 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); + } + + /// + /// Refreshes only the currently visible tab — used on auto-refresh timer tick. + /// + 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 + } + } + + /// + /// Refreshes the Overview tab: Collection Health, Duration Trends, Daily Summary, + /// Critical Issues, Default Trace, Current Config, Config Changes, Resource Overview, Running Jobs. + /// + 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); @@ -1140,6 +1220,74 @@ await Task.WhenAll( var durationLogs = await durationLogsTask; UpdateCollectorDurationChart(durationLogs); + } + catch (Exception ex) + { + Logger.Error($"Error refreshing Overview tab: {ex.Message}", ex); + } + } + + /// + /// Refreshes the Queries tab (delegated to QueryPerformanceContent UserControl). + /// + private async Task RefreshQueriesTabAsync() + { + try + { + await PerformanceTab.RefreshAllDataAsync(); + } + catch (Exception ex) + { + Logger.Error($"Error refreshing Queries tab: {ex.Message}", ex); + } + } + + /// + /// Refreshes the Resource Metrics tab (delegated to ResourceMetricsContent UserControl). + /// + private async Task RefreshResourceMetricsTabAsync() + { + try + { + await ResourceMetricsContent.RefreshAllDataAsync(); + } + catch (Exception ex) + { + Logger.Error($"Error refreshing Resource Metrics tab: {ex.Message}", ex); + } + } + + /// + /// Refreshes the Memory tab (delegated to MemoryContent UserControl). + /// + private async Task RefreshMemoryTabAsync() + { + try + { + await MemoryTab.RefreshAllDataAsync(); + } + catch (Exception ex) + { + Logger.Error($"Error refreshing Memory tab: {ex.Message}", ex); + } + } + + /// + /// Refreshes the Locking tab: Blocking events, deadlocks, blocking/deadlock stats, + /// lock wait stats, current waits duration, and current waits blocked sessions. + /// + 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 { @@ -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); + } + } + + /// + /// Refreshes the System Events tab (delegated to SystemEventsContent UserControl). + /// + 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"); + /// + /// Handles the main TabControl's SelectionChanged event to refresh the newly + /// visible tab with current data. Guards against bubbling from nested TabControls. + /// + 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; } } diff --git a/Lite/Controls/ServerTab.xaml b/Lite/Controls/ServerTab.xaml index 8acf02d9..35b41532 100644 --- a/Lite/Controls/ServerTab.xaml +++ b/Lite/Controls/ServerTab.xaml @@ -119,7 +119,7 @@ + BorderThickness="0" Padding="0" SelectionChanged="MainTabControl_SelectionChanged"> @@ -179,7 +179,7 @@ - + @@ -697,7 +697,7 @@ - + @@ -890,7 +890,7 @@ - + diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index ecf4308a..5e9a6148 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -38,6 +38,7 @@ public partial class ServerTab : UserControl public int ServerId => _serverId; private readonly CredentialService _credentialService; private readonly DispatcherTimer _refreshTimer; + private bool _isRefreshing; private readonly Dictionary _legendPanels = new(); private List _waitTypeItems = new(); private List _perfmonCounterItems = new(); @@ -140,7 +141,7 @@ public ServerTab(ServerConnection server, DuckDbInitializer duckDb, CredentialSe { Interval = TimeSpan.FromSeconds(60) }; - _refreshTimer.Tick += async (s, e) => await RefreshAllDataAsync(); + _refreshTimer.Tick += async (s, e) => await RefreshAllDataAsync(fullRefresh: false); _refreshTimer.Start(); /* Initialize time picker ComboBoxes */ @@ -379,7 +380,8 @@ private async void RefreshDataButton_Click(object sender, RoutedEventArgs e) { await ManualRefreshRequested.Invoke(); } - await RefreshAllDataAsync(); + /* Manual refresh loads all sub-tabs of the visible tab, not all 13 tabs */ + await RefreshAllDataAsync(fullRefresh: false); } finally { @@ -414,7 +416,7 @@ private async void TimeRangeCombo_SelectionChanged(object sender, SelectionChang if (!isCustom) { - await RefreshAllDataAsync(); + await RefreshAllDataAsync(fullRefresh: false); } } @@ -423,7 +425,7 @@ private async void CustomDateRange_Changed(object sender, SelectionChangedEventA if (!IsLoaded) return; if (FromDatePicker?.SelectedDate != null && ToDatePicker?.SelectedDate != null) { - await RefreshAllDataAsync(); + await RefreshAllDataAsync(fullRefresh: false); } } @@ -433,7 +435,7 @@ private async void CustomTimeCombo_Changed(object sender, SelectionChangedEventA /* Only refresh if we have valid dates selected */ if (FromDatePicker?.SelectedDate != null && ToDatePicker?.SelectedDate != null) { - await RefreshAllDataAsync(); + await RefreshAllDataAsync(fullRefresh: false); } } @@ -535,14 +537,18 @@ private void ApplyThemeRecursively(DependencyObject parent, Brush primaryBg, Bru /// /// Public entry point to trigger a data refresh from outside. + /// Loads only the visible tab — other tabs load on demand when clicked. /// public async void RefreshData() { - await RefreshAllDataAsync(); + await RefreshAllDataAsync(fullRefresh: false); } - private async System.Threading.Tasks.Task RefreshAllDataAsync() + private async System.Threading.Tasks.Task RefreshAllDataAsync(bool fullRefresh = false) { + if (_isRefreshing) return; + _isRefreshing = true; + var hoursBack = GetHoursBack(); /* Get custom date range if selected, converting local picker dates/times to server time */ @@ -562,134 +568,476 @@ private async System.Threading.Tasks.Task RefreshAllDataAsync() try { using var _profiler = Helpers.MethodProfiler.StartTiming($"ServerTab-{_server?.DisplayName}"); - var loadSw = Stopwatch.StartNew(); - /* Load all tabs in parallel */ + if (fullRefresh) + { + await RefreshAllTabsAsync(hoursBack, fromDate, toDate); + } + else + { + await RefreshVisibleTabAsync(hoursBack, fromDate, toDate, subTabOnly: true); + /* Always keep alert badge current even when Blocking tab is not visible */ + if (MainTabControl.SelectedIndex != 7) + await RefreshAlertCountsAsync(hoursBack, fromDate, toDate); + } + + var tz = ServerTimeHelper.GetTimezoneLabel(ServerTimeHelper.CurrentDisplayMode); + ConnectionStatusText.Text = $"{_server.ServerName} - Last refresh: {DateTime.Now:HH:mm:ss} ({tz})"; + } + catch (Exception ex) + { + ConnectionStatusText.Text = $"Error: {ex.Message}"; + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshAllDataAsync failed: {ex}"); + } + finally + { + _isRefreshing = false; + } + } + + private async System.Threading.Tasks.Task RefreshVisibleTabAsync(int hoursBack, DateTime? fromDate, DateTime? toDate, bool subTabOnly = false) + { + switch (MainTabControl.SelectedIndex) + { + case 0: await RefreshWaitStatsAsync(hoursBack, fromDate, toDate); break; + case 1: await RefreshQueriesAsync(hoursBack, fromDate, toDate, subTabOnly); break; + case 2: break; // Plan Viewer — no queries + case 3: await RefreshCpuAsync(hoursBack, fromDate, toDate); break; + case 4: await RefreshMemoryAsync(hoursBack, fromDate, toDate, subTabOnly); break; + case 5: await RefreshFileIoAsync(hoursBack, fromDate, toDate); break; + case 6: await RefreshTempDbAsync(hoursBack, fromDate, toDate); break; + case 7: await RefreshBlockingAsync(hoursBack, fromDate, toDate, subTabOnly); break; + case 8: await RefreshPerfmonAsync(hoursBack, fromDate, toDate); break; + case 9: await RefreshRunningJobsAsync(hoursBack, fromDate, toDate); break; + case 10: await RefreshConfigurationAsync(hoursBack, fromDate, toDate); break; + case 11: await RefreshDailySummaryAsync(hoursBack, fromDate, toDate); break; + case 12: await RefreshCollectionHealthAsync(hoursBack, fromDate, toDate); break; + } + } + + /// + /// Lightweight alert-only refresh — fetches blocking + deadlock counts and fires AlertCountsChanged. + /// Runs on every timer tick when the Blocking tab is NOT visible so the tab badge stays current. + /// + private async System.Threading.Tasks.Task RefreshAlertCountsAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var (blockingCount, deadlockCount, latestEventTime) = await _dataService.GetAlertCountsAsync(_serverId, hoursBack, fromDate, toDate); + AlertCountsChanged?.Invoke(blockingCount, deadlockCount, latestEventTime); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshAlertCountsAsync failed: {ex.Message}"); + } + } + + /// + /// Full refresh of all tabs — used for first load, manual refresh, and time range changes. + /// + private async System.Threading.Tasks.Task RefreshAllTabsAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + var loadSw = Stopwatch.StartNew(); + + /* Load all tabs in parallel */ + var snapshotsTask = _dataService.GetLatestQuerySnapshotsAsync(_serverId, hoursBack, fromDate, toDate); + var cpuTask = _dataService.GetCpuUtilizationAsync(_serverId, hoursBack, fromDate, toDate); + var memoryTask = _dataService.GetLatestMemoryStatsAsync(_serverId); + var memoryTrendTask = _dataService.GetMemoryTrendAsync(_serverId, hoursBack, fromDate, toDate); + var queryStatsTask = _dataService.GetTopQueriesByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes); + var procStatsTask = _dataService.GetTopProceduresByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes); + var fileIoTrendTask = _dataService.GetFileIoLatencyTrendAsync(_serverId, hoursBack, fromDate, toDate); + var fileIoThroughputTask = _dataService.GetFileIoThroughputTrendAsync(_serverId, hoursBack, fromDate, toDate); + var tempDbTask = _dataService.GetTempDbTrendAsync(_serverId, hoursBack, fromDate, toDate); + var tempDbFileIoTask = _dataService.GetTempDbFileIoTrendAsync(_serverId, hoursBack, fromDate, toDate); + var deadlockTask = _dataService.GetRecentDeadlocksAsync(_serverId, hoursBack, fromDate, toDate); + var blockedProcessTask = _dataService.GetRecentBlockedProcessReportsAsync(_serverId, hoursBack, fromDate, toDate); + var waitTypesTask = _dataService.GetDistinctWaitTypesAsync(_serverId, hoursBack, fromDate, toDate); + var memoryClerkTypesTask = _dataService.GetDistinctMemoryClerkTypesAsync(_serverId, hoursBack, fromDate, toDate); + var perfmonCountersTask = _dataService.GetDistinctPerfmonCountersAsync(_serverId, hoursBack, fromDate, toDate); + var queryStoreTask = _dataService.GetQueryStoreTopQueriesAsync(_serverId, hoursBack, 50, fromDate, toDate); + var memoryGrantTrendTask = _dataService.GetMemoryGrantTrendAsync(_serverId, hoursBack, fromDate, toDate); + var memoryGrantChartTask = _dataService.GetMemoryGrantChartDataAsync(_serverId, hoursBack, fromDate, toDate); + var serverConfigTask = SafeQueryAsync(() => _dataService.GetLatestServerConfigAsync(_serverId)); + var databaseConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseConfigAsync(_serverId)); + var databaseScopedConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseScopedConfigAsync(_serverId)); + var traceFlagsTask = SafeQueryAsync(() => _dataService.GetLatestTraceFlagsAsync(_serverId)); + var runningJobsTask = SafeQueryAsync(() => _dataService.GetRunningJobsAsync(_serverId)); + var collectionHealthTask = SafeQueryAsync(() => _dataService.GetCollectionHealthAsync(_serverId)); + var collectionLogTask = SafeQueryAsync(() => _dataService.GetRecentCollectionLogAsync(_serverId, hoursBack)); + var dailySummaryTask = _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate); + /* Core data tasks */ + await System.Threading.Tasks.Task.WhenAll( + snapshotsTask, cpuTask, memoryTask, memoryTrendTask, + queryStatsTask, procStatsTask, fileIoTrendTask, fileIoThroughputTask, tempDbTask, tempDbFileIoTask, + deadlockTask, blockedProcessTask, waitTypesTask, memoryClerkTypesTask, perfmonCountersTask, + queryStoreTask, memoryGrantTrendTask, memoryGrantChartTask, + serverConfigTask, databaseConfigTask, databaseScopedConfigTask, traceFlagsTask, + runningJobsTask, collectionHealthTask, collectionLogTask, dailySummaryTask); + + /* Trend chart tasks - run separately so failures don't kill the whole refresh */ + var lockWaitTrendTask = SafeQueryAsync(() => _dataService.GetLockWaitTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var blockingTrendTask = SafeQueryAsync(() => _dataService.GetBlockingTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var deadlockTrendTask = SafeQueryAsync(() => _dataService.GetDeadlockTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var queryDurationTrendTask = SafeQueryAsync(() => _dataService.GetQueryDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var procDurationTrendTask = SafeQueryAsync(() => _dataService.GetProcedureDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var queryStoreDurationTrendTask = SafeQueryAsync(() => _dataService.GetQueryStoreDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var executionCountTrendTask = SafeQueryAsync(() => _dataService.GetExecutionCountTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var currentWaitsDurationTask = SafeQueryAsync(() => _dataService.GetWaitingTaskTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var currentWaitsBlockedTask = SafeQueryAsync(() => _dataService.GetBlockedSessionTrendAsync(_serverId, hoursBack, fromDate, toDate)); + + await System.Threading.Tasks.Task.WhenAll( + lockWaitTrendTask, blockingTrendTask, deadlockTrendTask, + queryDurationTrendTask, procDurationTrendTask, queryStoreDurationTrendTask, executionCountTrendTask, + currentWaitsDurationTask, currentWaitsBlockedTask); + + loadSw.Stop(); + + /* Log data counts and timing for diagnostics */ + AppLogger.DataDiag("ServerTab", $"[{_server.DisplayName}] serverId={_serverId} hoursBack={hoursBack} dataLoad={loadSw.ElapsedMilliseconds}ms"); + AppLogger.DataDiag("ServerTab", $" Snapshots: {snapshotsTask.Result.Count}, CPU: {cpuTask.Result.Count}"); + AppLogger.DataDiag("ServerTab", $" Memory: {(memoryTask.Result != null ? "1" : "null")}, MemoryTrend: {memoryTrendTask.Result.Count}"); + AppLogger.DataDiag("ServerTab", $" QueryStats: {queryStatsTask.Result.Count}, ProcStats: {procStatsTask.Result.Count}"); + AppLogger.DataDiag("ServerTab", $" FileIoTrend: {fileIoTrendTask.Result.Count}"); + AppLogger.DataDiag("ServerTab", $" TempDb: {tempDbTask.Result.Count}, BlockedProcessReports: {blockedProcessTask.Result.Count}, Deadlocks: {deadlockTask.Result.Count}"); + AppLogger.DataDiag("ServerTab", $" WaitTypes: {waitTypesTask.Result.Count}, PerfmonCounters: {perfmonCountersTask.Result.Count}, QueryStore: {queryStoreTask.Result.Count}"); + + /* Update grids (via filter managers to preserve active filters) */ + _querySnapshotsFilterMgr!.UpdateData(snapshotsTask.Result); + LiveSnapshotIndicator.Text = ""; + _queryStatsFilterMgr!.UpdateData(queryStatsTask.Result); + SetInitialSort(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + _procStatsFilterMgr!.UpdateData(procStatsTask.Result); + SetInitialSort(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + _blockedProcessFilterMgr!.UpdateData(blockedProcessTask.Result); + _deadlockFilterMgr!.UpdateData(DeadlockProcessDetail.ParseFromRows(deadlockTask.Result)); + _queryStoreFilterMgr!.UpdateData(queryStoreTask.Result); + SetInitialSort(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); + _serverConfigFilterMgr!.UpdateData(serverConfigTask.Result); + _databaseConfigFilterMgr!.UpdateData(databaseConfigTask.Result); + _dbScopedConfigFilterMgr!.UpdateData(databaseScopedConfigTask.Result); + _traceFlagsFilterMgr!.UpdateData(traceFlagsTask.Result); + _runningJobsFilterMgr!.UpdateData(runningJobsTask.Result); + _collectionHealthFilterMgr!.UpdateData(collectionHealthTask.Result); + _collectionLogFilterMgr!.UpdateData(collectionLogTask.Result); + var dailySummary = await dailySummaryTask; + DailySummaryGrid.ItemsSource = dailySummary != null + ? new List { dailySummary } : null; + DailySummaryNoData.Visibility = dailySummary == null + ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed; + UpdateCollectorDurationChart(collectionLogTask.Result); + + /* Update memory summary */ + UpdateMemorySummary(memoryTask.Result); + + /* Update charts */ + UpdateCpuChart(cpuTask.Result); + UpdateMemoryChart(memoryTrendTask.Result, memoryGrantTrendTask.Result); + UpdateTempDbChart(tempDbTask.Result); + UpdateTempDbFileIoChart(tempDbFileIoTask.Result); + UpdateFileIoCharts(fileIoTrendTask.Result); + UpdateFileIoThroughputCharts(fileIoThroughputTask.Result); + UpdateLockWaitTrendChart(lockWaitTrendTask.Result, hoursBack, fromDate, toDate); + UpdateBlockingTrendChart(blockingTrendTask.Result, hoursBack, fromDate, toDate); + UpdateDeadlockTrendChart(deadlockTrendTask.Result, hoursBack, fromDate, toDate); + UpdateCurrentWaitsDurationChart(currentWaitsDurationTask.Result, hoursBack, fromDate, toDate); + UpdateCurrentWaitsBlockedChart(currentWaitsBlockedTask.Result, hoursBack, fromDate, toDate); + UpdateQueryDurationTrendChart(queryDurationTrendTask.Result); + UpdateProcDurationTrendChart(procDurationTrendTask.Result); + UpdateQueryStoreDurationTrendChart(queryStoreDurationTrendTask.Result); + UpdateExecutionCountTrendChart(executionCountTrendTask.Result); + UpdateMemoryGrantCharts(memoryGrantChartTask.Result); + + /* Populate pickers (preserve selections) */ + PopulateWaitTypePicker(waitTypesTask.Result); + PopulateMemoryClerkPicker(memoryClerkTypesTask.Result); + PopulatePerfmonPicker(perfmonCountersTask.Result); + + /* Update picker-driven charts */ + await UpdateWaitStatsChartFromPickerAsync(); + await UpdateMemoryClerksChartFromPickerAsync(); + await UpdatePerfmonChartFromPickerAsync(); + + /* Notify parent of alert counts for tab badge. + Include the latest event timestamp so acknowledgement is only + cleared when genuinely new events arrive, not when the time range changes. */ + var blockingCount = blockedProcessTask.Result.Count; + var deadlockCount = deadlockTask.Result.Count; + DateTime? latestEventTime = null; + if (blockingCount > 0 || deadlockCount > 0) + { + var latestBlocking = blockedProcessTask.Result.Max(r => (DateTime?)r.EventTime); + var latestDeadlock = deadlockTask.Result.Max(r => (DateTime?)r.DeadlockTime); + latestEventTime = latestBlocking > latestDeadlock ? latestBlocking : latestDeadlock; + } + AlertCountsChanged?.Invoke(blockingCount, deadlockCount, latestEventTime); + } + + /* ───────────────────────────── Per-tab refresh methods ───────────────────────────── */ + + /// Tab 0 — Wait Stats + private async System.Threading.Tasks.Task RefreshWaitStatsAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var waitTypesTask = _dataService.GetDistinctWaitTypesAsync(_serverId, hoursBack, fromDate, toDate); + await waitTypesTask; + PopulateWaitTypePicker(waitTypesTask.Result); + await UpdateWaitStatsChartFromPickerAsync(); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshWaitStatsAsync failed: {ex.Message}"); + } + } + + /// Tab 1 — Queries + private async System.Threading.Tasks.Task RefreshQueriesAsync(int hoursBack, DateTime? fromDate, DateTime? toDate, bool subTabOnly = false) + { + try + { + if (subTabOnly) + { + /* Timer tick: only refresh the visible sub-tab (8 queries → 1-4) */ + switch (QueriesSubTabControl.SelectedIndex) + { + case 0: // Performance Trends — 4 trend charts + var qdt = SafeQueryAsync(() => _dataService.GetQueryDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var pdt = SafeQueryAsync(() => _dataService.GetProcedureDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var qsdt = SafeQueryAsync(() => _dataService.GetQueryStoreDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var ect = SafeQueryAsync(() => _dataService.GetExecutionCountTrendAsync(_serverId, hoursBack, fromDate, toDate)); + await System.Threading.Tasks.Task.WhenAll(qdt, pdt, qsdt, ect); + UpdateQueryDurationTrendChart(qdt.Result); + UpdateProcDurationTrendChart(pdt.Result); + UpdateQueryStoreDurationTrendChart(qsdt.Result); + UpdateExecutionCountTrendChart(ect.Result); + break; + case 1: // Active Queries + var snapshots = await _dataService.GetLatestQuerySnapshotsAsync(_serverId, hoursBack, fromDate, toDate); + _querySnapshotsFilterMgr!.UpdateData(snapshots); + LiveSnapshotIndicator.Text = ""; + break; + case 2: // Top Queries by Duration + var queryStats = await _dataService.GetTopQueriesByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes); + _queryStatsFilterMgr!.UpdateData(queryStats); + SetInitialSort(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + break; + case 3: // Top Procedures by Duration + var procStats = await _dataService.GetTopProceduresByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes); + _procStatsFilterMgr!.UpdateData(procStats); + SetInitialSort(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + break; + case 4: // Query Store by Duration + var qsData = await _dataService.GetQueryStoreTopQueriesAsync(_serverId, hoursBack, 50, fromDate, toDate); + _queryStoreFilterMgr!.UpdateData(qsData); + SetInitialSort(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); + break; + } + return; + } + + /* Full refresh: load all sub-tabs */ var snapshotsTask = _dataService.GetLatestQuerySnapshotsAsync(_serverId, hoursBack, fromDate, toDate); - var cpuTask = _dataService.GetCpuUtilizationAsync(_serverId, hoursBack, fromDate, toDate); - var memoryTask = _dataService.GetLatestMemoryStatsAsync(_serverId); - var memoryTrendTask = _dataService.GetMemoryTrendAsync(_serverId, hoursBack, fromDate, toDate); var queryStatsTask = _dataService.GetTopQueriesByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes); var procStatsTask = _dataService.GetTopProceduresByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes); - var fileIoTask = _dataService.GetLatestFileIoStatsAsync(_serverId); - var fileIoTrendTask = _dataService.GetFileIoLatencyTrendAsync(_serverId, hoursBack, fromDate, toDate); - var fileIoThroughputTask = _dataService.GetFileIoThroughputTrendAsync(_serverId, hoursBack, fromDate, toDate); - var tempDbTask = _dataService.GetTempDbTrendAsync(_serverId, hoursBack, fromDate, toDate); - var tempDbFileIoTask = _dataService.GetTempDbFileIoTrendAsync(_serverId, hoursBack, fromDate, toDate); - var deadlockTask = _dataService.GetRecentDeadlocksAsync(_serverId, hoursBack, fromDate, toDate); - var blockedProcessTask = _dataService.GetRecentBlockedProcessReportsAsync(_serverId, hoursBack, fromDate, toDate); - var waitTypesTask = _dataService.GetDistinctWaitTypesAsync(_serverId, hoursBack, fromDate, toDate); - var memoryClerkTypesTask = _dataService.GetDistinctMemoryClerkTypesAsync(_serverId, hoursBack, fromDate, toDate); - var perfmonCountersTask = _dataService.GetDistinctPerfmonCountersAsync(_serverId, hoursBack, fromDate, toDate); var queryStoreTask = _dataService.GetQueryStoreTopQueriesAsync(_serverId, hoursBack, 50, fromDate, toDate); - var memoryGrantTrendTask = _dataService.GetMemoryGrantTrendAsync(_serverId, hoursBack, fromDate, toDate); - var memoryGrantChartTask = _dataService.GetMemoryGrantChartDataAsync(_serverId, hoursBack, fromDate, toDate); - var serverConfigTask = SafeQueryAsync(() => _dataService.GetLatestServerConfigAsync(_serverId)); - var databaseConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseConfigAsync(_serverId)); - var databaseScopedConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseScopedConfigAsync(_serverId)); - var traceFlagsTask = SafeQueryAsync(() => _dataService.GetLatestTraceFlagsAsync(_serverId)); - var runningJobsTask = SafeQueryAsync(() => _dataService.GetRunningJobsAsync(_serverId)); - var collectionHealthTask = SafeQueryAsync(() => _dataService.GetCollectionHealthAsync(_serverId)); - var collectionLogTask = SafeQueryAsync(() => _dataService.GetRecentCollectionLogAsync(_serverId, hoursBack)); - var dailySummaryTask = _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate); - /* Core data tasks */ - await System.Threading.Tasks.Task.WhenAll( - snapshotsTask, cpuTask, memoryTask, memoryTrendTask, - queryStatsTask, procStatsTask, fileIoTask, fileIoTrendTask, fileIoThroughputTask, tempDbTask, tempDbFileIoTask, - deadlockTask, blockedProcessTask, waitTypesTask, memoryClerkTypesTask, perfmonCountersTask, - queryStoreTask, memoryGrantTrendTask, memoryGrantChartTask, - serverConfigTask, databaseConfigTask, databaseScopedConfigTask, traceFlagsTask, - runningJobsTask, collectionHealthTask, collectionLogTask, dailySummaryTask); - - /* Trend chart tasks - run separately so failures don't kill the whole refresh */ - var lockWaitTrendTask = SafeQueryAsync(() => _dataService.GetLockWaitTrendAsync(_serverId, hoursBack, fromDate, toDate)); - var blockingTrendTask = SafeQueryAsync(() => _dataService.GetBlockingTrendAsync(_serverId, hoursBack, fromDate, toDate)); - var deadlockTrendTask = SafeQueryAsync(() => _dataService.GetDeadlockTrendAsync(_serverId, hoursBack, fromDate, toDate)); var queryDurationTrendTask = SafeQueryAsync(() => _dataService.GetQueryDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); var procDurationTrendTask = SafeQueryAsync(() => _dataService.GetProcedureDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); var queryStoreDurationTrendTask = SafeQueryAsync(() => _dataService.GetQueryStoreDurationTrendAsync(_serverId, hoursBack, fromDate, toDate)); var executionCountTrendTask = SafeQueryAsync(() => _dataService.GetExecutionCountTrendAsync(_serverId, hoursBack, fromDate, toDate)); - var currentWaitsDurationTask = SafeQueryAsync(() => _dataService.GetWaitingTaskTrendAsync(_serverId, hoursBack, fromDate, toDate)); - var currentWaitsBlockedTask = SafeQueryAsync(() => _dataService.GetBlockedSessionTrendAsync(_serverId, hoursBack, fromDate, toDate)); await System.Threading.Tasks.Task.WhenAll( - lockWaitTrendTask, blockingTrendTask, deadlockTrendTask, - queryDurationTrendTask, procDurationTrendTask, queryStoreDurationTrendTask, executionCountTrendTask, - currentWaitsDurationTask, currentWaitsBlockedTask); - - loadSw.Stop(); - - /* Log data counts and timing for diagnostics */ - AppLogger.DataDiag("ServerTab", $"[{_server.DisplayName}] serverId={_serverId} hoursBack={hoursBack} dataLoad={loadSw.ElapsedMilliseconds}ms"); - AppLogger.DataDiag("ServerTab", $" Snapshots: {snapshotsTask.Result.Count}, CPU: {cpuTask.Result.Count}"); - AppLogger.DataDiag("ServerTab", $" Memory: {(memoryTask.Result != null ? "1" : "null")}, MemoryTrend: {memoryTrendTask.Result.Count}"); - AppLogger.DataDiag("ServerTab", $" QueryStats: {queryStatsTask.Result.Count}, ProcStats: {procStatsTask.Result.Count}"); - AppLogger.DataDiag("ServerTab", $" FileIo: {fileIoTask.Result.Count}, FileIoTrend: {fileIoTrendTask.Result.Count}"); - AppLogger.DataDiag("ServerTab", $" TempDb: {tempDbTask.Result.Count}, BlockedProcessReports: {blockedProcessTask.Result.Count}, Deadlocks: {deadlockTask.Result.Count}"); - AppLogger.DataDiag("ServerTab", $" WaitTypes: {waitTypesTask.Result.Count}, PerfmonCounters: {perfmonCountersTask.Result.Count}, QueryStore: {queryStoreTask.Result.Count}"); + snapshotsTask, queryStatsTask, procStatsTask, queryStoreTask, + queryDurationTrendTask, procDurationTrendTask, queryStoreDurationTrendTask, executionCountTrendTask); - /* Update grids (via filter managers to preserve active filters) */ _querySnapshotsFilterMgr!.UpdateData(snapshotsTask.Result); LiveSnapshotIndicator.Text = ""; _queryStatsFilterMgr!.UpdateData(queryStatsTask.Result); SetInitialSort(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); _procStatsFilterMgr!.UpdateData(procStatsTask.Result); SetInitialSort(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); - _blockedProcessFilterMgr!.UpdateData(blockedProcessTask.Result); - _deadlockFilterMgr!.UpdateData(DeadlockProcessDetail.ParseFromRows(deadlockTask.Result)); _queryStoreFilterMgr!.UpdateData(queryStoreTask.Result); SetInitialSort(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); - _serverConfigFilterMgr!.UpdateData(serverConfigTask.Result); - _databaseConfigFilterMgr!.UpdateData(databaseConfigTask.Result); - _dbScopedConfigFilterMgr!.UpdateData(databaseScopedConfigTask.Result); - _traceFlagsFilterMgr!.UpdateData(traceFlagsTask.Result); - _runningJobsFilterMgr!.UpdateData(runningJobsTask.Result); - _collectionHealthFilterMgr!.UpdateData(collectionHealthTask.Result); - _collectionLogFilterMgr!.UpdateData(collectionLogTask.Result); - var dailySummary = await dailySummaryTask; - DailySummaryGrid.ItemsSource = dailySummary != null - ? new List { dailySummary } : null; - DailySummaryNoData.Visibility = dailySummary == null - ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed; - UpdateCollectorDurationChart(collectionLogTask.Result); - /* Update memory summary */ - UpdateMemorySummary(memoryTask.Result); + UpdateQueryDurationTrendChart(queryDurationTrendTask.Result); + UpdateProcDurationTrendChart(procDurationTrendTask.Result); + UpdateQueryStoreDurationTrendChart(queryStoreDurationTrendTask.Result); + UpdateExecutionCountTrendChart(executionCountTrendTask.Result); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshQueriesAsync failed: {ex.Message}"); + } + } - /* Update charts */ + /// Tab 3 — CPU + private async System.Threading.Tasks.Task RefreshCpuAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var cpuTask = _dataService.GetCpuUtilizationAsync(_serverId, hoursBack, fromDate, toDate); + await cpuTask; UpdateCpuChart(cpuTask.Result); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshCpuAsync failed: {ex.Message}"); + } + } + + /// Tab 4 — Memory + private async System.Threading.Tasks.Task RefreshMemoryAsync(int hoursBack, DateTime? fromDate, DateTime? toDate, bool subTabOnly = false) + { + try + { + if (subTabOnly) + { + /* Timer tick: only refresh the visible sub-tab (5 queries → 1-2) */ + switch (MemorySubTabControl.SelectedIndex) + { + case 0: // Overview — memory stats + trend + var memStats = await _dataService.GetLatestMemoryStatsAsync(_serverId); + var memTrend = await _dataService.GetMemoryTrendAsync(_serverId, hoursBack, fromDate, toDate); + var memGrantTrend = await _dataService.GetMemoryGrantTrendAsync(_serverId, hoursBack, fromDate, toDate); + UpdateMemorySummary(memStats); + UpdateMemoryChart(memTrend, memGrantTrend); + break; + case 1: // Memory Clerks + var clerkTypes = await _dataService.GetDistinctMemoryClerkTypesAsync(_serverId, hoursBack, fromDate, toDate); + PopulateMemoryClerkPicker(clerkTypes); + await UpdateMemoryClerksChartFromPickerAsync(); + break; + case 2: // Memory Grants + var grantChart = await _dataService.GetMemoryGrantChartDataAsync(_serverId, hoursBack, fromDate, toDate); + UpdateMemoryGrantCharts(grantChart); + break; + } + return; + } + + /* Full refresh: load all sub-tabs */ + var memoryTask = _dataService.GetLatestMemoryStatsAsync(_serverId); + var memoryTrendTask = _dataService.GetMemoryTrendAsync(_serverId, hoursBack, fromDate, toDate); + var memoryClerkTypesTask = _dataService.GetDistinctMemoryClerkTypesAsync(_serverId, hoursBack, fromDate, toDate); + var memoryGrantTrendTask = _dataService.GetMemoryGrantTrendAsync(_serverId, hoursBack, fromDate, toDate); + var memoryGrantChartTask = _dataService.GetMemoryGrantChartDataAsync(_serverId, hoursBack, fromDate, toDate); + + await System.Threading.Tasks.Task.WhenAll(memoryTask, memoryTrendTask, memoryClerkTypesTask, memoryGrantTrendTask, memoryGrantChartTask); + + UpdateMemorySummary(memoryTask.Result); UpdateMemoryChart(memoryTrendTask.Result, memoryGrantTrendTask.Result); - UpdateTempDbChart(tempDbTask.Result); - UpdateTempDbFileIoChart(tempDbFileIoTask.Result); + UpdateMemoryGrantCharts(memoryGrantChartTask.Result); + PopulateMemoryClerkPicker(memoryClerkTypesTask.Result); + await UpdateMemoryClerksChartFromPickerAsync(); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshMemoryAsync failed: {ex.Message}"); + } + } + + /// Tab 5 — File I/O + private async System.Threading.Tasks.Task RefreshFileIoAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var fileIoTrendTask = _dataService.GetFileIoLatencyTrendAsync(_serverId, hoursBack, fromDate, toDate); + var fileIoThroughputTask = _dataService.GetFileIoThroughputTrendAsync(_serverId, hoursBack, fromDate, toDate); + + await System.Threading.Tasks.Task.WhenAll(fileIoTrendTask, fileIoThroughputTask); + UpdateFileIoCharts(fileIoTrendTask.Result); UpdateFileIoThroughputCharts(fileIoThroughputTask.Result); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshFileIoAsync failed: {ex.Message}"); + } + } + + /// Tab 6 — TempDB + private async System.Threading.Tasks.Task RefreshTempDbAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var tempDbTask = _dataService.GetTempDbTrendAsync(_serverId, hoursBack, fromDate, toDate); + var tempDbFileIoTask = _dataService.GetTempDbFileIoTrendAsync(_serverId, hoursBack, fromDate, toDate); + + await System.Threading.Tasks.Task.WhenAll(tempDbTask, tempDbFileIoTask); + + UpdateTempDbChart(tempDbTask.Result); + UpdateTempDbFileIoChart(tempDbFileIoTask.Result); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshTempDbAsync failed: {ex.Message}"); + } + } + + /// Tab 7 — Blocking + private async System.Threading.Tasks.Task RefreshBlockingAsync(int hoursBack, DateTime? fromDate, DateTime? toDate, bool subTabOnly = false) + { + try + { + if (subTabOnly) + { + /* Timer tick: only refresh the visible sub-tab (7 queries → 1-3) + lightweight alert counts */ + switch (BlockingSubTabControl.SelectedIndex) + { + case 0: // Trends — 3 trend charts + var lwt = SafeQueryAsync(() => _dataService.GetLockWaitTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var bt = SafeQueryAsync(() => _dataService.GetBlockingTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var dt = SafeQueryAsync(() => _dataService.GetDeadlockTrendAsync(_serverId, hoursBack, fromDate, toDate)); + await System.Threading.Tasks.Task.WhenAll(lwt, bt, dt); + UpdateLockWaitTrendChart(lwt.Result, hoursBack, fromDate, toDate); + UpdateBlockingTrendChart(bt.Result, hoursBack, fromDate, toDate); + UpdateDeadlockTrendChart(dt.Result, hoursBack, fromDate, toDate); + break; + case 1: // Current Waits — 2 charts + var cwd = SafeQueryAsync(() => _dataService.GetWaitingTaskTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var cwb = SafeQueryAsync(() => _dataService.GetBlockedSessionTrendAsync(_serverId, hoursBack, fromDate, toDate)); + await System.Threading.Tasks.Task.WhenAll(cwd, cwb); + UpdateCurrentWaitsDurationChart(cwd.Result, hoursBack, fromDate, toDate); + UpdateCurrentWaitsBlockedChart(cwb.Result, hoursBack, fromDate, toDate); + break; + case 2: // Blocked Process Reports + var bpr = await _dataService.GetRecentBlockedProcessReportsAsync(_serverId, hoursBack, fromDate, toDate); + _blockedProcessFilterMgr!.UpdateData(bpr); + break; + case 3: // Deadlocks + var dlr = await _dataService.GetRecentDeadlocksAsync(_serverId, hoursBack, fromDate, toDate); + _deadlockFilterMgr!.UpdateData(DeadlockProcessDetail.ParseFromRows(dlr)); + break; + } + /* Always keep alert badge current when Blocking tab is visible */ + await RefreshAlertCountsAsync(hoursBack, fromDate, toDate); + return; + } + + /* Full refresh: load all sub-tabs */ + var blockedProcessTask = _dataService.GetRecentBlockedProcessReportsAsync(_serverId, hoursBack, fromDate, toDate); + var deadlockTask = _dataService.GetRecentDeadlocksAsync(_serverId, hoursBack, fromDate, toDate); + var lockWaitTrendTask = SafeQueryAsync(() => _dataService.GetLockWaitTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var blockingTrendTask = SafeQueryAsync(() => _dataService.GetBlockingTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var deadlockTrendTask = SafeQueryAsync(() => _dataService.GetDeadlockTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var currentWaitsDurationTask = SafeQueryAsync(() => _dataService.GetWaitingTaskTrendAsync(_serverId, hoursBack, fromDate, toDate)); + var currentWaitsBlockedTask = SafeQueryAsync(() => _dataService.GetBlockedSessionTrendAsync(_serverId, hoursBack, fromDate, toDate)); + + await System.Threading.Tasks.Task.WhenAll( + blockedProcessTask, deadlockTask, + lockWaitTrendTask, blockingTrendTask, deadlockTrendTask, + currentWaitsDurationTask, currentWaitsBlockedTask); + + _blockedProcessFilterMgr!.UpdateData(blockedProcessTask.Result); + _deadlockFilterMgr!.UpdateData(DeadlockProcessDetail.ParseFromRows(deadlockTask.Result)); + UpdateLockWaitTrendChart(lockWaitTrendTask.Result, hoursBack, fromDate, toDate); UpdateBlockingTrendChart(blockingTrendTask.Result, hoursBack, fromDate, toDate); UpdateDeadlockTrendChart(deadlockTrendTask.Result, hoursBack, fromDate, toDate); UpdateCurrentWaitsDurationChart(currentWaitsDurationTask.Result, hoursBack, fromDate, toDate); UpdateCurrentWaitsBlockedChart(currentWaitsBlockedTask.Result, hoursBack, fromDate, toDate); - UpdateQueryDurationTrendChart(queryDurationTrendTask.Result); - UpdateProcDurationTrendChart(procDurationTrendTask.Result); - UpdateQueryStoreDurationTrendChart(queryStoreDurationTrendTask.Result); - UpdateExecutionCountTrendChart(executionCountTrendTask.Result); - UpdateMemoryGrantCharts(memoryGrantChartTask.Result); - - /* Populate pickers (preserve selections) */ - PopulateWaitTypePicker(waitTypesTask.Result); - PopulateMemoryClerkPicker(memoryClerkTypesTask.Result); - PopulatePerfmonPicker(perfmonCountersTask.Result); - /* Update picker-driven charts */ - await UpdateWaitStatsChartFromPickerAsync(); - await UpdateMemoryClerksChartFromPickerAsync(); - await UpdatePerfmonChartFromPickerAsync(); - - var tz = ServerTimeHelper.GetTimezoneLabel(ServerTimeHelper.CurrentDisplayMode); - ConnectionStatusText.Text = $"{_server.ServerName} - Last refresh: {DateTime.Now:HH:mm:ss} ({tz})"; - - /* Notify parent of alert counts for tab badge. - Include the latest event timestamp so acknowledgement is only - cleared when genuinely new events arrive, not when the time range changes. */ + /* Notify parent of alert counts for tab badge */ var blockingCount = blockedProcessTask.Result.Count; var deadlockCount = deadlockTask.Result.Count; DateTime? latestEventTime = null; @@ -703,9 +1051,127 @@ Include the latest event timestamp so acknowledgement is only } catch (Exception ex) { - ConnectionStatusText.Text = $"Error: {ex.Message}"; - AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshAllDataAsync failed: {ex}"); + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshBlockingAsync failed: {ex.Message}"); + } + } + + /// Tab 8 — Perfmon + private async System.Threading.Tasks.Task RefreshPerfmonAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var perfmonCountersTask = _dataService.GetDistinctPerfmonCountersAsync(_serverId, hoursBack, fromDate, toDate); + await perfmonCountersTask; + PopulatePerfmonPicker(perfmonCountersTask.Result); + await UpdatePerfmonChartFromPickerAsync(); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshPerfmonAsync failed: {ex.Message}"); + } + } + + /// Tab 9 — Running Jobs + private async System.Threading.Tasks.Task RefreshRunningJobsAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var runningJobsTask = SafeQueryAsync(() => _dataService.GetRunningJobsAsync(_serverId)); + await runningJobsTask; + _runningJobsFilterMgr!.UpdateData(runningJobsTask.Result); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshRunningJobsAsync failed: {ex.Message}"); + } + } + + /// Tab 10 — Configuration + private async System.Threading.Tasks.Task RefreshConfigurationAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var serverConfigTask = SafeQueryAsync(() => _dataService.GetLatestServerConfigAsync(_serverId)); + var databaseConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseConfigAsync(_serverId)); + var databaseScopedConfigTask = SafeQueryAsync(() => _dataService.GetLatestDatabaseScopedConfigAsync(_serverId)); + var traceFlagsTask = SafeQueryAsync(() => _dataService.GetLatestTraceFlagsAsync(_serverId)); + + await System.Threading.Tasks.Task.WhenAll(serverConfigTask, databaseConfigTask, databaseScopedConfigTask, traceFlagsTask); + + _serverConfigFilterMgr!.UpdateData(serverConfigTask.Result); + _databaseConfigFilterMgr!.UpdateData(databaseConfigTask.Result); + _dbScopedConfigFilterMgr!.UpdateData(databaseScopedConfigTask.Result); + _traceFlagsFilterMgr!.UpdateData(traceFlagsTask.Result); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshConfigurationAsync failed: {ex.Message}"); + } + } + + /// Tab 11 — Daily Summary + private async System.Threading.Tasks.Task RefreshDailySummaryAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var dailySummaryTask = _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate); + var dailySummary = await dailySummaryTask; + DailySummaryGrid.ItemsSource = dailySummary != null + ? new List { dailySummary } : null; + DailySummaryNoData.Visibility = dailySummary == null + ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed; + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshDailySummaryAsync failed: {ex.Message}"); + } + } + + /// Tab 12 — Collection Health + private async System.Threading.Tasks.Task RefreshCollectionHealthAsync(int hoursBack, DateTime? fromDate, DateTime? toDate) + { + try + { + var collectionHealthTask = SafeQueryAsync(() => _dataService.GetCollectionHealthAsync(_serverId)); + var collectionLogTask = SafeQueryAsync(() => _dataService.GetRecentCollectionLogAsync(_serverId, hoursBack)); + + await System.Threading.Tasks.Task.WhenAll(collectionHealthTask, collectionLogTask); + + _collectionHealthFilterMgr!.UpdateData(collectionHealthTask.Result); + _collectionLogFilterMgr!.UpdateData(collectionLogTask.Result); + UpdateCollectorDurationChart(collectionLogTask.Result); + } + catch (Exception ex) + { + AppLogger.Info("ServerTab", $"[{_server.DisplayName}] RefreshCollectionHealthAsync failed: {ex.Message}"); + } + } + + /// + /// When the user switches main tabs or sub-tabs, refresh only the visible sub-tab. + /// All sub-tabs are loaded on first load and manual refresh — tab/sub-tab switches + /// only need to refresh the one the user is looking at. + /// + private async void MainTabControl_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (!IsLoaded || _dataService == null) return; + if (_isRefreshing) return; + if (e.Source != MainTabControl && e.Source != QueriesSubTabControl + && e.Source != MemorySubTabControl && e.Source != BlockingSubTabControl) return; + + var hoursBack = GetHoursBack(); + DateTime? fromDate = null, toDate = null; + if (IsCustomRange) + { + var fromLocal = GetDateTimeFromPickers(FromDatePicker!, FromHourCombo, FromMinuteCombo); + var toLocal = GetDateTimeFromPickers(ToDatePicker!, ToHourCombo, ToMinuteCombo); + if (fromLocal.HasValue && toLocal.HasValue) + { + fromDate = ServerTimeHelper.LocalToServerTime(fromLocal.Value); + toDate = ServerTimeHelper.LocalToServerTime(toLocal.Value); + } } + await RefreshVisibleTabAsync(hoursBack, fromDate, toDate, subTabOnly: true); } /// diff --git a/Lite/Services/LocalDataService.Blocking.cs b/Lite/Services/LocalDataService.Blocking.cs index f1bcf58c..4f9cb7a6 100644 --- a/Lite/Services/LocalDataService.Blocking.cs +++ b/Lite/Services/LocalDataService.Blocking.cs @@ -219,6 +219,46 @@ AND query_text NOT LIKE 'WAITFOR%' return items; } + /// + /// Gets lightweight blocking + deadlock counts and latest event time for alert badge updates. + /// Much cheaper than fetching full rows with XML — just COUNT(*) and MAX(time). + /// + public async Task<(int blockingCount, int deadlockCount, DateTime? latestEventTime)> GetAlertCountsAsync(int serverId, int hoursBack = 24, DateTime? fromDate = null, DateTime? toDate = null) + { + using var connection = await OpenConnectionAsync(); + using var command = connection.CreateCommand(); + + var (startTime, endTime) = GetTimeRange(hoursBack, fromDate, toDate); + + command.CommandText = @" +SELECT + (SELECT COUNT(*) FROM v_blocked_process_reports + WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3) AS blocking_count, + (SELECT COUNT(*) FROM v_deadlocks + WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3) AS deadlock_count, + (SELECT MAX(t) FROM ( + SELECT MAX(event_time) AS t FROM v_blocked_process_reports + WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 + UNION ALL + SELECT MAX(deadlock_time) AS t FROM v_deadlocks + WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 + )) AS latest_event_time"; + + command.Parameters.Add(new DuckDBParameter { Value = serverId }); + command.Parameters.Add(new DuckDBParameter { Value = startTime }); + command.Parameters.Add(new DuckDBParameter { Value = endTime }); + + using var reader = await command.ExecuteReaderAsync(); + if (!await reader.ReadAsync()) + return (0, 0, null); + + var blockingCount = reader.IsDBNull(0) ? 0 : Convert.ToInt32(reader.GetValue(0)); + var deadlockCount = reader.IsDBNull(1) ? 0 : Convert.ToInt32(reader.GetValue(1)); + var latestEventTime = reader.IsDBNull(2) ? (DateTime?)null : reader.GetDateTime(2); + + return (blockingCount, deadlockCount, latestEventTime); + } + /// /// Gets recent blocked process reports from the XE-based collector. /// diff --git a/Lite/Services/LocalDataService.QueryStore.cs b/Lite/Services/LocalDataService.QueryStore.cs index e0d7d302..a6dde182 100644 --- a/Lite/Services/LocalDataService.QueryStore.cs +++ b/Lite/Services/LocalDataService.QueryStore.cs @@ -50,7 +50,7 @@ public async Task> GetQueryStoreTopQueriesAsync(int serverId MAX(query_plan_hash) AS query_plan_hash, MAX(CASE WHEN is_forced_plan THEN TRUE ELSE FALSE END) AS is_forced_plan, MAX(plan_forcing_type) AS plan_forcing_type, - MAX(query_plan_text) AS query_plan_text, + NULL AS query_plan_text, MAX(execution_type_desc) AS execution_type_desc, MIN(first_execution_time) AS first_execution_time, AVG(CAST(avg_clr_time_us AS DOUBLE)) / 1000.0 AS avg_clr_time_ms, diff --git a/Lite/Services/RemoteCollectorService.QueryStore.cs b/Lite/Services/RemoteCollectorService.QueryStore.cs index 9c3c6f41..76aa6077 100644 --- a/Lite/Services/RemoteCollectorService.QueryStore.cs +++ b/Lite/Services/RemoteCollectorService.QueryStore.cs @@ -315,7 +315,7 @@ ELSE COALESCE( force_failure_count = qsp.force_failure_count, last_force_failure_reason = qsp.last_force_failure_reason_desc, compatibility_level = qsp.compatibility_level, - query_plan_text = CONVERT(nvarchar(max), qsp.query_plan), + query_plan_text = CONVERT(nvarchar(1), NULL), query_plan_hash = CONVERT(varchar(64), qsp.query_plan_hash, 1) FROM sys.query_store_runtime_stats AS qsrs JOIN sys.query_store_plan AS qsp @@ -326,7 +326,7 @@ JOIN sys.query_store_query_text AS qst ON qst.query_text_id = qsq.query_text_id WHERE qsrs.last_execution_time > @cutoff_time AND qst.query_sql_text NOT LIKE N''%PerformanceMonitorLite%'' - OPTION(RECOMPILE);', + OPTION(RECOMPILE, LOOP JOIN);', N'@cutoff_time datetime2(7)', @cutoff_time;";