2b80a6defa
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).
75 lines
2.5 KiB
C#
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;
|
|
}
|
|
}
|