Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Waiting to run
Collectors (SQL Server 2000+, version-specific SQL, least privilege): server, configuration, trace flags, databases (settings, backups in UTC, CHECKDB, VLFs, Query Store, files, tempdb), suspect pages, plan cache, wait stats and file I/O as deltas with restart/clear detection, performance counters, CPU from the scheduler monitor ring buffer, blocking (sleeping head blockers with open transactions), and deadlocks from system_health continuing from the last file offset. Rules: configuration (memory, MAXDOP per Microsoft NUMA guidance, cost threshold, ad hoc bloat, risky options, pending changes), databases (state, settings, backups, log backups, CHECKDB, suspect pages, VLFs, autogrowth, max size, Query Store, tempdb), waits (THREADPOOL, signal waits, locks, I/O, WRITELOG, parallelism, PAGELATCH), I/O latency, CPU, memory grants, blocking and deadlocks. Missing data makes a rule not measurable with the collector's reason. Integration tests run every collector as the least-privilege login and reproduce real blocking and a real deadlock; green on 2017 and 2022. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
126 lines
4.7 KiB
C#
126 lines
4.7 KiB
C#
using SQLInsight.Core.Data;
|
|
|
|
namespace SQLInsight.Core.Tests;
|
|
|
|
public class DeltaTests
|
|
{
|
|
private static readonly DateTime Start = new(2026, 9, 1, 8, 0, 0, DateTimeKind.Utc);
|
|
private static readonly DateTimeOffset T0 = new(2026, 9, 29, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
private static WaitStatsSnapshot Waits(DateTimeOffset at, DateTime start, params WaitStat[] waits) =>
|
|
new() { CapturedAt = at, SqlServerStartTime = start, Waits = waits };
|
|
|
|
[Fact]
|
|
public void Wait_delta_subtracts_and_drops_benign_waits()
|
|
{
|
|
var before = Waits(T0, Start, new WaitStat("LCK_M_X", 10, 1000, 10), new WaitStat("LAZYWRITER_SLEEP", 5, 5000, 0));
|
|
var after = Waits(T0.AddMinutes(5), Start, new WaitStat("LCK_M_X", 15, 4000, 20), new WaitStat("LAZYWRITER_SLEEP", 9, 90000, 0),
|
|
new WaitStat("WRITELOG", 100, 300, 5));
|
|
|
|
var delta = WaitStatsDelta.Compute(before, after)!;
|
|
|
|
Assert.Equal(TimeSpan.FromMinutes(5), delta.Interval);
|
|
Assert.Equal([new WaitStat("LCK_M_X", 5, 3000, 10), new WaitStat("WRITELOG", 100, 300, 5)], delta.Waits);
|
|
}
|
|
|
|
[Fact]
|
|
public void Wait_delta_is_null_after_restart()
|
|
{
|
|
var before = Waits(T0, Start, new WaitStat("LCK_M_X", 10, 1000, 10));
|
|
var after = Waits(T0.AddMinutes(5), Start.AddDays(1), new WaitStat("LCK_M_X", 1, 10, 0));
|
|
|
|
Assert.Null(WaitStatsDelta.Compute(before, after));
|
|
}
|
|
|
|
[Fact]
|
|
public void Wait_delta_is_null_when_stats_were_cleared()
|
|
{
|
|
var before = Waits(T0, Start, new WaitStat("LCK_M_X", 10, 1000, 10));
|
|
var after = Waits(T0.AddMinutes(5), Start, new WaitStat("LCK_M_X", 2, 50, 0));
|
|
|
|
Assert.Null(WaitStatsDelta.Compute(before, after));
|
|
}
|
|
|
|
[Fact]
|
|
public void First_sample_has_no_delta()
|
|
{
|
|
Assert.Null(WaitStatsDelta.Compute(null, Waits(T0, Start)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("SLEEP_TASK", true)]
|
|
[InlineData("QDS_PERSIST_TASK_MAIN_LOOP_SLEEP", true)]
|
|
[InlineData("CXCONSUMER", true)]
|
|
[InlineData("Total", true)]
|
|
[InlineData("CXPACKET", false)]
|
|
[InlineData("PAGEIOLATCH_SH", false)]
|
|
public void Benign_waits(string waitType, bool benign)
|
|
{
|
|
Assert.Equal(benign, WaitCatalog.IsBenign(waitType));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("LCK_M_IX", WaitCategory.Locks)]
|
|
[InlineData("PAGEIOLATCH_EX", WaitCategory.BufferIo)]
|
|
[InlineData("PAGELATCH_UP", WaitCategory.PageLatch)]
|
|
[InlineData("THREADPOOL", WaitCategory.WorkerThreads)]
|
|
[InlineData("WRITELOG", WaitCategory.TransactionLog)]
|
|
[InlineData("HADR_SYNC_COMMIT", WaitCategory.AvailabilityGroups)]
|
|
[InlineData("SOMETHING_NEW", WaitCategory.Other)]
|
|
public void Wait_categories(string waitType, WaitCategory category)
|
|
{
|
|
Assert.Equal(category, WaitCatalog.Categorize(waitType));
|
|
}
|
|
|
|
private static FileIoStat File(string db, int id, long reads, long stall) => new()
|
|
{
|
|
DatabaseName = db, FileId = id, FileType = "ROWS", PhysicalName = "x", Reads = reads, BytesRead = 0, ReadStallMs = stall,
|
|
Writes = 0, BytesWritten = 0, WriteStallMs = 0,
|
|
};
|
|
|
|
[Fact]
|
|
public void File_io_delta_skips_files_that_restarted_from_zero()
|
|
{
|
|
var before = new FileIoSnapshot { CapturedAt = T0, SqlServerStartTime = Start, Files = [File("A", 1, 100, 1000), File("B", 1, 500, 500)] };
|
|
var after = new FileIoSnapshot
|
|
{
|
|
CapturedAt = T0.AddMinutes(5),
|
|
SqlServerStartTime = Start,
|
|
Files = [File("A", 1, 300, 5000), File("B", 1, 10, 10), File("C", 1, 50, 50)], // B restored, C new
|
|
};
|
|
|
|
var delta = FileIoDelta.Compute(before, after)!;
|
|
|
|
var a = Assert.Single(delta.Files);
|
|
Assert.Equal(("A", 200L, 4000L), (a.DatabaseName, a.Reads, a.ReadStallMs));
|
|
Assert.Equal(20, a.AverageReadLatencyMs);
|
|
}
|
|
|
|
[Fact]
|
|
public void Counter_rates()
|
|
{
|
|
var before = new PerformanceCounterSnapshot { CapturedAt = T0, SqlServerStartTime = Start, BatchRequests = 1000 };
|
|
var after = before with { CapturedAt = T0.AddSeconds(10), BatchRequests = 3000 };
|
|
|
|
Assert.Equal(200, PerformanceCounterRates.Compute(before, after)!.BatchRequestsPerSecond);
|
|
}
|
|
|
|
[Fact]
|
|
public void Head_blockers_are_blockers_that_are_not_blocked()
|
|
{
|
|
var snapshot = new BlockingSnapshot
|
|
{
|
|
CapturedAt = T0,
|
|
Sessions =
|
|
[
|
|
new SessionInfo { SessionId = 51 },
|
|
new SessionInfo { SessionId = 60, BlockingSessionId = 51 },
|
|
new SessionInfo { SessionId = 61, BlockingSessionId = 60 },
|
|
new SessionInfo { SessionId = 70 },
|
|
new SessionInfo { SessionId = 71, BlockingSessionId = 70 },
|
|
],
|
|
};
|
|
|
|
Assert.Equal([51, 70], snapshot.HeadBlockers.Select(s => s.SessionId));
|
|
}
|
|
}
|