Skip to content

Commit 5f0248e

Browse files
Merge pull request erikdarlingdata#902 from erikdarlingdata/feature/899-total-non-idle-cpu-alert
Lite: surface total non-idle CPU + alert toggle (closes erikdarlingdata#899)
2 parents 8d3ca32 + e3d2d5b commit 5f0248e

5 files changed

Lines changed: 74 additions & 17 deletions

File tree

Lite/App.xaml.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717

1818
namespace PerformanceMonitorLite;
1919

20+
public enum CpuAlertMode
21+
{
22+
/// <summary>sql_server_cpu + other_process_cpu — matches OS user+system, "is the box in trouble".</summary>
23+
Total,
24+
/// <summary>SQL Server scheduler ProcessUtilization only.</summary>
25+
SqlOnly
26+
}
27+
2028
public partial class App : Application
2129
{
2230
[DllImport("shell32.dll", SetLastError = true)]
@@ -71,6 +79,8 @@ public partial class App : Application
7179
public static bool NotifyConnectionChanges { get; set; } = true;
7280
public static bool AlertCpuEnabled { get; set; } = true;
7381
public static int AlertCpuThreshold { get; set; } = 80;
82+
/// <summary>Which CPU metric the alert evaluates against. Total = sql_server_cpu + other_process_cpu (matches OS user+system). SqlOnly = SQL Server scheduler %.</summary>
83+
public static CpuAlertMode AlertCpuMode { get; set; } = CpuAlertMode.Total;
7484
public static bool AlertBlockingEnabled { get; set; } = true;
7585
public static int AlertBlockingThreshold { get; set; } = 1;
7686
public static bool AlertDeadlockEnabled { get; set; } = true;
@@ -323,6 +333,8 @@ public static void LoadAlertSettings()
323333
if (root.TryGetProperty("notify_connection_changes", out v)) NotifyConnectionChanges = v.GetBoolean();
324334
if (root.TryGetProperty("alert_cpu_enabled", out v)) AlertCpuEnabled = v.GetBoolean();
325335
if (root.TryGetProperty("alert_cpu_threshold", out v)) AlertCpuThreshold = v.GetInt32();
336+
if (root.TryGetProperty("alert_cpu_mode", out v) && Enum.TryParse<CpuAlertMode>(v.GetString(), out var mode))
337+
AlertCpuMode = mode;
326338
if (root.TryGetProperty("alert_blocking_enabled", out v)) AlertBlockingEnabled = v.GetBoolean();
327339
if (root.TryGetProperty("alert_blocking_threshold", out v)) AlertBlockingThreshold = v.GetInt32();
328340
if (root.TryGetProperty("alert_deadlock_enabled", out v)) AlertDeadlockEnabled = v.GetBoolean();

Lite/MainWindow.xaml.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,10 +1288,12 @@ private async void CheckPerformanceAlerts(ServerSummaryItem summary)
12881288
/* Skip popup/email alerts if user has acknowledged or silenced this server */
12891289
bool suppressPopups = !_alertStateService.ShouldShowAlerts(key);
12901290

1291-
/* CPU alerts */
1291+
/* CPU alerts — uses the metric the user selected (Total non-idle CPU by default, or SQL Server only). */
1292+
var alertCpuValue = summary.CpuPercentForAlert;
1293+
string cpuMetricLabel = App.AlertCpuMode == CpuAlertMode.Total ? "Total CPU" : "SQL CPU";
12921294
bool cpuExceeded = App.AlertCpuEnabled
1293-
&& summary.CpuPercent.HasValue
1294-
&& summary.CpuPercent.Value >= App.AlertCpuThreshold;
1295+
&& alertCpuValue.HasValue
1296+
&& alertCpuValue.Value >= App.AlertCpuThreshold;
12951297

12961298
if (cpuExceeded)
12971299
{
@@ -1306,16 +1308,16 @@ private async void CheckPerformanceAlerts(ServerSummaryItem summary)
13061308
{
13071309
_trayService.ShowNotification(
13081310
"High CPU",
1309-
$"{summary.DisplayName}: CPU at {summary.CpuPercent:F0}% (threshold: {App.AlertCpuThreshold}%)",
1311+
$"{summary.DisplayName}: {cpuMetricLabel} at {alertCpuValue:F0}% (threshold: {App.AlertCpuThreshold}%)",
13101312
Hardcodet.Wpf.TaskbarNotification.BalloonIcon.Warning);
13111313
}
13121314

1313-
var cpuDetailText = $" CPU: {summary.CpuPercent:F0}%\n Threshold: {App.AlertCpuThreshold}%";
1315+
var cpuDetailText = $" {cpuMetricLabel}: {alertCpuValue:F0}%\n Threshold: {App.AlertCpuThreshold}%";
13141316

13151317
await _emailAlertService.TrySendAlertEmailAsync(
13161318
"High CPU",
13171319
summary.DisplayName,
1318-
$"{summary.CpuPercent:F0}%",
1320+
$"{alertCpuValue:F0}%",
13191321
$"{App.AlertCpuThreshold}%",
13201322
summary.ServerId,
13211323
muted: isMuted,
@@ -1327,7 +1329,7 @@ await _emailAlertService.TrySendAlertEmailAsync(
13271329
_activeCpuAlert[key] = false;
13281330
_trayService.ShowNotification(
13291331
"CPU Resolved",
1330-
$"{summary.DisplayName}: CPU back to {summary.CpuPercent:F0}%",
1332+
$"{summary.DisplayName}: {cpuMetricLabel} back to {alertCpuValue:F0}%",
13311333
Hardcodet.Wpf.TaskbarNotification.BalloonIcon.Info);
13321334
}
13331335

Lite/Services/LocalDataService.Overview.cs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ public partial class LocalDataService
2424
using var connection = await OpenConnectionAsync();
2525

2626
double? cpuPercent = null;
27+
double? otherProcessCpuPercent = null;
2728
double? memoryMb = null;
2829
int blockingCount = 0;
2930
int deadlockCount = 0;
3031
DateTime? lastCollection = null;
3132

32-
/* Latest CPU */
33+
/* Latest CPU — read both SQL Server CPU and other-process CPU so the UI can surface
34+
total non-idle CPU alongside the SQL-only number. */
3335
using (var cmd = connection.CreateCommand())
3436
{
3537
cmd.CommandText = @"
36-
SELECT sqlserver_cpu_utilization, sample_time
38+
SELECT sqlserver_cpu_utilization, other_process_cpu_utilization, sample_time
3739
FROM v_cpu_utilization_stats
3840
WHERE server_id = $1
3941
ORDER BY sample_time DESC
@@ -43,7 +45,8 @@ ORDER BY sample_time DESC
4345
if (await reader.ReadAsync())
4446
{
4547
cpuPercent = reader.IsDBNull(0) ? null : ToDouble(reader.GetValue(0));
46-
lastCollection = reader.IsDBNull(1) ? null : reader.GetDateTime(1);
48+
otherProcessCpuPercent = reader.IsDBNull(1) ? null : ToDouble(reader.GetValue(1));
49+
lastCollection = reader.IsDBNull(2) ? null : reader.GetDateTime(2);
4750
}
4851
}
4952

@@ -112,6 +115,7 @@ FROM v_collection_log
112115
DisplayName = displayName,
113116
ServerId = serverId,
114117
CpuPercent = cpuPercent,
118+
OtherProcessCpuPercent = otherProcessCpuPercent,
115119
MemoryMb = memoryMb,
116120
BlockingCount = blockingCount,
117121
DeadlockCount = deadlockCount,
@@ -128,13 +132,34 @@ public class ServerSummaryItem
128132
public bool? IsOnline { get; set; }
129133
/// <summary>True when the server is reachable but one or more collectors have consecutive errors.</summary>
130134
public bool HasCollectorErrors { get; set; }
135+
/// <summary>SQL Server scheduler ProcessUtilization from sys.dm_os_ring_buffers. NULL on Azure SQL DB.</summary>
131136
public double? CpuPercent { get; set; }
137+
/// <summary>Non-SQL-Server CPU on the host (computed as 100 - SystemIdle - ProcessUtilization). NULL on Azure SQL DB.</summary>
138+
public double? OtherProcessCpuPercent { get; set; }
139+
/// <summary>Total non-idle CPU on the host = sql_server + other_process. Tracks closer to OS user+system counters.</summary>
140+
public double? TotalCpuPercent =>
141+
CpuPercent.HasValue ? CpuPercent.Value + (OtherProcessCpuPercent ?? 0) : null;
142+
/// <summary>The CPU value the alert evaluator and headline display use. Driven by App.AlertCpuMode.</summary>
143+
public double? CpuPercentForAlert =>
144+
App.AlertCpuMode == CpuAlertMode.Total ? (TotalCpuPercent ?? CpuPercent) : CpuPercent;
132145
public double? MemoryMb { get; set; }
133146
public int BlockingCount { get; set; }
134147
public int DeadlockCount { get; set; }
135148
public DateTime? LastCollectionTime { get; set; }
136149

137-
public string CpuDisplay => CpuPercent.HasValue ? $"{CpuPercent:F0}%" : "--";
150+
/// <summary>
151+
/// Headline CPU display. Shows total non-idle CPU prominently with the SQL-only number alongside,
152+
/// e.g. "64% (SQL 60%)". Falls back to a single number when only one value is available.
153+
/// </summary>
154+
public string CpuDisplay
155+
{
156+
get
157+
{
158+
if (!CpuPercent.HasValue) return "--";
159+
if (!OtherProcessCpuPercent.HasValue) return $"{CpuPercent:F0}%";
160+
return $"{TotalCpuPercent:F0}% (SQL {CpuPercent:F0}%)";
161+
}
162+
}
138163
public string MemoryDisplay => MemoryMb.HasValue ? $"{MemoryMb / 1024.0:F1} GB" : "--";
139164
public string BlockingDisplay => BlockingCount > 0 ? BlockingCount.ToString() : "0";
140165
public string DeadlockDisplay => DeadlockCount > 0 ? DeadlockCount.ToString() : "0";
@@ -158,14 +183,21 @@ public class ServerSummaryItem
158183
public bool IsOffline => IsOnline == false;
159184

160185
/* Color coding */
161-
public SolidColorBrush CpuBrush => MakeBrush(CpuPercent >= 80 ? "#E57373" : CpuPercent >= 50 ? "#FFB74D" : "#81C784");
186+
public SolidColorBrush CpuBrush
187+
{
188+
get
189+
{
190+
var v = CpuPercentForAlert;
191+
return MakeBrush(v >= 80 ? "#E57373" : v >= 50 ? "#FFB74D" : "#81C784");
192+
}
193+
}
162194
public SolidColorBrush BlockingBrush => MakeBrush(BlockingCount > 0 ? "#FFB74D" : "#81C784");
163195
public SolidColorBrush DeadlockBrush => MakeBrush(DeadlockCount > 0 ? "#E57373" : "#81C784");
164196
public SolidColorBrush CardBorderBrush => MakeBrush(
165197
IsOnline == false ? "#E57373" :
166198
DeadlockCount > 0 ? "#E57373" :
167199
BlockingCount > 0 ? "#FFB74D" :
168-
CpuPercent >= 80 ? "#FFB74D" :
200+
CpuPercentForAlert >= 80 ? "#FFB74D" :
169201
HasCollectorErrors ? "#FFD54F" : // amber border when collectors are failing
170202
"#2a2d35");
171203

Lite/Windows/SettingsWindow.xaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,12 @@
161161
<TextBox x:Name="AlertCpuThresholdBox" Width="45" Text="80" Margin="6,0,0,0" VerticalAlignment="Center"/>
162162
<TextBlock Text="%" VerticalAlignment="Center" Margin="2,0,0,0"
163163
Foreground="{DynamicResource ForegroundBrush}"/>
164-
<TextBlock Text="(default 80% — Lite collects directly from DMVs so catches spikes faster than Dashboard)"
165-
FontSize="10" FontStyle="Italic" Foreground="{DynamicResource ForegroundMutedBrush}"
166-
VerticalAlignment="Center" Margin="8,0,0,0"/>
164+
<TextBlock Text="measured as" VerticalAlignment="Center" Margin="8,0,4,0"
165+
Foreground="{DynamicResource ForegroundBrush}"/>
166+
<ComboBox x:Name="AlertCpuModeBox" Width="220" VerticalAlignment="Center">
167+
<ComboBoxItem Content="Total non-idle CPU (SQL + other processes)" Tag="Total"/>
168+
<ComboBoxItem Content="SQL Server only (scheduler %)" Tag="SqlOnly"/>
169+
</ComboBox>
167170
</StackPanel>
168171
<StackPanel Orientation="Horizontal" Margin="20,6,0,0">
169172
<CheckBox x:Name="AlertBlockingCheckBox" Content="Blocking sessions &#x2265;" VerticalAlignment="Center"

Lite/Windows/SettingsWindow.xaml.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ private void LoadAlertSettings()
576576
NotifyConnectionCheckBox.IsChecked = App.NotifyConnectionChanges;
577577
AlertCpuCheckBox.IsChecked = App.AlertCpuEnabled;
578578
AlertCpuThresholdBox.Text = App.AlertCpuThreshold.ToString();
579+
AlertCpuModeBox.SelectedIndex = App.AlertCpuMode == CpuAlertMode.SqlOnly ? 1 : 0;
579580
AlertBlockingCheckBox.IsChecked = App.AlertBlockingEnabled;
580581
AlertBlockingThresholdBox.Text = App.AlertBlockingThreshold.ToString();
581582
AlertDeadlockCheckBox.IsChecked = App.AlertDeadlockEnabled;
@@ -615,6 +616,7 @@ private bool SaveAlertSettings()
615616
App.AlertCpuEnabled = AlertCpuCheckBox.IsChecked == true;
616617
if (int.TryParse(AlertCpuThresholdBox.Text, out var cpu) && cpu > 0 && cpu <= 100)
617618
App.AlertCpuThreshold = cpu;
619+
App.AlertCpuMode = AlertCpuModeBox.SelectedIndex == 1 ? CpuAlertMode.SqlOnly : CpuAlertMode.Total;
618620
App.AlertBlockingEnabled = AlertBlockingCheckBox.IsChecked == true;
619621
if (int.TryParse(AlertBlockingThresholdBox.Text, out var blocking) && blocking > 0)
620622
App.AlertBlockingThreshold = blocking;
@@ -675,6 +677,7 @@ private bool SaveAlertSettings()
675677
root["notify_connection_changes"] = App.NotifyConnectionChanges;
676678
root["alert_cpu_enabled"] = App.AlertCpuEnabled;
677679
root["alert_cpu_threshold"] = App.AlertCpuThreshold;
680+
root["alert_cpu_mode"] = App.AlertCpuMode.ToString();
678681
root["alert_blocking_enabled"] = App.AlertBlockingEnabled;
679682
root["alert_blocking_threshold"] = App.AlertBlockingThreshold;
680683
root["alert_deadlock_enabled"] = App.AlertDeadlockEnabled;
@@ -728,6 +731,7 @@ private void AlertsEnabledCheckBox_Changed(object sender, RoutedEventArgs e)
728731
private void RestoreAlertDefaultsButton_Click(object sender, RoutedEventArgs e)
729732
{
730733
AlertCpuThresholdBox.Text = "80";
734+
AlertCpuModeBox.SelectedIndex = 0; // Total
731735
AlertBlockingThresholdBox.Text = "1";
732736
AlertDeadlockThresholdBox.Text = "1";
733737
AlertPoisonWaitThresholdBox.Text = "500";
@@ -753,7 +757,10 @@ private void UpdateAlertPreviewText()
753757
var parts = new System.Collections.Generic.List<string>();
754758

755759
if (AlertCpuCheckBox.IsChecked == true)
756-
parts.Add($"CPU > {AlertCpuThresholdBox.Text}%");
760+
{
761+
string cpuLabel = AlertCpuModeBox.SelectedIndex == 1 ? "SQL CPU" : "Total CPU";
762+
parts.Add($"{cpuLabel} > {AlertCpuThresholdBox.Text}%");
763+
}
757764
if (AlertBlockingCheckBox.IsChecked == true)
758765
parts.Add($"blocking >= {AlertBlockingThresholdBox.Text}");
759766
if (AlertDeadlockCheckBox.IsChecked == true)
@@ -778,6 +785,7 @@ private void UpdateAlertControlStates()
778785
NotifyConnectionCheckBox.IsEnabled = enabled;
779786
AlertCpuCheckBox.IsEnabled = enabled;
780787
AlertCpuThresholdBox.IsEnabled = enabled;
788+
AlertCpuModeBox.IsEnabled = enabled;
781789
AlertBlockingCheckBox.IsEnabled = enabled;
782790
AlertBlockingThresholdBox.IsEnabled = enabled;
783791
AlertDeadlockCheckBox.IsEnabled = enabled;

0 commit comments

Comments
 (0)