diff --git a/Dashboard/CollectorScheduleWindow.xaml b/Dashboard/CollectorScheduleWindow.xaml index 6487ff9..0538ad7 100644 --- a/Dashboard/CollectorScheduleWindow.xaml +++ b/Dashboard/CollectorScheduleWindow.xaml @@ -34,8 +34,24 @@ - + + + + + + + + + + + + + + + + diff --git a/Dashboard/CollectorScheduleWindow.xaml.cs b/Dashboard/CollectorScheduleWindow.xaml.cs index 4d1b126..a74f402 100644 --- a/Dashboard/CollectorScheduleWindow.xaml.cs +++ b/Dashboard/CollectorScheduleWindow.xaml.cs @@ -11,6 +11,7 @@ using System.ComponentModel; using System.Linq; using System.Windows; +using System.Windows.Controls; using PerformanceMonitorDashboard.Models; using PerformanceMonitorDashboard.Services; @@ -20,6 +21,107 @@ public partial class CollectorScheduleWindow : Window { private readonly DatabaseService _databaseService; private List? _schedules; + private bool _suppressPresetChange; + + private static readonly Dictionary> Presets = new(StringComparer.OrdinalIgnoreCase) + { + ["Aggressive"] = new(StringComparer.OrdinalIgnoreCase) + { + ["wait_stats_collector"] = 1, + ["query_stats_collector"] = 1, + ["memory_stats_collector"] = 1, + ["memory_pressure_events_collector"] = 1, + ["system_health_collector"] = 2, + ["blocked_process_xml_collector"] = 1, + ["deadlock_xml_collector"] = 1, + ["process_blocked_process_xml"] = 2, + ["blocking_deadlock_analyzer"] = 2, + ["process_deadlock_xml"] = 2, + ["query_store_collector"] = 2, + ["procedure_stats_collector"] = 1, + ["query_snapshots_collector"] = 1, + ["file_io_stats_collector"] = 1, + ["memory_grant_stats_collector"] = 1, + ["cpu_scheduler_stats_collector"] = 1, + ["memory_clerks_stats_collector"] = 2, + ["perfmon_stats_collector"] = 1, + ["cpu_utilization_stats_collector"] = 1, + ["trace_analysis_collector"] = 1, + ["default_trace_collector"] = 2, + ["configuration_issues_analyzer"] = 1, + ["latch_stats_collector"] = 1, + ["spinlock_stats_collector"] = 1, + ["tempdb_stats_collector"] = 1, + ["plan_cache_stats_collector"] = 2, + ["session_stats_collector"] = 1, + ["waiting_tasks_collector"] = 1, + ["running_jobs_collector"] = 2 + }, + ["Balanced"] = new(StringComparer.OrdinalIgnoreCase) + { + ["wait_stats_collector"] = 1, + ["query_stats_collector"] = 2, + ["memory_stats_collector"] = 1, + ["memory_pressure_events_collector"] = 1, + ["system_health_collector"] = 5, + ["blocked_process_xml_collector"] = 1, + ["deadlock_xml_collector"] = 1, + ["process_blocked_process_xml"] = 5, + ["blocking_deadlock_analyzer"] = 5, + ["process_deadlock_xml"] = 5, + ["query_store_collector"] = 2, + ["procedure_stats_collector"] = 2, + ["query_snapshots_collector"] = 1, + ["file_io_stats_collector"] = 1, + ["memory_grant_stats_collector"] = 1, + ["cpu_scheduler_stats_collector"] = 1, + ["memory_clerks_stats_collector"] = 5, + ["perfmon_stats_collector"] = 5, + ["cpu_utilization_stats_collector"] = 1, + ["trace_analysis_collector"] = 2, + ["default_trace_collector"] = 5, + ["configuration_issues_analyzer"] = 1, + ["latch_stats_collector"] = 1, + ["spinlock_stats_collector"] = 1, + ["tempdb_stats_collector"] = 1, + ["plan_cache_stats_collector"] = 5, + ["session_stats_collector"] = 1, + ["waiting_tasks_collector"] = 1, + ["running_jobs_collector"] = 1 + }, + ["Low-Impact"] = new(StringComparer.OrdinalIgnoreCase) + { + ["wait_stats_collector"] = 5, + ["query_stats_collector"] = 10, + ["memory_stats_collector"] = 10, + ["memory_pressure_events_collector"] = 5, + ["system_health_collector"] = 15, + ["blocked_process_xml_collector"] = 5, + ["deadlock_xml_collector"] = 5, + ["process_blocked_process_xml"] = 10, + ["blocking_deadlock_analyzer"] = 10, + ["process_deadlock_xml"] = 10, + ["query_store_collector"] = 30, + ["procedure_stats_collector"] = 10, + ["query_snapshots_collector"] = 5, + ["file_io_stats_collector"] = 10, + ["memory_grant_stats_collector"] = 5, + ["cpu_scheduler_stats_collector"] = 5, + ["memory_clerks_stats_collector"] = 30, + ["perfmon_stats_collector"] = 5, + ["cpu_utilization_stats_collector"] = 5, + ["trace_analysis_collector"] = 10, + ["default_trace_collector"] = 15, + ["configuration_issues_analyzer"] = 5, + ["latch_stats_collector"] = 5, + ["spinlock_stats_collector"] = 5, + ["tempdb_stats_collector"] = 5, + ["plan_cache_stats_collector"] = 15, + ["session_stats_collector"] = 5, + ["waiting_tasks_collector"] = 5, + ["running_jobs_collector"] = 30 + } + }; public CollectorScheduleWindow(DatabaseService databaseService) { @@ -59,6 +161,7 @@ private async System.Threading.Tasks.Task LoadSchedulesAsync() } ScheduleDataGrid.ItemsSource = _schedules; + DetectActivePreset(); } catch (Exception ex) { @@ -71,6 +174,101 @@ private async System.Threading.Tasks.Task LoadSchedulesAsync() } } + private void DetectActivePreset() + { + if (_schedules == null) return; + + _suppressPresetChange = true; + try + { + var currentIntervals = _schedules + .Where(s => s.FrequencyMinutes < 1440) + .ToDictionary(s => s.CollectorName, s => s.FrequencyMinutes, StringComparer.OrdinalIgnoreCase); + + foreach (var (presetName, presetIntervals) in Presets) + { + bool matches = true; + foreach (var (collector, freq) in presetIntervals) + { + if (currentIntervals.TryGetValue(collector, out int current) && current != freq) + { + matches = false; + break; + } + } + + if (matches) + { + for (int i = 0; i < PresetComboBox.Items.Count; i++) + { + if (PresetComboBox.Items[i] is ComboBoxItem item && + string.Equals(item.Content?.ToString(), presetName, StringComparison.OrdinalIgnoreCase)) + { + PresetComboBox.SelectedIndex = i; + return; + } + } + } + } + + /* No preset matched */ + PresetComboBox.SelectedIndex = 0; + } + finally + { + _suppressPresetChange = false; + } + } + + private async void PresetComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (_suppressPresetChange) return; + if (PresetComboBox.SelectedItem is not ComboBoxItem selected) return; + + string presetName = selected.Content?.ToString() ?? ""; + if (presetName == "Custom") return; + + var result = MessageBox.Show( + $"Apply the \"{presetName}\" preset?\n\nThis will change all collector frequencies. Enabled/disabled state and retention settings are not affected.", + "Apply Collection Preset", + MessageBoxButton.YesNo, + MessageBoxImage.Question + ); + + if (result != MessageBoxResult.Yes) + { + DetectActivePreset(); + return; + } + + try + { + await _databaseService.ApplyCollectionPresetAsync(presetName); + + /* Unsubscribe, reload, resubscribe */ + if (_schedules != null) + { + foreach (var schedule in _schedules) + { + schedule.PropertyChanged -= Schedule_PropertyChanged; + } + } + + await LoadSchedulesAsync(); + } + catch (Exception ex) + { + MessageBox.Show( + $"Failed to apply preset:\n\n{ex.Message}", + "Error Applying Preset", + MessageBoxButton.OK, + MessageBoxImage.Error + ); + + DetectActivePreset(); + } + } + private async void Schedule_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (sender is CollectorScheduleItem schedule) @@ -88,6 +286,11 @@ await _databaseService.UpdateCollectorScheduleAsync( schedule.FrequencyMinutes, schedule.RetentionDays ); + + if (e.PropertyName == nameof(CollectorScheduleItem.FrequencyMinutes)) + { + DetectActivePreset(); + } } catch (Exception ex) { diff --git a/Dashboard/Services/DatabaseService.cs b/Dashboard/Services/DatabaseService.cs index c5dfb36..6161061 100644 --- a/Dashboard/Services/DatabaseService.cs +++ b/Dashboard/Services/DatabaseService.cs @@ -357,5 +357,18 @@ public async Task UpdateCollectorScheduleAsync(int scheduleId, bool enabled, int await command.ExecuteNonQueryAsync(); } + + public async Task ApplyCollectionPresetAsync(string presetName) + { + await using var tc = await OpenThrottledConnectionAsync(); + var connection = tc.Connection; + + using var command = new SqlCommand("config.apply_collection_preset", connection); + command.CommandType = System.Data.CommandType.StoredProcedure; + command.CommandTimeout = 120; + command.Parameters.Add(new SqlParameter("@preset_name", SqlDbType.NVarChar, 128) { Value = presetName }); + + await command.ExecuteNonQueryAsync(); + } } } diff --git a/Lite/Services/ScheduleManager.cs b/Lite/Services/ScheduleManager.cs index ff96fe6..d09b188 100644 --- a/Lite/Services/ScheduleManager.cs +++ b/Lite/Services/ScheduleManager.cs @@ -28,6 +28,39 @@ public class ScheduleManager DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; + public static readonly string[] PresetNames = ["Low-Impact", "Balanced", "Aggressive"]; + + private static readonly Dictionary> s_presets = new(StringComparer.OrdinalIgnoreCase) + { + ["Aggressive"] = new(StringComparer.OrdinalIgnoreCase) + { + ["wait_stats"] = 1, ["query_stats"] = 1, ["procedure_stats"] = 1, + ["query_store"] = 2, ["query_snapshots"] = 1, ["cpu_utilization"] = 1, + ["file_io_stats"] = 1, ["memory_stats"] = 1, ["memory_clerks"] = 2, + ["tempdb_stats"] = 1, ["perfmon_stats"] = 1, ["deadlocks"] = 1, + ["memory_grant_stats"] = 1, ["waiting_tasks"] = 1, + ["blocked_process_report"] = 1, ["running_jobs"] = 2 + }, + ["Balanced"] = new(StringComparer.OrdinalIgnoreCase) + { + ["wait_stats"] = 1, ["query_stats"] = 1, ["procedure_stats"] = 1, + ["query_store"] = 5, ["query_snapshots"] = 1, ["cpu_utilization"] = 1, + ["file_io_stats"] = 1, ["memory_stats"] = 1, ["memory_clerks"] = 5, + ["tempdb_stats"] = 1, ["perfmon_stats"] = 1, ["deadlocks"] = 1, + ["memory_grant_stats"] = 1, ["waiting_tasks"] = 1, + ["blocked_process_report"] = 1, ["running_jobs"] = 5 + }, + ["Low-Impact"] = new(StringComparer.OrdinalIgnoreCase) + { + ["wait_stats"] = 5, ["query_stats"] = 10, ["procedure_stats"] = 10, + ["query_store"] = 30, ["query_snapshots"] = 5, ["cpu_utilization"] = 5, + ["file_io_stats"] = 10, ["memory_stats"] = 10, ["memory_clerks"] = 30, + ["tempdb_stats"] = 5, ["perfmon_stats"] = 5, ["deadlocks"] = 5, + ["memory_grant_stats"] = 5, ["waiting_tasks"] = 5, + ["blocked_process_report"] = 5, ["running_jobs"] = 30 + } + }; + private readonly string _schedulePath; private readonly ILogger? _logger; private List _schedules; @@ -160,6 +193,61 @@ public void UpdateSchedule(string collectorName, bool? enabled = null, int? freq } } + /// + /// Detects which preset matches the current intervals, or returns "Custom". + /// + public string GetActivePreset() + { + lock (_lock) + { + foreach (var (presetName, intervals) in s_presets) + { + bool matches = true; + foreach (var (collector, freq) in intervals) + { + var schedule = _schedules.FirstOrDefault(s => + s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase)); + if (schedule != null && schedule.FrequencyMinutes != freq) + { + matches = false; + break; + } + } + if (matches) return presetName; + } + return "Custom"; + } + } + + /// + /// Applies a named preset, changing all scheduled collector frequencies. + /// Does not modify enabled/disabled state or on-load (frequency=0) collectors. + /// + public void ApplyPreset(string presetName) + { + if (!s_presets.TryGetValue(presetName, out var intervals)) + { + throw new ArgumentException($"Unknown preset: {presetName}"); + } + + lock (_lock) + { + foreach (var (collector, freq) in intervals) + { + var schedule = _schedules.FirstOrDefault(s => + s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase)); + if (schedule != null) + { + schedule.FrequencyMinutes = freq; + } + } + + SaveSchedules(); + + _logger?.LogInformation("Applied collection preset '{Preset}'", presetName); + } + } + /// /// Loads schedules from the JSON config file. /// diff --git a/Lite/Windows/SettingsWindow.xaml b/Lite/Windows/SettingsWindow.xaml index ba43707..001c60a 100644 --- a/Lite/Windows/SettingsWindow.xaml +++ b/Lite/Windows/SettingsWindow.xaml @@ -315,8 +315,24 @@ - + + + + + + + + + + + + + + + +