12 private readonly
object _sync =
new();
13 private readonly Func<double> _clock;
14 private readonly Dictionary<long, TelemetrySource> _sources = [];
15 private Queue<TelemetrySnapshot> _history =
new(120);
18 private long _nextId, _generation;
19 private int _requests;
20 private bool _disposed, _truncated;
21 private double _started;
24 public RuntimeProfiler() : this(() => Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency) { }
40 ArgumentNullException.ThrowIfNull(options);
44 ObjectDisposedException.ThrowIf(_disposed,
this);
45 if (_requests != 0)
throw new InvalidOperationException(
"Configure telemetry while inactive.");
46 if (_sources.Count > options.SourceCapacity || _sources.Values.Any(s => s.Metrics.Count > options.MetricCapacity))
47 throw new InvalidOperationException(
"Configured limits are smaller than registered detail.");
49 _history =
new Queue<TelemetrySnapshot>(options.HistoryCapacity);
60 ObjectDisposedException.ThrowIf(_disposed,
this);
66 foreach (var source
in _sources.Values)
67 foreach (var accumulator
in source.Metrics.Values) accumulator.Rebase();
70 return new Request(
this);
81 ValidateName(backend);
84 ObjectDisposedException.ThrowIf(_disposed,
this);
85 if (_sources.Count >= _options.SourceCapacity) { _truncated =
true;
return null; }
87 _sources.Add(source.Id, source);
96 internal TelemetrySource? TryRegisterSource(
string name,
string backend)
109 return _latestSnapshot;
120 return Array.AsReadOnly(_history.ToArray());
125 public void Reset() { lock (_sync) { ObjectDisposedException.ThrowIf(_disposed,
this); ResetCore(); } }
132 if (_disposed)
return;
138 _latestSnapshot =
null;
144 internal long Begin() =>
IsCollecting ? Volatile.Read(ref _generation) : 0;
157 if (!_sources.ContainsKey(source.
Id))
return false;
158 if (source.Metrics.TryGetValue(name, out var existing))
161 existing.Availability = availability;
164 if (source.Metrics.Count >= _options.MetricCapacity) { _truncated =
true;
return false; }
165 source.Metrics.Add(name,
new Accumulator { Availability = availability, Kind = kind });
175 internal void Record(TelemetrySource source,
long generation,
string metric,
double value)
177 if (generation == 0 || !
IsCollecting || !
double.IsFinite(value))
return;
180 if (_requests == 0 || generation != _generation || !_sources.ContainsKey(source.Id))
return;
182 if (!source.Metrics.TryGetValue(metric, out var accumulator))
return;
184 accumulator.Add(value, _clock());
190 internal void Retire(TelemetrySource source) { lock (_sync) _sources.Remove(source.Id); }
194 internal void MarkTruncated(
long generation)
196 lock (_sync) {
if (generation == _generation && _requests != 0) _truncated =
true; }
199 private static void ValidateName(
string name)
201 if (
string.IsNullOrWhiteSpace(name) || name.Length > 96)
throw new ArgumentException(
"Names must contain 1–96 characters.", nameof(name));
204 private void ResetCore()
208 _latestSnapshot =
null;
211 foreach (var source
in _sources.Values)
212 foreach (var accumulator
in source.Metrics.Values) accumulator.Rebase();
215 private void ClearAccumulators()
217 foreach (var source
in _sources.Values)
218 foreach (var accumulator
in source.Metrics.Values) accumulator.Clear();
221 private void Advance()
223 if (_requests == 0)
return;
224 double now = _clock();
225 if (now - _started >= _options.Interval.TotalSeconds) Publish(now);
228 private void Publish(
double now)
230 var sources = _sources.Values.Select(source =>
new TelemetrySourceSnapshot(source.Id, source.Name, source.Backend,
231 new ReadOnlyDictionary<string, TelemetrySummary>(source.Metrics.ToDictionary(p => p.Key, p => p.Value.Snapshot())))).ToArray();
232 if (_history.Count == _options.HistoryCapacity) _history.Dequeue();
233 _latestSnapshot =
new(_generation, _started, now, Array.AsReadOnly(sources), _truncated);
234 _history.Enqueue(_latestSnapshot);
243 if (_requests == 0)
return;
246 double now = _clock();
247 if (now > _started) Publish(now);
256 private RuntimeProfiler? _owner = owner;
260 public void Dispose() => Interlocked.Exchange(ref _owner,
null)?.Stop();
264 internal sealed class Accumulator
271 private double _sum, _min, _max, _last, _observed;
272 private double? _baseline;
276 internal void Add(
double value,
double observed)
278 _min = _count == 0 ? value : Math.Min(_min, value);
279 _max = _count == 0 ? value : Math.Max(_max, value);
283 _observed = observed;
286 internal void Clear() {
if (_count > 0) _baseline = _last; _count = 0; _sum = 0; }
288 internal void Rebase() => _baseline =
null;
292 _count, _sum, _count == 0 ? null : _min, _count == 0 ? null : _max, _count == 0 ? null : _last)
295 LastObservedSeconds = _count == 0 ? null : _observed,
296 WindowDelta = Kind == TelemetryMetricKind.LifetimeCounter && _count > 0 && _baseline is { } baseline ? _last - baseline :
null
record TelemetrySourceSnapshot(long Id, string Name, string Backend, IReadOnlyDictionary< string, TelemetrySummary > Metrics)
Detached source identity, backend label, and measurements. No runtime objects are retained.