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