mod calibrate;
mod stats;
mod topology;
mod tui;
use core::sync::atomic::Ordering;
use std::os::fd::AsRawFd;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use log::{info, warn};
use nix::sys::signal::{SigSet, Signal};
use nix::sys::signalfd::{SfdFlags, SignalFd};
#[allow(non_camel_case_types, non_upper_case_globals, dead_code)]
mod bpf_intf {
include!(concat!(env!("OUT_DIR"), "/bpf_intf.rs"));
}
#[allow(non_camel_case_types, non_upper_case_globals, dead_code)]
mod bpf_skel {
include!(concat!(env!("OUT_DIR"), "/bpf_skel.rs"));
}
use bpf_skel::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum Profile {
Esports,
Legacy,
Gaming,
Default,
}
impl Profile {
fn values(&self) -> (u64, u64, u64) {
match self {
Profile::Esports => (1000, 4000, 50000),
Profile::Legacy => (4000, 12000, 200000),
Profile::Gaming => (2000, 8000, 100000),
Profile::Default => (2000, 8000, 100000),
}
}
fn starvation_threshold(&self) -> [u64; 8] {
match self {
Profile::Esports => [
1_500_000, 4_000_000, 20_000_000, 50_000_000, 50_000_000, 50_000_000, 50_000_000, 50_000_000, ],
Profile::Legacy => [
6_000_000, 16_000_000, 80_000_000, 200_000_000, 200_000_000,
200_000_000,
200_000_000,
200_000_000, ],
Profile::Gaming | Profile::Default => [
3_000_000, 8_000_000, 40_000_000, 100_000_000, 100_000_000,
100_000_000,
100_000_000,
100_000_000, ],
}
}
fn tier_multiplier(&self) -> [u32; 8] {
match self {
Profile::Esports | Profile::Legacy | Profile::Gaming | Profile::Default => [
768, 1024, 1229, 1434, 1434, 1434, 1434, 1434, ],
}
}
fn wait_budget(&self) -> [u64; 8] {
match self {
Profile::Esports => [
50_000, 1_000_000, 4_000_000, 0, 0, 0, 0, 0, ],
Profile::Legacy => [
200_000, 4_000_000, 16_000_000, 0, 0, 0, 0, 0, ],
Profile::Gaming | Profile::Default => [
100_000, 2_000_000, 8_000_000, 0, 0, 0, 0, 0, ],
}
}
fn tier_configs(&self, quantum_us: u64) -> [u64; 8] {
let starvation = self.starvation_threshold();
let multiplier = self.tier_multiplier();
let budget = self.wait_budget();
let mut configs = [0u64; 8];
for i in 0..8 {
configs[i] = (multiplier[i] as u64 & 0xFFF)
| ((quantum_us & 0xFFFF) << 12)
| (((budget[i] >> 10) & 0xFFFF) << 28)
| (((starvation[i] >> 10) & 0xFFFFF) << 44);
}
configs
}
}
#[derive(Parser, Debug)]
#[command(
author,
version,
about = "🍰 A sched_ext scheduler applying CAKE bufferbloat concepts to CPU scheduling",
verbatim_doc_comment
)]
struct Args {
#[arg(long, short, value_enum, default_value_t = Profile::Gaming, verbatim_doc_comment)]
profile: Profile,
#[arg(long, verbatim_doc_comment)]
quantum: Option<u64>,
#[arg(long, verbatim_doc_comment)]
new_flow_bonus: Option<u64>,
#[arg(long, verbatim_doc_comment)]
starvation: Option<u64>,
#[arg(long, short, verbatim_doc_comment)]
verbose: bool,
#[arg(long, default_value_t = 1, verbatim_doc_comment)]
interval: u64,
}
impl Args {
fn effective_values(&self) -> (u64, u64, u64) {
let (q, nfb, starv) = self.profile.values();
(
self.quantum.unwrap_or(q),
self.new_flow_bonus.unwrap_or(nfb),
self.starvation.unwrap_or(starv),
)
}
}
struct Scheduler<'a> {
skel: BpfSkel<'a>,
args: Args,
topology: topology::TopologyInfo,
latency_matrix: Vec<Vec<f64>>,
}
impl<'a> Scheduler<'a> {
fn new(
args: Args,
open_object: &'a mut std::mem::MaybeUninit<libbpf_rs::OpenObject>,
) -> Result<Self> {
use libbpf_rs::skel::{OpenSkel, SkelBuilder};
let skel_builder = BpfSkelBuilder::default();
let mut open_skel = skel_builder
.open(open_object)
.context("Failed to open BPF skeleton")?;
scx_utils::import_enums!(open_skel);
let topo = topology::detect()?;
let (quantum, new_flow_bonus, _starvation) = args.effective_values();
info!("Starting ETD calibration...");
let latency_matrix = calibrate::calibrate_full_matrix(
topo.nr_cpus,
&calibrate::EtdConfig::default(),
|current, total, is_complete| {
tui::render_calibration_progress(current, total, is_complete);
},
);
if let Some(rodata) = &mut open_skel.maps.rodata_data {
rodata.quantum_ns = quantum * 1000;
rodata.new_flow_bonus_ns = new_flow_bonus * 1000;
rodata.enable_stats = args.verbose;
rodata.tier_configs = args.profile.tier_configs(quantum);
rodata.has_hybrid = topo.has_hybrid_cores;
let llc_count = topo.llc_cpu_mask.iter().filter(|&&m| m != 0).count() as u32;
rodata.nr_llcs = llc_count.max(1);
rodata.nr_cpus = topo.nr_cpus.min(64) as u32; for (i, &llc_id) in topo.cpu_llc_id.iter().enumerate() {
rodata.cpu_llc_id[i] = llc_id as u32;
}
}
let skel = open_skel.load().context("Failed to load BPF program")?;
Ok(Self {
skel,
args,
topology: topo,
latency_matrix,
})
}
fn run(&mut self, shutdown: Arc<AtomicBool>) -> Result<()> {
let _link = self
.skel
.maps
.cake_ops
.attach_struct_ops()
.context("Failed to attach scheduler")?;
self.show_startup_splash()?;
if self.args.verbose {
tui::run_tui(
&mut self.skel,
shutdown.clone(),
self.args.interval,
self.topology.clone(),
)?;
} else {
let mut mask = SigSet::empty();
mask.add(Signal::SIGINT);
mask.add(Signal::SIGTERM);
mask.thread_block().context("Failed to block signals")?;
let sfd = SignalFd::with_flags(&mask, SfdFlags::SFD_NONBLOCK)
.context("Failed to create signalfd")?;
use nix::poll::{poll, PollFd, PollFlags};
use std::os::fd::BorrowedFd;
loop {
let poll_fd = unsafe {
PollFd::new(BorrowedFd::borrow_raw(sfd.as_raw_fd()), PollFlags::POLLIN)
};
let mut fds = [poll_fd];
let result = poll(&mut fds, nix::poll::PollTimeout::from(60_000u16));
match result {
Ok(n) if n > 0 => {
if let Ok(Some(siginfo)) = sfd.read_signal() {
info!("Received signal {} - shutting down", siginfo.ssi_signo);
shutdown.store(true, Ordering::Relaxed);
}
break;
}
Ok(_) => {
if scx_utils::uei_exited!(&self.skel, uei) {
match scx_utils::uei_report!(&self.skel, uei) {
Ok(reason) => {
warn!("BPF scheduler exited: {:?}", reason);
}
Err(e) => {
warn!("BPF scheduler exited (failed to get reason: {})", e);
}
}
break;
}
}
Err(nix::errno::Errno::EINTR) => {
if shutdown.load(Ordering::Relaxed) {
break;
}
}
Err(e) => {
warn!("poll() error: {}", e);
break;
}
}
}
}
info!("scx_cake scheduler shutting down");
Ok(())
}
fn show_startup_splash(&self) -> Result<()> {
let (q, _nfb, starv) = self.args.effective_values();
let profile_str = format!("{:?}", self.args.profile).to_uppercase();
tui::render_startup_screen(tui::StartupParams {
topology: &self.topology,
latency_matrix: &self.latency_matrix,
profile: &profile_str,
quantum: q,
starvation: starv,
})
}
}
fn main() -> Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let args = Args::parse();
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = shutdown.clone();
ctrlc::set_handler(move || {
info!("Received shutdown signal");
shutdown_clone.store(true, Ordering::Relaxed);
})?;
let mut open_object = std::mem::MaybeUninit::uninit();
let mut scheduler = Scheduler::new(args, &mut open_object)?;
scheduler.run(shutdown)?;
Ok(())
}