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
22 changes: 16 additions & 6 deletions Darling/Darling.Tests/DarlingMcpHealthParserToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,12 @@ viewer entry point still delegates rather than growing an independent (drift-pro
[Fact]
public void Significance_SchedulerIssue_MatchesViewer()
{
foreach (var status in new[] { "WARNING", "OK", "", null })
foreach (var (sqlCpu, other, mem) in new (int?, int?, int?)[] { (90, 0, 100), (89, 0, 100), (0, 50, 100), (0, 49, 100), (0, 0, 50), (0, 0, 51), (null, null, null) })
{
var r = new SchedulerIssueRecord { Status = status };
var r = new SchedulerIssueRecord { SqlCpuUtilization = sqlCpu, OtherProcessCpu = other, MemoryUtilization = mem };
Assert.Equal(SystemEventSignificance.IsSignificant(r), SystemHealthSignificance.IsSignificant(r));
}
Assert.True(SystemHealthSignificance.IsSignificant(new SchedulerIssueRecord { Status = "WARNING" }));
Assert.True(SystemHealthSignificance.IsSignificant(new SchedulerIssueRecord { SqlCpuUtilization = 90 }));
}

[Fact]
Expand Down Expand Up @@ -311,7 +311,7 @@ public void RoundTrip_ShredFixturesThenGate_KeepsOnlySignificantRows()
// SystemHealthSignificance keeps the significant rows. Assert the same significant set the
// viewer's System Events tab surfaces.
Assert.True(SystemHealthSignificance.IsSignificant(
SystemHealthParser.ParseSchedulerIssue(LoadFixture("scheduler_monitor.xml"))!)); // WARNING
SystemHealthParser.ParseSchedulerIssue(LoadFixture("scheduler_monitor_high_sql_cpu.xml"))!)); // SQL CPU 94 >= 90
Assert.True(SystemHealthSignificance.IsSignificant(
SystemHealthParser.ParseSevereError(LoadFixture("error_reported.xml"))!)); // severity 24
Assert.True(SystemHealthSignificance.IsSignificant(
Expand Down Expand Up @@ -398,7 +398,7 @@ await DarlingMcpTestData.ExecAsync(connection, ct,
VALUES ($1,$2,$3,$4,$5,$6,$7)",
CollectionIdGenerator.Next(), t, ServerId, ServerName, t, eventType, LoadFixture(fixture));

await PlantEvent(SystemHealthParser.SchedulerMonitorEvent, "scheduler_monitor.xml");
await PlantEvent(SystemHealthParser.SchedulerMonitorEvent, "scheduler_monitor_high_sql_cpu.xml");
await PlantEvent(SystemHealthParser.ErrorReportedEvent, "error_reported.xml");
await PlantEvent(SystemHealthParser.SpServerDiagnosticsEvent, "sp_server_diagnostics_system.xml");
await PlantEvent(SystemHealthParser.SpServerDiagnosticsEvent, "sp_server_diagnostics_query_processing_warning.xml");
Expand Down Expand Up @@ -441,10 +441,20 @@ await DarlingMcpTestData.ExecAsync(connection, ct,
Assert.Contains("the absence is a measurement", broker.GetProperty("message").GetString()!, StringComparison.Ordinal);

/* The data envelope carries the same witness pair. */
var scheduler = JsonDocument.Parse(await DarlingMcpHealthParserTools.GetSchedulerIssues(postgres, ServerName)).RootElement;
var schedulerJson = await DarlingMcpHealthParserTools.GetSchedulerIssues(postgres, ServerName);
var scheduler = JsonDocument.Parse(schedulerJson).RootElement;
Assert.True(scheduler.GetProperty("source_observed").GetBoolean());
Assert.Equal(t.ToString("o"), scheduler.GetProperty("last_captured_at").GetString());

/* #4452: the utilization fields the planted high-CPU sample carries — SQL CPU pinned at 94,
other-process CPU 4, idle 2, memory 100 — come back on the tool's own output text. This is
the port's product-path pin: it asserts on the served JSON text (not typed properties) so it
also compiles, unchanged, against the pre-port code, where these fields are null or absent. */
Assert.Contains("\"sql_cpu_utilization\":94", schedulerJson, StringComparison.Ordinal);
Assert.Contains("\"other_process_cpu\":4", schedulerJson, StringComparison.Ordinal);
Assert.Contains("\"system_idle\":2", schedulerJson, StringComparison.Ordinal);
Assert.Contains("\"memory_utilization\":100", schedulerJson, StringComparison.Ordinal);

/* an unknown server resolves to the listing error. */
Assert.StartsWith("Could not resolve server.", McpHelpers.ErrorMessageOf(await DarlingMcpHealthParserTools.GetSystemHealth(postgres, "darling-no-such-server")), StringComparison.Ordinal);

Expand Down
10 changes: 0 additions & 10 deletions Darling/Darling.Tests/Fixtures/SystemHealth/scheduler_monitor.xml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- #4452: real scheduler_monitor_system_health_ring_buffer_recorded event from a monitored field
store, call_stack stripped. process_utilization=94, system_idle=73->2 (other-process CPU =
100-94-2=4), memory_utilization=100, page_faults=505, working_set_delta=4096 bytes ->
4096/1048576 = 0.0039... MB, rounded to 0.00. SqlCpuUtilization 94 >= 90, so this sample is
significant on the SQL-CPU arm. -->
<event name="scheduler_monitor_system_health_ring_buffer_recorded" package="sqlos" timestamp="2026-09-26T22:08:48.939Z">
<data name="id"><type name="uint32" package="package0"/><value>0</value></data>
<data name="timestamp"><type name="uint64" package="package0"/><value>0</value></data>
<data name="process_utilization"><type name="uint32" package="package0"/><value>94</value></data>
<data name="system_idle"><type name="uint32" package="package0"/><value>2</value></data>
<data name="user_mode_time"><type name="uint64" package="package0"/><value>2233593750</value></data>
<data name="kernel_mode_time"><type name="uint64" package="package0"/><value>30312500</value></data>
<data name="page_faults"><type name="uint32" package="package0"/><value>505</value></data>
<data name="working_set_delta"><type name="int64" package="package0"/><value>4096</value></data>
<data name="memory_utilization"><type name="uint32" package="package0"/><value>100</value></data>
</event>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- #4452: DERIVED from the real scheduler_monitor_normal.xml capture (same element shape; only
the memory_utilization <value> number was changed). No real captured sample crossed the
low-memory threshold, so this fixture exercises that arm: sql_cpu=3, system_idle=95 ->
other-process CPU = 100-3-95=2, memory_utilization=50, which is <= 50 and so significant on
the memory arm alone (sql_cpu 3 < 90, other-process CPU 2 < 50). -->
<event name="scheduler_monitor_system_health_ring_buffer_recorded" package="sqlos" timestamp="2026-09-26T23:59:53.232Z">
<data name="id"><type name="uint32" package="package0"/><value>0</value></data>
<data name="timestamp"><type name="uint64" package="package0"/><value>0</value></data>
<data name="process_utilization"><type name="uint32" package="package0"/><value>3</value></data>
<data name="system_idle"><type name="uint32" package="package0"/><value>95</value></data>
<data name="user_mode_time"><type name="uint64" package="package0"/><value>176250000</value></data>
<data name="kernel_mode_time"><type name="uint64" package="package0"/><value>7187500</value></data>
<data name="page_faults"><type name="uint32" package="package0"/><value>99</value></data>
<data name="working_set_delta"><type name="int64" package="package0"/><value>77824</value></data>
<data name="memory_utilization"><type name="uint32" package="package0"/><value>50</value></data>
</event>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- #4452: real scheduler_monitor_system_health_ring_buffer_recorded event from a monitored field
store, call_stack stripped. process_utilization=3, system_idle=95 -> other-process CPU =
100-3-95=2, memory_utilization=100, page_faults=99, working_set_delta=77824 bytes ->
77824/1048576 = 0.0742... MB, rounded to 0.07. Every arm is well inside its threshold
(3 < 90, 2 < 50, 100 > 50), so this sample is NOT significant. -->
<event name="scheduler_monitor_system_health_ring_buffer_recorded" package="sqlos" timestamp="2026-09-26T23:59:53.232Z">
<data name="id"><type name="uint32" package="package0"/><value>0</value></data>
<data name="timestamp"><type name="uint64" package="package0"/><value>0</value></data>
<data name="process_utilization"><type name="uint32" package="package0"/><value>3</value></data>
<data name="system_idle"><type name="uint32" package="package0"/><value>95</value></data>
<data name="user_mode_time"><type name="uint64" package="package0"/><value>176250000</value></data>
<data name="kernel_mode_time"><type name="uint64" package="package0"/><value>7187500</value></data>
<data name="page_faults"><type name="uint32" package="package0"/><value>99</value></data>
<data name="working_set_delta"><type name="int64" package="package0"/><value>77824</value></data>
<data name="memory_utilization"><type name="uint32" package="package0"/><value>100</value></data>
</event>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!-- #4452: DERIVED from the real scheduler_monitor_normal.xml capture (same element shape;
only the process_utilization and system_idle <value> numbers were changed). No real captured
sample crossed the other-process-CPU threshold, so this fixture exercises that arm: sql_cpu=3,
system_idle=40 -> other-process CPU = 100-3-40=57, which is >= 50 and so significant on the
other-process arm alone (sql_cpu 3 < 90, memory_utilization 100 > 50). -->
<event name="scheduler_monitor_system_health_ring_buffer_recorded" package="sqlos" timestamp="2026-09-26T23:59:53.232Z">
<data name="id"><type name="uint32" package="package0"/><value>0</value></data>
<data name="timestamp"><type name="uint64" package="package0"/><value>0</value></data>
<data name="process_utilization"><type name="uint32" package="package0"/><value>3</value></data>
<data name="system_idle"><type name="uint32" package="package0"/><value>40</value></data>
<data name="user_mode_time"><type name="uint64" package="package0"/><value>176250000</value></data>
<data name="kernel_mode_time"><type name="uint64" package="package0"/><value>7187500</value></data>
<data name="page_faults"><type name="uint32" package="package0"/><value>99</value></data>
<data name="working_set_delta"><type name="int64" package="package0"/><value>77824</value></data>
<data name="memory_utilization"><type name="uint32" package="package0"/><value>100</value></data>
</event>
2 changes: 1 addition & 1 deletion Darling/Darling.Tests/McpToolGuideHeads.HealthParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ private static readonly (string Tool, string Fact)[] GateFacts =
("get_health_parser_memory_node_oom", "Ungated: every recorded OOM is returned."),
("get_health_parser_severe_errors", "Gated: severity 19 or higher only, benign connection-reset error numbers excluded"),
("get_health_parser_io_issues", "Gated: WARNING-state results only."),
("get_health_parser_scheduler_issues", "Gated: WARNING-state results only."),
("get_health_parser_scheduler_issues", "Gated: only samples where SQL Server's own CPU is pinned, other processes are using half the box or more, or memory utilization has dropped to half or below."),
("get_health_parser_memory_conditions", "Gated: only snapshots whose last notification is RESOURCE_MEMPHYSICAL_LOW."),
("get_health_parser_cpu_tasks", "Gated: WARNING-state results with at least 10 pending tasks only."),
("get_health_parser_memory_broker", "Gated: RESOURCE_MEMPHYSICAL_LOW notifications only."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ param get_health_parser_memory_node_oom.hours_back 41
param get_health_parser_memory_node_oom.limit 38
param get_health_parser_memory_node_oom.server_name 28

tool get_health_parser_scheduler_issues 446
tool get_health_parser_scheduler_issues 536
param get_health_parser_scheduler_issues.as_of 167
param get_health_parser_scheduler_issues.hours_back 41
param get_health_parser_scheduler_issues.limit 38
Expand Down
5 changes: 4 additions & 1 deletion Darling/Darling.Tests/McpToolsListBudgetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,10 @@ shared 32 KB budget and is not a served description either. */
this PR's own get_store_host (+616) and dev's #4279 Lite-parity head change (+181). Constant set to the
value McpToolsListBudgetTests itself measured on the merged tree, not the two deltas added by hand. */
// #4442: get_read_latency's served block.
private const int TotalCeilingBytes = 176_182;
// #4452 (merge): re-measured on the tree combining dev's #4442 get_read_latency addition with this
// branch's scheduler-issues tool description growth (+95). Constant set to the value
// McpToolsListBudgetTests itself measured on the merged tree, not the two deltas added by hand.
private const int TotalCeilingBytes = 176_277;



Expand Down
83 changes: 69 additions & 14 deletions Darling/Darling.Tests/SystemHealthParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class SystemHealthParserTests
private static string LoadFixture(string name) =>
File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "Fixtures", "SystemHealth", name));

