using System.Diagnostics; namespace ZB.MOM.WW.ScadaBridge.ResidualBench; /// --key value options. An unknown key is rejected by the consuming mode, not silently ignored. internal sealed class BenchArgs { private readonly Dictionary _values = new(StringComparer.Ordinal); private readonly HashSet _read = new(StringComparer.Ordinal); public static BenchArgs Parse(IEnumerable args) { var result = new BenchArgs(); var list = args.ToList(); for (var i = 0; i < list.Count; i++) { if (!list[i].StartsWith("--", StringComparison.Ordinal)) throw new ArgumentException($"unexpected argument '{list[i]}'"); var key = list[i][2..]; var value = i + 1 < list.Count && !list[i + 1].StartsWith("--", StringComparison.Ordinal) ? list[++i] : "true"; result._values[key] = value; } return result; } public string Get(string key, string fallback) { _read.Add(key); return _values.TryGetValue(key, out var v) ? v : fallback; } public int GetInt(string key, int fallback) => int.Parse(Get(key, fallback.ToString())); /// Throws if any supplied key was never read — a typo must not silently change the run. public void RejectUnknown() { var unknown = _values.Keys.Where(k => !_read.Contains(k)).ToList(); if (unknown.Count > 0) throw new ArgumentException($"unknown option(s): {string.Join(", ", unknown.Select(k => "--" + k))}"); } } /// Latency sample set with nearest-rank percentiles. internal sealed class Samples { private readonly List _ms = new(); public int Count => _ms.Count; public void Add(double ms) { lock (_ms) _ms.Add(ms); } public double Percentile(double p) { lock (_ms) { if (_ms.Count == 0) return double.NaN; var sorted = _ms.OrderBy(x => x).ToList(); var rank = (int)Math.Ceiling(p / 100.0 * sorted.Count) - 1; return sorted[Math.Clamp(rank, 0, sorted.Count - 1)]; } } public double Max { get { lock (_ms) return _ms.Count == 0 ? double.NaN : _ms.Max(); } } public string Summary() => $"n={Count} P50 {Percentile(50):F2} ms, P95 {Percentile(95):F2} ms, P99 {Percentile(99):F2} ms, max {Max:F2} ms"; public static double Time(Action action) { var sw = Stopwatch.StartNew(); action(); return sw.Elapsed.TotalMilliseconds; } }