Skip to main content

zerodds_corba_rt/
scheduling.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Static scheduling service (RT-CORBA scheduling profile): task model +
5//! a-priori schedulability analysis for fixed-priority preemptive scheduling.
6//!
7//! Two tests over a periodic task set `(T_i, C_i, D_i)`:
8//! - **Liu & Layland utilization bound** ([`TaskSet::is_schedulable_ll`]) —
9//!   *sufficient* for rate-monotonic with implicit deadlines (`D = T`):
10//!   `U = Σ C_i/T_i ≤ n·(2^(1/n) − 1)`.
11//! - **Response-time analysis** ([`TaskSet::is_schedulable_rta`]) — *exact*
12//!   (necessary + sufficient) for fixed-priority preemptive:
13//!   `R_i = C_i + Σ_{j∈hp(i)} ⌈R_i/T_j⌉·C_j` as a fixed point, schedulable
14//!   iff `R_i ≤ D_i` for all `i`.
15//!
16//! A certification-relevant profile (avionics/defense): it proves
17//! *in advance* that all deadlines are met under worst-case load.
18
19use alloc::vec::Vec;
20
21use crate::priority::Priority;
22
23/// A periodic (or sporadic) task in the static schedule.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct Task {
26    /// Period `T` (time units, e.g. µs): minimum gap between two releases.
27    pub period: u64,
28    /// Worst-case execution time `C` (same unit as `period`).
29    pub wcet: u64,
30    /// Relative deadline `D` (`≤ T`). `0` → implicit deadline (`D = T`).
31    pub deadline: u64,
32    /// Fixed execution priority (higher = more urgent). Derivable from the
33    /// periods via [`TaskSet::assign_rate_monotonic`].
34    pub priority: Priority,
35}
36
37impl Task {
38    /// Task with implicit deadline (`D = T`).
39    #[must_use]
40    pub fn new(period: u64, wcet: u64, priority: Priority) -> Self {
41        Self {
42            period,
43            wcet,
44            deadline: 0,
45            priority,
46        }
47    }
48
49    /// Task with explicit (constrained) deadline `D ≤ T`.
50    #[must_use]
51    pub fn with_deadline(period: u64, wcet: u64, deadline: u64, priority: Priority) -> Self {
52        Self {
53            period,
54            wcet,
55            deadline,
56            priority,
57        }
58    }
59
60    /// Effective relative deadline: `deadline`, or `period` if `deadline == 0`.
61    #[must_use]
62    pub fn effective_deadline(&self) -> u64 {
63        if self.deadline == 0 {
64            self.period
65        } else {
66            self.deadline
67        }
68    }
69}
70
71/// A task set for the static schedulability analysis.
72#[derive(Debug, Clone, Default)]
73pub struct TaskSet {
74    /// The contained tasks.
75    pub tasks: Vec<Task>,
76}
77
78/// `n`-th root of 2 via Newton iteration — `no_std`-capable, uses only
79/// basic f64 operations (no `powf`/`ln` from `std`).
80#[must_use]
81fn nth_root_of_two(n: u32) -> f64 {
82    if n == 0 {
83        return 1.0;
84    }
85    if n == 1 {
86        return 2.0;
87    }
88    let nf = f64::from(n);
89    let mut x = 1.0_f64;
90    // f(x) = x^n − 2, x_{k+1} = x_k − (x_k^n − 2)/(n·x_k^{n−1}).
91    let mut iter = 0;
92    while iter < 100 {
93        let mut x_nm1 = 1.0_f64; // x^{n-1}
94        let mut k = 0;
95        while k < n - 1 {
96            x_nm1 *= x;
97            k += 1;
98        }
99        let x_n = x_nm1 * x;
100        let denom = nf * x_nm1;
101        if denom == 0.0 {
102            break;
103        }
104        let next = x - (x_n - 2.0) / denom;
105        let delta = if next > x { next - x } else { x - next };
106        x = next;
107        if delta < 1e-13 {
108            break;
109        }
110        iter += 1;
111    }
112    x
113}
114
115impl TaskSet {
116    /// Empty task set.
117    #[must_use]
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Adds a task.
123    pub fn push(&mut self, task: Task) {
124        self.tasks.push(task);
125    }
126
127    /// Number of tasks.
128    #[must_use]
129    pub fn len(&self) -> usize {
130        self.tasks.len()
131    }
132
133    /// Whether the task set is empty.
134    #[must_use]
135    pub fn is_empty(&self) -> bool {
136        self.tasks.is_empty()
137    }
138
139    /// Total utilization `U = Σ C_i/T_i`.
140    #[must_use]
141    pub fn utilization(&self) -> f64 {
142        self.tasks
143            .iter()
144            .filter(|t| t.period > 0)
145            .map(|t| t.wcet as f64 / t.period as f64)
146            .sum()
147    }
148
149    /// The Liu & Layland utilization bound `n·(2^(1/n) − 1)` for `n` tasks.
150    #[must_use]
151    pub fn liu_layland_bound(n: usize) -> f64 {
152        if n == 0 {
153            return 1.0;
154        }
155        let nf = n as f64;
156        nf * (nth_root_of_two(n as u32) - 1.0)
157    }
158
159    /// Assigns rate-monotonic priorities: shorter period → higher priority.
160    /// The priorities are assigned evenly across `0..=MAX` in order
161    /// (unique, as long as ≤ 32768 tasks).
162    pub fn assign_rate_monotonic(&mut self) {
163        let n = self.tasks.len();
164        if n == 0 {
165            return;
166        }
167        // Indices by ascending period (shortest first = most urgent).
168        let mut order: Vec<usize> = (0..n).collect();
169        order.sort_by_key(|&i| self.tasks[i].period);
170        // Highest rank → highest priority. Unique, monotonically decreasing values.
171        for (rank, &idx) in order.iter().enumerate() {
172            let prio_val = (n - 1 - rank) as i32;
173            self.tasks[idx].priority = Priority::clamped(prio_val);
174        }
175    }
176
177    /// Higher-priority set `hp(i)`: tasks with *strictly higher* priority than
178    /// task `i` (they preempt `i`).
179    fn higher_priority_than(&self, i: usize) -> impl Iterator<Item = &Task> {
180        let pi = self.tasks[i].priority;
181        self.tasks
182            .iter()
183            .enumerate()
184            .filter(move |(j, t)| *j != i && t.priority > pi)
185            .map(|(_, t)| t)
186    }
187
188    /// Exact worst-case response time `R_i` of task `i` via fixed-point iteration.
189    /// `None` if `R_i` exceeds the deadline (task not schedulable).
190    #[must_use]
191    pub fn response_time(&self, i: usize) -> Option<u64> {
192        if i >= self.tasks.len() {
193            return None;
194        }
195        let ti = self.tasks[i];
196        let deadline = ti.effective_deadline();
197        let mut r = ti.wcet;
198        loop {
199            let interference: u64 = self
200                .higher_priority_than(i)
201                .map(|t| r.div_ceil(t.period) * t.wcet)
202                .sum();
203            let next = ti.wcet + interference;
204            if next == r {
205                return if r <= deadline { Some(r) } else { None };
206            }
207            if next > deadline {
208                return None; // diverges past the deadline → not schedulable
209            }
210            r = next;
211        }
212    }
213
214    /// Response times of all tasks (index-parallel to `tasks`).
215    #[must_use]
216    pub fn response_times(&self) -> Vec<Option<u64>> {
217        (0..self.tasks.len())
218            .map(|i| self.response_time(i))
219            .collect()
220    }
221
222    /// Exact schedulability test (response-time analysis): all `R_i ≤ D_i`.
223    #[must_use]
224    pub fn is_schedulable_rta(&self) -> bool {
225        (0..self.tasks.len()).all(|i| self.response_time(i).is_some())
226    }
227
228    /// Sufficient schedulability test (Liu & Layland): `U ≤ n·(2^(1/n) − 1)`.
229    /// Only meaningful for rate-monotonic with implicit deadlines; a `false`
230    /// does *not necessarily* mean not-schedulable (use RTA in that case).
231    #[must_use]
232    pub fn is_schedulable_ll(&self) -> bool {
233        self.utilization() <= Self::liu_layland_bound(self.tasks.len()) + 1e-9
234    }
235}
236
237#[cfg(test)]
238#[allow(clippy::unwrap_used, clippy::panic, clippy::float_cmp)]
239mod tests {
240    use super::*;
241
242    fn p(v: i16) -> Priority {
243        Priority::new(v).unwrap()
244    }
245
246    #[test]
247    fn implicit_deadline_defaults_to_period() {
248        let t = Task::new(10, 3, p(5));
249        assert_eq!(t.effective_deadline(), 10);
250        let t2 = Task::with_deadline(10, 3, 7, p(5));
251        assert_eq!(t2.effective_deadline(), 7);
252    }
253
254    #[test]
255    fn rate_monotonic_assigns_shortest_period_highest() {
256        let mut ts = TaskSet::new();
257        ts.push(Task::new(20, 5, p(0)));
258        ts.push(Task::new(4, 1, p(0)));
259        ts.push(Task::new(5, 2, p(0)));
260        ts.assign_rate_monotonic();
261        // T=4 most urgent → highest priority; T=20 lowest.
262        assert!(ts.tasks[1].priority > ts.tasks[2].priority);
263        assert!(ts.tasks[2].priority > ts.tasks[0].priority);
264    }
265
266    #[test]
267    fn response_time_analysis_exact_values() {
268        // Classic RM set: (T,C) = (4,1),(5,2),(20,5).
269        let mut ts = TaskSet::new();
270        ts.push(Task::new(4, 1, p(0)));
271        ts.push(Task::new(5, 2, p(0)));
272        ts.push(Task::new(20, 5, p(0)));
273        ts.assign_rate_monotonic();
274        let r = ts.response_times();
275        assert_eq!(r[0], Some(1)); // highest priority, no interference
276        assert_eq!(r[1], Some(3)); // 2 + ⌈3/4⌉·1
277        assert_eq!(r[2], Some(15)); // fixed point at 15
278        assert!(ts.is_schedulable_rta());
279    }
280
281    #[test]
282    fn rta_stronger_than_liu_layland() {
283        // U = 0.25+0.4+0.25 = 0.9 > LL bound(3) ≈ 0.78 → LL says "no",
284        // but RTA proves schedulability.
285        let mut ts = TaskSet::new();
286        ts.push(Task::new(4, 1, p(0)));
287        ts.push(Task::new(5, 2, p(0)));
288        ts.push(Task::new(20, 5, p(0)));
289        ts.assign_rate_monotonic();
290        assert!((ts.utilization() - 0.9).abs() < 1e-9);
291        assert!(!ts.is_schedulable_ll());
292        assert!(ts.is_schedulable_rta());
293    }
294
295    #[test]
296    fn overloaded_set_is_unschedulable() {
297        // (T,C) = (4,3),(5,3): U = 0.75+0.6 = 1.35 > 1 → impossible.
298        let mut ts = TaskSet::new();
299        ts.push(Task::new(4, 3, p(0)));
300        ts.push(Task::new(5, 3, p(0)));
301        ts.assign_rate_monotonic();
302        assert!(!ts.is_schedulable_rta());
303        assert!(ts.response_time(1).is_none());
304    }
305
306    #[test]
307    fn liu_layland_bound_known_points() {
308        assert!((TaskSet::liu_layland_bound(1) - 1.0).abs() < 1e-9);
309        assert!((TaskSet::liu_layland_bound(2) - 0.8284271).abs() < 1e-6);
310        assert!((TaskSet::liu_layland_bound(3) - 0.7797631).abs() < 1e-6);
311        // Converges towards ln(2).
312        let big = TaskSet::liu_layland_bound(1000);
313        assert!((big - core::f64::consts::LN_2).abs() < 1e-3, "big={big}");
314    }
315
316    #[test]
317    fn constrained_deadline_tightens_test() {
318        // (4,1),(5,2),(20,5) is schedulable with D=T; R_3 = 15.
319        // With D_3 = 12 < 15, task 3 becomes unschedulable.
320        let mut ts = TaskSet::new();
321        ts.push(Task::new(4, 1, p(0)));
322        ts.push(Task::new(5, 2, p(0)));
323        ts.push(Task::with_deadline(20, 5, 12, p(0)));
324        ts.assign_rate_monotonic();
325        assert!(!ts.is_schedulable_rta());
326    }
327}