Skip to main content

task_lifecycle/
task_lifecycle.rs

1//! Hardware check for the auxiliary task lifecycle rules that cannot
2//! be tested on the host, because they need a real audio system.
3//!
4//! Run by `scripts/smoke-test.sh`, which asserts on the line this
5//! prints from `cleanup`. It covers three things:
6//!
7//! - a task created in the `setup` of an audio system that is then
8//!   dropped without ever being started is retired with it, so
9//!   scheduling its handle from a *later* audio system does nothing;
10//! - a task belonging to the running audio system still works;
11//! - creating a task from `cleanup` — which runs inside the teardown —
12//!   fails instead of handing back a task that is about to be deleted.
13//!
14//! Cross-compile and run on the board (see docs/cross-compile.md):
15//!
16//! ```sh
17//! cargo build -p bela --release --target aarch64-unknown-linux-gnu --example task_lifecycle
18//! ```
19
20#![cfg_attr(
21    not(bela_device),
22    allow(
23        dead_code,
24        reason = "only the fallback main is reachable off-device; the application code should still compile and lint"
25    )
26)]
27
28use core::sync::atomic::{AtomicU64, Ordering};
29#[cfg(not(bela_device))]
30use std::process::ExitCode;
31use std::sync::{Arc, Mutex, PoisonError};
32
33use bela::{
34    AuxiliaryTask, BelaApplication, BlockContext, CleanupContext, Error, Priority, RenderContext,
35    SetupContext, ThreadInfo, rt_println,
36};
37
38const TASK_PRIORITY: Priority = Priority::new(50).expect("50 is within Bela's priority range");
39
40/// Creates the task whose handle is then used after its audio system
41/// is gone. Never started, only initialised and dropped.
42struct Abandoned {
43    runs: Arc<AtomicU64>,
44    handle: Arc<Mutex<Option<AuxiliaryTask>>>,
45}
46
47impl BelaApplication for Abandoned {
48    type RenderState = ();
49
50    fn setup(&mut self, _context: &SetupContext) -> bool {
51        let runs = Arc::clone(&self.runs);
52        match AuxiliaryTask::new("bela-rs-abandoned", TASK_PRIORITY, move || {
53            runs.fetch_add(1, Ordering::Relaxed);
54        }) {
55            Ok(task) => {
56                // Hand the handle out, as a Send handle could be handed
57                // to any other thread.
58                *self.handle.lock().unwrap_or_else(PoisonError::into_inner) = Some(task);
59                true
60            }
61            Err(error) => {
62                rt_println!("lifecycle: could not create the abandoned task: {error}");
63                false
64            }
65        }
66    }
67
68    fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
69
70    fn render(&self, _state: &mut (), _context: &mut RenderContext) {}
71}
72
73/// Runs for real, scheduling both the stale handle and one of its own.
74struct Survivor {
75    stale: Option<AuxiliaryTask>,
76    stale_runs: Arc<AtomicU64>,
77    fresh: Option<AuxiliaryTask>,
78    fresh_runs: Arc<AtomicU64>,
79    blocks: u64,
80    interval: u64,
81}
82
83impl BelaApplication for Survivor {
84    type RenderState = ();
85
86    fn setup(&mut self, context: &SetupContext) -> bool {
87        #[allow(
88            clippy::cast_possible_truncation,
89            clippy::cast_sign_loss,
90            reason = "the sample rate is a small positive number"
91        )]
92        let sample_rate_hz = context.audio_sample_rate() as u64;
93        self.interval = (sample_rate_hz / context.audio_frames().max(1) as u64).max(1);
94
95        let runs = Arc::clone(&self.fresh_runs);
96        self.fresh = AuxiliaryTask::new("bela-rs-survivor", TASK_PRIORITY, move || {
97            runs.fetch_add(1, Ordering::Relaxed);
98        })
99        .ok();
100        self.fresh.is_some()
101    }
102
103    fn create_render_state(&mut self, _thread: ThreadInfo, _context: &SetupContext) {}
104
105    fn render(&self, _state: &mut (), _context: &mut RenderContext) {}
106
107    // Real-time safe: a counter and two schedules, both of which
108    // return immediately. Once per block, on the main audio thread.
109    fn render_post(&mut self, _states: &mut [()], context: &mut BlockContext) {
110        self.blocks += 1;
111        if self.blocks % self.interval != 0 {
112            return;
113        }
114        if let Some(stale) = &self.stale {
115            stale.schedule(context);
116        }
117        if let Some(fresh) = &self.fresh {
118            fresh.schedule(context);
119        }
120    }
121
122    fn cleanup(&mut self, _states: &mut [()], _context: &CleanupContext) {
123        // `cleanup` runs inside the teardown, so this must fail.
124        let created = AuxiliaryTask::new("bela-rs-in-cleanup", TASK_PRIORITY, || {});
125        let cleanup_create = match created {
126            Err(Error::TaskCreateWhileStopping) => "rejected",
127            Err(_) => "failed-otherwise",
128            Ok(_) => "created",
129        };
130        rt_println!(
131            "lifecycle: stale-runs={} fresh-runs={} cleanup-create={}",
132            self.stale_runs.load(Ordering::Relaxed),
133            self.fresh_runs.load(Ordering::Relaxed),
134            cleanup_create
135        );
136    }
137}
138
139#[cfg(bela_device)]
140fn main() -> Result<(), Error> {
141    use bela::{Bela, Settings};
142
143    let stale_runs = Arc::new(AtomicU64::new(0));
144    let handle = Arc::new(Mutex::new(None));
145
146    // Initialised, never started, then dropped — a teardown all the
147    // same, and the task created in its setup goes with it.
148    drop(Bela::new(
149        Abandoned {
150            runs: Arc::clone(&stale_runs),
151            handle: Arc::clone(&handle),
152        },
153        &Settings::new(),
154    )?);
155    rt_println!("lifecycle: abandoned audio system dropped without starting");
156
157    let stale = handle.lock().unwrap_or_else(PoisonError::into_inner).take();
158    Bela::run(
159        Survivor {
160            stale,
161            stale_runs,
162            fresh: None,
163            fresh_runs: Arc::new(AtomicU64::new(0)),
164            blocks: 0,
165            interval: 1,
166        },
167        &Settings::new(),
168    )
169}
170
171#[cfg(not(bela_device))]
172fn main() -> ExitCode {
173    eprintln!("This example must be cross-compiled for Bela Gem (aarch64-unknown-linux-gnu).");
174    ExitCode::FAILURE
175}