Skip to content

Commit 03054f5

Browse files
committed
feat: add cpu isolation for system, perf and benchmark
1 parent 9eb9fc8 commit 03054f5

3 files changed

Lines changed: 177 additions & 1 deletion

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
use super::setup::run_with_sudo;
2+
use crate::prelude::*;
3+
4+
pub struct CpuIsolation;
5+
6+
impl CpuIsolation {
7+
pub fn new() -> Self {
8+
Self {}
9+
}
10+
11+
fn set_allowed_cpus(scope: &str, allowed_cpus: &[u32]) -> anyhow::Result<()> {
12+
let cpus = allowed_cpus
13+
.iter()
14+
.map(|cpu| cpu.to_string())
15+
.collect::<Vec<_>>()
16+
.join(",");
17+
18+
run_with_sudo(&[
19+
"systemctl",
20+
"set-property",
21+
scope,
22+
&format!("AllowedCPUs={}", cpus),
23+
])?;
24+
25+
Ok(())
26+
}
27+
28+
pub fn isolate(&self) -> anyhow::Result<()> {
29+
let (instrument_cpu, bench_cpu, system_cpus) = Self::cpu_bindings();
30+
Self::set_allowed_cpus("system.slice", &system_cpus)?;
31+
Self::set_allowed_cpus("init.scope", &system_cpus)?;
32+
33+
let user_cpus = vec![instrument_cpu, bench_cpu];
34+
Self::set_allowed_cpus("user.slice", &user_cpus)?;
35+
36+
Ok(())
37+
}
38+
39+
pub fn reset_isolation(&self) -> anyhow::Result<()> {
40+
let root_cpus = Self::root_cpus().unwrap_or_default();
41+
Self::set_allowed_cpus("system.slice", &root_cpus)?;
42+
Self::set_allowed_cpus("user.slice", &root_cpus)?;
43+
Self::set_allowed_cpus("init.scope", &root_cpus)?;
44+
45+
Ok(())
46+
}
47+
48+
/// Find the CPU cores to use for the isolated tasks:
49+
/// 1. System processes
50+
/// 2. Perf process / other instruments
51+
/// 3. Benchmarks
52+
///
53+
/// If isolated CPUs are available, they will be used for the perf and benchmark processes. The
54+
/// remaining online CPUs will be used for system processes.
55+
pub fn cpu_bindings() -> (u32, u32, Vec<u32>) {
56+
// Use system isolated cpus (done via boot parameter) which we can use for the benchmark
57+
// and perf process. If not isolated CPUs exist, we will use the default ones.
58+
let isolated = Self::isolated_cpus().unwrap_or_default();
59+
debug!("Isolated CPUs: {isolated:?}");
60+
61+
let root = Self::root_cpus().unwrap_or_default();
62+
debug!("Root CPUs: {root:?}");
63+
64+
assert!(
65+
root.len() + isolated.len() >= 3,
66+
"At least 3 CPUs are required"
67+
);
68+
69+
let (bench_cpu, system_cpus) = if isolated.is_empty() {
70+
(root[0], root[1..].to_vec())
71+
} else {
72+
(isolated[0], root)
73+
};
74+
75+
// WARN: The instrument cpu must also be a system CPU, otherwise perf will not work.
76+
let instrument_cpu = system_cpus[0];
77+
78+
(instrument_cpu, bench_cpu, system_cpus)
79+
}
80+
81+
fn parse_cpu_str(content: &str) -> Vec<u32> {
82+
content
83+
.trim()
84+
.split(',')
85+
.filter_map(|cpu| {
86+
if cpu.contains('-') {
87+
let (left, right) = cpu.split_once('-')?;
88+
let left = left.parse::<u32>().ok()?;
89+
let right = right.parse::<u32>().ok()?;
90+
91+
if left > right {
92+
return None;
93+
}
94+
95+
Some((left..=right).collect::<Vec<_>>())
96+
} else {
97+
Some(vec![cpu.parse::<u32>().ok()?])
98+
}
99+
})
100+
.flatten()
101+
.collect::<Vec<_>>()
102+
}
103+
104+
/// All CPUs that are not isolated.
105+
fn root_cpus() -> anyhow::Result<Vec<u32>> {
106+
let isolated = Self::isolated_cpus().unwrap_or_default();
107+
let cpus = Self::online_cpus()
108+
.unwrap_or_default()
109+
.into_iter()
110+
.filter(|cpu| !isolated.contains(cpu))
111+
.collect::<Vec<_>>();
112+
debug!("Root CPUs: {cpus:?}");
113+
114+
Ok(cpus)
115+
}
116+
117+
fn online_cpus() -> anyhow::Result<Vec<u32>> {
118+
let content = std::fs::read_to_string("/sys/devices/system/cpu/online")?;
119+
let cpus = Self::parse_cpu_str(&content);
120+
if cpus.is_empty() {
121+
return Err(anyhow::anyhow!("No online CPUs found"));
122+
}
123+
Ok(cpus)
124+
}
125+
126+
fn isolated_cpus() -> anyhow::Result<Vec<u32>> {
127+
let content = std::fs::read_to_string("/sys/devices/system/cpu/isolated")?;
128+
let cpus = Self::parse_cpu_str(&content);
129+
if cpus.is_empty() {
130+
return Err(anyhow::anyhow!("No isolated CPUs found"));
131+
}
132+
Ok(cpus)
133+
}
134+
}
135+
136+
impl Drop for CpuIsolation {
137+
fn drop(&mut self) {
138+
self.reset_isolation()
139+
.unwrap_or_else(|e| eprintln!("Failed to reset CPU isolation: {}", e));
140+
}
141+
}
142+
143+
#[cfg(test)]
144+
mod tests {
145+
use super::*;
146+
147+
#[test]
148+
fn test_parse_cpu_str() {
149+
assert_eq!(CpuIsolation::parse_cpu_str("").len(), 0);
150+
assert_eq!(CpuIsolation::parse_cpu_str("0,1,2,3"), vec![0, 1, 2, 3]);
151+
assert_eq!(CpuIsolation::parse_cpu_str("0-1,2-3"), vec![0, 1, 2, 3]);
152+
assert_eq!(
153+
CpuIsolation::parse_cpu_str("0,2,4-7"),
154+
vec![0, 2, 4, 5, 6, 7]
155+
);
156+
}
157+
158+
#[test]
159+
fn test_cpu_bindings() {
160+
let online_cpus = CpuIsolation::online_cpus().unwrap_or_default();
161+
assert!(!online_cpus.is_empty());
162+
163+
let (perf_cpu, bench_cpu, system_cpus) = CpuIsolation::cpu_bindings();
164+
assert!(perf_cpu != bench_cpu);
165+
assert!(!system_cpus.is_empty());
166+
assert!(!system_cpus.contains(&perf_cpu));
167+
assert!(!system_cpus.contains(&bench_cpu));
168+
}
169+
}

