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