1use std::collections::BTreeMap;
2use std::io::Write;
3use std::sync::Arc;
4use std::sync::atomic::AtomicBool;
5use std::sync::atomic::Ordering;
6use std::time::Duration;
7
8use anyhow::Result;
9use scx_stats::prelude::*;
10use scx_stats_derive::Stats;
11use scx_stats_derive::stat_doc;
12use serde::Deserialize;
13use serde::Serialize;
14
15use crate::console;
16
17#[stat_doc]
21#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
22#[stat(_om_prefix = "op_", _om_label = "callback")]
23pub struct OpStats {
24 #[stat(desc = "Times the callback ran")]
25 pub calls: u64,
26
27 #[stat(desc = "Nanoseconds spent in the callback")]
28 pub ns: u64,
29
30 #[stat(desc = "Runs refused because the callback was already active on the CPU")]
31 pub misses: u64,
32}
33
34#[stat_doc]
35#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
36#[stat(top)]
37pub struct Metrics {
38 #[stat(desc = "Monotonic nanoseconds covered by this sample")]
39 pub elapsed_ns: u64,
40
41 #[stat(desc = "Number of CPUs the overhead is spread over")]
42 pub nr_cpus: u64,
43
44 #[stat(desc = "Kernel BPF run-time statistics are enabled (1) or unavailable (0)")]
45 pub ops_profiled: u64,
46
47 #[stat(desc = "Periodic SIS_UTIL scan-budget updates")]
48 pub nr_sis_updates: u64,
49
50 #[stat(desc = "Sum of the scan budgets produced by SIS_UTIL updates")]
51 pub sis_scan_sum: u64,
52
53 #[stat(desc = "Bytes of arena pages the kernel has allocated")]
54 pub arena_bytes: u64,
55
56 #[stat(desc = "Size of the arena map in bytes")]
57 pub arena_max_bytes: u64,
58
59 #[stat(
60 desc = "Arena usage is known (1): counted at the allocator with --stats, or by the kernel"
61 )]
62 pub arena_counted: u64,
63
64 #[stat(desc = "Arena allocator objects currently allocated, task contexts mostly")]
65 pub arena_active_allocs: u64,
66
67 #[stat(desc = "Arena allocations that failed because the kernel refused a page")]
68 pub arena_alloc_failures: u64,
69
70 #[stat(desc = "sched_ext callbacks, keyed by name, see BPF_ENABLE_STATS")]
71 pub ops: BTreeMap<String, OpStats>,
72}
73
74impl Metrics {
75 fn delta(&self, rhs: &Self) -> Self {
77 let ops = self
78 .ops
79 .iter()
80 .map(|(name, cur)| {
81 let prev = rhs.ops.get(name).cloned().unwrap_or_default();
82 (
83 name.clone(),
84 OpStats {
85 calls: cur.calls.saturating_sub(prev.calls),
86 ns: cur.ns.saturating_sub(prev.ns),
87 misses: cur.misses.saturating_sub(prev.misses),
88 },
89 )
90 })
91 .collect();
92
93 Self {
94 elapsed_ns: self.elapsed_ns.saturating_sub(rhs.elapsed_ns),
95 nr_sis_updates: self.nr_sis_updates.saturating_sub(rhs.nr_sis_updates),
96 sis_scan_sum: self.sis_scan_sum.saturating_sub(rhs.sis_scan_sum),
97 ops,
98 ..self.clone()
99 }
100 }
101
102 fn calls(&self, op: &str) -> u64 {
103 self.ops.get(op).map(|o| o.calls).unwrap_or(0)
104 }
105
106 fn secs(&self) -> f64 {
107 (self.elapsed_ns.max(1)) as f64 / 1e9
108 }
109
110 fn ops_ns(&self) -> u64 {
111 self.ops.values().map(|o| o.ns).sum()
112 }
113
114 fn ops_cpu(&self) -> f64 {
116 self.ops_ns() as f64 / self.elapsed_ns.max(1) as f64
117 }
118
119 fn sis_avg_scan(&self) -> f64 {
120 if self.nr_sis_updates > 0 {
121 self.sis_scan_sum as f64 / self.nr_sis_updates as f64
122 } else {
123 0.0
124 }
125 }
126}
127
128const OP_ORDER: &[&str] = &[
132 "select_cid",
133 "enqueue",
134 "dequeue",
135 "dispatch",
136 "running",
137 "stopping",
138 "quiescent",
139 "tick",
140 "yield",
141 "core_sched_before",
142 "update_idle",
143 "set_weight",
144 "set_cmask",
145 "enable",
146 "init_task",
147 "exit_task",
148 "cpuctl_init",
149 "cpuctl_exit",
150 "cpuctl_set_weight",
151 "cpuctl_set_idle",
152 "cpuctl_set_bandwidth",
153 "cpuctl_move",
154 "init",
155 "exit",
156];
157
158fn op_rank(name: &str) -> usize {
159 OP_ORDER
160 .iter()
161 .position(|&n| n == name)
162 .unwrap_or(OP_ORDER.len())
163}
164
165fn human(v: f64) -> String {
167 if v >= 1e6 {
168 format!("{:.1}M", v / 1e6)
169 } else if v >= 1e4 {
170 format!("{:.0}k", v / 1e3)
171 } else if v >= 1e3 {
172 format!("{:.1}k", v / 1e3)
173 } else if v >= 10.0 || (v - v.round()).abs() < 0.05 {
174 format!("{:.0}", v)
175 } else {
176 format!("{:.1}", v)
177 }
178}
179
180pub fn human_bytes(bytes: u64) -> String {
182 const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB"];
183 let mut v = bytes as f64;
184 let mut unit = 0;
185 while v >= 1024.0 && unit + 1 < UNITS.len() {
186 v /= 1024.0;
187 unit += 1;
188 }
189 if unit == 0 {
190 format!("{bytes} B")
191 } else {
192 format!("{v:.1} {}", UNITS[unit])
193 }
194}
195
196fn human_ns(ns: f64) -> String {
198 if ns >= 1e6 {
199 format!("{:.2}ms", ns / 1e6)
200 } else if ns >= 1e3 {
201 format!("{:.2}us", ns / 1e3)
202 } else {
203 format!("{:.0}ns", ns)
204 }
205}
206
207struct Dashboard {
210 intv: Duration,
211 frames: u64,
212 started: std::time::Instant,
213}
214
215impl Dashboard {
216 fn new(intv: Duration) -> Self {
217 Self {
218 intv,
219 frames: 0,
220 started: std::time::Instant::now(),
221 }
222 }
223
224 fn render(&mut self, m: &Metrics) -> Result<()> {
225 if self.frames == 0 && m.elapsed_ns < self.intv.as_nanos() as u64 / 2 {
228 return Ok(());
229 }
230 self.frames += 1;
231 let mut buf = Vec::new();
232 self.frame(&mut buf, m)?;
233 for line in String::from_utf8_lossy(&buf).lines() {
234 console::emit(line);
235 }
236 Ok(())
237 }
238
239 fn frame<W: Write>(&self, w: &mut W, m: &Metrics) -> Result<()> {
241 let secs = m.secs();
242 let up = self.started.elapsed().as_secs();
243
244 let header = console::header(&[
245 format!("up {:02}:{:02}:{:02}", up / 3600, (up / 60) % 60, up % 60),
246 format!("{} CPUs", m.nr_cpus),
247 format!("{secs:.1} s/sample"),
248 format!("#{}", self.frames),
249 ]);
250 writeln!(w, "{header}")?;
251
252 let cpu = m.ops_cpu();
254 writeln!(
255 w,
256 "sched_ext callbacks overhead {:.2}% of one CPU ({:.3}% of {}) {}",
257 cpu * 100.0,
258 cpu * 100.0 / m.nr_cpus.max(1) as f64,
259 m.nr_cpus,
260 if m.ops_profiled == 1 {
261 "BPF_ENABLE_STATS on"
262 } else {
263 "BPF run-time stats unavailable"
264 },
265 )?;
266 writeln!(
267 w,
268 "{:<21}{:>9}{:>10}{:>9}{:>8} {}",
269 "callback", "calls/s", "avg", "cpu%", "share", "of callback time"
270 )?;
271 let mut ops: Vec<(&String, &OpStats)> = m.ops.iter().collect();
274 ops.sort_by_key(|(name, _)| (op_rank(name), (*name).clone()));
275 let total_ns = m.ops_ns().max(1) as f64;
276 let bar_w = 30;
277 for (name, o) in &ops {
278 let avg = if o.calls > 0 {
279 human_ns(o.ns as f64 / o.calls as f64)
280 } else {
281 "-".to_string()
282 };
283 let share = o.ns as f64 / total_ns;
284 let filled = (share * bar_w as f64).round() as usize;
285 let op_cpu = o.ns as f64 / m.elapsed_ns.max(1) as f64 * 100.0;
286 let misses = if o.misses > 0 {
287 format!(" {} misses", o.misses)
288 } else {
289 String::new()
290 };
291 writeln!(
292 w,
293 "{:<21}{:>9}{:>10}{:>8.2}%{:>7.1}% {}{}{misses}",
294 name,
295 human(o.calls as f64 / secs),
296 avg,
297 op_cpu,
298 share * 100.0,
299 "#".repeat(filled),
300 ".".repeat(bar_w - filled),
301 )?;
302 }
303 if ops.is_empty() {
304 writeln!(w, "no callback program found")?;
305 }
306 let per_switch = if m.calls("running") > 0 {
308 m.ops_ns() as f64 / m.calls("running") as f64
309 } else {
310 0.0
311 };
312 let rate =
314 |name: &str, op: &str| format!("{name:<8}{:>5}", human(m.calls(op) as f64 / secs));
315 writeln!(
316 w,
317 "events/s {} {} {} {}",
318 rate("wakeups", "select_cid"),
319 rate("switches", "running"),
320 rate("sleeps", "quiescent"),
321 rate("ticks", "tick"),
322 )?;
323 writeln!(
324 w,
325 " {} {} {} {}",
326 rate("yields", "yield"),
327 rate("forks", "init_task"),
328 rate("exits", "exit_task"),
329 rate("cmask", "set_cmask"),
330 )?;
331 writeln!(
332 w,
333 "callback time per context switch {}",
334 human_ns(per_switch)
335 )?;
336 writeln!(
337 w,
338 "sis updates {} avg scan budget {:.1}",
339 m.nr_sis_updates,
340 m.sis_avg_scan(),
341 )?;
342 if m.arena_counted == 1 {
343 writeln!(
344 w,
345 "BPF arena memory {} of {} ({:.2}%) active allocations {} failed allocations {}",
346 human_bytes(m.arena_bytes),
347 human_bytes(m.arena_max_bytes),
348 m.arena_bytes as f64 * 100.0 / m.arena_max_bytes.max(1) as f64,
349 m.arena_active_allocs,
350 m.arena_alloc_failures,
351 )?;
352 } else {
353 writeln!(
354 w,
355 "BPF arena memory {} reserved, usage counted with --stats active allocations {} failed allocations {}",
356 human_bytes(m.arena_max_bytes),
357 m.arena_active_allocs,
358 m.arena_alloc_failures,
359 )?;
360 }
361 Ok(())
362 }
363}
364
365pub fn server_data() -> StatsServerData<(), Metrics> {
366 let open: Box<dyn StatsOpener<(), Metrics>> = Box::new(move |(req_ch, res_ch)| {
367 req_ch.send(())?;
368 let mut prev = res_ch.recv()?;
369
370 let read: Box<dyn StatsReader<(), Metrics>> = Box::new(move |_args, (req_ch, res_ch)| {
371 req_ch.send(())?;
372 let cur = res_ch.recv()?;
373 let delta = cur.delta(&prev);
374 prev = cur;
375 delta.to_json()
376 });
377
378 Ok(read)
379 });
380
381 StatsServerData::new()
382 .add_meta(OpStats::meta())
383 .add_meta(Metrics::meta())
384 .add_ops("top", StatsOps { open, close: None })
385}
386
387pub fn monitor(intv: Duration, shutdown: Arc<AtomicBool>) -> Result<()> {
388 let mut dash = Dashboard::new(intv);
389 scx_utils::monitor_stats::<Metrics>(
390 &[],
391 intv,
392 || shutdown.load(Ordering::Relaxed),
393 |metrics| dash.render(&metrics),
394 )
395}