sqlinsight-pro/src/SQLInsight.Reports/TimeWindowAnalysis.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

105 lines
5.8 KiB
C#

using SQLInsight.Repository;
namespace SQLInsight.Reports;
/// <summary>
/// Everything the repository holds about one instance in one time window, compared with the same hours of the
/// previous weeks: the Statspack-style "what happened between 03 and 04". Used by the web page and the PDF.
/// </summary>
public sealed record TimeWindowAnalysis
{
public required InstanceOverview Instance { get; init; }
public required DateTimeOffset From { get; init; }
public required DateTimeOffset To { get; init; }
public required WindowResolution Resolution { get; init; }
public required int RawDays { get; init; }
public required IReadOnlyList<MetricComparison> Metrics { get; init; }
public required IReadOnlyList<WaitComparison> Waits { get; init; }
public required IReadOnlyList<FileLatency> Files { get; init; }
public required IReadOnlyList<StoredBlocking> Blocking { get; init; }
public required IReadOnlyList<StoredDeadlock> Deadlocks { get; init; }
public required IReadOnlyList<StoredFinding> Findings { get; init; }
public required TopQueryOrder QueryOrder { get; init; }
/// <summary>The heaviest statements. Their text is customer data: only for the local UI and PDF.</summary>
public required IReadOnlyList<TopQuery> TopQueries { get; init; }
public bool HasData => Metrics.Any(m => m.Points.Count > 0) || Waits.Count > 0 || Files.Count > 0 || Blocking.Count > 0
|| Deadlocks.Count > 0 || TopQueries.Count > 0;
/// <summary>The metrics shown, in order: key, label, unit, decimals, lower-is-worse, fixed maximum.</summary>
public static readonly IReadOnlyList<(string Metric, string Label, string Unit, int Decimals, bool LowerIsWorse, double? Max)> MetricDefinitions =
[
(Repository.Metrics.CpuSqlServer, "SQL Server CPU", " %", 0, false, 100),
(Repository.Metrics.CpuOther, "CPU brugt af andre processer", " %", 0, false, 100),
(Repository.Metrics.BatchRequestsPerSecond, "Batch requests pr. sekund", "", 1, false, null),
(Repository.Metrics.CompilationsPerSecond, "SQL compilations pr. sekund", "", 1, false, null),
(Repository.Metrics.PageLifeExpectancy, "Page life expectancy (s)", "", 0, true, null),
(Repository.Metrics.UserConnections, "Brugerforbindelser", "", 0, false, null),
(Repository.Metrics.MemoryGrantsPending, "Memory grants pending", "", 0, false, null),
(Repository.Metrics.ProcessesBlocked, "Blokerede processer", "", 0, false, null),
];
public static async Task<TimeWindowAnalysis> LoadAsync(
InsightRepository repository, InstanceOverview instance, DateTimeOffset from, DateTimeOffset to, DateTimeOffset now,
int rawDays = 14, TopQueryOrder order = TopQueryOrder.Cpu, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(repository);
ArgumentNullException.ThrowIfNull(instance);
var id = instance.InstanceId;
var resolution = TimeWindow.ChooseResolution(from, now, rawDays);
var weeks = Enumerable.Range(1, WindowBaseline.Weeks).Select(w => TimeSpan.FromDays(7 * w)).ToList();
var metrics = new List<MetricComparison>();
foreach (var (metric, label, unit, decimals, lowerIsWorse, max) in MetricDefinitions)
{
var points = resolution == WindowResolution.Raw
? await repository.GetMetricSamplesAsync(id, metric, from, to, cancellationToken).ConfigureAwait(false)
: await repository.GetMetricHourlyAsync(id, metric, from, to, cancellationToken).ConfigureAwait(false);
var earlier = new List<IReadOnlyList<MetricPoint>>();
foreach (var back in weeks)
{
earlier.Add(await repository.GetMetricHourlyAsync(id, metric, from - back, to - back, cancellationToken).ConfigureAwait(false));
}
metrics.Add(new MetricComparison(metric, label, unit, decimals, lowerIsWorse, max, points, WindowBaseline.NormalAverage(earlier)));
}
IReadOnlyList<WaitTotal> windowWaits;
double minutes;
if (resolution == WindowResolution.Raw)
{
windowWaits = await repository.GetWaitsAsync(id, from, to, cancellationToken).ConfigureAwait(false);
minutes = (to - from).TotalMinutes;
}
else
{
var hourly = await repository.GetWaitsHourlyAsync(id, from, to, cancellationToken).ConfigureAwait(false);
windowWaits = hourly.Select(h => new WaitTotal(h.WaitType, h.WaitingTasks, h.WaitTimeMs, 0)).ToList();
minutes = hourly.Count == 0 ? (to - from).TotalMinutes : hourly[0].Hours * 60.0;
}
var earlierWaits = new List<IReadOnlyList<HourlyWaitTotal>>();
foreach (var back in weeks)
{
earlierWaits.Add(await repository.GetWaitsHourlyAsync(id, from - back, to - back, cancellationToken).ConfigureAwait(false));
}
return new TimeWindowAnalysis
{
Instance = instance,
From = from,
To = to,
Resolution = resolution,
RawDays = rawDays,
Metrics = metrics,
Waits = WindowBaseline.CompareWaits(windowWaits, minutes, earlierWaits),
Files = await repository.GetFileLatencyAsync(id, from, to, cancellationToken).ConfigureAwait(false),
Blocking = await repository.GetBlockingAsync(id, from, to, cancellationToken).ConfigureAwait(false),
Deadlocks = await repository.GetDeadlocksAsync(id, from, to, cancellationToken).ConfigureAwait(false),
Findings = await repository.GetFindingsActiveDuringAsync(id, from, to, cancellationToken).ConfigureAwait(false),
QueryOrder = order,
TopQueries = await repository.GetTopQueriesAsync(id, from, to, order, 25, cancellationToken).ConfigureAwait(false),
};
}
}