scx_utils/
misc.rs

1use anyhow::Result;
2use anyhow::{anyhow, Context};
3use log::{info, warn};
4use nix::sys::resource::{getrlimit, setrlimit, Resource, RLIM_INFINITY};
5use scx_stats::prelude::*;
6use serde::Deserialize;
7use std::path::Path;
8use std::thread::sleep;
9use std::time::Duration;
10
11pub fn monitor_stats<T>(
12    stats_args: &[(String, String)],
13    intv: Duration,
14    mut should_exit: impl FnMut() -> bool,
15    mut output: impl FnMut(T) -> Result<()>,
16) -> Result<()>
17where
18    T: for<'a> Deserialize<'a>,
19{
20    let mut retry_cnt: u32 = 0;
21
22    const RETRYABLE_ERRORS: [std::io::ErrorKind; 2] = [
23        std::io::ErrorKind::NotFound,
24        std::io::ErrorKind::ConnectionRefused,
25    ];
26
27    while !should_exit() {
28        let mut client = match StatsClient::new().connect() {
29            Ok(v) => v,
30            Err(e) => match e.downcast_ref::<std::io::Error>() {
31                Some(ioe) if RETRYABLE_ERRORS.contains(&ioe.kind()) => {
32                    if retry_cnt == 1 {
33                        info!("Stats server not available, retrying...");
34                    }
35                    retry_cnt += 1;
36                    sleep(Duration::from_secs(1));
37                    continue;
38                }
39                _ => Err(e)?,
40            },
41        };
42        retry_cnt = 0;
43
44        while !should_exit() {
45            let stats = match client.request::<T>("stats", stats_args.to_owned()) {
46                Ok(v) => v,
47                Err(e) => {
48                    if let Some(ioe) = e.downcast_ref::<std::io::Error>() {
49                        info!("Connection to stats_server failed ({ioe})");
50                    } else {
51                        warn!("Error handling stats_server result: {e}");
52                    }
53                    sleep(Duration::from_secs(1));
54                    break;
55                }
56            };
57            output(stats)?;
58            sleep(intv);
59        }
60    }
61
62    Ok(())
63}
64
65pub fn try_set_rlimit_infinity() {
66    // Increase MEMLOCK size since the BPF scheduler might use
67    // more than the current limit
68    if setrlimit(Resource::RLIMIT_MEMLOCK, RLIM_INFINITY, RLIM_INFINITY).is_err() {
69        // If there is an error in expanding rlimit to infinity,
70        // show the current rlimits then proceed.
71        if let Ok((soft, hard)) = getrlimit(Resource::RLIMIT_MEMLOCK) {
72            warn!("Current MEMLOCK limit: soft={soft}, hard={hard}");
73        } else {
74            warn!("Cannot change or query MEMLOCK limit");
75        }
76    }
77}
78
79/// Read a file and parse its content into the specified type.
80///
81/// Trims null and whitespace before parsing.
82///
83/// # Errors
84/// Returns an error if reading or parsing fails.
85pub fn read_from_file<T>(path: &Path) -> Result<T>
86where
87    T: std::str::FromStr,
88    T::Err: std::error::Error + Send + Sync + 'static,
89{
90    let val = std::fs::read_to_string(path)
91        .with_context(|| format!("Failed to open or read file: {}", path.display()))?;
92
93    let val = val.trim_end_matches('\0').trim();
94
95    val.parse::<T>()
96        .with_context(|| format!("Failed to parse content '{}' from {}", val, path.display()))
97}
98
99pub fn read_file_usize_vec(path: &Path, separator: char) -> Result<Vec<usize>> {
100    let val = std::fs::read_to_string(path)?;
101    let val = val.trim_end_matches('\0');
102
103    val.split(separator)
104        .map(|s| {
105            s.trim()
106                .parse::<usize>()
107                .map_err(|_| anyhow!("Failed to parse '{}' as usize", s))
108        })
109        .collect::<Result<Vec<usize>>>()
110}
111
112pub fn read_file_byte(path: &Path) -> Result<usize> {
113    let val = std::fs::read_to_string(path)?;
114    let val = val.trim_end_matches('\0');
115    let val = val.trim();
116
117    // E.g., 10K, 10M, 10G, 10
118    if let Some(sval) = val.strip_suffix("K") {
119        let byte = sval.parse::<usize>()?;
120        return Ok(byte * 1024);
121    }
122    if let Some(sval) = val.strip_suffix("M") {
123        let byte = sval.parse::<usize>()?;
124        return Ok(byte * 1024 * 1024);
125    }
126    if let Some(sval) = val.strip_suffix("G") {
127        let byte = sval.parse::<usize>()?;
128        return Ok(byte * 1024 * 1024 * 1024);
129    }
130
131    let byte = val.parse::<usize>()?;
132    Ok(byte)
133}
134
135/* Load is reported as weight * duty cycle
136 *
137 * In the Linux kernel, EEDVF uses default weight = 1 s.t.
138 * load for a nice-0 thread runnable for time slice = 1
139 *
140 * To conform with cgroup weights convention, sched-ext uses
141 * the convention of default weight = 100 with the formula
142 * 100 * nice ^ 1.5. This means load for a nice-0 thread
143 * runnable for time slice = 100.
144 *
145 * To ensure we report load metrics consistently with the Linux
146 * kernel, we divide load by 100.0 prior to reporting metrics.
147 * This is also more intuitive for users since 1 CPU roughly
148 * means 1 unit of load.
149 *
150 * We only do this prior to reporting as its easier to work with
151 * weight as integers in BPF / userspace than floating point.
152 */
153pub fn normalize_load_metric(metric: f64) -> f64 {
154    metric / 100.0
155}
156
157/// Find the best split size for dividing a total number of items
158/// given a range of sizes.
159///
160/// Searches from starting with min to max to ideally find an even
161/// split with no remainders. Otherwise, choose a size that minimizes
162/// the remainder favoring smaller sizes.
163///
164/// # Arguments
165/// * `total_items` - Total number of items to split
166/// * `min_size` - Minimum size per split
167/// * `max_size` - Maximum size per split
168///
169/// # Returns
170/// The optimal partition size
171pub fn find_best_split_size(total_items: usize, min_size: usize, max_size: usize) -> usize {
172    if total_items <= min_size {
173        return total_items;
174    }
175
176    let mut optimal_size = min_size;
177
178    for size in min_size..=max_size {
179        if total_items % size == 0 {
180            optimal_size = size;
181            break;
182        }
183        // If no perfect division, use the one that minimizes remainder
184        if total_items % size < total_items % optimal_size {
185            optimal_size = size;
186        }
187    }
188
189    optimal_size
190}