use core::ffi::c_int;
use core::fmt;
use core::num::NonZeroU32;
use core::time::Duration;
use bela_sys::BelaCpuData;
use crate::context::Context;
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CpuUsage {
percentage: f32,
busy: u64,
total: u64,
measurements_per_cycle: u32,
measurements_taken: u32,
}
impl CpuUsage {
const fn from_raw(raw: &BelaCpuData) -> Self {
Self {
percentage: raw.percentage,
busy: raw.busy,
total: raw.total,
measurements_per_cycle: raw.count,
measurements_taken: raw.currentCount,
}
}
#[must_use]
pub const fn percentage(&self) -> f32 {
self.percentage
}
#[must_use]
pub const fn busy(&self) -> Duration {
Duration::from_nanos(self.busy)
}
#[must_use]
pub const fn total(&self) -> Duration {
Duration::from_nanos(self.total)
}
#[must_use]
pub const fn measurements_per_cycle(&self) -> u32 {
self.measurements_per_cycle
}
#[must_use]
pub const fn measurements_taken(&self) -> u32 {
self.measurements_taken
}
}
impl fmt::Display for CpuUsage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:.1}% busy, averaged over {} measurements",
self.percentage, self.measurements_per_cycle
)
}
}
impl Context {
#[must_use]
pub fn cpu_usage(&self) -> Option<CpuUsage> {
monitoring_data().map(|data| CpuUsage::from_raw(&data))
}
}
#[cfg(bela_device)]
fn monitoring_data() -> Option<BelaCpuData> {
let data = unsafe { bela_sys::Bela_cpuMonitoringGet() };
if data.is_null() {
return None;
}
let data = unsafe { *data };
(data.count != 0).then_some(data)
}
#[cfg(not(bela_device))]
const fn monitoring_data() -> Option<BelaCpuData> {
None
}
#[cfg(bela_device)]
pub fn apply_monitoring(cycle: Option<c_int>) -> Result<(), Error> {
let Some(count) = cycle else {
return disable_monitoring();
};
let data = unsafe {
if bela_sys::Bela_cpuMonitoringInit(count) != 0 {
return Err(Error::CpuMonitoring);
}
bela_sys::Bela_cpuMonitoringGet()
};
if data.is_null() {
return Err(Error::CpuMonitoring);
}
unsafe {
bela_sys::Bela_cpuTic(data);
discard_first_measurement(&mut *data);
}
Ok(())
}
#[cfg(bela_device)]
fn disable_monitoring() -> Result<(), Error> {
let data = unsafe { bela_sys::Bela_cpuMonitoringGet() };
if data.is_null() {
return Err(Error::CpuMonitoring);
}
unsafe { (*data).count = 0 };
Ok(())
}
pub const MAX_MONITORED_PERIOD_SIZE: c_int = 128;
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated audio system applies settings; still unit-tested on the host"
)
)]
pub const fn check_period_size(period_size: c_int) -> Result<(), Error> {
if period_size <= MAX_MONITORED_PERIOD_SIZE {
Ok(())
} else {
Err(Error::CpuMonitoringPeriodSize(period_size))
}
}
#[cfg_attr(
not(bela_device),
allow(
dead_code,
reason = "only the device-gated audio system enables monitoring; still unit-tested on the host"
)
)]
pub const fn check_cycle(measurements_per_cycle: NonZeroU32) -> Result<c_int, Error> {
let count = measurements_per_cycle.get();
#[allow(
clippy::cast_possible_wrap,
reason = "the comparison above rules out the values that would wrap"
)]
if count <= c_int::MAX as u32 {
Ok(count as c_int)
} else {
Err(Error::CpuMonitoringCycle(count))
}
}
#[derive(Debug)]
pub struct CpuTimer {
data: BelaCpuData,
primed: bool,
}
impl CpuTimer {
#[must_use]
pub const fn new(measurements_per_cycle: NonZeroU32) -> Self {
let mut data = ZEROED_CPU_DATA;
data.count = measurements_per_cycle.get();
Self {
data,
primed: false,
}
}
pub fn measure(&mut self) -> CpuSection<'_> {
self.tic();
CpuSection { timer: self }
}
#[cfg_attr(
not(bela_device),
allow(
clippy::missing_const_for_fn,
reason = "only const off-device, where the clock is never read"
)
)]
pub fn tic(&mut self) {
self.tic_raw();
if !self.primed {
discard_first_measurement(&mut self.data);
self.primed = true;
}
}
#[cfg_attr(
not(bela_device),
allow(
clippy::missing_const_for_fn,
reason = "only const off-device, where the clock is never read"
)
)]
pub fn toc(&mut self) {
self.toc_raw();
}
#[must_use]
pub const fn usage(&self) -> CpuUsage {
CpuUsage::from_raw(&self.data)
}
#[cfg(bela_device)]
fn tic_raw(&mut self) {
unsafe { bela_sys::Bela_cpuTic(&raw mut self.data) };
}
#[cfg(bela_device)]
fn toc_raw(&mut self) {
unsafe { bela_sys::Bela_cpuToc(&raw mut self.data) };
}
#[cfg(not(bela_device))]
#[allow(
clippy::needless_pass_by_ref_mut,
clippy::unused_self,
reason = "mirrors the device signature, which mutates the counters"
)]
const fn tic_raw(&mut self) {}
#[cfg(not(bela_device))]
#[allow(
clippy::needless_pass_by_ref_mut,
clippy::unused_self,
reason = "mirrors the device signature, which mutates the counters"
)]
const fn toc_raw(&mut self) {}
}
const fn discard_first_measurement(data: &mut BelaCpuData) {
data.busy = 0;
data.total = 0;
data.currentCount = 0;
data.percentage = 0.0;
}
const ZEROED_CPU_DATA: BelaCpuData = BelaCpuData {
count: 0,
currentCount: 0,
busy: 0,
total: 0,
tic: ZEROED_TIMESPEC,
toc: ZEROED_TIMESPEC,
percentage: 0.0,
};
const ZEROED_TIMESPEC: bela_sys::timespec = bela_sys::timespec {
tv_sec: 0,
tv_nsec: 0,
};
#[derive(Debug)]
#[must_use = "the measurement ends when this is dropped, so `let _ = ...` measures nothing"]
pub struct CpuSection<'a> {
timer: &'a mut CpuTimer,
}
impl Drop for CpuSection<'_> {
fn drop(&mut self) {
self.timer.toc();
}
}
#[cfg(test)]
#[allow(
clippy::float_cmp,
reason = "the counters are copied verbatim, so the expected values are exact"
)]
mod tests {
use super::*;
fn cycle_of(measurements: u32) -> NonZeroU32 {
NonZeroU32::new(measurements).expect("the test cycles are non-zero")
}
fn measured() -> BelaCpuData {
BelaCpuData {
count: 1000,
currentCount: 250,
busy: 3_000_000,
total: 12_000_000,
tic: bela_sys::timespec {
tv_sec: 5,
tv_nsec: 6,
},
toc: bela_sys::timespec {
tv_sec: 7,
tv_nsec: 8,
},
percentage: 12.34,
}
}
#[test]
fn a_reading_reports_belas_counters() {
let usage = CpuUsage::from_raw(&measured());
assert!(
(usage.percentage() - 12.34).abs() < f32::EPSILON,
"expected the percentage from the last completed cycle, got {}",
usage.percentage()
);
assert_eq!(usage.busy(), Duration::from_millis(3));
assert_eq!(usage.total(), Duration::from_millis(12));
assert_eq!(usage.measurements_per_cycle(), 1000);
assert_eq!(usage.measurements_taken(), 250);
}
#[test]
fn a_reading_prints_the_percentage_and_the_cycle() {
let usage = CpuUsage::from_raw(&measured());
assert_eq!(
usage.to_string(),
"12.3% busy, averaged over 1000 measurements"
);
}
#[test]
fn nothing_measured_yet_reads_as_zero() {
let usage = CpuUsage::from_raw(&ZEROED_CPU_DATA);
assert_eq!(usage.percentage(), 0.0);
assert_eq!(usage.busy(), Duration::ZERO);
assert_eq!(usage.measurements_per_cycle(), 0);
assert_eq!(usage.measurements_taken(), 0);
}
#[test]
fn a_cycle_that_fits_in_a_c_int_is_passed_through() {
assert_eq!(check_cycle(cycle_of(2000)), Ok(2000));
assert_eq!(
check_cycle(cycle_of(c_int::MAX.unsigned_abs())),
Ok(c_int::MAX),
"the largest representable cycle should still be accepted"
);
}
#[test]
fn a_cycle_too_large_for_a_c_int_is_refused() {
let over = c_int::MAX.unsigned_abs() + 1;
assert_eq!(
check_cycle(cycle_of(over)),
Err(Error::CpuMonitoringCycle(over))
);
assert_eq!(
check_cycle(cycle_of(u32::MAX)),
Err(Error::CpuMonitoringCycle(u32::MAX))
);
}
#[test]
fn period_sizes_that_keep_render_on_the_measured_thread_are_accepted() {
for frames in [1, 16, 32, 64, MAX_MONITORED_PERIOD_SIZE] {
assert_eq!(check_period_size(frames), Ok(()), "{frames} frames");
}
}
#[test]
fn a_period_size_that_moves_render_off_the_measured_thread_is_refused() {
let over = MAX_MONITORED_PERIOD_SIZE + 1;
assert_eq!(
check_period_size(over),
Err(Error::CpuMonitoringPeriodSize(over))
);
assert_eq!(
check_period_size(256),
Err(Error::CpuMonitoringPeriodSize(256))
);
}
#[test]
fn a_timer_starts_with_the_cycle_it_was_given() {
let usage = CpuTimer::new(cycle_of(64)).usage();
assert_eq!(usage.measurements_per_cycle(), 64);
assert_eq!(usage.measurements_taken(), 0);
assert_eq!(usage.percentage(), 0.0);
}
#[test]
fn a_timer_takes_a_cycle_no_c_int_could_hold() {
let usage = CpuTimer::new(cycle_of(u32::MAX)).usage();
assert_eq!(usage.measurements_per_cycle(), u32::MAX);
}
#[test]
fn the_first_measurement_is_discarded() {
let mut data = BelaCpuData {
count: 1,
currentCount: 1,
busy: 40_000,
total: 9_000_000_000_000,
percentage: 0.000_001,
..measured()
};
discard_first_measurement(&mut data);
assert_eq!(data.busy, 0);
assert_eq!(data.total, 0);
assert_eq!(data.currentCount, 0);
assert_eq!(data.percentage, 0.0);
assert_eq!(
(data.tic.tv_sec, data.tic.tv_nsec),
(5, 6),
"the timestamp is what the next measurement is taken from"
);
}
#[test]
fn only_the_first_tic_discards_anything() {
let mut timer = CpuTimer::new(cycle_of(4));
timer.tic();
assert!(timer.primed, "the first tic should have primed the timer");
timer.data.busy = 40_000;
timer.data.total = 360_000;
timer.data.currentCount = 1;
timer.tic();
assert_eq!(timer.usage().busy(), Duration::from_nanos(40_000));
assert_eq!(timer.usage().total(), Duration::from_nanos(360_000));
assert_eq!(timer.usage().measurements_taken(), 1);
}
#[test]
fn a_section_tocs_when_it_is_dropped() {
let mut timer = CpuTimer::new(cycle_of(4));
{
let _section = timer.measure();
}
assert!(timer.primed, "measure() should have tic'd");
}
#[test]
fn there_is_nothing_to_report_off_device() {
use core::mem;
let mut context: bela_sys::BelaContext = unsafe { mem::zeroed() };
let context = unsafe { Context::from_mut_ptr(&raw mut context) };
assert_eq!(
context.cpu_usage(),
None,
"off-device there is no audio thread to monitor"
);
}
}