Skip to main content

scx_lavd/
stats.rs

1use std::collections::BTreeMap;
2use std::io::Write;
3use std::sync::atomic::AtomicBool;
4use std::sync::atomic::Ordering;
5use std::sync::Arc;
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::stat_doc;
14use scx_stats_derive::Stats;
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 % 2 == 0 {
102            "\x1b[90m" // Dark gray for even mseq
103        } else {
104            "\x1b[37m" // white for odd mseq
105        };
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)]
321pub enum StatsReq {
322    NewSampler(ThreadId),
323    SysStatsReq {
324        tid: ThreadId,
325    },
326    SchedSamplesNr {
327        tid: ThreadId,
328        nr_samples: u64,
329        interval_ms: u64,
330    },
331}
332
333impl StatsReq {
334    fn from_args_stats(tid: ThreadId) -> Result<Self> {
335        Ok(Self::SysStatsReq { tid })
336    }
337
338    fn from_args_samples(
339        tid: ThreadId,
340        nr_cpus_onln: u64,
341        args: &BTreeMap<String, String>,
342    ) -> Result<Self> {
343        let mut nr_samples = 1;
344
345        if let Some(arg) = args.get("nr_samples") {
346            nr_samples = arg.trim().parse()?;
347        }
348
349        let mut interval_ms = 1000;
350        if nr_samples > nr_cpus_onln {
351            // More samples, shorter sampling interval.
352            let f = nr_samples / nr_cpus_onln * 2;
353            interval_ms /= f;
354        }
355
356        Ok(Self::SchedSamplesNr {
357            tid,
358            nr_samples,
359            interval_ms,
360        })
361    }
362}
363
364#[derive(Debug)]
365pub enum StatsRes {
366    Ack,
367    Bye,
368    SysStats(SysStats),
369    SchedSamples(SchedSamples),
370}
371
372pub fn server_data(nr_cpus_onln: u64) -> StatsServerData<StatsReq, StatsRes> {
373    let open: Box<dyn StatsOpener<StatsReq, StatsRes>> = Box::new(move |(req_ch, res_ch)| {
374        let tid = std::thread::current().id();
375        req_ch.send(StatsReq::NewSampler(tid))?;
376        match res_ch.recv()? {
377            StatsRes::Ack => {}
378            res => bail!("invalid response: {:?}", res),
379        }
380
381        let read: Box<dyn StatsReader<StatsReq, StatsRes>> =
382            Box::new(move |_args, (req_ch, res_ch)| {
383                let req = StatsReq::from_args_stats(tid)?;
384                req_ch.send(req)?;
385
386                let stats = match res_ch.recv()? {
387                    StatsRes::SysStats(v) => v,
388                    StatsRes::Bye => bail!("preempted by another sampler"),
389                    res => bail!("invalid response: {:?}", res),
390                };
391
392                stats.to_json()
393            });
394        Ok(read)
395    });
396
397    let samples_open: Box<dyn StatsOpener<StatsReq, StatsRes>> =
398        Box::new(move |(req_ch, res_ch)| {
399            let tid = std::thread::current().id();
400            req_ch.send(StatsReq::NewSampler(tid))?;
401            match res_ch.recv()? {
402                StatsRes::Ack => {}
403                res => bail!("invalid response: {:?}", res),
404            }
405
406            let read: Box<dyn StatsReader<StatsReq, StatsRes>> =
407                Box::new(move |args, (req_ch, res_ch)| {
408                    let req = StatsReq::from_args_samples(tid, nr_cpus_onln, args)?;
409                    req_ch.send(req)?;
410
411                    let samples = match res_ch.recv()? {
412                        StatsRes::SchedSamples(v) => v,
413                        StatsRes::Bye => bail!("preempted by another sampler"),
414                        res => bail!("invalid response: {:?}", res),
415                    };
416
417                    samples.to_json()
418                });
419            Ok(read)
420        });
421
422    StatsServerData::new()
423        .add_meta(SysStats::meta())
424        .add_ops("top", StatsOps { open, close: None })
425        .add_meta(SchedSample::meta())
426        .add_ops(
427            "sched_samples",
428            StatsOps {
429                open: samples_open,
430                close: None,
431            },
432        )
433}
434
435pub fn monitor_sched_samples(nr_samples: u64, shutdown: Arc<AtomicBool>) -> Result<()> {
436    scx_utils::monitor_stats::<SchedSamples>(
437        &vec![
438            ("target".into(), "sched_samples".into()),
439            ("nr_samples".into(), nr_samples.to_string()),
440        ],
441        Duration::from_secs(0),
442        || shutdown.load(Ordering::Relaxed),
443        |ts| {
444            let mut stdout = std::io::stdout();
445            for sample in ts.samples.iter() {
446                sample.format(&mut stdout)?;
447            }
448            Ok(())
449        },
450    )
451}
452
453pub fn monitor(intv: Duration, shutdown: Arc<AtomicBool>) -> Result<()> {
454    scx_utils::monitor_stats::<SysStats>(
455        &[],
456        intv,
457        || shutdown.load(Ordering::Relaxed),
458        |sysstats| {
459            sysstats
460                .format(&mut std::io::stdout())
461                .context("failed to format sysstats")?;
462            Ok(())
463        },
464    )
465}