oxideav_core/execution.rs
1//! Runtime hints passed from the executor to codecs and filters.
2//!
3//! An [`ExecutionContext`] carries advisory information — today only a
4//! thread budget — that codecs can use to tune their internal
5//! parallelism. Codecs that don't care can ignore it; the default trait
6//! method on [`Decoder`](../../oxideav_codec/trait.Decoder.html) /
7//! [`Encoder`](../../oxideav_codec/trait.Encoder.html) is a no-op.
8//!
9//! # Threading contract
10//!
11//! The context is the **single threading authority** for a codec:
12//!
13//! * A codec runs **serial until told otherwise** — before
14//! `set_execution_context` is called (or when it never is), internal
15//! fan-out is one worker.
16//! * Every internal fan-out is bounded through
17//! [`ExecutionContext::effective_workers`], never by querying the host
18//! directly. Host-derived budgets are the *caller's* decision, made by
19//! constructing the context with [`ExecutionContext::auto`].
20//! * Threading stays optional: a codec with no internal parallelism
21//! simply keeps the default no-op trait method, and callers must
22//! always work with a codec that runs serial regardless of the budget
23//! they granted.
24
25/// Advisory runtime information handed to a codec after construction.
26///
27/// The struct is deliberately tiny for now. New fields can be added
28/// without breaking API consumers that already construct the value via
29/// [`ExecutionContext::serial`] or [`ExecutionContext::with_threads`].
30#[derive(Clone, Debug)]
31pub struct ExecutionContext {
32 /// Advisory cap on how many threads a codec may use for its own
33 /// internal parallelism (slice-parallel decode, GOP-parallel decode,
34 /// etc.). Always `≥ 1`. `1` means "caller requests serial execution
35 /// from this codec" — obey it unless you have a very good reason.
36 pub threads: usize,
37}
38
39impl ExecutionContext {
40 /// Ask the codec to run strictly single-threaded.
41 pub const fn serial() -> Self {
42 Self { threads: 1 }
43 }
44
45 /// Budget the codec to at most `threads` internal workers. Values
46 /// below 1 are clamped up to 1.
47 pub fn with_threads(threads: usize) -> Self {
48 Self {
49 threads: threads.max(1),
50 }
51 }
52
53 /// Derive the budget from the host:
54 /// [`std::thread::available_parallelism`], falling back to `1` when
55 /// the host refuses to answer.
56 ///
57 /// This is the **caller-side** convenience for "use the machine".
58 /// Codecs never call it — they receive whatever budget the caller
59 /// chose and bound their fan-out with [`Self::effective_workers`].
60 pub fn auto() -> Self {
61 let threads = std::thread::available_parallelism()
62 .map(std::num::NonZeroUsize::get)
63 .unwrap_or(1);
64 Self { threads }
65 }
66
67 /// Bound a codec-internal fan-out: the number of workers to spawn
68 /// for `work_units` independent units of work under this budget.
69 ///
70 /// Returns `min(self.threads, work_units)`, and never less than 1
71 /// (`work_units == 0` still yields 1 so degenerate inputs stay on
72 /// the plain serial path). This is the one clamp codecs use for
73 /// every slice-/tile-/field-/GOP-parallel dispatch; querying host
74 /// parallelism directly from codec code is out of contract.
75 pub fn effective_workers(&self, work_units: usize) -> usize {
76 self.threads.min(work_units).max(1)
77 }
78}
79
80impl Default for ExecutionContext {
81 fn default() -> Self {
82 Self::serial()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::ExecutionContext;
89
90 #[test]
91 fn serial_is_one_thread_and_default() {
92 assert_eq!(ExecutionContext::serial().threads, 1);
93 assert_eq!(ExecutionContext::default().threads, 1);
94 }
95
96 #[test]
97 fn with_threads_clamps_up_to_one() {
98 assert_eq!(ExecutionContext::with_threads(0).threads, 1);
99 assert_eq!(ExecutionContext::with_threads(1).threads, 1);
100 assert_eq!(ExecutionContext::with_threads(8).threads, 8);
101 }
102
103 #[test]
104 fn auto_is_at_least_one() {
105 assert!(ExecutionContext::auto().threads >= 1);
106 }
107
108 #[test]
109 fn effective_workers_clamps_both_sides() {
110 let ctx = ExecutionContext::with_threads(4);
111 assert_eq!(ctx.effective_workers(0), 1);
112 assert_eq!(ctx.effective_workers(1), 1);
113 assert_eq!(ctx.effective_workers(3), 3);
114 assert_eq!(ctx.effective_workers(4), 4);
115 assert_eq!(ctx.effective_workers(64), 4);
116
117 let serial = ExecutionContext::serial();
118 assert_eq!(serial.effective_workers(64), 1);
119 assert_eq!(serial.effective_workers(0), 1);
120 }
121}