diff --git a/Lite/App.xaml.cs b/Lite/App.xaml.cs index 8583a08..0164df1 100644 --- a/Lite/App.xaml.cs +++ b/Lite/App.xaml.cs @@ -17,6 +17,14 @@ namespace PerformanceMonitorLite; +public enum CpuAlertMode +{ + /// sql_server_cpu + other_process_cpu — matches OS user+system, "is the box in trouble". + Total, + /// SQL Server scheduler ProcessUtilization only. + SqlOnly +} + public partial class App : Application { [DllImport("shell32.dll", SetLastError = true)] @@ -71,6 +79,8 @@ public partial class App : Application public static bool NotifyConnectionChanges { get; set; } = true; public static bool AlertCpuEnabled { get; set; } = true; public static int AlertCpuThreshold { get; set; } = 80; + /// Which CPU metric the alert evaluates against. Total = sql_server_cpu + other_process_cpu (matches OS user+system). SqlOnly = SQL Server scheduler %. + public static CpuAlertMode AlertCpuMode { get; set; } = CpuAlertMode.Total; public static bool AlertBlockingEnabled { get; set; } = true; public static int AlertBlockingThreshold { get; set; } = 1; public static bool AlertDeadlockEnabled { get; set; } = true; @@ -323,6 +333,8 @@ public static void LoadAlertSettings() if (root.TryGetProperty("notify_connection_changes", out v)) NotifyConnectionChanges = v.GetBoolean(); if (root.TryGetProperty("alert_cpu_enabled", out v)) AlertCpuEnabled = v.GetBoolean(); if (root.TryGetProperty("alert_cpu_threshold", out v)) AlertCpuThreshold = v.GetInt32(); + if (root.TryGetProperty("alert_cpu_mode", out v) && Enum.TryParse(v.GetString(), out var mode)) + AlertCpuMode = mode; if (root.TryGetProperty("alert_blocking_enabled", out v)) AlertBlockingEnabled = v.GetBoolean(); if (root.TryGetProperty("alert_blocking_threshold", out v)) AlertBlockingThreshold = v.GetInt32(); if (root.TryGetProperty("alert_deadlock_enabled", out v)) AlertDeadlockEnabled = v.GetBoolean(); diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index 59819af..2825462 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -1288,10 +1288,12 @@ private async void CheckPerformanceAlerts(ServerSummaryItem summary) /* Skip popup/email alerts if user has acknowledged or silenced this server */ bool suppressPopups = !_alertStateService.ShouldShowAlerts(key); - /* CPU alerts */ + /* CPU alerts — uses the metric the user selected (Total non-idle CPU by default, or SQL Server only). */ + var alertCpuValue = summary.CpuPercentForAlert; + string cpuMetricLabel = App.AlertCpuMode == CpuAlertMode.Total ? "Total CPU" : "SQL CPU"; bool cpuExceeded = App.AlertCpuEnabled - && summary.CpuPercent.HasValue - && summary.CpuPercent.Value >= App.AlertCpuThreshold; + && alertCpuValue.HasValue + && alertCpuValue.Value >= App.AlertCpuThreshold; if (cpuExceeded) { @@ -1306,16 +1308,16 @@ private async void CheckPerformanceAlerts(ServerSummaryItem summary) { _trayService.ShowNotification( "High CPU", - $"{summary.DisplayName}: CPU at {summary.CpuPercent:F0}% (threshold: {App.AlertCpuThreshold}%)", + $"{summary.DisplayName}: {cpuMetricLabel} at {alertCpuValue:F0}% (threshold: {App.AlertCpuThreshold}%)", Hardcodet.Wpf.TaskbarNotification.BalloonIcon.Warning); } - var cpuDetailText = $" CPU: {summary.CpuPercent:F0}%\n Threshold: {App.AlertCpuThreshold}%"; + var cpuDetailText = $" {cpuMetricLabel}: {alertCpuValue:F0}%\n Threshold: {App.AlertCpuThreshold}%"; await _emailAlertService.TrySendAlertEmailAsync( "High CPU", summary.DisplayName, - $"{summary.CpuPercent:F0}%", + $"{alertCpuValue:F0}%", $"{App.AlertCpuThreshold}%", summary.ServerId, muted: isMuted, @@ -1327,7 +1329,7 @@ await _emailAlertService.TrySendAlertEmailAsync( _activeCpuAlert[key] = false; _trayService.ShowNotification( "CPU Resolved", - $"{summary.DisplayName}: CPU back to {summary.CpuPercent:F0}%", + $"{summary.DisplayName}: {cpuMetricLabel} back to {alertCpuValue:F0}%", Hardcodet.Wpf.TaskbarNotification.BalloonIcon.Info); } diff --git a/Lite/Services/LocalDataService.Overview.cs b/Lite/Services/LocalDataService.Overview.cs index eee8a89..104b9e2 100644 --- a/Lite/Services/LocalDataService.Overview.cs +++ b/Lite/Services/LocalDataService.Overview.cs @@ -24,16 +24,18 @@ public partial class LocalDataService using var connection = await OpenConnectionAsync(); double? cpuPercent = null; + double? otherProcessCpuPercent = null; double? memoryMb = null; int blockingCount = 0; int deadlockCount = 0; DateTime? lastCollection = null; - /* Latest CPU */ + /* Latest CPU — read both SQL Server CPU and other-process CPU so the UI can surface + total non-idle CPU alongside the SQL-only number. */ using (var cmd = connection.CreateCommand()) { cmd.CommandText = @" -SELECT sqlserver_cpu_utilization, sample_time +SELECT sqlserver_cpu_utilization, other_process_cpu_utilization, sample_time FROM v_cpu_utilization_stats WHERE server_id = $1 ORDER BY sample_time DESC @@ -43,7 +45,8 @@ ORDER BY sample_time DESC if (await reader.ReadAsync()) { cpuPercent = reader.IsDBNull(0) ? null : ToDouble(reader.GetValue(0)); - lastCollection = reader.IsDBNull(1) ? null : reader.GetDateTime(1); + otherProcessCpuPercent = reader.IsDBNull(1) ? null : ToDouble(reader.GetValue(1)); + lastCollection = reader.IsDBNull(2) ? null : reader.GetDateTime(2); } } @@ -112,6 +115,7 @@ FROM v_collection_log DisplayName = displayName, ServerId = serverId, CpuPercent = cpuPercent, + OtherProcessCpuPercent = otherProcessCpuPercent, MemoryMb = memoryMb, BlockingCount = blockingCount, DeadlockCount = deadlockCount, @@ -128,13 +132,34 @@ public class ServerSummaryItem public bool? IsOnline { get; set; } /// True when the server is reachable but one or more collectors have consecutive errors. public bool HasCollectorErrors { get; set; } + /// SQL Server scheduler ProcessUtilization from sys.dm_os_ring_buffers. NULL on Azure SQL DB. public double? CpuPercent { get; set; } + /// Non-SQL-Server CPU on the host (computed as 100 - SystemIdle - ProcessUtilization). NULL on Azure SQL DB. + public double? OtherProcessCpuPercent { get; set; } + /// Total non-idle CPU on the host = sql_server + other_process. Tracks closer to OS user+system counters. + public double? TotalCpuPercent => + CpuPercent.HasValue ? CpuPercent.Value + (OtherProcessCpuPercent ?? 0) : null; + /// The CPU value the alert evaluator and headline display use. Driven by App.AlertCpuMode. + public double? CpuPercentForAlert => + App.AlertCpuMode == CpuAlertMode.Total ? (TotalCpuPercent ?? CpuPercent) : CpuPercent; public double? MemoryMb { get; set; } public int BlockingCount { get; set; } public int DeadlockCount { get; set; } public DateTime? LastCollectionTime { get; set; } - public string CpuDisplay => CpuPercent.HasValue ? $"{CpuPercent:F0}%" : "--"; + /// + /// Headline CPU display. Shows total non-idle CPU prominently with the SQL-only number alongside, + /// e.g. "64% (SQL 60%)". Falls back to a single number when only one value is available. + /// + public string CpuDisplay + { + get + { + if (!CpuPercent.HasValue) return "--"; + if (!OtherProcessCpuPercent.HasValue) return $"{CpuPercent:F0}%"; + return $"{TotalCpuPercent:F0}% (SQL {CpuPercent:F0}%)"; + } + } public string MemoryDisplay => MemoryMb.HasValue ? $"{MemoryMb / 1024.0:F1} GB" : "--"; public string BlockingDisplay => BlockingCount > 0 ? BlockingCount.ToString() : "0"; public string DeadlockDisplay => DeadlockCount > 0 ? DeadlockCount.ToString() : "0"; @@ -158,14 +183,21 @@ public class ServerSummaryItem public bool IsOffline => IsOnline == false; /* Color coding */ - public SolidColorBrush CpuBrush => MakeBrush(CpuPercent >= 80 ? "#E57373" : CpuPercent >= 50 ? "#FFB74D" : "#81C784"); + public SolidColorBrush CpuBrush + { + get + { + var v = CpuPercentForAlert; + return MakeBrush(v >= 80 ? "#E57373" : v >= 50 ? "#FFB74D" : "#81C784"); + } + } public SolidColorBrush BlockingBrush => MakeBrush(BlockingCount > 0 ? "#FFB74D" : "#81C784"); public SolidColorBrush DeadlockBrush => MakeBrush(DeadlockCount > 0 ? "#E57373" : "#81C784"); public SolidColorBrush CardBorderBrush => MakeBrush( IsOnline == false ? "#E57373" : DeadlockCount > 0 ? "#E57373" : BlockingCount > 0 ? "#FFB74D" : - CpuPercent >= 80 ? "#FFB74D" : + CpuPercentForAlert >= 80 ? "#FFB74D" : HasCollectorErrors ? "#FFD54F" : // amber border when collectors are failing "#2a2d35"); diff --git a/Lite/Windows/SettingsWindow.xaml b/Lite/Windows/SettingsWindow.xaml index f10c76e..47fe448 100644 --- a/Lite/Windows/SettingsWindow.xaml +++ b/Lite/Windows/SettingsWindow.xaml @@ -161,9 +161,12 @@ - + + + + + 0 && cpu <= 100) App.AlertCpuThreshold = cpu; + App.AlertCpuMode = AlertCpuModeBox.SelectedIndex == 1 ? CpuAlertMode.SqlOnly : CpuAlertMode.Total; App.AlertBlockingEnabled = AlertBlockingCheckBox.IsChecked == true; if (int.TryParse(AlertBlockingThresholdBox.Text, out var blocking) && blocking > 0) App.AlertBlockingThreshold = blocking; @@ -675,6 +677,7 @@ private bool SaveAlertSettings() root["notify_connection_changes"] = App.NotifyConnectionChanges; root["alert_cpu_enabled"] = App.AlertCpuEnabled; root["alert_cpu_threshold"] = App.AlertCpuThreshold; + root["alert_cpu_mode"] = App.AlertCpuMode.ToString(); root["alert_blocking_enabled"] = App.AlertBlockingEnabled; root["alert_blocking_threshold"] = App.AlertBlockingThreshold; root["alert_deadlock_enabled"] = App.AlertDeadlockEnabled; @@ -728,6 +731,7 @@ private void AlertsEnabledCheckBox_Changed(object sender, RoutedEventArgs e) private void RestoreAlertDefaultsButton_Click(object sender, RoutedEventArgs e) { AlertCpuThresholdBox.Text = "80"; + AlertCpuModeBox.SelectedIndex = 0; // Total AlertBlockingThresholdBox.Text = "1"; AlertDeadlockThresholdBox.Text = "1"; AlertPoisonWaitThresholdBox.Text = "500"; @@ -753,7 +757,10 @@ private void UpdateAlertPreviewText() var parts = new System.Collections.Generic.List(); if (AlertCpuCheckBox.IsChecked == true) - parts.Add($"CPU > {AlertCpuThresholdBox.Text}%"); + { + string cpuLabel = AlertCpuModeBox.SelectedIndex == 1 ? "SQL CPU" : "Total CPU"; + parts.Add($"{cpuLabel} > {AlertCpuThresholdBox.Text}%"); + } if (AlertBlockingCheckBox.IsChecked == true) parts.Add($"blocking >= {AlertBlockingThresholdBox.Text}"); if (AlertDeadlockCheckBox.IsChecked == true) @@ -778,6 +785,7 @@ private void UpdateAlertControlStates() NotifyConnectionCheckBox.IsEnabled = enabled; AlertCpuCheckBox.IsEnabled = enabled; AlertCpuThresholdBox.IsEnabled = enabled; + AlertCpuModeBox.IsEnabled = enabled; AlertBlockingCheckBox.IsEnabled = enabled; AlertBlockingThresholdBox.IsEnabled = enabled; AlertDeadlockCheckBox.IsEnabled = enabled;