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 };
}
}