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