private static readonly DateTime SchedulerTime = new(2026, 7, 5, 11, 58, 0, 123, DateTimeKind.Utc);
private static readonly DateTime SchedulerHighCpuTime = new(2026, 9, 26, 22, 8, 48, 939, DateTimeKind.Utc);
private static readonly DateTime ErrorTime = new(2026, 7, 5, 12, 0, 5, 500, DateTimeKind.Utc);
private static readonly DateTime ResourceTime = new(2026, 7, 5, 12, 1, 0, 0, DateTimeKind.Utc);
private static readonly DateTime BrokerTime = new(2026, 7, 5, 12, 2, 10, 250, DateTimeKind.Utc);
Expand All @@ -41,20 +41,75 @@ private static string LoadFixture(string name) =>
// ── SchedulerIssues (scheduler_monitor_system_health_ring_buffer_recorded) ──

[Fact]
public void SchedulerIssue_ShredsEveryColumn()
public void SchedulerIssue_ShredsEveryColumn_HighSqlCpu()
{
var r = SystemHealthParser.ParseSchedulerIssue(LoadFixture("scheduler_monitor.xml"));
// Real field capture: process_utilization=94, system_idle=2 -> other = 100-94-2=4;
// working_set_delta 4096 bytes -> 4096/1048576 = 0.00390625 MB, rounded to 0.00.
var r = SystemHealthParser.ParseSchedulerIssue(LoadFixture("scheduler_monitor_high_sql_cpu.xml"));

Assert.NotNull(r);
Assert.Equal(SchedulerTime, r!.EventTime);
Assert.Equal(10, r.SchedulerId);
Assert.Equal(3, r.CpuId);
Assert.Equal("WARNING", r.Status); // data[@name="status"]/text, NOT the numeric value
Assert.True(r.IsOnline);
Assert.False(r.IsRunnable);
Assert.True(r.IsRunning);
Assert.Equal(4500L, r.NonYieldingTimeMs); // data[@name="non_yielding_time"] (no _ms suffix in XML)
Assert.Equal(4L, r.ThreadQuantumMs); // data[@name="thread_quantum"]
Assert.Equal(SchedulerHighCpuTime, r!.EventTime);
Assert.Equal(94, r.SqlCpuUtilization);
Assert.Equal(4, r.OtherProcessCpu);
Assert.Equal(2, r.SystemIdle);
Assert.Equal(100, r.MemoryUtilization);
Assert.Equal(505L, r.PageFaults);
Assert.Equal(0.00m, r.WorkingSetDeltaMb);
Assert.True(SystemHealthSignificance.IsSignificant(r)); // SQL CPU 94 >= 90
}

