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}