sqlinsight-pro/tests/SQLInsight.Agent.Tests/TimeWindowTests.cs
Søren Eggert Lundsteen Olsen 554a6a71f5 Time window analysis: what happened between 03 and 04, as a page and PDF
SQLInsight Web: /instanser/{navn}/analyse?fra=&til= (datetime-local in the
display time zone; presets last hour, tonight 00-06, yesterday; at most 31
days). "Analysér et tidsrum" on the instance page, and blocking/deadlock
times there open the window half an hour around the event.

For the window: load (CPU, batch requests, compilations, PLE, connections,
memory grants pending, blocked processes) from 5-minute samples within
Retention:RawDays, otherwise hourly history (shown on the page); waits per
minute against the same hours and weekday of the previous 4 weeks, with
clear deviations marked; top SQL sortable by CPU, duration, reads and
executions; file I/O latency; blocking chains; deadlocks; findings active
in the window. An empty window explains the retention. "Hent som PDF"
renders the same via MigraDoc (/analyse.pdf).

Docs: docs/analyse.md (also on the community site); databehandling.md: the
statement text stays in the customer's repository and is never sent to
the hub.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 15:46:56 +02:00

133 lines
6.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using SQLInsight.Core.Data;
using SQLInsight.Reports;
using SQLInsight.Repository;
namespace SQLInsight.Agent.Tests;
public class TimeWindowTests
{
private static readonly TimeZoneInfo Copenhagen = TimeZoneInfo.FindSystemTimeZoneById("Europe/Copenhagen");
private static readonly DateTimeOffset Now = new(2026, 10, 1, 10, 0, 0, TimeSpan.Zero);
[Fact]
public void Local_input_is_converted_to_utc_in_the_display_time_zone()
{
// 20 September 2026 is summer time in Copenhagen (UTC+2): 03:00–04:00 local is 01:00–02:00 UTC.
Assert.True(TimeWindow.TryParse("2026-09-20T03:00", "2026-09-20T04:00", Copenhagen, out var from, out var to, out var error));
Assert.Null(error);
Assert.Equal(new DateTimeOffset(2026, 9, 20, 1, 0, 0, TimeSpan.Zero), from);
Assert.Equal(new DateTimeOffset(2026, 9, 20, 2, 0, 0, TimeSpan.Zero), to);
Assert.Equal("2026-09-20T03:00", TimeWindow.ToInput(from, Copenhagen));
}
[Fact]
public void Winter_time_and_the_skipped_summer_time_hour_are_handled()
{
Assert.True(TimeWindow.TryParse("2026-12-01T03:00", "2026-12-01T04:00", Copenhagen, out var from, out _, out _));
Assert.Equal(new DateTimeOffset(2026, 12, 1, 2, 0, 0, TimeSpan.Zero), from);
// 29 March 2026 02:30 does not exist in Copenhagen: the hour after is used rather than failing.
Assert.True(TimeWindow.TryParse("2026-03-29T02:30", "2026-03-29T05:00", Copenhagen, out var skipped, out _, out _));
Assert.Equal(new DateTimeOffset(2026, 3, 29, 1, 30, 0, TimeSpan.Zero), skipped);
}
[Theory]
[InlineData("2026-09-20T04:00", "2026-09-20T03:00", "Til skal ligge efter fra.")]
[InlineData("2026-08-01T00:00", "2026-09-20T03:00", "Højst 31 dage ad gangen.")]
[InlineData("i går", "2026-09-20T03:00", "Angiv fra og til som dato og klokkeslæt.")]
public void Invalid_windows_are_refused_with_a_reason(string from, string to, string expected)
{
Assert.False(TimeWindow.TryParse(from, to, Copenhagen, out _, out _, out var error));
Assert.Equal(expected, error);
}
[Fact]
public void Raw_samples_are_used_inside_their_retention_and_hourly_history_before()
{
Assert.Equal(WindowResolution.Raw, TimeWindow.ChooseResolution(Now.AddDays(-13), Now, rawDays: 14));
Assert.Equal(WindowResolution.Hourly, TimeWindow.ChooseResolution(Now.AddDays(-15), Now, rawDays: 14));
}
[Fact]
public void Presets_follow_the_local_calendar()
{
// 10:00 UTC = 12:00 in Copenhagen.
var (nightFrom, nightTo) = TimeWindow.Preset("i-nat", Now, Copenhagen)!.Value;
Assert.Equal(new DateTimeOffset(2026, 9, 30, 22, 0, 0, TimeSpan.Zero), nightFrom);
Assert.Equal(new DateTimeOffset(2026, 10, 1, 4, 0, 0, TimeSpan.Zero), nightTo);
var (yesterdayFrom, yesterdayTo) = TimeWindow.Preset("i-gaar", Now, Copenhagen)!.Value;
Assert.Equal(new DateTimeOffset(2026, 9, 29, 22, 0, 0, TimeSpan.Zero), yesterdayFrom);
Assert.Equal(new DateTimeOffset(2026, 9, 30, 22, 0, 0, TimeSpan.Zero), yesterdayTo);
// Before 06 local, "i nat" ends now.
var early = new DateTimeOffset(2026, 10, 1, 2, 0, 0, TimeSpan.Zero);
Assert.Equal(early, TimeWindow.Preset("i-nat", early, Copenhagen)!.Value.To);
Assert.Null(TimeWindow.Preset("ukendt", Now, Copenhagen));
}
[Fact]
public void Waits_are_compared_per_minute_with_the_weeks_that_have_history()
{
var window = new List<WaitTotal>
{
new("LCK_M_X", 50, 600_000, 0), // 10 min in a 60-minute window = 10 000 ms/min
new("PAGEIOLATCH_SH", 1_000, 60_000, 0), // 1 min = 1 000 ms/min
};
IReadOnlyList<IReadOnlyList<HourlyWaitTotal>> weeks =
[
[new("LCK_M_X", 60_000, 10, 1), new("PAGEIOLATCH_SH", 60_000, 900, 1)], // 1 000 ms/min each
[new("LCK_M_X", 180_000, 10, 1), new("PAGEIOLATCH_SH", 60_000, 900, 1)], // 3 000 and 1 000
[], // no data that week: ignored
];
var result = WindowBaseline.CompareWaits(window, 60, weeks);
var locks = result[0];
Assert.Equal("LCK_M_X", locks.WaitType);
Assert.Equal(10_000, locks.PerMinute);
Assert.Equal(2_000, locks.NormalPerMinute);
Assert.Equal(5, locks.Ratio);
Assert.True(locks.IsUnusual);
var io = result[1];
Assert.Equal(1_000, io.NormalPerMinute);
Assert.False(io.IsUnusual);
}
[Fact]
public void Without_history_the_normal_is_unknown_and_nothing_is_flagged_by_ratio()
{
var result = WindowBaseline.CompareWaits([new WaitTotal("CXPACKET", 1, 1_000, 0)], 60, [[], []]);
Assert.Null(result[0].NormalPerMinute);
Assert.Null(result[0].Ratio);
Assert.False(result[0].IsUnusual);
}
[Fact]
public void Metric_is_unusual_when_clearly_worse_than_normal()
{
var at = Now.UtcDateTime;
var cpu = new MetricComparison("cpu", "CPU", " %", 0, LowerIsWorse: false, 100, [new MetricPoint(at, 80), new MetricPoint(at.AddMinutes(5), 90)],
WindowBaseline.NormalAverage([[new MetricPoint(at.AddDays(-7), 30)], [new MetricPoint(at.AddDays(-14), 50)]]));
Assert.Equal(40, cpu.NormalAverage);
Assert.True(cpu.IsUnusual);
var ple = new MetricComparison("ple", "PLE", "", 0, LowerIsWorse: true, null, [new MetricPoint(at, 200)], 1_000);
Assert.True(ple.IsUnusual);
var normal = new MetricComparison("cpu", "CPU", " %", 0, false, 100, [new MetricPoint(at, 42)], 40);
Assert.False(normal.IsUnusual);
Assert.Null(WindowBaseline.NormalAverage([[], []]));
}
[Theory]
[InlineData("varighed", TopQueryOrder.Duration)]
[InlineData("laesninger", TopQueryOrder.Reads)]
[InlineData("koersler", TopQueryOrder.Executions)]
[InlineData(null, TopQueryOrder.Cpu)]
[InlineData("noget-andet", TopQueryOrder.Cpu)]
public void Order_comes_from_the_url(string? value, TopQueryOrder expected) => Assert.Equal(expected, AnalysisOrder.Parse(value));
}