[Fact]
public void SchedulerIssue_ShredsEveryColumn_Normal()
{
// Real field capture: process_utilization=3, system_idle=95 -> other = 100-3-95=2;
// working_set_delta 77824 bytes -> 77824/1048576 = 0.074218... MB, rounded to 0.07.
var r = SystemHealthParser.ParseSchedulerIssue(LoadFixture("scheduler_monitor_normal.xml"));

Assert.NotNull(r);
Assert.Equal(3, r!.SqlCpuUtilization);
Assert.Equal(2, r.OtherProcessCpu);
Assert.Equal(95, r.SystemIdle);
Assert.Equal(100, r.MemoryUtilization);
Assert.Equal(99L, r.PageFaults);
Assert.Equal(0.07m, r.WorkingSetDeltaMb);
Assert.False(SystemHealthSignificance.IsSignificant(r));
}

[Fact]
public void SchedulerIssue_OtherProcessCpu_Significant()
{
// Derived from the real "normal" capture (system_idle changed 95 -> 40) to exercise the
// other-process-CPU arm: other = 100-3-40=57 >= 50.
var r = SystemHealthParser.ParseSchedulerIssue(LoadFixture("scheduler_monitor_other_process_cpu.xml"));
Assert.NotNull(r);
Assert.Equal(57, r!.OtherProcessCpu);
Assert.True(SystemHealthSignificance.IsSignificant(r));
}

