Skip to main content

scx_mavd/
stats.rs

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" // Dark gray for even mseq
110        } else {
111            "\x1b[37m" // white for odd mseq
112        };
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// Renaming the *Req variants is an API decision for the scheduler author.
330#[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            // More samples, shorter sampling interval.
362            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}