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>
133 lines
6.1 KiB
C#
133 lines
6.1 KiB
C#
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));
|
||
}
|