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::size_of;
40use std::mem::MaybeUninit;
41use std::os::fd::AsFd;
42use std::os::fd::AsRawFd;
43use std::sync::atomic::AtomicBool;
44use std::sync::atomic::Ordering;
45use std::sync::Arc;
46use std::time::Duration;
47
48use anyhow::Result;
49use clap::CommandFactory;
50use clap::Parser;
51use clap_complete::generate;
52use clap_complete::Shell;
53use crossbeam::channel::RecvTimeoutError;
54use libbpf_rs::AsRawLibbpf;
55use libbpf_rs::MapCore;
56use log::info;
57use scx_stats::prelude::*;
58use scx_utils::build_id;
59use scx_utils::compat;
60use scx_utils::libbpf_clap_opts::LibbpfOpts;
61use scx_utils::pm;
62use scx_utils::scx_ops_attach;
63use scx_utils::scx_ops_load;
64use scx_utils::scx_ops_open;
65use scx_utils::try_set_rlimit_infinity;
66use scx_utils::uei_exited;
67use scx_utils::uei_report;
68use scx_utils::UserExitInfo;
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 if let Some(bss) = skel.maps.bss_data.as_mut() {
440 bss.mlfq_gpu_trace_mask |= mask;
441 }
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, m.q2_placements, m.q3_placements, m.enq_fastpath,
1047 m.enq_regular, m.enq_pinned_idle, m.enq_pinned_busy,
1048 m.enq_pinned_global, m.enq_no_tctx, m.enq_bad_weight,
1049 m.enq_no_deadline, m.promotions, m.demotions, m.aging_boosts,
1050 m.short_sleep_boosts, m.cpuperf_boosts, m.preemption_kicks,
1051 m.total_runtime, m.on_cpu, m.steals, m.steals_same_llc,
1052 m.steals_cross_llc, m.keep_running,
1053 m.rt_takeovers, m.rt_evacuations, m.rt_redirects, m.rt_reenqs,
1054 m.tree_model_generation, m.tree_model_nodes, m.tree_model_samples,
1055 m.tree_mae_tree_us, m.tree_mae_ema_us, self.model.corr,
1056 m.tree_inference, m.tree_fallback, m.tree_disagree,
1057 m.tree_samples_emitted, m.tree_samples_dropped,
1058 m.tree_samples_cap_dropped, m.wakeup_total, m.adapt_steps
1059 );
1060 let _ = self.struct_ops.take();
1061 uei_report!(&self.skel, uei)
1062 }
1063
1064 fn ingest_sample(&mut self, s: TreeSample) {
1073 if !tree_admit_pid(&mut self.pid_counts, s.pid) {
1074 self.tree_samples_cap_dropped += 1;
1075 return;
1076 }
1077 if self.window.len() == MLFQ_TREE_WINDOW_MAX {
1078 let evicted = self
1079 .window
1080 .pop_front()
1081 .expect("a full window pops the oldest sample");
1082 tree_evict_pid(&mut self.pid_counts, evicted.pid);
1083 }
1084 self.window.push_back(s);
1085
1086 let due = match self.last_train_at {
1087 None => self.window.len() >= MLFQ_TREE_MIN_SAMPLES,
1088 Some(t) => {
1089 t.elapsed() >= MLFQ_TREE_RETRAIN_INTERVAL
1090 && self.window.len() >= MLFQ_TREE_MIN_SAMPLES
1091 }
1092 };
1093 if due {
1094 self.kick_training();
1095 }
1096 }
1097
1098 fn kick_training(&mut self) {
1105 self.last_train_at = Some(std::time::Instant::now());
1106 self.train_snapshot_buf.clear();
1110 self.train_snapshot_buf.extend(self.window.iter().copied());
1111 let mut snapshot = Vec::new();
1116 std::mem::swap(&mut snapshot, &mut self.train_snapshot_buf);
1117 self.train_snapshot_buf = Vec::with_capacity(MLFQ_TREE_WINDOW_MAX);
1119 if self.train_tx.try_send(snapshot).is_err() {
1120 log::warn!("MLFQ tree training already in flight, skipping this retrain");
1121 }
1122 }
1123
1124 fn poll_train_results(&mut self) {
1127 while let Ok(res) = self.train_rx.try_recv() {
1128 match res {
1129 Ok(r) => self.apply_train_result(r),
1130 Err(e) => {
1131 log::warn!("MLFQ tree training failed, keeping the previous model: {e:#}");
1132 }
1133 }
1134 }
1135 }
1136
1137 fn apply_train_result(&mut self, res: TrainResult) {
1144 if let Err(e) = mlfq_tree::serialize_validate(&res.tree) {
1145 log::warn!("MLFQ tree training failed validation, keeping the previous model: {e}");
1146 return;
1147 }
1148
1149 let gen = self.model.generation + 1;
1150 if res.nr_pids_train < MLFQ_TREE_MIN_PIDS {
1158 log::info!(
1159 "MLFQ tree gen {} rejected: fit slice has only {} distinct pids (< {} required), keeping the previous model",
1160 gen, res.nr_pids_train, MLFQ_TREE_MIN_PIDS
1161 );
1162 return;
1163 }
1164 let published_corr = if self.model.generation == 0 {
1165 None
1166 } else {
1167 Some(self.model.corr)
1168 };
1169 if !mlfq_tree::should_publish(res.mae_tree, res.mae_ema, res.corr, published_corr) {
1170 log::info!(
1171 "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",
1172 gen,
1173 res.mae_tree / 1e3,
1174 res.mae_ema / 1e3,
1175 res.corr,
1176 published_corr.unwrap_or(0.0)
1177 );
1178 return;
1179 }
1180
1181 if let Err(e) = self.publish_tree(&res.tree, gen) {
1182 log::warn!("MLFQ tree publish failed, keeping the previous model: {e:#}");
1183 return;
1184 }
1185
1186 self.model = ModelMeta {
1187 generation: gen,
1188 nr_samples: res.nr_train,
1189 nr_nodes: res.tree.nodes.len(),
1190 mae_tree_us: (res.mae_tree / 1e3).round() as u64,
1191 mae_ema_us: (res.mae_ema / 1e3).round() as u64,
1192 corr: res.corr,
1193 };
1194 info!(
1195 "MLFQ tree model gen {}, nodes {}, fit {} samples (holdout {}), MAE_tree={}us MAE_ema={}us corr={:.3}",
1196 gen,
1197 res.tree.nodes.len(),
1198 res.nr_train,
1199 res.holdout_len,
1200 self.model.mae_tree_us,
1201 self.model.mae_ema_us,
1202 self.model.corr
1203 );
1204 }
1205
1206 fn publish_tree(&mut self, tree: &mlfq_tree::SerializedTree, gen: u64) -> Result<()> {
1226 let old_meta = {
1227 let bss = self
1228 .skel
1229 .maps
1230 .bss_data
1231 .as_ref()
1232 .expect("bss_data missing, the BPF object has no .bss section");
1233 bss.mlfq_tree_ctrl.meta
1234 };
1235 let old_gen = old_meta >> crate::bpf_intf::MLFQ_TREE_META_GENERATION_SHIFT;
1236 if gen <= old_gen && old_gen != 0 {
1238 anyhow::bail!("monotonic gen violation: new {} <= old {}", gen, old_gen);
1239 }
1240 let old_active = (old_meta >> 1) & 1;
1241 let new_active = 1 - old_active;
1242 let key = (new_active as u32).to_ne_bytes();
1243
1244 let node_bytes = unsafe {
1245 std::slice::from_raw_parts(
1246 tree.nodes.as_ptr().cast::<u8>(),
1247 tree.nodes.len() * size_of::<mlfq_tree::TreeNode>(),
1248 )
1249 };
1250 let mut buf = vec![0u8; size_of::<mlfq_tree_store>()];
1251 buf[..node_bytes.len()].copy_from_slice(node_bytes);
1252 self.skel
1253 .maps
1254 .mlfq_tree_map
1255 .update(&key, &buf, libbpf_rs::MapFlags::ANY)?;
1256
1257 std::sync::atomic::fence(Ordering::Release);
1259
1260 {
1261 let bss = self
1262 .skel
1263 .maps
1264 .bss_data
1265 .as_mut()
1266 .expect("bss_data missing, the BPF object has no .bss section");
1267 bss.mlfq_tree_ctrl.meta = mlfq_tree::tree_meta(gen, tree.nodes.len(), new_active);
1268 }
1269 Ok(())
1270 }
1271}
1272
1273impl Drop for Scheduler<'_> {
1274 fn drop(&mut self) {
1275 if let Some(jh) = self.webui_join.take() {
1288 let _ = jh.join();
1289 }
1290 info!("Unregister {SCHEDULER_NAME} scheduler");
1291 }
1292}
1293
1294struct TrainResult {
1298 tree: mlfq_tree::SerializedTree,
1299 nr_train: usize,
1301 nr_pids_train: usize,
1303 holdout_len: usize,
1305 mae_tree: f64,
1307 mae_ema: f64,
1309 corr: f64,
1311}
1312
1313fn split_holdout(samples: &[TreeSample]) -> Result<(&[TreeSample], &[TreeSample]), String> {
1321 if samples.len() >= 20 {
1322 let cut = samples.len() - samples.len() / 10;
1323 Ok(samples.split_at(cut))
1324 } else {
1325 Err(format!(
1326 "training window holds only {} samples; {} are needed for a 90/10 holdout split",
1327 samples.len(),
1328 20
1329 ))
1330 }
1331}
1332
1333fn tree_admit_pid(counts: &mut HashMap<u32, u32>, pid: u32) -> bool {
1338 let c = counts.entry(pid).or_insert(0);
1339 if *c >= MLFQ_TREE_PER_PID_CAP {
1340 return false;
1341 }
1342 *c += 1;
1343 true
1344}
1345
1346fn tree_evict_pid(counts: &mut HashMap<u32, u32>, pid: u32) {
1350 if let Some(c) = counts.get_mut(&pid) {
1351 *c -= 1;
1352 if *c == 0 {
1353 counts.remove(&pid);
1354 }
1355 }
1356}
1357
1358fn tree_distinct_pids(samples: &[TreeSample]) -> usize {
1360 let mut seen = HashSet::new();
1361 for s in samples {
1362 seen.insert(s.pid);
1363 }
1364 seen.len()
1365}
1366
1367#[allow(dead_code)]
1377fn train_model(samples: &[TreeSample]) -> Result<TrainResult, String> {
1378 let mut scratch = FitScratch::new();
1379 train_model_with_scratch(samples, &mut scratch)
1380}
1381
1382fn train_model_with_scratch(
1386 samples: &[TreeSample],
1387 scratch: &mut FitScratch,
1388) -> Result<TrainResult, String> {
1389 let (train, holdout) = split_holdout(samples)?;
1390
1391 let tree = mlfq_tree::fit_with_scratch(
1392 train,
1393 MLFQ_TREE_MAX_DEPTH,
1394 MLFQ_TREE_MIN_LEAF,
1395 MLFQ_TREE_MAX_NODES,
1396 mlfq_tree::DEFAULT_MIN_REL_VAR_REDUCTION,
1397 scratch,
1398 );
1399 mlfq_tree::serialize_validate(&tree).map_err(|e| {
1400 format!("tree failed validation: {e}")
1403 })?;
1404
1405 scratch.actuals.clear();
1408 scratch.actuals.extend(holdout.iter().map(|s| s.label_ns));
1409 let actuals = &scratch.actuals;
1410 scratch.preds.clear();
1411 for s in holdout {
1412 let feats = s.feats;
1413 scratch.preds.push(mlfq_tree::predict(&tree, &feats));
1414 }
1415 let preds = &scratch.preds;
1416 scratch.weights_full.clear();
1419 mlfq_tree::sample_weights_into(samples.len(), &mut scratch.weights_full);
1420 let holdout_weights = &scratch.weights_full[train.len()..];
1421 scratch.ema_preds.clear();
1422 scratch
1423 .ema_preds
1424 .extend(holdout.iter().map(|s| s.feats.ema));
1425 let ema_preds = &scratch.ema_preds;
1426 let mae_tree = mlfq_tree::weighted_holdout_mae(preds, actuals, holdout_weights);
1427 let mae_ema = mlfq_tree::weighted_holdout_mae(ema_preds, actuals, holdout_weights);
1428 let corr = mlfq_tree::pearson(preds, actuals);
1429
1430 Ok(TrainResult {
1431 tree,
1432 nr_train: train.len(),
1433 nr_pids_train: tree_distinct_pids(train),
1434 holdout_len: holdout.len(),
1435 mae_tree,
1436 mae_ema,
1437 corr,
1438 })
1439}
1440
1441fn spawn_train_worker() -> (
1450 crossbeam::channel::Sender<Vec<TreeSample>>,
1451 crossbeam::channel::Receiver<Result<TrainResult, anyhow::Error>>,
1452) {
1453 let (job_tx, job_rx) = crossbeam::channel::bounded::<Vec<TreeSample>>(1);
1454 let (res_tx, res_rx) = crossbeam::channel::bounded::<Result<TrainResult, anyhow::Error>>(1);
1455 std::thread::spawn(move || {
1456 let mut scratch = FitScratch::new();
1457 while let Ok(samples) = job_rx.recv() {
1458 let res = train_model_with_scratch(&samples, &mut scratch).map_err(anyhow::Error::msg);
1459 if res_tx.send(res).is_err() {
1460 break;
1461 }
1462 }
1463 });
1464 (job_tx, res_rx)
1465}
1466
1467fn main() -> Result<()> {
1468 let opts = Opts::parse();
1469
1470 if let Some(shell) = opts.completions {
1471 generate(
1472 shell,
1473 &mut Opts::command(),
1474 SCHEDULER_NAME,
1475 &mut std::io::stdout(),
1476 );
1477 return Ok(());
1478 }
1479
1480 let monitor_only = opts.monitor.is_some();
1481
1482 if opts.version {
1485 println!("{} {}", SCHEDULER_NAME, full_version());
1486 return Ok(());
1487 }
1488
1489 if opts.help_stats {
1490 stats::server_data().describe_meta(&mut std::io::stdout(), None)?;
1491 return Ok(());
1492 }
1493
1494 if !monitor_only {
1495 simplelog::TermLogger::init(
1496 if opts.debug {
1497 simplelog::LevelFilter::Debug
1498 } else {
1499 simplelog::LevelFilter::Info
1500 },
1501 simplelog::Config::default(),
1502 simplelog::TerminalMode::Stderr,
1503 simplelog::ColorChoice::Auto,
1504 )?;
1505
1506 let smt_suffix = match topology::smt_enabled() {
1509 Some(true) => " SMT on",
1510 Some(false) => " SMT off",
1511 None => "",
1512 };
1513 info!("{} {}{}", SCHEDULER_NAME, full_version(), smt_suffix);
1514 info!(
1515 "scheduler options: {}",
1516 std::env::args().skip(1).collect::<Vec<_>>().join(" ")
1517 );
1518 }
1519
1520 let shutdown = Arc::new(AtomicBool::new(false));
1521 let shutdown_clone = shutdown.clone();
1522
1523 ctrlc::set_handler(move || {
1524 shutdown_clone.store(true, Ordering::Relaxed);
1525 })?;
1526
1527 if let Some(intv) = opts.monitor.or(opts.stats) {
1528 let monitor_shutdown = shutdown.clone();
1529 let jh = std::thread::spawn(move || {
1530 if let Err(err) = stats::monitor(Duration::from_secs_f64(intv), monitor_shutdown) {
1531 log::warn!("stats monitor thread finished with error: {err}");
1532 }
1533 });
1534
1535 if monitor_only {
1536 let _ = jh.join();
1537 return Ok(());
1538 }
1539 }
1540
1541 let mut open_object = MaybeUninit::<libbpf_rs::OpenObject>::uninit();
1542 loop {
1543 let mut sched = Scheduler::init(&opts, &mut open_object, shutdown.clone())?;
1544 if !sched.run(shutdown.clone())?.should_restart() {
1545 break;
1546 }
1547 std::thread::sleep(Duration::from_millis(100));
1550 }
1551
1552 webui::restore_loader_sandbox();
1562
1563 info!("Scheduler exited");
1564
1565 Ok(())
1566}
1567
1568#[cfg(test)]
1569mod math_test {
1570 use std::process::Command;
1571
1572 fn host_cc() -> String {
1573 if let Ok(cc) = std::env::var("CC") {
1574 return cc;
1575 }
1576 for cand in ["clang", "cc", "gcc"] {
1578 if Command::new(cand).arg("--version").status().is_ok() {
1579 return cand.to_string();
1580 }
1581 }
1582 "cc".to_string()
1583 }
1584
1585 #[test]
1586 fn mlfq_pure_math_native_harness() {
1587 let manifest_dir = env!("CARGO_MANIFEST_DIR");
1588 let src = format!("{manifest_dir}/src/bpf/mlfq_math_test.c");
1589 let intf_dir = format!("{manifest_dir}/src/bpf");
1590 let out_dir = std::env::temp_dir().join("scx_mlfq_math_test");
1591 std::fs::create_dir_all(&out_dir).unwrap();
1592 let exe = out_dir.join("mlfq_math_test");
1593
1594 let status = Command::new(host_cc())
1595 .args(["-O2", "-Wall", "-Werror", "-std=c11"])
1596 .args(["-I", &intf_dir])
1597 .arg(&src)
1598 .args(["-o", exe.to_str().unwrap()])
1599 .status()
1600 .expect("failed to compile mlfq_math_test.c");
1601
1602 assert!(status.success(), "native harness failed to compile");
1603
1604 let output = Command::new(&exe)
1605 .output()
1606 .expect("failed to run the native harness");
1607
1608 let stdout = String::from_utf8_lossy(&output.stdout);
1609 print!("{stdout}");
1610
1611 assert!(
1612 output.status.success(),
1613 "native harness failed:\n{stdout}{}",
1614 String::from_utf8_lossy(&output.stderr)
1615 );
1616 assert!(
1617 stdout.contains("All tests passed"),
1618 "native harness did not report success"
1619 );
1620 }
1621
1622 fn mk_sample(pid: u32, label_ns: u64) -> crate::mlfq_tree::TreeSample {
1625 crate::mlfq_tree::TreeSample {
1626 pid,
1627 version: crate::mlfq_tree::MLFQ_TREE_SAMPLE_VERSION,
1628 queue: 1,
1629 feats: crate::mlfq_tree::TreeFeats::default(),
1630 label_ns,
1631 }
1632 }
1633
1634 #[test]
1635 fn split_holdout_cut_and_rejection() {
1636 let samples: Vec<_> = (0..100).map(|i| mk_sample(i as u32, i as u64)).collect();
1637
1638 let (train, holdout) = crate::split_holdout(&samples).unwrap();
1640 assert_eq!(train.len(), 90);
1641 assert_eq!(holdout.len(), 10);
1642 let first_train = train[0].label_ns;
1643 let first_holdout = holdout[0].label_ns;
1644 assert_eq!(first_train, 0);
1645 assert_eq!(first_holdout, 90);
1646
1647 let (train, holdout) = crate::split_holdout(&samples[..20]).unwrap();
1649 assert_eq!(train.len(), 18);
1650 assert_eq!(holdout.len(), 2);
1651
1652 assert!(crate::split_holdout(&samples[..19]).is_err());
1655 assert!(crate::split_holdout(&[]).is_err());
1656 }
1657
1658 #[test]
1659 fn pid_count_cap_evict_and_prune() {
1660 let mut counts = std::collections::HashMap::new();
1661
1662 for _ in 0..crate::MLFQ_TREE_PER_PID_CAP {
1664 assert!(crate::tree_admit_pid(&mut counts, 7));
1665 }
1666 assert!(!crate::tree_admit_pid(&mut counts, 7));
1668 assert!(crate::tree_admit_pid(&mut counts, 8));
1670
1671 crate::tree_evict_pid(&mut counts, 7);
1673 assert!(crate::tree_admit_pid(&mut counts, 7));
1674
1675 for _ in 0..crate::MLFQ_TREE_PER_PID_CAP {
1678 crate::tree_evict_pid(&mut counts, 7);
1679 }
1680 assert!(!counts.contains_key(&7));
1681 assert_eq!(counts.get(&8), Some(&1));
1682 }
1683
1684 #[test]
1685 fn distinct_pids_concentration_gate() {
1686 assert_eq!(crate::tree_distinct_pids(&[]), 0);
1687 assert_eq!(
1688 crate::tree_distinct_pids(&[mk_sample(1, 0), mk_sample(1, 1), mk_sample(2, 2)]),
1689 2
1690 );
1691
1692 let few: Vec<_> = (0..crate::MLFQ_TREE_MIN_PIDS - 1)
1695 .map(|i| mk_sample(i as u32, i as u64))
1696 .collect();
1697 assert!(crate::tree_distinct_pids(&few) < crate::MLFQ_TREE_MIN_PIDS);
1698 let enough: Vec<_> = (0..crate::MLFQ_TREE_MIN_PIDS)
1699 .map(|i| mk_sample(i as u32, i as u64))
1700 .collect();
1701 assert!(crate::tree_distinct_pids(&enough) >= crate::MLFQ_TREE_MIN_PIDS);
1702 }
1703}