namespace SQLInsight.Core.Data; /// /// Cumulative I/O per database file (sys.dm_io_virtual_file_stats). /// public sealed record FileIoSnapshot { public required DateTimeOffset CapturedAt { get; init; } public required DateTime SqlServerStartTime { get; init; } public required IReadOnlyList Files { get; init; } } public sealed record FileIoStat { public required string DatabaseName { get; init; } public required int FileId { get; init; } /// ROWS or LOG. public required string FileType { get; init; } public required string PhysicalName { get; init; } /// /// The volume (drive or mount point) the file lives on, e.g. D:\. From sys.dm_os_volume_stats where /// the instance has it, otherwise derived from the path (). /// public string? Volume { get; init; } public required long Reads { get; init; } public required long BytesRead { get; init; } public required long ReadStallMs { get; init; } public required long Writes { get; init; } public required long BytesWritten { get; init; } public required long WriteStallMs { get; init; } public double? AverageReadLatencyMs => Reads == 0 ? null : (double)ReadStallMs / Reads; public double? AverageWriteLatencyMs => Writes == 0 ? null : (double)WriteStallMs / Writes; } /// /// Which drive a database file is on, when SQL Server cannot say (before 2008 R2 SP1 there is no /// sys.dm_os_volume_stats). /// public static class DiskVolumes { /// /// D:\Data\x.mdf → D:\; \\nas\sql\x.mdf → \\nas\sql\; a Linux path → /. A mount point /// under a drive letter cannot be told apart from the drive itself this way. /// public static string FromPhysicalName(string physicalName) { ArgumentNullException.ThrowIfNull(physicalName); var path = physicalName.Trim(); if (path.Length >= 2 && char.IsAsciiLetter(path[0]) && path[1] == ':') { return char.ToUpperInvariant(path[0]) + @":\"; } if (path.StartsWith(@"\\", StringComparison.Ordinal)) { var parts = path[2..].Split('\\', StringSplitOptions.RemoveEmptyEntries); return parts.Length >= 2 ? $@"\\{parts[0]}\{parts[1]}\" : path; } return path.StartsWith('/') ? "/" : path; } /// The file's volume as reported, or derived from its path. public static string Of(FileIoStat file) { ArgumentNullException.ThrowIfNull(file); return string.IsNullOrWhiteSpace(file.Volume) ? FromPhysicalName(file.PhysicalName) : file.Volume; } } /// /// I/O on one drive over an interval: the sum of its database files. Latency is weighted by the number of I/Os. /// public sealed record DiskActivity(string Volume, int DataFiles, int LogFiles, long Reads, long Writes, long BytesRead, long BytesWritten, long ReadStallMs, long WriteStallMs, double Seconds) { public double? ReadIops => Seconds > 0 ? Reads / Seconds : null; public double? WriteIops => Seconds > 0 ? Writes / Seconds : null; public double? ReadMbPerSecond => Seconds > 0 ? BytesRead / 1048576.0 / Seconds : null; public double? WriteMbPerSecond => Seconds > 0 ? BytesWritten / 1048576.0 / Seconds : null; public double? AverageReadMs => Reads == 0 ? null : (double)ReadStallMs / Reads; public double? AverageWriteMs => Writes == 0 ? null : (double)WriteStallMs / Writes; /// Groups the files of one interval by drive. public static IReadOnlyList ByVolume(IEnumerable files, double seconds) { ArgumentNullException.ThrowIfNull(files); return files .GroupBy(DiskVolumes.Of, StringComparer.OrdinalIgnoreCase) .Select(g => new DiskActivity(g.Key, g.Count(f => f.FileType != "LOG"), g.Count(f => f.FileType == "LOG"), g.Sum(f => f.Reads), g.Sum(f => f.Writes), g.Sum(f => f.BytesRead), g.Sum(f => f.BytesWritten), g.Sum(f => f.ReadStallMs), g.Sum(f => f.WriteStallMs), seconds)) .OrderBy(d => d.Volume, StringComparer.OrdinalIgnoreCase) .ToList(); } } /// /// I/O per file between two snapshots. /// public sealed record FileIoDelta { public required DateTimeOffset From { get; init; } public required DateTimeOffset To { get; init; } public required IReadOnlyList Files { get; init; } public static FileIoDelta? Compute(FileIoSnapshot? previous, FileIoSnapshot current) { ArgumentNullException.ThrowIfNull(current); if (previous is null || previous.SqlServerStartTime != current.SqlServerStartTime || current.CapturedAt <= previous.CapturedAt) { return null; } var before = previous.Files.ToDictionary(f => (f.DatabaseName, f.FileId)); var files = new List(); foreach (var now in current.Files) { // A database restored or attached since the last snapshot starts from zero again; skip it this time. if (!before.TryGetValue((now.DatabaseName, now.FileId), out var then) || now.Reads < then.Reads || now.Writes < then.Writes) { continue; } files.Add(now with { Reads = now.Reads - then.Reads, BytesRead = now.BytesRead - then.BytesRead, ReadStallMs = now.ReadStallMs - then.ReadStallMs, Writes = now.Writes - then.Writes, BytesWritten = now.BytesWritten - then.BytesWritten, WriteStallMs = now.WriteStallMs - then.WriteStallMs, }); } return new FileIoDelta { From = previous.CapturedAt, To = current.CapturedAt, Files = files }; } }