[Fact]
public void SchedulerIssue_LowMemory_Significant()
{
// Derived from the real "normal" capture (memory_utilization changed 100 -> 50) to exercise
// the low-memory arm: 50 <= 50.
var r = SystemHealthParser.ParseSchedulerIssue(LoadFixture("scheduler_monitor_low_memory.xml"));
Assert.NotNull(r);
Assert.Equal(50, r!.MemoryUtilization);
Assert.True(SystemHealthSignificance.IsSignificant(r));
}

[Theory]
[InlineData(90, 0, 100, true)] // sql cpu boundary: 90 is significant
[InlineData(89, 0, 100, false)] // one under, not significant
[InlineData(0, 50, 100, true)] // other-process boundary: 50 is significant (sql=0,idle=50->other=50)
[InlineData(0, 51, 100, false)] // other-process 49, not significant (idle=51->other=49)
[InlineData(0, 100, 50, true)] // memory boundary: 50 is significant
[InlineData(0, 100, 51, false)] // memory 51, not significant
[InlineData(null, null, null, false)] // all null -> not significant (SQL null comparison is false)
public void SchedulerIssue_SignificanceBoundaries(int? sqlCpu, int? idle, int? mem, bool expected)
{
var other = sqlCpu is { } sc && idle is { } id ? 100 - sc - id : (int?)null;
var r = new SchedulerIssueRecord { SqlCpuUtilization = sqlCpu, OtherProcessCpu = other, SystemIdle = idle, MemoryUtilization = mem };
Assert.Equal(expected, SystemHealthSignificance.IsSignificant(r));
}

