scxcash/
main.rs

1// Copyright (c) Meta Platforms, Inc. and affiliates.
2
3// This software may be used and distributed according to the terms of the
4// GNU General Public License version 2.
5
6use anyhow::Result;
7use clap::Parser;
8use log::*;
9
10mod monitors;
11use monitors::HintsTlsMonitor;
12use monitors::{CacheMonitor, CacheMonitorValue, PerfSampleMonitor, SoftDirtyCacheMonitor};
13use std::mem::MaybeUninit;
14pub mod bpf_intf;
15pub mod bpf_skel; // generated at build time
16pub use bpf_skel as bpf;
17
18/// scxcash: Cache Usage Analyzer for sched_ext Schedulers
19#[derive(Debug, Parser)]
20#[command(verbatim_doc_comment)]
21struct Opts {
22    /// PID of the process to monitor for cache usage events.
23    #[clap(short = 'p', long)]
24    pid: Option<u32>,
25
26    /// Enable verbose output, including libbpf details. Specify multiple
27    /// times to increase verbosity.
28    #[clap(short = 'v', long, action = clap::ArgAction::Count)]
29    verbose: u8,
30
31    /// Enable the soft-dirty cache monitor. If no monitor selection flags are
32    /// provided, this monitor is enabled by default.
33    #[clap(long, action = clap::ArgAction::SetTrue)]
34    soft_dirty: bool,
35
36    /// Enable perf sampling monitor (prototype: samples HW cycles for now).
37    #[clap(long, action = clap::ArgAction::SetTrue)]
38    perf_sample: bool,
39
40    /// Perf sampling frequency (samples per second).
41    #[clap(long = "perf-freq", default_value_t = 1000)]
42    perf_freq: u64,
43
44    /// Output consumed monitor values as JSON (one per line). When not set,
45    /// values are printed using Debug formatting.
46    #[clap(long, action = clap::ArgAction::SetTrue)]
47    json: bool,
48
49    /// Interval in seconds between poll/consume cycles. Defaults to 1s.
50    #[clap(short = 'i', long, default_value_t = 1)]
51    interval: u64,
52
53    /// Ring buffer size in megabytes (default is 4 MB).
54    #[clap(long, default_value_t = 4)]
55    ring_mb: u32,
56
57    /// Monitor task-local storage updates for a hint map.
58    #[clap(long, action = clap::ArgAction::SetTrue)]
59    hints: bool,
60
61    /// Pin path of the task hint TLS map (mandatory with --hints).
62    #[clap(long = "hints-map")]
63    hints_map: Option<String>,
64}
65
66fn main() -> Result<()> {
67    let opts = Opts::parse();
68
69    let llv = match opts.verbose {
70        0 => simplelog::LevelFilter::Info,
71        1 => simplelog::LevelFilter::Debug,
72        _ => simplelog::LevelFilter::Trace,
73    };
74
75    let mut lcfg = simplelog::ConfigBuilder::new();
76    lcfg.set_time_offset_to_local()
77        .expect("Failed to set local time offset")
78        .set_time_level(simplelog::LevelFilter::Error)
79        .set_location_level(simplelog::LevelFilter::Off)
80        .set_target_level(simplelog::LevelFilter::Off)
81        .set_thread_level(simplelog::LevelFilter::Off);
82
83    simplelog::TermLogger::init(
84        llv,
85        lcfg.build(),
86        simplelog::TerminalMode::Stderr,
87        simplelog::ColorChoice::Auto,
88    )?;
89
90    // TODO(kkd): Set libbpf verbosity
91
92    let pid_opt = opts.pid;
93
94    let any_monitor_flag = opts.soft_dirty || opts.perf_sample || opts.hints;
95    // For now support at most two monitors; use separate storages to simplify lifetimes.
96    let mut soft_dirty_open: MaybeUninit<libbpf_rs::OpenObject> = MaybeUninit::uninit();
97    let mut perf_open: MaybeUninit<libbpf_rs::OpenObject> = MaybeUninit::uninit();
98    let mut hints_open: MaybeUninit<libbpf_rs::OpenObject> = MaybeUninit::uninit();
99
100    let mut monitors: Vec<Box<dyn CacheMonitor<'_>>> = Vec::new();
101
102    let ring_size_bytes = (opts.ring_mb as u64)
103        .saturating_mul(1024 * 1024)
104        .max(4 * 1024);
105
106    if !any_monitor_flag || opts.soft_dirty {
107        let monitor = SoftDirtyCacheMonitor::new(&mut soft_dirty_open, pid_opt, ring_size_bytes)?;
108        monitors.push(Box::new(monitor));
109    }
110    if !any_monitor_flag || opts.perf_sample {
111        let monitor = PerfSampleMonitor::new(&mut perf_open, pid_opt, opts.perf_freq)?;
112        monitors.push(Box::new(monitor));
113    }
114
115    if !any_monitor_flag || opts.hints {
116        let pin_path = opts
117            .hints_map
118            .as_deref()
119            .ok_or_else(|| anyhow::anyhow!("--hints-map must be provided with --hints"))?;
120        let monitor = HintsTlsMonitor::new(&mut hints_open, pin_path, ring_size_bytes)?;
121        monitors.push(Box::new(monitor));
122    }
123
124    if monitors.is_empty() {
125        return Err(anyhow::anyhow!("No cache monitors selected"));
126    }
127
128    let sleep_dur = std::time::Duration::from_secs(opts.interval.max(1));
129    let shutdown_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
130    let sf_clone = shutdown_flag.clone();
131    ctrlc::set_handler(move || {
132        sf_clone.store(true, std::sync::atomic::Ordering::Relaxed);
133    })?;
134
135    while !shutdown_flag.load(std::sync::atomic::Ordering::Relaxed) {
136        for m in monitors.iter_mut() {
137            m.poll()?;
138        }
139
140        std::thread::sleep(sleep_dur);
141
142        for m in monitors.iter_mut() {
143            if opts.json {
144                m.consume(
145                    &mut |v: CacheMonitorValue| match serde_json::to_string(&v) {
146                        Ok(s) => println!("{}", s),
147                        Err(e) => error!("Failed to serialize value: {e}"),
148                    },
149                )?;
150            } else {
151                m.consume(&mut |v: CacheMonitorValue| println!("{:?}", v))?;
152            }
153        }
154    }
155
156    Ok(())
157}