1use anyhow::Context;
21use anyhow::Result;
22use anyhow::bail;
23
24const NSEC_PER_USEC: u64 = 1_000;
26const NSEC_PER_MSEC: u64 = 1_000_000;
27const NSEC_PER_SEC: u64 = 1_000_000_000;
28
29const 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
44const BUDGET_MAX_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_BUDGET_MAX_NS as u64;
46
47const ALPHA: u64 = crate::bpf_intf::mlfq_consts_MLFQ_ALPHA as u64;
49
50const 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
54const EMA_HALF_LIFE_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_EMA_HALF_LIFE_NS as u64;
56
57const AGING_PERIOD_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_AGING_PERIOD_NS as u64;
59
60const 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
70const LONG_SLEEP_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_LONG_SLEEP_NS as u64;
72
73const SAMEQ_PREEMPT_MIN_RUN_NS: u64 =
76 crate::bpf_intf::mlfq_consts_MLFQ_SAMEQ_PREEMPT_MIN_RUN_NS as u64;
77
78const PREEMPT_SLICE_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_PREEMPT_SLICE_NS as u64;
81
82const 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
87const RTDL_DRAIN_INTERVAL_NS: u64 = crate::bpf_intf::mlfq_consts_MLFQ_RTDL_DRAIN_INTERVAL_NS as u64;
89
90const 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#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct Config {
101 pub q1_slice_ns: u64,
103 pub q2_slice_ns: u64,
105 pub q3_slice_ns: u64,
107 pub budget_max_ns: u64,
109 pub alpha: u64,
111 pub t_l_ns: u64,
113 pub t_h_ns: u64,
115 pub ema_half_life_ns: u64,
117 pub aging_period_ns: u64,
119 pub short_sleep_ns: u64,
121 pub short_sleep_rate_limit_ns: u64,
123 pub hysteresis_sleep_ns: u64,
125 pub long_sleep_ns: u64,
127 pub sameq_preempt_min_run_ns: u64,
130 pub preempt_slice_ns: u64,
132 pub q1_quota: u32,
134 pub q2_quota: u32,
136 pub dispatch_max_batch: u32,
138 pub rtdl_drain_interval_ns: u64,
140 pub tree_t_int_ns: u64,
142 pub tree_t_bound_ns: u64,
144 pub adapt_enabled: bool,
146}
147
148impl Default for Config {
149 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 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 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 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 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 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#[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#[cfg(test)]
404#[allow(dead_code)]
407impl ConfigBuilder {
408 pub fn q1_slice_ns(mut self, v: u64) -> Self {
410 self.q1_slice_ns = Some(v);
411 self
412 }
413
414 pub fn q2_slice_ns(mut self, v: u64) -> Self {
416 self.q2_slice_ns = Some(v);
417 self
418 }
419
420 pub fn q3_slice_ns(mut self, v: u64) -> Self {
422 self.q3_slice_ns = Some(v);
423 self
424 }
425
426 pub fn budget_max_ns(mut self, v: u64) -> Self {
428 self.budget_max_ns = Some(v);
429 self
430 }
431
432 pub fn alpha(mut self, v: u64) -> Self {
434 self.alpha = Some(v);
435 self
436 }
437
438 pub fn t_l_ns(mut self, v: u64) -> Self {
440 self.t_l_ns = Some(v);
441 self
442 }
443
444 pub fn t_h_ns(mut self, v: u64) -> Self {
446 self.t_h_ns = Some(v);
447 self
448 }
449
450 pub fn ema_half_life_ns(mut self, v: u64) -> Self {
452 self.ema_half_life_ns = Some(v);
453 self
454 }
455
456 pub fn aging_period_ns(mut self, v: u64) -> Self {
458 self.aging_period_ns = Some(v);
459 self
460 }
461
462 pub fn short_sleep_ns(mut self, v: u64) -> Self {
464 self.short_sleep_ns = Some(v);
465 self
466 }
467
468 pub fn short_sleep_rate_limit_ns(mut self, v: u64) -> Self {
470 self.short_sleep_rate_limit_ns = Some(v);
471 self
472 }
473
474 pub fn hysteresis_sleep_ns(mut self, v: u64) -> Self {
476 self.hysteresis_sleep_ns = Some(v);
477 self
478 }
479
480 pub fn long_sleep_ns(mut self, v: u64) -> Self {
482 self.long_sleep_ns = Some(v);
483 self
484 }
485
486 pub fn sameq_preempt_min_run_ns(mut self, v: u64) -> Self {
488 self.sameq_preempt_min_run_ns = Some(v);
489 self
490 }
491
492 pub fn preempt_slice_ns(mut self, v: u64) -> Self {
494 self.preempt_slice_ns = Some(v);
495 self
496 }
497
498 pub fn q1_quota(mut self, v: u32) -> Self {
500 self.q1_quota = Some(v);
501 self
502 }
503
504 pub fn q2_quota(mut self, v: u32) -> Self {
506 self.q2_quota = Some(v);
507 self
508 }
509
510 pub fn dispatch_max_batch(mut self, v: u32) -> Self {
512 self.dispatch_max_batch = Some(v);
513 self
514 }
515
516 pub fn rtdl_drain_interval_ns(mut self, v: u64) -> Self {
518 self.rtdl_drain_interval_ns = Some(v);
519 self
520 }
521
522 pub fn tree_t_int_ns(mut self, v: u64) -> Self {
524 self.tree_t_int_ns = Some(v);
525 self
526 }
527
528 pub fn tree_t_bound_ns(mut self, v: u64) -> Self {
530 self.tree_t_bound_ns = Some(v);
531 self
532 }
533
534 pub fn adapt_enabled(mut self, v: bool) -> Self {
536 self.adapt_enabled = Some(v);
537 self
538 }
539
540 pub fn build(self) -> Result<Config> {
545 let defaults = Config::default();
546 let cfg = Config {
547 q1_slice_ns: self.q1_slice_ns.unwrap_or(defaults.q1_slice_ns),
548 q2_slice_ns: self.q2_slice_ns.unwrap_or(defaults.q2_slice_ns),
549 q3_slice_ns: self.q3_slice_ns.unwrap_or(defaults.q3_slice_ns),
550 budget_max_ns: self.budget_max_ns.unwrap_or(defaults.budget_max_ns),
551 alpha: self.alpha.unwrap_or(defaults.alpha),
552 t_l_ns: self.t_l_ns.unwrap_or(defaults.t_l_ns),
553 t_h_ns: self.t_h_ns.unwrap_or(defaults.t_h_ns),
554 ema_half_life_ns: self.ema_half_life_ns.unwrap_or(defaults.ema_half_life_ns),
555 aging_period_ns: self.aging_period_ns.unwrap_or(defaults.aging_period_ns),
556 short_sleep_ns: self.short_sleep_ns.unwrap_or(defaults.short_sleep_ns),
557 short_sleep_rate_limit_ns: self
558 .short_sleep_rate_limit_ns
559 .unwrap_or(defaults.short_sleep_rate_limit_ns),
560 hysteresis_sleep_ns: self
561 .hysteresis_sleep_ns
562 .unwrap_or(defaults.hysteresis_sleep_ns),
563 long_sleep_ns: self.long_sleep_ns.unwrap_or(defaults.long_sleep_ns),
564 sameq_preempt_min_run_ns: self
565 .sameq_preempt_min_run_ns
566 .unwrap_or(defaults.sameq_preempt_min_run_ns),
567 preempt_slice_ns: self.preempt_slice_ns.unwrap_or(defaults.preempt_slice_ns),
568 q1_quota: self.q1_quota.unwrap_or(defaults.q1_quota),
569 q2_quota: self.q2_quota.unwrap_or(defaults.q2_quota),
570 dispatch_max_batch: self
571 .dispatch_max_batch
572 .unwrap_or(defaults.dispatch_max_batch),
573 rtdl_drain_interval_ns: self
574 .rtdl_drain_interval_ns
575 .unwrap_or(defaults.rtdl_drain_interval_ns),
576 tree_t_int_ns: self.tree_t_int_ns.unwrap_or(defaults.tree_t_int_ns),
577 tree_t_bound_ns: self.tree_t_bound_ns.unwrap_or(defaults.tree_t_bound_ns),
578 adapt_enabled: self.adapt_enabled.unwrap_or(defaults.adapt_enabled),
579 };
580 cfg.validate()?;
581 Ok(cfg)
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
590 fn defaults_match_intf_h() {
591 use crate::bpf_intf::{
599 mlfq_consts_MLFQ_AGING_PERIOD_NS, mlfq_consts_MLFQ_ALPHA,
600 mlfq_consts_MLFQ_BUDGET_MAX_NS, mlfq_consts_MLFQ_DISPATCH_MAX_BATCH,
601 mlfq_consts_MLFQ_EMA_HALF_LIFE_NS, mlfq_consts_MLFQ_HYSTERESIS_SLEEP_NS,
602 mlfq_consts_MLFQ_LONG_SLEEP_NS, mlfq_consts_MLFQ_PREEMPT_SLICE_NS,
603 mlfq_consts_MLFQ_Q1_QUOTA, mlfq_consts_MLFQ_Q1_SLICE_NS, mlfq_consts_MLFQ_Q2_QUOTA,
604 mlfq_consts_MLFQ_Q2_SLICE_NS, mlfq_consts_MLFQ_Q3_SLICE_NS,
605 mlfq_consts_MLFQ_RTDL_DRAIN_INTERVAL_NS, mlfq_consts_MLFQ_SAMEQ_PREEMPT_MIN_RUN_NS,
606 mlfq_consts_MLFQ_SHORT_SLEEP_NS, mlfq_consts_MLFQ_SHORT_SLEEP_RATE_LIMIT_NS,
607 mlfq_consts_MLFQ_T_H_NS, mlfq_consts_MLFQ_T_L_NS, mlfq_consts_MLFQ_TREE_T_BOUND_NS,
608 mlfq_consts_MLFQ_TREE_T_INT_NS,
609 };
610
611 let cfg = Config::default();
612 cfg.validate().unwrap();
613 assert_eq!(cfg.q1_slice_ns, mlfq_consts_MLFQ_Q1_SLICE_NS as u64);
614 assert_eq!(cfg.q2_slice_ns, mlfq_consts_MLFQ_Q2_SLICE_NS as u64);
615 assert_eq!(cfg.q3_slice_ns, mlfq_consts_MLFQ_Q3_SLICE_NS as u64);
616 assert_eq!(cfg.budget_max_ns, mlfq_consts_MLFQ_BUDGET_MAX_NS as u64);
617 assert_eq!(cfg.alpha, mlfq_consts_MLFQ_ALPHA as u64);
618 assert_eq!(cfg.t_l_ns, mlfq_consts_MLFQ_T_L_NS as u64);
619 assert_eq!(cfg.t_h_ns, mlfq_consts_MLFQ_T_H_NS as u64);
620 assert_eq!(
621 cfg.ema_half_life_ns,
622 mlfq_consts_MLFQ_EMA_HALF_LIFE_NS as u64
623 );
624 assert_eq!(cfg.aging_period_ns, mlfq_consts_MLFQ_AGING_PERIOD_NS as u64);
625 assert_eq!(cfg.short_sleep_ns, mlfq_consts_MLFQ_SHORT_SLEEP_NS as u64);
626 assert_eq!(
627 cfg.short_sleep_rate_limit_ns,
628 mlfq_consts_MLFQ_SHORT_SLEEP_RATE_LIMIT_NS as u64
629 );
630 assert_eq!(
631 cfg.hysteresis_sleep_ns,
632 mlfq_consts_MLFQ_HYSTERESIS_SLEEP_NS as u64
633 );
634 assert_eq!(cfg.long_sleep_ns, mlfq_consts_MLFQ_LONG_SLEEP_NS as u64);
635 assert_eq!(
636 cfg.sameq_preempt_min_run_ns,
637 mlfq_consts_MLFQ_SAMEQ_PREEMPT_MIN_RUN_NS as u64
638 );
639 assert_eq!(
640 cfg.preempt_slice_ns,
641 mlfq_consts_MLFQ_PREEMPT_SLICE_NS as u64
642 );
643 assert_eq!(cfg.q1_quota, mlfq_consts_MLFQ_Q1_QUOTA);
644 assert_eq!(cfg.q2_quota, mlfq_consts_MLFQ_Q2_QUOTA);
645 assert_eq!(cfg.dispatch_max_batch, mlfq_consts_MLFQ_DISPATCH_MAX_BATCH);
646 assert_eq!(
647 cfg.rtdl_drain_interval_ns,
648 mlfq_consts_MLFQ_RTDL_DRAIN_INTERVAL_NS as u64
649 );
650 assert_eq!(cfg.tree_t_int_ns, mlfq_consts_MLFQ_TREE_T_INT_NS as u64);
651 assert_eq!(cfg.tree_t_bound_ns, mlfq_consts_MLFQ_TREE_T_BOUND_NS as u64);
652 assert!(cfg.adapt_enabled, "the adaptation ships enabled");
653 }
654
655 #[test]
656 fn builder_defaults_equal_config_defaults() {
657 let cfg = ConfigBuilder::default().build().unwrap();
658 assert_eq!(cfg, Config::default());
659 }
660
661 #[test]
662 fn builder_overrides_individual_fields() {
663 let cfg = ConfigBuilder::default()
664 .q1_slice_ns(500_000)
665 .build()
666 .unwrap();
667 assert_eq!(cfg.q1_slice_ns, 500_000);
668 assert_eq!(cfg.q2_slice_ns, Config::default().q2_slice_ns);
669 }
670
671 #[test]
672 fn builder_overrides_sameq_min_run() {
673 let cfg = ConfigBuilder::default()
674 .sameq_preempt_min_run_ns(250_000)
675 .build()
676 .unwrap();
677 assert_eq!(cfg.sameq_preempt_min_run_ns, 250_000);
678 assert_eq!(cfg.q1_slice_ns, Config::default().q1_slice_ns);
679 }
680
681 #[test]
682 fn builder_overrides_preempt_slice() {
683 let cfg = ConfigBuilder::default()
684 .preempt_slice_ns(100_000)
685 .build()
686 .unwrap();
687 assert_eq!(cfg.preempt_slice_ns, 100_000);
688 assert_eq!(cfg.q1_slice_ns, Config::default().q1_slice_ns);
689 }
690
691 #[test]
692 fn rejects_zero_slices() {
693 assert!(ConfigBuilder::default().q1_slice_ns(0).build().is_err());
694 assert!(ConfigBuilder::default().q2_slice_ns(0).build().is_err());
695 assert!(ConfigBuilder::default().q3_slice_ns(0).build().is_err());
696 }
697
698 #[test]
699 fn rejects_t_l_at_or_above_t_h() {
700 assert!(ConfigBuilder::default().t_l_ns(2_000_000).build().is_err());
701 assert!(ConfigBuilder::default().t_l_ns(3_000_000).build().is_err());
702 }
703
704 #[test]
705 fn rejects_t_h_at_or_above_budget_max() {
706 assert!(ConfigBuilder::default().t_h_ns(6_000_000).build().is_err());
707 assert!(ConfigBuilder::default().t_h_ns(7_000_000).build().is_err());
708 }
709
710 #[test]
711 fn rejects_zero_quotas() {
712 assert!(ConfigBuilder::default().q1_quota(0).build().is_err());
713 assert!(ConfigBuilder::default().q2_quota(0).build().is_err());
714 assert!(
715 ConfigBuilder::default()
716 .dispatch_max_batch(0)
717 .build()
718 .is_err()
719 );
720 }
721
722 #[test]
723 fn rejects_quotas_consuming_the_batch() {
724 let cfg = ConfigBuilder::default()
726 .q1_quota(16)
727 .q2_quota(16)
728 .dispatch_max_batch(32)
729 .build();
730 assert!(cfg.is_err());
731 }
732
733 #[test]
734 fn rejects_dispatch_batch_above_ops_bound() {
735 let cfg = ConfigBuilder::default()
738 .dispatch_max_batch(crate::bpf_intf::mlfq_consts_MLFQ_DISPATCH_MAX_BATCH + 1)
739 .build();
740 assert!(cfg.is_err());
741 let cfg = ConfigBuilder::default()
743 .dispatch_max_batch(crate::bpf_intf::mlfq_consts_MLFQ_DISPATCH_MAX_BATCH)
744 .build();
745 assert!(cfg.is_ok());
746 }
747
748 #[test]
749 fn rejects_zero_aging_period() {
750 assert!(ConfigBuilder::default().aging_period_ns(0).build().is_err());
751 }
752
753 #[test]
754 fn rejects_tree_bands_out_of_order() {
755 assert!(ConfigBuilder::default().tree_t_int_ns(0).build().is_err());
756 let cfg = ConfigBuilder::default()
757 .tree_t_int_ns(3_000_000)
758 .tree_t_bound_ns(3_000_000)
759 .build();
760 assert!(cfg.is_err());
761 let cfg = ConfigBuilder::default()
762 .tree_t_int_ns(4_000_000)
763 .tree_t_bound_ns(3_000_000)
764 .build();
765 assert!(cfg.is_err());
766 assert!(ConfigBuilder::default().build().is_ok());
768 }
769
770 #[test]
771 fn adapt_gate_flag_round_trips() {
772 let cfg = ConfigBuilder::default()
773 .adapt_enabled(true)
774 .build()
775 .unwrap();
776 assert!(cfg.adapt_enabled);
777 let cfg = ConfigBuilder::default().build().unwrap();
778 assert!(cfg.adapt_enabled);
779 }
780
781 #[test]
782 fn describe_is_stable() {
783 let cfg = Config::default();
784 let s = cfg.describe();
785 assert!(s.contains("slices: Q1=1000us Q2=2000us Q3=4000us"));
786 assert!(s.contains("T_L=250us, T_H=2000us"));
787 assert!(s.contains("aging_period=1s"));
788 assert!(s.contains("rtdl_drain_interval=1000us"));
789 assert!(s.contains("quotas: Q1=4 Q2=8 max_batch=32"));
790 assert!(s.contains("tree_bands: T_INT=1000us T_BOUND=3000us"));
791 assert!(s.contains("adapt_enabled=true"));
792 }
793}