Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
load_measurer.hpp
Go to the documentation of this file.
1#pragma once
2
5
6#include <chrono>
7#include <algorithm>
8#include <atomic>
9#include <cmath>
10#include <cstdint>
11#include <limits>
12
13#include <numeric>
14
15#if defined(__APPLE__)
16#include <mach/mach_time.h>
17#endif
18
19namespace pulp::audio {
20
26 float load = 0.0f;
27 float peak_load = 0.0f;
28 float last_load = 0.0f;
29 std::int64_t elapsed_ns = 0;
30 std::int64_t available_ns = 0;
31 std::uint64_t callback_count = 0;
32 std::uint64_t overload_count = 0;
33};
34
41
43 float watch_load = 0.75f;
44 float overload_load = 1.0f;
45 float critical_load = 1.25f;
46 std::uint64_t watch_xruns = 1;
47 std::uint64_t critical_xruns = 3;
48 std::uint64_t watch_overloads = 1;
49 std::uint64_t critical_overloads = 3;
50};
51
59
60inline const char* to_string(AudioRuntimeOverloadSeverity severity) noexcept {
61 switch (severity) {
62 case AudioRuntimeOverloadSeverity::Nominal: return "nominal";
63 case AudioRuntimeOverloadSeverity::Watch: return "watch";
64 case AudioRuntimeOverloadSeverity::Overloaded: return "overloaded";
65 case AudioRuntimeOverloadSeverity::Critical: return "critical";
66 }
67 return "critical";
68}
69
71 const AudioProcessLoadSnapshot& load,
72 std::uint64_t xrun_count,
73 const AudioRuntimeOverloadPolicy& policy = {}) noexcept {
74 const float watch_load =
75 std::isfinite(policy.watch_load) && policy.watch_load > 0.0f
76 ? policy.watch_load
77 : 0.75f;
78 const float overload_load =
79 std::isfinite(policy.overload_load) && policy.overload_load > watch_load
80 ? policy.overload_load
81 : std::max(watch_load, 1.0f);
82 const float critical_load =
83 std::isfinite(policy.critical_load) && policy.critical_load > overload_load
84 ? policy.critical_load
85 : std::max(overload_load, 1.25f);
86 const float observed_load =
87 std::max({load.load, load.peak_load, load.last_load});
88
89 AudioRuntimeOverloadReport report;
90 if (observed_load >= critical_load
91 || (policy.critical_xruns > 0 && xrun_count >= policy.critical_xruns)
92 || (policy.critical_overloads > 0
93 && load.overload_count >= policy.critical_overloads)) {
95 report.should_shed_optional_work = true;
96 report.should_bypass_optional_work = true;
97 report.validation_failure = true;
98 report.action = "bypass-optional-work";
99 return report;
100 }
101
102 if (observed_load >= overload_load
103 || (policy.watch_xruns > 0 && xrun_count >= policy.watch_xruns)
104 || (policy.watch_overloads > 0
105 && load.overload_count >= policy.watch_overloads)) {
107 report.should_shed_optional_work = true;
108 report.action = "shed-optional-work";
109 return report;
110 }
111
112 if (observed_load >= watch_load) {
113 report.severity = AudioRuntimeOverloadSeverity::Watch;
114 report.action = "monitor";
115 return report;
116 }
117
118 return report;
119}
120
128 std::uint64_t numer = 1;
129 std::uint64_t denom = 1;
130
133 static constexpr LoadClockRatio reduced(std::uint64_t numer, std::uint64_t denom) noexcept {
134 if (numer == 0 || denom == 0)
135 return LoadClockRatio{0, 1};
136 const std::uint64_t divisor = std::gcd(numer, denom);
137 return LoadClockRatio{numer / divisor, denom / divisor};
138 }
139
141 static LoadClockRatio host() noexcept {
142#if defined(__APPLE__)
143 mach_timebase_info_data_t timebase{};
144 mach_timebase_info(&timebase); // leaves {0, 0} on failure: unavailable
145 return reduced(timebase.numer, timebase.denom);
146#else
147 return LoadClockRatio{1, 1};
148#endif
149 }
150
152 constexpr std::int64_t to_ns(std::uint64_t ticks) const noexcept {
153 constexpr auto kMax = static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max());
154 if (denom == 0 || numer == 0)
155 return 0;
156 if (ticks > kMax / numer)
157 return std::numeric_limits<std::int64_t>::max();
158 return static_cast<std::int64_t>(std::min(ticks * numer / denom, kMax));
159 }
160
161 friend constexpr bool operator==(const LoadClockRatio&, const LoadClockRatio&) = default;
162};
163
183 public:
187
190 explicit AudioProcessLoadMeasurer(LoadClockRatio ratio) noexcept : ratio_(ratio) {}
191
193 LoadClockRatio clock_ratio() const noexcept {
194 return ratio_;
195 }
196
200 static std::uint64_t read_ticks() noexcept {
201#if defined(__APPLE__)
202 return mach_absolute_time();
203#else
204 return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
205 std::chrono::steady_clock::now().time_since_epoch())
206 .count());
207#endif
208 }
209
213 void begin(int num_frames, float sample_rate) noexcept {
214#if defined(PULP_AUDIOWORKLET_DISABLE_LOAD_TIMING)
215 // AudioWorkletGlobalScope does not provide the clock import used by
216 // steady_clock. Leave telemetry explicitly unavailable in that profile.
217 (void)num_frames;
218 (void)sample_rate;
219 return;
220#else
221 start_ticks_ = read_ticks();
222
223 if (num_frames <= 0 || !(sample_rate > 0.0f)) {
224 available_ns_ = 0;
225 return;
226 }
227
228 const double available_ns =
229 static_cast<double>(num_frames) / static_cast<double>(sample_rate) * 1e9;
230
231 if (!std::isfinite(available_ns) || available_ns <= 0.0 ||
232 available_ns > static_cast<double>(std::numeric_limits<int64_t>::max())) {
233 available_ns_ = 0;
234 return;
235 }
236
237 available_ns_ = static_cast<int64_t>(available_ns);
238#endif
239 }
240
242 void end() noexcept {
243#if defined(PULP_AUDIOWORKLET_DISABLE_LOAD_TIMING)
244 return;
245#else
246 const std::uint64_t end_ticks = read_ticks();
247 const int64_t elapsed_ns =
248 ratio_.to_ns(end_ticks >= start_ticks_ ? end_ticks - start_ticks_ : 0);
249
250 if (available_ns_ > 0) {
251 float raw = static_cast<float>(elapsed_ns) / static_cast<float>(available_ns_);
252 // Smooth with exponential moving average
253 const float previous_load = load_.load(std::memory_order_relaxed);
254 const float next_load = previous_load + smoothing_ * (raw - previous_load);
255 const float previous_peak = peak_load_.load(std::memory_order_relaxed);
256 load_.store(next_load, std::memory_order_relaxed);
257 peak_load_.store(std::max(previous_peak, raw), std::memory_order_relaxed);
258 last_load_.store(raw, std::memory_order_relaxed);
259 last_elapsed_ns_.store(elapsed_ns, std::memory_order_relaxed);
260 last_available_ns_.store(available_ns_, std::memory_order_relaxed);
261 callback_count_.fetch_add(1, std::memory_order_relaxed);
262 if (raw >= overload_threshold_) {
263 overload_count_.fetch_add(1, std::memory_order_relaxed);
264 }
265 }
266#endif
267 }
268
270 float load() const noexcept { return load_.load(std::memory_order_relaxed); }
271
273 float peak_load() const noexcept { return peak_load_.load(std::memory_order_relaxed); }
274
276 float last_load() const noexcept { return last_load_.load(std::memory_order_relaxed); }
277
280 std::int64_t last_elapsed_ns() const noexcept {
281 return last_elapsed_ns_.load(std::memory_order_relaxed);
282 }
283
285 std::uint64_t callback_count() const noexcept {
286 return callback_count_.load(std::memory_order_relaxed);
287 }
288
290 std::uint64_t overload_count() const noexcept {
291 return overload_count_.load(std::memory_order_relaxed);
292 }
293
297 .load = load_.load(std::memory_order_relaxed),
298 .peak_load = peak_load_.load(std::memory_order_relaxed),
299 .last_load = last_load_.load(std::memory_order_relaxed),
300 .elapsed_ns = last_elapsed_ns_.load(std::memory_order_relaxed),
301 .available_ns = last_available_ns_.load(std::memory_order_relaxed),
302 .callback_count = callback_count_.load(std::memory_order_relaxed),
303 .overload_count = overload_count_.load(std::memory_order_relaxed),
304 };
305 }
306
308 void reset_peak() noexcept { peak_load_.store(0.0f, std::memory_order_relaxed); }
309
311 void reset() noexcept {
312 load_.store(0.0f, std::memory_order_relaxed);
313 peak_load_.store(0.0f, std::memory_order_relaxed);
314 last_load_.store(0.0f, std::memory_order_relaxed);
315 last_elapsed_ns_.store(0, std::memory_order_relaxed);
316 last_available_ns_.store(0, std::memory_order_relaxed);
317 callback_count_.store(0, std::memory_order_relaxed);
318 overload_count_.store(0, std::memory_order_relaxed);
319 available_ns_ = 0;
320 start_ticks_ = 0;
321 }
322
325 void set_smoothing(float alpha) noexcept {
326 smoothing_ = std::clamp(alpha, 0.0f, 1.0f);
327 }
328
331 void set_overload_threshold(float load) noexcept {
332 overload_threshold_ = (std::isfinite(load) && load > 0.0f) ? load : 1.0f;
333 }
334
335 private:
336 // The measurer reads its clock twice per measured span, on the audio
337 // thread. On Apple platforms std::chrono::steady_clock goes through
338 // clock_gettime() and mach_continuous_time(), which showed as about 8% of
339 // a small SignalGraph block for the always-on whole-graph span alone, so
340 // Apple targets read mach_absolute_time() instead: the uptime counter,
341 // whose libsystem implementation orders the counter read. Continuous time
342 // also counts system sleep, which a callback span never contains, so
343 // uptime measures the same durations. Measured on an Apple M3 Ultra, best
344 // of 9 runs of 2M reads: mach_absolute_time() 5.3 ns, steady_clock::now()
345 // 15.2 ns, and an isb plus cntvct_el0 read converted with a 128-bit
346 // divide on every read 36.9 ns. The tick-to-nanosecond ratio is read once
347 // at construction (ratio_), so begin() and end() take no lock, run no
348 // guarded static initialization, and convert each span with one 64-bit
349 // multiply and divide.
350 LoadClockRatio ratio_;
351 std::uint64_t start_ticks_ = 0;
352 int64_t available_ns_ = 0;
353 std::atomic<float> load_{0.0f};
354 std::atomic<float> peak_load_{0.0f};
355 std::atomic<float> last_load_{0.0f};
356 std::atomic<std::int64_t> last_elapsed_ns_{0};
357 std::atomic<std::int64_t> last_available_ns_{0};
358 std::atomic<std::uint64_t> callback_count_{0};
359 std::atomic<std::uint64_t> overload_count_{0};
360 float smoothing_ = 0.1f;
361 float overload_threshold_ = 1.0f;
362};
363
364} // namespace pulp::audio
float last_load() const noexcept
Last raw load sample.
Definition load_measurer.hpp:276
void set_overload_threshold(float load) noexcept
Set the raw-load threshold that increments overload_count().
Definition load_measurer.hpp:331
AudioProcessLoadMeasurer(LoadClockRatio ratio) noexcept
Uses ratio to convert clock ticks to nanoseconds.
Definition load_measurer.hpp:190
float peak_load() const noexcept
Peak load since last reset.
Definition load_measurer.hpp:273
void begin(int num_frames, float sample_rate) noexcept
Call at the start of the audio callback.
Definition load_measurer.hpp:213
AudioProcessLoadMeasurer() noexcept
Reads the platform timebase once, here, so begin() and end() never query it.
Definition load_measurer.hpp:186
void set_smoothing(float alpha) noexcept
Set smoothing factor (0 = no smoothing, 1 = no averaging).
Definition load_measurer.hpp:325
std::uint64_t callback_count() const noexcept
Number of valid callback windows measured since the last reset.
Definition load_measurer.hpp:285
std::uint64_t overload_count() const noexcept
Number of valid callback windows whose raw load reached the overload threshold.
Definition load_measurer.hpp:290
std::int64_t last_elapsed_ns() const noexcept
Most recent measured callback duration, in nanoseconds (0 before the first valid window).
Definition load_measurer.hpp:280
AudioProcessLoadSnapshot snapshot() const noexcept
Latest telemetry snapshot. Safe to poll from UI/diagnostic threads.
Definition load_measurer.hpp:295
float load() const noexcept
Current smoothed CPU load (0.0 = idle, 1.0 = full buffer, >1.0 = overrun).
Definition load_measurer.hpp:270
void reset() noexcept
Reset all state.
Definition load_measurer.hpp:311
void reset_peak() noexcept
Reset peak load tracker.
Definition load_measurer.hpp:308
LoadClockRatio clock_ratio() const noexcept
The tick-to-nanosecond ratio captured at construction.
Definition load_measurer.hpp:193
void end() noexcept
Call at the end of the audio callback.
Definition load_measurer.hpp:242
static std::uint64_t read_ticks() noexcept
One raw read of the measurer's monotonic clock, in its own ticks (convert spans with clock_ratio()).
Definition load_measurer.hpp:200
Definition analyzer_provider.hpp:12
AudioRuntimeOverloadReport evaluate_audio_runtime_overload(const AudioProcessLoadSnapshot &load, std::uint64_t xrun_count, const AudioRuntimeOverloadPolicy &policy={}) noexcept
Definition load_measurer.hpp:70
AudioRuntimeOverloadSeverity
Definition load_measurer.hpp:35
@ Critical
Definition load_measurer.hpp:39
@ Overloaded
Definition load_measurer.hpp:38
@ Nominal
Definition load_measurer.hpp:36
@ Watch
Definition load_measurer.hpp:37
const char * to_string(LiveDspNodeKind kind) noexcept
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
Definition beat_division.hpp:11
Snapshot published by AudioProcessLoadMeasurer.
Definition load_measurer.hpp:25
float peak_load
Highest raw load since the last reset.
Definition load_measurer.hpp:27
float load
Smoothed CPU load. 1.0 == full buffer.
Definition load_measurer.hpp:26
std::int64_t available_ns
Definition load_measurer.hpp:30
std::int64_t elapsed_ns
Definition load_measurer.hpp:29
std::uint64_t callback_count
Definition load_measurer.hpp:31
float last_load
Most recent raw load sample.
Definition load_measurer.hpp:28
std::uint64_t overload_count
Definition load_measurer.hpp:32
Definition load_measurer.hpp:42
std::uint64_t critical_xruns
Definition load_measurer.hpp:47
float overload_load
Definition load_measurer.hpp:44
float watch_load
Definition load_measurer.hpp:43
float critical_load
Definition load_measurer.hpp:45
std::uint64_t watch_overloads
Definition load_measurer.hpp:48
std::uint64_t watch_xruns
Definition load_measurer.hpp:46
std::uint64_t critical_overloads
Definition load_measurer.hpp:49
Definition load_measurer.hpp:52
bool should_shed_optional_work
Definition load_measurer.hpp:54
bool validation_failure
Definition load_measurer.hpp:56
const char * action
Definition load_measurer.hpp:57
AudioRuntimeOverloadSeverity severity
Definition load_measurer.hpp:53
bool should_bypass_optional_work
Definition load_measurer.hpp:55
Converts a span of monotonic clock ticks to nanoseconds.
Definition load_measurer.hpp:127
std::uint64_t denom
Definition load_measurer.hpp:129
constexpr std::int64_t to_ns(std::uint64_t ticks) const noexcept
Nanoseconds in a span of ticks, saturating at INT64_MAX.
Definition load_measurer.hpp:152
friend constexpr bool operator==(const LoadClockRatio &, const LoadClockRatio &)=default
std::uint64_t numer
Definition load_measurer.hpp:128
static constexpr LoadClockRatio reduced(std::uint64_t numer, std::uint64_t denom) noexcept
numer/denom reduced by their GCD.
Definition load_measurer.hpp:133
static LoadClockRatio host() noexcept
The running platform's timebase. Not for the audio thread.
Definition load_measurer.hpp:141