Skip to main content

scx_mlfq/
config.rs

1// SPDX-License-Identifier: GPL-2.0
2//
3// Copyright (c) 2026 Galih Tama <galpt@v.recipes>
4//
5// This software may be used and distributed according to the terms of the GNU
6// General Public License version 2.
7
8//! Userspace configuration, validated scheduling constants written into
9//! BPF rodata.
10//!
11//! `Config` is the single validated set of scheduling constants; the BPF side
12//! reads them from `const volatile` rodata globals declared in
13//! `src/bpf/main.bpf.c` (see `src/bpf/intf.h` for the compile-time
14//! defaults, which are the source of truth for every value here).
15//!
16//! The scheduler is knob-free. The production path uses `Config::default()`
17//! validated by `Config::validate()`. `ConfigBuilder` exists only to drive
18//! the validation contract from the unit tests.
19
20use anyhow::bail;
21use anyhow::Context;
22use anyhow::Result;
23
24/// Time units matching `enum mlfq_consts` in `src/bpf/intf.h`.
25const NSEC_PER_USEC: u64 = 1_000;
26const NSEC_PER_MSEC: u64 = 1_000_000;
27const NSEC_PER_SEC: u64 = 1_000_000_000;
28
29/*
30 * Defaults must match `enum mlfq_consts` in `src/bpf/intf.h`; the BPF
31 * compile-time values are the contract. The defaults are therefore derived
32 * from the bindgen-generated constants (the same source `topology.rs`
33 * cross-checks its constants against), so an intf.h change propagates here
34 * automatically and the `defaults_match_intf_h` test pins the binding.
35 * SHORT_SLEEP_NS is the one explicit value and the test pins it to the
36 * intf.h constant.
37 */
38
39/// Per-queue request sizes.
40const Q1_SLICE_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_Q1_SLICE_NS as u64;
41const Q2_SLICE_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_Q2_SLICE_NS as u64;
42const Q3_SLICE_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_Q3_SLICE_NS as u64;
43
44/// EMA gauge ceiling.
45const BUDGET_MAX_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_BUDGET_MAX_NS as u64;
46
47/// Climb aggressiveness, fixed.
48const ALPHA: u64 = crate::bpf_intf::mlfq_consts_MLFQ_ALPHA as u64;
49
50/// Classification thresholds.
51const T_L_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_T_L_NS as u64;
52const T_H_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_T_H_NS as u64;
53
54/// EMA decay half-life.
55const EMA_HALF_LIFE_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_EMA_HALF_LIFE_NS as u64;
56
57/// Aging period.
58const AGING_PERIOD_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_AGING_PERIOD_NS as u64;
59
60/// Short-sleep boost window. Periodic wakeup cadences such as the 60 Hz
61/// frame interval stay interactive. The per-task boost rate limit bounds
62/// the churn. The value is set against the slowest common cadence, so
63/// faster refresh rates, which sleep less per frame, fall inside the
64/// window as well.
65const SHORT_SLEEP_NS: u64 = 32 * NSEC_PER_MSEC;
66const SHORT_SLEEP_RATE_LIMIT_NS: u64 =
67    crate::bpf_intf::mlfq_consts_MLFQ_SHORT_SLEEP_RATE_LIMIT_NS as u64;
68const HYSTERESIS_SLEEP_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_HYSTERESIS_SLEEP_NS as u64;
69
70/// A sleep longer than this collapses the gauge.
71const LONG_SLEEP_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_LONG_SLEEP_NS as u64;
72
73/// Minimum residency before a same-queue wakeup may preempt the running
74/// task. Zero, the default, makes the interactive rule unconditional.
75const SAMEQ_PREEMPT_MIN_RUN_NS: u64 =
76    crate::bpf_intf::mlfq_consts_MLFQ_SAMEQ_PREEMPT_MIN_RUN_NS as u64;
77
78/// Slice cap for a preempting wakeup, in nsecs. The displaced task
79/// resumes at the next scheduling event once the cap expires.
80const PREEMPT_SLICE_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_PREEMPT_SLICE_NS as u64;
81
82/// Dispatch quotas.
83const Q1_QUOTA: u32 = crate::bpf_intf::mlfq_consts_MLFQ_Q1_QUOTA;
84const Q2_QUOTA: u32 = crate::bpf_intf::mlfq_consts_MLFQ_Q2_QUOTA;
85const DISPATCH_MAX_BATCH: u32 = crate::bpf_intf::mlfq_consts_MLFQ_DISPATCH_MAX_BATCH;
86
87/// Drain interval of the realtime-takeover evacuation, nsecs.
88const RTDL_DRAIN_INTERVAL_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_RTDL_DRAIN_INTERVAL_NS as u64;
89
90/// Tree band edges, the rodata bases of the effective adaptation values.
91const TREE_T_INT_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_TREE_T_INT_NS as u64;
92const TREE_T_BOUND_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_TREE_T_BOUND_NS as u64;
93
94/// Validated scheduling constants.
95///
96/// Every field maps to a `const volatile` rodata global in
97/// `src/bpf/main.bpf.c`; field names match the BPF globals 1:1 so
98/// `Config::apply()` is a mechanical write-through.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Config {
101    /// Q1 request size (interactive), nsecs.
102    pub q1_slice_ns: u64,
103    /// Q2 request size (default), nsecs.
104    pub q2_slice_ns: u64,
105    /// Q3 request size (batch), nsecs.
106    pub q3_slice_ns: u64,
107    /// EMA gauge ceiling, nsecs.
108    pub budget_max_ns: u64,
109    /// EMA climb aggressiveness (fixed).
110    pub alpha: u64,
111    /// Interactive threshold T_L, nsecs.
112    pub t_l_ns: u64,
113    /// CPU-bound threshold T_H, nsecs.
114    pub t_h_ns: u64,
115    /// EMA decay half-life, nsecs.
116    pub ema_half_life_ns: u64,
117    /// Global aging period, nsecs.
118    pub aging_period_ns: u64,
119    /// Short-sleep boost window, nsecs.
120    pub short_sleep_ns: u64,
121    /// Per-task short-sleep boost rate limit, nsecs.
122    pub short_sleep_rate_limit_ns: u64,
123    /// Sleep counted as "short" for the wake_cnt hysteresis, nsecs.
124    pub hysteresis_sleep_ns: u64,
125    /// Sleep beyond which the gauge collapses, nsecs.
126    pub long_sleep_ns: u64,
127    /// Minimum residency before a same-queue wakeup may preempt, nsecs.
128    /// Zero, the default, makes the interactive rule unconditional.
129    pub sameq_preempt_min_run_ns: u64,
130    /// Slice cap for a preempting wakeup, nsecs.
131    pub preempt_slice_ns: u64,
132    /// Q1 dispatch quota per dispatch() call.
133    pub q1_quota: u32,
134    /// Q2 dispatch quota per dispatch() call.
135    pub q2_quota: u32,
136    /// Dispatch-loop bound.
137    pub dispatch_max_batch: u32,
138    /// Drain interval of the realtime-takeover evacuation, nsecs.
139    pub rtdl_drain_interval_ns: u64,
140    /// Tree Q1/Q2 band edge, the base of the effective value, nsecs.
141    pub tree_t_int_ns: u64,
142    /// Tree Q2/Q3 band edge, the base of the effective value, nsecs.
143    pub tree_t_bound_ns: u64,
144    /// Master gate of the threshold adaptation (false = fixed thresholds).
145    pub adapt_enabled: bool,
146}
147
148impl Default for Config {
149    /// Compile-time defaults from `src/bpf/intf.h`.
150    fn default() -> Self {
151        Self {
152            q1_slice_ns: Q1_SLICE_NS,
153            q2_slice_ns: Q2_SLICE_NS,
154            q3_slice_ns: Q3_SLICE_NS,
155            budget_max_ns: BUDGET_MAX_NS,
156            alpha: ALPHA,
157            t_l_ns: T_L_NS,
158            t_h_ns: T_H_NS,
159            ema_half_life_ns: EMA_HALF_LIFE_NS,
160            aging_period_ns: AGING_PERIOD_NS,
161            short_sleep_ns: SHORT_SLEEP_NS,
162            short_sleep_rate_limit_ns: SHORT_SLEEP_RATE_LIMIT_NS,
163            hysteresis_sleep_ns: HYSTERESIS_SLEEP_NS,
164            long_sleep_ns: LONG_SLEEP_NS,
165            sameq_preempt_min_run_ns: SAMEQ_PREEMPT_MIN_RUN_NS,
166            preempt_slice_ns: PREEMPT_SLICE_NS,
167            q1_quota: Q1_QUOTA,
168            q2_quota: Q2_QUOTA,
169            dispatch_max_batch: DISPATCH_MAX_BATCH,
170            rtdl_drain_interval_ns: RTDL_DRAIN_INTERVAL_NS,
171            tree_t_int_ns: TREE_T_INT_NS,
172            tree_t_bound_ns: TREE_T_BOUND_NS,
173            adapt_enabled: true,
174        }
175    }
176}
177
178impl Config {
179    /// Validate the configuration against the invariants the BPF side
180    /// relies on (`src/bpf/intf.h`).
181    ///
182    /// An invalid configuration is a programming error, not a runtime
183    /// condition: the production path validates `Config::default()` before
184    /// any value is written into rodata, and the unit tests drive the same
185    /// contract through `ConfigBuilder::build()`.
186    pub fn validate(&self) -> Result<()> {
187        if self.q1_slice_ns == 0 || self.q2_slice_ns == 0 || self.q3_slice_ns == 0 {
188            bail!(
189                "queue slices must be non-zero (got Q1={} Q2={} Q3={})",
190                self.q1_slice_ns,
191                self.q2_slice_ns,
192                self.q3_slice_ns
193            );
194        }
195        if self.budget_max_ns == 0 {
196            bail!("budget_max must be non-zero");
197        }
198        if self.alpha == 0 {
199            bail!("alpha must be non-zero");
200        }
201        if self.t_l_ns == 0 {
202            bail!("T_L must be non-zero");
203        }
204        if self.t_l_ns >= self.t_h_ns {
205            bail!(
206                "T_L ({}) must be strictly below T_H ({})",
207                self.t_l_ns,
208                self.t_h_ns
209            );
210        }
211        if self.t_h_ns >= self.budget_max_ns {
212            bail!(
213                "T_H ({}) must be strictly below BUDGET_MAX ({})",
214                self.t_h_ns,
215                self.budget_max_ns
216            );
217        }
218        if self.ema_half_life_ns == 0 {
219            bail!("EMA half-life must be non-zero");
220        }
221        if self.aging_period_ns == 0 {
222            bail!("aging period must be non-zero");
223        }
224        if self.short_sleep_ns == 0 {
225            bail!("short-sleep window must be non-zero");
226        }
227        if self.short_sleep_rate_limit_ns == 0 {
228            bail!("short-sleep rate limit must be non-zero");
229        }
230        if self.hysteresis_sleep_ns == 0 {
231            bail!("hysteresis sleep window must be non-zero");
232        }
233        if self.long_sleep_ns == 0 {
234            bail!("long-sleep window must be non-zero");
235        }
236        if self.q1_quota == 0 || self.q2_quota == 0 {
237            bail!(
238                "dispatch quotas must be non-zero (got Q1={} Q2={})",
239                self.q1_quota,
240                self.q2_quota
241            );
242        }
243        if self.dispatch_max_batch == 0 {
244            bail!("dispatch_max_batch must be non-zero");
245        }
246        /*
247         * dispatch() serves Q1 up to its quota, then Q2 up to its quota,
248         * then the Q3 remainder within dispatch_max_batch.
249         * If the quotas consume the whole batch, Q3 never runs on a busy
250         * system. The Q3 starvation bound depends on this slack.
251         */
252        if u64::from(self.q1_quota) + u64::from(self.q2_quota) >= u64::from(self.dispatch_max_batch)
253        {
254            bail!(
255                "Q1+Q2 quotas ({}+{}) must leave headroom for Q3 within the dispatch batch ({})",
256                self.q1_quota,
257                self.q2_quota,
258                self.dispatch_max_batch
259            );
260        }
261        /*
262         * The kernel's per-dispatch() move budget is set from the ops table
263         * (.dispatch_max_batch = MLFQ_DISPATCH_MAX_BATCH in main.bpf.c).
264         * A userspace batch larger than that constant would make the BPF
265         * dispatch loops try to move more tasks than the kernel allows per
266         * call, so the rodata value must never exceed it.
267         */
268        if self.dispatch_max_batch > crate::bpf_intf::mlfq_consts_MLFQ_DISPATCH_MAX_BATCH {
269            bail!(
270                "dispatch_max_batch ({}) exceeds the ops-table bound ({})",
271                self.dispatch_max_batch,
272                crate::bpf_intf::mlfq_consts_MLFQ_DISPATCH_MAX_BATCH
273            );
274        }
275        if self.rtdl_drain_interval_ns == 0 {
276            bail!("rtdl drain interval must be non-zero");
277        }
278        if self.tree_t_int_ns == 0 {
279            bail!("tree T_INT must be non-zero");
280        }
281        if self.tree_t_int_ns >= self.tree_t_bound_ns {
282            bail!(
283                "tree T_INT ({}) must be strictly below T_BOUND ({})",
284                self.tree_t_int_ns,
285                self.tree_t_bound_ns
286            );
287        }
288        Ok(())
289    }
290
291    /// Write the validated constants into the BPF object's rodata section.
292    ///
293    /// Must be called on the opened, not-yet-loaded skeleton, before
294    /// `scx_ops_load!()`. The rodata section becomes read-only after load.
295    pub fn apply(&self, skel: &mut crate::bpf_skel::OpenBpfSkel<'_>) -> Result<()> {
296        let rodata = skel
297            .maps
298            .rodata_data
299            .as_mut()
300            .context("rodata missing, the BPF object has no .rodata section")?;
301        rodata.mlfq_q1_slice_ns = self.q1_slice_ns;
302        rodata.mlfq_q2_slice_ns = self.q2_slice_ns;
303        rodata.mlfq_q3_slice_ns = self.q3_slice_ns;
304        rodata.mlfq_budget_max_ns = self.budget_max_ns;
305        rodata.mlfq_alpha = self.alpha;
306        rodata.mlfq_t_l_ns = self.t_l_ns;
307        rodata.mlfq_t_h_ns = self.t_h_ns;
308        rodata.mlfq_ema_half_life_ns = self.ema_half_life_ns;
309        rodata.mlfq_aging_period_ns = self.aging_period_ns;
310        rodata.mlfq_short_sleep_ns = self.short_sleep_ns;
311        rodata.mlfq_short_sleep_rate_limit_ns = self.short_sleep_rate_limit_ns;
312        rodata.mlfq_hysteresis_sleep_ns = self.hysteresis_sleep_ns;
313        rodata.mlfq_long_sleep_ns = self.long_sleep_ns;
314        rodata.mlfq_sameq_preempt_min_run_ns = self.sameq_preempt_min_run_ns;
315        rodata.mlfq_preempt_slice_ns = self.preempt_slice_ns;
316        rodata.mlfq_q1_quota = self.q1_quota;
317        rodata.mlfq_q2_quota = self.q2_quota;
318        rodata.mlfq_dispatch_max_batch = self.dispatch_max_batch;
319        rodata.mlfq_rtdl_drain_interval_ns = self.rtdl_drain_interval_ns;
320        rodata.mlfq_tree_t_int_ns = self.tree_t_int_ns;
321        rodata.mlfq_tree_t_bound_ns = self.tree_t_bound_ns;
322        rodata.mlfq_adapt_enabled = self.adapt_enabled;
323        Ok(())
324    }
325
326    /// One-line summary of the applied constants for the startup log.
327    pub fn describe(&self) -> String {
328        format!(
329            "slices: Q1={}us Q2={}us Q3={}us, T_L={}us, T_H={}us, \
330             budget_max={}us, alpha={}, ema_half_life={}us, aging_period={}s, \
331             short_sleep={}us, ss_rate_limit={}us, hysteresis_sleep={}us, \
332             long_sleep={}ms, sameq_min_run={}us, preempt_slice={}us, \
333             rtdl_drain_interval={}us, quotas: Q1={} Q2={} max_batch={}, \
334             tree_bands: T_INT={}us T_BOUND={}us, adapt_enabled={}",
335            self.q1_slice_ns / NSEC_PER_USEC,
336            self.q2_slice_ns / NSEC_PER_USEC,
337            self.q3_slice_ns / NSEC_PER_USEC,
338            self.t_l_ns / NSEC_PER_USEC,
339            self.t_h_ns / NSEC_PER_USEC,
340            self.budget_max_ns / NSEC_PER_USEC,
341            self.alpha,
342            self.ema_half_life_ns / NSEC_PER_USEC,
343            self.aging_period_ns / NSEC_PER_SEC,
344            self.short_sleep_ns / NSEC_PER_USEC,
345            self.short_sleep_rate_limit_ns / NSEC_PER_USEC,
346            self.hysteresis_sleep_ns / NSEC_PER_USEC,
347            self.long_sleep_ns / NSEC_PER_MSEC,
348            self.sameq_preempt_min_run_ns / NSEC_PER_USEC,
349            self.preempt_slice_ns / NSEC_PER_USEC,
350            self.rtdl_drain_interval_ns / NSEC_PER_USEC,
351            self.q1_quota,
352            self.q2_quota,
353            self.dispatch_max_batch,
354            self.tree_t_int_ns / NSEC_PER_USEC,
355            self.tree_t_bound_ns / NSEC_PER_USEC,
356            self.adapt_enabled,
357        )
358    }
359}
360
361/// Builder for `Config`, assembled from optional setters.
362///
363/// Every setter is optional; unset fields fall back to the `intf.h`
364/// defaults. `build()` validates the result and returns an error for any
365/// configuration that would break a BPF invariant.
366///
367/// Production code never uses this type: the scheduler is deliberately
368/// knob-free, so `main.rs` applies `Config::default()` directly. The
369/// builder lives under `#[cfg(test)]` and exercises every field through
370/// the validation contract.
371#[cfg(test)]
372#[derive(Debug, Clone, Default)]
373pub struct ConfigBuilder {
374    q1_slice_ns: Option<u64>,
375    q2_slice_ns: Option<u64>,
376    q3_slice_ns: Option<u64>,
377    budget_max_ns: Option<u64>,
378    alpha: Option<u64>,
379    t_l_ns: Option<u64>,
380    t_h_ns: Option<u64>,
381    ema_half_life_ns: Option<u64>,
382    aging_period_ns: Option<u64>,
383    short_sleep_ns: Option<u64>,
384    short_sleep_rate_limit_ns: Option<u64>,
385    hysteresis_sleep_ns: Option<u64>,
386    long_sleep_ns: Option<u64>,
387    sameq_preempt_min_run_ns: Option<u64>,
388    preempt_slice_ns: Option<u64>,
389    q1_quota: Option<u32>,
390    q2_quota: Option<u32>,
391    dispatch_max_batch: Option<u32>,
392    rtdl_drain_interval_ns: Option<u64>,
393    tree_t_int_ns: Option<u64>,
394    tree_t_bound_ns: Option<u64>,
395    adapt_enabled: Option<bool>,
396}
397
398/*
399 * The setters assemble a Config from the intf.h defaults and run it
400 * through build()'s validation. Test-only. The production path always
401 * uses Config::default().
402 */
403#[cfg(test)]
404impl ConfigBuilder {
405    /// Set the Q1 (interactive) request size in nsecs.
406    pub fn q1_slice_ns(mut self, v: u64) -> Self {
407        self.q1_slice_ns = Some(v);
408        self
409    }
410
411    /// Set the Q2 (default) request size in nsecs.
412    pub fn q2_slice_ns(mut self, v: u64) -> Self {
413        self.q2_slice_ns = Some(v);
414        self
415    }
416
417    /// Set the Q3 (batch) request size in nsecs.
418    pub fn q3_slice_ns(mut self, v: u64) -> Self {
419        self.q3_slice_ns = Some(v);
420        self
421    }
422
423    /// Set the EMA gauge ceiling in nsecs.
424    pub fn budget_max_ns(mut self, v: u64) -> Self {
425        self.budget_max_ns = Some(v);
426        self
427    }
428
429    /// Set the EMA climb aggressiveness.
430    pub fn alpha(mut self, v: u64) -> Self {
431        self.alpha = Some(v);
432        self
433    }
434
435    /// Set the interactive threshold T_L in nsecs.
436    pub fn t_l_ns(mut self, v: u64) -> Self {
437        self.t_l_ns = Some(v);
438        self
439    }
440
441    /// Set the CPU-bound threshold T_H in nsecs.
442    pub fn t_h_ns(mut self, v: u64) -> Self {
443        self.t_h_ns = Some(v);
444        self
445    }
446
447    /// Set the EMA decay half-life in nsecs.
448    pub fn ema_half_life_ns(mut self, v: u64) -> Self {
449        self.ema_half_life_ns = Some(v);
450        self
451    }
452
453    /// Set the global aging period in nsecs.
454    pub fn aging_period_ns(mut self, v: u64) -> Self {
455        self.aging_period_ns = Some(v);
456        self
457    }
458
459    /// Set the short-sleep boost window in nsecs.
460    pub fn short_sleep_ns(mut self, v: u64) -> Self {
461        self.short_sleep_ns = Some(v);
462        self
463    }
464
465    /// Set the per-task short-sleep boost rate limit in nsecs.
466    pub fn short_sleep_rate_limit_ns(mut self, v: u64) -> Self {
467        self.short_sleep_rate_limit_ns = Some(v);
468        self
469    }
470
471    /// Set the sleep counted as "short" for the wake_cnt hysteresis in nsecs.
472    pub fn hysteresis_sleep_ns(mut self, v: u64) -> Self {
473        self.hysteresis_sleep_ns = Some(v);
474        self
475    }
476
477    /// Set the long-sleep gauge-collapse window in nsecs.
478    pub fn long_sleep_ns(mut self, v: u64) -> Self {
479        self.long_sleep_ns = Some(v);
480        self
481    }
482
483    /// Set the same-queue preemption minimum residency in nsecs.
484    pub fn sameq_preempt_min_run_ns(mut self, v: u64) -> Self {
485        self.sameq_preempt_min_run_ns = Some(v);
486        self
487    }
488
489    /// Set the preempting-wakeup slice cap in nsecs.
490    pub fn preempt_slice_ns(mut self, v: u64) -> Self {
491        self.preempt_slice_ns = Some(v);
492        self
493    }
494
495    /// Set the Q1 dispatch quota.
496    pub fn q1_quota(mut self, v: u32) -> Self {
497        self.q1_quota = Some(v);
498        self
499    }
500
501    /// Set the Q2 dispatch quota.
502    pub fn q2_quota(mut self, v: u32) -> Self {
503        self.q2_quota = Some(v);
504        self
505    }
506
507    /// Set the dispatch-loop bound.
508    pub fn dispatch_max_batch(mut self, v: u32) -> Self {
509        self.dispatch_max_batch = Some(v);
510        self
511    }
512
513    /// Set the realtime-takeover drain interval in nsecs.
514    pub fn rtdl_drain_interval_ns(mut self, v: u64) -> Self {
515        self.rtdl_drain_interval_ns = Some(v);
516        self
517    }
518
519    /// Set the tree Q1/Q2 band edge (the base of the effective value).
520    pub fn tree_t_int_ns(mut self, v: u64) -> Self {
521        self.tree_t_int_ns = Some(v);
522        self
523    }
524
525    /// Set the tree Q2/Q3 band edge (the base of the effective value).
526    pub fn tree_t_bound_ns(mut self, v: u64) -> Self {
527        self.tree_t_bound_ns = Some(v);
528        self
529    }
530
531    /// Set the threshold-adaptation master gate.
532    pub fn adapt_enabled(mut self, v: bool) -> Self {
533        self.adapt_enabled = Some(v);
534        self
535    }
536
537    /// Assemble and validate the configuration.
538    ///
539    /// Returns an error if any invariant is violated; the resulting
540    /// `Config` is guaranteed valid.
541    pub fn build(self) -> Result<Config> {
542        let defaults = Config::default();
543        let cfg = Config {
544            q1_slice_ns: self.q1_slice_ns.unwrap_or(defaults.q1_slice_ns),
545            q2_slice_ns: self.q2_slice_ns.unwrap_or(defaults.q2_slice_ns),
546            q3_slice_ns: self.q3_slice_ns.unwrap_or(defaults.q3_slice_ns),
547            budget_max_ns: self.budget_max_ns.unwrap_or(defaults.budget_max_ns),
548            alpha: self.alpha.unwrap_or(defaults.alpha),
549            t_l_ns: self.t_l_ns.unwrap_or(defaults.t_l_ns),
550            t_h_ns: self.t_h_ns.unwrap_or(defaults.t_h_ns),
551            ema_half_life_ns: self.ema_half_life_ns.unwrap_or(defaults.ema_half_life_ns),
552            aging_period_ns: self.aging_period_ns.unwrap_or(defaults.aging_period_ns),
553            short_sleep_ns: self.short_sleep_ns.unwrap_or(defaults.short_sleep_ns),
554            short_sleep_rate_limit_ns: self
555                .short_sleep_rate_limit_ns
556                .unwrap_or(defaults.short_sleep_rate_limit_ns),
557            hysteresis_sleep_ns: self
558                .hysteresis_sleep_ns
559                .unwrap_or(defaults.hysteresis_sleep_ns),
560            long_sleep_ns: self.long_sleep_ns.unwrap_or(defaults.long_sleep_ns),
561            sameq_preempt_min_run_ns: self
562                .sameq_preempt_min_run_ns
563                .unwrap_or(defaults.sameq_preempt_min_run_ns),
564            preempt_slice_ns: self.preempt_slice_ns.unwrap_or(defaults.preempt_slice_ns),
565            q1_quota: self.q1_quota.unwrap_or(defaults.q1_quota),
566            q2_quota: self.q2_quota.unwrap_or(defaults.q2_quota),
567            dispatch_max_batch: self
568                .dispatch_max_batch
569                .unwrap_or(defaults.dispatch_max_batch),
570            rtdl_drain_interval_ns: self
571                .rtdl_drain_interval_ns
572                .unwrap_or(defaults.rtdl_drain_interval_ns),
573            tree_t_int_ns: self.tree_t_int_ns.unwrap_or(defaults.tree_t_int_ns),
574            tree_t_bound_ns: self.tree_t_bound_ns.unwrap_or(defaults.tree_t_bound_ns),
575            adapt_enabled: self.adapt_enabled.unwrap_or(defaults.adapt_enabled),
576        };
577        cfg.validate()?;
578        Ok(cfg)
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585
586    #[test]
587    fn defaults_match_intf_h() {
588        /*
589         * Cross-check the Config defaults against the bindgen-generated
590         * enum mlfq_consts constants, the way topology.rs cross-checks its
591         * constants: intf.h is the single source of truth, and this test
592         * pins the binding so a default that diverges from the BPF side
593         * (or a constant that stops existing) fails here.
594         */
595        use crate::bpf_intf::{
596            mlfq_consts_MLFQ_AGING_PERIOD_NS, mlfq_consts_MLFQ_ALPHA,
597            mlfq_consts_MLFQ_BUDGET_MAX_NS, mlfq_consts_MLFQ_DISPATCH_MAX_BATCH,
598            mlfq_consts_MLFQ_EMA_HALF_LIFE_NS, mlfq_consts_MLFQ_HYSTERESIS_SLEEP_NS,
599            mlfq_consts_MLFQ_LONG_SLEEP_NS, mlfq_consts_MLFQ_PREEMPT_SLICE_NS,
600            mlfq_consts_MLFQ_Q1_QUOTA, mlfq_consts_MLFQ_Q1_SLICE_NS, mlfq_consts_MLFQ_Q2_QUOTA,
601            mlfq_consts_MLFQ_Q2_SLICE_NS, mlfq_consts_MLFQ_Q3_SLICE_NS,
602            mlfq_consts_MLFQ_RTDL_DRAIN_INTERVAL_NS, mlfq_consts_MLFQ_SAMEQ_PREEMPT_MIN_RUN_NS,
603            mlfq_consts_MLFQ_SHORT_SLEEP_NS, mlfq_consts_MLFQ_SHORT_SLEEP_RATE_LIMIT_NS,
604            mlfq_consts_MLFQ_TREE_T_BOUND_NS, mlfq_consts_MLFQ_TREE_T_INT_NS,
605            mlfq_consts_MLFQ_T_H_NS, mlfq_consts_MLFQ_T_L_NS,
606        };
607
608        let cfg = Config::default();
609        cfg.validate().unwrap();
610        assert_eq!(cfg.q1_slice_ns, mlfq_consts_MLFQ_Q1_SLICE_NS as u64);
611        assert_eq!(cfg.q2_slice_ns, mlfq_consts_MLFQ_Q2_SLICE_NS as u64);
612        assert_eq!(cfg.q3_slice_ns, mlfq_consts_MLFQ_Q3_SLICE_NS as u64);
613        assert_eq!(cfg.budget_max_ns, mlfq_consts_MLFQ_BUDGET_MAX_NS as u64);
614        assert_eq!(cfg.alpha, mlfq_consts_MLFQ_ALPHA as u64);
615        assert_eq!(cfg.t_l_ns, mlfq_consts_MLFQ_T_L_NS as u64);
616        assert_eq!(cfg.t_h_ns, mlfq_consts_MLFQ_T_H_NS as u64);
617        assert_eq!(
618            cfg.ema_half_life_ns,
619            mlfq_consts_MLFQ_EMA_HALF_LIFE_NS as u64
620        );
621        assert_eq!(cfg.aging_period_ns, mlfq_consts_MLFQ_AGING_PERIOD_NS as u64);
622        assert_eq!(cfg.short_sleep_ns, mlfq_consts_MLFQ_SHORT_SLEEP_NS as u64);
623        assert_eq!(
624            cfg.short_sleep_rate_limit_ns,
625            mlfq_consts_MLFQ_SHORT_SLEEP_RATE_LIMIT_NS as u64
626        );
627        assert_eq!(
628            cfg.hysteresis_sleep_ns,
629            mlfq_consts_MLFQ_HYSTERESIS_SLEEP_NS as u64
630        );
631        assert_eq!(cfg.long_sleep_ns, mlfq_consts_MLFQ_LONG_SLEEP_NS as u64);
632        assert_eq!(
633            cfg.sameq_preempt_min_run_ns,
634            mlfq_consts_MLFQ_SAMEQ_PREEMPT_MIN_RUN_NS as u64
635        );
636        assert_eq!(
637            cfg.preempt_slice_ns,
638            mlfq_consts_MLFQ_PREEMPT_SLICE_NS as u64
639        );
640        assert_eq!(cfg.q1_quota, mlfq_consts_MLFQ_Q1_QUOTA);
641        assert_eq!(cfg.q2_quota, mlfq_consts_MLFQ_Q2_QUOTA);
642        assert_eq!(cfg.dispatch_max_batch, mlfq_consts_MLFQ_DISPATCH_MAX_BATCH);
643        assert_eq!(
644            cfg.rtdl_drain_interval_ns,
645            mlfq_consts_MLFQ_RTDL_DRAIN_INTERVAL_NS as u64
646        );
647        assert_eq!(cfg.tree_t_int_ns, mlfq_consts_MLFQ_TREE_T_INT_NS as u64);
648        assert_eq!(cfg.tree_t_bound_ns, mlfq_consts_MLFQ_TREE_T_BOUND_NS as u64);
649        assert!(cfg.adapt_enabled, "the adaptation ships enabled");
650    }
651
652    #[test]
653    fn builder_defaults_equal_config_defaults() {
654        let cfg = ConfigBuilder::default().build().unwrap();
655        assert_eq!(cfg, Config::default());
656    }
657
658    #[test]
659    fn builder_overrides_individual_fields() {
660        let cfg = ConfigBuilder::default()
661            .q1_slice_ns(500_000)
662            .build()
663            .unwrap();
664        assert_eq!(cfg.q1_slice_ns, 500_000);
665        assert_eq!(cfg.q2_slice_ns, Config::default().q2_slice_ns);
666    }
667
668    #[test]
669    fn builder_overrides_sameq_min_run() {
670        let cfg = ConfigBuilder::default()
671            .sameq_preempt_min_run_ns(250_000)
672            .build()
673            .unwrap();
674        assert_eq!(cfg.sameq_preempt_min_run_ns, 250_000);
675        assert_eq!(cfg.q1_slice_ns, Config::default().q1_slice_ns);
676    }
677
678    #[test]
679    fn builder_overrides_preempt_slice() {
680        let cfg = ConfigBuilder::default()
681            .preempt_slice_ns(100_000)
682            .build()
683            .unwrap();
684        assert_eq!(cfg.preempt_slice_ns, 100_000);
685        assert_eq!(cfg.q1_slice_ns, Config::default().q1_slice_ns);
686    }
687
688    #[test]
689    fn rejects_zero_slices() {
690        assert!(ConfigBuilder::default().q1_slice_ns(0).build().is_err());
691        assert!(ConfigBuilder::default().q2_slice_ns(0).build().is_err());
692        assert!(ConfigBuilder::default().q3_slice_ns(0).build().is_err());
693    }
694
695    #[test]
696    fn rejects_t_l_at_or_above_t_h() {
697        assert!(ConfigBuilder::default().t_l_ns(2_000_000).build().is_err());
698        assert!(ConfigBuilder::default().t_l_ns(3_000_000).build().is_err());
699    }
700
701    #[test]
702    fn rejects_t_h_at_or_above_budget_max() {
703        assert!(ConfigBuilder::default().t_h_ns(6_000_000).build().is_err());
704        assert!(ConfigBuilder::default().t_h_ns(7_000_000).build().is_err());
705    }
706
707    #[test]
708    fn rejects_zero_quotas() {
709        assert!(ConfigBuilder::default().q1_quota(0).build().is_err());
710        assert!(ConfigBuilder::default().q2_quota(0).build().is_err());
711        assert!(ConfigBuilder::default()
712            .dispatch_max_batch(0)
713            .build()
714            .is_err());
715    }
716
717    #[test]
718    fn rejects_quotas_consuming_the_batch() {
719        // Q1+Q2 must leave headroom for Q3 within dispatch_max_batch.
720        let cfg = ConfigBuilder::default()
721            .q1_quota(16)
722            .q2_quota(16)
723            .dispatch_max_batch(32)
724            .build();
725        assert!(cfg.is_err());
726    }
727
728    #[test]
729    fn rejects_dispatch_batch_above_ops_bound() {
730        // The rodata batch must never exceed the ops-table bound
731        // (.dispatch_max_batch = MLFQ_DISPATCH_MAX_BATCH in main.bpf.c).
732        let cfg = ConfigBuilder::default()
733            .dispatch_max_batch(crate::bpf_intf::mlfq_consts_MLFQ_DISPATCH_MAX_BATCH + 1)
734            .build();
735        assert!(cfg.is_err());
736        // At the ops-table bound itself the config is valid.
737        let cfg = ConfigBuilder::default()
738            .dispatch_max_batch(crate::bpf_intf::mlfq_consts_MLFQ_DISPATCH_MAX_BATCH)
739            .build();
740        assert!(cfg.is_ok());
741    }
742
743    #[test]
744    fn rejects_zero_aging_period() {
745        assert!(ConfigBuilder::default().aging_period_ns(0).build().is_err());
746    }
747
748    #[test]
749    fn rejects_tree_bands_out_of_order() {
750        assert!(ConfigBuilder::default().tree_t_int_ns(0).build().is_err());
751        let cfg = ConfigBuilder::default()
752            .tree_t_int_ns(3_000_000)
753            .tree_t_bound_ns(3_000_000)
754            .build();
755        assert!(cfg.is_err());
756        let cfg = ConfigBuilder::default()
757            .tree_t_int_ns(4_000_000)
758            .tree_t_bound_ns(3_000_000)
759            .build();
760        assert!(cfg.is_err());
761        // The default band pair is valid.
762        assert!(ConfigBuilder::default().build().is_ok());
763    }
764
765    #[test]
766    fn adapt_gate_flag_round_trips() {
767        let cfg = ConfigBuilder::default()
768            .adapt_enabled(true)
769            .build()
770            .unwrap();
771        assert!(cfg.adapt_enabled);
772        let cfg = ConfigBuilder::default().build().unwrap();
773        assert!(cfg.adapt_enabled);
774    }
775
776    #[test]
777    fn describe_is_stable() {
778        let cfg = Config::default();
779        let s = cfg.describe();
780        assert!(s.contains("slices: Q1=1000us Q2=2000us Q3=4000us"));
781        assert!(s.contains("T_L=250us, T_H=2000us"));
782        assert!(s.contains("aging_period=1s"));
783        assert!(s.contains("rtdl_drain_interval=1000us"));
784        assert!(s.contains("quotas: Q1=4 Q2=8 max_batch=32"));
785        assert!(s.contains("tree_bands: T_INT=1000us T_BOUND=3000us"));
786        assert!(s.contains("adapt_enabled=true"));
787    }
788}