src/run/runner/helpers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod env;
22
pub mod get_bench_command;
3+
pub mod isolation;
34
pub mod profile_folder;
45
pub mod run_command_with_log_pipe;
56
pub mod setup;

src/run/runner/wall_time/perf/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![cfg_attr(not(unix), allow(dead_code, unused_mut))]
22

33
use crate::prelude::*;
4+
use crate::run::runner::helpers::isolation::CpuIsolation;
45
use crate::run::runner::helpers::run_command_with_log_pipe::run_command_with_log_pipe_and_callback;
56
use crate::run::runner::helpers::setup::run_with_sudo;
67
use crate::run::runner::valgrind::helpers::ignored_objects_path::get_objects_path_to_ignore;
@@ -96,10 +97,15 @@ impl PerfRunner {
9697
};
9798
debug!("Using call graph mode: {}", cg_mode);
9899

100+
let (perf_cpu, bench_cpu, _) = CpuIsolation::cpu_bindings();
101+
let cpu_isolation = CpuIsolation::new();
102+
cpu_isolation.isolate()?;
103+
99104
cmd.args([
100105
"-c",
101106
&format!(
102-
"perf record --user-callchains --freq=999 --switch-output --control=fifo:{},{} --delay=-1 -g --call-graph={cg_mode} --output={} -- {bench_cmd}",
107+
"taskset -c {perf_cpu} perf record --user-callchains --freq=999 --switch-output --control=fifo:{},{} --delay=-1 -g --call-graph={cg_mode} --output={} -- \
108+
taskset -c {bench_cpu} {bench_cmd}",
103109
perf_fifo.ctl_fifo_path.to_string_lossy(),
104110
perf_fifo.ack_fifo_path.to_string_lossy(),
105111
perf_file.path().to_string_lossy()

0 commit comments

Comments
 (0)