Skip to main content

tenferro_cpu/
affinity.rs

1use std::num::NonZeroUsize;
2
3/// Return a best-effort CPU count available to the current process.
4///
5/// This first tries an OS-standard process-affinity query when supported, then
6/// falls back to `std::thread::available_parallelism()`, and finally to `1`.
7///
8/// # Examples
9///
10/// ```
11/// let available = tenferro_cpu::available_parallelism();
12/// assert!(available >= 1);
13/// ```
14pub fn available_parallelism() -> usize {
15    process_cpu_affinity_count()
16        .or_else(standard_available_parallelism)
17        .unwrap_or(1)
18}
19
20/// Return the current process affinity mask size when the platform exposes a
21/// standard affinity API.
22///
23/// Platforms without an affinity query return `None`.
24///
25/// # Examples
26///
27/// ```
28/// let count = tenferro_cpu::process_cpu_affinity_count();
29/// if let Some(count) = count {
30///     assert!(count >= 1);
31/// }
32/// ```
33pub fn process_cpu_affinity_count() -> Option<usize> {
34    platform_process_cpu_affinity_count()
35}
36
37pub(crate) fn standard_available_parallelism() -> Option<usize> {
38    std::thread::available_parallelism()
39        .ok()
40        .map(NonZeroUsize::get)
41}
42
43#[cfg(any(target_os = "linux", target_os = "android", test))]
44fn count_affinity_mask_bits(mask: &[u8]) -> Option<usize> {
45    let count = mask.iter().map(|byte| byte.count_ones() as usize).sum();
46    (count > 0).then_some(count)
47}
48
49#[cfg(any(target_os = "linux", target_os = "android"))]
50const LINUX_EINVAL: i32 = 22;
51
52#[cfg(any(target_os = "linux", target_os = "android"))]
53fn linux_next_affinity_mask_bytes(mask_bytes: usize, errno: Option<i32>) -> Option<usize> {
54    (errno == Some(LINUX_EINVAL))
55        .then(|| mask_bytes.checked_mul(2))
56        .flatten()
57}
58
59#[cfg(any(target_os = "linux", target_os = "android"))]
60fn platform_process_cpu_affinity_count() -> Option<usize> {
61    unsafe extern "C" {
62        fn sched_getaffinity(pid: i32, cpusetsize: usize, mask: *mut core::ffi::c_void) -> i32;
63    }
64
65    const INITIAL_MASK_BYTES: usize = 128;
66
67    let mut mask_bytes = INITIAL_MASK_BYTES;
68    loop {
69        let mut mask = vec![0u8; mask_bytes];
70        // SAFETY: `mask` is a live allocation of `mask_bytes` bytes, and pid 0
71        // asks the OS to query the current process affinity.
72        let rc = unsafe {
73            sched_getaffinity(0, mask_bytes, mask.as_mut_ptr().cast::<core::ffi::c_void>())
74        };
75        if rc == 0 {
76            return count_affinity_mask_bits(&mask);
77        }
78
79        mask_bytes = linux_next_affinity_mask_bytes(
80            mask_bytes,
81            std::io::Error::last_os_error().raw_os_error(),
82        )?;
83    }
84}
85
86#[cfg(target_os = "windows")]
87fn platform_process_cpu_affinity_count() -> Option<usize> {
88    type Handle = *mut core::ffi::c_void;
89    type DwordPtr = usize;
90    type Word = u16;
91
92    unsafe extern "system" {
93        fn GetCurrentProcess() -> Handle;
94        fn GetProcessAffinityMask(
95            process: Handle,
96            process_affinity_mask: *mut DwordPtr,
97            system_affinity_mask: *mut DwordPtr,
98        ) -> i32;
99        fn GetActiveProcessorGroupCount() -> Word;
100        fn GetActiveProcessorCount(group_number: Word) -> u32;
101        fn GetProcessGroupAffinity(
102            process: Handle,
103            group_count: *mut Word,
104            group_array: *mut Word,
105        ) -> i32;
106    }
107
108    // SAFETY: `GetCurrentProcess` takes no arguments and returns a pseudo-handle
109    // owned by the process; it must not be closed by the caller.
110    let process = unsafe { GetCurrentProcess() };
111    // SAFETY: This Windows query takes no pointers and has no preconditions.
112    let system_group_count = unsafe { GetActiveProcessorGroupCount() };
113
114    if system_group_count <= 1 {
115        let mut process_mask = 0usize;
116        let mut system_mask = 0usize;
117        // SAFETY: `process` is the current-process pseudo-handle and both
118        // output pointers refer to live local variables for the duration of the call.
119        let ok = unsafe {
120            GetProcessAffinityMask(
121                process,
122                std::ptr::addr_of_mut!(process_mask),
123                std::ptr::addr_of_mut!(system_mask),
124            )
125        };
126        if ok != 0 {
127            let count = process_mask.count_ones() as usize;
128            return (count > 0).then_some(count);
129        }
130        // SAFETY: Group 0 exists when Windows reports at most one active group.
131        let count = unsafe { GetActiveProcessorCount(0) } as usize;
132        return (count > 0).then_some(count);
133    }
134
135    let mut group_count: Word = 0;
136    // SAFETY: Windows accepts a null group array to query the required processor-group
137    // count. That probe is the expected failure path: `group_count` is a live output
138    // variable and a nonzero count after a failed call is the value needed for the
139    // second call below.
140    let ok = unsafe {
141        GetProcessGroupAffinity(
142            process,
143            std::ptr::addr_of_mut!(group_count),
144            std::ptr::null_mut(),
145        )
146    };
147    if ok != 0 || group_count == 0 {
148        // SAFETY: `u16::MAX` requests the total count across all processor groups.
149        let count = unsafe { GetActiveProcessorCount(u16::MAX) } as usize;
150        return (count > 0).then_some(count);
151    }
152
153    let mut groups = vec![0u16; group_count as usize];
154    // SAFETY: `groups` has `group_count` entries and both output pointers stay
155    // valid for the duration of the call.
156    let ok = unsafe {
157        GetProcessGroupAffinity(
158            process,
159            std::ptr::addr_of_mut!(group_count),
160            groups.as_mut_ptr(),
161        )
162    };
163    if ok == 0 || group_count == 0 {
164        // SAFETY: `u16::MAX` requests the total count across all processor groups.
165        let count = unsafe { GetActiveProcessorCount(u16::MAX) } as usize;
166        return (count > 0).then_some(count);
167    }
168
169    if group_count == 1 {
170        let mut process_mask = 0usize;
171        let mut system_mask = 0usize;
172        // SAFETY: `process` is the current-process pseudo-handle and both
173        // output pointers refer to live local variables for the duration of the call.
174        let ok = unsafe {
175            GetProcessAffinityMask(
176                process,
177                std::ptr::addr_of_mut!(process_mask),
178                std::ptr::addr_of_mut!(system_mask),
179            )
180        };
181        if ok != 0 {
182            let count = process_mask.count_ones() as usize;
183            return (count > 0).then_some(count);
184        }
185    }
186
187    let count = groups
188        .into_iter()
189        .map(|group| {
190            // SAFETY: Group identifiers are returned by `GetProcessGroupAffinity`.
191            (unsafe { GetActiveProcessorCount(group) }) as usize
192        })
193        .sum();
194    (count > 0).then_some(count)
195}
196
197#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "windows")))]
198fn platform_process_cpu_affinity_count() -> Option<usize> {
199    None
200}
201
202#[cfg(test)]
203mod tests;