1use std::num::NonZeroUsize;
2
3use thiserror::Error;
4
5use crate::{CpuId, CpuSet};
6
7#[derive(Debug, Error)]
22pub enum CpuAffinityError {
23 #[error("CPU affinity set is invalid: {0}")]
25 CpuSet(#[from] crate::CpuSetError),
26 #[error("setting thread affinity failed: {source}")]
28 Set {
29 #[source]
30 source: std::io::Error,
31 },
32 #[error("querying worker affinity failed: {source}")]
34 Query {
35 #[source]
36 source: std::io::Error,
37 },
38 #[error("setting thread affinity is unsupported on this platform")]
40 UnsupportedPlatform,
41 #[error("failed to verify worker affinity")]
43 VerificationUnavailable,
44 #[error("verification returned affinity {observed:?}")]
46 Verification { observed: Vec<CpuId> },
47 #[error("affinity mask size overflow")]
49 MaskSizeOverflow,
50 #[error("CPU {cpu} exceeds supported affinity mask size of {max_bytes} bytes")]
52 MaskTooLarge { cpu: CpuId, max_bytes: usize },
53 #[error("failed to allocate affinity mask: {source}")]
55 MaskAllocation {
56 #[source]
57 source: std::collections::TryReserveError,
58 },
59 #[error("cannot set an empty affinity mask")]
61 EmptyMask,
62}
63
64pub(crate) trait ThreadAffinity: Clone + Send + Sync + 'static {
65 fn pin_current(&self, cpu: CpuId) -> Result<CpuSet, CpuAffinityError>;
66}
67
68#[derive(Clone, Copy, Debug)]
69pub(crate) struct SystemThreadAffinity;
70
71impl ThreadAffinity for SystemThreadAffinity {
72 fn pin_current(&self, cpu: CpuId) -> Result<CpuSet, CpuAffinityError> {
73 set_current_thread_affinity(&CpuSet::new([cpu])?)?;
74 process_cpu_affinity().ok_or(CpuAffinityError::VerificationUnavailable)
75 }
76}
77
78#[cfg(all(test, target_os = "linux"))]
79pub(crate) fn current_cpu() -> Result<CpuId, CpuAffinityError> {
80 unsafe extern "C" {
81 fn sched_getcpu() -> i32;
82 }
83 let cpu = unsafe { sched_getcpu() };
86 usize::try_from(cpu)
87 .map(CpuId::new)
88 .map_err(|_| CpuAffinityError::Query {
89 source: std::io::Error::last_os_error(),
90 })
91}
92
93fn set_current_thread_affinity(cpus: &CpuSet) -> Result<(), CpuAffinityError> {
94 #[cfg(any(target_os = "linux", target_os = "android"))]
95 {
96 unsafe extern "C" {
97 fn sched_setaffinity(
98 pid: i32,
99 cpusetsize: usize,
100 mask: *const core::ffi::c_void,
101 ) -> i32;
102 }
103
104 let mask = build_affinity_mask(cpus)?;
105 let rc =
108 unsafe { sched_setaffinity(0, mask.len(), mask.as_ptr().cast::<core::ffi::c_void>()) };
109 (rc == 0)
110 .then_some(())
111 .ok_or_else(|| CpuAffinityError::Set {
112 source: std::io::Error::last_os_error(),
113 })
114 }
115 #[cfg(not(any(target_os = "linux", target_os = "android")))]
116 {
117 let _ = cpus;
118 Err(CpuAffinityError::UnsupportedPlatform)
119 }
120}
121
122#[cfg(any(target_os = "linux", target_os = "android", test))]
123fn build_affinity_mask(cpus: &CpuSet) -> Result<Vec<u8>, CpuAffinityError> {
124 const MIN_MASK_BYTES: usize = 128;
125 const MAX_MASK_BYTES: usize = 1 << 20;
126
127 let highest_cpu = cpus
128 .as_slice()
129 .last()
130 .copied()
131 .ok_or(CpuAffinityError::EmptyMask)?;
132 let required_bytes = highest_cpu
133 .as_usize()
134 .checked_div(u8::BITS as usize)
135 .and_then(|index| index.checked_add(1))
136 .ok_or(CpuAffinityError::MaskSizeOverflow)?
137 .max(MIN_MASK_BYTES);
138 if required_bytes > MAX_MASK_BYTES {
139 return Err(CpuAffinityError::MaskTooLarge {
140 cpu: highest_cpu,
141 max_bytes: MAX_MASK_BYTES,
142 });
143 }
144
145 let mut mask = Vec::new();
146 mask.try_reserve_exact(required_bytes)
147 .map_err(|source| CpuAffinityError::MaskAllocation { source })?;
148 mask.resize(required_bytes, 0u8);
149 for cpu in cpus.as_slice() {
150 let byte = cpu.as_usize() / u8::BITS as usize;
151 let bit = cpu.as_usize() % u8::BITS as usize;
152 mask[byte] |= 1 << bit;
153 }
154 Ok(mask)
155}
156
157pub fn available_parallelism() -> usize {
169 process_cpu_affinity_count()
170 .or_else(standard_available_parallelism)
171 .unwrap_or(1)
172}
173
174pub fn process_cpu_affinity_count() -> Option<usize> {
188 platform_process_cpu_affinity_count()
189}
190
191pub fn process_cpu_affinity() -> Option<CpuSet> {
204 platform_process_cpu_affinity()
205}
206
207pub(crate) fn standard_available_parallelism() -> Option<usize> {
208 std::thread::available_parallelism()
209 .ok()
210 .map(NonZeroUsize::get)
211}
212
213#[cfg(test)]
214fn count_affinity_mask_bits(mask: &[u8]) -> Option<usize> {
215 cpu_set_from_affinity_mask(mask).map(|cpus| cpus.len())
216}
217
218#[cfg(any(target_os = "linux", target_os = "android", test))]
219fn cpu_set_from_affinity_mask(mask: &[u8]) -> Option<CpuSet> {
220 let cpus = mask.iter().enumerate().flat_map(|(byte_index, byte)| {
221 (0..u8::BITS as usize)
222 .filter(move |bit| byte & (1 << bit) != 0)
223 .map(move |bit| CpuId::new(byte_index * u8::BITS as usize + bit))
224 });
225 CpuSet::new(cpus).ok()
226}
227
228#[cfg(any(target_os = "linux", target_os = "android"))]
229const LINUX_EINVAL: i32 = 22;
230
231#[cfg(any(target_os = "linux", target_os = "android"))]
232fn linux_next_affinity_mask_bytes(mask_bytes: usize, errno: Option<i32>) -> Option<usize> {
233 (errno == Some(LINUX_EINVAL))
234 .then(|| mask_bytes.checked_mul(2))
235 .flatten()
236}
237
238#[cfg(any(target_os = "linux", target_os = "android"))]
239fn platform_process_cpu_affinity_count() -> Option<usize> {
240 platform_process_cpu_affinity().map(|cpus| cpus.len())
241}
242
243#[cfg(any(target_os = "linux", target_os = "android"))]
244fn platform_process_cpu_affinity() -> Option<CpuSet> {
245 unsafe extern "C" {
246 fn sched_getaffinity(pid: i32, cpusetsize: usize, mask: *mut core::ffi::c_void) -> i32;
247 }
248
249 const INITIAL_MASK_BYTES: usize = 128;
250
251 let mut mask_bytes = INITIAL_MASK_BYTES;
252 loop {
253 let mut mask = vec![0u8; mask_bytes];
254 let rc = unsafe {
257 sched_getaffinity(0, mask_bytes, mask.as_mut_ptr().cast::<core::ffi::c_void>())
258 };
259 if rc == 0 {
260 return cpu_set_from_affinity_mask(&mask);
261 }
262
263 mask_bytes = linux_next_affinity_mask_bytes(
264 mask_bytes,
265 std::io::Error::last_os_error().raw_os_error(),
266 )?;
267 }
268}
269
270#[cfg(not(any(target_os = "linux", target_os = "android")))]
271fn platform_process_cpu_affinity() -> Option<CpuSet> {
272 None
273}
274
275#[cfg(target_os = "windows")]
276fn platform_process_cpu_affinity_count() -> Option<usize> {
277 type Handle = *mut core::ffi::c_void;
278 type DwordPtr = usize;
279 type Word = u16;
280
281 unsafe extern "system" {
282 fn GetCurrentProcess() -> Handle;
283 fn GetProcessAffinityMask(
284 process: Handle,
285 process_affinity_mask: *mut DwordPtr,
286 system_affinity_mask: *mut DwordPtr,
287 ) -> i32;
288 fn GetActiveProcessorGroupCount() -> Word;
289 fn GetActiveProcessorCount(group_number: Word) -> u32;
290 fn GetProcessGroupAffinity(
291 process: Handle,
292 group_count: *mut Word,
293 group_array: *mut Word,
294 ) -> i32;
295 }
296
297 let process = unsafe { GetCurrentProcess() };
300 let system_group_count = unsafe { GetActiveProcessorGroupCount() };
302
303 if system_group_count <= 1 {
304 let mut process_mask = 0usize;
305 let mut system_mask = 0usize;
306 let ok = unsafe {
309 GetProcessAffinityMask(
310 process,
311 std::ptr::addr_of_mut!(process_mask),
312 std::ptr::addr_of_mut!(system_mask),
313 )
314 };
315 if ok != 0 {
316 let count = process_mask.count_ones() as usize;
317 return (count > 0).then_some(count);
318 }
319 let count = unsafe { GetActiveProcessorCount(0) } as usize;
321 return (count > 0).then_some(count);
322 }
323
324 let mut group_count: Word = 0;
325 let ok = unsafe {
330 GetProcessGroupAffinity(
331 process,
332 std::ptr::addr_of_mut!(group_count),
333 std::ptr::null_mut(),
334 )
335 };
336 if ok != 0 || group_count == 0 {
337 let count = unsafe { GetActiveProcessorCount(u16::MAX) } as usize;
339 return (count > 0).then_some(count);
340 }
341
342 let mut groups = vec![0u16; group_count as usize];
343 let ok = unsafe {
346 GetProcessGroupAffinity(
347 process,
348 std::ptr::addr_of_mut!(group_count),
349 groups.as_mut_ptr(),
350 )
351 };
352 if ok == 0 || group_count == 0 {
353 let count = unsafe { GetActiveProcessorCount(u16::MAX) } as usize;
355 return (count > 0).then_some(count);
356 }
357
358 if group_count == 1 {
359 let mut process_mask = 0usize;
360 let mut system_mask = 0usize;
361 let ok = unsafe {
364 GetProcessAffinityMask(
365 process,
366 std::ptr::addr_of_mut!(process_mask),
367 std::ptr::addr_of_mut!(system_mask),
368 )
369 };
370 if ok != 0 {
371 let count = process_mask.count_ones() as usize;
372 return (count > 0).then_some(count);
373 }
374 }
375
376 let count = groups
377 .into_iter()
378 .map(|group| {
379 (unsafe { GetActiveProcessorCount(group) }) as usize
381 })
382 .sum();
383 (count > 0).then_some(count)
384}
385
386#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "windows")))]
387fn platform_process_cpu_affinity_count() -> Option<usize> {
388 None
389}
390
391#[cfg(test)]
392mod tests;