1mod bpf_skel;
9mod build_identity;
10mod core_performance;
11pub use bpf_skel::*;
12pub mod bpf_intf;
13pub use bpf_intf::*;
14
15use std::collections::BTreeMap;
16use std::io::IsTerminal;
17use std::mem::MaybeUninit;
18use std::sync::Arc;
19use std::sync::atomic::AtomicBool;
20use std::sync::atomic::AtomicU64;
21use std::sync::atomic::Ordering;
22use std::time::Duration;
23
24use anyhow::Context;
25use anyhow::Result;
26use clap::Parser;
27use libbpf_rs::AsRawLibbpf;
28use libbpf_rs::MapCore;
29use libbpf_rs::MapFlags;
30use libbpf_rs::OpenObject;
31use log::info;
32use log::warn;
33use scx_utils::NR_CPU_IDS;
34use scx_utils::Topology;
35use scx_utils::UserExitInfo;
36use scx_utils::build_id;
37use scx_utils::compat;
38use scx_utils::scx_ops_attach;
39use scx_utils::scx_ops_load;
40use scx_utils::scx_ops_open;
41use scx_utils::try_set_rlimit_infinity;
42use scx_utils::uei_exited;
43use scx_utils::uei_report;
44
45const SCHEDULER_NAME: &str = "scx_cake";
46
47fn configure_acquire_census(skel: &mut OpenBpfSkel<'_>, probe_on: bool) {
50 if !probe_on {
51 skel.struct_ops.cake_ops_mut().cpu_acquire = std::ptr::null_mut();
52 skel.progs.cake_cpu_acquire.set_autoload(false);
53 }
54}
55
56#[derive(Debug, PartialEq, Eq, Parser)]
58#[command(after_help = toggle_help())]
59struct Opts {
60 #[clap(long)]
62 print_topology: bool,
63
64 #[clap(short = 'v', long, action = clap::ArgAction::SetTrue)]
68 verbose: bool,
69
70 #[clap(short = 'V', long, action = clap::ArgAction::SetTrue)]
72 version: bool,
73
74 #[clap(long = "toggle", value_name = "NAME=0|1")]
78 toggle: Vec<String>,
79
80 #[clap(long = "handoff-ns", value_name = "NS")]
83 handoff_ns: Option<u64>,
84}
85
86const TOGGLES: [(&str, &str, u8); 1] = [("probe", "diagnostics", 0)];
90
91fn toggle_help() -> String {
92 let mut s = String::from("Toggles (--toggle NAME=0|1, repeatable):\n");
93 for (name, what, dfl) in TOGGLES {
94 s.push_str(&format!(" {name:<9} {what:<20} default {dfl}\n"));
95 }
96 s
97}
98
99fn attach_irq_pair<L>(
101 leave: impl FnOnce() -> Result<L>,
102 enter: impl FnOnce() -> Result<L>,
103) -> Result<[L; 2]> {
104 let leave = leave().context("exit hook failed")?;
105 let enter = enter().context("entry hook failed")?;
106 Ok([enter, leave])
107}
108
109struct Scheduler<'a> {
110 skel: BpfSkel<'a>,
111 struct_ops: Option<libbpf_rs::Link>,
112 _irq_links: Vec<libbpf_rs::Link>,
114 probe_on: bool,
116 verbose: bool,
119 sinks: SinkMonitor,
121 pref_die: Option<PreferredDie>,
123 x3d_mode: Option<String>,
124}
125
126impl<'a> Scheduler<'a> {
127 fn init(opts: &Opts, open_object: &'a mut MaybeUninit<OpenObject>) -> Result<Self> {
128 try_set_rlimit_infinity();
129
130 let topo = Topology::new().context("failed to read topology")?;
133 let physical = topo.all_cores.len();
134 let total = topo.all_cpus.len();
135
136 let slice_us = bpf_intf::consts_SLICE_NS as u64 / bpf_intf::consts_NSEC_PER_USEC as u64;
137 let queued_wakeup = *compat::SCX_OPS_ALLOW_QUEUED_WAKEUP != 0;
138 let dsq_peek = compat::ksym_exists("scx_bpf_dsq_peek").unwrap_or(false);
139
140 info!(
141 "{} {}",
142 SCHEDULER_NAME,
143 build_id::full_version(env!("CARGO_PKG_VERSION"))
144 );
145 match build_identity::binary_sha256() {
146 Ok(hash) => info!("Build SHA256 {hash}"),
147 Err(err) => warn!("Build SHA256 unavailable: {err:#}"),
148 }
149 info!("CPU {physical} cores / {total} hardware threads");
150 info!(
151 "Cache {} shared last-level cache domain{}",
152 topo.all_llcs.len(),
153 if topo.all_llcs.len() == 1 { "" } else { "s" }
154 );
155
156 let mut skel_builder = BpfSkelBuilder::default();
158 skel_builder.obj_builder.debug(opts.verbose);
159 if !opts.verbose {
162 scx_utils::init_libbpf_logging(Some(libbpf_rs::PrintLevel::Warn));
163 }
164 let mut skel = scx_ops_open!(skel_builder, open_object, cake_ops, None)?;
165 let ret = unsafe {
169 libbpf_rs::libbpf_sys::bpf_map__set_autoattach(
170 skel.maps.cake_ops.as_libbpf_object().as_ptr(),
171 false,
172 )
173 };
174 anyhow::ensure!(ret == 0, "Failed to disable cake_ops auto-attach: {ret}");
175 for prog in [
178 &mut skel.progs.cake_irq_enter,
179 &mut skel.progs.cake_irq_leave,
180 &mut skel.progs.cake_softirq_enter,
181 &mut skel.progs.cake_softirq_leave,
182 &mut skel.progs.cake_cpu_idle,
183 ] {
184 prog.set_autoattach(false);
185 }
186
187 let rodata = skel
191 .maps
192 .rodata_data
193 .as_mut()
194 .context("BPF rodata unavailable for CPU topology")?;
195
196 let span = (*NR_CPU_IDS).max(
200 std::fs::read_to_string("/sys/devices/system/cpu/possible")
201 .ok()
202 .and_then(|t| last_cpu_id(&t))
203 .map_or(0, |last| last + 1),
204 );
205 anyhow::ensure!(
206 span <= bpf_intf::consts_MAX_CPUS as usize,
207 "host nr_cpu_ids {} exceeds Cake's compiled MAX_CPUS {}",
208 span,
209 bpf_intf::consts_MAX_CPUS
210 );
211 rodata.nr_cpu_span = span as u32;
212 let one_word = *NR_CPU_IDS <= 64;
215 rodata.cake_one_word = u8::from(one_word);
216 rodata.cake_tick_ns = coarse_tick_ns();
217 if let Some(ns) = opts.handoff_ns {
218 rodata.cake_handoff_max_ns = ns;
219 warn!(" sweep handoff_max {ns} ns (observe-only override)");
220 }
221 let (idle_driver, idle_states) = cpuidle_states();
222 for (slot, ns) in rodata.cake_idle_state_ns.iter_mut().zip(&idle_states) {
223 *slot = *ns;
224 }
225 rodata.cake_idle_exit_max_ns = idle_states.iter().copied().max().unwrap_or(0);
226 let idle_exit_max_ns = if one_word {
229 rodata.cake_idle_exit_max_ns
230 } else {
231 0
232 };
233 match &idle_driver {
234 Some(d) => info!(
235 "Idle {d}: {} power-saving states, up to {} us exit latency",
236 idle_states.len(),
237 idle_states.iter().copied().max().unwrap_or(0) / 1000
238 ),
239 None => {
240 info!("Idle No CPU idle driver; no idle-exit cost applied")
241 }
242 }
243 if one_word && topo.all_llcs.len() == 1 && topo.has_little_cores() {
246 rodata.cake_cap_tiers = 1;
247 rodata.cake_cap_word = topo
248 .all_cpus
249 .values()
250 .filter(|c| c.core_type != scx_utils::CoreType::Little)
251 .fold(0u64, |w, c| w | (1u64 << c.id));
252 info!(
253 "Hybrid CPU {} high-capacity logical CPUs preferred",
254 rodata.cake_cap_word.count_ones()
255 );
256 }
257 if !one_word {
258 warn!(
259 " span {} CPU ids exceed one census word: claim walk, seats, deep-idle avoidance and die-local pools off, kernel idle pick only",
260 *NR_CPU_IDS
261 );
262 }
263
264 let fields: [&mut u8; 1] = [&mut rodata.cake_tog_probe];
267 let mut slots: Vec<(&str, &str, &mut u8)> = TOGGLES
268 .iter()
269 .zip(fields)
270 .map(|((name, what, dfl), field)| {
271 if *field != *dfl {
272 warn!(" toggle {name} defaults to {} in BPF but {dfl} in TOGGLES; fix the table", *field);
273 }
274 (*name, *what, field)
275 })
276 .collect();
277 let defaults: Vec<u8> = slots.iter().map(|s| *s.2).collect();
278 for spec in &opts.toggle {
279 let Some((name, val)) = spec.split_once('=') else {
282 warn!(" toggle ignored `{spec}`: expected NAME=0|1");
283 continue;
284 };
285 let on = match val {
286 "0" => 0u8,
287 "1" => 1u8,
288 _ => {
289 warn!(" toggle ignored `{spec}`: value must be 0 or 1");
290 continue;
291 }
292 };
293 if let Some(slot) = slots.iter_mut().find(|s| s.0 == name) {
294 *slot.2 = on;
295 } else {
296 warn!(" toggle ignored `{spec}`: no toggle named {name}");
297 }
298 }
299
300 let core_performance = core_performance::CorePerformanceLayout::discover(&topo);
301 let rank_words = core_performance.rank_words();
302 if one_word && rank_words.len() > 1 {
303 rodata.cake_rank_tiers = rank_words.len() as u32;
304 rodata.cpu_perf_known = rank_words.iter().fold(0, |all, word| all | word);
305 for (slot, word) in rodata.cpu_perf_tier.iter_mut().zip(rank_words) {
306 *slot = word;
307 }
308 }
309 if rodata.cake_rank_tiers > 1 {
310 info!(
311 "Placement Platform core preferences enabled ({} levels)",
312 rodata.cake_rank_tiers
313 );
314 } else {
315 info!("Placement No additional core ranking applied");
316 }
317 if opts.verbose {
318 info!("Core order {}", core_performance.summary());
319 }
320 let pref_die = preferred_die(&topo, &core_performance);
321 let x3d_mode = amd_x3d_mode();
322 if let Some(die) = &pref_die {
323 info!("Cache bias Preferred cache domain selected");
324 let w = preferred_word(die, x3d_mode.as_deref());
325 if opts.verbose {
326 info!(
327 "Cache mask {w:#x} (x3d mode {})",
328 x3d_mode.as_deref().unwrap_or("n/a")
329 );
330 }
331 }
332 if opts.verbose {
333 for core in core_performance.details() {
334 info!(" {core}");
335 }
336 }
337 if opts.verbose {
338 info!(" slice {slice_us}µs, {total} per-CPU vtime queues + one wake pool per LLC");
339 }
340 let kernel_line = format!(
343 " kernel queued_wakeup {}, dsq_peek {}",
344 if queued_wakeup { "on" } else { "UNSUPPORTED" },
345 if dsq_peek { "native" } else { "MISSING" }
346 );
347 if !queued_wakeup || !dsq_peek {
348 warn!("{kernel_line}");
349 } else if opts.verbose {
350 info!("{kernel_line}");
351 }
352 let probe_on = slots.iter().any(|s| s.0 == "probe" && *s.2 == 1);
353 let changed: Vec<String> = slots
356 .iter()
357 .zip(&defaults)
358 .filter(|(s, d)| *s.2 != **d)
359 .map(|(s, _)| format!("{}={} {}", s.0, s.2, s.1))
360 .collect();
361 if opts.verbose {
362 let line: Vec<String> = slots.iter().map(|s| format!("{}={}", s.0, s.2)).collect();
363 info!(" toggle {}", line.join(" "));
364 }
365 if !changed.is_empty() {
366 info!(" toggle {} (all others at default)", changed.join(", "));
367 } else if !opts.toggle.is_empty() {
368 info!(" toggle all at default");
369 }
370
371 match probe_handoff_hop_ns() {
374 Some(probe) => {
375 let (med, p99) = (probe.median, probe.p99);
379 let hm = rodata.cake_handoff_max_ns;
380 let hop_max = rodata.cake_tick_ns / 4;
383 rodata.cake_wake_hop_ns = if p99 <= hop_max {
384 p99
385 } else {
386 warn!(" probe hop p99 {p99}ns exceeds {hop_max}ns; tick predictor off");
387 0
388 };
389 if opts.verbose {
390 info!(
391 "Handoff Hop median {med} ns / p99 {p99} ns; handoff limit {hm} ns (diagnostic)"
392 );
393 }
394 }
395 None => {
396 log::warn!(" probe handoff probe failed (diagnostic only)");
397 }
398 }
399
400 let siblings = &mut rodata.cpu_sibling;
405 siblings.fill(-1);
406 for core in topo.all_cores.values() {
407 let cpu_ids: Vec<usize> = core.cpus.keys().copied().collect();
408
409 if cpu_ids.len() < 2 {
410 continue;
411 }
412 for (idx, cpu) in cpu_ids.iter().copied().enumerate() {
413 let sibling = cpu_ids[(idx + 1) % cpu_ids.len()];
414
415 anyhow::ensure!(
416 cpu < siblings.len() && sibling <= i32::MAX as usize,
417 "CPU topology id exceeds Cake's compiled sibling map"
418 );
419 siblings[cpu] = sibling as i32;
420 }
421 }
422 if let Some((shift, left, right)) = smt_fold(siblings) {
423 rodata.cake_smt_shift = shift;
424 rodata.cake_smt_left = left;
425 rodata.cake_smt_right = right;
426 }
427
428 {
432 let rodata = skel
433 .maps
434 .rodata_data
435 .as_mut()
436 .context("BPF rodata unavailable for cache topology")?;
437 let order = &mut rodata.cpu_steal_order;
438 let span = bpf_intf::consts_STEAL_SPAN as usize;
439 let fits = topo
440 .all_cpus
441 .keys()
442 .next_back()
443 .is_none_or(|cpu| *cpu < span);
444 let llc_of = |cpu: &scx_utils::Cpu| -> usize { cpu.llc_id };
445 let nr_llcs = topo.all_llcs.len();
446 let multi_ccd = nr_llcs > 1;
447 rodata.steal_order_live = u8::from(fits);
448 rodata.nr_steal_cpus = topo.all_cpus.len().saturating_sub(1) as u32;
449 order.fill(0);
450
451 {
454 let cpus: Vec<(usize, usize)> =
455 topo.all_cpus.values().map(|c| (c.id, llc_of(c))).collect();
456 let max_llcs = if rodata.cake_one_word != 0 {
459 bpf_intf::consts_MAX_LLCS as usize
460 } else {
461 1
462 };
463 let layout = LlcLayout::build(&cpus, max_llcs);
464 if let Some(n) = layout.collapsed {
465 warn!(
466 " llc {n} domains, {max_llcs} pool(s) usable here; one pool, LLC-blind"
467 );
468 }
469 rodata.cpu_llc_id.fill(0);
470 rodata.cpu_llc_domain.fill(u16::MAX);
471 rodata.cpu_llc_word.fill(u64::MAX);
472 for (cpu, llc) in &cpus {
473 if *cpu < rodata.cpu_llc_id.len() {
474 rodata.cpu_llc_id[*cpu] = layout.dense[llc];
475 rodata.cpu_llc_domain[*cpu] = layout.domain[llc];
476 rodata.cpu_llc_word[*cpu] = layout.word[llc];
477 }
478 }
479 rodata.nr_llcs = layout.nr_llcs;
480 }
481
482 {
485 let online = online_cpu_set(&topo);
486 let mut per_llc: BTreeMap<usize, u32> = BTreeMap::new();
487 for cpu in topo.all_cpus.values() {
488 if online.contains(&cpu.id) {
489 *per_llc.entry(llc_of(cpu)).or_insert(0) += 1;
490 }
491 }
492 rodata.nr_cpu_online3 = per_llc.values().sum::<u32>() * 3;
493 rodata.cpu_llc_online3.fill(0);
494 let mut die_max = 0u32;
495 for cpu in topo.all_cpus.values() {
496 let n = per_llc.get(&llc_of(cpu)).copied().unwrap_or(0);
497 die_max = die_max.max(n);
498 if cpu.id < rodata.cpu_llc_online3.len() {
499 rodata.cpu_llc_online3[cpu.id] = n * 3;
500 }
501 }
502 rodata.cake_claim_tries = (die_max / 4).max(bpf_intf::consts_CLAIM_TRIES_MIN);
503 }
504
505 if multi_ccd && !fits {
506 warn!(" ccd host wider than steal matrix ({span} CPUs); ring steal only");
507 }
508 if fits {
509 let mut llc_cache: BTreeMap<usize, usize> = BTreeMap::new();
510 for cpu in topo.all_cpus.values() {
511 let e = llc_cache.entry(llc_of(cpu)).or_insert(0);
512 *e = (*e).max(cpu.cache_size);
513 }
514 let policy = bpf_intf::consts_CCD_STEAL_POLICY;
515 let nr_ids = *NR_CPU_IDS;
516
517 for src in topo.all_cpus.values() {
518 let mut candidates: Vec<_> = topo
519 .all_cpus
520 .values()
521 .filter(|dst| dst.id != src.id)
522 .collect();
523 candidates.sort_by_key(|dst| {
524 let class = if llc_of(dst) == llc_of(src) {
525 0
526 } else if policy > 1 && llc_cache[&llc_of(dst)] == llc_cache[&llc_of(src)] {
527 1
528 } else {
529 2
530 };
531 (
533 class,
534 u8::from(dst.core_id != src.core_id),
535 (dst.id + nr_ids - src.id) % nr_ids,
536 )
537 });
538 let base = src.id * span;
539 for (slot, dst) in candidates.into_iter().enumerate() {
540 order[base + slot] = dst.id as u16;
541 }
542 }
543 }
544 }
545
546 configure_acquire_census(&mut skel, probe_on);
547 if idle_exit_max_ns == 0 {
548 skel.progs.cake_cpu_idle.set_autoload(false);
549 }
550
551 let mut skel = scx_ops_load!(skel, cake_ops, uei)?;
553
554 let mut irq_links = Vec::with_capacity(4);
558 for (name, leave, enter) in [
559 (
560 "irq",
561 &skel.progs.cake_irq_leave,
562 &skel.progs.cake_irq_enter,
563 ),
564 (
565 "softirq",
566 &skel.progs.cake_softirq_leave,
567 &skel.progs.cake_softirq_enter,
568 ),
569 ] {
570 match attach_irq_pair(|| Ok(leave.attach()?), || Ok(enter.attach()?)) {
571 Ok(links) => irq_links.extend(links),
572 Err(e) => warn!(" irq {name} {e:#}; this source uses chronic steering only"),
573 }
574 }
575
576 if idle_exit_max_ns > 0 {
577 match skel.progs.cake_cpu_idle.attach() {
578 Ok(link) => irq_links.push(link),
579 Err(e) => warn!(" idle cpu_idle tracepoint {e:#}; idle depth unknown"),
580 }
581 }
582 if let (Some(die), Some(bss)) = (&pref_die, skel.maps.bss_data.as_mut()) {
583 bss.cake_llc_pref_word = preferred_word(die, x3d_mode.as_deref());
584 }
585
586 let struct_ops = Some(scx_ops_attach!(skel, cake_ops)?);
588
589 if let Some(bss) = skel.maps.bss_data.as_ref() {
590 let on_cpu = match bss.cake_compat_on_cpu_size {
591 1 => "u8",
592 4 => "32-bit",
593 _ => "unknown (idle kick retained)",
594 };
595 let cpu_curr = if bss.cake_compat_cpu_curr_kfunc {
596 "kfunc"
597 } else {
598 "rq fallback"
599 };
600 let tracking = match (bss.cake_compat_on_cpu_size, bss.cake_compat_cpu_curr_kfunc) {
601 (1 | 4, true) => "Native kernel support",
602 (1 | 4, false) => "Compatibility fallback for this kernel",
603 _ => "Unknown task-state layout; conservative idle kicks enabled",
604 };
605 info!("Task state {tracking}");
606 if opts.verbose {
607 info!("Kernel paths on_cpu={on_cpu} / cpu_curr={cpu_curr}");
608 }
609 }
610 info!("🍰 Running");
611 let tick_ns = skel.maps.rodata_data.as_ref().map_or(0, |r| r.cake_tick_ns);
613 if opts.verbose {
614 if tick_ns > 0 {
615 info!(" age tick clock ({tick_ns} ns per tick)");
616 } else {
617 info!(" age precise clock (no tick-sized coarse clock resolution)");
618 }
619 }
620
621 if let Err(err) = drop_privileges_for_observers() {
625 warn!("post-attach capability drop failed: {err}");
626 }
627
628 Ok(Self {
629 skel,
630 struct_ops,
631 _irq_links: irq_links,
632 probe_on,
633 verbose: opts.verbose,
634 sinks: SinkMonitor::new(*NR_CPU_IDS),
635 pref_die,
636 x3d_mode,
637 })
638 }
639
640 fn exited(&mut self) -> bool {
641 uei_exited!(&self.skel, uei)
642 }
643
644 fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<UserExitInfo> {
645 let mut polls: u32 = 0;
646 while !shutdown.load(Ordering::Relaxed) && !self.exited() {
647 std::thread::sleep(Duration::from_secs(1));
648 polls += 1;
649
650 if self.verbose && (polls == 2 || polls == 6) {
653 let depths = |bss: &bpf_skel::types::bss| -> Vec<u32> {
654 bss.cake_irq_live
655 .iter()
656 .take(*NR_CPU_IDS)
657 .map(|s| s.depth)
658 .collect()
659 };
660 let first = self.skel.maps.bss_data.as_ref().map(|b| depths(b));
661 std::thread::sleep(Duration::from_millis(50));
662 let second = self.skel.maps.bss_data.as_ref().map(|b| depths(b));
663 if let (Some(a), Some(b)) = (first, second) {
664 let stuck: Vec<String> = a
665 .iter()
666 .zip(&b)
667 .enumerate()
668 .filter(|(_, (x, y))| **x != 0 && **y != 0)
669 .map(|(i, (x, y))| format!("slot{}.{}:{x}/{y}", i / 2, i % 2))
670 .collect();
671 info!(
672 " irq in-handler depth stuck on {} CPU(s) at {polls} s {}",
673 stuck.len(),
674 stuck.join(" ")
675 );
676 }
677 }
678
679 if let Some(set) = self.sinks.tick(*NR_CPU_IDS) {
680 self.publish_sinks(&set);
681 }
682 if let Some(die) = &self.pref_die {
684 let mode = amd_x3d_mode();
685 if mode != self.x3d_mode {
686 if let Some(bss) = self.skel.maps.bss_data.as_mut() {
687 bss.cake_llc_pref_word = preferred_word(die, mode.as_deref());
688 }
689 info!(" die x3d mode {}", mode.as_deref().unwrap_or("n/a"));
690 self.x3d_mode = mode;
691 }
692 }
693 }
694
695 if let (true, Some(bss)) = (self.probe_on, self.skel.maps.bss_data.as_ref()) {
697 let n = bss.cake_blackbox_n;
698 for i in 0..n.min(4) {
699 let b = &bss.cake_blackbox[i as usize];
700 let comm = String::from_utf8_lossy(
701 &b.comm
702 .iter()
703 .map(|c| *c as u8)
704 .take_while(|c| *c != 0)
705 .collect::<Vec<u8>>(),
706 )
707 .to_string();
708 info!(
709 " BLACKBOX wait {:.2} ms {} pid {} kind {} target cpu{} caller cpu{} waker {} ran_on cpu{} seats {:#018x} core_free {:#018x} thread_free {:#018x} idle {:#018x}",
710 b.wait_ns as f64 / 1e6,
711 comm,
712 b.pid,
713 b.kind,
714 b.target,
715 b.caller,
716 b.waker_pid,
717 b.ran_on,
718 b.seats,
719 b.core_free,
720 b.thread_free,
721 b.idle_word
722 );
723 }
724 }
725 if let (true, Some(bss)) = (self.probe_on, self.skel.maps.bss_data.as_ref()) {
726 let bands: Vec<String> = (0..bss.cake_acquire_hist.len())
728 .map(|b| format!("{}us", 1u64 << b))
729 .collect();
730 info!(" g93 bands (>=) {}", bands.join(" "));
731 for (path, name) in ["non-immed", "immed", "slice-0"].iter().enumerate() {
732 let row: Vec<String> = bss.cake_release_census[path]
733 .iter()
734 .map(|v| v.to_string())
735 .collect();
736 info!(" g93 release {:9} {}", name, row.join(" "));
737 }
738 let reasons: Vec<String> = bss
739 .cake_release_reason
740 .iter()
741 .map(|v| v.to_string())
742 .collect();
743 info!(
744 " g93 release by reason rt/dl/stop/unknown {}",
745 reasons.join(" ")
746 );
747 let holds: Vec<String> = bss
748 .cake_acquire_hist
749 .iter()
750 .map(|v| v.to_string())
751 .collect();
752 info!(" g93 hold {}", holds.join(" "));
753 let shift = bpf_intf::consts_CAKE_HIST_SHIFT;
756 for (kind, name) in ["hop", "handoff", "burst"].iter().enumerate() {
757 let bands = bss.cake_hist[0][kind].len();
758 let sum: Vec<u64> = (0..bands)
759 .map(|b| bss.cake_hist.iter().map(|cpu| cpu[kind][b]).sum())
760 .collect();
761 let total: u64 = sum.iter().sum();
762 if total == 0 {
763 continue;
764 }
765 let mut peaks: Vec<usize> = (0..bands).collect();
766 peaks.sort_by_key(|b| std::cmp::Reverse(sum[*b]));
767 let (lo, hi) = (peaks[0].min(peaks[1]), peaks[0].max(peaks[1]));
768 let valley = (lo..=hi).min_by_key(|b| sum[*b]).unwrap_or(lo);
769 let mut acc = 0u64;
770 let median = (0..bands)
771 .find(|b| {
772 acc += sum[*b];
773 acc * 2 >= total
774 })
775 .unwrap_or(0);
776 let rows: Vec<String> = sum.iter().map(|v| v.to_string()).collect();
777 info!(
778 " hist {name:7} n={total} median>={}ns split>={}ns {}",
779 1u64 << (median as u32 + shift),
780 1u64 << (valley as u32 + shift),
781 rows.join(" ")
782 );
783 }
784 let tried: Vec<String> = (0..4)
787 .map(|g| {
788 bss.cake_tried
789 .iter()
790 .map(|row| row[g])
791 .sum::<u64>()
792 .to_string()
793 })
794 .collect();
795 info!(" tried serial/retake/probe/tick {}", tried.join(" "));
796 }
797 if self.probe_on {
798 const NAMES: [&str; 138] = [
799 "select_calls",
800 "serial",
801 "home_warm",
802 "wp_attempt",
803 "wp_tiny",
804 "wp_small",
805 "wp_protect",
806 "wp_vtime",
807 "wp_starved",
808 "wp_fired",
809 "pl_local",
810 "pl_local_on",
811 "pl_cpuq_wake",
812 "pl_cpuq_cont",
813 "pl_global",
814 "h300_local",
815 "h300_local_on",
816 "h300_cpuq_wake",
817 "h300_cpuq_cont",
818 "h300_global",
819 "h1ms_local",
820 "h1ms_local_on",
821 "h1ms_cpuq_wake",
822 "h1ms_cpuq_cont",
823 "h1ms_global",
824 "pl_self",
825 "h300_self",
826 "h1ms_self",
827 "hd_sync",
828 "hd_starved",
829 "hd_irq",
830 "hd_aff",
831 "hd_contended",
832 "hd_notidle",
833 "home_busy",
834 "home_localq",
835 "h300_home_busy",
836 "ui_enter",
837 "ui_enter_idlew",
838 "ui_exit",
839 "ui_exit_idlew",
840 "qmark_set",
841 "qmark_set_skip",
842 "qmark_clr",
843 "qmark_clr_skip",
844 "seat_clr",
845 "seat_set",
846 "running",
847 "frontier_st",
848 "wake_served_st",
849 "wake_mark_st",
850 "taci",
851 "taci_win",
852 "taci_stage",
853 "taci_home",
854 "taci_groove",
855 "taci_warm_core",
856 "taci_warm_thread",
857 "taci_warm",
858 "taci_hint",
859 "taci_notify",
860 "pick_idle",
861 "kick",
862 "nrq",
863 "dsq_insert",
864 "move_local",
865 "kt",
866 "kt_period",
867 "kt_ticksoon",
868 "kt_occupant",
869 "kt_handoff",
870 "kt_wakeclock",
871 "kt_running",
872 "kt_probe",
873 "task_storage",
874 "cpu_curr",
875 "core_contended",
876 "stage_probe",
877 "taciw_stage",
878 "taciw_home",
879 "taciw_groove",
880 "taciw_warm_core",
881 "taciw_warm_thread",
882 "taciw_warm",
883 "taciw_hint",
884 "taciw_notify",
885 "t_nrq",
886 "t_taci",
887 "t_pick",
888 "t_kick",
889 "t_cpu_curr",
890 "t_task_storage",
891 "t_move",
892 "t_insert",
893 "t_ui_idlew",
894 "t_qmark",
895 "t_cal",
896 "leak_home",
897 "leak_kick",
898 "leak_dispatch",
899 "seat_immune",
900 "seat_retake",
901 "seat_reroute",
902 "seat_decline",
903 "pool_forward",
904 "claim_retry",
905 "kt_pool",
906 "x_serial",
907 "kt_local",
908 "x_kt_local",
909 "x_claim",
910 "notify_kick",
911 "x_notify_kick",
912 "probe_fired",
913 "x_probe_fired",
914 "pool_served",
915 "x_pool_served",
916 "steal_moved",
917 "x_steal_moved",
918 "pool_direct",
919 "kick_alone",
920 "seat_skip",
921 "grant_vacant",
922 "grant_expired",
923 "grant_lt_tick",
924 "grant_ge_tick",
925 "hd_corebusy",
926 "rej_irq",
927 "rej_tick",
928 "expiry_preempt",
929 "release",
930 "acquire",
931 "select_direct",
932 "ui_kick",
933 "pend_kick",
934 "pend_heal",
935 "release_serve",
936 "pinned_preempt",
937 ];
938 let nr = self
941 .skel
942 .maps
943 .cake_stats
944 .info()
945 .map(|i| i.info.max_entries)
946 .unwrap_or(0) as usize;
947 if nr != NAMES.len() {
948 warn!(
949 " census name table has {} entries, map has {}: names are out of sync",
950 NAMES.len(),
951 nr
952 );
953 }
954 let mut tot = [0u64; NAMES.len()];
955 for (i, t) in tot.iter_mut().enumerate() {
956 let key = (i as u32).to_ne_bytes();
957 if let Ok(Some(percpu)) =
958 self.skel.maps.cake_stats.lookup_percpu(&key, MapFlags::ANY)
959 {
960 for cpu in &percpu {
961 if cpu.len() >= 8 {
962 *t += u64::from_ne_bytes(cpu[..8].try_into().unwrap());
963 }
964 }
965 }
966 }
967 let sel = tot[0].max(1) as f64;
968 for (i, name) in NAMES.iter().enumerate() {
969 info!(
970 " arms {name:<13} {:>12} {:>6.2}% of select",
971 tot[i],
972 tot[i] as f64 * 100.0 / sel
973 );
974 }
975 }
976
977 if let (true, Some(bss)) = (self.verbose, self.skel.maps.bss_data.as_mut()) {
978 let ev = &bss.cake_events;
979 info!(
980 " events select_fallback {} keep_last {} enq_skip_exiting {}",
981 ev.SCX_EV_SELECT_CPU_FALLBACK,
982 ev.SCX_EV_DISPATCH_KEEP_LAST,
983 ev.SCX_EV_ENQ_SKIP_EXITING
984 );
985 }
986
987 self.struct_ops.take();
988 info!("Stopped");
989 uei_report!(&self.skel, uei)
990 }
991
992 fn publish_sinks(&mut self, set: &[bool]) {
996 const N: usize = bpf_intf::consts_QMASK_WORDS as usize;
997 let words = sink_words::<N>(set);
998 let siblings: Vec<i32> = self
1001 .skel
1002 .maps
1003 .rodata_data
1004 .as_ref()
1005 .map(|ro| ro.cpu_sibling.to_vec())
1006 .unwrap_or_default();
1007 let mut cores = words;
1008 for (cpu, hot) in set.iter().take(N * 64).enumerate() {
1009 if let (true, Some(&sib)) = (*hot, siblings.get(cpu))
1010 && sib >= 0
1011 && (sib as usize) < N * 64
1012 {
1013 cores[sib as usize / 64] |= 1u64 << (sib as usize % 64);
1014 }
1015 }
1016 let Some(bss) = self.skel.maps.bss_data.as_mut() else {
1017 return;
1018 };
1019 for (published, word) in bss.cpu_irq_hot_words.iter_mut().zip(words) {
1020 unsafe { AtomicU64::from_ptr(published) }.store(word, Ordering::Relaxed);
1024 }
1025 for (published, word) in bss.cpu_irq_hot_cores.iter_mut().zip(cores) {
1026 unsafe { AtomicU64::from_ptr(published) }.store(word, Ordering::Relaxed);
1028 }
1029
1030 if self.verbose {
1033 let named: Vec<usize> = set
1034 .iter()
1035 .enumerate()
1036 .filter_map(|(cpu, hot)| hot.then_some(cpu))
1037 .collect();
1038 info!(" irq sinks {named:?} steered around");
1039 }
1040 }
1041}
1042
1043fn sink_words<const N: usize>(set: &[bool]) -> [u64; N] {
1044 let mut words = [0; N];
1045 for (cpu, hot) in set.iter().take(N * 64).enumerate() {
1046 if *hot {
1047 words[cpu / 64] |= 1u64 << (cpu % 64);
1048 }
1049 }
1050 words
1051}
1052
1053struct HandoffProbe {
1055 median: u64,
1057 p99: u64,
1061}
1062
1063fn read_irq_ticks(nr_cpus: usize) -> Option<Vec<Option<u64>>> {
1068 let text = std::fs::read_to_string("/proc/stat").ok()?;
1069 let mut ticks = vec![None; nr_cpus];
1070 let mut seen = false;
1071 for line in text.lines() {
1072 let mut fields = line.split_whitespace();
1073 let Some(cpu) = fields
1074 .next()
1075 .and_then(|l| l.strip_prefix("cpu"))
1076 .and_then(|n| n.parse::<usize>().ok())
1077 else {
1078 continue;
1079 };
1080 if cpu >= nr_cpus {
1081 continue;
1082 }
1083 let vals: Vec<u64> = fields.filter_map(|v| v.parse().ok()).collect();
1085 if vals.len() < 7 {
1086 continue;
1087 }
1088 ticks[cpu] = Some(vals[5] + vals[6]);
1089 seen = true;
1090 }
1091 seen.then_some(ticks)
1092}
1093
1094fn last_cpu_id(list: &str) -> Option<usize> {
1096 list.trim()
1097 .rsplit(',')
1098 .next()?
1099 .rsplit('-')
1100 .next()?
1101 .parse()
1102 .ok()
1103}
1104
1105fn sink_deltas(current: &[Option<u64>], before: &[Option<u64>]) -> (Vec<usize>, Vec<u64>) {
1109 current
1110 .iter()
1111 .zip(before)
1112 .enumerate()
1113 .filter_map(|(cpu, (c, b))| Some((cpu, (*c)?.checked_sub((*b)?)?)))
1114 .unzip()
1115}
1116
1117fn sinks_by_widest_gap(deltas: &[u64]) -> Option<Vec<bool>> {
1122 let mut ranked: Vec<(f64, usize)> = deltas
1123 .iter()
1124 .enumerate()
1125 .map(|(cpu, &d)| ((d as f64).max(0.5), cpu))
1126 .collect();
1127 ranked.sort_by(|a, b| b.0.total_cmp(&a.0));
1128
1129 let mut cut = 0;
1130 let mut widest = 1.0f64;
1131 for i in 1..ranked.len() {
1132 let ratio = ranked[i - 1].0 / ranked[i].0;
1133 if ratio > widest {
1134 widest = ratio;
1135 cut = i;
1136 }
1137 }
1138 if cut == 0 {
1139 return Some(vec![false; deltas.len()]);
1140 }
1141 if cut * 2 >= deltas.len() {
1142 return None;
1143 }
1144
1145 let mut hot = vec![false; deltas.len()];
1146 for &(_, cpu) in &ranked[..cut] {
1147 hot[cpu] = true;
1148 }
1149 Some(hot)
1150}
1151
1152struct SinkMonitor {
1158 prev: Option<Vec<Option<u64>>>,
1161 published: Vec<bool>,
1163 hot_streak: Vec<u32>,
1165 quiet: Vec<u32>,
1167 stable: u32,
1169 interval: u32,
1171 ticks: u32,
1172}
1173
1174impl SinkMonitor {
1175 const STABLE_POLLS: u32 = 8;
1177 const INTERVAL_MAX: u32 = 4;
1180 const FLAG_POLLS: u32 = 2;
1182 const UNFLAG_POLLS: u32 = 3;
1184
1185 fn new(nr_cpus: usize) -> Self {
1186 Self {
1187 prev: None,
1188 published: vec![false; nr_cpus],
1189 hot_streak: vec![0; nr_cpus],
1190 quiet: vec![0; nr_cpus],
1191 stable: 0,
1192 interval: 1,
1193 ticks: 0,
1194 }
1195 }
1196
1197 fn tick(&mut self, nr_cpus: usize) -> Option<Vec<bool>> {
1199 self.ticks = self.ticks.wrapping_add(1);
1200 if !self.ticks.is_multiple_of(self.interval) {
1201 return None;
1202 }
1203 self.observe(read_irq_ticks(nr_cpus))
1204 }
1205
1206 fn uncertain(&mut self) {
1209 self.hot_streak.fill(0);
1210 self.quiet.fill(0);
1211 self.stable = 0;
1212 self.interval = 1;
1213 }
1214
1215 fn observe(&mut self, sample: Option<Vec<Option<u64>>>) -> Option<Vec<bool>> {
1216 let Some(sample) = sample else {
1217 self.prev = None;
1218 self.uncertain();
1219 return None;
1220 };
1221 let before = self.prev.replace(sample)?;
1222 let current = self.prev.as_ref()?;
1223 let (present, deltas) = sink_deltas(current, &before);
1224 if present.is_empty() {
1225 self.uncertain();
1226 return None;
1227 }
1228 let Some(ranked) = sinks_by_widest_gap(&deltas) else {
1229 self.uncertain();
1230 return None;
1231 };
1232 let mut hot = vec![false; self.published.len()];
1233 for (&cpu, &is_hot) in present.iter().zip(&ranked) {
1234 if cpu < hot.len() {
1235 hot[cpu] = is_hot;
1236 }
1237 }
1238
1239 let mut changed = false;
1240 let mut disagree = false;
1241 for (cpu, &is_hot) in hot.iter().enumerate() {
1242 if present.binary_search(&cpu).is_err() {
1243 self.hot_streak[cpu] = 0;
1244 self.quiet[cpu] = 0;
1245 continue;
1246 }
1247 disagree |= is_hot != self.published[cpu];
1248 if is_hot {
1249 self.quiet[cpu] = 0;
1250 self.hot_streak[cpu] = (self.hot_streak[cpu] + 1).min(Self::FLAG_POLLS);
1251 if !self.published[cpu]
1254 && self.hot_streak[cpu] >= Self::FLAG_POLLS
1255 && (self.published.iter().filter(|h| **h).count() + 1) * 2 < present.len()
1256 {
1257 self.published[cpu] = true;
1258 changed = true;
1259 }
1260 } else {
1261 self.hot_streak[cpu] = 0;
1262 if self.published[cpu] {
1263 self.quiet[cpu] += 1;
1264 if self.quiet[cpu] >= Self::UNFLAG_POLLS {
1265 self.published[cpu] = false;
1266 self.quiet[cpu] = 0;
1267 changed = true;
1268 }
1269 }
1270 }
1271 }
1272
1273 if changed {
1274 self.stable = 0;
1275 self.interval = 1;
1276 return Some(self.published.clone());
1277 }
1278 if disagree {
1279 self.stable = 0;
1282 self.interval = 1;
1283 return None;
1284 }
1285 self.stable += 1;
1286 if self.stable >= Self::STABLE_POLLS {
1287 self.stable = 0;
1288 self.interval = (self.interval * 2).min(Self::INTERVAL_MAX);
1289 }
1290 None
1291 }
1292}
1293
1294fn coarse_tick_ns() -> u64 {
1298 const TICK_MIN_NS: u64 = 500_000;
1299 const TICK_MAX_NS: u64 = 20_000_000;
1300 let mut ts = libc::timespec {
1301 tv_sec: 0,
1302 tv_nsec: 0,
1303 };
1304 if unsafe { libc::clock_getres(libc::CLOCK_MONOTONIC_COARSE, &mut ts) } != 0 {
1306 return 0;
1307 }
1308 let ns = ts.tv_sec as u64 * 1_000_000_000 + ts.tv_nsec as u64;
1309 if (TICK_MIN_NS..=TICK_MAX_NS).contains(&ns) {
1310 ns
1311 } else {
1312 0
1313 }
1314}
1315
1316fn cpuidle_states() -> (Option<String>, Vec<u64>) {
1319 let base = std::path::Path::new("/sys/devices/system/cpu");
1320 let driver = std::fs::read_to_string(base.join("cpuidle/current_driver"))
1321 .ok()
1322 .map(|s| s.trim().to_string())
1323 .filter(|s| !s.is_empty() && s != "none");
1324 let mut states = Vec::new();
1325 if driver.is_some() {
1326 let mut idx = 0;
1327 while let Ok(lat) =
1328 std::fs::read_to_string(base.join(format!("cpu0/cpuidle/state{idx}/latency")))
1329 {
1330 let disabled =
1331 std::fs::read_to_string(base.join(format!("cpu0/cpuidle/state{idx}/disable")))
1332 .is_ok_and(|d| d.trim() == "1");
1333 let us: u64 = lat.trim().parse().unwrap_or(0);
1334 states.push(if disabled { 0 } else { us * 1000 });
1335 idx += 1;
1336 }
1337 }
1338 (driver, states)
1339}
1340
1341fn amd_x3d_mode() -> Option<String> {
1344 let dir = std::fs::read_dir("/sys/bus/platform/drivers/amd_x3d_vcache").ok()?;
1345 for entry in dir.flatten() {
1346 if let Ok(mode) = std::fs::read_to_string(entry.path().join("amd_x3d_mode")) {
1347 return Some(mode.trim().to_string());
1348 }
1349 }
1350 None
1351}
1352
1353struct PreferredDie {
1357 cache: u64,
1358 frequency: u64,
1359}
1360
1361fn preferred_die(
1362 topo: &Topology,
1363 perf: &core_performance::CorePerformanceLayout,
1364) -> Option<PreferredDie> {
1365 if topo.all_llcs.len() < 2 || *NR_CPU_IDS > 64 {
1366 return None;
1367 }
1368 let mut word: BTreeMap<usize, u64> = BTreeMap::new();
1369 let mut big: BTreeMap<usize, u32> = BTreeMap::new();
1370 let mut cache: BTreeMap<usize, usize> = BTreeMap::new();
1371 for cpu in topo.all_cpus.values() {
1372 *word.entry(cpu.llc_id).or_insert(0) |= 1u64 << cpu.id;
1373 if cpu.core_type != scx_utils::CoreType::Little {
1374 *big.entry(cpu.llc_id).or_insert(0) += 1;
1375 }
1376 let e = cache.entry(cpu.llc_id).or_insert(0);
1377 *e = (*e).max(cpu.cache_size);
1378 }
1379 let mut pref: BTreeMap<usize, u64> = BTreeMap::new();
1380 for core in &perf.cores {
1381 *pref.entry(core.identity.llc).or_insert(0) += u64::from(core.preference.unwrap_or(0));
1382 }
1383 let best = |key: &dyn Fn(&usize) -> u64| -> u64 {
1384 word.keys()
1385 .max_by_key(|l| (key(l), std::cmp::Reverse(**l)))
1386 .map_or(0, |l| word[l])
1387 };
1388 let by_big = best(&|l| u64::from(big.get(l).copied().unwrap_or(0)));
1389 let big_differs = big.values().max() != big.values().min();
1390 let by_cache = best(&|l| cache.get(l).copied().unwrap_or(0) as u64);
1391 let by_pref = best(&|l| pref.get(l).copied().unwrap_or(0));
1392 let all: u64 = word.values().fold(0, |a, w| a | w);
1393 let cache_differs = cache.values().max() != cache.values().min();
1394 Some(if big_differs {
1395 PreferredDie {
1396 cache: by_big,
1397 frequency: by_big,
1398 }
1399 } else if cache_differs && word.len() == 2 {
1400 PreferredDie {
1403 cache: by_cache,
1404 frequency: all & !by_cache,
1405 }
1406 } else {
1407 PreferredDie {
1408 cache: by_cache,
1409 frequency: by_pref,
1410 }
1411 })
1412}
1413
1414fn preferred_word(die: &PreferredDie, mode: Option<&str>) -> u64 {
1415 if mode == Some("cache") {
1416 die.cache
1417 } else {
1418 die.frequency
1419 }
1420}
1421
1422fn online_cpu_set(topo: &Topology) -> std::collections::BTreeSet<usize> {
1424 let all: std::collections::BTreeSet<usize> = topo.all_cpus.keys().copied().collect();
1425 let Ok(text) = std::fs::read_to_string("/sys/devices/system/cpu/online") else {
1426 return all;
1427 };
1428 let mut set = std::collections::BTreeSet::new();
1429 for part in text.trim().split(',') {
1430 let (lo, hi) = match part.split_once('-') {
1431 Some((a, b)) => (a.parse::<usize>(), b.parse::<usize>()),
1432 None => (part.parse::<usize>(), part.parse::<usize>()),
1433 };
1434 if let (Ok(lo), Ok(hi)) = (lo, hi) {
1435 set.extend(lo..=hi);
1436 }
1437 }
1438 if set.is_empty() { all } else { set }
1439}
1440
1441fn probe_handoff_hop_ns() -> Option<HandoffProbe> {
1446 const WARMUP: u32 = 200;
1447 const ITERS: u32 = 2_000;
1448 let total = WARMUP + ITERS;
1449
1450 let pair = Arc::new((std::sync::Mutex::new(0u32), std::sync::Condvar::new()));
1451 let peer = Arc::clone(&pair);
1452
1453 let responder = std::thread::Builder::new()
1454 .name("cake-probe".into())
1455 .spawn(move || {
1456 let (lock, cv) = &*peer;
1457 let mut turn = lock.lock().ok()?;
1458 for _ in 0..total {
1459 while turn.is_multiple_of(2) {
1460 turn = cv.wait(turn).ok()?;
1461 }
1462 *turn = turn.wrapping_add(1);
1463 cv.notify_one();
1464 }
1465 Some(())
1466 })
1467 .ok()?;
1468
1469 let mut hops: Vec<u64> = Vec::with_capacity(ITERS as usize);
1470 {
1471 let (lock, cv) = &*pair;
1472 let mut turn = lock.lock().ok()?;
1473 for i in 0..total {
1474 let t0 = std::time::Instant::now();
1475 *turn = turn.wrapping_add(1);
1476 cv.notify_one();
1477 while *turn % 2 == 1 {
1478 turn = cv.wait(turn).ok()?;
1479 }
1480 if i >= WARMUP {
1481 hops.push(t0.elapsed().as_nanos() as u64 / 2);
1483 }
1484 }
1485 }
1486 responder.join().ok()?;
1487
1488 if hops.len() < ITERS as usize / 2 {
1489 return None;
1490 }
1491 hops.sort_unstable();
1492 let median = hops[hops.len() / 2];
1493 let p99 = hops[hops.len() * 99 / 100];
1494 if median == 0 || p99 == 0 {
1495 return None;
1496 }
1497 Some(HandoffProbe { median, p99 })
1498}
1499
1500fn parse_opts_lenient(
1504 args: impl IntoIterator<Item = impl Into<std::ffi::OsString>>,
1505) -> std::result::Result<(Opts, Vec<String>), clap::Error> {
1506 use clap::error::ContextKind;
1507 use clap::error::ErrorKind;
1508
1509 let mut args: Vec<std::ffi::OsString> = args.into_iter().map(Into::into).collect();
1510 let mut dropped: Vec<String> = Vec::new();
1511 loop {
1512 match Opts::try_parse_from(&args) {
1513 Ok(opts) => return Ok((opts, dropped)),
1514 Err(e) if e.kind() == ErrorKind::UnknownArgument => {
1515 let Some(bad) = e
1516 .get(ContextKind::InvalidArg)
1517 .map(|v| v.to_string())
1518 .filter(|s| !s.is_empty())
1519 else {
1520 return Err(e);
1521 };
1522 let name = bad.split('=').next().unwrap_or(&bad).to_string();
1525 let short = name
1526 .strip_prefix('-')
1527 .filter(|s| s.chars().count() == 1)
1528 .and_then(|s| s.chars().next());
1529 let Some((pos, split)) = args.iter().enumerate().skip(1).find_map(|(pos, a)| {
1530 let s = a.to_string_lossy();
1531 if s == bad || s == name || s.starts_with(&format!("{name}=")) {
1532 return Some((pos, 0));
1533 }
1534 if s.starts_with('-')
1538 && !s.starts_with("--")
1539 && let Some(short) = short
1540 {
1541 return s
1542 .char_indices()
1543 .skip(1)
1544 .find_map(|(split, c)| (c == short).then_some((pos, split)));
1545 }
1546 None
1547 }) else {
1548 return Err(e);
1549 };
1550 let mut tok = args.remove(pos).to_string_lossy().into_owned();
1551 let mut next_pos = pos;
1552 if split > 0 {
1553 if split > 1 {
1554 args.insert(pos, tok[..split].into());
1555 next_pos += 1;
1556 }
1557 tok = format!("-{}", &tok[split..]);
1558 }
1559 if tok == name && tok.starts_with('-') && next_pos < args.len() {
1562 let next = args[next_pos].to_string_lossy();
1563 if !next.starts_with('-') {
1564 tok.push(' ');
1565 tok.push_str(&next);
1566 args.remove(next_pos);
1567 }
1568 }
1569 dropped.push(tok);
1570 }
1571 Err(e) => return Err(e),
1572 }
1573 }
1574}
1575
1576fn drop_privileges_for_observers() -> std::io::Result<()> {
1580 #[repr(C)]
1581 struct CapHeader {
1582 version: u32,
1583 pid: i32,
1584 }
1585 #[repr(C)]
1586 #[derive(Clone, Copy)]
1587 struct CapData {
1588 effective: u32,
1589 permitted: u32,
1590 inheritable: u32,
1591 }
1592 let header = CapHeader {
1593 version: 0x2008_0522,
1594 pid: 0,
1595 };
1596 let data = [CapData {
1597 effective: 0,
1598 permitted: 0,
1599 inheritable: 0,
1600 }; 2];
1601 if unsafe { libc::syscall(libc::SYS_capset, &header, data.as_ptr()) } != 0 {
1602 return Err(std::io::Error::last_os_error());
1603 }
1604 if unsafe { libc::prctl(libc::PR_SET_DUMPABLE, 1, 0, 0, 0) } != 0 {
1605 return Err(std::io::Error::last_os_error());
1606 }
1607 Ok(())
1608}
1609
1610fn reexec_self() -> Result<()> {
1612 use std::os::unix::process::CommandExt;
1613 let exe = std::fs::read_link("/proc/self/exe")
1614 .context("failed to resolve /proc/self/exe for restart")?;
1615 let err = std::process::Command::new(exe)
1616 .args(std::env::args_os().skip(1))
1617 .exec();
1618 Err(anyhow::Error::new(err).context("re-exec after kernel restart request failed"))
1619}
1620
1621fn main() -> Result<()> {
1622 let (opts, ignored_args) = parse_opts_lenient(std::env::args_os()).unwrap_or_else(|e| e.exit());
1623
1624 let mut lcfg = simplelog::ConfigBuilder::new();
1625 lcfg.set_time_level(simplelog::LevelFilter::Error)
1626 .set_location_level(simplelog::LevelFilter::Off)
1627 .set_target_level(simplelog::LevelFilter::Off)
1628 .set_thread_level(simplelog::LevelFilter::Off);
1629 simplelog::TermLogger::init(
1630 simplelog::LevelFilter::Info,
1631 lcfg.build(),
1632 simplelog::TerminalMode::Stderr,
1633 simplelog::ColorChoice::Auto,
1634 )?;
1635 for a in &ignored_args {
1636 warn!(" args ignored unknown option `{a}`; cake has no such option, defaults used");
1637 }
1638
1639 if opts.version {
1640 println!(
1641 "{} {}",
1642 SCHEDULER_NAME,
1643 build_id::full_version(env!("CARGO_PKG_VERSION"))
1644 );
1645 match build_identity::binary_sha256() {
1646 Ok(hash) => println!("Build SHA256: {hash}"),
1647 Err(err) => println!("Build SHA256: unavailable ({err:#})"),
1648 }
1649 return Ok(());
1650 }
1651 if opts.print_topology {
1652 let topo = Topology::new().context("failed to read topology")?;
1653 let layout = core_performance::CorePerformanceLayout::discover(&topo);
1654 println!("{}", layout.summary());
1655 for core in layout.details() {
1656 println!("{core}");
1657 }
1658 return Ok(());
1659 }
1660
1661 let shutdown = Arc::new(AtomicBool::new(false));
1662 let shutdown_clone = shutdown.clone();
1663 ctrlc::set_handler(move || {
1664 if std::io::stderr().is_terminal() {
1667 eprintln!();
1668 }
1669 shutdown_clone.store(true, Ordering::Relaxed);
1670 })
1671 .context("Error setting Ctrl-C handler")?;
1672
1673 let mut open_object = MaybeUninit::uninit();
1674 let mut sched = Scheduler::init(&opts, &mut open_object)?;
1675
1676 if sched.run(shutdown.clone())?.should_restart() {
1677 info!("Restart requested by the kernel — re-executing");
1678 reexec_self()?;
1679 }
1680
1681 Ok(())
1682}
1683
1684fn smt_fold(siblings: &[i32]) -> Option<(u32, u64, u64)> {
1687 let (mut shift, mut left, mut right) = (0, 0, 0);
1688 for (cpu, &sib) in siblings.iter().take(64).enumerate() {
1689 if !(0..64).contains(&sib) || sib as usize == cpu {
1690 continue;
1691 }
1692 let distance = (cpu as u32).abs_diff(sib as u32);
1693 if shift != 0 && shift != distance {
1694 return None;
1695 }
1696 shift = distance;
1697 if cpu < sib as usize {
1698 left |= 1u64 << cpu;
1699 } else {
1700 right |= 1u64 << cpu;
1701 }
1702 }
1703 Some((shift, left, right))
1704}
1705
1706struct LlcLayout {
1709 domain: BTreeMap<usize, u16>,
1711 dense: BTreeMap<usize, u8>,
1714 word: BTreeMap<usize, u64>,
1718 nr_llcs: u32,
1719 collapsed: Option<usize>,
1721}
1722
1723impl LlcLayout {
1724 fn build(cpus: &[(usize, usize)], max_llcs: usize) -> Self {
1725 let mut word: BTreeMap<usize, u64> = BTreeMap::new();
1726 for (cpu, llc) in cpus {
1727 let w = word.entry(*llc).or_insert(0);
1728 if *cpu < 64 {
1729 *w |= 1u64 << cpu;
1730 }
1731 }
1732 for w in word.values_mut() {
1733 if *w == 0 {
1734 *w = u64::MAX;
1735 }
1736 }
1737 let n = word.len();
1738 let collapsed = (n > max_llcs).then_some(n);
1739 let domain = word
1740 .keys()
1741 .enumerate()
1742 .map(|(i, llc)| (*llc, i as u16))
1743 .collect();
1744 let dense: BTreeMap<usize, u8> = word
1745 .keys()
1746 .enumerate()
1747 .map(|(i, llc)| (*llc, if collapsed.is_some() { 0 } else { i as u8 }))
1748 .collect();
1749 let nr_llcs = if collapsed.is_some() {
1750 1
1751 } else {
1752 n.max(1) as u32
1753 };
1754 Self {
1755 domain,
1756 dense,
1757 word,
1758 nr_llcs,
1759 collapsed,
1760 }
1761 }
1762}
1763
1764#[cfg(test)]
1765mod tests {
1766 #[test]
1767 fn legacy_profiles_use_default_options() {
1768 let (defaults, _) = super::parse_opts_lenient(["scx_cake"]).unwrap();
1769 for args in [
1770 vec!["--profile", "gaming"],
1771 vec!["--profile=performance"],
1772 vec!["-p", "powersave"],
1773 vec!["-pperformance"],
1774 vec!["--profile"],
1775 vec!["--performance", "--powersave"],
1776 ] {
1777 let (opts, dropped) =
1778 super::parse_opts_lenient(std::iter::once("scx_cake").chain(args.clone())).unwrap();
1779 assert_eq!(opts, defaults, "{args:?}");
1780 assert!(!dropped.is_empty(), "{args:?}");
1781 }
1782 }
1783
1784 #[test]
1785 fn legacy_options_preserve_supported_arguments() {
1786 let (expected, _) =
1787 super::parse_opts_lenient(["scx_cake", "-v", "--toggle", "g85=0", "--print-topology"])
1788 .unwrap();
1789 let (opts, dropped) = super::parse_opts_lenient([
1790 "scx_cake",
1791 "--profile",
1792 "gaming",
1793 "-v",
1794 "--obsolete=42",
1795 "--toggle",
1796 "g85=0",
1797 "--powersave",
1798 "--print-topology",
1799 ])
1800 .unwrap();
1801 assert_eq!(opts, expected);
1802 assert_eq!(
1803 dropped,
1804 ["--profile gaming", "--obsolete=42", "--powersave"]
1805 );
1806 }
1807
1808 #[test]
1809 fn legacy_short_option_preserves_cluster_prefix() {
1810 let (opts, dropped) =
1811 super::parse_opts_lenient(["scx_cake", "-vpperformance", "--toggle=g85=0"]).unwrap();
1812 assert!(opts.verbose);
1813 assert_eq!(opts.toggle, ["g85=0"]);
1814 assert_eq!(dropped, ["-pperformance"]);
1815 }
1816
1817 #[test]
1818 fn supported_option_errors_and_help_remain_errors() {
1819 use clap::error::ErrorKind;
1820 for (args, kind) in [
1821 (vec!["--toggle"], ErrorKind::InvalidValue),
1822 (vec!["--verbose=garbage"], ErrorKind::TooManyValues),
1823 (
1824 vec!["--profile", "gaming", "--help"],
1825 ErrorKind::DisplayHelp,
1826 ),
1827 ] {
1828 let err = super::parse_opts_lenient(std::iter::once("scx_cake").chain(args.clone()))
1829 .unwrap_err();
1830 assert_eq!(err.kind(), kind, "{args:?}");
1831 }
1832 }
1833
1834 #[test]
1835 fn drop_privileges_in_child() {
1836 const CHILD: &str = "SCX_CAKE_OBSERVER_TEST_CHILD";
1837 if std::env::var_os(CHILD).is_none() {
1838 let status = std::process::Command::new(std::env::current_exe().unwrap())
1839 .args(["--exact", "tests::drop_privileges_in_child"])
1840 .env(CHILD, "1")
1841 .status()
1842 .unwrap();
1843 assert!(status.success());
1844 return;
1845 }
1846 super::drop_privileges_for_observers().unwrap();
1847 let status = std::fs::read_to_string("/proc/thread-self/status").unwrap();
1848 for set in ["CapInh:", "CapPrm:", "CapEff:"] {
1849 let value = status.lines().find(|line| line.starts_with(set)).unwrap();
1850 assert_eq!(
1851 u64::from_str_radix(value.split_whitespace().nth(1).unwrap(), 16).unwrap(),
1852 0
1853 );
1854 }
1855 assert_eq!(unsafe { libc::prctl(libc::PR_GET_DUMPABLE, 0, 0, 0, 0) }, 1);
1856 }
1857
1858 #[test]
1859 fn smt_fold_matches_every_single_cpu_mapping() {
1860 let mut siblings = [-1; 64];
1861 assert_eq!(super::smt_fold(&siblings), Some((0, 0, 0)));
1862 for distance in 1..64 {
1866 for (cpu, sib) in siblings.iter_mut().enumerate() {
1867 *sib = if cpu + distance < 64 {
1868 (cpu + distance) as i32
1869 } else if cpu >= distance {
1870 (cpu - distance) as i32
1871 } else {
1872 -1
1873 };
1874 }
1875 let (shift, left, right) = super::smt_fold(&siblings).unwrap();
1876 for (cpu, &sib) in siblings.iter().enumerate() {
1877 let bit = 1u64 << cpu;
1878 let actual = bit | ((bit & left) << shift) | ((bit & right) >> shift);
1879 let expected = bit | if sib >= 0 { 1u64 << sib } else { 0 };
1880 assert_eq!(actual, expected);
1881 }
1882 }
1883 siblings.fill(-1);
1884 siblings[0] = 1;
1885 siblings[1] = 0;
1886 siblings[2] = 4;
1887 assert_eq!(super::smt_fold(&siblings), None);
1888 siblings.fill(64); assert_eq!(super::smt_fold(&siblings), Some((0, 0, 0)));
1890 }
1891
1892 #[test]
1893 fn idle_and_release_callbacks_are_registered() {
1894 use libbpf_rs::skel::SkelBuilder;
1895 use std::mem::MaybeUninit;
1896
1897 let mut object = MaybeUninit::uninit();
1898 let builder = super::BpfSkelBuilder::default();
1899 let mut skel = builder.open(&mut object).expect("open embedded BPF object");
1900 assert!(!skel.struct_ops.cake_ops_mut().update_idle.is_null());
1901 assert!(!skel.struct_ops.cake_ops_mut().cpu_release.is_null());
1902 assert!(!skel.struct_ops.cake_ops_mut().disable.is_null());
1903 assert!(skel.progs.cake_cpu_release.autoload());
1904 }
1905
1906 use super::{
1907 LlcLayout, SinkMonitor, attach_irq_pair, last_cpu_id, sink_deltas, sink_words,
1908 sinks_by_widest_gap,
1909 };
1910
1911 #[test]
1914 #[ignore = "requires BPF load capabilities and a sched_ext kernel"]
1915 fn verifier_load_topologies() -> anyhow::Result<()> {
1916 use super::BpfSkelBuilder;
1917 use scx_utils::{scx_ops_load, scx_ops_open};
1918 use std::mem::MaybeUninit;
1919
1920 libbpf_rs::set_print(Some((libbpf_rs::PrintLevel::Warn, |_, message| {
1921 eprint!("{message}");
1922 })));
1923 let mut failures = Vec::new();
1924 let steal_span = super::bpf_intf::consts_STEAL_SPAN as usize;
1925 for (cpus, llcs) in [
1926 (1usize, 1usize),
1927 (16, 1),
1928 (32, 1),
1929 (32, 2),
1930 (64, 16),
1931 (48, 4),
1932 (96, 4),
1933 (128, 8),
1934 (128, 1),
1935 (1024, 1),
1936 ] {
1937 let mut object = MaybeUninit::uninit();
1938 let builder = BpfSkelBuilder::default();
1939 let mut skel = scx_ops_open!(builder, &mut object, cake_ops, None)?;
1940 let one_word = cpus <= 64;
1941 let cores = cpus.div_ceil(2);
1942 let ro = skel.maps.rodata_data.as_mut().unwrap();
1943 ro.nr_cpu_span = cpus as u32;
1944 ro.nr_llcs = llcs as u32;
1945 ro.cake_one_word = u8::from(one_word);
1946 ro.cake_rank_tiers = if one_word { cores as u32 } else { 0 };
1947 ro.cake_claim_tries = ((cpus / llcs.max(1)) as u32 / 4).max(4);
1948 ro.cpu_perf_known = if one_word { u64::MAX >> (64 - cpus) } else { 0 };
1949 ro.cpu_sibling.fill(-1);
1950 ro.cpu_llc_domain.fill(u16::MAX);
1951 ro.cake_smt_shift = if cpus == 1 { 0 } else { cores.min(64) as u32 };
1952 if cores < 64 {
1953 ro.cake_smt_left = u64::MAX >> (64 - cores);
1954 ro.cake_smt_right = ro.cake_smt_left << cores;
1955 }
1956 ro.steal_order_live = u8::from(llcs > 1 && cpus <= steal_span);
1957 ro.nr_steal_cpus = (cpus - 1) as u32;
1958 for cpu in 0..cpus {
1959 let domain = (cpu % cores) / (cores / llcs);
1960 if cpus > 1 {
1961 ro.cpu_sibling[cpu] = (cpu ^ cores) as i32;
1962 }
1963 ro.cpu_llc_id[cpu] = domain as u8;
1964 ro.cpu_llc_domain[cpu] = domain as u16;
1965 if one_word {
1966 ro.cpu_perf_tier[cpu % cores] |= 1u64 << cpu;
1967 }
1968 for peer in 0..cpus.min(64) {
1969 if (peer % cores) / (cores / llcs) == domain {
1970 ro.cpu_llc_word[cpu] |= 1u64 << peer;
1971 }
1972 }
1973 if ro.steal_order_live != 0 {
1974 for peer in 1..cpus {
1975 ro.cpu_steal_order[cpu * steal_span + peer - 1] =
1976 ((cpu + peer) % cpus) as u16;
1977 }
1978 }
1979 }
1980 match scx_ops_load!(skel, cake_ops, uei) {
1981 Ok(loaded) => {
1982 eprintln!("verifier accepted: {cpus} CPUs, {llcs} LLCs");
1983 drop(loaded);
1984 }
1985 Err(error) => failures.push(format!("{cpus} CPUs, {llcs} LLCs: {error:#}")),
1986 };
1987 }
1988 anyhow::ensure!(failures.is_empty(), "{}", failures.join("\n"));
1989 Ok(())
1990 }
1991
1992 #[test]
1993 fn irq_publication_preserves_sparse_wide_cpu_ids_and_clears_old_bits() {
1994 let mut set = vec![false; 257];
1995 for cpu in [0, 63, 64, 127, 128, 255, 256] {
1996 set[cpu] = true;
1997 }
1998 assert_eq!(
1999 sink_words::<4>(&set),
2000 [1 | (1 << 63), 1 | (1 << 63), 1, 1 << 63]
2001 );
2002 assert_eq!(sink_words::<4>(&[false; 1]), [0; 4]);
2003 assert_eq!(sink_words::<4>(&[]), [0; 4]);
2004 }
2005
2006 #[test]
2007 fn unknown_irq_windows_break_streaks_without_inventing_quiet() {
2008 let mut monitor = SinkMonitor::new(8);
2009 monitor.published[0] = true;
2010 monitor.hot_streak[1] = 1;
2011 monitor.quiet[0] = 2;
2012 monitor.stable = 7;
2013 monitor.interval = SinkMonitor::INTERVAL_MAX;
2014 assert_eq!(monitor.observe(None), None);
2015 assert!(monitor.published[0]);
2016 assert_eq!(monitor.hot_streak, vec![0; 8]);
2017 assert_eq!(monitor.quiet, vec![0; 8]);
2018 assert_eq!((monitor.stable, monitor.interval), (0, 1));
2019 assert_eq!(monitor.observe(Some(vec![Some(0); 8])), None);
2020 monitor.hot_streak[1] = 1;
2022 assert_eq!(
2023 monitor.observe(Some(vec![
2024 Some(100),
2025 Some(100),
2026 Some(100),
2027 Some(100),
2028 Some(0),
2029 Some(0),
2030 Some(0),
2031 Some(0)
2032 ])),
2033 None
2034 );
2035 assert_eq!(monitor.hot_streak, vec![0; 8]);
2036 assert!(monitor.published[0]);
2037 }
2038
2039 #[test]
2040 fn irq_counter_reset_and_missing_cpu_are_unknown_not_zero_load() {
2041 assert_eq!(
2042 sink_deltas(&[Some(2), Some(5)], &[Some(10), Some(3)]),
2043 (vec![1], vec![2])
2044 );
2045 let mut monitor = SinkMonitor::new(8);
2046 monitor.observe(Some(vec![Some(100); 8]));
2047 monitor.published[0] = true;
2048 monitor.quiet[0] = 2;
2049 let mut next = vec![Some(101); 8];
2050 next[0] = None;
2051 monitor.observe(Some(next));
2052 assert!(monitor.published[0]);
2053 assert_eq!(monitor.quiet[0], 0);
2054 }
2055
2056 #[test]
2057 fn irq_rank_disagreement_resets_the_slowed_interval_before_publication() {
2058 let mut monitor = SinkMonitor::new(8);
2059 monitor.observe(Some(vec![Some(0); 8]));
2060 monitor.published[0] = true;
2061 monitor.stable = SinkMonitor::STABLE_POLLS - 1;
2062 monitor.interval = SinkMonitor::INTERVAL_MAX;
2063 let mut next = vec![Some(1); 8];
2066 next[1] = Some(1000);
2067 assert_eq!(monitor.observe(Some(next)), None);
2068 assert_eq!((monitor.stable, monitor.interval), (0, 1));
2069 assert!(monitor.published[0]);
2070 assert!(!monitor.published[1]);
2071 monitor.published = vec![false; 8];
2073 monitor.published[1] = true;
2074 monitor.hot_streak.fill(0);
2075 let mut next = vec![Some(2); 8];
2076 next[1] = Some(2000);
2077 assert_eq!(monitor.observe(Some(next)), None);
2078 assert_eq!((monitor.stable, monitor.interval), (1, 1));
2079 }
2080 use std::{cell::RefCell, rc::Rc};
2081
2082 #[test]
2083 fn irq_pair_orders_attach_teardown_and_cleans_partial_failures() {
2084 struct Link(&'static str, Rc<RefCell<Vec<&'static str>>>);
2085 impl Drop for Link {
2086 fn drop(&mut self) {
2087 self.1.borrow_mut().push(self.0);
2088 }
2089 }
2090 for fail in [None, Some("leave"), Some("enter")] {
2091 let events = Rc::new(RefCell::new(Vec::new()));
2092 let pair = attach_irq_pair(
2093 || {
2094 events.borrow_mut().push("attach leave");
2095 anyhow::ensure!(fail != Some("leave"), "injected");
2096 Ok(Link("drop leave", events.clone()))
2097 },
2098 || {
2099 events.borrow_mut().push("attach enter");
2100 anyhow::ensure!(fail != Some("enter"), "injected");
2101 Ok(Link("drop enter", events.clone()))
2102 },
2103 );
2104 assert_eq!(pair.is_ok(), fail.is_none());
2105 drop(pair);
2106 let expected = match fail {
2107 None => vec!["attach leave", "attach enter", "drop enter", "drop leave"],
2108 Some("leave") => vec!["attach leave"],
2109 _ => vec!["attach leave", "attach enter", "drop leave"],
2110 };
2111 assert_eq!(*events.borrow(), expected);
2112 }
2113 }
2114
2115 #[test]
2116 fn llc_identity_survives_wide_pool_collapse_and_sparse_llc_ids() {
2117 let cpus: Vec<_> = (0..128).map(|c| (c, 1000 + (c % 64) / 4)).collect();
2118 let l = LlcLayout::build(&cpus, 1);
2119 assert_eq!(l.nr_llcs, 1);
2120 assert_ne!(l.domain[&cpus[0].1], l.domain[&cpus[80].1]);
2121 assert_eq!(l.domain[&cpus[0].1], l.domain[&cpus[64].1]);
2122 assert!(l.dense.values().all(|d| *d == 0));
2123 }
2124
2125 #[test]
2126 fn cpulist_last_id() {
2127 assert_eq!(last_cpu_id("0-15\n"), Some(15));
2128 assert_eq!(last_cpu_id("0-3,8-11"), Some(11));
2129 assert_eq!(last_cpu_id("0"), Some(0));
2130 assert_eq!(last_cpu_id(""), None);
2131 }
2132
2133 fn grid(nr_cpus: usize, per_llc: usize) -> Vec<(usize, usize)> {
2134 (0..nr_cpus).map(|c| (c, c / per_llc)).collect()
2135 }
2136
2137 #[test]
2138 fn offline_possible_ids_are_not_ranked() {
2139 let before: Vec<Option<u64>> = (0..64).map(|c| (c < 8).then_some(100)).collect();
2143 let current: Vec<Option<u64>> = (0..64).map(|c| (c < 8).then_some(103)).collect();
2144 let (present, deltas) = sink_deltas(¤t, &before);
2145 assert_eq!(present, (0..8).collect::<Vec<_>>());
2146 assert_eq!(deltas, vec![3; 8]);
2147 assert_eq!(sinks_by_widest_gap(&deltas).unwrap(), vec![false; 8]);
2148 }
2149
2150 #[test]
2151 fn llc_layout_counts_domains_above_cpu_63() {
2152 let l = LlcLayout::build(&grid(128, 8), 16);
2155 assert_eq!(l.nr_llcs, 16);
2156 assert!(l.collapsed.is_none());
2157 assert_eq!(l.dense[&9], 9);
2158 assert_eq!(l.word[&0], 0xff);
2159 assert_eq!(l.word[&7], 0xff << 56);
2160 assert_eq!(l.word[&9], u64::MAX);
2162 }
2163
2164 #[test]
2165 fn llc_layout_dual_ccd() {
2166 let l = LlcLayout::build(&grid(32, 16), 16);
2167 assert_eq!(l.nr_llcs, 2);
2168 assert_eq!(l.dense[&0], 0);
2169 assert_eq!(l.dense[&1], 1);
2170 assert_eq!(l.word[&1], 0xffff << 16);
2171 }
2172
2173 #[test]
2174 fn llc_layout_collapses_past_max_llcs() {
2175 let l = LlcLayout::build(&grid(256, 4), 16);
2176 assert_eq!(l.collapsed, Some(64));
2177 assert_eq!(l.nr_llcs, 1);
2178 assert!(l.dense.values().all(|d| *d == 0));
2179 assert_eq!(l.domain[&0], 0);
2180 assert_eq!(l.domain[&63], 63);
2181 }
2182
2183 #[test]
2184 fn llc_layout_one_llc_and_sparse_ids() {
2185 let l = LlcLayout::build(&[(0, 3), (2, 3), (70, 3)], 16);
2186 assert_eq!(l.nr_llcs, 1);
2187 assert_eq!(l.dense[&3], 0);
2188 assert_eq!(l.word[&3], 0b101);
2189 }
2190
2191 fn flagged(hot: &[bool]) -> Vec<usize> {
2192 hot.iter()
2193 .enumerate()
2194 .filter_map(|(cpu, h)| h.then_some(cpu))
2195 .collect()
2196 }
2197
2198 #[test]
2199 fn bimodal_host_cuts_above_the_gap() {
2200 let deltas = [0, 1, 0, 0, 0, 20, 0, 1, 0, 0, 0, 0, 0, 32, 0, 0];
2203 let hot = sinks_by_widest_gap(&deltas).unwrap();
2204 assert_eq!(flagged(&hot), vec![5, 13]);
2205 }
2206
2207 #[test]
2208 fn flat_distribution_has_no_sinks() {
2209 assert_eq!(sinks_by_widest_gap(&[3; 16]).unwrap(), vec![false; 16]);
2210 assert_eq!(sinks_by_widest_gap(&[0; 16]).unwrap(), vec![false; 16]);
2211 }
2212
2213 #[test]
2214 fn half_machine_cut_is_untrusted() {
2215 let deltas = [50, 50, 50, 50, 50, 50, 50, 50, 0, 0, 0, 0, 0, 0, 0, 0];
2217 assert!(sinks_by_widest_gap(&deltas).is_none());
2218 }
2219
2220 #[test]
2221 fn lone_loud_cpu_separates_from_quiet_machine() {
2222 let mut deltas = [0u64; 16];
2223 deltas[13] = 4;
2224 let hot = sinks_by_widest_gap(&deltas).unwrap();
2225 assert_eq!(flagged(&hot), vec![13]);
2226 }
2227}