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