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 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+