Skip to content

Commit cc83cd3

Browse files
Add named collection presets (Aggressive, Balanced, Low-Impact) — closes #454
One-click profile switching for all collector frequencies, requested in #454. SQL: New config.apply_collection_preset proc replaces three legacy profile procs. Uses table variable + UPDATE JOIN for single-operation preset apply. Drops config.enable_realtime_monitoring, enable_consulting_analysis, enable_baseline_monitoring. Dashboard: Preset ComboBox in CollectorScheduleWindow with auto-detection of active preset by comparing current intervals against known definitions. Confirmation dialog before applying. Manual edits switch to Custom. Lite: Same UI pattern in SettingsWindow. Preset definitions and ApplyPreset/GetActivePreset methods on ScheduleManager. Tested all three presets on sql2022 (29 collectors each). Both apps build clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 27d4176 commit cc83cd3

7 files changed

Lines changed: 623 additions & 153 deletions

File tree

Dashboard/CollectorScheduleWindow.xaml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,24 @@
3434
<!-- Header -->
3535
<TextBlock Grid.Row="0" Text="Collector Schedules" FontWeight="Bold" FontSize="16"
3636
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,4"/>
37-
<TextBlock Grid.Row="1" Text="Configure collection frequency and data retention for each collector."
38-
Foreground="{DynamicResource ForegroundMutedBrush}" FontSize="12" Margin="0,0,0,12"/>
37+
<Grid Grid.Row="1" Margin="0,0,0,12">
38+
<Grid.ColumnDefinitions>
39+
<ColumnDefinition Width="*"/>
40+
<ColumnDefinition Width="Auto"/>
41+
</Grid.ColumnDefinitions>
42+
<TextBlock Grid.Column="0" Text="Configure collection frequency and data retention for each collector."
43+
Foreground="{DynamicResource ForegroundMutedBrush}" FontSize="12" VerticalAlignment="Center"/>
44+
<StackPanel Grid.Column="1" Orientation="Horizontal">
45+
<TextBlock Text="Preset:" Foreground="{DynamicResource ForegroundBrush}" FontSize="12"
46+
VerticalAlignment="Center" Margin="0,0,8,0"/>
47+
<ComboBox x:Name="PresetComboBox" Width="140" SelectionChanged="PresetComboBox_SelectionChanged">
48+
<ComboBoxItem Content="Custom" IsEnabled="False"/>
49+
<ComboBoxItem Content="Low-Impact"/>
50+
<ComboBoxItem Content="Balanced"/>
51+
<ComboBoxItem Content="Aggressive"/>
52+
</ComboBox>
53+
</StackPanel>
54+
</Grid>
3955

4056
<!-- Schedule Grid -->
4157
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4">

Dashboard/CollectorScheduleWindow.xaml.cs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using System.ComponentModel;
1212
using System.Linq;
1313
using System.Windows;
14+
using System.Windows.Controls;
1415
using PerformanceMonitorDashboard.Models;
1516
using PerformanceMonitorDashboard.Services;
1617

