using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; using System.Text; using Npgsql; namespace MeterVault.Integration.Tests.Performance; /// The timings of one scenario: every measured run, and the SQL a separate counted run sent. internal sealed record ScenarioTiming( string Id, string Description, IReadOnlyList Milliseconds, int Commands, IReadOnlyList Statements, string Shape) { /// The summed duration of the SQL commands in the counted run (execution to reader close). public double SqlMilliseconds { get; init; } public double Median => Percentile(0.5); public double P95 => Percentile(0.95); public double Min => Milliseconds.Min(); public double Max => Milliseconds.Max(); /// Nearest-rank percentile; the median of an even count is the mean of the middle two. private double Percentile(double p) { var sorted = Milliseconds.Order().ToArray(); if (p == 0.5 && sorted.Length % 2 == 0) { return (sorted[(sorted.Length / 2) - 1] + sorted[sorted.Length / 2]) / 2; } var rank = (int)Math.Ceiling(p * sorted.Length); return sorted[Math.Clamp(rank - 1, 0, sorted.Length - 1)]; } } /// Markdown building and the facts about the machine and database a result was taken on. internal static class PerfReport { public static string F(double value, int digits = 1) => value.ToString("F" + digits.ToString(CultureInfo.InvariantCulture), CultureInfo.InvariantCulture); public static string N(long value) => value.ToString("N0", CultureInfo.InvariantCulture); /// A markdown table cell: pipes escaped, newlines flattened. public static string Cell(string text) => text.Replace("|", "\\|", StringComparison.Ordinal).Replace('\n', ' '); /// CPU model, cores, memory, OS and runtime. public static IReadOnlyList<(string Name, string Value)> Machine() { var memory = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes; return [ ("CPU", CpuModel()), ("Logical processors", Environment.ProcessorCount.ToString(CultureInfo.InvariantCulture)), ("Memory visible to .NET", F(memory / 1024d / 1024 / 1024) + " GiB"), ("OS", RuntimeInformation.OSDescription), (".NET", RuntimeInformation.FrameworkDescription), ("Docker", Docker()), ]; } /// The share of all logical processors busy over (Windows only): how loaded the machine was. public static async Task HostLoadAsync(TimeSpan window) { if (!OperatingSystem.IsWindows() || !NativeMethods.GetSystemTimes(out var idle1, out var kernel1, out var user1)) { return "n/a"; } await Task.Delay(window); if (!NativeMethods.GetSystemTimes(out var idle2, out var kernel2, out var user2)) { return "n/a"; } // Kernel time includes idle time. var total = (kernel2 - kernel1) + (user2 - user1); var busy = total - (idle2 - idle1); return total <= 0 ? "n/a" : F(100d * busy / total, 0) + " % of all logical processors busy (sampled over " + F(window.TotalSeconds, 0) + " s)"; } /// PostgreSQL/Timescale versions and the settings that shape plans. public static async Task> DatabaseAsync(NpgsqlConnection connection) { var rows = new List<(string, string)> { ("PostgreSQL", (await PerfDatabase.ScalarAsync(connection, "SHOW server_version"))?.ToString() ?? "?"), ("TimescaleDB", (await PerfDatabase.ScalarAsync(connection, "SELECT extversion FROM pg_extension WHERE extname = 'timescaledb'"))?.ToString() ?? "?"), }; await using var command = new NpgsqlCommand( """ SELECT name, setting || coalesce(' ' || unit, '') FROM pg_settings WHERE name IN ('shared_buffers', 'work_mem', 'effective_cache_size', 'max_parallel_workers_per_gather', 'max_worker_processes', 'jit', 'random_page_cost', 'timescaledb.max_tuples_decompressed_per_dml_transaction') ORDER BY name """, connection); await using var reader = await command.ExecuteReaderAsync(); while (await reader.ReadAsync()) { rows.Add((reader.GetString(0), reader.GetString(1))); } return rows; } /// Row counts and on-disk sizes of the tables the pipeline and the reader use. public static async Task> TablesAsync(NpgsqlConnection connection) { var result = new List<(string, long, string, string)>(); (string Table, bool Hypertable)[] tables = [ ("reading", true), ("consumption", true), ("consumption_rollup", false), ("consumption_rollup_month", false), ("meter_coverage", false), ("meter_rollup_state", false), ("meter_event", false), ("meter", false), ("tariff", false), ("manual_cost", false), ("meter_link", false), ("cost_category_member", false), ]; foreach (var (table, hypertable) in tables) { var rows = Convert.ToInt64(await PerfDatabase.ScalarAsync(connection, $"SELECT count(*) FROM {table}"), CultureInfo.InvariantCulture); var bytes = Convert.ToInt64( await PerfDatabase.ScalarAsync(connection, hypertable ? $"SELECT hypertable_size('{table}')" : $"SELECT pg_total_relation_size('{table}')"), CultureInfo.InvariantCulture); var note = string.Empty; if (hypertable) { note = (await PerfDatabase.ScalarAsync( connection, $"SELECT count(*) || ' chunks, ' || count(*) FILTER (WHERE is_compressed) || ' compressed' FROM timescaledb_information.chunks WHERE hypertable_name = '{table}'")) ?.ToString() ?? string.Empty; } result.Add((table, rows, F(bytes / 1024d / 1024) + " MiB", note)); } return result; } private static string CpuModel() { try { if (OperatingSystem.IsWindows()) { using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0"); if (key?.GetValue("ProcessorNameString") is string name) { return name.Trim(); } } else if (File.Exists("/proc/cpuinfo")) { var line = File.ReadLines("/proc/cpuinfo").FirstOrDefault(l => l.StartsWith("model name", StringComparison.Ordinal)); if (line is not null) { return line[(line.IndexOf(':', StringComparison.Ordinal) + 1)..].Trim(); } } } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) { return "unknown (" + ex.Message + ")"; } return "unknown"; } private static string Docker() { try { using var process = Process.Start(new ProcessStartInfo("docker", "info --format \"{{.ServerVersion}}|{{.OperatingSystem}}|{{.NCPU}} CPUs|{{.MemTotal}}\"") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }); if (process is null) { return "unknown"; } var output = process.StandardOutput.ReadToEnd().Trim(); process.WaitForExit(10_000); var parts = output.Split('|'); if (parts.Length == 4 && long.TryParse(parts[3], CultureInfo.InvariantCulture, out var bytes)) { parts[0] = "Engine " + parts[0]; parts[3] = F(bytes / 1024d / 1024 / 1024) + " GiB"; return string.Join(", ", parts); } return output.Length > 0 ? output : "unknown"; } catch (Exception ex) when (ex is System.ComponentModel.Win32Exception or InvalidOperationException) { return "unknown (" + ex.Message + ")"; } } /// Appends a markdown table. public static void Table(StringBuilder md, IReadOnlyList header, IEnumerable> rows) { md.Append("| ").AppendJoin(" | ", header).AppendLine(" |"); md.Append('|').AppendJoin("|", header.Select(_ => "---")).AppendLine("|"); foreach (var row in rows) { md.Append("| ").AppendJoin(" | ", row.Select(Cell)).AppendLine(" |"); } md.AppendLine(); } private static class NativeMethods { [DllImport("kernel32.dll", SetLastError = true)] [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetSystemTimes(out long idleTime, out long kernelTime, out long userTime); } }