Skip to main content

scx_mitosis/
stats.rs

1use std::collections::BTreeMap;
2use std::io::Write;
3use std::sync::Arc;
4use std::sync::atomic::AtomicBool;
5use std::sync::atomic::Ordering;
6use std::time::Duration;
7
8use anyhow::Result;
9use serde::Deserialize;
10use serde::Serialize;
11
12use scx_stats::prelude::*;
13use scx_stats_derive::Stats;
14use scx_stats_derive::stat_doc;
15
16use crate::DistributionStats;
17
18#[stat_doc]
19#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
20#[stat(_om_prefix = "s_")]
21#[stat(top)]
22pub struct SubcellMetrics {
23    #[stat(desc = "Subcell name")]
24    pub name: String,
25    #[stat(desc = "Number of cpus")]
26    pub num_cpus: u32,
27    #[stat(desc = "CPU utilization %")]
28    pub util_pct: f64,
29    #[stat(desc = "Borrowed CPU time % of running")]
30    pub demand_borrow_pct: f64,
31    #[stat(desc = "Lent CPU time %")]
32    pub lent_pct: f64,
33    #[stat(desc = "EWMA-smoothed utilization %")]
34    pub smoothed_util_pct: f64,
35    #[stat(desc = "Running time in this interval")]
36    pub running_ns: u64,
37    #[stat(desc = "Borrowed running time in this interval")]
38    pub borrowed_ns: u64,
39    #[stat(desc = "Lent running time in this interval")]
40    pub lent_ns: u64,
41    #[stat(desc = "Time spent runnable but waiting for a CPU in this interval")]
42    pub queued_ns: u64,
43    #[stat(desc = "Average runnable tasks over the interval (running + queued)")]
44    pub load: f64,
45    #[stat(desc = "EWMA-smoothed average runnable tasks")]
46    pub smoothed_load: f64,
47    #[stat(desc = "Demand: sum of the tasks' unconstrained CPU usage, in CPUs")]
48    pub demand: f64,
49    #[stat(desc = "EWMA-smoothed demand")]
50    pub smoothed_demand: f64,
51}
52
53impl SubcellMetrics {
54    pub fn update_name_and_cpus(&mut self, name: &str, num_cpus: u32) {
55        self.name = name.to_string();
56        self.num_cpus = num_cpus;
57    }
58
59    pub fn update_demand(
60        &mut self,
61        util_pct: f64,
62        demand_borrow_pct: f64,
63        lent_pct: f64,
64        running_ns: u64,
65        borrowed_ns: u64,
66        lent_ns: u64,
67        queued_ns: u64,
68        load: f64,
69        demand: f64,
70    ) {
71        self.util_pct = util_pct;
72        self.demand_borrow_pct = demand_borrow_pct;
73        self.lent_pct = lent_pct;
74        self.running_ns = running_ns;
75        self.borrowed_ns = borrowed_ns;
76        self.lent_ns = lent_ns;
77        self.queued_ns = queued_ns;
78        self.load = load;
79        self.demand = demand;
80    }
81}
82
83#[stat_doc]
84#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
85#[stat(_om_prefix = "c_")]
86#[stat(top)]
87pub struct CellMetrics {
88    #[stat(desc = "Number of cpus")]
89    pub num_cpus: u32,
90    #[stat(
91        desc = "Cgroup path of this cell, relative to the cgroup root (\"/\" for the root cell)",
92        _om_skip
93    )]
94    pub cgroup_path: String,
95    #[stat(desc = "Local queue %")]
96    pub local_q_pct: f64,
97    #[stat(desc = "CPU queue %")]
98    pub cpu_q_pct: f64,
99    #[stat(desc = "Cell queue %")]
100    pub cell_q_pct: f64,
101    #[stat(desc = "Borrowed CPU %")]
102    pub borrowed_pct: f64,
103    #[stat(desc = "Affinity violations % of global")]
104    pub affn_violations_pct: f64,
105    #[stat(desc = "Steal %")]
106    pub steal_pct: f64,
107    #[stat(desc = "Orphaned LLC DSQ drain events")]
108    pub drain_cnt: u64,
109    #[stat(desc = "Orphaned LLC DSQ affinity rescue events")]
110    pub drain_affn_cnt: u64,
111    #[stat(desc = "Pin reject skipped %")]
112    pub pin_skip_pct: f64,
113    #[stat(desc = "Slice shrink events")]
114    pub slice_shrink: u64,
115    #[stat(desc = "Slice shrink at max")]
116    pub slice_shrink_max: u64,
117    #[stat(desc = "Slice shrink proportional")]
118    pub slice_shrink_proportional: u64,
119    #[stat(desc = "Slice shrink at min")]
120    pub slice_shrink_min: u64,
121    #[stat(desc = "Decision share % of global")]
122    pub share_of_decisions_pct: f64,
123    #[stat(desc = "Cell scheduling decisions")]
124    total_decisions: u64,
125    #[stat(desc = "CPU utilization %")]
126    pub util_pct: f64,
127    #[stat(desc = "Borrowed CPU time % of running")]
128    pub demand_borrow_pct: f64,
129    #[stat(desc = "Lent CPU time %")]
130    pub lent_pct: f64,
131    #[stat(desc = "EWMA-smoothed utilization %")]
132    pub smoothed_util_pct: f64,
133    #[stat(desc = "Per-subcell metrics")]
134    pub subcells: BTreeMap<u32, SubcellMetrics>,
135}
136
137impl CellMetrics {
138    pub fn update(&mut self, ds: &DistributionStats) {
139        self.local_q_pct = ds.local_q_pct;
140        self.cpu_q_pct = ds.cpu_q_pct;
141        self.cell_q_pct = ds.cell_q_pct;
142        self.borrowed_pct = ds.borrowed_pct;
143        self.affn_violations_pct = ds.affn_viol_pct;
144        self.steal_pct = ds.steal_pct;
145        self.pin_skip_pct = ds.pin_skip_pct;
146        self.share_of_decisions_pct = ds.share_of_decisions_pct;
147        self.total_decisions = ds.total_decisions;
148    }
149
150    pub fn update_demand(&mut self, util_pct: f64, demand_borrow_pct: f64, lent_pct: f64) {
151        self.util_pct = util_pct;
152        self.demand_borrow_pct = demand_borrow_pct;
153        self.lent_pct = lent_pct;
154    }
155}
156
157#[stat_doc]
158#[derive(Clone, Debug, Default, Serialize, Deserialize, Stats)]
159#[stat(top)]
160pub struct Metrics {
161    #[stat(desc = "Number of cells")]
162    pub num_cells: u32,
163    #[stat(desc = "Local queue %")]
164    pub local_q_pct: f64,
165    #[stat(desc = "CPU queue %")]
166    pub cpu_q_pct: f64,
167    #[stat(desc = "Cell queue %")]
168    pub cell_q_pct: f64,
169    #[stat(desc = "Borrowed CPU %")]
170    pub borrowed_pct: f64,
171    #[stat(desc = "Affinity violations % of global")]
172    pub affn_violations_pct: f64,
173    #[stat(desc = "Steal %")]
174    pub steal_pct: f64,
175    #[stat(desc = "Orphaned LLC DSQ drain events")]
176    pub drain_cnt: u64,
177    #[stat(desc = "Orphaned LLC DSQ affinity rescue events")]
178    pub drain_affn_cnt: u64,
179    #[stat(desc = "Pin reject skipped %")]
180    pub pin_skip_pct: f64,
181    #[stat(desc = "Slice shrink events")]
182    pub slice_shrink: u64,
183    #[stat(desc = "Slice shrink at max")]
184    pub slice_shrink_max: u64,
185    #[stat(desc = "Slice shrink proportional")]
186    pub slice_shrink_proportional: u64,
187    #[stat(desc = "Slice shrink at min")]
188    pub slice_shrink_min: u64,
189    #[stat(desc = "Decision share % of global")]
190    pub share_of_decisions_pct: f64,
191    #[stat(desc = "Cell scheduling decisions")]
192    total_decisions: u64,
193    #[stat(desc = "CPU utilization %")]
194    pub util_pct: f64,
195    #[stat(desc = "Borrowed CPU time % of running")]
196    pub demand_borrow_pct: f64,
197    #[stat(desc = "Lent CPU time %")]
198    pub lent_pct: f64,
199    #[stat(desc = "Number of rebalancing events")]
200    pub rebalance_count: u64,
201    #[stat(
202        desc = "1 if the cell-0 holdout has taken a CPU already claimed by a workload cell, else 0"
203    )]
204    pub enforced_holdout: u64,
205    #[stat(desc = "Per-cell metrics")]
206    pub cells: BTreeMap<u32, CellMetrics>,
207}
208
209impl Metrics {
210    pub fn update(&mut self, ds: &DistributionStats) {
211        self.local_q_pct = ds.local_q_pct;
212        self.cpu_q_pct = ds.cpu_q_pct;
213        self.cell_q_pct = ds.cell_q_pct;
214        self.borrowed_pct = ds.borrowed_pct;
215        self.affn_violations_pct = ds.affn_viol_pct;
216        self.steal_pct = ds.steal_pct;
217        self.pin_skip_pct = ds.pin_skip_pct;
218        self.share_of_decisions_pct = ds.share_of_decisions_pct;
219        self.total_decisions = ds.total_decisions;
220    }
221
222    pub fn update_demand(&mut self, util_pct: f64, demand_borrow_pct: f64, lent_pct: f64) {
223        self.util_pct = util_pct;
224        self.demand_borrow_pct = demand_borrow_pct;
225        self.lent_pct = lent_pct;
226    }
227
228    fn delta(&self, _: &Self) -> Self {
229        Self { ..self.clone() }
230    }
231
232    fn format<W: Write>(&self, w: &mut W) -> Result<()> {
233        writeln!(w, "{}", serde_json::to_string_pretty(self)?)?;
234        Ok(())
235    }
236}
237
238pub fn server_data() -> StatsServerData<(), Metrics> {
239    let open: Box<dyn StatsOpener<(), Metrics>> = Box::new(move |(req_ch, res_ch)| {
240        req_ch.send(())?;
241        let mut prev = res_ch.recv()?;
242
243        let read: Box<dyn StatsReader<(), Metrics>> = Box::new(move |_args, (req_ch, res_ch)| {
244            req_ch.send(())?;
245            let cur = res_ch.recv()?;
246            let delta = cur.delta(&prev);
247            prev = cur;
248            delta.to_json()
249        });
250
251        Ok(read)
252    });
253
254    StatsServerData::new()
255        .add_meta(Metrics::meta())
256        .add_meta(CellMetrics::meta())
257        .add_meta(SubcellMetrics::meta())
258        .add_ops("top", StatsOps { open, close: None })
259}
260
261pub fn monitor(intv: Duration, shutdown: Arc<AtomicBool>) -> Result<()> {
262    scx_utils::monitor_stats::<Metrics>(
263        &[],
264        intv,
265        || shutdown.load(Ordering::Relaxed),
266        |metrics| metrics.format(&mut std::io::stdout()),
267    )
268}