@@ -20,6 +21,107 @@ public partial class CollectorScheduleWindow : Window
2021
{
2122
private readonly DatabaseService _databaseService;
2223
private List<CollectorScheduleItem>? _schedules;
24+
private bool _suppressPresetChange;
25+
26+
private static readonly Dictionary<string, Dictionary<string, int>> Presets = new(StringComparer.OrdinalIgnoreCase)
27+
{
28+
["Aggressive"] = new(StringComparer.OrdinalIgnoreCase)
29+
{
30+
["wait_stats_collector"] = 1,
31+
["query_stats_collector"] = 1,
32+
["memory_stats_collector"] = 1,
33+
["memory_pressure_events_collector"] = 1,
34+
["system_health_collector"] = 2,
35+
["blocked_process_xml_collector"] = 1,
36+
["deadlock_xml_collector"] = 1,
37+
["process_blocked_process_xml"] = 2,
38+
["blocking_deadlock_analyzer"] = 2,
39+
["process_deadlock_xml"] = 2,
40+
["query_store_collector"] = 2,
41+
["procedure_stats_collector"] = 1,
42+
["query_snapshots_collector"] = 1,
43+
["file_io_stats_collector"] = 1,
44+
["memory_grant_stats_collector"] = 1,
45+
["cpu_scheduler_stats_collector"] = 1,
46+
["memory_clerks_stats_collector"] = 2,
47+
["perfmon_stats_collector"] = 1,
48+
["cpu_utilization_stats_collector"] = 1,
49+
["trace_analysis_collector"] = 1,
50+
["default_trace_collector"] = 2,
51+
["configuration_issues_analyzer"] = 1,
52+
["latch_stats_collector"] = 1,
53+
["spinlock_stats_collector"] = 1,
54+
["tempdb_stats_collector"] = 1,
55+
["plan_cache_stats_collector"] = 2,
56+
["session_stats_collector"] = 1,
57+
["waiting_tasks_collector"] = 1,
58+
["running_jobs_collector"] = 2
59+
},
60+
["Balanced"] = new(StringComparer.OrdinalIgnoreCase)
61+
{
62+
["wait_stats_collector"] = 1,
63+
["query_stats_collector"] = 2,
64+
["memory_stats_collector"] = 1,
65+
["memory_pressure_events_collector"] = 1,
66+
["system_health_collector"] = 5,
67+
["blocked_process_xml_collector"] = 1,
68+
["deadlock_xml_collector"] = 1,
69+
["process_blocked_process_xml"] = 5,
70+
["blocking_deadlock_analyzer"] = 5,
71+
["process_deadlock_xml"] = 5,
72+
["query_store_collector"] = 2,
73+
["procedure_stats_collector"] = 2,
74+
["query_snapshots_collector"] = 1,
75+
["file_io_stats_collector"] = 1,
76+
["memory_grant_stats_collector"] = 1,
77+
["cpu_scheduler_stats_collector"] = 1,
78+
["memory_clerks_stats_collector"] = 5,
79+
["perfmon_stats_collector"] = 5,
80+
["cpu_utilization_stats_collector"] = 1,
81+
["trace_analysis_collector"] = 2,
82+
["default_trace_collector"] = 5,
83+
["configuration_issues_analyzer"] = 1,
84+
["latch_stats_collector"] = 1,
85+
["spinlock_stats_collector"] = 1,
86+
["tempdb_stats_collector"] = 1,
87+
["plan_cache_stats_collector"] = 5,
88+
["session_stats_collector"] = 1,
89+
["waiting_tasks_collector"] = 1,
90+
["running_jobs_collector"] = 1
91+
},
92+
["Low-Impact"] = new(StringComparer.OrdinalIgnoreCase)
93+
{
94+
["wait_stats_collector"] = 5,
95+
["query_stats_collector"] = 10,
96+
["memory_stats_collector"] = 10,
97+
["memory_pressure_events_collector"] = 5,
98+
["system_health_collector"] = 15,
99+
["blocked_process_xml_collector"] = 5,
100+
["deadlock_xml_collector"] = 5,
101+
["process_blocked_process_xml"] = 10,
102+
["blocking_deadlock_analyzer"] = 10,
103+
["process_deadlock_xml"] = 10,
104+
["query_store_collector"] = 30,
105+
["procedure_stats_collector"] = 10,
106+
["query_snapshots_collector"] = 5,
107+
["file_io_stats_collector"] = 10,
108+
["memory_grant_stats_collector"] = 5,
109+
["cpu_scheduler_stats_collector"] = 5,
110+
["memory_clerks_stats_collector"] = 30,
111+
["perfmon_stats_collector"] = 5,
112+
["cpu_utilization_stats_collector"] = 5,
113+
["trace_analysis_collector"] = 10,
114+
["default_trace_collector"] = 15,
115+
["configuration_issues_analyzer"] = 5,
116+
["latch_stats_collector"] = 5,
117+
["spinlock_stats_collector"] = 5,
118+
["tempdb_stats_collector"] = 5,
119+
["plan_cache_stats_collector"] = 15,
120+
["session_stats_collector"] = 5,
121+
["waiting_tasks_collector"] = 5,
122+
["running_jobs_collector"] = 30
123+
}
124+
};
23125

24126
public CollectorScheduleWindow(DatabaseService databaseService)
25127
{
@@ -59,6 +161,7 @@ private async System.Threading.Tasks.Task LoadSchedulesAsync()
59161
}
60162

61163
ScheduleDataGrid.ItemsSource = _schedules;
164+
DetectActivePreset();
62165
}
63166
catch (Exception ex)
64167
{
@@ -71,6 +174,101 @@ private async System.Threading.Tasks.Task LoadSchedulesAsync()
71174
}
72175
}
73176

