1use std::io;
4
5pub fn allowed_cpus() -> Vec<usize> {
8 #[cfg(target_os = "linux")]
9 {
10 let mut mask = [0u8; 128]; let r = unsafe { crate::os::sys::sched_getaffinity(0, mask.len(), mask.as_mut_ptr()) };
13 if r == 0 {
14 let v: Vec<usize> = (0..mask.len() * 8).filter(|&i| mask[i / 8] & (1 << (i % 8)) != 0).collect();
15 if !v.is_empty() {
16 return v;
17 }
18 }
19 }
20 #[cfg(windows)]
21 {
22 use crate::os::sys;
23 let (mut p, mut s) = (0usize, 0usize);
24 if unsafe { sys::GetProcessAffinityMask(sys::GetCurrentProcess(), &mut p, &mut s) } != 0 && p != 0 {
26 return (0..usize::BITS as usize).filter(|&i| p & (1 << i) != 0).collect();
27 }
28 }
29 #[cfg(any(target_os = "linux", windows))]
30 {
31 vec![0]
32 }
33 #[cfg(not(any(target_os = "linux", windows)))]
34 {
35 (0..cpu_count()).collect()
36 }
37}
38
39pub fn cpu_count() -> usize {
43 static N: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
45 *N.get_or_init(cpu_count_now)
46}
47
48fn cpu_count_now() -> usize {
49 #[cfg(any(target_os = "linux", windows))]
50 {
51 let n = allowed_cpus().len().max(1);
52 #[cfg(target_os = "linux")]
53 if let Some(q) = cgroup_quota() {
54 return n.min(q).max(1);
55 }
56 n
57 }
58 #[cfg(not(any(target_os = "linux", windows)))]
59 {
60 std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
61 }
62}
63
64#[cfg(target_os = "linux")]
66#[inline(never)]
67fn cgroup_quota() -> Option<usize> {
68 const ROOT: &str = "/sys/fs/cgroup";
70 let mut buf = [0u8; 4096];
71 let groups = read_small_bytes("/proc/self/cgroup", &mut buf);
72 let path = groups.split(|&b| b == b'\n').find_map(|l| l.strip_prefix(b"0::"))?;
73 let path = std::str::from_utf8(path).ok()?.trim_end_matches('/');
74 let mut dir = String::with_capacity(ROOT.len() + path.len() + 8);
75 dir.push_str(ROOT);
76 dir.push_str(path);
77 let mut best: Option<usize> = None;
78 loop {
79 let len = dir.len();
80 dir.push_str("/cpu.max");
81 let mut b = [0u8; 64];
82 if let Some(q) = parse_cpu_max(read_small_bytes(&dir, &mut b)) {
83 best = Some(best.map_or(q, |b| b.min(q)));
84 }
85 dir.truncate(len);
86 if len <= ROOT.len() {
87 return best;
88 }
89 match dir.rfind('/') {
90 Some(i) => dir.truncate(i),
91 None => return best,
92 }
93 }
94}
95
96#[inline(never)]
109pub fn net_rx_concentrated() -> bool {
110 #[cfg(target_os = "linux")]
111 {
112 let mut buf = [0u8; 4096];
114 match softirq_concentrated(read_small_bytes("/proc/stat", &mut buf)) {
115 Some(cpu) => {
116 RX_CPU.store(cpu, std::sync::atomic::Ordering::Relaxed);
117 true
118 }
119 None => false,
120 }
121 }
122 #[cfg(not(target_os = "linux"))]
123 {
124 false
125 }
126}
127
128#[cfg(target_os = "linux")]
130static RX_CPU: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(usize::MAX);
131
132#[cfg(target_os = "linux")]
136fn softirq_concentrated(stat: &[u8]) -> Option<usize> {
137 let (mut best, mut most, mut total) = (0, 0u64, 0u64);
138 let (mut n, mut cur, mut cpu, mut digits, mut on, mut prev) = (0, 0u64, 0u64, false, false, 0u8);
141 for &b in stat {
142 if b.is_ascii_digit() {
143 if !digits && n == 0 {
144 on = prev == b'u';
145 }
146 cur = cur.wrapping_mul(10).wrapping_add(u64::from(b - b'0'));
147 digits = true;
148 } else {
149 if digits {
150 if on && n == 0 {
151 cpu = cur;
152 } else if on && n == 7 {
153 total = total.wrapping_add(cur);
154 if cur > most {
155 (best, most) = (cpu as usize, cur);
156 }
157 }
158 (n, cur, digits) = (n + 1, 0, false);
159 }
160 if b == b'\n' {
161 n = 0;
162 }
163 }
164 prev = b;
165 }
166 (total >= 200 && most * 2 >= total).then_some(best)
167}
168
169pub fn keep_current_thread_off_net_rx() {
173 #[cfg(target_os = "linux")]
174 keep_current_thread_off(RX_CPU.load(std::sync::atomic::Ordering::Relaxed));
175}
176
177#[cfg(target_os = "linux")]
178fn keep_current_thread_off(cpu: usize) {
179 let mut mask = [0u8; 128];
180 let (byte, bit) = (cpu / 8, 1u8 << (cpu % 8));
181 if byte >= mask.len() || unsafe { crate::os::sys::sched_getaffinity(0, mask.len(), mask.as_mut_ptr()) } != 0 || mask[byte] & bit == 0 {
183 return;
184 }
185 mask[byte] &= !bit;
186 if mask.iter().any(|&b| b != 0) {
187 unsafe { crate::os::sys::sched_setaffinity(0, mask.len(), mask.as_ptr()) };
189 }
190}
191
192#[cfg(target_os = "linux")]
195#[inline(never)]
196pub(crate) fn read_small_bytes<'b>(path: &str, buf: &'b mut [u8]) -> &'b [u8] {
197 use std::io::Read;
198 let n = std::fs::File::open(path).and_then(|mut f| f.read(buf)).unwrap_or(0);
199 &buf[..n]
200}
201
202#[cfg(target_os = "linux")]
204fn parse_cpu_max(s: &[u8]) -> Option<usize> {
205 let mut it = s.split(u8::is_ascii_whitespace).filter(|w| !w.is_empty());
206 let quota = parse_u64(it.next()?)?;
207 let period = it.next().map_or(Some(100_000), parse_u64)?;
208 if quota == 0 || period == 0 {
209 return None;
210 }
211 Some(quota.div_ceil(period) as usize)
212}
213
214#[cfg(target_os = "linux")]
216fn parse_u64(d: &[u8]) -> Option<u64> {
217 if d.is_empty() || d.len() > 19 || !d.iter().all(u8::is_ascii_digit) {
218 return None;
219 }
220 Some(d.iter().fold(0, |n, &c| n * 10 + u64::from(c - b'0')))
221}
222
223#[cfg(target_arch = "x86_64")]
225pub(crate) mod x86 {
226 pub const SSSE3: u32 = 1 << 0;
227 pub const SSE41: u32 = 1 << 1;
228 pub const SSE42: u32 = 1 << 2;
229 pub const AES: u32 = 1 << 3;
230 pub const PCLMULQDQ: u32 = 1 << 4;
231 pub const SHA: u32 = 1 << 5;
232 pub const AVX2: u32 = 1 << 6;
233 pub const BMI2: u32 = 1 << 7;
234 pub const ADX: u32 = 1 << 8;
235 pub(super) const DONE: u32 = 1 << 31;
236}
237
238#[cfg(target_arch = "x86_64")]
242#[inline]
243pub(crate) fn x86_features() -> u32 {
244 use std::sync::atomic::{AtomicU32, Ordering};
245 static F: AtomicU32 = AtomicU32::new(0);
246 let f = F.load(Ordering::Relaxed);
247 if f != 0 {
248 return f;
249 }
250 let f = detect_x86() | x86::DONE;
251 F.store(f, Ordering::Relaxed);
252 f
253}
254
255#[cfg(target_arch = "x86_64")]
256#[cold]
257#[inline(never)]
258#[allow(unused_unsafe)]
260fn detect_x86() -> u32 {
261 use core::arch::x86_64::{__cpuid, __cpuid_count};
262 let bit = |r: u32, b: u32, v: u32| if r & (1 << b) != 0 { v } else { 0 };
263 let (max, l1) = unsafe { (__cpuid(0).eax, __cpuid(1)) };
265 let mut f = bit(l1.ecx, 9, x86::SSSE3) | bit(l1.ecx, 19, x86::SSE41) | bit(l1.ecx, 20, x86::SSE42) | bit(l1.ecx, 25, x86::AES) | bit(l1.ecx, 1, x86::PCLMULQDQ);
266 if max >= 7 {
267 let l7 = unsafe { __cpuid_count(7, 0) };
269 f |= bit(l7.ebx, 29, x86::SHA) | bit(l7.ebx, 8, x86::BMI2) | bit(l7.ebx, 19, x86::ADX);
270 if l7.ebx & (1 << 5) != 0 && l1.ecx & (1 << 27) != 0 && l1.ecx & (1 << 28) != 0 && xcr0() & 6 == 6 {
272 f |= x86::AVX2;
273 }
274 }
275 f
276}
277
278#[cfg(target_arch = "x86_64")]
279fn xcr0() -> u64 {
280 #[target_feature(enable = "xsave")]
281 unsafe fn read() -> u64 {
282 unsafe { core::arch::x86_64::_xgetbv(0) }
284 }
285 unsafe { read() }
287}
288
289pub fn pin_current_thread(cpu: usize) -> io::Result<()> {
292 #[cfg(target_os = "linux")]
293 {
294 if cpu >= 1024 {
295 return Err(io::Error::new(io::ErrorKind::InvalidInput, "CPU index too large"));
296 }
297 let mut mask = [0u8; 128];
298 mask[cpu / 8] |= 1 << (cpu % 8);
299 if unsafe { crate::os::sys::sched_setaffinity(0, mask.len(), mask.as_ptr()) } != 0 {
301 return Err(io::Error::last_os_error());
302 }
303 Ok(())
304 }
305 #[cfg(windows)]
306 {
307 use crate::os::sys;
308 if cpu >= usize::BITS as usize {
309 return Err(io::Error::new(io::ErrorKind::InvalidInput, "CPU index beyond the first processor group"));
310 }
311 if unsafe { sys::SetThreadAffinityMask(sys::GetCurrentThread(), 1usize << cpu) } == 0 {
313 return Err(io::Error::last_os_error());
314 }
315 Ok(())
316 }
317 #[cfg(not(any(target_os = "linux", windows)))]
318 {
319 let _ = cpu;
320 Err(io::Error::new(io::ErrorKind::Unsupported, "thread affinity is not supported on this OS"))
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[cfg(target_os = "linux")]
329 #[test]
330 fn cpu_max_parsing() {
331 assert_eq!(parse_cpu_max(b"max 100000
332"), None);
333 assert_eq!(parse_cpu_max(b"200000 100000
334"), Some(2));
335 assert_eq!(parse_cpu_max(b"150000 100000"), Some(2));
336 assert_eq!(parse_cpu_max(b"50000 100000"), Some(1));
337 assert_eq!(parse_cpu_max(b"300000"), Some(3));
338 assert_eq!(parse_cpu_max(b"0 100000"), None);
339 assert_eq!(parse_cpu_max(b""), None);
340 assert!(cpu_count() <= allowed_cpus().len());
341 }
342
343 #[cfg(target_os = "linux")]
344 #[test]
345 fn net_rx_concentration() {
346 let k12 = b"cpu 53911 675 72359 206845 12924 0 29173 141 0 0\ncpu0 15347 395 22098 54484 3504 0 159 36 0 0\ncpu1 15620 280 23352 52153 4305 0 105 38 0 0\ncpu2 9220 0 8644 44155 1482 0 24987 36 0 0\ncpu3 13724 0 18265 56053 3633 0 3922 31 0 0\nintr 123 0 0\n";
349 assert_eq!(softirq_concentrated(k12), Some(2));
350 let spread = b"cpu 1 2 3 4 5 6 4000\ncpu0 1 2 3 4 5 6 1000\ncpu1 1 2 3 4 5 6 900\ncpu2 1 2 3 4 5 6 1100\ncpu3 1 2 3 4 5 6 1000\n";
351 assert_eq!(softirq_concentrated(spread), None);
352 assert_eq!(softirq_concentrated(b"cpu 0 0 0 0 0 0 0\ncpu0 1 2 3 4 5 6 5\ncpu1 1 2 3 4 5 6 150\n"), None, "too little to tell");
353 assert_eq!(softirq_concentrated(b"cpu 9\ncpu0 1 2 3 4 5 6 10\ncpu1 1 2 3 4 5 6 900\ncpu2 1 2 3\n"), Some(1), "a cut line is not counted");
354 assert_eq!(softirq_concentrated(b"cpu 1 2 3 4 5 6 9000\ncpu0 1 2 3 4 5 6 1000\ncpu1 1 2 3 4 5 6 1000\n"), Some(0), "not the total line");
355 assert_eq!(softirq_concentrated(b""), None);
356 let _ = net_rx_concentrated(); }
358
359 #[cfg(target_os = "linux")]
360 #[test]
361 fn a_thread_can_be_kept_off_one_cpu() {
362 let cpus = allowed_cpus();
363 std::thread::spawn(move || {
364 keep_current_thread_off(100_000); assert_eq!(allowed_cpus(), cpus);
366 keep_current_thread_off(cpus[0]);
367 if cpus.len() > 1 {
368 assert_eq!(allowed_cpus(), cpus[1..], "the first CPU left");
369 for &c in &cpus[2..] {
370 keep_current_thread_off(c);
371 }
372 assert_eq!(allowed_cpus(), [cpus[1]]);
373 keep_current_thread_off(cpus[1]);
374 assert_eq!(allowed_cpus(), [cpus[1]], "never the last CPU");
375 } else {
376 assert_eq!(allowed_cpus(), cpus, "never the last CPU");
377 }
378 })
379 .join()
380 .unwrap();
381 }
382
383 #[cfg(target_arch = "x86_64")]
384 #[test]
385 fn x86_features_match_std() {
386 let f = x86_features();
387 let checks = [
388 (x86::SSSE3, std::is_x86_feature_detected!("ssse3")),
389 (x86::SSE41, std::is_x86_feature_detected!("sse4.1")),
390 (x86::SSE42, std::is_x86_feature_detected!("sse4.2")),
391 (x86::AES, std::is_x86_feature_detected!("aes")),
392 (x86::PCLMULQDQ, std::is_x86_feature_detected!("pclmulqdq")),
393 (x86::SHA, std::is_x86_feature_detected!("sha")),
394 (x86::AVX2, std::is_x86_feature_detected!("avx2")),
395 (x86::BMI2, std::is_x86_feature_detected!("bmi2")),
396 (x86::ADX, std::is_x86_feature_detected!("adx")),
397 ];
398 for (i, (bit, std)) in checks.into_iter().enumerate() {
399 assert_eq!(f & bit != 0, std, "feature {i}");
400 }
401 }
402
403 #[test]
404 fn count_and_pin() {
405 let cpus = allowed_cpus();
406 assert!(!cpus.is_empty());
407 assert!(cpu_count() >= 1);
408 let last = *cpus.last().unwrap();
409 std::thread::spawn(move || {
410 let _ = last;
411 #[cfg(any(target_os = "linux", windows))]
412 pin_current_thread(last).unwrap();
413 #[cfg(target_os = "linux")]
414 assert_eq!(allowed_cpus(), vec![last]);
415 })
416 .join()
417 .unwrap();
418 }
419}