1use std::collections::BTreeMap;
2use std::io::Write;
3use std::path::PathBuf;
4use std::sync::Arc;
5use std::sync::atomic::AtomicBool;
6use std::sync::atomic::Ordering;
7use std::thread::ThreadId;
8use std::time::Duration;
9
10use anyhow::bail;
11use anyhow::{Context, Result};
12use gpoint::GPoint;
13use scx_stats::prelude::*;
14use scx_stats_derive::Stats;
15use scx_stats_derive::stat_doc;
16use serde::Deserialize;
17use serde::Serialize;
18
19#[stat_doc]
20#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
21#[stat(top)]
22pub struct SysStats {
23 #[stat(desc = "Sequence ID of this message")]
24 pub mseq: u64,
25
26 #[stat(desc = "Number of runnable tasks in runqueues")]
27 pub nr_queued_task: u64,
28
29 #[stat(desc = "Number of active CPUs when core compaction is enabled")]
30 pub nr_active: u32,
31
32 #[stat(desc = "Number of context switches")]
33 pub nr_sched: u64,
34
35 #[stat(desc = "Number of task preemption triggered")]
36 pub nr_preempt: u64,
37
38 #[stat(
39 desc = "Number of re-enqueues with the KFUNC reason: the RT/DL drain and, in sub mode, the revoke sweep"
40 )]
41 pub nr_reenq_kfunc: u64,
42
43 #[stat(desc = "% of performance-critical tasks")]
44 pub pc_pc: f64,
45
46 #[stat(desc = "% of latency-critical tasks")]
47 pub pc_lc: f64,
48
49 #[stat(desc = "% of cross domain task migration")]
50 pub pc_x_migration: f64,
51
52 #[stat(desc = "Number of stealee domains")]
53 pub nr_stealee: u32,
54
55 #[stat(desc = "% of tasks scheduled on big cores")]
56 pub pc_big: f64,
57
58 #[stat(desc = "% of performance-critical tasks scheduled on big cores")]
59 pub pc_pc_on_big: f64,
60
61 #[stat(desc = "% of latency-critical tasks scheduled on big cores")]
62 pub pc_lc_on_big: f64,
63
64 #[stat(desc = "Current power mode")]
65 pub power_mode: String,
66
67 #[stat(desc = "% of performance mode")]
68 pub pc_performance: f64,
69
70 #[stat(desc = "% of balanced mode")]
71 pub pc_balanced: f64,
72
73 #[stat(desc = "% of powersave mode")]
74 pub pc_powersave: f64,
75}
76
77impl SysStats {
78 pub fn format_header<W: Write>(w: &mut W) -> Result<()> {
79 writeln!(
80 w,
81 "\x1b[93m| {:8} | {:9} | {:9} | {:8} | {:9} | {:8} | {:8} | {:8} | {:8} | {:8} | {:8} | {:8} | {:8} | {:11} | {:12} | {:12} | {:12} |\x1b[0m",
82 "MSEQ",
83 "# Q TASK",
84 "# ACT CPU",
85 "# SCHED",
86 "# PREEMPT",
87 "# REENQ",
88 "PERF-CR%",
89 "LAT-CR%",
90 "X-MIG%",
91 "# STLEE",
92 "BIG%",
93 "PC/BIG%",
94 "LC/BIG%",
95 "POWER MODE",
96 "PERFORMANCE%",
97 "BALANCED%",
98 "POWERSAVE%",
99 )?;
100 Ok(())
101 }
102
103 fn format<W: Write>(&self, w: &mut W) -> Result<()> {
104 if self.mseq % 10 == 1 {
105 Self::format_header(w)?;
106 }
107
108 let color = if self.mseq.is_multiple_of(2) {
109 "\x1b[90m" } else {
111 "\x1b[37m" };
113
114 writeln!(
115 w,
116 "{color}| {:8} | {:9} | {:9} | {:8} | {:9} | {:8} | {:8} | {:8} | {:8} | {:8} | {:8} | {:8} | {:8} | {:11} | {:12} | {:12} | {:12} |\x1b[0m",
117 self.mseq,
118 self.nr_queued_task,
119 self.nr_active,
120 self.nr_sched,
121 self.nr_preempt,
122 self.nr_reenq_kfunc,
123 GPoint(self.pc_pc),
124 GPoint(self.pc_lc),
125 GPoint(self.pc_x_migration),
126 self.nr_stealee,
127 GPoint(self.pc_big),
128 GPoint(self.pc_pc_on_big),
129 GPoint(self.pc_lc_on_big),
130 self.power_mode,
131 GPoint(self.pc_performance),
132 GPoint(self.pc_balanced),
133 GPoint(self.pc_powersave),
134 )?;
135 Ok(())
136 }
137}
138
139#[stat_doc]
140#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
141#[stat(top, _om_prefix = "s_", _om_label = "sched_sample")]
142pub struct SchedSample {
143 #[stat(desc = "Sequence ID of this message")]
144 pub mseq: u64,
145 #[stat(desc = "Process ID")]
146 pub pid: i32,
147 #[stat(desc = "Task name")]
148 pub comm: String,
149 #[stat(
150 desc = "LR: 'L'atency-critical or 'R'egular, HI: performance-'H'ungry or performance-'I'nsensitive, BT: 'B'ig or li'T'tle, EG: 'E'ligible or 'G'reedy, PN: 'P'reempting or 'N'ot"
151 )]
152 pub stat: String,
153 #[stat(desc = "CPU ID where this task is scheduled on")]
154 pub cpu_id: u32,
155 #[stat(desc = "CPU ID where a task ran last time.")]
156 pub prev_cpu_id: u32,
157 #[stat(desc = "CPU ID suggested when a task is enqueued.")]
158 pub suggested_cpu_id: u32,
159 #[stat(desc = "Waker's process ID")]
160 pub waker_pid: i32,
161 #[stat(desc = "Waker's task name")]
162 pub waker_comm: String,
163 #[stat(desc = "Assigned time slice")]
164 pub slice_wall: u64,
165 #[stat(desc = "Amount of time actually used by task in a slice")]
166 pub slice_used_wall: u64,
167 #[stat(desc = "Latency criticality of this task")]
168 pub lat_cri: u32,
169 #[stat(desc = "Average latency criticality in a system")]
170 pub avg_lat_cri: u32,
171 #[stat(desc = "Static priority (20 == nice 0)")]
172 pub static_prio: u16,
173 #[stat(desc = "Time interval from the last quiescent time to this runnable time.")]
174 pub rerunnable_interval_wall: u64,
175 #[stat(desc = "Time interval from the last stopped time.")]
176 pub resched_interval_wall: u64,
177 #[stat(desc = "How often this task is scheduled per second")]
178 pub run_freq: u64,
179 #[stat(desc = "Average runtime per schedule")]
180 pub avg_runtime_wall: u64,
181 #[stat(desc = "How frequently this task waits for other tasks")]
182 pub wait_freq: u64,
183 #[stat(desc = "How frequently this task wakes other tasks")]
184 pub wake_freq: u64,
185 #[stat(desc = "Performance criticality of this task")]
186 pub perf_cri: u32,
187 #[stat(desc = "Performance criticality threshold")]
188 pub thr_perf_cri: u32,
189 #[stat(desc = "Target performance level of this CPU")]
190 pub cpuperf_cur: u32,
191 #[stat(desc = "CPU utilization of this CPU")]
192 pub cpu_util_wall: u64,
193 #[stat(desc = "Invariant CPU utilization of this CPU scaled by CPU capacity and frequency")]
194 pub cpu_util_invr: u64,
195 #[stat(desc = "Steal utilization of this CPU (IRQ + hypervisor steal + RT/DL)")]
196 pub steal_util_wall: u64,
197 #[stat(desc = "Invariant steal utilization of this CPU scaled by CPU capacity and frequency")]
198 pub steal_util_invr: u64,
199 #[stat(desc = "Utilization of this CPU by domain-pinned tasks")]
200 pub dom_pinned_util_wall: u64,
201 #[stat(
202 desc = "Invariant utilization of this CPU by domain-pinned tasks scaled by CPU capacity and frequency"
203 )]
204 pub dom_pinned_util_invr: u64,
205 #[stat(desc = "Number of active CPUs when core compaction is enabled")]
206 pub nr_active: u32,
207 #[stat(desc = "DSQ ID where this task was dispatched from")]
208 pub dsq_id: u64,
209 #[stat(desc = "Consume latency of this DSQ (shows how contended the DSQ is)")]
210 pub dsq_consume_lat: u64,
211 #[stat(desc = "CPU's latency headroom (1024 - ravg(irq_steal_util))")]
212 pub lat_headroom: u32,
213 #[stat(desc = "Preemption vulnerability threshold step")]
214 pub vuln_thresh: u32,
215 #[stat(desc = "Task's estimated utilization from ravg")]
216 pub task_util_est: u32,
217 #[stat(desc = "Task's normalized latency criticality [0, 1024]")]
218 pub norm_lat_cri: u16,
219 #[stat(desc = "Per-CPU warmth (cache/TLB) [0, 1024]")]
220 pub cpu_heat: u16,
221 #[stat(desc = "CPU the per-CPU warmth belongs to")]
222 pub warm_cpu_id: u16,
223}
224
225impl SchedSample {
226 pub fn format_header<W: Write>(w: &mut W) -> Result<()> {
227 writeln!(
228 w,
229 "\x1b[93m| {:6} | {:7} | {:17} | {:5} | {:4} | {:8} | {:8} | {:8} | {:17} | {:8} | {:11} | {:8} | {:7} | {:8} | {:12} | {:12} | {:9} | {:9} | {:9} | {:9} | {:8} | {:8} | {:8} | {:8} | {:9} | {:10} | {:11} | {:9} | {:10} | {:6} | {:6} | {:10} | {:7} | {:6} | {:8} | {:7} | {:6} | {:8} |\x1b[0m",
230 "MSEQ",
231 "PID",
232 "COMM",
233 "STAT",
234 "CPU",
235 "PRV_CPU",
236 "SUG_CPU",
237 "WKER_PID",
238 "WKER_COMM",
239 "SLC_NS",
240 "SLC_USED_NS",
241 "LAT_CRI",
242 "AVG_LC",
243 "ST_PRIO",
244 "RERNBL_NS",
245 "RESCHD_NS",
246 "RUN_FREQ",
247 "RUN_TM_NS",
248 "WAIT_FREQ",
249 "WAKE_FREQ",
250 "PERF_CRI",
251 "THR_PC",
252 "CPUFREQ",
253 "CPU_UTIL",
254 "CPU_IUTIL",
255 "STEAL_UTIL",
256 "STEAL_IUTIL",
257 "DPIN_UTIL",
258 "DPIN_IUTIL",
259 "NR_ACT",
260 "DSQ_ID",
261 "DSQ_LAT_NS",
262 "LAT_HDR",
263 "VLN_TH",
264 "TSK_UTIL",
265 "NRM_LC",
266 "HEAT",
267 "WARM_CPU",
268 )?;
269 Ok(())
270 }
271
272 pub fn format<W: Write>(&self, w: &mut W) -> Result<()> {
273 if self.mseq % 10 == 1 {
274 Self::format_header(w)?;
275 }
276
277 writeln!(
278 w,
279 "| {:6} | {:7} | {:17} | {:5} | {:4} | {:8} | {:8} | {:8} | {:17} | {:8} | {:11} | {:8} | {:7} | {:8} | {:12} | {:12} | {:9} | {:9} | {:9} | {:9} | {:8} | {:8} | {:8} | {:8} | {:9} | {:10} | {:11} | {:9} | {:10} | {:6} | {:6} | {:10} | {:7} | {:6} | {:8} | {:7} | {:6} | {:8} |",
280 self.mseq,
281 self.pid,
282 self.comm,
283 self.stat,
284 self.cpu_id,
285 self.prev_cpu_id,
286 self.suggested_cpu_id,
287 self.waker_pid,
288 self.waker_comm,
289 self.slice_wall,
290 self.slice_used_wall,
291 self.lat_cri,
292 self.avg_lat_cri,
293 self.static_prio,
294 self.rerunnable_interval_wall,
295 self.resched_interval_wall,
296 self.run_freq,
297 self.avg_runtime_wall,
298 self.wait_freq,
299 self.wake_freq,
300 self.perf_cri,
301 self.thr_perf_cri,
302 self.cpuperf_cur,
303 self.cpu_util_wall,
304 self.cpu_util_invr,
305 self.steal_util_wall,
306 self.steal_util_invr,
307 self.dom_pinned_util_wall,
308 self.dom_pinned_util_invr,
309 self.nr_active,
310 self.dsq_id,
311 self.dsq_consume_lat,
312 self.lat_headroom,
313 self.vuln_thresh,
314 self.task_util_est,
315 self.norm_lat_cri,
316 self.cpu_heat,
317 self.warm_cpu_id,
318 )?;
319 Ok(())
320 }
321}
322
323#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
324pub struct SchedSamples {
325 pub samples: Vec<SchedSample>,
326}
327
328#[derive(Debug)]
329#[allow(clippy::enum_variant_names)]
331pub enum StatsReq {
332 NewSampler(ThreadId),
333 SysStatsReq {
334 tid: ThreadId,
335 },
336 SchedSamplesNr {
337 tid: ThreadId,
338 nr_samples: u64,
339 interval_ms: u64,
340 },
341}
342
343impl StatsReq {
344 fn from_args_stats(tid: ThreadId) -> Result<Self> {
345 Ok(Self::SysStatsReq { tid })
346 }
347
348 fn from_args_samples(
349 tid: ThreadId,
350 nr_cpus_onln: u64,
351 args: &BTreeMap<String, String>,
352 ) -> Result<Self> {
353 let mut nr_samples = 1;
354
355 if let Some(arg) = args.get("nr_samples") {
356 nr_samples = arg.trim().parse()?;
357 }
358
359 let mut interval_ms = 1000;
360 if nr_samples > nr_cpus_onln {
361 let f = nr_samples / nr_cpus_onln * 2;
363 interval_ms /= f;
364 }
365
366 Ok(Self::SchedSamplesNr {
367 tid,
368 nr_samples,
369 interval_ms,
370 })
371 }
372}
373
374#[derive(Debug)]
375pub enum StatsRes {
376 Ack,
377 Bye,
378 SysStats(SysStats),
379 SchedSamples(SchedSamples),
380}
381
382pub fn server_data(nr_cpus_onln: u64) -> StatsServerData<StatsReq, StatsRes> {
383 let open: Box<dyn StatsOpener<StatsReq, StatsRes>> = Box::new(move |(req_ch, res_ch)| {
384 let tid = std::thread::current().id();
385 req_ch.send(StatsReq::NewSampler(tid))?;
386 match res_ch.recv()? {
387 StatsRes::Ack => {}
388 res => bail!("invalid response: {:?}", res),
389 }
390
391 let read: Box<dyn StatsReader<StatsReq, StatsRes>> =
392 Box::new(move |_args, (req_ch, res_ch)| {
393 let req = StatsReq::from_args_stats(tid)?;
394 req_ch.send(req)?;
395
396 let stats = match res_ch.recv()? {
397 StatsRes::SysStats(v) => v,
398 StatsRes::Bye => bail!("preempted by another sampler"),
399 res => bail!("invalid response: {:?}", res),
400 };
401
402 stats.to_json()
403 });
404 Ok(read)
405 });
406
407 let samples_open: Box<dyn StatsOpener<StatsReq, StatsRes>> =
408 Box::new(move |(req_ch, res_ch)| {
409 let tid = std::thread::current().id();
410 req_ch.send(StatsReq::NewSampler(tid))?;
411 match res_ch.recv()? {
412 StatsRes::Ack => {}
413 res => bail!("invalid response: {:?}", res),
414 }
415
416 let read: Box<dyn StatsReader<StatsReq, StatsRes>> =
417 Box::new(move |args, (req_ch, res_ch)| {
418 let req = StatsReq::from_args_samples(tid, nr_cpus_onln, args)?;
419 req_ch.send(req)?;
420
421 let samples = match res_ch.recv()? {
422 StatsRes::SchedSamples(v) => v,
423 StatsRes::Bye => bail!("preempted by another sampler"),
424 res => bail!("invalid response: {:?}", res),
425 };
426
427 samples.to_json()
428 });
429 Ok(read)
430 });
431
432 StatsServerData::new()
433 .add_meta(SysStats::meta())
434 .add_ops("top", StatsOps { open, close: None })
435 .add_meta(SchedSample::meta())
436 .add_ops(
437 "sched_samples",
438 StatsOps {
439 open: samples_open,
440 close: None,
441 },
442 )
443}
444
445pub fn monitor_sched_samples(
446 nr_samples: u64,
447 shutdown: Arc<AtomicBool>,
448 sched_path: Option<PathBuf>,
449) -> Result<()> {
450 scx_utils::monitor_stats_sched::<SchedSamples>(
451 sched_path.as_deref(),
452 &[
453 ("target".into(), "sched_samples".into()),
454 ("nr_samples".into(), nr_samples.to_string()),
455 ],
456 Duration::from_secs(0),
457 || shutdown.load(Ordering::Relaxed),
458 |ts| {
459 let mut stdout = std::io::stdout();
460 for sample in ts.samples.iter() {
461 sample.format(&mut stdout)?;
462 }
463 Ok(())
464 },
465 )
466}
467
468pub fn monitor(
469 intv: Duration,
470 shutdown: Arc<AtomicBool>,
471 sched_path: Option<PathBuf>,
472) -> Result<()> {
473 scx_utils::monitor_stats_sched::<SysStats>(
474 sched_path.as_deref(),
475 &[],
476 intv,
477 || shutdown.load(Ordering::Relaxed),
478 |sysstats| {
479 sysstats
480 .format(&mut std::io::stdout())
481 .context("failed to format sysstats")?;
482 Ok(())
483 },
484 )
485}