177+
private void DetectActivePreset()
178+
{
179+
if (_schedules == null) return;
180+
181+
_suppressPresetChange = true;
182+
try
183+
{
184+
var currentIntervals = _schedules
185+
.Where(s => s.FrequencyMinutes < 1440)
186+
.ToDictionary(s => s.CollectorName, s => s.FrequencyMinutes, StringComparer.OrdinalIgnoreCase);
187+
188+
foreach (var (presetName, presetIntervals) in Presets)
189+
{
190+
bool matches = true;
191+
foreach (var (collector, freq) in presetIntervals)
192+
{
193+
if (currentIntervals.TryGetValue(collector, out int current) && current != freq)
194+
{
195+
matches = false;
196+
break;
197+
}
198+
}
199+
200+
if (matches)
201+
{
202+
for (int i = 0; i < PresetComboBox.Items.Count; i++)
203+
{
204+
if (PresetComboBox.Items[i] is ComboBoxItem item &&
205+
string.Equals(item.Content?.ToString(), presetName, StringComparison.OrdinalIgnoreCase))
206+
{
207+
PresetComboBox.SelectedIndex = i;
208+
return;
209+
}
210+
}
211+
}
212+
}
213+
214+
/* No preset matched */
215+
PresetComboBox.SelectedIndex = 0;
216+
}
217+
finally
218+
{
219+
_suppressPresetChange = false;
220+
}
221+
}
222+
223+
private async void PresetComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
224+
{
225+
if (_suppressPresetChange) return;
226+
if (PresetComboBox.SelectedItem is not ComboBoxItem selected) return;
227+
228+
string presetName = selected.Content?.ToString() ?? "";
229+
if (presetName == "Custom") return;
230+
231+
var result = MessageBox.Show(
232+
$"Apply the \"{presetName}\" preset?\n\nThis will change all collector frequencies. Enabled/disabled state and retention settings are not affected.",
233+
"Apply Collection Preset",
234+
MessageBoxButton.YesNo,
235+
MessageBoxImage.Question
236+
);
237+
238+
if (result != MessageBoxResult.Yes)
239+
{
240+
DetectActivePreset();
241+
return;
242+
}
243+
244+
try
245+
{
246+
await _databaseService.ApplyCollectionPresetAsync(presetName);
247+
248+
/* Unsubscribe, reload, resubscribe */
249+
if (_schedules != null)
250+
{
251+
foreach (var schedule in _schedules)
252+
{
253+
schedule.PropertyChanged -= Schedule_PropertyChanged;
254+
}
255+
}
256+
257+
await LoadSchedulesAsync();
258+
}
259+
catch (Exception ex)
260+
{
261+
MessageBox.Show(
262+
$"Failed to apply preset:\n\n{ex.Message}",
263+
"Error Applying Preset",
264+
MessageBoxButton.OK,
265+
MessageBoxImage.Error
266+
);
267+
268+
DetectActivePreset();
269+
}
270+
}
271+
74272
private async void Schedule_PropertyChanged(object? sender, PropertyChangedEventArgs e)
75273
{
76274
if (sender is CollectorScheduleItem schedule)
@@ -88,6 +286,11 @@ await _databaseService.UpdateCollectorScheduleAsync(
88286
schedule.FrequencyMinutes,
89287
schedule.RetentionDays
90288
);
289+
290+
if (e.PropertyName == nameof(CollectorScheduleItem.FrequencyMinutes))
291+
{
292+
DetectActivePreset();
293+
}
91294
}
92295
catch (Exception ex)
93296
{

Dashboard/Services/DatabaseService.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,5 +357,18 @@ public async Task UpdateCollectorScheduleAsync(int scheduleId, bool enabled, int
357357

358358
await command.ExecuteNonQueryAsync();
359359
}
360+
361+
public async Task ApplyCollectionPresetAsync(string presetName)
362+
{
363+
await using var tc = await OpenThrottledConnectionAsync();
364+
var connection = tc.Connection;
365+
366+
using var command = new SqlCommand("config.apply_collection_preset", connection);
367+
command.CommandType = System.Data.CommandType.StoredProcedure;
368+
command.CommandTimeout = 120;
369+
command.Parameters.Add(new SqlParameter("@preset_name", SqlDbType.NVarChar, 128) { Value = presetName });
370+
371+
await command.ExecuteNonQueryAsync();
372+
}
360373
}
361374
}

Lite/Services/ScheduleManager.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,39 @@ public class ScheduleManager
2828
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
2929
};
3030

