Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions Dashboard/CollectorScheduleWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,24 @@
<!-- Header -->
<TextBlock Grid.Row="0" Text="Collector Schedules" FontWeight="Bold" FontSize="16"
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,4"/>
<TextBlock Grid.Row="1" Text="Configure collection frequency and data retention for each collector."
Foreground="{DynamicResource ForegroundMutedBrush}" FontSize="12" Margin="0,0,0,12"/>
<Grid Grid.Row="1" Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Configure collection frequency and data retention for each collector."
Foreground="{DynamicResource ForegroundMutedBrush}" FontSize="12" VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Horizontal">
<TextBlock Text="Preset:" Foreground="{DynamicResource ForegroundBrush}" FontSize="12"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<ComboBox x:Name="PresetComboBox" Width="140" SelectionChanged="PresetComboBox_SelectionChanged">
<ComboBoxItem Content="Custom" IsEnabled="False"/>
<ComboBoxItem Content="Low-Impact"/>
<ComboBoxItem Content="Balanced"/>
<ComboBoxItem Content="Aggressive"/>
</ComboBox>
</StackPanel>
</Grid>

<!-- Schedule Grid -->
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="4">
Expand Down
203 changes: 203 additions & 0 deletions Dashboard/CollectorScheduleWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using PerformanceMonitorDashboard.Models;
using PerformanceMonitorDashboard.Services;

Expand All @@ -20,6 +21,107 @@ public partial class CollectorScheduleWindow : Window
{
private readonly DatabaseService _databaseService;
private List<CollectorScheduleItem>? _schedules;
private bool _suppressPresetChange;

private static readonly Dictionary<string, Dictionary<string, int>> 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)
{
Expand Down Expand Up @@ -59,6 +161,7 @@ private async System.Threading.Tasks.Task LoadSchedulesAsync()
}

ScheduleDataGrid.ItemsSource = _schedules;
DetectActivePreset();
}
catch (Exception ex)
{
Expand All @@ -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)
Expand All @@ -88,6 +286,11 @@ await _databaseService.UpdateCollectorScheduleAsync(
schedule.FrequencyMinutes,
schedule.RetentionDays
);

if (e.PropertyName == nameof(CollectorScheduleItem.FrequencyMinutes))
{
DetectActivePreset();
}
}
catch (Exception ex)
{
Expand Down
13 changes: 13 additions & 0 deletions Dashboard/Services/DatabaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
88 changes: 88 additions & 0 deletions Lite/Services/ScheduleManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,39 @@ public class ScheduleManager
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

public static readonly string[] PresetNames = ["Low-Impact", "Balanced", "Aggressive"];

private static readonly Dictionary<string, Dictionary<string, int>> 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<ScheduleManager>? _logger;
private List<CollectorSchedule> _schedules;
Expand Down Expand Up @@ -160,6 +193,61 @@ public void UpdateSchedule(string collectorName, bool? enabled = null, int? freq
}
}

/// <summary>
/// Detects which preset matches the current intervals, or returns "Custom".
/// </summary>
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";
}
}

/// <summary>
/// Applies a named preset, changing all scheduled collector frequencies.
/// Does not modify enabled/disabled state or on-load (frequency=0) collectors.
/// </summary>
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);
}
}

/// <summary>
/// Loads schedules from the JSON config file.
/// </summary>
Expand Down
Loading
Loading