1use std::{collections::BTreeMap, fmt};
2
3#[non_exhaustive]
4#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
5pub enum Affinity {
16 Disabled,
18 Cpu,
20 Core,
25 Soft,
33}
34
35impl Default for Affinity {
36 fn default() -> Self {
37 if cfg!(windows) {
38 Affinity::Soft
39 } else {
40 Affinity::Core
41 }
42 }
43}
44
45#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
46pub(crate) struct Cpu {
48 group: u16,
49 num: u32,
50}
51
52impl fmt::Display for Cpu {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 if self.group == 0 {
55 write!(f, "{}", self.num)
56 } else {
57 write!(f, "{}:{}", self.group, self.num)
58 }
59 }
60}
61
62#[derive(Copy, Clone, Debug)]
63struct CpuInfo {
69 cpu: Cpu,
70 core: usize,
71 l3: usize,
72 node: usize,
73 class: u32,
74 allowed: bool,
75}
76
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub(crate) struct Placement {
80 mode: Affinity,
81 cpu: Cpu,
82 core: Vec<Cpu>,
84 siblings: Vec<Cpu>,
86}
87
88impl Placement {
89 pub(crate) fn cpu(idx: usize) -> Self {
94 let cpu = sys::cpu(idx);
95 Placement {
96 mode: Affinity::Cpu,
97 cpu,
98 core: vec![cpu],
99 siblings: vec![cpu],
100 }
101 }
102
103 pub(crate) fn apply(&self) -> bool {
105 sys::apply(self.mode, self.cpu, &self.core)
106 }
107
108 pub(crate) fn cpus(&self) -> Vec<usize> {
112 let mut cpus = vec![sys::index(self.cpu)];
113 cpus.extend(
114 self.siblings
115 .iter()
116 .filter(|cpu| **cpu != self.cpu)
117 .map(|cpu| sys::index(*cpu)),
118 );
119 cpus
120 }
121}
122
123pub(crate) fn incoming_cpu(stream: &std::net::TcpStream) -> Option<usize> {
130 sys::incoming_cpu(stream).map(sys::index)
131}
132
133impl fmt::Display for Placement {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 write!(f, "{:?} cpu {} (core", self.mode, self.cpu)?;
136 for c in &self.core {
137 write!(f, " {c}")?;
138 }
139 write!(f, ")")
140 }
141}
142
143pub(crate) fn plan(mode: Affinity) -> Vec<Placement> {
148 if mode == Affinity::Disabled {
149 return Vec::new();
150 }
151 order(&sys::topology())
152 .into_iter()
153 .map(|(cpu, core, siblings)| Placement {
154 mode,
155 cpu,
156 core,
157 siblings,
158 })
159 .collect()
160}
161
162pub(crate) fn cpu_count() -> usize {
164 if cfg!(windows) {
166 let n = sys::topology().iter().filter(|info| info.allowed).count();
167 if n > 0 {
168 return n;
169 }
170 }
171 std::thread::available_parallelism().map_or(2, std::num::NonZeroUsize::get)
172}
173
174struct Core {
175 cpus: Vec<Cpu>,
176 all: Vec<Cpu>,
177 l3: usize,
178 node: usize,
179 class: u32,
180}
181
182fn order(cpus: &[CpuInfo]) -> Vec<(Cpu, Vec<Cpu>, Vec<Cpu>)> {
187 let mut cores = BTreeMap::<usize, Core>::new();
188 for info in cpus {
189 let core = cores.entry(info.core).or_insert_with(|| Core {
190 cpus: Vec::new(),
191 all: Vec::new(),
192 l3: info.l3,
193 node: info.node,
194 class: info.class,
195 });
196 core.all.push(info.cpu);
197 if info.allowed {
198 core.cpus.push(info.cpu);
199 }
200 }
201 let mut cores: Vec<Core> = cores
202 .into_values()
203 .filter(|core| !core.cpus.is_empty())
204 .collect();
205 for core in &mut cores {
206 core.cpus.sort_unstable();
207 core.cpus.dedup();
208 core.all.sort_unstable();
209 core.all.dedup();
210 }
211 cores.sort_by_key(|core| core.cpus[0]);
212
213 let mut classes: Vec<u32> = cores.iter().map(|core| core.class).collect();
214 classes.sort_unstable_by(|a, b| b.cmp(a));
215 classes.dedup();
216
217 let mut seq = Vec::with_capacity(cores.len());
220 for class in classes {
221 let mut nodes = BTreeMap::<usize, BTreeMap<usize, Vec<&Core>>>::new();
222 for core in cores.iter().filter(|core| core.class == class) {
223 nodes
224 .entry(core.node)
225 .or_default()
226 .entry(core.l3)
227 .or_default()
228 .push(core);
229 }
230 seq.extend(interleave(
231 nodes
232 .into_values()
233 .map(|l3| interleave(l3.into_values().collect()))
234 .collect(),
235 ));
236 }
237
238 let depth = cores.iter().map(|core| core.cpus.len()).max().unwrap_or(0);
240 let mut result = Vec::with_capacity(cpus.len());
241 for idx in 0..depth {
242 for core in &seq {
243 if let Some(cpu) = core.cpus.get(idx) {
244 result.push((*cpu, core.cpus.clone(), core.all.clone()));
245 }
246 }
247 }
248 result
249}
250
251fn interleave<T>(lists: Vec<Vec<T>>) -> Vec<T> {
253 let mut iters: Vec<_> = lists.into_iter().map(Vec::into_iter).collect();
254 let mut result = Vec::new();
255 loop {
256 let len = result.len();
257 result.extend(iters.iter_mut().filter_map(Iterator::next));
258 if result.len() == len {
259 return result;
260 }
261 }
262}
263
264#[cfg(any(target_os = "linux", target_os = "android"))]
265mod sys {
266 use std::{fs, mem, path::Path};
267
268 use super::{Affinity, Cpu, CpuInfo};
269
270 pub(super) fn topology() -> Vec<CpuInfo> {
271 let root = Path::new("/sys/devices/system/cpu");
272 let allowed = allowed();
273 let mut cpus = list(&root.join("online")).unwrap_or_default();
274 cpus.extend(&allowed);
275 cpus.sort_unstable();
276 cpus.dedup();
277 read(root, &cpus, &allowed)
278 }
279
280 pub(super) fn read(root: &Path, cpus: &[u32], allowed: &[u32]) -> Vec<CpuInfo> {
283 cpus.iter()
284 .map(|&num| {
285 let dir = root.join(format!("cpu{num}"));
286 let first = |files: &[&str]| {
287 files
288 .iter()
289 .find_map(|file| list(&dir.join(file))?.into_iter().min())
290 .unwrap_or(num) as usize
291 };
292 let node = fs::read_dir(&dir)
293 .ok()
294 .and_then(|entries| {
295 entries.flatten().find_map(|entry| {
296 entry
297 .file_name()
298 .to_str()?
299 .strip_prefix("node")?
300 .parse()
301 .ok()
302 })
303 })
304 .unwrap_or(0);
305 let class = fs::read_to_string(dir.join("cpu_capacity"))
306 .ok()
307 .and_then(|s| s.trim().parse().ok())
308 .unwrap_or(0);
309
310 CpuInfo {
311 cpu: Cpu { group: 0, num },
312 core: first(&["topology/core_cpus_list", "topology/thread_siblings_list"]),
313 l3: first(&[
314 "cache/index3/shared_cpu_list",
315 "topology/package_cpus_list",
316 "topology/core_siblings_list",
317 ]),
318 node,
319 class,
320 allowed: allowed.contains(&num),
321 }
322 })
323 .collect()
324 }
325
326 fn list(path: &Path) -> Option<Vec<u32>> {
327 parse_list(&fs::read_to_string(path).ok()?)
328 }
329
330 pub(super) fn parse_list(s: &str) -> Option<Vec<u32>> {
332 let mut cpus = Vec::new();
333 for item in s.trim().split(',').filter(|item| !item.is_empty()) {
334 if let Some((from, to)) = item.split_once('-') {
335 cpus.extend(from.parse::<u32>().ok()?..=to.parse().ok()?);
336 } else {
337 cpus.push(item.parse().ok()?);
338 }
339 }
340 Some(cpus)
341 }
342
343 fn allowed() -> Vec<u32> {
345 query_mask(|mask| {
346 let res = unsafe {
349 libc::sched_getaffinity(0, mem::size_of_val(mask), mask.as_mut_ptr().cast())
350 };
351 if res == 0 {
352 Ok(())
353 } else {
354 Err(std::io::Error::last_os_error().raw_os_error().unwrap_or(0))
355 }
356 })
357 .unwrap_or_default()
358 }
359
360 const SET_WORDS: usize = mem::size_of::<libc::cpu_set_t>() / mem::size_of::<libc::c_ulong>();
362 const MAX_WORDS: usize = SET_WORDS << 10;
364
365 pub(super) fn query_mask(
370 mut query: impl FnMut(&mut [libc::c_ulong]) -> Result<(), i32>,
371 ) -> Option<Vec<u32>> {
372 let mut words = SET_WORDS;
373 loop {
374 let mut mask = vec![0; words];
375 match query(&mut mask) {
376 Ok(()) => {
377 let bits = libc::c_ulong::BITS;
378 return Some(
379 (0u32..)
380 .zip(&mask)
381 .flat_map(|(idx, word)| {
382 (0..bits)
383 .filter(move |bit| word & (1 << bit) != 0)
384 .map(move |bit| idx * bits + bit)
385 })
386 .collect(),
387 );
388 }
389 Err(libc::EINVAL) if words < MAX_WORDS => words *= 2,
390 Err(_) => return None,
391 }
392 }
393 }
394
395 pub(super) fn mask_of(cpus: &[Cpu]) -> Vec<libc::c_ulong> {
397 let bits = libc::c_ulong::BITS;
398 let max = cpus.iter().map(|cpu| cpu.num / bits + 1).max().unwrap_or(0);
399 let mut mask = vec![0; SET_WORDS.max(max as usize)];
400 for cpu in cpus {
401 mask[(cpu.num / bits) as usize] |= 1 << (cpu.num % bits);
402 }
403 mask
404 }
405
406 pub(super) fn cpu(idx: usize) -> Cpu {
407 Cpu {
408 group: 0,
409 num: u32::try_from(idx).unwrap_or(u32::MAX),
410 }
411 }
412
413 pub(super) fn index(cpu: Cpu) -> usize {
414 usize::try_from(cpu.num).unwrap_or(usize::MAX)
415 }
416
417 pub(super) fn incoming_cpu(stream: &std::net::TcpStream) -> Option<Cpu> {
418 use std::os::fd::AsRawFd;
419
420 #[cfg(target_os = "linux")]
421 const SO_INCOMING_CPU: libc::c_int = libc::SO_INCOMING_CPU;
422 #[cfg(target_os = "android")]
423 const SO_INCOMING_CPU: libc::c_int = 49;
424
425 let mut cpu: libc::c_int = -1;
426 let mut len = libc::socklen_t::try_from(mem::size_of::<libc::c_int>()).ok()?;
427 let res = unsafe {
429 libc::getsockopt(
430 stream.as_raw_fd(),
431 libc::SOL_SOCKET,
432 SO_INCOMING_CPU,
433 (&raw mut cpu).cast(),
434 &raw mut len,
435 )
436 };
437 let num = u32::try_from(cpu).ok().filter(|_| res == 0)?;
439 Some(Cpu { group: 0, num })
440 }
441
442 pub(super) fn apply(mode: Affinity, cpu: Cpu, core: &[Cpu]) -> bool {
443 let cpus = if mode == Affinity::Cpu {
444 std::slice::from_ref(&cpu)
445 } else {
446 core
447 };
448 if cpus
449 .iter()
450 .any(|cpu| cpu.num as usize >= MAX_WORDS * libc::c_ulong::BITS as usize)
451 {
452 return false;
453 }
454 let mask = mask_of(cpus);
455 unsafe {
458 libc::sched_setaffinity(0, mem::size_of_val(&mask[..]), mask.as_ptr().cast()) == 0
459 }
460 }
461
462 #[cfg(test)]
463 pub(super) fn current() -> (Vec<Cpu>, Option<Cpu>) {
465 let cpus = allowed()
466 .into_iter()
467 .map(|num| Cpu { group: 0, num })
468 .collect();
469 (cpus, None)
470 }
471}
472
473#[cfg(windows)]
474mod sys {
475 use std::{mem, net::TcpStream, os::windows::io::AsRawSocket, ptr};
476
477 use windows_sys::Win32::Networking::WinSock::{
478 SIO_QUERY_RSS_PROCESSOR_INFO, SOCKET, SOCKET_PROCESSOR_AFFINITY, WSAIoctl,
479 };
480 use windows_sys::Win32::System::JobObjects::{
481 JobObjectGroupInformationEx, QueryInformationJobObject,
482 };
483 use windows_sys::Win32::System::Kernel::PROCESSOR_NUMBER;
484 use windows_sys::Win32::System::SystemInformation::{
485 CpuSetInformation, GROUP_AFFINITY, GetLogicalProcessorInformationEx,
486 GetSystemCpuSetInformation, RelationAll, RelationCache, RelationNumaNode,
487 RelationNumaNodeEx, RelationProcessorCore, SYSTEM_CPU_SET_INFORMATION,
488 SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX,
489 };
490 use windows_sys::Win32::System::Threading::{
491 GetActiveProcessorCount, GetActiveProcessorGroupCount, GetCurrentProcess, GetCurrentThread,
492 GetProcessAffinityMask, GetProcessDefaultCpuSets, GetProcessGroupAffinity,
493 GetThreadGroupAffinity, SetThreadGroupAffinity, SetThreadIdealProcessorEx,
494 };
495
496 use super::{Affinity, Cpu, CpuInfo};
497
498 #[allow(non_upper_case_globals)]
499 pub(super) fn topology() -> Vec<CpuInfo> {
500 let Some(buf) = query() else {
501 return Vec::new();
502 };
503
504 let mut cpus = Vec::new();
505 let mut l3 = Vec::new();
506 let mut nodes = Vec::new();
507 let base = buf.as_ptr();
508 let len = buf.len() * mem::size_of::<u64>();
509 let mut offset = 0;
510 let mut idx = 0;
511
512 unsafe {
516 while offset + mem::size_of::<SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>() <= len {
517 let rec = base
518 .byte_add(offset)
519 .cast::<SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX>();
520 let size = (*rec).Size as usize;
521 if size == 0 {
522 break;
523 }
524 let info = ptr::addr_of!((*rec).Anonymous);
525 match (*rec).Relationship {
526 RelationProcessorCore => {
527 let p = ptr::addr_of!((*info).Processor);
528 let class = u32::from((*p).EfficiencyClass);
529 let masks = ptr::addr_of!((*p).GroupMask).cast::<GROUP_AFFINITY>();
530 for mask in masks_of(masks, (*p).GroupCount) {
531 cpus.extend(cpus_of(&mask).map(|cpu| CpuInfo {
532 cpu,
533 core: idx,
534 l3: usize::MAX,
535 node: usize::MAX - usize::from(cpu.group),
536 class,
537 allowed: true,
538 }));
539 }
540 }
541 RelationCache => {
542 let c = ptr::addr_of!((*info).Cache);
543 if (*c).Level == 3 {
544 let masks = ptr::addr_of!((*c).Anonymous).cast::<GROUP_AFFINITY>();
545 l3.extend(masks_of(masks, (*c).GroupCount).map(|m| (idx, m)));
546 }
547 }
548 RelationNumaNode | RelationNumaNodeEx => {
549 let n = ptr::addr_of!((*info).NumaNode);
550 let node = (*n).NodeNumber as usize;
551 let masks = ptr::addr_of!((*n).Anonymous).cast::<GROUP_AFFINITY>();
552 nodes.extend(masks_of(masks, (*n).GroupCount).map(|m| (node, m)));
553 }
554 _ => (),
555 }
556 offset += size;
557 idx += 1;
558 }
559 }
560
561 let contains = |mask: &GROUP_AFFINITY, cpu: Cpu| {
562 mask.Group == cpu.group && cpu.num < usize::BITS && mask.Mask & (1 << cpu.num) != 0
563 };
564 for info in &mut cpus {
565 if let Some((id, _)) = l3.iter().find(|(_, m)| contains(m, info.cpu)) {
566 info.l3 = *id;
567 } else {
568 info.l3 = usize::MAX - usize::from(info.cpu.group);
570 }
571 if let Some((id, _)) = nodes.iter().find(|(_, m)| contains(m, info.cpu)) {
572 info.node = *id;
573 }
574 }
575
576 let mask = affinity_mask();
577 for info in &mut cpus {
578 info.allowed = mask.is_none_or(|(group, mask)| {
579 info.cpu.group == group
580 && info.cpu.num < usize::BITS
581 && mask & (1 << info.cpu.num) != 0
582 });
583 }
584 if let Some(restricted) = restricted()
585 && cpus
586 .iter()
587 .any(|info| info.allowed && restricted.contains(&info.cpu))
588 {
589 for info in &mut cpus {
590 info.allowed &= restricted.contains(&info.cpu);
591 }
592 }
593 cpus
594 }
595
596 pub(super) fn restricted() -> Option<Vec<Cpu>> {
601 let mut result: Option<Vec<Cpu>> = None;
602
603 unsafe {
605 let mut masks = [GROUP_AFFINITY::default(); 64];
606 let mut len = 0u32;
607 if QueryInformationJobObject(
608 ptr::null_mut(),
609 JobObjectGroupInformationEx,
610 masks.as_mut_ptr().cast(),
611 u32::try_from(mem::size_of_val(&masks)).unwrap_or(0),
612 &raw mut len,
613 ) != 0
614 {
615 let n = len as usize / mem::size_of::<GROUP_AFFINITY>();
616 let cpus: Vec<Cpu> = masks[..n.min(masks.len())]
617 .iter()
618 .flat_map(cpus_of)
619 .collect();
620 if !cpus.is_empty() {
621 result = Some(cpus);
622 }
623 }
624
625 let process = GetCurrentProcess();
626 let mut count = 0u32;
627 GetProcessDefaultCpuSets(process, ptr::null_mut(), 0, &raw mut count);
628 let mut ids = vec![0u32; count as usize];
629 if count > 0
630 && GetProcessDefaultCpuSets(process, ids.as_mut_ptr(), count, &raw mut count) != 0
631 {
632 ids.truncate(count as usize);
633 let sets: Vec<Cpu> = cpu_sets()
634 .into_iter()
635 .filter(|(id, cpu)| {
636 ids.contains(id) && result.as_ref().is_none_or(|r| r.contains(cpu))
637 })
638 .map(|(_, cpu)| cpu)
639 .collect();
640 if !sets.is_empty() {
641 result = Some(sets);
642 }
643 }
644 }
645 result
646 }
647
648 fn cpu_sets() -> Vec<(u32, Cpu)> {
650 let mut result = Vec::new();
651 unsafe {
654 let mut len = 0u32;
655 GetSystemCpuSetInformation(ptr::null_mut(), 0, &raw mut len, ptr::null_mut(), 0);
656 let mut buf = vec![0u64; (len as usize).div_ceil(mem::size_of::<u64>())];
657 if GetSystemCpuSetInformation(
658 buf.as_mut_ptr().cast(),
659 len,
660 &raw mut len,
661 ptr::null_mut(),
662 0,
663 ) == 0
664 {
665 return result;
666 }
667 let base = buf.as_ptr();
668 let mut offset = 0;
669 while offset + mem::size_of::<SYSTEM_CPU_SET_INFORMATION>() <= len as usize {
670 let rec = base.byte_add(offset).cast::<SYSTEM_CPU_SET_INFORMATION>();
671 let size = (*rec).Size as usize;
672 if size == 0 {
673 break;
674 }
675 if (*rec).Type == CpuSetInformation {
676 let set = ptr::addr_of!((*rec).Anonymous.CpuSet);
677 result.push((
678 (*set).Id,
679 Cpu {
680 group: (*set).Group,
681 num: u32::from((*set).LogicalProcessorIndex),
682 },
683 ));
684 }
685 offset += size;
686 }
687 }
688 result
689 }
690
691 fn query() -> Option<Vec<u64>> {
692 let mut len = 0u32;
693 unsafe {
695 GetLogicalProcessorInformationEx(RelationAll, ptr::null_mut(), &raw mut len);
696 for _ in 0..3 {
697 let mut buf = vec![0u64; (len as usize).div_ceil(mem::size_of::<u64>())];
698 if GetLogicalProcessorInformationEx(
699 RelationAll,
700 buf.as_mut_ptr().cast(),
701 &raw mut len,
702 ) != 0
703 {
704 buf.truncate((len as usize).div_ceil(mem::size_of::<u64>()));
705 return Some(buf);
706 }
707 }
708 }
709 None
710 }
711
712 unsafe fn masks_of(
716 ptr: *const GROUP_AFFINITY,
717 count: u16,
718 ) -> impl Iterator<Item = GROUP_AFFINITY> {
719 (0..usize::from(count.max(1))).map(move |i| unsafe { ptr.add(i).read_unaligned() })
721 }
722
723 fn cpus_of(mask: &GROUP_AFFINITY) -> impl Iterator<Item = Cpu> {
724 let (group, mask) = (mask.Group, mask.Mask);
725 (0..usize::BITS)
726 .filter(move |num| mask & (1 << num) != 0)
727 .map(move |num| Cpu { group, num })
728 }
729
730 pub(super) fn affinity_mask() -> Option<(u16, usize)> {
738 unsafe {
740 let process = GetCurrentProcess();
741 let (mut mask, mut system) = (0, 0);
742 if GetProcessAffinityMask(process, &raw mut mask, &raw mut system) == 0
744 || mask == 0
745 || mask == system
746 {
747 return None;
748 }
749 let (mut count, mut group) = (1u16, 0u16);
750 (GetProcessGroupAffinity(process, &raw mut count, &raw mut group) != 0)
751 .then_some((group, mask))
752 }
753 }
754
755 fn mask_of(cpus: &[Cpu], group: u16) -> usize {
756 cpus.iter()
757 .filter(|cpu| cpu.group == group && cpu.num < usize::BITS)
758 .fold(0, |mask, cpu| mask | (1 << cpu.num))
759 }
760
761 pub(super) fn cpu(mut idx: usize) -> Cpu {
762 unsafe {
764 for group in 0..GetActiveProcessorGroupCount() {
765 let n = GetActiveProcessorCount(group);
766 match u32::try_from(idx) {
767 Ok(num) if num < n => return Cpu { group, num },
768 _ => idx -= n as usize,
769 }
770 }
771 }
772 Cpu {
774 group: u16::MAX,
775 num: 0,
776 }
777 }
778
779 pub(super) fn index(cpu: Cpu) -> usize {
780 let before: u32 = (0..cpu.group)
782 .map(|group| unsafe { GetActiveProcessorCount(group) })
783 .sum();
784 (before + cpu.num) as usize
785 }
786
787 #[allow(clippy::cast_possible_truncation)]
789 pub(super) fn incoming_cpu(stream: &TcpStream) -> Option<Cpu> {
790 let mut info = SOCKET_PROCESSOR_AFFINITY::default();
791 let mut len = 0u32;
792 let res = unsafe {
795 WSAIoctl(
796 stream.as_raw_socket() as SOCKET,
797 SIO_QUERY_RSS_PROCESSOR_INFO,
798 ptr::null(),
799 0,
800 (&raw mut info).cast(),
801 mem::size_of::<SOCKET_PROCESSOR_AFFINITY>() as u32,
802 &raw mut len,
803 ptr::null_mut(),
804 None,
805 )
806 };
807 (res == 0 && len as usize >= mem::size_of::<SOCKET_PROCESSOR_AFFINITY>()).then_some(Cpu {
808 group: info.Processor.Group,
809 num: u32::from(info.Processor.Number),
810 })
811 }
812
813 pub(super) fn apply(mode: Affinity, cpu: Cpu, core: &[Cpu]) -> bool {
814 unsafe {
816 let thread = GetCurrentThread();
817 let mut affinity = GROUP_AFFINITY {
818 Mask: 0,
819 Group: cpu.group,
820 Reserved: [0; 3],
821 };
822 match mode {
823 Affinity::Disabled => true,
824 Affinity::Cpu | Affinity::Core => {
825 affinity.Mask = if mode == Affinity::Cpu {
826 mask_of(&[cpu], cpu.group)
827 } else {
828 mask_of(core, cpu.group)
829 };
830 SetThreadGroupAffinity(thread, &raw const affinity, ptr::null_mut()) != 0
831 }
832 Affinity::Soft => {
833 let mut current = GROUP_AFFINITY::default();
835 if GetThreadGroupAffinity(thread, &raw mut current) != 0
836 && current.Group != cpu.group
837 {
838 let n = GetActiveProcessorCount(cpu.group);
839 affinity.Mask = if n >= usize::BITS { usize::MAX } else { (1 << n) - 1 };
840 if SetThreadGroupAffinity(thread, &raw const affinity, ptr::null_mut()) == 0
841 {
842 return false;
843 }
844 }
845 let Ok(num) = u8::try_from(cpu.num) else {
846 return false;
847 };
848 let ideal = PROCESSOR_NUMBER {
849 Group: cpu.group,
850 Number: num,
851 Reserved: 0,
852 };
853 SetThreadIdealProcessorEx(thread, &raw const ideal, ptr::null_mut()) != 0
854 }
855 }
856 }
857 }
858
859 #[cfg(test)]
860 pub(super) fn current() -> (Vec<Cpu>, Option<Cpu>) {
862 use windows_sys::Win32::System::Threading::GetThreadIdealProcessorEx;
863
864 unsafe {
866 let thread = GetCurrentThread();
867 let mut affinity = GROUP_AFFINITY::default();
868 let cpus = if GetThreadGroupAffinity(thread, &raw mut affinity) != 0 {
869 cpus_of(&affinity).collect()
870 } else {
871 Vec::new()
872 };
873 let mut ideal = PROCESSOR_NUMBER::default();
874 let ideal = (GetThreadIdealProcessorEx(thread, &raw mut ideal) != 0).then_some(Cpu {
875 group: ideal.Group,
876 num: u32::from(ideal.Number),
877 });
878 (cpus, ideal)
879 }
880 }
881}
882
883#[cfg(not(any(windows, target_os = "linux", target_os = "android")))]
884mod sys {
885 use super::{Affinity, Cpu, CpuInfo};
886
887 pub(super) fn topology() -> Vec<CpuInfo> {
888 Vec::new()
889 }
890
891 pub(super) fn cpu(idx: usize) -> Cpu {
892 Cpu {
893 group: 0,
894 num: u32::try_from(idx).unwrap_or(u32::MAX),
895 }
896 }
897
898 pub(super) fn index(cpu: Cpu) -> usize {
899 usize::try_from(cpu.num).unwrap_or(usize::MAX)
900 }
901
902 pub(super) fn incoming_cpu(_: &std::net::TcpStream) -> Option<Cpu> {
903 None
904 }
905
906 pub(super) fn apply(_: Affinity, _: Cpu, _: &[Cpu]) -> bool {
907 false
908 }
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914
915 fn cpu(num: u32) -> Cpu {
916 Cpu { group: 0, num }
917 }
918
919 fn info(num: u32, core: usize, l3: usize, node: usize) -> CpuInfo {
920 CpuInfo {
921 cpu: cpu(num),
922 core,
923 l3,
924 node,
925 class: 0,
926 allowed: true,
927 }
928 }
929
930 fn nums(cpus: &[CpuInfo]) -> Vec<u32> {
931 order(cpus).into_iter().map(|(cpu, ..)| cpu.num).collect()
932 }
933
934 #[test]
935 fn order_smt() {
936 let cpus: Vec<_> = (0..8).map(|n| info(n, n as usize / 2, 0, 0)).collect();
938 assert_eq!(nums(&cpus), [0, 2, 4, 6, 1, 3, 5, 7]);
939 assert_eq!(order(&cpus)[5].1, [cpu(2), cpu(3)]);
940
941 let cpus: Vec<_> = (0..8).map(|n| info(n, n as usize % 4, 0, 0)).collect();
943 assert_eq!(nums(&cpus), [0, 1, 2, 3, 4, 5, 6, 7]);
944 assert_eq!(order(&cpus)[5].1, [cpu(1), cpu(5)]);
945 }
946
947 #[test]
948 fn order_cache_and_nodes() {
949 let cpus: Vec<_> = (0..8)
951 .map(|n| info(n, n as usize, n as usize / 4, 0))
952 .collect();
953 assert_eq!(nums(&cpus), [0, 4, 1, 5, 2, 6, 3, 7]);
954
955 let cpus: Vec<_> = (0..8)
957 .map(|n| info(n, n as usize, n as usize / 2, n as usize / 4))
958 .collect();
959 assert_eq!(nums(&cpus), [0, 4, 2, 6, 1, 5, 3, 7]);
960
961 let cpus: Vec<_> = (0..6)
963 .map(|n| info(n, n as usize, usize::from(n > 3), 0))
964 .collect();
965 assert_eq!(nums(&cpus), [0, 4, 1, 5, 2, 3]);
966 }
967
968 #[test]
969 fn order_hybrid() {
970 let mut cpus: Vec<_> = (0..4).map(|n| info(n, n as usize / 2, 0, 0)).collect();
972 for c in &mut cpus {
973 c.class = 1;
974 }
975 cpus.extend((4..6).map(|n| info(n, n as usize, 0, 0)));
976 assert_eq!(nums(&cpus), [0, 2, 4, 5, 1, 3]);
977 }
978
979 #[test]
980 fn order_restricted() {
981 let cpus = [
983 info(1, 0, 0, 0),
984 info(2, 1, 0, 0),
985 info(3, 1, 0, 0),
986 info(5, 2, 0, 0),
987 ];
988 assert_eq!(nums(&cpus), [1, 2, 5, 3]);
989 assert_eq!(order(&cpus)[0].1, [cpu(1)]);
990 assert_eq!(order(&cpus)[3].1, [cpu(2), cpu(3)]);
991 assert_eq!(order(&[]), []);
992
993 let mut cpus: Vec<_> = (0..8).map(|n| info(n, n as usize % 4, 0, 0)).collect();
995 for c in &mut cpus {
996 c.allowed = [4, 6].contains(&c.cpu.num);
997 }
998 let order = order(&cpus);
999 assert_eq!(nums(&cpus), [4, 6]);
1000 assert_eq!(order[0].1, [cpu(4)]);
1001 assert_eq!(order[0].2, [cpu(0), cpu(4)]);
1002 assert_eq!(order[1].2, [cpu(2), cpu(6)]);
1003 }
1004
1005 #[test]
1006 fn plan_system() {
1007 assert_eq!(plan(Affinity::Disabled), []);
1008
1009 let plan = plan(Affinity::Core);
1010 let mut cpus: Vec<_> = plan
1011 .iter()
1012 .map(|p| {
1013 assert_eq!(p.mode, Affinity::Core);
1014 assert!(p.core.contains(&p.cpu));
1015 assert!(p.core.iter().all(|cpu| p.siblings.contains(cpu)));
1016 p.cpu
1017 })
1018 .collect();
1019 cpus.sort_unstable();
1020 cpus.dedup();
1021 assert_eq!(cpus.len(), plan.len());
1022 assert!(cpu_count() > 0);
1023 if cfg!(windows) {
1024 assert_eq!(cpu_count(), plan.len());
1025 }
1026 #[cfg(any(windows, target_os = "linux", target_os = "android"))]
1027 assert_ne!(plan, []);
1028 #[cfg(any(target_os = "linux", target_os = "android"))]
1029 assert_eq!(sys::current().0.len(), plan.len());
1030 #[cfg(not(any(windows, target_os = "linux", target_os = "android")))]
1031 assert_eq!(plan, []);
1032 }
1033
1034 #[cfg(any(windows, target_os = "linux", target_os = "android"))]
1035 #[test]
1036 fn apply_placement() {
1037 let Some(Placement { cpu, core, .. }) = plan(Affinity::Core).pop() else {
1038 return;
1039 };
1040 for mode in [Affinity::Cpu, Affinity::Core, Affinity::Soft] {
1041 let p = Placement {
1042 mode,
1043 cpu,
1044 core: core.clone(),
1045 siblings: core.clone(),
1046 };
1047 let (cpus, ideal) = std::thread::spawn(move || {
1048 assert!(p.apply());
1049 sys::current()
1050 })
1051 .join()
1052 .unwrap();
1053 match mode {
1054 Affinity::Cpu => assert_eq!(cpus, [cpu]),
1055 Affinity::Core => assert_eq!(cpus, core),
1056 _ if cfg!(windows) => assert_eq!(ideal, Some(cpu)),
1057 _ => assert_eq!(cpus, core),
1058 }
1059 }
1060 }
1061
1062 #[cfg(any(windows, target_os = "linux", target_os = "android"))]
1063 #[test]
1064 fn apply_cpu_index() {
1065 let Some(cpu) = plan(Affinity::Core)
1066 .into_iter()
1067 .map(|p| p.cpu)
1068 .filter(|cpu| cpu.group == 0)
1069 .max()
1070 else {
1071 return;
1072 };
1073 let p = Placement::cpu(cpu.num as usize);
1074 assert_eq!(p.cpu, cpu);
1075 let (cpus, _) = std::thread::spawn(move || {
1076 assert!(p.apply());
1077 sys::current()
1078 })
1079 .join()
1080 .unwrap();
1081 assert_eq!(cpus, [cpu]);
1082
1083 assert!(!Placement::cpu(usize::MAX >> 1).apply());
1084 }
1085
1086 #[test]
1087 fn cpu_indexes() {
1088 for p in plan(Affinity::Core) {
1089 let cpus = p.cpus();
1090 assert_eq!(sys::cpu(cpus[0]), p.cpu);
1091 assert_eq!(cpus.len(), p.siblings.len());
1092 for idx in &cpus {
1093 assert!(p.siblings.contains(&sys::cpu(*idx)), "{p}: {cpus:?}");
1094 }
1095 }
1096 }
1097
1098 #[test]
1099 fn incoming_cpu_loopback() {
1100 let lst = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1101 let client = std::net::TcpStream::connect(lst.local_addr().unwrap()).unwrap();
1102 let (server, _) = lst.accept().unwrap();
1103 for stream in [&client, &server] {
1106 let cpu = incoming_cpu(stream);
1107 if let Some(cpu) = cpu {
1108 assert_eq!(sys::index(sys::cpu(cpu)), cpu);
1109 }
1110 #[cfg(any(target_os = "linux", target_os = "android"))]
1111 assert!(cpu.is_some());
1112 }
1113 }
1114
1115 #[cfg(any(target_os = "linux", target_os = "android"))]
1116 #[test]
1117 fn linux_sysfs() {
1118 use std::fs;
1119
1120 assert_eq!(
1121 sys::parse_list("0-3,8-11\n"),
1122 Some(vec![0, 1, 2, 3, 8, 9, 10, 11])
1123 );
1124 assert_eq!(sys::parse_list("5"), Some(vec![5]));
1125 assert_eq!(sys::parse_list("\n"), Some(vec![]));
1126 assert_eq!(sys::parse_list("a-b"), None);
1127
1128 let root = std::env::temp_dir().join(format!("ntex-affinity-{}", std::process::id()));
1131 let _ = fs::remove_dir_all(&root);
1132 for (num, core) in [(0, "0,2"), (1, "1,3"), (2, "0,2")] {
1133 let dir = root.join(format!("cpu{num}"));
1134 fs::create_dir_all(dir.join("topology")).unwrap();
1135 fs::create_dir_all(dir.join("cache/index3")).unwrap();
1136 fs::create_dir_all(dir.join("node1")).unwrap();
1137 fs::write(dir.join("topology/core_cpus_list"), core).unwrap();
1138 fs::write(dir.join("cache/index3/shared_cpu_list"), "0-3\n").unwrap();
1139 }
1140 let cpus = sys::read(&root, &[0, 1, 2, 3], &[0, 1, 2, 3]);
1141 let restricted = sys::read(&root, &[0, 1, 2, 3], &[0, 1, 3]);
1142 fs::remove_dir_all(&root).unwrap();
1143
1144 let ids: Vec<_> = cpus.iter().map(|c| (c.core, c.l3, c.node)).collect();
1145 assert_eq!(ids, [(0, 0, 1), (1, 0, 1), (0, 0, 1), (3, 3, 0)]);
1146 assert_eq!(nums(&cpus), [3, 0, 1, 2]);
1147
1148 assert_eq!(nums(&restricted), [3, 0, 1]);
1150 let (_, core, siblings) = &order(&restricted)[1];
1151 assert_eq!(
1152 (&core[..], &siblings[..]),
1153 (&[cpu(0)][..], &[cpu(0), cpu(2)][..])
1154 );
1155 }
1156
1157 #[cfg(any(target_os = "linux", target_os = "android"))]
1158 #[test]
1159 fn linux_large_mask() {
1160 let bits = libc::c_ulong::BITS;
1161 let mask = sys::mask_of(&[cpu(1), cpu(3000)]);
1162 assert_eq!(mask.len(), (3000 / bits + 1) as usize);
1163 assert_eq!(mask[0], 2);
1164 assert_eq!(mask[(3000 / bits) as usize], 1 << (3000 % bits));
1165 assert_eq!(
1166 sys::mask_of(&[cpu(0)]).len() * size_of::<libc::c_ulong>(),
1167 size_of::<libc::cpu_set_t>()
1168 );
1169
1170 let mut sizes = Vec::new();
1172 let cpus = sys::query_mask(|m| {
1173 sizes.push(m.len());
1174 if m.len() * bits as usize <= 3000 {
1175 return Err(libc::EINVAL);
1176 }
1177 m[..mask.len()].copy_from_slice(&mask);
1178 Ok(())
1179 });
1180 assert_eq!(cpus, Some(vec![1, 3000]));
1181 assert_eq!(sizes.len(), 3, "{sizes:?}");
1182 assert_eq!(sys::query_mask(|_| Err(libc::EPERM)), None);
1183 assert_eq!(sys::query_mask(|_| Err(libc::EINVAL)), None);
1184
1185 assert!(!Placement::cpu(usize::MAX >> 1).apply());
1186 }
1187
1188 #[cfg(windows)]
1189 #[test]
1190 fn windows_process_mask() {
1191 use windows_sys::Win32::System::Threading::{
1192 ALL_PROCESSOR_GROUPS, GetActiveProcessorCount, GetCurrentProcess,
1193 GetProcessAffinityMask, SetProcessAffinityMask,
1194 };
1195
1196 const CHILD: &str = "NTEX_AFFINITY_MASK_CHILD";
1197 if std::env::var_os(CHILD).is_none() {
1198 if sys::affinity_mask().is_none() && sys::restricted().is_none() {
1200 let n = unsafe { GetActiveProcessorCount(ALL_PROCESSOR_GROUPS) };
1202 assert_eq!(plan(Affinity::Core).len(), n as usize);
1203 }
1204 let status = std::process::Command::new(std::env::current_exe().unwrap())
1206 .args(["affinity::tests::windows_process_mask", "--exact"])
1207 .env(CHILD, "1")
1208 .status()
1209 .unwrap();
1210 assert!(status.success());
1211 return;
1212 }
1213
1214 unsafe {
1216 let process = GetCurrentProcess();
1217 let (mut mask, mut system) = (0, 0);
1218 assert_ne!(
1219 GetProcessAffinityMask(process, &raw mut mask, &raw mut system),
1220 0
1221 );
1222 if mask.count_ones() < 3 {
1223 return;
1224 }
1225 let high = usize::BITS - 1 - mask.leading_zeros();
1227 let next = usize::BITS - 1 - (mask & !(1 << high)).leading_zeros();
1228 let restricted = (1usize << high) | (1 << next);
1229 assert_ne!(SetProcessAffinityMask(process, restricted), 0);
1230
1231 let (group, mask) = sys::affinity_mask().unwrap();
1232 assert_eq!(mask, restricted);
1233 let mut cpus: Vec<_> = plan(Affinity::Core).into_iter().map(|p| p.cpu).collect();
1234 cpus.sort_unstable();
1235 assert_eq!(cpus, [Cpu { group, num: next }, Cpu { group, num: high }]);
1236 assert_eq!(cpu_count(), 2);
1237 }
1238 }
1239}