1use std::mem::MaybeUninit;
6
7use anyhow::Result;
8use libbpf_rs::skel::{OpenSkel, SkelBuilder};
9use libbpf_rs::MapCore;
10
11use crate::bpf_skel::*;
12use crate::tuning::{OscillatorState, TuningKnobs};
13use scx_pandemonium::event::EventLog;
14
15const SCX_EXIT_NONE: i32 = 0;
17const SCX_ECODE_RST_MASK: u64 = 1 << 16;
18
19const SCX_DSQ_FLAG_BUILTIN: u64 = 1u64 << 63;
21const SCX_DSQ_FLAG_LOCAL_ON: u64 = 1u64 << 62;
22
23#[repr(C)]
25#[derive(Default, Clone, Copy)]
26pub struct PandemoniumStats {
27 pub nr_dispatches: u64,
28 pub nr_idle_hits: u64,
29 pub nr_shared: u64,
30 pub nr_preempt: u64,
31 pub wake_lat_sum: u64,
32 pub wake_lat_samples: u64,
33 pub nr_keep_running: u64,
34 pub nr_hard_kicks: u64,
35 pub nr_soft_kicks: u64,
36 pub nr_enq_wakeup: u64,
37 pub nr_enq_requeue: u64,
38 pub wake_lat_idle_sum: u64,
39 pub wake_lat_idle_cnt: u64,
40 pub wake_lat_kick_sum: u64,
41 pub wake_lat_kick_cnt: u64,
42 pub nr_l2_hit_batch: u64,
43 pub nr_l2_miss_batch: u64,
44 pub nr_l2_hit_interactive: u64,
45 pub nr_l2_miss_interactive: u64,
46 pub nr_reenqueue: u64,
47 pub batch_sojourn_ns: u64,
48 pub longrun_mode_active: u64,
49 pub nr_overflow_rescue: u64,
50 pub nr_cross_domain: [u64; 8],
53 pub nr_osc_park: u64,
55 pub nr_spill_kick_preempt: u64,
58 pub nr_steal: u64,
63 pub rq_depth_sum: u64,
70 pub rq_depth_samples: u64,
71}
72
73const _: () = assert!(std::mem::size_of::<PandemoniumStats>() == 288);
78const _: () = assert!(std::mem::size_of::<TuningKnobs>() == 64);
81
82const _: () = assert!(crate::bpf_intf::MAX_AFFINITY_CANDIDATES == crate::bpf_intf::MAX_CPUS >> 3);
87
88const KNOBS_PIN: &str = "/sys/fs/bpf/pandemonium/tuning_knobs";
91
92pub struct Scheduler<'a> {
93 skel: MainSkel<'a>,
94 _link: libbpf_rs::Link,
95 pub log: EventLog,
96}
97
98impl<'a> Scheduler<'a> {
99 pub fn init(
100 open_object: &'a mut MaybeUninit<libbpf_rs::OpenObject>,
101 nr_cpus_override: Option<u64>,
102 ) -> Result<Self> {
103 let builder = MainSkelBuilder::default();
105 let mut open_skel = builder.open(open_object)?;
106
107 {
109 let ops = open_skel.struct_ops.pandemonium_ops_mut();
110 let name_field = &mut ops.name;
111 let version_suffix = scx_utils::build_id::ops_version_suffix(env!("CARGO_PKG_VERSION"));
112 let bytes = version_suffix.as_bytes();
113 let mut i = 0;
114 let mut bytes_idx = 0;
115 let mut found_null = false;
116 while i < name_field.len() - 1 {
117 found_null |= name_field[i] == 0;
118 if !found_null {
119 i += 1;
120 continue;
121 }
122 if bytes_idx < bytes.len() {
123 name_field[i] = bytes[bytes_idx] as i8;
124 bytes_idx += 1;
125 } else {
126 break;
127 }
128 i += 1;
129 }
130 name_field[i] = 0;
131 }
132
133 let rodata = open_skel.maps.rodata_data.as_mut().unwrap();
135
136 let possible = libbpf_rs::num_possible_cpus()? as u64;
137 rodata.nr_cpu_ids = nr_cpus_override.unwrap_or(possible);
138
139 rodata.__SCX_DSQ_FLAG_BUILTIN = SCX_DSQ_FLAG_BUILTIN;
141 rodata.__SCX_DSQ_FLAG_LOCAL_ON = SCX_DSQ_FLAG_LOCAL_ON;
142 rodata.__SCX_DSQ_INVALID = SCX_DSQ_FLAG_BUILTIN;
143 rodata.__SCX_DSQ_GLOBAL = SCX_DSQ_FLAG_BUILTIN | 1;
144 rodata.__SCX_DSQ_LOCAL = SCX_DSQ_FLAG_BUILTIN | SCX_DSQ_FLAG_LOCAL_ON;
145 rodata.__SCX_DSQ_LOCAL_ON = SCX_DSQ_FLAG_BUILTIN | SCX_DSQ_FLAG_LOCAL_ON | 1;
146 rodata.__SCX_DSQ_LOCAL_CPU_MASK = 0xFFFFFFFF;
147
148 rodata.__SCX_KICK_IDLE = 1;
150 rodata.__SCX_KICK_PREEMPT = 2;
151 rodata.__SCX_KICK_WAIT = 4;
152
153 let mut skel = open_skel.load()?;
155
156 let link = skel.maps.pandemonium_ops.attach_struct_ops()?;
158
159 let pin_dir = "/sys/fs/bpf/pandemonium";
161 let bpffs_ok = std::fs::create_dir_all(pin_dir).is_ok();
162 if bpffs_ok {
163 std::fs::remove_file(KNOBS_PIN).ok();
164 skel.maps.tuning_knobs_map.pin(KNOBS_PIN).ok();
165
166 let cache_pin = "/sys/fs/bpf/pandemonium/cache_domain";
167 std::fs::remove_file(cache_pin).ok();
168 skel.maps.cache_domain.pin(cache_pin).ok();
169 } else {
170 log_warn!("BPFFS NOT AVAILABLE: map pinning skipped (scheduler still functional)");
171 }
172
173 Ok(Self {
174 skel,
175 _link: link,
176 log: EventLog::new(),
177 })
178 }
179
180 pub fn read_stats_percpu(&self) -> Vec<PandemoniumStats> {
193 let key = 0u32.to_ne_bytes();
194 let percpu_vals = match self
195 .skel
196 .maps
197 .stats_map
198 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
199 {
200 Ok(Some(v)) => v,
201 _ => return Vec::new(),
202 };
203 let mut out = Vec::with_capacity(percpu_vals.len());
204 for cpu_val in &percpu_vals {
205 if cpu_val.len() >= std::mem::size_of::<PandemoniumStats>() {
206 out.push(unsafe {
207 std::ptr::read_unaligned(cpu_val.as_ptr() as *const PandemoniumStats)
208 });
209 }
210 }
211 out
212 }
213
214 pub fn fold_stats(percpu: &[PandemoniumStats]) -> PandemoniumStats {
222 let mut total = PandemoniumStats::default();
223 for stats in percpu {
224 {
225 total.nr_dispatches += stats.nr_dispatches;
226 total.nr_idle_hits += stats.nr_idle_hits;
227 total.nr_shared += stats.nr_shared;
228 total.nr_preempt += stats.nr_preempt;
229 total.wake_lat_sum += stats.wake_lat_sum;
230 total.wake_lat_samples += stats.wake_lat_samples;
231 total.nr_keep_running += stats.nr_keep_running;
232 total.nr_hard_kicks += stats.nr_hard_kicks;
233 total.nr_soft_kicks += stats.nr_soft_kicks;
234 total.nr_enq_wakeup += stats.nr_enq_wakeup;
235 total.nr_enq_requeue += stats.nr_enq_requeue;
236 total.wake_lat_idle_sum += stats.wake_lat_idle_sum;
237 total.wake_lat_idle_cnt += stats.wake_lat_idle_cnt;
238 total.wake_lat_kick_sum += stats.wake_lat_kick_sum;
239 total.wake_lat_kick_cnt += stats.wake_lat_kick_cnt;
240 total.nr_l2_hit_batch += stats.nr_l2_hit_batch;
241 total.nr_l2_miss_batch += stats.nr_l2_miss_batch;
242 total.nr_l2_hit_interactive += stats.nr_l2_hit_interactive;
243 total.nr_l2_miss_interactive += stats.nr_l2_miss_interactive;
244 total.nr_reenqueue += stats.nr_reenqueue;
245 if stats.batch_sojourn_ns > total.batch_sojourn_ns {
246 total.batch_sojourn_ns = stats.batch_sojourn_ns;
247 }
248 if stats.longrun_mode_active > total.longrun_mode_active {
249 total.longrun_mode_active = stats.longrun_mode_active;
250 }
251 total.nr_overflow_rescue += stats.nr_overflow_rescue;
252 for i in 0..8 {
253 total.nr_cross_domain[i] += stats.nr_cross_domain[i];
254 }
255 total.nr_osc_park += stats.nr_osc_park;
256 total.nr_spill_kick_preempt += stats.nr_spill_kick_preempt;
257 total.nr_steal += stats.nr_steal;
258 total.rq_depth_sum += stats.rq_depth_sum;
263 total.rq_depth_samples += stats.rq_depth_samples;
264 }
265 }
266
267 total
268 }
269
270 pub fn read_stats(&self) -> PandemoniumStats {
274 Self::fold_stats(&self.read_stats_percpu())
275 }
276
277 fn broadcast_global_fields(per_cpu: &mut [TuningKnobs]) {
289 let (first, rest) = match per_cpu.split_first_mut() {
290 Some(v) => v,
291 None => return,
292 };
293 for k in rest.iter_mut() {
294 k.topology_tau_ns = first.topology_tau_ns;
295 k.codel_eq_ns = first.codel_eq_ns;
296 k.affinity_mode = first.affinity_mode;
297 }
298 }
299
300 pub fn write_tuning_knobs(&self, knobs: &TuningKnobs) -> Result<()> {
304 let n = libbpf_rs::num_possible_cpus()?;
305 self.write_tuning_knobs_percpu(&vec![*knobs; n])
306 }
307
308 pub fn write_tuning_knobs_percpu(&self, per_cpu: &[TuningKnobs]) -> Result<()> {
313 let n = libbpf_rs::num_possible_cpus()?;
314 let mut vals: Vec<TuningKnobs> = (0..n)
315 .map(|i| per_cpu.get(i).copied().unwrap_or_else(|| per_cpu[0]))
316 .collect();
317 Self::broadcast_global_fields(&mut vals);
318 let key = 0u32.to_ne_bytes();
319 let sz = std::mem::size_of::<TuningKnobs>();
320 let bytes: Vec<Vec<u8>> = vals
321 .iter()
322 .map(|k| unsafe {
323 std::slice::from_raw_parts(k as *const TuningKnobs as *const u8, sz).to_vec()
324 })
325 .collect();
326 self.skel
327 .maps
328 .tuning_knobs_map
329 .update_percpu(&key, &bytes, libbpf_rs::MapFlags::ANY)?;
330 Ok(())
331 }
332
333 pub fn write_topology_fields(&self, tau_ns: u64, codel_eq_ns: u64) -> Result<()> {
339 let mut knobs = self.read_tuning_knobs();
340 knobs.topology_tau_ns = tau_ns;
341 knobs.codel_eq_ns = codel_eq_ns;
342 self.write_tuning_knobs(&knobs)
343 }
344
345 pub fn read_oscillator_state(&self) -> OscillatorState {
356 let bss = match self.skel.maps.bss_data.as_ref() {
357 Some(b) => b,
358 None => return OscillatorState::default(),
359 };
360 let data = match self.skel.maps.data_data.as_ref() {
361 Some(d) => d,
362 None => return OscillatorState::default(),
363 };
364 OscillatorState {
365 codel_target_ns: bss.codel_target_ns,
366 codel_target_floor_ns: bss.codel_target_floor_ns,
367 codel_target_max_ns: data.codel_target_max_ns,
368 home_dist_extra_ns: self.read_reff_value(0, 0) as u64,
372 }
373 }
374
375 pub fn read_tuning_knobs_percpu(&self) -> Vec<TuningKnobs> {
378 let key = 0u32.to_ne_bytes();
379 let vals = match self
380 .skel
381 .maps
382 .tuning_knobs_map
383 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
384 {
385 Ok(Some(v)) => v,
386 _ => return Vec::new(),
387 };
388 let sz = std::mem::size_of::<TuningKnobs>();
389 vals.iter()
390 .filter(|v| v.len() >= sz)
391 .map(|v| unsafe { std::ptr::read_unaligned(v.as_ptr() as *const TuningKnobs) })
392 .collect()
393 }
394
395 pub fn read_tuning_knobs(&self) -> TuningKnobs {
403 self.read_tuning_knobs_percpu()
404 .first()
405 .copied()
406 .unwrap_or_default()
407 }
408
409 pub fn read_wake_lat_hist(&self) -> [[u64; 12]; 3] {
412 let mut result = [[0u64; 12]; 3];
413 for key_idx in 0u32..36 {
414 let key = key_idx.to_ne_bytes();
415 if let Ok(Some(percpu_vals)) = self
416 .skel
417 .maps
418 .wake_lat_hist
419 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
420 {
421 let tier = (key_idx / 12) as usize;
422 let bucket = (key_idx % 12) as usize;
423 for cpu_val in &percpu_vals {
424 if cpu_val.len() >= std::mem::size_of::<u64>() {
425 let val: u64 =
426 unsafe { std::ptr::read_unaligned(cpu_val.as_ptr() as *const u64) };
427 result[tier][bucket] += val;
428 }
429 }
430 }
431 }
432 result
433 }
434
435 pub fn read_sleep_hist(&self) -> [u64; 4] {
438 let mut result = [0u64; 4];
439 for key_idx in 0u32..4 {
440 let key = key_idx.to_ne_bytes();
441 if let Ok(Some(percpu_vals)) = self
442 .skel
443 .maps
444 .sleep_hist
445 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
446 {
447 for cpu_val in &percpu_vals {
448 if cpu_val.len() >= std::mem::size_of::<u64>() {
449 let val: u64 =
450 unsafe { std::ptr::read_unaligned(cpu_val.as_ptr() as *const u64) };
451 result[key_idx as usize] += val;
452 }
453 }
454 }
455 }
456 result
457 }
458
459 pub fn write_cache_domain(&self, cpu: u32, l2_group: u32) -> Result<()> {
461 let key = cpu.to_ne_bytes();
462 let val = l2_group.to_ne_bytes();
463 self.skel
464 .maps
465 .cache_domain
466 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
467 Ok(())
468 }
469
470 pub fn write_cpu_domain(&self, cpu: u32, domain: u32) -> Result<()> {
473 let key = cpu.to_ne_bytes();
474 let val = domain.to_ne_bytes();
475 self.skel
476 .maps
477 .cpu_domain
478 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
479 Ok(())
480 }
481
482 pub fn write_nr_overflow_domains(&mut self, nr_overflow_domains: u32) {
486 if let Some(data) = self.skel.maps.data_data.as_mut() {
487 data.nr_overflow_domains = nr_overflow_domains;
488 }
489 }
490
491 pub fn write_l2_sibling(&self, group_id: u32, slot: u32, cpu: u32) -> Result<()> {
493 let key = (group_id * 8 + slot).to_ne_bytes();
494 let val = cpu.to_ne_bytes();
495 self.skel
496 .maps
497 .l2_siblings
498 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
499 Ok(())
500 }
501
502 pub fn write_affinity_rank(&self, cpu: u32, slot: u32, target_cpu: u32) -> Result<()> {
506 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
510 let key = (cpu * stride + slot).to_ne_bytes();
511 let val = target_cpu.to_ne_bytes();
512 self.skel
513 .maps
514 .affinity_rank
515 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
516 Ok(())
517 }
518
519 pub fn write_reff_value(&self, cpu: u32, slot: u32, value: u32) -> Result<()> {
522 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
523 let key = (cpu * stride + slot).to_ne_bytes();
524 let val = value.to_ne_bytes();
525 self.skel
526 .maps
527 .reff_value
528 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
529 Ok(())
530 }
531
532 pub fn write_spill_depth(&self, cpu: u32, slot: u32, value: u32) -> Result<()> {
537 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
538 let key = (cpu * stride + slot).to_ne_bytes();
539 let val = value.to_ne_bytes();
540 self.skel
541 .maps
542 .spill_depth
543 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
544 Ok(())
545 }
546
547 pub fn write_domain_phi(&self, cpu: u32, slot: u32, value: u32) -> Result<()> {
551 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
552 let key = (cpu * stride + slot).to_ne_bytes();
553 let val = value.to_ne_bytes();
554 self.skel
555 .maps
556 .domain_phi
557 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
558 Ok(())
559 }
560
561 pub fn read_reff_value(&self, cpu: u32, slot: u32) -> u32 {
565 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
566 let key = (cpu * stride + slot).to_ne_bytes();
567 match self
568 .skel
569 .maps
570 .reff_value
571 .lookup(&key, libbpf_rs::MapFlags::ANY)
572 {
573 Ok(Some(bytes)) if bytes.len() >= 4 => {
574 let v = u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
575 if v == u32::MAX {
576 0
577 } else {
578 v
579 }
580 }
581 _ => 0,
582 }
583 }
584
585 pub fn read_exit_info(&self) -> bool {
587 let data = self.skel.maps.data_data.as_ref().unwrap();
588 let kind = data.uei.kind;
589 let exit_code = data.uei.exit_code;
590
591 if kind != SCX_EXIT_NONE {
592 let reason_bytes: &[u8] =
593 unsafe { std::slice::from_raw_parts(data.uei.reason.as_ptr() as *const u8, 128) };
594 let msg_bytes: &[u8] =
595 unsafe { std::slice::from_raw_parts(data.uei.msg.as_ptr() as *const u8, 1024) };
596
597 let reason = std::str::from_utf8(reason_bytes)
598 .unwrap_or("unknown")
599 .trim_end_matches('\0');
600 let msg = std::str::from_utf8(msg_bytes)
601 .unwrap_or("")
602 .trim_end_matches('\0');
603
604 log_warn!("BPF exit: kind={} code={}", kind, exit_code);
605 if !reason.is_empty() {
606 log_warn!("BPF exit reason: {}", reason);
607 }
608 if !msg.is_empty() {
609 log_warn!("BPF exit msg: {}", msg);
610 }
611 }
612
613 (exit_code as u64 & SCX_ECODE_RST_MASK) != 0
614 }
615
616 pub fn exited(&self) -> bool {
617 self.skel.maps.data_data.as_ref().unwrap().uei.kind != SCX_EXIT_NONE
618 }
619}
620
621impl Drop for Scheduler<'_> {
622 fn drop(&mut self) {
623 let _ = self.skel.maps.tuning_knobs_map.unpin(KNOBS_PIN);
624 let _ = self
625 .skel
626 .maps
627 .cache_domain
628 .unpin("/sys/fs/bpf/pandemonium/cache_domain");
629 let _ = std::fs::remove_dir("/sys/fs/bpf/pandemonium");
630 }
631}
632
633#[cfg(test)]
643mod fold_tests {
644 use super::*;
645
646 fn cpu(dispatches: u64, sojourn: u64, longrun: u64, xdom: [u64; 8]) -> PandemoniumStats {
647 let mut s = PandemoniumStats::default();
648 s.nr_dispatches = dispatches;
649 s.batch_sojourn_ns = sojourn;
650 s.longrun_mode_active = longrun;
651 s.nr_cross_domain = xdom;
652 s
653 }
654
655 #[test]
656 fn counters_sum_across_cpus() {
657 let cpus = [
658 cpu(10, 0, 0, [0; 8]),
659 cpu(7, 0, 0, [0; 8]),
660 cpu(3, 0, 0, [0; 8]),
661 ];
662 assert_eq!(Scheduler::fold_stats(&cpus).nr_dispatches, 20);
663 }
664
665 #[test]
666 fn sojourn_takes_the_max_not_the_sum() {
667 let cpus = [
671 cpu(0, 100, 0, [0; 8]),
672 cpu(0, 500, 0, [0; 8]),
673 cpu(0, 300, 0, [0; 8]),
674 ];
675 assert_eq!(Scheduler::fold_stats(&cpus).batch_sojourn_ns, 500);
676 }
677
678 #[test]
679 fn longrun_flag_takes_the_max_not_the_sum() {
680 let cpus = [
684 cpu(0, 0, 1, [0; 8]),
685 cpu(0, 0, 1, [0; 8]),
686 cpu(0, 0, 0, [0; 8]),
687 ];
688 assert_eq!(Scheduler::fold_stats(&cpus).longrun_mode_active, 1);
689 }
690
691 #[test]
692 fn cross_domain_folds_elementwise_per_path() {
693 let mut a = [0u64; 8];
694 let mut b = [0u64; 8];
695 a[0] = 5;
696 a[3] = 2;
697 b[0] = 1;
698 b[7] = 9;
699 let got = Scheduler::fold_stats(&[cpu(0, 0, 0, a), cpu(0, 0, 0, b)]).nr_cross_domain;
700 assert_eq!(got[0], 6, "path 0 must sum across CPUs");
701 assert_eq!(got[3], 2);
702 assert_eq!(got[7], 9);
703 assert_eq!(got[1], 0, "an untouched path stays zero");
704 }
705
706 #[test]
707 fn empty_array_folds_to_default() {
708 let got = Scheduler::fold_stats(&[]);
711 assert_eq!(got.nr_dispatches, 0);
712 assert_eq!(got.batch_sojourn_ns, 0);
713 }
714}
715
716#[cfg(test)]
725mod knob_broadcast_tests {
726 use super::*;
727
728 fn knobs(slice: u64, tau: u64) -> TuningKnobs {
729 let mut k = TuningKnobs::default();
730 k.slice_ns = slice;
731 k.topology_tau_ns = tau;
732 k
733 }
734
735 #[test]
736 fn global_fields_are_broadcast_from_slot_zero() {
737 let mut v = vec![knobs(100, 7_000), knobs(200, 9_999), knobs(300, 1)];
738 Scheduler::broadcast_global_fields(&mut v);
739 for (i, k) in v.iter().enumerate() {
740 assert_eq!(k.topology_tau_ns, 7_000, "slot {i} diverged on tau");
741 }
742 }
743
744 #[test]
745 fn per_cpu_fields_survive_the_broadcast() {
746 let mut v = vec![knobs(100, 7_000), knobs(200, 0), knobs(300, 0)];
750 Scheduler::broadcast_global_fields(&mut v);
751 assert_eq!(v[0].slice_ns, 100);
752 assert_eq!(v[1].slice_ns, 200);
753 assert_eq!(v[2].slice_ns, 300);
754 }
755
756 #[test]
757 fn uniform_input_stays_uniform() {
758 let mut v = vec![knobs(100, 7_000); 8];
761 let before = v.clone();
762 Scheduler::broadcast_global_fields(&mut v);
763 for (a, b) in before.iter().zip(v.iter()) {
764 assert_eq!(a.slice_ns, b.slice_ns);
765 assert_eq!(a.topology_tau_ns, b.topology_tau_ns);
766 }
767 }
768
769 #[test]
770 fn empty_and_single_slot_are_no_ops() {
771 let mut none: Vec<TuningKnobs> = Vec::new();
772 Scheduler::broadcast_global_fields(&mut none);
773 let mut one = vec![knobs(100, 7_000)];
774 Scheduler::broadcast_global_fields(&mut one);
775 assert_eq!(one[0].slice_ns, 100);
776 }
777}