⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/sched/
deadline.rs

1//! Earliest Deadline First (EDF) Deadline Scheduler
2//!
3//! Implements SCHED_DEADLINE scheduling policy alongside the existing CFS
4//! scheduler. Deadline tasks always preempt CFS tasks. Among deadline tasks,
5//! the one with the earliest absolute deadline is selected. Admission control
6//! ensures total CPU utilization does not exceed 100% (using fixed-point
7//! arithmetic scaled by 1000).
8//!
9//! Key concepts:
10//! - **Runtime**: Maximum execution time per period (nanoseconds)
11//! - **Deadline**: Relative deadline from start of period (nanoseconds)
12//! - **Period**: Activation period (nanoseconds)
13//! - **Admission control**: Sum of (runtime/period) for all tasks must not
14//!   exceed 1.0
15//! - **Replenishment**: Runtime is reset at period boundaries
16
17#[cfg(feature = "alloc")]
18extern crate alloc;
19#[cfg(feature = "alloc")]
20use alloc::collections::BTreeMap;
21
22use crate::{
23    error::{KernelError, SchedError},
24    process::ProcessId,
25};
26
27/// SCHED_DEADLINE policy constant (Linux-compatible value)
28pub const SCHED_DEADLINE: u32 = 6;
29
30/// Fixed-point scale factor for utilization calculations.
31/// Utilization is represented as parts per 1000 (permille).
32/// A task with runtime=period has utilization = 1000.
33/// Total utilization must not exceed 1000 (= 100%).
34const UTIL_SCALE: u64 = 1000;
35
36/// Maximum number of deadline tasks (for bounded resource usage)
37const MAX_DEADLINE_TASKS: usize = 64;
38
39/// A deadline scheduling entity describing a task's timing parameters.
40#[derive(Debug, Clone, Copy)]
41pub struct DeadlineEntity {
42    /// Process ID
43    pub pid: ProcessId,
44    /// Worst-case execution time per period (nanoseconds)
45    pub runtime_ns: u64,
46    /// Relative deadline from period start (nanoseconds)
47    pub deadline_ns: u64,
48    /// Activation period (nanoseconds)
49    pub period_ns: u64,
50    /// Remaining runtime in current period (nanoseconds).
51    /// Decremented by tick(); reset by replenish().
52    pub runtime_remaining: u64,
53    /// Absolute deadline for current period (nanoseconds since boot).
54    /// Used to determine scheduling priority (earliest wins).
55    pub deadline_abs: u64,
56    /// Absolute start of current period (nanoseconds since boot).
57    /// Used to detect period boundaries for replenishment.
58    pub period_start: u64,
59    /// Whether this entity is currently throttled (runtime exhausted).
60    pub throttled: bool,
61}
62
63impl DeadlineEntity {
64    /// Create a new deadline entity.
65    ///
66    /// # Arguments
67    /// * `pid` - Process ID
68    /// * `runtime_ns` - Worst-case execution time per period
69    /// * `deadline_ns` - Relative deadline (must be <= period)
70    /// * `period_ns` - Activation period
71    /// * `now_ns` - Current time in nanoseconds since boot
72    pub fn new(
73        pid: ProcessId,
74        runtime_ns: u64,
75        deadline_ns: u64,
76        period_ns: u64,
77        now_ns: u64,
78    ) -> Self {
79        Self {
80            pid,
81            runtime_ns,
82            deadline_ns,
83            period_ns,
84            runtime_remaining: runtime_ns,
85            deadline_abs: now_ns.saturating_add(deadline_ns),
86            period_start: now_ns,
87            throttled: false,
88        }
89    }
90
91    /// Compute the utilization of this entity in permille (parts per 1000).
92    /// Returns `runtime_ns * 1000 / period_ns`.
93    fn utilization_permille(&self) -> u64 {
94        if self.period_ns == 0 {
95            return UTIL_SCALE; // Treat zero-period as full utilization
96        }
97        self.runtime_ns.saturating_mul(UTIL_SCALE) / self.period_ns
98    }
99
100    /// Check if this entity's period has expired and needs replenishment.
101    fn needs_replenish(&self, now_ns: u64) -> bool {
102        now_ns >= self.period_start.saturating_add(self.period_ns)
103    }
104
105    /// Replenish runtime for a new period.
106    fn replenish(&mut self, now_ns: u64) {
107        // Advance to the current period (handle missed periods)
108        if self.period_ns > 0 {
109            while self.period_start.saturating_add(self.period_ns) <= now_ns {
110                self.period_start = self.period_start.saturating_add(self.period_ns);
111            }
112        }
113        self.deadline_abs = self.period_start.saturating_add(self.deadline_ns);
114        self.runtime_remaining = self.runtime_ns;
115        self.throttled = false;
116    }
117}
118
119/// Scheduling attributes for sched_setattr-style interface.
120#[derive(Debug, Clone, Copy)]
121pub struct SchedAttr {
122    /// Scheduling policy (should be SCHED_DEADLINE)
123    pub policy: u32,
124    /// Worst-case execution time per period (nanoseconds)
125    pub runtime_ns: u64,
126    /// Relative deadline (nanoseconds)
127    pub deadline_ns: u64,
128    /// Activation period (nanoseconds)
129    pub period_ns: u64,
130}
131
132impl SchedAttr {
133    /// Validate the scheduling attributes.
134    pub fn validate(&self) -> Result<(), KernelError> {
135        if self.policy != SCHED_DEADLINE {
136            return Err(KernelError::InvalidArgument {
137                name: "policy",
138                value: "must be SCHED_DEADLINE",
139            });
140        }
141        if self.runtime_ns == 0 {
142            return Err(KernelError::InvalidArgument {
143                name: "runtime_ns",
144                value: "must be > 0",
145            });
146        }
147        if self.deadline_ns == 0 {
148            return Err(KernelError::InvalidArgument {
149                name: "deadline_ns",
150                value: "must be > 0",
151            });
152        }
153        if self.period_ns == 0 {
154            return Err(KernelError::InvalidArgument {
155                name: "period_ns",
156                value: "must be > 0",
157            });
158        }
159        if self.runtime_ns > self.deadline_ns {
160            return Err(KernelError::InvalidArgument {
161                name: "runtime_ns",
162                value: "must be <= deadline_ns",
163            });
164        }
165        if self.deadline_ns > self.period_ns {
166            return Err(KernelError::InvalidArgument {
167                name: "deadline_ns",
168                value: "must be <= period_ns",
169            });
170        }
171        Ok(())
172    }
173}
174
175/// The Earliest Deadline First (EDF) deadline scheduler.
176///
177/// Manages deadline tasks separately from CFS. Deadline tasks always have
178/// higher priority than CFS tasks. Among deadline tasks, the one with the
179/// earliest absolute deadline that still has runtime remaining is selected.
180#[cfg(feature = "alloc")]
181pub struct DeadlineScheduler {
182    /// All registered deadline entities, keyed by PID.
183    tasks: BTreeMap<u64, DeadlineEntity>,
184    /// Total utilization in permille (sum of runtime/period * 1000 for all
185    /// tasks). Must not exceed 1000 (100%).
186    total_utilization: u64,
187    /// PID of the currently running deadline task (if any).
188    current_pid: Option<u64>,
189}
190
191#[cfg(feature = "alloc")]
192impl Default for DeadlineScheduler {
193    fn default() -> Self {
194        Self::new()
195    }
196}
197
198#[cfg(feature = "alloc")]
199impl DeadlineScheduler {
200    /// Create a new empty deadline scheduler.
201    pub const fn new() -> Self {
202        Self {
203            tasks: BTreeMap::new(),
204            total_utilization: 0,
205            current_pid: None,
206        }
207    }
208
209    /// Return the number of registered deadline tasks.
210    pub fn task_count(&self) -> usize {
211        self.tasks.len()
212    }
213
214    /// Return the total utilization in permille.
215    pub fn total_utilization(&self) -> u64 {
216        self.total_utilization
217    }
218
219    /// Check if a task with the given PID is registered.
220    pub fn has_task(&self, pid: ProcessId) -> bool {
221        self.tasks.contains_key(&pid.0)
222    }
223
224    /// Add a deadline task with admission control.
225    ///
226    /// Performs admission control: the new task is accepted only if total
227    /// utilization (including this task) does not exceed 1000 permille (100%).
228    ///
229    /// # Arguments
230    /// * `pid` - Process ID
231    /// * `runtime_ns` - Worst-case execution time per period (nanoseconds)
232    /// * `deadline_ns` - Relative deadline (nanoseconds, must be <= period)
233    /// * `period_ns` - Activation period (nanoseconds)
234    /// * `now_ns` - Current time in nanoseconds since boot
235    ///
236    /// # Errors
237    /// Returns `KernelError` if:
238    /// - Parameters are invalid (zero values, runtime > deadline > period)
239    /// - Admission control fails (total utilization would exceed 100%)
240    /// - Maximum number of deadline tasks reached
241    /// - Task already registered
242    pub fn add_task(
243        &mut self,
244        pid: ProcessId,
245        runtime_ns: u64,
246        deadline_ns: u64,
247        period_ns: u64,
248        now_ns: u64,
249    ) -> Result<(), KernelError> {
250        // Validate parameters
251        let attr = SchedAttr {
252            policy: SCHED_DEADLINE,
253            runtime_ns,
254            deadline_ns,
255            period_ns,
256        };
257        attr.validate()?;
258
259        // Check for duplicate
260        if self.tasks.contains_key(&pid.0) {
261            return Err(KernelError::SchedulerError(SchedError::AlreadyScheduled));
262        }
263
264        // Check capacity
265        if self.tasks.len() >= MAX_DEADLINE_TASKS {
266            return Err(KernelError::ResourceExhausted {
267                resource: "deadline_tasks",
268            });
269        }
270
271        // Admission control: check if adding this task would exceed 100% utilization
272        let entity = DeadlineEntity::new(pid, runtime_ns, deadline_ns, period_ns, now_ns);
273        let new_util = entity.utilization_permille();
274        let proposed_total = self.total_utilization.saturating_add(new_util);
275
276        if proposed_total > UTIL_SCALE {
277            return Err(KernelError::ResourceExhausted {
278                resource: "deadline_bandwidth",
279            });
280        }
281
282        self.total_utilization = proposed_total;
283        self.tasks.insert(pid.0, entity);
284        Ok(())
285    }
286
287    /// Add a task using SchedAttr parameters.
288    pub fn add_task_attr(
289        &mut self,
290        pid: ProcessId,
291        attr: &SchedAttr,
292        now_ns: u64,
293    ) -> Result<(), KernelError> {
294        attr.validate()?;
295        self.add_task(
296            pid,
297            attr.runtime_ns,
298            attr.deadline_ns,
299            attr.period_ns,
300            now_ns,
301        )
302    }
303
304    /// Remove a deadline task.
305    ///
306    /// Returns the removed entity, or an error if the task was not found.
307    pub fn remove_task(&mut self, pid: ProcessId) -> Result<DeadlineEntity, KernelError> {
308        let entity = self
309            .tasks
310            .remove(&pid.0)
311            .ok_or(KernelError::SchedulerError(SchedError::TaskNotFound {
312                id: pid.0,
313            }))?;
314
315        // Reclaim utilization
316        let util = entity.utilization_permille();
317        self.total_utilization = self.total_utilization.saturating_sub(util);
318
319        // Clear current if this was the running task
320        if self.current_pid == Some(pid.0) {
321            self.current_pid = None;
322        }
323
324        Ok(entity)
325    }
326
327    /// Pick the next deadline task to run.
328    ///
329    /// Returns the PID of the task with the earliest absolute deadline that
330    /// still has runtime remaining (not throttled). Returns `None` if no
331    /// eligible deadline task exists.
332    pub fn pick_next(&mut self) -> Option<ProcessId> {
333        let mut best_pid: Option<u64> = None;
334        let mut best_deadline = u64::MAX;
335
336        for (&pid, entity) in self.tasks.iter() {
337            // Skip throttled tasks (runtime exhausted)
338            if entity.throttled {
339                continue;
340            }
341            // Skip tasks with no remaining runtime
342            if entity.runtime_remaining == 0 {
343                continue;
344            }
345            // Pick the task with the earliest absolute deadline
346            if entity.deadline_abs < best_deadline {
347                best_deadline = entity.deadline_abs;
348                best_pid = Some(pid);
349            }
350        }
351
352        if let Some(pid) = best_pid {
353            self.current_pid = Some(pid);
354            Some(ProcessId(pid))
355        } else {
356            self.current_pid = None;
357            None
358        }
359    }
360
361    /// Account elapsed time against the currently running deadline task.
362    ///
363    /// Decrements `runtime_remaining` for the currently running deadline task.
364    /// If runtime is exhausted, the task is throttled until the next period.
365    ///
366    /// # Arguments
367    /// * `elapsed_ns` - Nanoseconds elapsed since last tick
368    ///
369    /// # Returns
370    /// `true` if the current task was throttled (needs reschedule), `false`
371    /// otherwise.
372    pub fn tick(&mut self, elapsed_ns: u64) -> bool {
373        let pid = match self.current_pid {
374            Some(pid) => pid,
375            None => return false,
376        };
377
378        let entity = match self.tasks.get_mut(&pid) {
379            Some(e) => e,
380            None => {
381                self.current_pid = None;
382                return false;
383            }
384        };
385
386        if entity.runtime_remaining <= elapsed_ns {
387            entity.runtime_remaining = 0;
388            entity.throttled = true;
389            true // Needs reschedule
390        } else {
391            entity.runtime_remaining = entity.runtime_remaining.saturating_sub(elapsed_ns);
392            false
393        }
394    }
395
396    /// Replenish runtime for tasks whose periods have expired.
397    ///
398    /// Iterates all deadline tasks and resets runtime for any whose period
399    /// has elapsed. Should be called periodically (e.g., on timer tick).
400    ///
401    /// # Arguments
402    /// * `now_ns` - Current time in nanoseconds since boot
403    ///
404    /// # Returns
405    /// Number of tasks that were replenished.
406    pub fn replenish(&mut self, now_ns: u64) -> usize {
407        let mut count = 0;
408
409        for entity in self.tasks.values_mut() {
410            if entity.needs_replenish(now_ns) {
411                entity.replenish(now_ns);
412                count += 1;
413            }
414        }
415
416        count
417    }
418
419    /// Compute the time (in nanoseconds) until the next deadline event.
420    ///
421    /// This is the minimum of:
422    /// - The runtime remaining for the current task (throttle point)
423    /// - The time until the next period boundary (replenishment point)
424    /// - The time until the earliest absolute deadline
425    ///
426    /// Returns `None` if there are no deadline tasks. The returned value can
427    /// be used to program the APIC timer for precise deadline scheduling.
428    pub fn next_deadline_event(&self, now_ns: u64) -> Option<u64> {
429        if self.tasks.is_empty() {
430            return None;
431        }
432
433        let mut min_event = u64::MAX;
434
435        for entity in self.tasks.values() {
436            // Time until period boundary (replenishment)
437            let period_end = entity.period_start.saturating_add(entity.period_ns);
438            if period_end > now_ns {
439                let until_replenish = period_end.saturating_sub(now_ns);
440                if until_replenish < min_event {
441                    min_event = until_replenish;
442                }
443            }
444
445            // Time until absolute deadline
446            if entity.deadline_abs > now_ns {
447                let until_deadline = entity.deadline_abs.saturating_sub(now_ns);
448                if until_deadline < min_event {
449                    min_event = until_deadline;
450                }
451            }
452        }
453
454        // Also consider runtime remaining of current task
455        if let Some(pid) = self.current_pid {
456            if let Some(entity) = self.tasks.get(&pid) {
457                if !entity.throttled && entity.runtime_remaining < min_event {
458                    min_event = entity.runtime_remaining;
459                }
460            }
461        }
462
463        if min_event == u64::MAX {
464            None
465        } else {
466            Some(min_event)
467        }
468    }
469
470    /// Check whether a deadline task should preempt the current CFS task.
471    ///
472    /// Returns `true` if any non-throttled deadline task with remaining runtime
473    /// exists, meaning it should preempt CFS.
474    pub fn should_preempt_cfs(&self) -> bool {
475        self.tasks
476            .values()
477            .any(|e| !e.throttled && e.runtime_remaining > 0)
478    }
479
480    /// Get a reference to a deadline entity by PID.
481    pub fn get_task(&self, pid: ProcessId) -> Option<&DeadlineEntity> {
482        self.tasks.get(&pid.0)
483    }
484
485    /// Set the currently running deadline task PID.
486    pub fn set_current(&mut self, pid: Option<ProcessId>) {
487        self.current_pid = pid.map(|p| p.0);
488    }
489
490    /// Get the currently running deadline task PID.
491    pub fn current(&self) -> Option<ProcessId> {
492        self.current_pid.map(ProcessId)
493    }
494
495    /// Recalculate total utilization from scratch.
496    /// Useful after bulk modifications.
497    fn recalculate_utilization(&mut self) {
498        self.total_utilization = self
499            .tasks
500            .values()
501            .map(|e| e.utilization_permille())
502            .fold(0u64, |acc, u| acc.saturating_add(u));
503    }
504}
505
506/// Global deadline scheduler instance, protected by a spinlock.
507#[cfg(feature = "alloc")]
508pub(crate) static DEADLINE_SCHEDULER: spin::Mutex<DeadlineScheduler> =
509    spin::Mutex::new(DeadlineScheduler::new());
510
511// ============================================================================
512// Unit Tests
513// ============================================================================
514
515#[cfg(test)]
516mod tests {
517    #[cfg(feature = "alloc")]
518    #[allow(unused_imports)]
519    use alloc::vec;
520
521    use super::*;
522
523    // Helper to create a scheduler and add a task
524    #[cfg(feature = "alloc")]
525    fn make_scheduler() -> DeadlineScheduler {
526        DeadlineScheduler::new()
527    }
528
529    // --- DeadlineEntity tests ---
530
531    #[test]
532    fn test_entity_utilization_permille() {
533        // 50% utilization: runtime=5ms, period=10ms
534        let e = DeadlineEntity::new(ProcessId(1), 5_000_000, 10_000_000, 10_000_000, 0);
535        assert_eq!(e.utilization_permille(), 500);
536
537        // 100% utilization
538        let e = DeadlineEntity::new(ProcessId(2), 10_000_000, 10_000_000, 10_000_000, 0);
539        assert_eq!(e.utilization_permille(), 1000);
540
541        // 10% utilization
542        let e = DeadlineEntity::new(ProcessId(3), 1_000_000, 10_000_000, 10_000_000, 0);
543        assert_eq!(e.utilization_permille(), 100);
544
545        // Zero period => full utilization (safety)
546        let e = DeadlineEntity::new(ProcessId(4), 1_000_000, 0, 0, 0);
547        assert_eq!(e.utilization_permille(), 1000);
548    }
549
550    #[test]
551    fn test_entity_needs_replenish() {
552        let e = DeadlineEntity::new(ProcessId(1), 1_000_000, 5_000_000, 10_000_000, 100);
553        // Before period end
554        assert!(!e.needs_replenish(5_000_000));
555        // At period end
556        assert!(e.needs_replenish(10_000_100));
557        // After period end
558        assert!(e.needs_replenish(20_000_000));
559    }
560
561    #[test]
562    fn test_entity_replenish() {
563        let mut e = DeadlineEntity::new(ProcessId(1), 2_000_000, 5_000_000, 10_000_000, 0);
564        e.runtime_remaining = 0;
565        e.throttled = true;
566
567        // Replenish at period boundary
568        e.replenish(10_000_000);
569        assert_eq!(e.runtime_remaining, 2_000_000);
570        assert_eq!(e.period_start, 10_000_000);
571        assert_eq!(e.deadline_abs, 15_000_000); // 10M + 5M
572        assert!(!e.throttled);
573    }
574
575    #[test]
576    fn test_entity_replenish_skipped_periods() {
577        let mut e = DeadlineEntity::new(ProcessId(1), 1_000_000, 5_000_000, 10_000_000, 0);
578        e.runtime_remaining = 0;
579        e.throttled = true;
580
581        // Skip 3 periods
582        e.replenish(35_000_000);
583        assert_eq!(e.period_start, 30_000_000);
584        assert_eq!(e.deadline_abs, 35_000_000);
585        assert_eq!(e.runtime_remaining, 1_000_000);
586    }
587
588    // --- SchedAttr validation tests ---
589
590    #[test]
591    fn test_sched_attr_validate_valid() {
592        let attr = SchedAttr {
593            policy: SCHED_DEADLINE,
594            runtime_ns: 1_000_000,
595            deadline_ns: 5_000_000,
596            period_ns: 10_000_000,
597        };
598        assert!(attr.validate().is_ok());
599    }
600
601    #[test]
602    fn test_sched_attr_validate_wrong_policy() {
603        let attr = SchedAttr {
604            policy: 0,
605            runtime_ns: 1_000_000,
606            deadline_ns: 5_000_000,
607            period_ns: 10_000_000,
608        };
609        assert!(attr.validate().is_err());
610    }
611
612    #[test]
613    fn test_sched_attr_validate_zero_runtime() {
614        let attr = SchedAttr {
615            policy: SCHED_DEADLINE,
616            runtime_ns: 0,
617            deadline_ns: 5_000_000,
618            period_ns: 10_000_000,
619        };
620        assert!(attr.validate().is_err());
621    }
622
623    #[test]
624    fn test_sched_attr_validate_runtime_exceeds_deadline() {
625        let attr = SchedAttr {
626            policy: SCHED_DEADLINE,
627            runtime_ns: 6_000_000,
628            deadline_ns: 5_000_000,
629            period_ns: 10_000_000,
630        };
631        assert!(attr.validate().is_err());
632    }
633
634    #[test]
635    fn test_sched_attr_validate_deadline_exceeds_period() {
636        let attr = SchedAttr {
637            policy: SCHED_DEADLINE,
638            runtime_ns: 1_000_000,
639            deadline_ns: 15_000_000,
640            period_ns: 10_000_000,
641        };
642        assert!(attr.validate().is_err());
643    }
644
645    // --- DeadlineScheduler tests ---
646
647    #[cfg(feature = "alloc")]
648    #[test]
649    fn test_add_task_basic() {
650        let mut sched = make_scheduler();
651        let result = sched.add_task(
652            ProcessId(1),
653            1_000_000,  // 1ms runtime
654            5_000_000,  // 5ms deadline
655            10_000_000, // 10ms period
656            0,
657        );
658        assert!(result.is_ok());
659        assert_eq!(sched.task_count(), 1);
660        assert_eq!(sched.total_utilization(), 100); // 10%
661    }
662
663    #[cfg(feature = "alloc")]
664    #[test]
665    fn test_add_task_duplicate() {
666        let mut sched = make_scheduler();
667        let _ = sched.add_task(ProcessId(1), 1_000_000, 5_000_000, 10_000_000, 0);
668        let result = sched.add_task(ProcessId(1), 1_000_000, 5_000_000, 10_000_000, 0);
669        assert!(result.is_err());
670    }
671
672    #[cfg(feature = "alloc")]
673    #[test]
674    fn test_admission_control_accept() {
675        let mut sched = make_scheduler();
676        // 30% + 30% + 30% = 90% -> should accept
677        assert!(sched
678            .add_task(ProcessId(1), 3_000_000, 10_000_000, 10_000_000, 0)
679            .is_ok());
680        assert!(sched
681            .add_task(ProcessId(2), 3_000_000, 10_000_000, 10_000_000, 0)
682            .is_ok());
683        assert!(sched
684            .add_task(ProcessId(3), 3_000_000, 10_000_000, 10_000_000, 0)
685            .is_ok());
686        assert_eq!(sched.total_utilization(), 900);
687    }
688
689    #[cfg(feature = "alloc")]
690    #[test]
691    fn test_admission_control_reject() {
692        let mut sched = make_scheduler();
693        // 60% + 50% = 110% -> second should be rejected
694        assert!(sched
695            .add_task(ProcessId(1), 6_000_000, 10_000_000, 10_000_000, 0)
696            .is_ok());
697        let result = sched.add_task(ProcessId(2), 5_000_000, 10_000_000, 10_000_000, 0);
698        assert!(result.is_err());
699        assert_eq!(sched.task_count(), 1);
700        assert_eq!(sched.total_utilization(), 600);
701    }
702
703    #[cfg(feature = "alloc")]
704    #[test]
705    fn test_admission_control_exact_100_percent() {
706        let mut sched = make_scheduler();
707        // Exactly 100% should be accepted
708        assert!(sched
709            .add_task(ProcessId(1), 10_000_000, 10_000_000, 10_000_000, 0)
710            .is_ok());
711        assert_eq!(sched.total_utilization(), 1000);
712        // Adding any more should fail
713        let result = sched.add_task(ProcessId(2), 1_000_000, 10_000_000, 10_000_000, 0);
714        assert!(result.is_err());
715    }
716
717    #[cfg(feature = "alloc")]
718    #[test]
719    fn test_remove_task() {
720        let mut sched = make_scheduler();
721        let _ = sched.add_task(ProcessId(1), 3_000_000, 10_000_000, 10_000_000, 0);
722        let _ = sched.add_task(ProcessId(2), 2_000_000, 10_000_000, 10_000_000, 0);
723        assert_eq!(sched.total_utilization(), 500); // 30% + 20%
724
725        let result = sched.remove_task(ProcessId(1));
726        assert!(result.is_ok());
727        assert_eq!(sched.task_count(), 1);
728        assert_eq!(sched.total_utilization(), 200); // only 20% remains
729    }
730
731    #[cfg(feature = "alloc")]
732    #[test]
733    fn test_remove_task_not_found() {
734        let mut sched = make_scheduler();
735        let result = sched.remove_task(ProcessId(99));
736        assert!(result.is_err());
737    }
738
739    #[cfg(feature = "alloc")]
740    #[test]
741    fn test_remove_then_readmit() {
742        let mut sched = make_scheduler();
743        // Fill to 100%
744        let _ = sched.add_task(ProcessId(1), 10_000_000, 10_000_000, 10_000_000, 0);
745        assert_eq!(sched.total_utilization(), 1000);
746
747        // Remove frees bandwidth
748        let _ = sched.remove_task(ProcessId(1));
749        assert_eq!(sched.total_utilization(), 0);
750
751        // Can add a new task now
752        assert!(sched
753            .add_task(ProcessId(2), 5_000_000, 10_000_000, 10_000_000, 0)
754            .is_ok());
755    }
756
757    #[cfg(feature = "alloc")]
758    #[test]
759    fn test_pick_next_earliest_deadline() {
760        let mut sched = make_scheduler();
761        // Task 1: deadline_abs = 0 + 20M = 20M
762        let _ = sched.add_task(ProcessId(1), 1_000_000, 20_000_000, 30_000_000, 0);
763        // Task 2: deadline_abs = 0 + 10M = 10M (earlier)
764        let _ = sched.add_task(ProcessId(2), 1_000_000, 10_000_000, 30_000_000, 0);
765        // Task 3: deadline_abs = 0 + 15M = 15M
766        let _ = sched.add_task(ProcessId(3), 1_000_000, 15_000_000, 30_000_000, 0);
767
768        let next = sched.pick_next();
769        assert_eq!(next, Some(ProcessId(2))); // Earliest deadline wins
770    }
771
772    #[cfg(feature = "alloc")]
773    #[test]
774    fn test_pick_next_skips_throttled() {
775        let mut sched = make_scheduler();
776        // Task 1: earlier deadline but will be throttled
777        let _ = sched.add_task(ProcessId(1), 1_000_000, 5_000_000, 10_000_000, 0);
778        // Task 2: later deadline
779        let _ = sched.add_task(ProcessId(2), 1_000_000, 8_000_000, 10_000_000, 0);
780
781        // Throttle task 1
782        if let Some(e) = sched.tasks.get_mut(&1) {
783            e.throttled = true;
784            e.runtime_remaining = 0;
785        }
786
787        let next = sched.pick_next();
788        assert_eq!(next, Some(ProcessId(2)));
789    }
790
791    #[cfg(feature = "alloc")]
792    #[test]
793    fn test_pick_next_none_when_all_throttled() {
794        let mut sched = make_scheduler();
795        let _ = sched.add_task(ProcessId(1), 1_000_000, 5_000_000, 10_000_000, 0);
796
797        if let Some(e) = sched.tasks.get_mut(&1) {
798            e.throttled = true;
799            e.runtime_remaining = 0;
800        }
801
802        assert_eq!(sched.pick_next(), None);
803    }
804
805    #[cfg(feature = "alloc")]
806    #[test]
807    fn test_pick_next_empty() {
808        let mut sched = make_scheduler();
809        assert_eq!(sched.pick_next(), None);
810    }
811
812    #[cfg(feature = "alloc")]
813    #[test]
814    fn test_tick_decrements_runtime() {
815        let mut sched = make_scheduler();
816        let _ = sched.add_task(ProcessId(1), 5_000_000, 10_000_000, 10_000_000, 0);
817        sched.current_pid = Some(1);
818
819        let throttled = sched.tick(1_000_000); // 1ms tick
820        assert!(!throttled);
821
822        let entity = sched.get_task(ProcessId(1)).unwrap();
823        assert_eq!(entity.runtime_remaining, 4_000_000);
824        assert!(!entity.throttled);
825    }
826
827    #[cfg(feature = "alloc")]
828    #[test]
829    fn test_tick_runtime_exhaustion() {
830        let mut sched = make_scheduler();
831        let _ = sched.add_task(ProcessId(1), 2_000_000, 10_000_000, 10_000_000, 0);
832        sched.current_pid = Some(1);
833
834        // Tick away all runtime
835        let throttled = sched.tick(2_000_000);
836        assert!(throttled);
837
838        let entity = sched.get_task(ProcessId(1)).unwrap();
839        assert_eq!(entity.runtime_remaining, 0);
840        assert!(entity.throttled);
841    }
842
843    #[cfg(feature = "alloc")]
844    #[test]
845    fn test_tick_over_exhaustion() {
846        let mut sched = make_scheduler();
847        let _ = sched.add_task(ProcessId(1), 1_000_000, 10_000_000, 10_000_000, 0);
848        sched.current_pid = Some(1);
849
850        // Tick more than runtime remaining
851        let throttled = sched.tick(5_000_000);
852        assert!(throttled);
853
854        let entity = sched.get_task(ProcessId(1)).unwrap();
855        assert_eq!(entity.runtime_remaining, 0);
856        assert!(entity.throttled);
857    }
858
859    #[cfg(feature = "alloc")]
860    #[test]
861    fn test_tick_no_current() {
862        let mut sched = make_scheduler();
863        let throttled = sched.tick(1_000_000);
864        assert!(!throttled);
865    }
866
867    #[cfg(feature = "alloc")]
868    #[test]
869    fn test_replenish_at_period_boundary() {
870        let mut sched = make_scheduler();
871        let _ = sched.add_task(ProcessId(1), 2_000_000, 5_000_000, 10_000_000, 0);
872        sched.current_pid = Some(1);
873
874        // Exhaust runtime
875        sched.tick(2_000_000);
876        assert!(sched.get_task(ProcessId(1)).unwrap().throttled);
877
878        // Replenish at period boundary (10ms)
879        let count = sched.replenish(10_000_000);
880        assert_eq!(count, 1);
881
882        let entity = sched.get_task(ProcessId(1)).unwrap();
883        assert_eq!(entity.runtime_remaining, 2_000_000);
884        assert!(!entity.throttled);
885        assert_eq!(entity.period_start, 10_000_000);
886    }
887
888    #[cfg(feature = "alloc")]
889    #[test]
890    fn test_replenish_not_yet_due() {
891        let mut sched = make_scheduler();
892        let _ = sched.add_task(ProcessId(1), 2_000_000, 5_000_000, 10_000_000, 0);
893
894        // Too early for replenishment
895        let count = sched.replenish(5_000_000);
896        assert_eq!(count, 0);
897    }
898
899    #[cfg(feature = "alloc")]
900    #[test]
901    fn test_next_deadline_event() {
902        let mut sched = make_scheduler();
903        // period=10ms, deadline=5ms
904        let _ = sched.add_task(ProcessId(1), 2_000_000, 5_000_000, 10_000_000, 0);
905
906        let event = sched.next_deadline_event(0);
907        assert!(event.is_some());
908        // Should be min of: period_end(10M), deadline_abs(5M)
909        // = 5M (earliest deadline)
910        assert_eq!(event.unwrap(), 5_000_000);
911    }
912
913    #[cfg(feature = "alloc")]
914    #[test]
915    fn test_next_deadline_event_with_current_runtime() {
916        let mut sched = make_scheduler();
917        // period=10ms, deadline=5ms, runtime=1ms
918        let _ = sched.add_task(ProcessId(1), 1_000_000, 5_000_000, 10_000_000, 0);
919        sched.current_pid = Some(1);
920
921        let event = sched.next_deadline_event(0);
922        assert!(event.is_some());
923        // min(period_end=10M, deadline_abs=5M, runtime_remaining=1M) = 1M
924        assert_eq!(event.unwrap(), 1_000_000);
925    }
926
927    #[cfg(feature = "alloc")]
928    #[test]
929    fn test_next_deadline_event_empty() {
930        let sched = make_scheduler();
931        assert_eq!(sched.next_deadline_event(0), None);
932    }
933
934    #[cfg(feature = "alloc")]
935    #[test]
936    fn test_should_preempt_cfs() {
937        let mut sched = make_scheduler();
938        assert!(!sched.should_preempt_cfs());
939
940        let _ = sched.add_task(ProcessId(1), 1_000_000, 5_000_000, 10_000_000, 0);
941        assert!(sched.should_preempt_cfs());
942
943        // Throttle the task
944        if let Some(e) = sched.tasks.get_mut(&1) {
945            e.throttled = true;
946        }
947        assert!(!sched.should_preempt_cfs());
948    }
949
950    #[cfg(feature = "alloc")]
951    #[test]
952    fn test_full_lifecycle() {
953        let mut sched = make_scheduler();
954
955        // Add two tasks: T1 (20%, deadline 5ms) and T2 (30%, deadline 8ms)
956        let _ = sched.add_task(ProcessId(1), 2_000_000, 5_000_000, 10_000_000, 0);
957        let _ = sched.add_task(ProcessId(2), 3_000_000, 8_000_000, 10_000_000, 0);
958        assert_eq!(sched.total_utilization(), 500); // 50%
959
960        // Pick next: T1 has earlier deadline (5ms vs 8ms)
961        assert_eq!(sched.pick_next(), Some(ProcessId(1)));
962
963        // T1 runs for 2ms -> exhausted
964        assert!(sched.tick(2_000_000));
965
966        // Pick next: T1 throttled, T2 runs
967        assert_eq!(sched.pick_next(), Some(ProcessId(2)));
968
969        // T2 runs for 3ms -> exhausted
970        assert!(sched.tick(3_000_000));
971
972        // All throttled
973        assert_eq!(sched.pick_next(), None);
974        assert!(!sched.should_preempt_cfs());
975
976        // Replenish at period boundary (10ms)
977        let count = sched.replenish(10_000_000);
978        assert_eq!(count, 2);
979        assert!(sched.should_preempt_cfs());
980
981        // Both tasks available again; pick earliest deadline
982        let next = sched.pick_next();
983        assert!(next.is_some());
984    }
985
986    #[cfg(feature = "alloc")]
987    #[test]
988    fn test_recalculate_utilization() {
989        let mut sched = make_scheduler();
990        let _ = sched.add_task(ProcessId(1), 3_000_000, 10_000_000, 10_000_000, 0);
991        let _ = sched.add_task(ProcessId(2), 2_000_000, 10_000_000, 10_000_000, 0);
992
993        // Manually corrupt utilization
994        sched.total_utilization = 999;
995        sched.recalculate_utilization();
996        assert_eq!(sched.total_utilization, 500); // 30% + 20%
997    }
998}