Skip to main content

ntex_server/
affinity.rs

1use std::{collections::BTreeMap, fmt};
2
3#[non_exhaustive]
4#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
5/// CPU affinity of worker threads.
6///
7/// Workers are placed on one logical CPU of each physical core first, spread
8/// across NUMA nodes and shared L3 caches, and on the remaining SMT siblings
9/// after that. On hybrid CPUs, performance cores are used before efficiency
10/// cores. When there are more workers than CPUs, the placement starts over.
11///
12/// The default is [`Soft`](Self::Soft) on Windows and [`Core`](Self::Core) on
13/// other platforms. Affinity is supported on Windows, Linux and Android, other
14/// platforms ignore it.
15pub enum Affinity {
16    /// Worker threads are not pinned, the OS scheduler places them.
17    Disabled,
18    /// Pins each worker to one logical CPU.
19    Cpu,
20    /// Pins each worker to the logical CPUs of one physical core.
21    ///
22    /// The scheduler can move the worker between the SMT siblings of its
23    /// core. Works like [`Cpu`](Self::Cpu) on CPUs without SMT.
24    Core,
25    /// Sets the preferred CPU of each worker without pinning it.
26    ///
27    /// On Windows, this sets the ideal processor of the worker thread. The
28    /// scheduler runs the worker there when that CPU is free, and on another
29    /// CPU otherwise. On machines with several processor groups, the worker is
30    /// also kept in the group of its CPU. Other platforms have no such hint,
31    /// there it works like [`Core`](Self::Core).
32    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)]
46/// Logical CPU, `group` is the Windows processor group.
47pub(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)]
63/// Topology of one logical CPU.
64///
65/// `core`, `l3` and `node` identify the physical core, L3 cache and NUMA
66/// node. Higher `class` means a faster core. `allowed` is `false` if the
67/// process may not use the CPU.
68struct 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)]
78/// CPU placement of a worker thread.
79pub(crate) struct Placement {
80    mode: Affinity,
81    cpu: Cpu,
82    /// Logical CPUs of the physical core of `cpu` the process may use.
83    core: Vec<Cpu>,
84    /// All logical CPUs of the physical core of `cpu`.
85    siblings: Vec<Cpu>,
86}
87
88impl Placement {
89    /// Pins to the logical CPU with index `idx`.
90    ///
91    /// On Windows, CPUs are numbered across processor groups, the CPUs of
92    /// group 1 follow the CPUs of group 0.
93    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    /// Applies the placement to the current thread.
104    pub(crate) fn apply(&self) -> bool {
105        sys::apply(self.mode, self.cpu, &self.core)
106    }
107
108    /// Returns the index of the placement CPU, followed by the indexes of
109    /// its SMT siblings, including siblings the process may not use. The
110    /// network stack may still process packets on them.
111    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
123/// Returns the index of the logical CPU that receives the packets of the
124/// connection, if known.
125///
126/// On Windows, this is the receive-side scaling (RSS) CPU of the connection,
127/// on Linux and Android the CPU that processed its last received packet
128/// (`SO_INCOMING_CPU`). Not supported on other platforms.
129pub(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
143/// Returns worker placements in the order workers should use them.
144///
145/// Each logical CPU available to the process appears once. Empty if
146/// affinity is disabled or not supported.
147pub(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
162/// Returns the number of logical CPUs the process may use.
163pub(crate) fn cpu_count() -> usize {
164    // std counts only one processor group on Windows
165    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
182/// Orders the logical CPUs the process may use, see [`Affinity`].
183///
184/// Returns each CPU with the CPUs of its physical core the process may use,
185/// and all CPUs of that core.
186fn 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    // cores of each class, interleaved across L3 caches within a NUMA node,
218    // then across NUMA nodes
219    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    // first logical CPU of every core, then the second one, and so on
239    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
251/// Takes the first item of each list, then the second one, and so on.
252fn 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    /// Reads the topology of `cpus` from sysfs at `root`, the process may
281    /// use the `allowed` ones.
282    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    /// Parses a CPU list like `0-3,8-11`.
331    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    /// Returns the CPUs the current thread may run on.
344    fn allowed() -> Vec<u32> {
345        query_mask(|mask| {
346            // SAFETY: the kernel writes at most `size_of_val(mask)` bytes,
347            // the mask has the layout of a larger `cpu_set_t`
348            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    /// Words of a `cpu_set_t`.
361    const SET_WORDS: usize = mem::size_of::<libc::cpu_set_t>() / mem::size_of::<libc::c_ulong>();
362    /// Largest mask tried, in words.
363    const MAX_WORDS: usize = SET_WORDS << 10;
364
365    /// Reads a CPU mask with `query`.
366    ///
367    /// The kernel mask may be larger than `cpu_set_t`, the query fails with
368    /// `EINVAL` until the mask is large enough.
369    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    /// Returns a CPU mask of `cpus`, at least the size of `cpu_set_t`.
396    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        // SAFETY: the output buffer holds `len` bytes
428        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        // -1 if no packet was received yet
438        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        // SAFETY: the kernel reads `size_of_val(mask)` bytes, the mask has
456        // the layout of a larger `cpu_set_t`
457        unsafe {
458            libc::sched_setaffinity(0, mem::size_of_val(&mask[..]), mask.as_ptr().cast()) == 0
459        }
460    }
461
462    #[cfg(test)]
463    /// Returns the CPUs the current thread may run on and its preferred CPU.
464    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        // SAFETY: records are 8-byte aligned and lie within the buffer filled
513        // by GetLogicalProcessorInformationEx, `GroupCount` masks follow
514        // each record's mask field
515        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                // no L3 cache, group by NUMA node
569                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    /// Returns the CPUs allowed by the job object group affinity and the
597    /// default CPU sets of the process, if any.
598    ///
599    /// The process affinity mask covers only one processor group.
600    pub(super) fn restricted() -> Option<Vec<Cpu>> {
601        let mut result: Option<Vec<Cpu>> = None;
602
603        // SAFETY: buffers hold the lengths passed with them
604        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    /// Returns the id and CPU of each CPU set of the system.
649    fn cpu_sets() -> Vec<(u32, Cpu)> {
650        let mut result = Vec::new();
651        // SAFETY: the buffer holds `len` bytes and is 8-byte aligned, records
652        // lie within the buffer
653        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        // SAFETY: the buffer holds `len` bytes and is 8-byte aligned
694        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    /// Reads `count` group masks starting at `ptr`.
713    ///
714    /// Older Windows versions report zero for a single mask.
715    unsafe fn masks_of(
716        ptr: *const GROUP_AFFINITY,
717        count: u16,
718    ) -> impl Iterator<Item = GROUP_AFFINITY> {
719        // SAFETY: the caller guarantees `count` masks at `ptr`
720        (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    /// Returns the processor group and affinity mask of the process if the
731    /// mask restricts it to part of its group.
732    ///
733    /// A process starts in one processor group, and its threads may move to
734    /// other groups. The groups the process currently runs in do not restrict
735    /// it, an explicit affinity mask (`start /affinity`, job object affinity
736    /// limits) does.
737    pub(super) fn affinity_mask() -> Option<(u16, usize)> {
738        // SAFETY: plain process affinity queries, the buffer holds one group
739        unsafe {
740            let process = GetCurrentProcess();
741            let (mut mask, mut system) = (0, 0);
742            // zero masks if the process runs in several groups
743            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        // SAFETY: plain processor count queries
763        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        // no such CPU, applying it fails
773        Cpu {
774            group: u16::MAX,
775            num: 0,
776        }
777    }
778
779    pub(super) fn index(cpu: Cpu) -> usize {
780        // SAFETY: plain processor count queries
781        let before: u32 = (0..cpu.group)
782            .map(|group| unsafe { GetActiveProcessorCount(group) })
783            .sum();
784        (before + cpu.num) as usize
785    }
786
787    // sockets are pointer-sized, the output buffer has a few bytes
788    #[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        // SAFETY: the output buffer holds the length passed with it, the
793        // call completes synchronously without an overlapped structure
794        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        // SAFETY: plain thread affinity calls on the current thread
815        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                    // the ideal processor must be in the thread's group
834                    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    /// Returns the CPUs the current thread may run on and its preferred CPU.
861    pub(super) fn current() -> (Vec<Cpu>, Option<Cpu>) {
862        use windows_sys::Win32::System::Threading::GetThreadIdealProcessorEx;
863
864        // SAFETY: plain thread affinity calls on the current thread
865        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        // siblings numbered next to each other (Windows)
937        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        // siblings numbered apart (Linux)
942        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        // two L3 caches
950        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        // two NUMA nodes with two L3 caches each
956        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        // uneven L3 caches
962        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        // performance cores with SMT, efficiency cores without
971        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        // the process may only use some CPUs
982        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        // siblings the process may not use are kept for locality only
994        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        // loopback connections have no RSS CPU on Windows, if known the CPU
1104        // must exist
1105        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        // cpus 0-2 on node 1 share an L3 cache, cpu0 and cpu2 are siblings.
1129        // cpu3 has no topology files, it is a separate core on node 0
1130        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        // cpu2 is not allowed, it remains a sibling of cpu0
1149        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        // the kernel mask is larger than `cpu_set_t`
1171        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            // without restrictions, all processor groups are used
1199            if sys::affinity_mask().is_none() && sys::restricted().is_none() {
1200                // SAFETY: plain processor count query
1201                let n = unsafe { GetActiveProcessorCount(ALL_PROCESSOR_GROUPS) };
1202                assert_eq!(plan(Affinity::Core).len(), n as usize);
1203            }
1204            // the process mask is changed in a separate process
1205            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        // SAFETY: plain process affinity calls on the current process
1215        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            // the two highest CPUs of the group
1226            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}