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>
105 lines
5.8 KiB
C#
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),
|
|
};
|
|
}
|
|
}
|