1use std::mem::MaybeUninit;
6
7use anyhow::Result;
8use libbpf_rs::MapCore;
9use libbpf_rs::skel::{OpenSkel, SkelBuilder};
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 nr_kick_declined: u64,
75 pub nr_stay_cost_held: u64,
76 pub nr_stay_move_taken: u64,
77 pub rq_depth_sum: u64,
84 pub rq_depth_samples: u64,
85}
86
87const _: () = assert!(std::mem::size_of::<PandemoniumStats>() == 312);
93const _: () = assert!(std::mem::size_of::<TuningKnobs>() == 64);
96
97const _: () = assert!(crate::bpf_intf::MAX_AFFINITY_CANDIDATES == crate::bpf_intf::MAX_CPUS >> 3);
102
103const KNOBS_PIN: &str = "/sys/fs/bpf/pandemonium/tuning_knobs";
106
107pub struct Scheduler<'a> {
108 skel: MainSkel<'a>,
109 _link: libbpf_rs::Link,
110 pub log: EventLog,
111}
112
113impl<'a> Scheduler<'a> {
114 pub fn init(
115 open_object: &'a mut MaybeUninit<libbpf_rs::OpenObject>,
116 nr_cpus_override: Option<u64>,
117 ) -> Result<Self> {
118 let builder = MainSkelBuilder::default();
120 let mut open_skel = builder.open(open_object)?;
121
122 {
124 let ops = open_skel.struct_ops.pandemonium_ops_mut();
125 let name_field = &mut ops.name;
126 let version_suffix = scx_utils::build_id::ops_version_suffix(env!("CARGO_PKG_VERSION"));
127 let bytes = version_suffix.as_bytes();
128 let mut i = 0;
129 let mut bytes_idx = 0;
130 let mut found_null = false;
131 while i < name_field.len() - 1 {
132 found_null |= name_field[i] == 0;
133 if !found_null {
134 i += 1;
135 continue;
136 }
137 if bytes_idx < bytes.len() {
138 name_field[i] = bytes[bytes_idx] as i8;
139 bytes_idx += 1;
140 } else {
141 break;
142 }
143 i += 1;
144 }
145 name_field[i] = 0;
146 }
147
148 let rodata = open_skel.maps.rodata_data.as_mut().unwrap();
150
151 let possible = libbpf_rs::num_possible_cpus()? as u64;
152 rodata.nr_cpu_ids = nr_cpus_override.unwrap_or(possible);
153
154 rodata.__SCX_DSQ_FLAG_BUILTIN = SCX_DSQ_FLAG_BUILTIN;
156 rodata.__SCX_DSQ_FLAG_LOCAL_ON = SCX_DSQ_FLAG_LOCAL_ON;
157 rodata.__SCX_DSQ_INVALID = SCX_DSQ_FLAG_BUILTIN;
158 rodata.__SCX_DSQ_GLOBAL = SCX_DSQ_FLAG_BUILTIN | 1;
159 rodata.__SCX_DSQ_LOCAL = SCX_DSQ_FLAG_BUILTIN | SCX_DSQ_FLAG_LOCAL_ON;
160 rodata.__SCX_DSQ_LOCAL_ON = SCX_DSQ_FLAG_BUILTIN | SCX_DSQ_FLAG_LOCAL_ON | 1;
161 rodata.__SCX_DSQ_LOCAL_CPU_MASK = 0xFFFFFFFF;
162
163 rodata.__SCX_KICK_IDLE = 1;
165 rodata.__SCX_KICK_PREEMPT = 2;
166 rodata.__SCX_KICK_WAIT = 4;
167
168 let mut skel = open_skel.load()?;
170
171 let link = skel.maps.pandemonium_ops.attach_struct_ops()?;
173
174 let pin_dir = "/sys/fs/bpf/pandemonium";
176 let bpffs_ok = std::fs::create_dir_all(pin_dir).is_ok();
177 if bpffs_ok {
178 std::fs::remove_file(KNOBS_PIN).ok();
179 skel.maps.tuning_knobs_map.pin(KNOBS_PIN).ok();
180
181 let cache_pin = "/sys/fs/bpf/pandemonium/cache_domain";
182 std::fs::remove_file(cache_pin).ok();
183 skel.maps.cache_domain.pin(cache_pin).ok();
184 } else {
185 log_warn!("BPFFS NOT AVAILABLE: map pinning skipped (scheduler still functional)");
186 }
187
188 Ok(Self {
189 skel,
190 _link: link,
191 log: EventLog::new(),
192 })
193 }
194
195 pub fn read_stats_percpu(&self) -> Vec<PandemoniumStats> {
208 let key = 0u32.to_ne_bytes();
209 let percpu_vals = match self
210 .skel
211 .maps
212 .stats_map
213 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
214 {
215 Ok(Some(v)) => v,
216 _ => return Vec::new(),
217 };
218 let mut out = Vec::with_capacity(percpu_vals.len());
219 for cpu_val in &percpu_vals {
220 if cpu_val.len() >= std::mem::size_of::<PandemoniumStats>() {
221 out.push(unsafe {
222 std::ptr::read_unaligned(cpu_val.as_ptr() as *const PandemoniumStats)
223 });
224 }
225 }
226 out
227 }
228
229 pub fn fold_stats(percpu: &[PandemoniumStats]) -> PandemoniumStats {
237 let mut total = PandemoniumStats::default();
238 for stats in percpu {
239 {
240 total.nr_dispatches += stats.nr_dispatches;
241 total.nr_idle_hits += stats.nr_idle_hits;
242 total.nr_shared += stats.nr_shared;
243 total.nr_preempt += stats.nr_preempt;
244 total.wake_lat_sum += stats.wake_lat_sum;
245 total.wake_lat_samples += stats.wake_lat_samples;
246 total.nr_keep_running += stats.nr_keep_running;
247 total.nr_hard_kicks += stats.nr_hard_kicks;
248 total.nr_soft_kicks += stats.nr_soft_kicks;
249 total.nr_enq_wakeup += stats.nr_enq_wakeup;
250 total.nr_enq_requeue += stats.nr_enq_requeue;
251 total.wake_lat_idle_sum += stats.wake_lat_idle_sum;
252 total.wake_lat_idle_cnt += stats.wake_lat_idle_cnt;
253 total.wake_lat_kick_sum += stats.wake_lat_kick_sum;
254 total.wake_lat_kick_cnt += stats.wake_lat_kick_cnt;
255 total.nr_l2_hit_batch += stats.nr_l2_hit_batch;
256 total.nr_l2_miss_batch += stats.nr_l2_miss_batch;
257 total.nr_l2_hit_interactive += stats.nr_l2_hit_interactive;
258 total.nr_l2_miss_interactive += stats.nr_l2_miss_interactive;
259 total.nr_reenqueue += stats.nr_reenqueue;
260 if stats.batch_sojourn_ns > total.batch_sojourn_ns {
261 total.batch_sojourn_ns = stats.batch_sojourn_ns;
262 }
263 if stats.longrun_mode_active > total.longrun_mode_active {
264 total.longrun_mode_active = stats.longrun_mode_active;
265 }
266 total.nr_overflow_rescue += stats.nr_overflow_rescue;
267 for i in 0..8 {
268 total.nr_cross_domain[i] += stats.nr_cross_domain[i];
269 }
270 total.nr_osc_park += stats.nr_osc_park;
271 total.nr_spill_kick_preempt += stats.nr_spill_kick_preempt;
272 total.nr_steal += stats.nr_steal;
273 total.nr_kick_declined += stats.nr_kick_declined;
274 total.nr_stay_cost_held += stats.nr_stay_cost_held;
275 total.nr_stay_move_taken += stats.nr_stay_move_taken;
276 total.rq_depth_sum += stats.rq_depth_sum;
281 total.rq_depth_samples += stats.rq_depth_samples;
282 }
283 }
284
285 total
286 }
287
288 pub fn read_stats(&self) -> PandemoniumStats {
292 Self::fold_stats(&self.read_stats_percpu())
293 }
294
295 fn broadcast_global_fields(per_cpu: &mut [TuningKnobs]) {
307 let (first, rest) = match per_cpu.split_first_mut() {
308 Some(v) => v,
309 None => return,
310 };
311 for k in rest.iter_mut() {
312 k.topology_tau_ns = first.topology_tau_ns;
313 k.codel_eq_ns = first.codel_eq_ns;
314 k.affinity_mode = first.affinity_mode;
315 }
316 }
317
318 pub fn write_tuning_knobs(&self, knobs: &TuningKnobs) -> Result<()> {
322 let n = libbpf_rs::num_possible_cpus()?;
323 self.write_tuning_knobs_percpu(&vec![*knobs; n])
324 }
325
326 pub fn write_tuning_knobs_percpu(&self, per_cpu: &[TuningKnobs]) -> Result<()> {
331 let n = libbpf_rs::num_possible_cpus()?;
332 let mut vals: Vec<TuningKnobs> = (0..n)
333 .map(|i| per_cpu.get(i).copied().unwrap_or_else(|| per_cpu[0]))
334 .collect();
335 Self::broadcast_global_fields(&mut vals);
336 let key = 0u32.to_ne_bytes();
337 let sz = std::mem::size_of::<TuningKnobs>();
338 let bytes: Vec<Vec<u8>> = vals
339 .iter()
340 .map(|k| unsafe {
341 std::slice::from_raw_parts(k as *const TuningKnobs as *const u8, sz).to_vec()
342 })
343 .collect();
344 self.skel
345 .maps
346 .tuning_knobs_map
347 .update_percpu(&key, &bytes, libbpf_rs::MapFlags::ANY)?;
348 Ok(())
349 }
350
351 pub fn write_topology_fields(&self, tau_ns: u64, codel_eq_ns: u64) -> Result<()> {
357 let mut knobs = self.read_tuning_knobs();
358 knobs.topology_tau_ns = tau_ns;
359 knobs.codel_eq_ns = codel_eq_ns;
360 self.write_tuning_knobs(&knobs)
361 }
362
363 pub fn read_oscillator_state(&self) -> OscillatorState {
374 let bss = match self.skel.maps.bss_data.as_ref() {
375 Some(b) => b,
376 None => return OscillatorState::default(),
377 };
378 let data = match self.skel.maps.data_data.as_ref() {
379 Some(d) => d,
380 None => return OscillatorState::default(),
381 };
382 OscillatorState {
383 codel_target_ns: bss.codel_target_ns,
384 codel_target_floor_ns: bss.codel_target_floor_ns,
385 codel_target_max_ns: data.codel_target_max_ns,
386 home_dist_extra_ns: self.read_reff_value(0, 0) as u64,
390 }
391 }
392
393 pub fn read_tuning_knobs_percpu(&self) -> Vec<TuningKnobs> {
396 let key = 0u32.to_ne_bytes();
397 let vals = match self
398 .skel
399 .maps
400 .tuning_knobs_map
401 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
402 {
403 Ok(Some(v)) => v,
404 _ => return Vec::new(),
405 };
406 let sz = std::mem::size_of::<TuningKnobs>();
407 vals.iter()
408 .filter(|v| v.len() >= sz)
409 .map(|v| unsafe { std::ptr::read_unaligned(v.as_ptr() as *const TuningKnobs) })
410 .collect()
411 }
412
413 pub fn read_tuning_knobs(&self) -> TuningKnobs {
421 self.read_tuning_knobs_percpu()
422 .first()
423 .copied()
424 .unwrap_or_default()
425 }
426
427 pub fn read_wake_lat_hist(&self) -> [[u64; 12]; 3] {
430 let mut result = [[0u64; 12]; 3];
431 for key_idx in 0u32..36 {
432 let key = key_idx.to_ne_bytes();
433 if let Ok(Some(percpu_vals)) = self
434 .skel
435 .maps
436 .wake_lat_hist
437 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
438 {
439 let tier = (key_idx / 12) as usize;
440 let bucket = (key_idx % 12) as usize;
441 for cpu_val in &percpu_vals {
442 if cpu_val.len() >= std::mem::size_of::<u64>() {
443 let val: u64 =
444 unsafe { std::ptr::read_unaligned(cpu_val.as_ptr() as *const u64) };
445 result[tier][bucket] += val;
446 }
447 }
448 }
449 }
450 result
451 }
452
453 pub fn read_sleep_hist(&self) -> [u64; 4] {
456 let mut result = [0u64; 4];
457 for key_idx in 0u32..4 {
458 let key = key_idx.to_ne_bytes();
459 if let Ok(Some(percpu_vals)) = self
460 .skel
461 .maps
462 .sleep_hist
463 .lookup_percpu(&key, libbpf_rs::MapFlags::ANY)
464 {
465 for cpu_val in &percpu_vals {
466 if cpu_val.len() >= std::mem::size_of::<u64>() {
467 let val: u64 =
468 unsafe { std::ptr::read_unaligned(cpu_val.as_ptr() as *const u64) };
469 result[key_idx as usize] += val;
470 }
471 }
472 }
473 }
474 result
475 }
476
477 pub fn write_cache_domain(&self, cpu: u32, l2_group: u32) -> Result<()> {
479 let key = cpu.to_ne_bytes();
480 let val = l2_group.to_ne_bytes();
481 self.skel
482 .maps
483 .cache_domain
484 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
485 Ok(())
486 }
487
488 pub fn write_cpu_domain(&self, cpu: u32, domain: u32) -> Result<()> {
491 let key = cpu.to_ne_bytes();
492 let val = domain.to_ne_bytes();
493 self.skel
494 .maps
495 .cpu_domain
496 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
497 Ok(())
498 }
499
500 pub fn write_nr_overflow_domains(&mut self, nr_overflow_domains: u32) {
504 if let Some(data) = self.skel.maps.data_data.as_mut() {
505 data.nr_overflow_domains = nr_overflow_domains;
506 }
507 }
508
509 pub fn write_affinity_domain_peers(&mut self, peers: u32) {
514 if let Some(data) = self.skel.maps.data_data.as_mut() {
515 data.affinity_domain_peers = peers;
516 }
517 }
518
519 pub fn write_l2_sibling(&self, group_id: u32, slot: u32, cpu: u32) -> Result<()> {
521 let key = (group_id * 8 + slot).to_ne_bytes();
522 let val = cpu.to_ne_bytes();
523 self.skel
524 .maps
525 .l2_siblings
526 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
527 Ok(())
528 }
529
530 pub fn write_affinity_rank(&self, cpu: u32, slot: u32, target_cpu: u32) -> Result<()> {
534 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
538 let key = (cpu * stride + slot).to_ne_bytes();
539 let val = target_cpu.to_ne_bytes();
540 self.skel
541 .maps
542 .affinity_rank
543 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
544 Ok(())
545 }
546
547 pub fn write_reff_value(&self, cpu: u32, slot: u32, value: u32) -> Result<()> {
550 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
551 let key = (cpu * stride + slot).to_ne_bytes();
552 let val = value.to_ne_bytes();
553 self.skel
554 .maps
555 .reff_value
556 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
557 Ok(())
558 }
559
560 pub fn write_domain_phi(&self, cpu: u32, slot: u32, value: u32) -> Result<()> {
564 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
565 let key = (cpu * stride + slot).to_ne_bytes();
566 let val = value.to_ne_bytes();
567 self.skel
568 .maps
569 .domain_phi
570 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
571 Ok(())
572 }
573
574 pub fn write_reff_frac(&self, cpu: u32, slot: u32, value: u32) -> Result<()> {
580 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
581 let key = (cpu * stride + slot).to_ne_bytes();
582 let val = value.to_ne_bytes();
583 self.skel
584 .maps
585 .reff_frac
586 .update(&key, &val, libbpf_rs::MapFlags::ANY)?;
587 Ok(())
588 }
589
590 pub fn read_reff_value(&self, cpu: u32, slot: u32) -> u32 {
594 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES;
595 let key = (cpu * stride + slot).to_ne_bytes();
596 match self
597 .skel
598 .maps
599 .reff_value
600 .lookup(&key, libbpf_rs::MapFlags::ANY)
601 {
602 Ok(Some(bytes)) if bytes.len() >= 4 => {
603 let v = u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
604 if v == u32::MAX { 0 } else { v }
605 }
606 _ => 0,
607 }
608 }
609
610 pub fn read_exit_info(&self) -> bool {
612 let data = self.skel.maps.data_data.as_ref().unwrap();
613 let kind = data.uei.kind;
614 let exit_code = data.uei.exit_code;
615
616 if kind != SCX_EXIT_NONE {
617 let reason_bytes: &[u8] =
618 unsafe { std::slice::from_raw_parts(data.uei.reason.as_ptr() as *const u8, 128) };
619 let msg_bytes: &[u8] =
620 unsafe { std::slice::from_raw_parts(data.uei.msg.as_ptr() as *const u8, 1024) };
621
622 let reason = std::str::from_utf8(reason_bytes)
623 .unwrap_or("unknown")
624 .trim_end_matches('\0');
625 let msg = std::str::from_utf8(msg_bytes)
626 .unwrap_or("")
627 .trim_end_matches('\0');
628
629 log_warn!("BPF exit: kind={} code={}", kind, exit_code);
630 if !reason.is_empty() {
631 log_warn!("BPF exit reason: {}", reason);
632 }
633 if !msg.is_empty() {
634 log_warn!("BPF exit msg: {}", msg);
635 }
636 }
637
638 (exit_code as u64 & SCX_ECODE_RST_MASK) != 0
639 }
640
641 pub fn exited(&self) -> bool {
642 self.skel.maps.data_data.as_ref().unwrap().uei.kind != SCX_EXIT_NONE
643 }
644}
645
646impl Drop for Scheduler<'_> {
647 fn drop(&mut self) {
648 let _ = self.skel.maps.tuning_knobs_map.unpin(KNOBS_PIN);
649 let _ = self
650 .skel
651 .maps
652 .cache_domain
653 .unpin("/sys/fs/bpf/pandemonium/cache_domain");
654 let _ = std::fs::remove_dir("/sys/fs/bpf/pandemonium");
655 }
656}