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: u64,
157    #[stat(desc = "Amount of time actually used by task in a slice")]
158    pub slice_used: 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: u64,
167    #[stat(desc = "Time interval from the last stopped time.")]
168    pub resched_interval: 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: 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: u64,
185    #[stat(desc = "Scaled CPU utilization of this CPU")]
186    pub cpu_sutil: u64,
187    #[stat(desc = "Number of active CPUs when core compaction is enabled")]
188    pub nr_active: u32,
189    #[stat(desc = "DSQ ID where this task was dispatched from")]
190    pub dsq_id: u64,
191    #[stat(desc = "Consume latency of this DSQ (shows how contended the DSQ is)")]
192    pub dsq_consume_lat: u64,
193}
194
195impl SchedSample {
196    pub fn format_header<W: Write>(w: &mut W) -> Result<()> {
197        writeln!(
198            w,
199            "\x1b[93m| {:6} | {:7} | {:17} | {:5} | {:4} | {:8} | {:8} | {:8} | {:17} | {:8} | {:8} | {:8} | {:7} | {:8} | {:12} | {:12} | {:9} | {:9} | {:9} | {:9} | {:8} | {:8} | {:8} | {:8} | {:9} | {:6} | {:6} | {:10} |\x1b[0m",
200            "MSEQ",
201            "PID",
202            "COMM",
203            "STAT",
204            "CPU",
205            "PRV_CPU",
206            "SUG_CPU",
207            "WKER_PID",
208            "WKER_COMM",
209            "SLC_NS",
210            "SLC_USED_NS",
211            "LAT_CRI",
212            "AVG_LC",
213            "ST_PRIO",
214            "RERNBL_NS",
215            "RESCHD_NS",
216            "RUN_FREQ",
217            "RUN_TM_NS",
218            "WAIT_FREQ",
219            "WAKE_FREQ",
220            "PERF_CRI",
221            "THR_PC",
222            "CPUFREQ",
223            "CPU_UTIL",
224            "CPU_SUTIL",
225            "NR_ACT",
226            "DSQ_ID",
227            "DSQ_LAT_NS",
228        )?;
229        Ok(())
230    }
231
232    pub fn format<W: Write>(&self, w: &mut W) -> Result<()> {
233        if self.mseq % 10 == 1 {
234            Self::format_header(w)?;
235        }
236
237        writeln!(
238            w,
239            "| {:6} | {:7} | {:17} | {:5} | {:4} | {:8} | {:8} | {:8} | {:17} | {:8} | {:8} | {:8} | {:7} | {:8} | {:12} | {:12} | {:9} | {:9} | {:9} | {:9} | {:8} | {:8} | {:8} | {:8} | {:9} | {:6} | {:12} | {:12} |",
240            self.mseq,
241            self.pid,
242            self.comm,
243            self.stat,
244            self.cpu_id,
245            self.prev_cpu_id,
246            self.suggested_cpu_id,
247            self.waker_pid,
248            self.waker_comm,
249            self.slice,
250            self.slice_used,
251            self.lat_cri,
252            self.avg_lat_cri,
253            self.static_prio,
254            self.rerunnable_interval,
255            self.resched_interval,
256            self.run_freq,
257            self.avg_runtime,
258            self.wait_freq,
259            self.wake_freq,
260            self.perf_cri,
261            self.thr_perf_cri,
262            self.cpuperf_cur,
263            self.cpu_util,
264            self.cpu_sutil,
265            self.nr_active,
266            self.dsq_id,
267            self.dsq_consume_lat,
268        )?;
269        Ok(())
270    }
271}
272
273#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
274pub struct SchedSamples {
275    pub samples: Vec<SchedSample>,
276}
277
278#[derive(Debug)]
279pub enum StatsReq {
280    NewSampler(ThreadId),
281    SysStatsReq {
282        tid: ThreadId,
283    },
284    SchedSamplesNr {
285        tid: ThreadId,
286        nr_samples: u64,
287        interval_ms: u64,
288    },
289}
290
291impl StatsReq {
292    fn from_args_stats(tid: ThreadId) -> Result<Self> {
293        Ok(Self::SysStatsReq { tid })
294    }
295
296    fn from_args_samples(
297        tid: ThreadId,
298        nr_cpus_onln: u64,
299        args: &BTreeMap<String, String>,
300    ) -> Result<Self> {
301        let mut nr_samples = 1;
302
303        if let Some(arg) = args.get("nr_samples") {
304            nr_samples = arg.trim().parse()?;
305        }
306
307        let mut interval_ms = 1000;
308        if nr_samples > nr_cpus_onln {
309            // More samples, shorter sampling interval.
310            let f = nr_samples / nr_cpus_onln * 2;
311            interval_ms /= f;
312        }
313
314        Ok(Self::SchedSamplesNr {
315            tid,
316            nr_samples,
317            interval_ms,
318        })
319    }
320}
321
322#[derive(Debug)]
323pub enum StatsRes {
324    Ack,
325    Bye,
326    SysStats(SysStats),
327    SchedSamples(SchedSamples),
328}
329
330pub fn server_data(nr_cpus_onln: u64) -> StatsServerData<StatsReq, StatsRes> {
331    let open: Box<dyn StatsOpener<StatsReq, StatsRes>> = Box::new(move |(req_ch, res_ch)| {
332        let tid = std::thread::current().id();
333        req_ch.send(StatsReq::NewSampler(tid))?;
334        match res_ch.recv()? {
335            StatsRes::Ack => {}
336            res => bail!("invalid response: {:?}", res),
337        }
338
339        let read: Box<dyn StatsReader<StatsReq, StatsRes>> =
340            Box::new(move |_args, (req_ch, res_ch)| {
341                let req = StatsReq::from_args_stats(tid)?;
342                req_ch.send(req)?;
343
344                let stats = match res_ch.recv()? {
345                    StatsRes::SysStats(v) => v,
346                    StatsRes::Bye => bail!("preempted by another sampler"),
347                    res => bail!("invalid response: {:?}", res),
348                };
349
350                stats.to_json()
351            });
352        Ok(read)
353    });
354
355    let samples_open: Box<dyn StatsOpener<StatsReq, StatsRes>> =
356        Box::new(move |(req_ch, res_ch)| {
357            let tid = std::thread::current().id();
358            req_ch.send(StatsReq::NewSampler(tid))?;
359            match res_ch.recv()? {
360                StatsRes::Ack => {}
361                res => bail!("invalid response: {:?}", res),
362            }
363
364            let read: Box<dyn StatsReader<StatsReq, StatsRes>> =
365                Box::new(move |args, (req_ch, res_ch)| {
366                    let req = StatsReq::from_args_samples(tid, nr_cpus_onln, args)?;
367                    req_ch.send(req)?;
368
369                    let samples = match res_ch.recv()? {
370                        StatsRes::SchedSamples(v) => v,
371                        StatsRes::Bye => bail!("preempted by another sampler"),
372                        res => bail!("invalid response: {:?}", res),
373                    };
374
375                    samples.to_json()
376                });
377            Ok(read)
378        });
379
380    StatsServerData::new()
381        .add_meta(SysStats::meta())
382        .add_ops("top", StatsOps { open, close: None })
383        .add_meta(SchedSample::meta())
384        .add_ops(
385            "sched_samples",
386            StatsOps {
387                open: samples_open,
388                close: None,
389            },
390        )
391}
392
393pub fn monitor_sched_samples(nr_samples: u64, shutdown: Arc<AtomicBool>) -> Result<()> {
394    scx_utils::monitor_stats::<SchedSamples>(
395        &vec![
396            ("target".into(), "sched_samples".into()),
397            ("nr_samples".into(), nr_samples.to_string()),
398        ],
399        Duration::from_secs(0),
400        || shutdown.load(Ordering::Relaxed),
401        |ts| {
402            let mut stdout = std::io::stdout();
403            for sample in ts.samples.iter() {
404                sample.format(&mut stdout)?;
405            }
406            Ok(())
407        },
408    )
409}
410
411pub fn monitor(intv: Duration, shutdown: Arc<AtomicBool>) -> Result<()> {
412    scx_utils::monitor_stats::<SysStats>(
413        &vec![],
414        intv,
415        || shutdown.load(Ordering::Relaxed),
416        |sysstats| {
417            sysstats
418                .format(&mut std::io::stdout())
419                .context("failed to format sysstats")?;
420            Ok(())
421        },
422    )
423}