Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.ResidualBench/BenchSupport.cs
T
Joseph Doherty 2b80a6defa test(perf): measure phase-8 WP-4 residual criteria xc-3/4/5/8 (#49)
New ResidualBench tool (eventlog, flatten, audit, audit-contention) and
docker/measure-ui-responsiveness.sh. All four residuals pass; results in
docs/plans/2026-11-10-wp4-residual-scope-results.md. Findings filed as
#122, #123 and #124 (fixed in the following commit).
2026-11-10 16:46:42 -05:00

75 lines
2.5 KiB
C#

using System.Diagnostics;
namespace ZB.MOM.WW.ScadaBridge.ResidualBench;
/// <summary><c>--key value</c> options. An unknown key is rejected by the consuming mode, not silently ignored.</summary>
internal sealed class BenchArgs
{
private readonly Dictionary<string, string> _values = new(StringComparer.Ordinal);
private readonly HashSet<string> _read = new(StringComparer.Ordinal);
public static BenchArgs Parse(IEnumerable<string> 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()));
/// <summary>Throws if any supplied key was never read — a typo must not silently change the run.</summary>
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))}");
}
}
/// <summary>Latency sample set with nearest-rank percentiles.</summary>
internal sealed class Samples
{
private readonly List<double> _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;
}
}