namespace SQLInsight.Core.Data; /// /// Cumulative statistics for the statements that ran since the previous collection (sys.dm_exec_query_stats, /// grouped per statement), plus statements remembered from earlier collections so their totals can still be diffed. /// public sealed record QueryStatsSnapshot { /// Most statements remembered between collections; the least recently executed are forgotten first. public const int MaxRemembered = 2000; public required DateTimeOffset CapturedAt { get; init; } /// The server's own clock (GETDATE()) at capture: creation_time and last_execution_time are in it. public required DateTime ServerTime { get; init; } public required DateTime SqlServerStartTime { get; init; } public required IReadOnlyList Queries { get; init; } } /// /// One statement's totals since its first plan in cache was compiled. Times are in microseconds, as the DMV gives them. /// public sealed record QueryStat { /// query_hash as hex (SQL Server 2008+), or sql_handle:start:end on SQL Server 2005. public required string Key { get; init; } public string? DatabaseName { get; init; } /// The statement, cut to characters. Customer data: never sent to the hub. public string? Text { get; init; } public required long Executions { get; init; } public required long WorkerTimeUs { get; init; } public required long ElapsedTimeUs { get; init; } public required long LogicalReads { get; init; } public required long PhysicalReads { get; init; } public required long LogicalWrites { get; init; } /// The oldest cached plan's creation_time (server clock). A newer value means the plan was recompiled. public required DateTime CreatedAt { get; init; } /// last_execution_time (server clock). public required DateTime LastExecutionAt { get; init; } public const int MaxTextLength = 4000; } /// /// What each statement used between two collections. /// public sealed record QueryStatsDelta { public required DateTimeOffset From { get; init; } public required DateTimeOffset To { get; init; } public required IReadOnlyList Queries { get; init; } /// /// The change per statement. A statement only gets a delta when its starting point is known: it was seen before /// with the same plan, or its plan was compiled after the previous collection (then all of its totals are new). /// A plan recompiled or evicted in between, or counters that went backwards, start over: no negative numbers. /// After an instance restart there is no delta at all. /// public static QueryStatsDelta? Compute(QueryStatsSnapshot? previous, QueryStatsSnapshot current) { ArgumentNullException.ThrowIfNull(current); if (previous is null || previous.SqlServerStartTime != current.SqlServerStartTime || current.CapturedAt <= previous.CapturedAt) { return null; } var before = previous.Queries.ToDictionary(q => q.Key, StringComparer.Ordinal); var changes = new List(); foreach (var now in current.Queries) { // Remembered from an earlier collection but not run since: nothing new. if (now.LastExecutionAt < previous.ServerTime && before.ContainsKey(now.Key)) { continue; } var newSinceLast = now.CreatedAt >= previous.ServerTime; QueryStatDelta? delta; if (before.TryGetValue(now.Key, out var then) && then.CreatedAt == now.CreatedAt && !WentBackwards(then, now)) { delta = Difference(now, then); } else { delta = newSinceLast ? Difference(now, null) : null; } if (delta is { Executions: > 0 } or { WorkerTimeUs: > 0 }) { changes.Add(delta); } } return new QueryStatsDelta { From = previous.CapturedAt, To = current.CapturedAt, Queries = changes.OrderByDescending(q => q.WorkerTimeUs).ThenByDescending(q => q.ElapsedTimeUs).ToList(), }; } /// /// The snapshot to keep for the next collection: this collection's statements plus the remembered ones not seen /// now, at most . After a restart the old ones are dropped. /// public static IReadOnlyList Remember(QueryStatsSnapshot? previous, IReadOnlyList current, DateTime sqlServerStartTime) { ArgumentNullException.ThrowIfNull(current); var keys = current.Select(q => q.Key).ToHashSet(StringComparer.Ordinal); var older = previous is null || previous.SqlServerStartTime != sqlServerStartTime ? [] : previous.Queries.Where(q => !keys.Contains(q.Key)); return current.Concat(older.OrderByDescending(q => q.LastExecutionAt)) .Take(QueryStatsSnapshot.MaxRemembered) .ToList(); } private static bool WentBackwards(QueryStat then, QueryStat now) => now.Executions < then.Executions || now.WorkerTimeUs < then.WorkerTimeUs || now.ElapsedTimeUs < then.ElapsedTimeUs || now.LogicalReads < then.LogicalReads || now.PhysicalReads < then.PhysicalReads || now.LogicalWrites < then.LogicalWrites; private static QueryStatDelta Difference(QueryStat now, QueryStat? then) => new() { Key = now.Key, DatabaseName = now.DatabaseName, Text = now.Text, Executions = now.Executions - (then?.Executions ?? 0), WorkerTimeUs = now.WorkerTimeUs - (then?.WorkerTimeUs ?? 0), ElapsedTimeUs = now.ElapsedTimeUs - (then?.ElapsedTimeUs ?? 0), LogicalReads = now.LogicalReads - (then?.LogicalReads ?? 0), PhysicalReads = now.PhysicalReads - (then?.PhysicalReads ?? 0), LogicalWrites = now.LogicalWrites - (then?.LogicalWrites ?? 0), }; } public sealed record QueryStatDelta { public required string Key { get; init; } public string? DatabaseName { get; init; } /// The statement. Customer data: shown only in the local web UI, never sent to the hub. public string? Text { get; init; } public required long Executions { get; init; } public required long WorkerTimeUs { get; init; } public required long ElapsedTimeUs { get; init; } public required long LogicalReads { get; init; } public required long PhysicalReads { get; init; } public required long LogicalWrites { get; init; } }