Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
RuntimeProfiler.cs
1using System.Collections.ObjectModel;
2using System.Diagnostics;
3
5
10public sealed class RuntimeProfiler : IDisposable
11{
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);
16 private TelemetrySnapshot? _latestSnapshot;
17 private TelemetryOptions _options = new();
18 private long _nextId, _generation;
19 private int _requests;
20 private bool _disposed, _truncated;
21 private double _started;
22
24 public RuntimeProfiler() : this(() => Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency) { }
25
28 internal RuntimeProfiler(Func<double> clock) => _clock = clock;
29
31 public bool IsCollecting => Volatile.Read(ref _requests) != 0;
32
34 public TelemetryOptions Options { get { lock (_sync) return _options; } }
35
38 public void Configure(TelemetryOptions options)
39 {
40 ArgumentNullException.ThrowIfNull(options);
41 options.Validate();
42 lock (_sync)
43 {
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.");
48 _options = options;
49 _history = new Queue<TelemetrySnapshot>(options.HistoryCapacity);
50 ResetCore();
51 }
52 }
53
56 public IDisposable Start()
57 {
58 lock (_sync)
59 {
60 ObjectDisposedException.ThrowIf(_disposed, this);
61 if (_requests == 0)
62 {
63 _generation++;
64 _started = _clock();
65 ClearAccumulators();
66 foreach (var source in _sources.Values)
67 foreach (var accumulator in source.Metrics.Values) accumulator.Rebase();
68 }
69 _requests++;
70 return new Request(this);
71 }
72 }
73
78 public TelemetrySource? RegisterSource(string name, string backend)
79 {
80 ValidateName(name);
81 ValidateName(backend);
82 lock (_sync)
83 {
84 ObjectDisposedException.ThrowIf(_disposed, this);
85 if (_sources.Count >= _options.SourceCapacity) { _truncated = true; return null; }
86 var source = new TelemetrySource(this, ++_nextId, name, backend);
87 _sources.Add(source.Id, source);
88 return source;
89 }
90 }
91
96 internal TelemetrySource? TryRegisterSource(string name, string backend)
97 {
98 lock (_sync)
99 return _disposed ? null : RegisterSource(name, backend);
100 }
101
105 {
106 lock (_sync)
107 {
108 Advance();
109 return _latestSnapshot;
110 }
111 }
112
115 public IReadOnlyList<TelemetrySnapshot> GetHistory()
116 {
117 lock (_sync)
118 {
119 Advance();
120 return Array.AsReadOnly(_history.ToArray());
121 }
122 }
123
125 public void Reset() { lock (_sync) { ObjectDisposedException.ThrowIf(_disposed, this); ResetCore(); } }
126
128 public void Dispose()
129 {
130 lock (_sync)
131 {
132 if (_disposed) return;
133 _disposed = true;
134 _requests = 0;
135 _generation++;
136 _sources.Clear();
137 _history.Clear();
138 _latestSnapshot = null;
139 }
140 }
141
144 internal long Begin() => IsCollecting ? Volatile.Read(ref _generation) : 0;
145
152 internal bool Define(TelemetrySource source, string name, TelemetryAvailability availability, TelemetryMetricKind kind)
153 {
154 ValidateName(name);
155 lock (_sync)
156 {
157 if (!_sources.ContainsKey(source.Id)) return false;
158 if (source.Metrics.TryGetValue(name, out var existing))
159 {
160 if (existing.Availability == TelemetryAvailability.Unsupported && availability == TelemetryAvailability.NotYetSampled)
161 existing.Availability = availability;
162 return true;
163 }
164 if (source.Metrics.Count >= _options.MetricCapacity) { _truncated = true; return false; }
165 source.Metrics.Add(name, new Accumulator { Availability = availability, Kind = kind });
166 return true;
167 }
168 }
169
175 internal void Record(TelemetrySource source, long generation, string metric, double value)
176 {
177 if (generation == 0 || !IsCollecting || !double.IsFinite(value)) return;
178 lock (_sync)
179 {
180 if (_requests == 0 || generation != _generation || !_sources.ContainsKey(source.Id)) return;
181 Advance();
182 if (!source.Metrics.TryGetValue(metric, out var accumulator)) return;
183 if (accumulator.Availability is TelemetryAvailability.Unsupported or TelemetryAvailability.NotApplicable) return;
184 accumulator.Add(value, _clock());
185 }
186 }
187
190 internal void Retire(TelemetrySource source) { lock (_sync) _sources.Remove(source.Id); }
191
194 internal void MarkTruncated(long generation)
195 {
196 lock (_sync) { if (generation == _generation && _requests != 0) _truncated = true; }
197 }
198
199 private static void ValidateName(string name)
200 {
201 if (string.IsNullOrWhiteSpace(name) || name.Length > 96) throw new ArgumentException("Names must contain 1–96 characters.", nameof(name));
202 }
203
204 private void ResetCore()
205 {
206 _generation++;
207 _history.Clear();
208 _latestSnapshot = null;
209 _started = _clock();
210 ClearAccumulators();
211 foreach (var source in _sources.Values)
212 foreach (var accumulator in source.Metrics.Values) accumulator.Rebase();
213 }
214
215 private void ClearAccumulators()
216 {
217 foreach (var source in _sources.Values)
218 foreach (var accumulator in source.Metrics.Values) accumulator.Clear();
219 }
220
221 private void Advance()
222 {
223 if (_requests == 0) return;
224 double now = _clock();
225 if (now - _started >= _options.Interval.TotalSeconds) Publish(now);
226 }
227
228 private void Publish(double now)
229 {
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);
235 ClearAccumulators();
236 _started = now;
237 }
238
239 private void Stop()
240 {
241 lock (_sync)
242 {
243 if (_requests == 0) return;
244 if (_requests == 1)
245 {
246 double now = _clock();
247 if (now > _started) Publish(now);
248 _generation++;
249 }
250 _requests--;
251 }
252 }
253
254 private sealed class Request(RuntimeProfiler owner) : IDisposable
255 {
256 private RuntimeProfiler? _owner = owner;
260 public void Dispose() => Interlocked.Exchange(ref _owner, null)?.Stop();
261 }
262
264 internal sealed class Accumulator
265 {
267 internal TelemetryAvailability Availability;
269 internal TelemetryMetricKind Kind;
270 private long _count;
271 private double _sum, _min, _max, _last, _observed;
272 private double? _baseline;
276 internal void Add(double value, double observed)
277 {
278 _min = _count == 0 ? value : Math.Min(_min, value);
279 _max = _count == 0 ? value : Math.Max(_max, value);
280 _count++;
281 _sum += value;
282 _last = value;
283 _observed = observed;
284 }
286 internal void Clear() { if (_count > 0) _baseline = _last; _count = 0; _sum = 0; }
288 internal void Rebase() => _baseline = null;
291 internal TelemetrySummary Snapshot() => new(_count == 0 ? Availability : TelemetryAvailability.Available,
292 _count, _sum, _count == 0 ? null : _min, _count == 0 ? null : _max, _count == 0 ? null : _last)
293 {
294 Kind = Kind,
295 LastObservedSeconds = _count == 0 ? null : _observed,
296 WindowDelta = Kind == TelemetryMetricKind.LifetimeCounter && _count > 0 && _baseline is { } baseline ? _last - baseline : null
297 };
298 }
299}
300
302public sealed class TelemetrySource : IDisposable
303{
304 private readonly RuntimeProfiler _owner;
306 internal Dictionary<string, RuntimeProfiler.Accumulator> Metrics { get; } = [];
312 internal TelemetrySource(RuntimeProfiler owner, long id, string name, string backend)
313 => (_owner, Id, Name, Backend) = (owner, id, name, backend);
315 public long Id { get; }
317 public string Name { get; }
319 public string Backend { get; }
325 public bool Define(string name, TelemetryAvailability availability = TelemetryAvailability.NotYetSampled,
327 => _owner.Define(this, name, availability == TelemetryAvailability.Available ? TelemetryAvailability.NotYetSampled : availability, kind);
330 internal void MarkTruncated(long generation) => _owner.MarkTruncated(generation);
333 public long BeginSample() => _owner.Begin();
338 public void Record(long generation, string metric, double value) => _owner.Record(this, generation, metric, value);
340 public void Dispose() => _owner.Retire(this);
341}
Opt-in, bounded runtime measurements independent of legacy CPS notifications.
TelemetrySnapshot? GetLatestSnapshot()
Returns the latest completed bucket, or null before one completes.
bool IsCollecting
Whether any collection request is active; safe on producer threads.
void Dispose()
Stops collection, invalidates in-flight samples, and retires all source handles.
void Reset()
Clears telemetry history and rebases observations without changing renderer counters or leases.
IReadOnlyList< TelemetrySnapshot > GetHistory()
Returns bounded completed buckets in chronological order without consuming them.
TelemetrySource? RegisterSource(string name, string backend)
Registers an explicitly named source without retaining any runtime object.
void Configure(TelemetryOptions options)
Changes shared settings while inactive, clearing previous measurements.
TelemetryOptions Options
Current immutable settings.
IDisposable Start()
Starts or joins collection. Disposing the last request stops collection.
RuntimeProfiler()
Creates an independent collector using the monotonic Stopwatch clock.
Bounded extension source. Define measurements once, capture a generation before work,...
bool Define(string name, TelemetryAvailability availability=TelemetryAvailability.NotYetSampled, TelemetryMetricKind kind=TelemetryMetricKind.Sample)
Defines a measurement before use. Include units and scope in its name.
void Record(long generation, string metric, double value)
Records a finite observation without retaining raw samples.
long BeginSample()
Captures an epoch before measured work; zero means disabled.
void Dispose()
Permanently retires the source; late observations are ignored.
long Id
Session-local identity, unchanged by collector reset or temporary context loss.
record TelemetrySummary(TelemetryAvailability Availability, long Count, double Sum, double? Minimum, double? Maximum, double? Last)
Detached statistics for observations in one window; durations use milliseconds.
record TelemetrySourceSnapshot(long Id, string Name, string Backend, IReadOnlyDictionary< string, TelemetrySummary > Metrics)
Detached source identity, backend label, and measurements. No runtime objects are retained.
TelemetryAvailability
Whether an observation is meaningful for its source and window.
TelemetryMetricKind
How observations should be interpreted when combining windows.
record TelemetryOptions
Validated shared settings; configure only when collection is inactive.
record TelemetrySnapshot(long Generation, double StartedSeconds, double EndedSeconds, IReadOnlyList< TelemetrySourceSnapshot > Sources, bool Truncated)
Detached completed window. Sources can be observed asynchronously within its interval.