// ── SevereErrors (error_reported) ──
Expand Down Expand Up @@ -385,7 +440,7 @@ public void ParseEvents_DispatchesEachEventToItsCategory()
{
var events = new (string?, string?)[]
{
(SystemHealthParser.SchedulerMonitorEvent, LoadFixture("scheduler_monitor.xml")),
(SystemHealthParser.SchedulerMonitorEvent, LoadFixture("scheduler_monitor_high_sql_cpu.xml")),
(SystemHealthParser.ErrorReportedEvent, LoadFixture("error_reported.xml")),
(SystemHealthParser.SpServerDiagnosticsEvent, LoadFixture("sp_server_diagnostics_system.xml")),
(SystemHealthParser.SpServerDiagnosticsEvent, LoadFixture("sp_server_diagnostics_resource.xml")),
Expand All @@ -410,7 +465,7 @@ public void ParseEvents_DispatchesEachEventToItsCategory()
Assert.Single(result.MemoryConditions);
Assert.Single(result.CpuTasks);
Assert.Equal(2, result.IoIssues.Count);
Assert.Equal(10, result.SchedulerIssues[0].SchedulerId);
Assert.Equal(94, result.SchedulerIssues[0].SqlCpuUtilization);
Assert.Equal(823, result.SevereErrors[0].ErrorNumber);
Assert.Equal(6, result.SystemHealth[0].BadPagesDetected);
}
Expand Down
Loading
Loading