using System.Diagnostics;
namespace ZymonicServices;
///
/// Optional, process-wide diagnostics. Metric names must be fixed code labels, never user data.
/// Counts, totals and maxima cover the whole reporting interval; percentiles use its last 512 samples.
///
public sealed class ZymonicPerformanceRecorder
{
public const int SampleLimit = 512;
public const int MetricLimit = 64;
private static ZymonicPerformanceRecorder? _current;
private readonly object _sync = new();
private readonly Dictionary<(string Name, string Unit), Samples> _metrics = new();
public static ZymonicPerformanceRecorder? Current
{
get => Volatile.Read(ref _current);
set => Volatile.Write(ref _current, value);
}
public IDisposable Measure(string name) => new Measurement(this, name);
public void RecordElapsed(string name, long started)
{
Record(name, "ms", Stopwatch.GetElapsedTime(started).TotalMilliseconds);
}
public void RecordCount(string name, long value) => Record(name, "count", value);
public void Record(string name, string unit, double value)
{
// Instrumentation must not interrupt an application operation for an invalid sample.
if (name.Length <= 100 && name.Length > 0 &&
(unit == "ms" || unit == "count") && double.IsFinite(value) && value >= 0)
{
lock (_sync)
{
var key = (name, unit);
if (_metrics.TryGetValue(key, out var samples))
{
samples.Add(value);
}
else if (_metrics.Count < MetricLimit)
{
samples = new Samples();
samples.Add(value);
_metrics.Add(key, samples);
}
}
}
}
public IReadOnlyList TakeSnapshot()
{
KeyValuePair<(string Name, string Unit), Samples>[] metrics;
lock (_sync)
{
metrics = _metrics.ToArray();
_metrics.Clear();
}
// Sort outside the lock so reporting does not hold up the UI or request threads.
return metrics.Select(metric => metric.Value.Summarise(metric.Key.Name, metric.Key.Unit)).ToArray();
}
private sealed class Samples
{
private readonly double[] _values = new double[SampleLimit];
private long _count;
private double _total;
private double _maximum;
public void Add(double value)
{
_values[_count % SampleLimit] = value;
_count++;
_total += value;
_maximum = Math.Max(_maximum, value);
}
public ZymonicPerformanceSummary Summarise(string name, string unit)
{
int sampled = (int)Math.Min(_count, SampleLimit);
Array.Sort(_values, 0, sampled);
double median = sampled % 2 == 0
? (_values[sampled / 2 - 1] + _values[sampled / 2]) / 2
: _values[sampled / 2];
return new(name, unit, _count, _total, median,
_values[(int)Math.Ceiling(sampled * 0.95) - 1], _maximum, sampled);
}
}
private sealed class Measurement : IDisposable
{
private ZymonicPerformanceRecorder? _recorder;
private readonly string _name;
private readonly long _started = Stopwatch.GetTimestamp();
public Measurement(ZymonicPerformanceRecorder recorder, string name)
{
_recorder = recorder;
_name = name;
}
public void Dispose()
{
Interlocked.Exchange(ref _recorder, null)?.RecordElapsed(_name, _started);
}
}
}
public sealed record ZymonicPerformanceSummary(string Name, string Unit, long Count, double Total,
double Median, double P95, double Maximum, int Sampled);