31+
public static readonly string[] PresetNames = ["Low-Impact", "Balanced", "Aggressive"];
32+
33+
private static readonly Dictionary<string, Dictionary<string, int>> s_presets = new(StringComparer.OrdinalIgnoreCase)
34+
{
35+
["Aggressive"] = new(StringComparer.OrdinalIgnoreCase)
36+
{
37+
["wait_stats"] = 1, ["query_stats"] = 1, ["procedure_stats"] = 1,
38+
["query_store"] = 2, ["query_snapshots"] = 1, ["cpu_utilization"] = 1,
39+
["file_io_stats"] = 1, ["memory_stats"] = 1, ["memory_clerks"] = 2,
40+
["tempdb_stats"] = 1, ["perfmon_stats"] = 1, ["deadlocks"] = 1,
41+
["memory_grant_stats"] = 1, ["waiting_tasks"] = 1,
42+
["blocked_process_report"] = 1, ["running_jobs"] = 2
43+
},
44+
["Balanced"] = new(StringComparer.OrdinalIgnoreCase)
45+
{
46+
["wait_stats"] = 1, ["query_stats"] = 1, ["procedure_stats"] = 1,
47+
["query_store"] = 5, ["query_snapshots"] = 1, ["cpu_utilization"] = 1,
48+
["file_io_stats"] = 1, ["memory_stats"] = 1, ["memory_clerks"] = 5,
49+
["tempdb_stats"] = 1, ["perfmon_stats"] = 1, ["deadlocks"] = 1,
50+
["memory_grant_stats"] = 1, ["waiting_tasks"] = 1,
51+
["blocked_process_report"] = 1, ["running_jobs"] = 5
52+
},
53+
["Low-Impact"] = new(StringComparer.OrdinalIgnoreCase)
54+
{
55+
["wait_stats"] = 5, ["query_stats"] = 10, ["procedure_stats"] = 10,
56+
["query_store"] = 30, ["query_snapshots"] = 5, ["cpu_utilization"] = 5,
57+
["file_io_stats"] = 10, ["memory_stats"] = 10, ["memory_clerks"] = 30,
58+
["tempdb_stats"] = 5, ["perfmon_stats"] = 5, ["deadlocks"] = 5,
59+
["memory_grant_stats"] = 5, ["waiting_tasks"] = 5,
60+
["blocked_process_report"] = 5, ["running_jobs"] = 30
61+
}
62+
};
63+
3164
private readonly string _schedulePath;
3265
private readonly ILogger<ScheduleManager>? _logger;
3366
private List<CollectorSchedule> _schedules;
@@ -160,6 +193,61 @@ public void UpdateSchedule(string collectorName, bool? enabled = null, int? freq
160193
}
161194
}
162195

196+
/// <summary>
197+
/// Detects which preset matches the current intervals, or returns "Custom".
198+
/// </summary>
199+
public string GetActivePreset()
200+
{
201+
lock (_lock)
202+
{
203+
foreach (var (presetName, intervals) in s_presets)
204+
{
205+
bool matches = true;
206+
foreach (var (collector, freq) in intervals)
207+
{
208+
var schedule = _schedules.FirstOrDefault(s =>
209+
s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase));
210+
if (schedule != null && schedule.FrequencyMinutes != freq)
211+
{
212+
matches = false;
213+
break;
214+
}
215+
}
216+
if (matches) return presetName;
217+
}
218+
return "Custom";
219+
}
220+
}
221+
222+
/// <summary>
223+
/// Applies a named preset, changing all scheduled collector frequencies.
224+
/// Does not modify enabled/disabled state or on-load (frequency=0) collectors.
225+
/// </summary>
226+
public void ApplyPreset(string presetName)
227+
{
228+
if (!s_presets.TryGetValue(presetName, out var intervals))
229+
{
230+
throw new ArgumentException($"Unknown preset: {presetName}");
231+
}
232+
233+
lock (_lock)
234+
{
235+
foreach (var (collector, freq) in intervals)
236+
{
237+
var schedule = _schedules.FirstOrDefault(s =>
238+
s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase));
239+
if (schedule != null)
240+
{
241+
schedule.FrequencyMinutes = freq;
242+
}
243+
}
244+
245+
SaveSchedules();
246+
247+
_logger?.LogInformation("Applied collection preset '{Preset}'", presetName);
248+
}
249+
}
250+
163251
/// <summary>
164252
/// Loads schedules from the JSON config file.
165253
/// </summary>

0 commit comments

Comments
 (0)