1mod bpf_skel;
20pub use bpf_skel::*;
21pub mod bpf_intf;
22pub use bpf_intf::*;
23
24mod alloc;
25mod config;
26mod mlfq_tree;
27mod stats;
28mod topology;
29
30#[cfg(feature = "count_alloc")]
31#[global_allocator]
32static ALLOC: alloc::TrackingAllocator = alloc::TrackingAllocator;
33
34mod webui;
35
36use std::collections::HashMap;
37use std::collections::HashSet;
38use std::collections::VecDeque;
39use std::mem::MaybeUninit;
40use std::mem::size_of;
41use std::os::fd::AsFd;
42use std::os::fd::AsRawFd;
43use std::sync::Arc;
44use std::sync::atomic::AtomicBool;
45use std::sync::atomic::Ordering;
46use std::time::Duration;
47
48use anyhow::Result;
49use clap::CommandFactory;
50use clap::Parser;
51use clap_complete::Shell;
52use clap_complete::generate;
53use crossbeam::channel::RecvTimeoutError;
54use libbpf_rs::AsRawLibbpf;
55use libbpf_rs::MapCore;
56use log::info;
57use scx_stats::prelude::*;
58use scx_utils::UserExitInfo;
59use scx_utils::build_id;
60use scx_utils::compat;
61use scx_utils::libbpf_clap_opts::LibbpfOpts;
62use scx_utils::pm;
63use scx_utils::scx_ops_attach;
64use scx_utils::scx_ops_load;
65use scx_utils::scx_ops_open;
66use scx_utils::try_set_rlimit_infinity;
67use scx_utils::uei_exited;
68use scx_utils::uei_report;
69
70use config::Config;
71use mlfq_tree::FitScratch;
72use mlfq_tree::TreeSample;
73use stats::Metrics;
74
75const SCHEDULER_NAME: &str = "scx_mlfq";
76
77const NSEC_PER_USEC: u64 = crate::bpf_intf::mlfq_consts_NSEC_PER_USEC as u64;
79
80const MLFQ_TREE_MAX_NODES: usize = crate::bpf_intf::mlfq_consts_MLFQ_TREE_MAX_NODES as usize;
82const MLFQ_TREE_MAX_DEPTH: usize = crate::bpf_intf::mlfq_consts_MLFQ_TREE_MAX_DEPTH as usize;
83const MLFQ_TREE_MIN_SAMPLES: usize = crate::bpf_intf::mlfq_consts_MLFQ_TREE_MIN_SAMPLES as usize;
84
85const MLFQ_MAX_CPUS: usize = crate::bpf_intf::mlfq_consts_MLFQ_MAX_CPUS as usize;
87
88const MLFQ_TREE_WINDOW_MAX: usize = 16384;
103
104const MLFQ_TREE_PER_PID_CAP: u32 = (MLFQ_TREE_WINDOW_MAX / 20) as u32;
114
115const MLFQ_TREE_MIN_PIDS: usize = 8;
124
125const MLFQ_TREE_MIN_LEAF: usize = 32;
127const MLFQ_TREE_RETRAIN_INTERVAL: Duration = Duration::from_secs(60);
128
129const MLFQ_IDLE_RESUME_LATENCY_US: i32 = 10;
138
139fn full_version() -> String {
140 build_id::full_version(env!("CARGO_PKG_VERSION"))
141}
142
143#[derive(Debug, Parser)]
144#[command(name = SCHEDULER_NAME, version, disable_version_flag = true)]
145struct Opts {
146 #[clap(long)]
148 stats: Option<f64>,
149
150 #[clap(long)]
152 monitor: Option<f64>,
153
154 #[clap(short = 'd', long, action = clap::ArgAction::SetTrue)]
156 debug: bool,
157
158 #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
160 verbose: bool,
161
162 #[clap(long, default_value = "1048576")]
165 exit_dump_len: u32,
166
167 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
169 version: bool,
170
171 #[clap(long)]
173 help_stats: bool,
174
175 #[clap(long, value_name = "SHELL", hide = true)]
177 completions: Option<Shell>,
178
179 #[clap(long = "no-webui", action = clap::ArgAction::SetTrue)]
186 no_webui: bool,
187
188 #[clap(flatten, next_help_heading = "Libbpf Options")]
189 libbpf: LibbpfOpts,
190}
191
192#[derive(Clone, Copy, Debug, Default)]
195struct ModelMeta {
196 generation: u64,
198 nr_samples: usize,
200 nr_nodes: usize,
202 mae_tree_us: u64,
205 mae_ema_us: u64,
208 corr: f64,
211}
212
213struct Scheduler<'a> {
217 skel: BpfSkel<'a>,
218 struct_ops: Option<libbpf_rs::Link>,
219 stats_server: StatsServer<(), Metrics>,
220 webui_tx: Option<crossbeam::channel::Sender<stats::WebMetrics>>,
227 webui_join: Option<std::thread::JoinHandle<()>>,
228 cpu_static: Vec<stats::PerCpuMetrics>,
229 cur_freq_khz: Vec<u64>,
236 freq_read_at: Option<std::time::Instant>,
237 started_at: std::time::Instant,
238 #[expect(dead_code)]
245 pm_qos_fd: Option<std::fs::File>,
246 rb_mgr: libbpf_rs::RingBuffer<'static>,
254 sample_rx: crossbeam::channel::Receiver<TreeSample>,
255 window: VecDeque<TreeSample>,
256 pid_counts: HashMap<u32, u32>,
263 tree_samples_cap_dropped: u64,
264 last_train_at: Option<std::time::Instant>,
265 train_tx: crossbeam::channel::Sender<Vec<TreeSample>>,
266 train_rx: crossbeam::channel::Receiver<Result<TrainResult, anyhow::Error>>,
267 model: ModelMeta,
268 train_snapshot_buf: Vec<TreeSample>,
272 web_statics_buf: Vec<Option<stats::PerCpuMetrics>>,
273 web_per_cpu_buf: Vec<stats::PerCpuMetrics>,
274 #[allow(dead_code)]
275 op_lat_buf: Vec<u64>,
276 wakeup_raw_buf: Vec<u8>,
277 op_lat_raw_buf: Vec<u8>,
278}
279
280impl<'a> Scheduler<'a> {
281 fn init(
282 opts: &'a Opts,
283 open_object: &'a mut MaybeUninit<libbpf_rs::OpenObject>,
284 shutdown: Arc<AtomicBool>,
285 ) -> Result<Self> {
286 try_set_rlimit_infinity();
287
288 let mut skel_builder = BpfSkelBuilder::default();
289 skel_builder.obj_builder.debug(opts.debug || opts.verbose);
290
291 let open_opts = opts.libbpf.clone().into_bpf_open_opts();
292 let mut skel = scx_ops_open!(skel_builder, open_object, mlfq_ops, open_opts)?;
293
294 let config = Config::default();
297 config.validate()?;
298 config.apply(&mut skel)?;
299 info!("Config: {}", config.describe());
300
301 let topology_plan = topology::init_topology(&mut skel)?;
304
305 let mut flags = *compat::SCX_OPS_ENQ_EXITING
322 | *compat::SCX_OPS_ENQ_LAST
323 | *compat::SCX_OPS_ENQ_MIGRATION_DISABLED
324 | *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP;
325 if *compat::SCX_OPS_KEEP_BUILTIN_IDLE != 0 {
326 flags |= *compat::SCX_OPS_KEEP_BUILTIN_IDLE;
327 skel.maps
328 .rodata_data
329 .as_mut()
330 .expect("rodata missing, the BPF object has no .rodata section")
331 .mlfq_idle_tracking = 1;
332 } else {
333 skel.struct_ops.mlfq_ops_mut().update_idle = std::ptr::null_mut();
334 }
335 skel.struct_ops.mlfq_ops_mut().flags = flags;
336
337 skel.struct_ops.mlfq_ops_mut().exit_dump_len = opts.exit_dump_len;
344
345 unsafe {
359 libbpf_rs::libbpf_sys::bpf_program__set_autoload(
360 skel.progs.mlfq_sched_switch.as_libbpf_object().as_ptr(),
361 true,
362 );
363 }
364 {
374 let has = |p1: &str, p2: &str| {
375 std::path::Path::new(p1).exists() || std::path::Path::new(p2).exists()
376 };
377 if !has(
378 "/sys/kernel/debug/tracing/events/amdgpu/amdgpu_cs",
379 "/sys/kernel/tracing/events/amdgpu/amdgpu_cs",
380 ) {
381 skel.progs.mlfq_amdgpu_cs.set_autoload(false);
382 }
383 if !has(
384 "/sys/kernel/debug/tracing/events/amdgpu/amdgpu_cs_ioctl",
385 "/sys/kernel/tracing/events/amdgpu/amdgpu_cs_ioctl",
386 ) {
387 skel.progs.mlfq_amdgpu_cs_ioctl.set_autoload(false);
388 }
389 if !has(
390 "/sys/kernel/debug/tracing/events/gpu_scheduler/drm_sched_job_queue",
391 "/sys/kernel/tracing/events/gpu_scheduler/drm_sched_job_queue",
392 ) {
393 skel.progs.mlfq_gpu_sched_queue.set_autoload(false);
394 }
395 }
396
397 let mut skel = scx_ops_load!(skel, mlfq_ops, uei)?;
398
399 if let Err(e) = topology::write_primary_bitmap(&mut skel, &topology_plan.capacity) {
405 log::warn!(
406 "failed to write the primary bitmap, falling back to all-primary placement: {e:#}"
407 );
408 }
409 if let Err(e) = topology::write_llc_bitmaps(&mut skel, &topology_plan.llcs) {
410 log::warn!("failed to write the LLC bitmaps, disabling LLC-aware placement: {e:#}");
411 }
412 if let Err(e) = topology::write_llc_cpu_lists(&mut skel, &topology_plan.llcs) {
417 log::warn!(
418 "failed to write the per-LLC CPU lists, disabling LLC-aware stealing: {e:#}"
419 );
420 }
421
422 {
428 let mut mask: u32 = 0;
429 if skel.progs.mlfq_amdgpu_cs.autoload() {
430 mask |= crate::bpf_intf::MLFQ_GPU_TRACE_AMDGPU_CS;
431 }
432 if skel.progs.mlfq_amdgpu_cs_ioctl.autoload() {
433 mask |= crate::bpf_intf::MLFQ_GPU_TRACE_AMDGPU_CS_IOCTL;
434 }
435 if skel.progs.mlfq_gpu_sched_queue.autoload() {
436 mask |= crate::bpf_intf::MLFQ_GPU_TRACE_GPU_SCHED;
437 }
438 if mask != 0
439 && let Some(bss) = skel.maps.bss_data.as_mut()
440 {
441 bss.mlfq_gpu_trace_mask |= mask;
442 }
443 }
444
445 let struct_ops = scx_ops_attach!(skel, mlfq_ops)?;
446
447 let pm_qos_fd = if pm::cpu_idle_resume_latency_supported() {
458 match pm::update_global_idle_resume_latency(MLFQ_IDLE_RESUME_LATENCY_US) {
459 Ok(f) => {
460 info!(
461 "PM QoS idle resume latency held at {}us",
462 MLFQ_IDLE_RESUME_LATENCY_US
463 );
464 Some(f)
465 }
466 Err(e) => {
467 log::warn!("failed to set the PM QoS idle resume latency: {e:#}");
468 None
469 }
470 }
471 } else {
472 log::warn!(
473 "PM QoS idle resume latency is not supported; the constraint is not applied"
474 );
475 None
476 };
477
478 let stats_server = StatsServer::new(stats::server_data()).launch()?;
479
480 let (webui_tx, webui_join): (
489 Option<crossbeam::channel::Sender<stats::WebMetrics>>,
490 Option<std::thread::JoinHandle<()>>,
491 ) = if opts.no_webui {
492 (None, None)
493 } else {
494 let (tx, rx) = crossbeam::channel::bounded::<stats::WebMetrics>(16);
495 let shutdown = shutdown.clone();
496 let jh = std::thread::spawn(move || {
497 webui::start(rx, shutdown);
498 });
499 (Some(tx), Some(jh))
500 };
501 let cpu_static = if opts.no_webui {
502 Vec::new()
503 } else {
504 topology::web_cpu_static()
505 };
506
507 let (sample_tx, sample_rx) = crossbeam::channel::bounded(4096);
519 let mut rb_builder = libbpf_rs::RingBufferBuilder::new();
520 rb_builder.add(&skel.maps.mlfq_samples, move |data| {
521 if data.len() < size_of::<TreeSample>() {
522 return 0;
523 }
524 let s = unsafe { std::ptr::read_unaligned(data.as_ptr().cast::<TreeSample>()) };
529 if !mlfq_tree::sample_version_matches(&s) {
530 return 0;
531 }
532 let _ = sample_tx.try_send(s);
533 0
534 })?;
535 let rb_mgr = rb_builder.build()?;
536
537 let (train_tx, train_rx) = spawn_train_worker();
548
549 Ok(Self {
550 skel,
551 struct_ops: Some(struct_ops),
552 stats_server,
553 webui_tx,
554 webui_join,
555 cpu_static,
556 cur_freq_khz: Vec::with_capacity(MLFQ_MAX_CPUS),
557 freq_read_at: None,
558 started_at: std::time::Instant::now(),
559 pm_qos_fd,
560 rb_mgr,
561 sample_rx,
562 window: VecDeque::with_capacity(MLFQ_TREE_WINDOW_MAX),
563 pid_counts: HashMap::with_capacity(2048),
564 tree_samples_cap_dropped: 0,
565 last_train_at: None,
566 train_tx,
567 train_rx,
568 model: ModelMeta::default(),
569 train_snapshot_buf: Vec::with_capacity(MLFQ_TREE_WINDOW_MAX),
570 web_statics_buf: Vec::with_capacity(MLFQ_MAX_CPUS),
571 web_per_cpu_buf: Vec::with_capacity(MLFQ_MAX_CPUS),
572 op_lat_buf: Vec::with_capacity(
573 crate::bpf_intf::mlfq_op_lat_slots_MLFQ_OP_LAT_OPS as usize
574 * crate::bpf_intf::mlfq_op_lat_consts_MLFQ_OP_LAT_BUCKETS as usize,
575 ),
576 wakeup_raw_buf: Vec::with_capacity(16 * MLFQ_MAX_CPUS),
577 op_lat_raw_buf: Vec::with_capacity(64 * MLFQ_MAX_CPUS),
578 })
579 }
580
581 fn get_metrics(&mut self) -> Metrics {
582 let op_lat = self.read_op_lat();
583 let wakeup_total = self.read_wakeup_total();
584 let bss_data = self
585 .skel
586 .maps
587 .bss_data
588 .as_ref()
589 .expect("bss_data missing, the BPF object has no .bss section");
590 let s = &bss_data.mlfq_stats;
591 let g = &bss_data.mlfq_sys_gauge;
592 let a = &bss_data.mlfq_adapt_state;
593 Metrics {
594 on_cpu: s.on_cpu,
595 total_runtime: s.total_runtime,
596 uptime_ns: self.started_at.elapsed().as_nanos() as u64,
597 q1_placements: s.q1_placements,
598 q2_placements: s.q2_placements,
599 q3_placements: s.q3_placements,
600 promotions: s.promotions,
601 demotions: s.demotions,
602 aging_boosts: s.aging_boosts,
603 short_sleep_boosts: s.short_sleep_boosts,
604 preemption_kicks: s.preemption_kicks,
605 cpuperf_boosts: s.cpuperf_boosts,
606 steals: s.steals,
607 steals_same_llc: s.steals_same_llc,
608 steals_cross_llc: s.steals_cross_llc,
609 keep_running: s.keep_running,
610 enq_no_tctx: s.enq_no_tctx,
611 enq_bad_weight: s.enq_bad_weight,
612 enq_no_deadline: s.enq_no_deadline,
613 enq_fastpath: s.enq_fastpath,
614 enq_regular: s.enq_regular,
615 enq_pinned_idle: s.enq_pinned_idle,
616 enq_pinned_busy: s.enq_pinned_busy,
617 enq_pinned_global: s.enq_pinned_global,
618 tree_inference: s.tree_inference,
619 tree_fallback: s.tree_fallback,
620 tree_disagree: s.tree_disagree,
621 tree_samples_emitted: s.tree_samples_emitted,
622 tree_samples_dropped: s.tree_samples_dropped,
623 rt_takeovers: s.rt_takeovers,
624 rt_evacuations: s.rt_evacuations,
625 rt_redirects: s.rt_redirects,
626 rt_reenqs: s.rt_reenqs,
627 op_lat,
628 tree_samples_cap_dropped: self.tree_samples_cap_dropped,
629 tree_model_generation: self.model.generation,
630 tree_model_nodes: self.model.nr_nodes as u64,
631 tree_model_samples: self.model.nr_samples as u64,
632 tree_mae_tree_us: self.model.mae_tree_us,
633 tree_mae_ema_us: self.model.mae_ema_us,
634 tree_corr_milli: (self.model.corr * 1000.0).round() as i64,
635 sys_lat_ema_us: g.lat_ema / NSEC_PER_USEC,
636 sys_rate_ema: g.rate_ema,
637 t_l_eff_us: a.t_l_eff_ns / NSEC_PER_USEC,
638 t_h_eff_us: a.t_h_eff_ns / NSEC_PER_USEC,
639 t_int_eff_us: a.t_int_eff_ns / NSEC_PER_USEC,
640 t_bnd_eff_us: a.t_bnd_eff_ns / NSEC_PER_USEC,
641 guard_eff_us: a.guard_eff_ns / NSEC_PER_USEC,
642 adapt_shift: a.shift_fp,
643 wakeup_total,
644 adapt_steps: u64::from(g.adapt_steps),
645 }
646 }
647
648 fn get_web_metrics(&mut self) -> stats::WebMetrics {
652 let nr_cpus_bss = self
657 .skel
658 .maps
659 .bss_data
660 .as_ref()
661 .expect("bss_data missing, the BPF object has no .bss section")
662 .nr_cpu_ids as usize;
663 let now = std::time::Instant::now();
664 if self
665 .freq_read_at
666 .is_none_or(|t| now.duration_since(t).as_secs() >= 1)
667 {
668 let nr = nr_cpus_bss.min(MLFQ_MAX_CPUS);
669 self.cur_freq_khz.clear();
670 self.cur_freq_khz.reserve(nr);
671 for cpu in 0..nr {
672 self.cur_freq_khz
673 .push(topology::current_freq_khz(cpu as u32));
674 }
675 self.freq_read_at = Some(now);
676 }
677 let bss_data = self
678 .skel
679 .maps
680 .bss_data
681 .as_ref()
682 .expect("bss_data missing, the BPF object has no .bss section");
683
684 let nr_cpus = (bss_data.nr_cpu_ids as usize).min(MLFQ_MAX_CPUS);
689 self.web_statics_buf.clear();
692 self.web_statics_buf.resize_with(nr_cpus, || None);
693 for s in &self.cpu_static {
694 if (s.id as usize) < nr_cpus {
695 self.web_statics_buf[s.id as usize] = Some(s.clone());
696 }
697 }
698 self.web_per_cpu_buf.clear();
699 for (cpu, static_entry) in self.web_statics_buf.iter().enumerate().take(nr_cpus) {
700 let mut entry = static_entry.clone().unwrap_or_default();
701 entry.id = cpu as u32;
702 entry.cur_freq_khz = self.cur_freq_khz.get(cpu).copied().unwrap_or(0);
703
704 let state = self.read_cpu_state_noalloc(cpu);
705 entry.running_queue = state.running_queue;
706 entry.running_pid = state.running_pid;
707 entry.running_gpu_submit = state.running_gpu_submit;
708
709 let rt = self.read_rtdl_state_noalloc(cpu);
710 entry.rt_occupied = rt.flags & crate::bpf_intf::MLFQ_RTDL_OCCUPIED != 0;
711 self.web_per_cpu_buf.push(entry);
712 }
713 let mut per_cpu = Vec::with_capacity(nr_cpus);
716 std::mem::swap(&mut per_cpu, &mut self.web_per_cpu_buf);
717
718 let gpu_submit_total = bss_data.mlfq_gpu_submit_total;
721 let gpu_trace_mask = bss_data.mlfq_gpu_trace_mask;
722 let stats = self.get_metrics();
723 stats::WebMetrics {
724 stats,
725 per_cpu,
726 queue_runnable: self.read_queue_runnable(),
727 llc_runnable: self.read_llc_runnable(),
728 gpu_submit_total,
729 gpu_trace_mask,
730 }
731 }
732
733 #[allow(dead_code)]
736 fn read_cpu_state(&self, cpu: usize) -> mlfq_cpu_state {
737 let key = (cpu as u32).to_ne_bytes();
738 match self
739 .skel
740 .maps
741 .cpu_state_stor
742 .lookup(&key, libbpf_rs::MapFlags::ANY)
743 {
744 Ok(Some(bytes)) if bytes.len() >= size_of::<mlfq_cpu_state>() => {
745 unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<mlfq_cpu_state>()) }
749 }
750 _ => mlfq_cpu_state {
751 running_queue: 0,
752 running_pid: 0,
753 steal_scan_off: 0,
754 cpu_ema: 0,
755 cpu_ema_at: 0,
756 running_deadline: 0,
757 run_start_at: 0,
758 running_gpu_submit: 0,
759 pad2: 0,
760 },
761 }
762 }
763
764 #[allow(dead_code)]
767 fn read_rtdl_state(&self, cpu: usize) -> mlfq_rtdl_state {
768 let key = (cpu as u32).to_ne_bytes();
769 match self
770 .skel
771 .maps
772 .rtdl_state_stor
773 .lookup(&key, libbpf_rs::MapFlags::ANY)
774 {
775 Ok(Some(bytes)) if bytes.len() >= size_of::<mlfq_rtdl_state>() => {
776 unsafe { std::ptr::read_unaligned(bytes.as_ptr().cast::<mlfq_rtdl_state>()) }
779 }
780 _ => mlfq_rtdl_state {
781 flags: 0,
782 pad: 0,
783 last_drain_at: 0,
784 },
785 }
786 }
787
788 fn read_queue_runnable(&self) -> Vec<u64> {
796 let bss_data = self
797 .skel
798 .maps
799 .bss_data
800 .as_ref()
801 .expect("bss_data missing, the BPF object has no .bss section");
802 bss_data
803 .mlfq_queue_runnable
804 .iter()
805 .map(|v| *v as u64)
806 .collect()
807 }
808
809 fn read_llc_runnable(&self) -> Vec<u64> {
812 let bss_data = self
813 .skel
814 .maps
815 .bss_data
816 .as_ref()
817 .expect("bss_data missing, the BPF object has no .bss section");
818 bss_data
819 .mlfq_llc_runnable
820 .iter()
821 .map(|v| *v as u64)
822 .collect()
823 }
824
825 fn exited(&self) -> bool {
826 uei_exited!(&self.skel, uei)
827 }
828
829 fn read_cpu_state_noalloc(&self, cpu: usize) -> mlfq_cpu_state {
833 if cpu >= MLFQ_MAX_CPUS {
834 return mlfq_cpu_state {
835 running_queue: 0,
836 running_pid: 0,
837 steal_scan_off: 0,
838 cpu_ema: 0,
839 cpu_ema_at: 0,
840 running_deadline: 0,
841 run_start_at: 0,
842 running_gpu_submit: 0,
843 pad2: 0,
844 };
845 }
846 let fd = self.skel.maps.cpu_state_stor.as_fd().as_raw_fd();
847 let key = cpu as u32;
848 let mut out = std::mem::MaybeUninit::<mlfq_cpu_state>::uninit();
849 let ret = unsafe {
850 libbpf_rs::libbpf_sys::bpf_map_lookup_elem(
851 fd,
852 &key as *const _ as *const std::ffi::c_void,
853 out.as_mut_ptr() as *mut std::ffi::c_void,
854 )
855 };
856 if ret == 0 {
857 unsafe { out.assume_init() }
858 } else {
859 mlfq_cpu_state {
860 running_queue: 0,
861 running_pid: 0,
862 steal_scan_off: 0,
863 cpu_ema: 0,
864 cpu_ema_at: 0,
865 running_deadline: 0,
866 run_start_at: 0,
867 running_gpu_submit: 0,
868 pad2: 0,
869 }
870 }
871 }
872
873 fn read_rtdl_state_noalloc(&self, cpu: usize) -> mlfq_rtdl_state {
875 if cpu >= MLFQ_MAX_CPUS {
876 return mlfq_rtdl_state {
877 flags: 0,
878 pad: 0,
879 last_drain_at: 0,
880 };
881 }
882 let fd = self.skel.maps.rtdl_state_stor.as_fd().as_raw_fd();
883 let key = cpu as u32;
884 let mut out = std::mem::MaybeUninit::<mlfq_rtdl_state>::uninit();
885 let ret = unsafe {
886 libbpf_rs::libbpf_sys::bpf_map_lookup_elem(
887 fd,
888 &key as *const _ as *const std::ffi::c_void,
889 out.as_mut_ptr() as *mut std::ffi::c_void,
890 )
891 };
892 if ret == 0 {
893 unsafe { out.assume_init() }
894 } else {
895 mlfq_rtdl_state {
896 flags: 0,
897 pad: 0,
898 last_drain_at: 0,
899 }
900 }
901 }
902
903 #[allow(clippy::chunks_exact_to_as_chunks)]
908 fn read_op_lat(&mut self) -> Vec<u64> {
909 let nr_ops = crate::bpf_intf::mlfq_op_lat_slots_MLFQ_OP_LAT_OPS as usize;
910 let buckets = crate::bpf_intf::mlfq_op_lat_consts_MLFQ_OP_LAT_BUCKETS as usize;
911 self.op_lat_buf.clear();
914 self.op_lat_buf.resize(nr_ops * buckets, 0);
915 let nr_cpus = (self
916 .skel
917 .maps
918 .bss_data
919 .as_ref()
920 .map(|b| b.nr_cpu_ids as usize)
921 .unwrap_or(1))
922 .min(MLFQ_MAX_CPUS);
923 let value_size = 8 * buckets;
926 let raw_size = value_size * nr_cpus;
927 self.op_lat_raw_buf.clear();
928 self.op_lat_raw_buf.resize(raw_size, 0);
929 for op in 0..nr_ops {
930 let key = (op as u32).to_ne_bytes();
931 let fd = self.skel.maps.mlfq_op_lat.as_fd().as_raw_fd();
932 let ret = unsafe {
933 libbpf_rs::libbpf_sys::bpf_map_lookup_elem(
934 fd,
935 &key as *const _ as *const std::ffi::c_void,
936 self.op_lat_raw_buf.as_mut_ptr() as *mut std::ffi::c_void,
937 )
938 };
939 if ret != 0 {
940 continue;
941 }
942 for cpu in 0..nr_cpus {
943 let base = cpu * value_size;
944 for b in 0..buckets {
945 let off = base + b * 8;
946 let slot = &self.op_lat_raw_buf[off..off + 8];
947 let v = u64::from_ne_bytes(slot.try_into().expect("8-byte slot"));
948 self.op_lat_buf[op * buckets + b] =
949 self.op_lat_buf[op * buckets + b].wrapping_add(v);
950 }
951 }
952 }
953 self.op_lat_buf.clone()
957 }
958
959 fn read_wakeup_total(&mut self) -> u64 {
965 let nr_cpus = (self
966 .skel
967 .maps
968 .bss_data
969 .as_ref()
970 .map(|b| b.nr_cpu_ids as usize)
971 .unwrap_or(1))
972 .min(MLFQ_MAX_CPUS);
973 let value_size = std::mem::size_of::<crate::bpf_intf::mlfq_wakeup_counters>();
974 let raw_size = value_size * nr_cpus;
975 self.wakeup_raw_buf.clear();
976 self.wakeup_raw_buf.resize(raw_size, 0);
977 let fd = self.skel.maps.mlfq_wakeup_stats.as_fd().as_raw_fd();
978 let key = 0u32.to_ne_bytes();
979 let ret = unsafe {
980 libbpf_rs::libbpf_sys::bpf_map_lookup_elem(
981 fd,
982 &key as *const _ as *const std::ffi::c_void,
983 self.wakeup_raw_buf.as_mut_ptr() as *mut std::ffi::c_void,
984 )
985 };
986 if ret != 0 {
987 return 0;
988 }
989 let mut total = 0u64;
990 for cpu in 0..nr_cpus {
991 let base = cpu * value_size;
992 let slot = &self.wakeup_raw_buf[base..base + 8];
993 let v = u64::from_ne_bytes(slot.try_into().expect("8-byte total"));
994 total = total.wrapping_add(v);
995 }
996 total
997 }
998
999 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
1000 let (res_ch, req_ch) = self.stats_server.channels();
1001
1002 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
1003 self.rb_mgr.consume()?;
1010 while let Ok(s) = self.sample_rx.try_recv() {
1011 self.ingest_sample(s);
1012 }
1013 self.poll_train_results();
1014
1015 match req_ch.recv_timeout(Duration::from_millis(100)) {
1016 Ok(()) => {
1017 let web = self.get_web_metrics();
1021 if let Some(ref tx) = self.webui_tx {
1022 let _ = tx.try_send(web);
1023 }
1024 res_ch.send(self.get_metrics())?
1025 }
1026 Err(RecvTimeoutError::Timeout) => {
1027 let web = self.get_web_metrics();
1028 if let Some(ref tx) = self.webui_tx {
1029 let _ = tx.try_send(web);
1030 }
1031 }
1032 Err(e) => Err(e)?,
1033 }
1034 }
1035
1036 self.rb_mgr.consume()?;
1038 while let Ok(s) = self.sample_rx.try_recv() {
1039 self.ingest_sample(s);
1040 }
1041 self.poll_train_results();
1042
1043 let m = self.get_metrics();
1044 log::info!(
1045 "mlfq exit counters: Q1={} Q2={} Q3={} fastpath={} regular={} pin_idle={} pin_busy={} pin_global={} drop_tctx={} drop_weight={} drop_deadline={} promotions={} demotions={} aging_boosts={} short_sleep_boosts={} cpuperf_boosts={} preempt_kicks={} runtime={} on_cpu={} steals={} steals_same_llc={} steals_cross_llc={} keep_running={} rt_takeovers={} rt_evacuations={} rt_redirects={} rt_reenqs={} tree gen={} nodes={} samples={} mae={}us ema_mae={}us corr={:.3} tree_inf={} tree_fallback={} tree_disagree={} tree_emitted={} tree_dropped={} tree_cap_dropped={} wakeups={} adapt_steps={}",
1046 m.q1_placements,
1047 m.q2_placements,
1048 m.q3_placements,
1049 m.enq_fastpath,
1050 m.enq_regular,
1051 m.enq_pinned_idle,
1052 m.enq_pinned_busy,
1053 m.enq_pinned_global,
1054 m.enq_no_tctx,
1055 m.enq_bad_weight,
1056 m.enq_no_deadline,
1057 m.promotions,
1058 m.demotions,
1059 m.aging_boosts,
1060 m.short_sleep_boosts,
1061 m.cpuperf_boosts,
1062 m.preemption_kicks,
1063 m.total_runtime,
1064 m.on_cpu,
1065 m.steals,
1066 m.steals_same_llc,
1067 m.steals_cross_llc,
1068 m.keep_running,
1069 m.rt_takeovers,
1070 m.rt_evacuations,
1071 m.rt_redirects,
1072 m.rt_reenqs,
1073 m.tree_model_generation,
1074 m.tree_model_nodes,
1075 m.tree_model_samples,
1076 m.tree_mae_tree_us,
1077 m.tree_mae_ema_us,
1078 self.model.corr,
1079 m.tree_inference,
1080 m.tree_fallback,
1081 m.tree_disagree,
1082 m.tree_samples_emitted,
1083 m.tree_samples_dropped,
1084 m.tree_samples_cap_dropped,
1085 m.wakeup_total,
1086 m.adapt_steps
1087 );
1088 let _ = self.struct_ops.take();
1089 uei_report!(&self.skel, uei)
1090 }
1091
1092 fn ingest_sample(&mut self, s: TreeSample) {
1101 if !tree_admit_pid(&mut self.pid_counts, s.pid) {
1102 self.tree_samples_cap_dropped += 1;
1103 return;
1104 }
1105 if self.window.len() == MLFQ_TREE_WINDOW_MAX {
1106 let evicted = self
1107 .window
1108 .pop_front()
1109 .expect("a full window pops the oldest sample");
1110 tree_evict_pid(&mut self.pid_counts, evicted.pid);
1111 }
1112 self.window.push_back(s);
1113
1114 let due = match self.last_train_at {
1115 None => self.window.len() >= MLFQ_TREE_MIN_SAMPLES,
1116 Some(t) => {
1117 t.elapsed() >= MLFQ_TREE_RETRAIN_INTERVAL
1118 && self.window.len() >= MLFQ_TREE_MIN_SAMPLES
1119 }
1120 };
1121 if due {
1122 self.kick_training();
1123 }
1124 }
1125
1126 fn kick_training(&mut self) {
1133 self.last_train_at = Some(std::time::Instant::now());
1134 self.train_snapshot_buf.clear();
1138 self.train_snapshot_buf.extend(self.window.iter().copied());
1139 let mut snapshot = Vec::new();
1144 std::mem::swap(&mut snapshot, &mut self.train_snapshot_buf);
1145 self.train_snapshot_buf = Vec::with_capacity(MLFQ_TREE_WINDOW_MAX);
1147 if self.train_tx.try_send(snapshot).is_err() {
1148 log::warn!("MLFQ tree training already in flight, skipping this retrain");
1149 }
1150 }
1151
1152 fn poll_train_results(&mut self) {
1155 while let Ok(res) = self.train_rx.try_recv() {
1156 match res {
1157 Ok(r) => self.apply_train_result(r),
1158 Err(e) => {
1159 log::warn!("MLFQ tree training failed, keeping the previous model: {e:#}");
1160 }
1161 }
1162 }
1163 }
1164
1165 fn apply_train_result(&mut self, res: TrainResult) {
1172 if let Err(e) = mlfq_tree::serialize_validate(&res.tree) {
1173 log::warn!("MLFQ tree training failed validation, keeping the previous model: {e}");
1174 return;
1175 }
1176
1177 let new_gen = self.model.generation + 1;
1178 if res.nr_pids_train < MLFQ_TREE_MIN_PIDS {
1186 log::info!(
1187 "MLFQ tree gen {} rejected: fit slice has only {} distinct pids (< {} required), keeping the previous model",
1188 new_gen,
1189 res.nr_pids_train,
1190 MLFQ_TREE_MIN_PIDS
1191 );
1192 return;
1193 }
1194 let published_corr = if self.model.generation == 0 {
1195 None
1196 } else {
1197 Some(self.model.corr)
1198 };
1199 if !mlfq_tree::should_publish(res.mae_tree, res.mae_ema, res.corr, published_corr) {
1200 log::info!(
1201 "MLFQ tree gen {} rejected: holdout MAE_tree={:.1}us > MAE_ema={:.1}us or corr {:.3} below floor 0.30 or not above published {:.3}, keeping the previous model",
1202 new_gen,
1203 res.mae_tree / 1e3,
1204 res.mae_ema / 1e3,
1205 res.corr,
1206 published_corr.unwrap_or(0.0)
1207 );
1208 return;
1209 }
1210
1211 if let Err(e) = self.publish_tree(&res.tree, new_gen) {
1212 log::warn!("MLFQ tree publish failed, keeping the previous model: {e:#}");
1213 return;
1214 }
1215
1216 self.model = ModelMeta {
1217 generation: new_gen,
1218 nr_samples: res.nr_train,
1219 nr_nodes: res.tree.nodes.len(),
1220 mae_tree_us: (res.mae_tree / 1e3).round() as u64,
1221 mae_ema_us: (res.mae_ema / 1e3).round() as u64,
1222 corr: res.corr,
1223 };
1224 info!(
1225 "MLFQ tree model gen {}, nodes {}, fit {} samples (holdout {}), MAE_tree={}us MAE_ema={}us corr={:.3}",
1226 new_gen,
1227 res.tree.nodes.len(),
1228 res.nr_train,
1229 res.holdout_len,
1230 self.model.mae_tree_us,
1231 self.model.mae_ema_us,
1232 self.model.corr
1233 );
1234 }
1235
1236 fn publish_tree(&mut self, tree: &mlfq_tree::SerializedTree, new_gen: u64) -> Result<()> {
1256 let old_meta = {
1257 let bss = self
1258 .skel
1259 .maps
1260 .bss_data
1261 .as_ref()
1262 .expect("bss_data missing, the BPF object has no .bss section");
1263 bss.mlfq_tree_ctrl.meta
1264 };
1265 let old_gen = old_meta >> crate::bpf_intf::MLFQ_TREE_META_GENERATION_SHIFT;
1266 if new_gen <= old_gen && old_gen != 0 {
1268 anyhow::bail!(
1269 "monotonic gen violation: new {} <= old {}",
1270 new_gen,
1271 old_gen
1272 );
1273 }
1274 let old_active = (old_meta >> 1) & 1;
1275 let new_active = 1 - old_active;
1276 let key = (new_active as u32).to_ne_bytes();
1277
1278 let node_bytes = unsafe {
1279 std::slice::from_raw_parts(
1280 tree.nodes.as_ptr().cast::<u8>(),
1281 tree.nodes.len() * size_of::<mlfq_tree::TreeNode>(),
1282 )
1283 };
1284 let mut buf = vec![0u8; size_of::<mlfq_tree_store>()];
1285 buf[..node_bytes.len()].copy_from_slice(node_bytes);
1286 self.skel
1287 .maps
1288 .mlfq_tree_map
1289 .update(&key, &buf, libbpf_rs::MapFlags::ANY)?;
1290
1291 std::sync::atomic::fence(Ordering::Release);
1293
1294 {
1295 let bss = self
1296 .skel
1297 .maps
1298 .bss_data
1299 .as_mut()
1300 .expect("bss_data missing, the BPF object has no .bss section");
1301 bss.mlfq_tree_ctrl.meta = mlfq_tree::tree_meta(new_gen, tree.nodes.len(), new_active);
1302 }
1303 Ok(())
1304 }
1305}
1306
1307impl Drop for Scheduler<'_> {
1308 fn drop(&mut self) {
1309 if let Some(jh) = self.webui_join.take() {
1322 let _ = jh.join();
1323 }
1324 info!("Unregister {SCHEDULER_NAME} scheduler");
1325 }
1326}
1327
1328struct TrainResult {
1332 tree: mlfq_tree::SerializedTree,
1333 nr_train: usize,
1335 nr_pids_train: usize,
1337 holdout_len: usize,
1339 mae_tree: f64,
1341 mae_ema: f64,
1343 corr: f64,
1345}
1346
1347fn split_holdout(samples: &[TreeSample]) -> Result<(&[TreeSample], &[TreeSample]), String> {
1355 if samples.len() >= 20 {
1356 let cut = samples.len() - samples.len() / 10;
1357 Ok(samples.split_at(cut))
1358 } else {
1359 Err(format!(
1360 "training window holds only {} samples; {} are needed for a 90/10 holdout split",
1361 samples.len(),
1362 20
1363 ))
1364 }
1365}
1366
1367fn tree_admit_pid(counts: &mut HashMap<u32, u32>, pid: u32) -> bool {
1372 let c = counts.entry(pid).or_insert(0);
1373 if *c >= MLFQ_TREE_PER_PID_CAP {
1374 return false;
1375 }
1376 *c += 1;
1377 true
1378}
1379
1380fn tree_evict_pid(counts: &mut HashMap<u32, u32>, pid: u32) {
1384 if let Some(c) = counts.get_mut(&pid) {
1385 *c -= 1;
1386 if *c == 0 {
1387 counts.remove(&pid);
1388 }
1389 }
1390}
1391
1392fn tree_distinct_pids(samples: &[TreeSample]) -> usize {
1394 let mut seen = HashSet::new();
1395 for s in samples {
1396 seen.insert(s.pid);
1397 }
1398 seen.len()
1399}
1400
1401#[allow(dead_code)]
1411fn train_model(samples: &[TreeSample]) -> Result<TrainResult, String> {
1412 let mut scratch = FitScratch::new();
1413 train_model_with_scratch(samples, &mut scratch)
1414}
1415
1416fn train_model_with_scratch(
1420 samples: &[TreeSample],
1421 scratch: &mut FitScratch,
1422) -> Result<TrainResult, String> {
1423 let (train, holdout) = split_holdout(samples)?;
1424
1425 let tree = mlfq_tree::fit_with_scratch(
1426 train,
1427 MLFQ_TREE_MAX_DEPTH,
1428 MLFQ_TREE_MIN_LEAF,
1429 MLFQ_TREE_MAX_NODES,
1430 mlfq_tree::DEFAULT_MIN_REL_VAR_REDUCTION,
1431 scratch,
1432 );
1433 mlfq_tree::serialize_validate(&tree).map_err(|e| {
1434 format!("tree failed validation: {e}")
1437 })?;
1438
1439 scratch.actuals.clear();
1442 scratch.actuals.extend(holdout.iter().map(|s| s.label_ns));
1443 let actuals = &scratch.actuals;
1444 scratch.preds.clear();
1445 for s in holdout {
1446 let feats = s.feats;
1447 scratch.preds.push(mlfq_tree::predict(&tree, &feats));
1448 }
1449 let preds = &scratch.preds;
1450 scratch.weights_full.clear();
1453 mlfq_tree::sample_weights_into(samples.len(), &mut scratch.weights_full);
1454 let holdout_weights = &scratch.weights_full[train.len()..];
1455 scratch.ema_preds.clear();
1456 scratch
1457 .ema_preds
1458 .extend(holdout.iter().map(|s| s.feats.ema));
1459 let ema_preds = &scratch.ema_preds;
1460 let mae_tree = mlfq_tree::weighted_holdout_mae(preds, actuals, holdout_weights);
1461 let mae_ema = mlfq_tree::weighted_holdout_mae(ema_preds, actuals, holdout_weights);
1462 let corr = mlfq_tree::pearson(preds, actuals);
1463
1464 Ok(TrainResult {
1465 tree,
1466 nr_train: train.len(),
1467 nr_pids_train: tree_distinct_pids(train),
1468 holdout_len: holdout.len(),
1469 mae_tree,
1470 mae_ema,
1471 corr,
1472 })
1473}
1474
1475fn spawn_train_worker() -> (
1484 crossbeam::channel::Sender<Vec<TreeSample>>,
1485 crossbeam::channel::Receiver<Result<TrainResult, anyhow::Error>>,
1486) {
1487 let (job_tx, job_rx) = crossbeam::channel::bounded::<Vec<TreeSample>>(1);
1488 let (res_tx, res_rx) = crossbeam::channel::bounded::<Result<TrainResult, anyhow::Error>>(1);
1489 std::thread::spawn(move || {
1490 let mut scratch = FitScratch::new();
1491 while let Ok(samples) = job_rx.recv() {
1492 let res = train_model_with_scratch(&samples, &mut scratch).map_err(anyhow::Error::msg);
1493 if res_tx.send(res).is_err() {
1494 break;
1495 }
1496 }
1497 });
1498 (job_tx, res_rx)
1499}
1500
1501fn main() -> Result<()> {
1502 let opts = Opts::parse();
1503
1504 if let Some(shell) = opts.completions {
1505 generate(
1506 shell,
1507 &mut Opts::command(),
1508 SCHEDULER_NAME,
1509 &mut std::io::stdout(),
1510 );
1511 return Ok(());
1512 }
1513
1514 let monitor_only = opts.monitor.is_some();
1515
1516 if opts.version {
1519 println!("{} {}", SCHEDULER_NAME, full_version());
1520 return Ok(());
1521 }
1522
1523 if opts.help_stats {
1524 stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
1525 return Ok(());
1526 }
1527
1528 if !monitor_only {
1529 simplelog::TermLogger::init(
1530 if opts.debug {
1531 simplelog::LevelFilter::Debug
1532 } else {
1533 simplelog::LevelFilter::Info
1534 },
1535 simplelog::Config::default(),
1536 simplelog::TerminalMode::Stderr,
1537 simplelog::ColorChoice::Auto,
1538 )?;
1539
1540 let smt_suffix = match topology::smt_enabled() {
1543 Some(true) => " SMT on",
1544 Some(false) => " SMT off",
1545 None => "",
1546 };
1547 info!("{} {}{}", SCHEDULER_NAME, full_version(), smt_suffix);
1548 info!(
1549 "scheduler options: {}",
1550 std::env::args().skip(1).collect::<Vec<_>>().join(" ")
1551 );
1552 }
1553
1554 let shutdown = Arc::new(AtomicBool::new(false));
1555 let shutdown_clone = shutdown.clone();
1556
1557 ctrlc::set_handler(move || {
1558 shutdown_clone.store(true, Ordering::Relaxed);
1559 })?;
1560
1561 if let Some(intv) = opts.monitor.or(opts.stats) {
1562 let monitor_shutdown = shutdown.clone();
1563 let jh = std::thread::spawn(move || {
1564 if let Err(err) = stats::monitor(Duration::from_secs_f64(intv), monitor_shutdown) {
1565 log::warn!("stats monitor thread finished with error: {err}");
1566 }
1567 });
1568
1569 if monitor_only {
1570 let _ = jh.join();
1571 return Ok(());
1572 }
1573 }
1574
1575 let mut open_object = MaybeUninit::<libbpf_rs::OpenObject>::uninit();
1576 loop {
1577 let mut sched = Scheduler::init(&opts, &mut open_object, shutdown.clone())?;
1578 if !sched.run(shutdown.clone())?.should_restart() {
1579 break;
1580 }
1581 std::thread::sleep(Duration::from_millis(100));
1584 }
1585
1586 webui::restore_loader_sandbox();
1596
1597 info!("Scheduler exited");
1598
1599 Ok(())
1600}
1601
1602#[cfg(test)]
1603mod math_test {
1604 use std::process::Command;
1605
1606 fn host_cc() -> String {
1607 if let Ok(cc) = std::env::var("CC") {
1608 return cc;
1609 }
1610 for cand in ["clang", "cc", "gcc"] {
1612 if Command::new(cand).arg("--version").status().is_ok() {
1613 return cand.to_string();
1614 }
1615 }
1616 "cc".to_string()
1617 }
1618
1619 #[test]
1620 fn mlfq_pure_math_native_harness() {
1621 let manifest_dir = env!("CARGO_MANIFEST_DIR");
1622 let src = format!("{manifest_dir}/src/bpf/mlfq_math_test.c");
1623 let intf_dir = format!("{manifest_dir}/src/bpf");
1624 let out_dir = std::env::temp_dir().join("scx_mlfq_math_test");
1625 std::fs::create_dir_all(&out_dir).unwrap();
1626 let exe = out_dir.join("mlfq_math_test");
1627
1628 let status = Command::new(host_cc())
1629 .args(["-O2", "-Wall", "-Werror", "-std=c11"])
1630 .args(["-I", &intf_dir])
1631 .arg(&src)
1632 .args(["-o", exe.to_str().unwrap()])
1633 .status()
1634 .expect("failed to compile mlfq_math_test.c");
1635
1636 assert!(status.success(), "native harness failed to compile");
1637
1638 let output = Command::new(&exe)
1639 .output()
1640 .expect("failed to run the native harness");
1641
1642 let stdout = String::from_utf8_lossy(&output.stdout);
1643 print!("{stdout}");
1644
1645 assert!(
1646 output.status.success(),
1647 "native harness failed:\n{stdout}{}",
1648 String::from_utf8_lossy(&output.stderr)
1649 );
1650 assert!(
1651 stdout.contains("All tests passed"),
1652 "native harness did not report success"
1653 );
1654 }
1655
1656 fn mk_sample(pid: u32, label_ns: u64) -> crate::mlfq_tree::TreeSample {
1659 crate::mlfq_tree::TreeSample {
1660 pid,
1661 version: crate::mlfq_tree::MLFQ_TREE_SAMPLE_VERSION,
1662 queue: 1,
1663 feats: crate::mlfq_tree::TreeFeats::default(),
1664 label_ns,
1665 }
1666 }
1667
1668 #[test]
1669 fn split_holdout_cut_and_rejection() {
1670 let samples: Vec<_> = (0..100).map(|i| mk_sample(i as u32, i as u64)).collect();
1671
1672 let (train, holdout) = crate::split_holdout(&samples).unwrap();
1674 assert_eq!(train.len(), 90);
1675 assert_eq!(holdout.len(), 10);
1676 let first_train = train[0].label_ns;
1677 let first_holdout = holdout[0].label_ns;
1678 assert_eq!(first_train, 0);
1679 assert_eq!(first_holdout, 90);
1680
1681 let (train, holdout) = crate::split_holdout(&samples[..20]).unwrap();
1683 assert_eq!(train.len(), 18);
1684 assert_eq!(holdout.len(), 2);
1685
1686 assert!(crate::split_holdout(&samples[..19]).is_err());
1689 assert!(crate::split_holdout(&[]).is_err());
1690 }
1691
1692 #[test]
1693 fn pid_count_cap_evict_and_prune() {
1694 let mut counts = std::collections::HashMap::new();
1695
1696 for _ in 0..crate::MLFQ_TREE_PER_PID_CAP {
1698 assert!(crate::tree_admit_pid(&mut counts, 7));
1699 }
1700 assert!(!crate::tree_admit_pid(&mut counts, 7));
1702 assert!(crate::tree_admit_pid(&mut counts, 8));
1704
1705 crate::tree_evict_pid(&mut counts, 7);
1707 assert!(crate::tree_admit_pid(&mut counts, 7));
1708
1709 for _ in 0..crate::MLFQ_TREE_PER_PID_CAP {
1712 crate::tree_evict_pid(&mut counts, 7);
1713 }
1714 assert!(!counts.contains_key(&7));
1715 assert_eq!(counts.get(&8), Some(&1));
1716 }
1717
1718 #[test]
1719 fn distinct_pids_concentration_gate() {
1720 assert_eq!(crate::tree_distinct_pids(&[]), 0);
1721 assert_eq!(
1722 crate::tree_distinct_pids(&[mk_sample(1, 0), mk_sample(1, 1), mk_sample(2, 2)]),
1723 2
1724 );
1725
1726 let few: Vec<_> = (0..crate::MLFQ_TREE_MIN_PIDS - 1)
1729 .map(|i| mk_sample(i as u32, i as u64))
1730 .collect();
1731 assert!(crate::tree_distinct_pids(&few) < crate::MLFQ_TREE_MIN_PIDS);
1732 let enough: Vec<_> = (0..crate::MLFQ_TREE_MIN_PIDS)
1733 .map(|i| mk_sample(i as u32, i as u64))
1734 .collect();
1735 assert!(crate::tree_distinct_pids(&enough) >= crate::MLFQ_TREE_MIN_PIDS);
1736 }
1737}