using SQLInsight.Repository;
namespace SQLInsight.Reports;
///
/// 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.
///
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 Metrics { get; init; }
public required IReadOnlyList Waits { get; init; }
public required IReadOnlyList Files { get; init; }
/// Disk speed per drive, against the normal for the same hours.
public IReadOnlyList Disks { get; init; } = [];
public required IReadOnlyList Blocking { get; init; }
public required IReadOnlyList Deadlocks { get; init; }
/// Backup, restore, DBCC and other operations with progress that ran in the window.
public IReadOnlyList LongOperations { get; init; } = [];
/// User transactions open a minute or more (or holding much log) in the window. Statements and logins are customer data.
public IReadOnlyList LongTransactions { get; init; } = [];
///
/// A statement's CPU time as a share of the window, in percent of one CPU core: 100 means one core all the time.
///
public double ActivityPercent(TopQuery query)
{
ArgumentNullException.ThrowIfNull(query);
var seconds = (To - From).TotalSeconds;
return seconds <= 0 ? 0 : 100.0 * query.WorkerTimeUs / (seconds * 1_000_000);
}
public required IReadOnlyList Findings { get; init; }
public required TopQueryOrder QueryOrder { get; init; }
/// The heaviest statements. Their text is customer data: only for the local UI and PDF.
public required IReadOnlyList TopQueries { get; init; }
/// What the assessment of the measurements needs besides them (buffer pool size, max server memory).
public MetricContext Context { get; init; } = new();
/// The assessment of a measurement in the window (OK, Bemærk, Reagér).
public Assessment Assess(MetricComparison metric)
{
ArgumentNullException.ThrowIfNull(metric);
return MetricAssessment.Assess(metric.Metric, metric.Points, Context);
}
private static IReadOnlyList Points(List metrics, string metric) =>
metrics.FirstOrDefault(m => m.Metric == metric)?.Points ?? [];
public bool HasData => Metrics.Any(m => m.Points.Count > 0) || Waits.Count > 0 || Files.Count > 0 || Disks.Count > 0 || Blocking.Count > 0
|| Deadlocks.Count > 0 || TopQueries.Count > 0 || LongOperations.Count > 0 || LongTransactions.Count > 0;
/// The metrics shown, in order: key, label, unit, decimals, lower-is-worse, fixed maximum.
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),
(Repository.Metrics.TempdbUsedMegabytes, "tempdb i brug (MB)", "", 0, false, null),
(Repository.Metrics.VersionStoreMegabytes, "Version store (MB)", "", 0, false, null),
];
public static async Task 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();
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>();
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 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>();
var earlierDisks = new List>();
foreach (var back in weeks)
{
earlierWaits.Add(await repository.GetWaitsHourlyAsync(id, from - back, to - back, cancellationToken).ConfigureAwait(false));
earlierDisks.Add(await repository.GetDiskActivityHourlyAsync(id, from - back, to - back, cancellationToken).ConfigureAwait(false));
}
var windowDisks = resolution == WindowResolution.Raw
? await repository.GetDiskActivityAsync(id, from, to, cancellationToken).ConfigureAwait(false)
: await repository.GetDiskActivityHourlyAsync(id, from, to, cancellationToken).ConfigureAwait(false);
return new TimeWindowAnalysis
{
Instance = instance,
From = from,
To = to,
Resolution = resolution,
RawDays = rawDays,
Metrics = metrics,
Context = MetricContext.From(
await repository.GetSnapshotAsync(id, "configuration", cancellationToken).ConfigureAwait(false),
await repository.GetSnapshotAsync(id, "server", cancellationToken).ConfigureAwait(false),
Points(metrics, Repository.Metrics.BatchRequestsPerSecond), Points(metrics, Repository.Metrics.CpuSqlServer), Points(metrics, Repository.Metrics.CpuOther)),
Waits = WindowBaseline.CompareWaits(windowWaits, minutes, earlierWaits),
Files = await repository.GetFileLatencyAsync(id, from, to, cancellationToken).ConfigureAwait(false),
Disks = WindowBaseline.CompareDisks(windowDisks, earlierDisks),
Blocking = await repository.GetBlockingAsync(id, from, to, cancellationToken).ConfigureAwait(false),
Deadlocks = await repository.GetDeadlocksAsync(id, from, to, cancellationToken).ConfigureAwait(false),
LongOperations = await repository.GetLongOperationsAsync(id, from, to, 100, cancellationToken).ConfigureAwait(false),
LongTransactions = await repository.GetLongTransactionsAsync(id, from, to, 100, 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),
};
}
}