Skip to main content

many_cpus/
processor_set_builder.rs

1use std::collections::VecDeque;
2use std::fmt::Debug;
3use std::num::NonZero;
4
5use foldhash::{HashMap, HashMapExt, HashSet, HashSetExt};
6use itertools::Itertools;
7use nonempty::NonEmpty;
8use rand::prelude::*;
9use rand::rng;
10
11use crate::pal::Platform;
12use crate::{
13    EfficiencyClass, MemoryRegionId, Processor, ProcessorId, ProcessorSet, SystemHardware,
14};
15
16/// Builds a [`ProcessorSet`] based on specified criteria.
17///
18/// You can obtain an instance by calling [`ProcessorSet::to_builder()`] on an existing [`ProcessorSet`],
19/// typically either [`SystemHardware::processors()`] or [`SystemHardware::all_processors()`].
20/// # External constraints
21///
22/// The operating system may define constraints that prohibit the application from using all
23/// the available processors (e.g. when the app is containerized and provided limited
24/// hardware resources).
25///
26/// This package treats platform constraints as follows:
27///
28/// * Hard limits on which processors are allowed are respected - forbidden processors are mostly
29///   ignored by this package and cannot be used to spawn threads, though such processors are still
30///   accounted for when inspecting hardware information such as "max processor ID".
31///   The mechanisms for defining such limits are cgroups on Linux and job objects on Windows.
32///   See `examples/obey_job_affinity_limits_windows.rs` for a Windows-specific example.
33/// * Soft limits on which processors are allowed are ignored by default - specifying a processor
34///   affinity via `taskset` on Linux, `start.exe /affinity 0xff` on Windows or similar mechanisms
35///   does not affect the set of processors this package will use by default, though you can opt in to
36///   this via [`.where_available_for_current_thread()`][crate::ProcessorSetBuilder::where_available_for_current_thread].
37/// * Any operating system enforced processor time quota is taken as the upper bound for the processor
38///   count of the processor set returned by [`SystemHardware::processors()`].
39/// * Any other processor set can be opt-in quota-limited when building the processor set. For example, by calling `SystemHardware::current().all_processors().to_builder().enforce_resource_quota().take_all()`.
40///
41/// See `examples/obey_job_resource_quota_limits_windows.rs` for a Windows-specific example of processor
42/// time quota enforcement.
43///
44/// # Avoiding operating system quota penalties
45///
46/// If a process exceeds the processor time limit, the operating system will delay executing the
47/// process further until the "debt is paid off". This is undesirable for most workloads because:
48///
49/// 1. There will be random latency spikes from when the operating system decides to apply a delay.
50/// 1. The delay may not be evenly applied across all threads of the process, leading to unbalanced
51///    load between worker threads.
52///
53/// For predictable behavior that does not suffer from delay side-effects, it is important that the
54/// process does not exceed the processor time limit. To keep out of trouble,
55/// follow these guidelines:
56///
57/// * Ensure that all your concurrently executing thread pools are derived from the same processor
58///   set, so there is a single set of processors (up to the resource quota) that all work of the
59///   process will be executed on. Any new processor sets you create should be subsets of this set,
60///   thereby ensuring that all worker threads combined do not exceed the quota.
61/// * Ensure that the original processor set is constructed while obeying the resource quota (which is
62///   enabled by default).
63///
64/// If your resource constraints are already applied on process startup, you can use
65/// `SystemHardware::current().processors()` as the master set from which all other
66/// processor sets are derived using either `.take()` or `.to_builder()`. This will ensure the
67/// processor time quota is obeyed because `processors()` is size-limited to the quota.
68///
69/// ```rust
70/// # use many_cpus::SystemHardware;
71/// # use new_zealand::nz;
72/// let hw = SystemHardware::current();
73///
74/// // By taking both senders and receivers from the same original processor set, we
75/// // guarantee that all worker threads combined cannot exceed the processor time quota.
76/// let mail_senders = hw
77///     .processors()
78///     .take(nz!(2))
79///     .expect("need at least 2 processors for mail workers")
80///     .spawn_threads(|_| send_mail());
81///
82/// let mail_receivers = hw
83///     .processors()
84///     .take(nz!(2))
85///     .expect("need at least 2 processors for mail workers")
86///     .spawn_threads(|_| receive_mail());
87/// # fn send_mail() {}
88/// # fn receive_mail() {}
89/// ```
90///
91/// # Inheriting processor affinity from current thread
92///
93/// By default, the processor affinity of the current thread is ignored when building a processor
94/// set, as this type may be used from a thread with a different processor affinity than the threads
95/// one wants to configure.
96///
97/// However, if you do wish to inherit the processor affinity from the current thread, you may do
98/// so by calling [`.where_available_for_current_thread()`] on the builder. This filters out all
99/// processors that the current thread is not configured to execute on.
100///
101/// [`.where_available_for_current_thread()`]: ProcessorSetBuilder::where_available_for_current_thread
102/// [`SystemHardware::processors()`]: crate::SystemHardware::processors
103/// [`SystemHardware::all_processors()`]: crate::SystemHardware::all_processors
104#[derive(Clone, Debug)]
105pub struct ProcessorSetBuilder {
106    processor_type_selector: ProcessorTypeSelector,
107    memory_region_selector: MemoryRegionSelector,
108
109    except_indexes: HashSet<ProcessorId>,
110
111    obey_resource_quota: bool,
112
113    /// When set, only processors with IDs in this set are considered as candidates.
114    /// When `None`, all platform processors are considered.
115    source_processor_ids: Option<HashSet<ProcessorId>>,
116
117    /// The hardware instance to use for pin status tracking and platform access.
118    /// Passed to any processor set we create.
119    hardware: SystemHardware,
120}
121
122impl ProcessorSetBuilder {
123    #[must_use]
124    pub(crate) fn with_internals(hardware: SystemHardware) -> Self {
125        Self {
126            processor_type_selector: ProcessorTypeSelector::Any,
127            memory_region_selector: MemoryRegionSelector::Any,
128            except_indexes: HashSet::new(),
129            obey_resource_quota: false,
130            source_processor_ids: None,
131            hardware,
132        }
133    }
134
135    /// Restricts the builder to only consider processors from the given set.
136    /// This is used internally when building from an existing `ProcessorSet`.
137    #[must_use]
138    pub(crate) fn source_processors(mut self, processors: &NonEmpty<Processor>) -> Self {
139        let ids: HashSet<ProcessorId> = processors.iter().map(Processor::id).collect();
140        self.source_processor_ids = Some(ids);
141        self
142    }
143
144    /// Requires that all processors in the set be marked as [performance processors][1].
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use many_cpus::SystemHardware;
150    /// use new_zealand::nz;
151    ///
152    /// // Get up to 4 performance processors (or fewer if not available)
153    /// let performance_processors = SystemHardware::current()
154    ///     .processors()
155    ///     .to_builder()
156    ///     .performance_processors_only()
157    ///     .take(nz!(4));
158    ///
159    /// if let Some(processors) = performance_processors {
160    ///     println!("Found {} performance processors", processors.len());
161    /// } else {
162    ///     println!("Could not find 4 performance processors");
163    /// }
164    /// ```
165    ///
166    /// [1]: EfficiencyClass::Performance
167    #[must_use]
168    pub fn performance_processors_only(mut self) -> Self {
169        self.processor_type_selector = ProcessorTypeSelector::Performance;
170        self
171    }
172
173    /// Requires that all processors in the set be marked as [efficiency processors][1].
174    ///
175    /// # Example
176    ///
177    /// ```
178    /// use std::num::NonZero;
179    ///
180    /// use many_cpus::SystemHardware;
181    ///
182    /// // Get all available efficiency processors for background tasks
183    /// let efficiency_processors = SystemHardware::current()
184    ///     .processors()
185    ///     .to_builder()
186    ///     .efficiency_processors_only()
187    ///     .take_all();
188    ///
189    /// if let Some(processors) = efficiency_processors {
190    ///     println!(
191    ///         "Using {} efficiency processors for background work",
192    ///         processors.len()
193    ///     );
194    ///
195    ///     // Spawn threads on efficiency processors to handle background tasks
196    ///     let threads = processors.spawn_threads(|processor| {
197    ///         println!(
198    ///             "Background worker started on efficiency processor {}",
199    ///             processor.id()
200    ///         );
201    ///         // Background work here...
202    ///     });
203    /// # for thread in threads { thread.join().unwrap(); }
204    /// }
205    /// ```
206    ///
207    /// [1]: EfficiencyClass::Efficiency
208    #[must_use]
209    pub fn efficiency_processors_only(mut self) -> Self {
210        self.processor_type_selector = ProcessorTypeSelector::Efficiency;
211        self
212    }
213
214    /// Requires that all processors in the set be from different memory regions, selecting a
215    /// maximum of 1 processor from each memory region.
216    ///
217    /// # Example
218    ///
219    /// ```
220    /// use many_cpus::SystemHardware;
221    /// use new_zealand::nz;
222    ///
223    /// // Get one processor from each memory region for distributed processing
224    /// let distributed_processors = SystemHardware::current()
225    ///     .processors()
226    ///     .to_builder()
227    ///     .different_memory_regions()
228    ///     .take(nz!(4));
229    ///
230    /// if let Some(processors) = distributed_processors {
231    ///     println!(
232    ///         "Selected {} processors from different memory regions",
233    ///         processors.len()
234    ///     );
235    ///
236    ///     // Each processor will be in a different memory region,
237    ///     // ideal for parallel work on separate data sets
238    ///     for processor in &processors {
239    ///         println!(
240    ///             "Processor {} in memory region {}",
241    ///             processor.id(),
242    ///             processor.memory_region_id()
243    ///         );
244    ///     }
245    /// }
246    /// ```
247    #[must_use]
248    pub fn different_memory_regions(mut self) -> Self {
249        self.memory_region_selector = MemoryRegionSelector::RequireDifferent;
250        self
251    }
252
253    /// Requires that all processors in the set be from the same memory region.
254    ///
255    /// # Example
256    ///
257    /// ```
258    /// use many_cpus::SystemHardware;
259    /// use new_zealand::nz;
260    ///
261    /// // Get processors from the same memory region for data locality
262    /// let local_processors = SystemHardware::current()
263    ///     .processors()
264    ///     .to_builder()
265    ///     .same_memory_region()
266    ///     .take(nz!(3));
267    ///
268    /// if let Some(processors) = local_processors {
269    ///     println!(
270    ///         "Selected {} processors from the same memory region",
271    ///         processors.len()
272    ///     );
273    ///
274    ///     // All processors share the same memory region for optimal data sharing
275    ///     let memory_region = processors.processors().first().memory_region_id();
276    ///     println!("All processors are in memory region {}", memory_region);
277    ///
278    ///     // Ideal for cooperative processing of shared data
279    ///     let threads = processors.spawn_threads(|processor| {
280    ///         println!(
281    ///             "Worker on processor {} accessing shared memory region {}",
282    ///             processor.id(),
283    ///             processor.memory_region_id()
284    ///         );
285    ///         // Process shared data here...
286    ///     });
287    /// # for thread in threads { thread.join().unwrap(); }
288    /// }
289    /// ```
290    #[must_use]
291    pub fn same_memory_region(mut self) -> Self {
292        self.memory_region_selector = MemoryRegionSelector::RequireSame;
293        self
294    }
295
296    /// Declares a preference that all processors in the set be from different memory regions,
297    /// though will select multiple processors from the same memory region as needed to satisfy
298    /// the requested processor count (while keeping the spread maximal).
299    #[must_use]
300    pub fn prefer_different_memory_regions(mut self) -> Self {
301        self.memory_region_selector = MemoryRegionSelector::PreferDifferent;
302        self
303    }
304
305    /// Declares a preference that all processors in the set be from the same memory region,
306    /// though will select processors from different memory regions as needed to satisfy the
307    /// requested processor count (while keeping the spread minimal).
308    #[must_use]
309    pub fn prefer_same_memory_region(mut self) -> Self {
310        self.memory_region_selector = MemoryRegionSelector::PreferSame;
311        self
312    }
313
314    /// Uses a predicate to identify processors that are valid candidates for building the
315    /// processor set, with a return value of `bool` indicating that a processor is a valid
316    /// candidate for selection into the set.
317    ///
318    /// The candidates are passed to this function without necessarily first considering all other
319    /// conditions - even if this predicate returns `true`, the processor may end up being filtered
320    /// out by other conditions. Conversely, some candidates may already be filtered out before
321    /// being passed to this predicate.
322    ///
323    /// # Example
324    ///
325    /// ```
326    /// use many_cpus::{EfficiencyClass, SystemHardware};
327    /// use new_zealand::nz;
328    ///
329    /// // Select only even-numbered performance processors
330    /// let filtered_processors = SystemHardware::current()
331    ///     .processors()
332    ///     .to_builder()
333    ///     .filter(|p| p.efficiency_class() == EfficiencyClass::Performance && p.id() % 2 == 0)
334    ///     .take(nz!(2));
335    ///
336    /// if let Some(processors) = filtered_processors {
337    ///     for processor in &processors {
338    ///         println!(
339    ///             "Selected processor {} (performance, even ID)",
340    ///             processor.id()
341    ///         );
342    ///     }
343    /// }
344    /// ```
345    #[must_use]
346    pub fn filter(mut self, predicate: impl Fn(&Processor) -> bool) -> Self {
347        // We invoke the filters immediately because the API gets really annoying if the
348        // predicate has to borrow some stuff because it would need to be 'static and that
349        // is cumbersome (since we do not return a generic-lifetimed thing back to the caller).
350        for processor in self.candidate_processors() {
351            if !predicate(&processor) {
352                self.except_indexes.insert(processor.id());
353            }
354        }
355
356        self
357    }
358
359    /// Removes specific processors from the set of candidates.
360    ///
361    /// # Example
362    ///
363    /// ```
364    /// use std::num::NonZero;
365    ///
366    /// use many_cpus::SystemHardware;
367    ///
368    /// // Get the default set and remove the first processor
369    /// let all_processors = SystemHardware::current().processors();
370    /// let first_processor = all_processors.processors().first().clone();
371    ///
372    /// let remaining_processors = all_processors
373    ///     .to_builder()
374    ///     .except([&first_processor])
375    ///     .take_all();
376    ///
377    /// if let Some(processors) = remaining_processors {
378    ///     println!(
379    ///         "Using {} processors (excluding processor {})",
380    ///         processors.len(),
381    ///         first_processor.id()
382    ///     );
383    /// }
384    /// ```
385    #[must_use]
386    pub fn except<'a, I>(mut self, processors: I) -> Self
387    where
388        I: IntoIterator<Item = &'a Processor>,
389    {
390        for processor in processors {
391            self.except_indexes.insert(processor.id());
392        }
393
394        self
395    }
396
397    /// Removes processors from the set of candidates if they are not available for use by the
398    /// current thread.
399    ///
400    /// This is a convenient way to identify the set of processors the platform prefers a process
401    /// to use when called from a non-customized thread such as the `main()` entrypoint.
402    ///
403    /// # Example
404    ///
405    /// ```
406    /// use many_cpus::SystemHardware;
407    ///
408    /// // Get processors available to the current thread (respects OS affinity settings)
409    /// let available_processors = SystemHardware::current()
410    ///     .processors()
411    ///     .to_builder()
412    ///     .where_available_for_current_thread()
413    ///     .take_all()
414    ///     .expect("current thread must be running on at least one processor");
415    ///
416    /// println!(
417    ///     "Current thread can use {} processors",
418    ///     available_processors.len()
419    /// );
420    ///
421    /// // Compare with all processors on the system
422    /// let all_processors = SystemHardware::current().all_processors();
423    ///
424    /// if available_processors.len() < all_processors.len() {
425    ///     println!("Thread affinity is restricting processor usage");
426    ///     println!("  Total system processors: {}", all_processors.len());
427    ///     println!("  Available to this thread: {}", available_processors.len());
428    /// }
429    /// ```
430    #[must_use]
431    pub fn where_available_for_current_thread(mut self) -> Self {
432        let current_thread_processors = self.hardware.platform().current_thread_processors();
433
434        for processor in self.candidate_processors() {
435            if !current_thread_processors.contains(&processor.id()) {
436                self.except_indexes.insert(processor.id());
437            }
438        }
439
440        self
441    }
442
443    /// Enforces the process resource quota when determining the maximum number of processors
444    /// that can be included in the created processor set.
445    ///
446    /// By default, the builder does not enforce the resource quota. Call this method to limit
447    /// the processor set to the quota. See the type-level documentation for more details on
448    /// resource quota handling best practices.
449    ///
450    /// Note that [`SystemHardware::processors()`] already enforces the resource quota, so you
451    /// do not need to call this method when deriving from that processor set.
452    ///
453    /// # Example
454    ///
455    /// ```
456    /// use many_cpus::SystemHardware;
457    /// use new_zealand::nz;
458    ///
459    /// // Get all processors and explicitly enforce resource quota.
460    /// let quota_processors = SystemHardware::current()
461    ///     .all_processors()
462    ///     .to_builder()
463    ///     .enforce_resource_quota()
464    ///     .take(nz!(4));
465    /// ```
466    ///
467    /// [`SystemHardware::processors()`]: crate::SystemHardware::processors
468    #[must_use]
469    pub fn enforce_resource_quota(mut self) -> Self {
470        self.obey_resource_quota = true;
471        self
472    }
473
474    /// Creates a processor set with a specific number of processors that match the
475    /// configured criteria.
476    ///
477    /// If multiple candidate sets are a match, returns an arbitrary one of them. For example, if
478    /// there are six valid candidate processors then `take(4)` may return any four of them.
479    ///
480    /// Returns `None` if there were not enough candidate processors to satisfy the request.
481    ///
482    /// # Resource quota
483    ///
484    /// By default, the resource quota is not enforced. Call [`enforce_resource_quota()`][1]
485    /// to limit results to the quota. See the type-level documentation for more details on
486    /// resource quota handling best practices.
487    ///
488    /// [1]: ProcessorSetBuilder::enforce_resource_quota
489    #[must_use]
490    pub fn take(self, count: NonZero<usize>) -> Option<ProcessorSet> {
491        if let Some(max_count) = self.resource_quota_processor_count_limit()
492            && count.get() > max_count
493        {
494            // We cannot satisfy the request.
495            return None;
496        }
497
498        let candidates = self.candidates_by_memory_region();
499
500        if candidates.is_empty() {
501            // No candidates to choose from - everything was filtered out.
502            return None;
503        }
504
505        let processors = match self.memory_region_selector {
506            MemoryRegionSelector::Any => {
507                // We do not care about memory regions, so merge into one big happy family and
508                // pick a random `count` processors from it. As long as there is enough!
509                let all_processors = candidates
510                    .values()
511                    .flat_map(|x| x.iter().cloned())
512                    .collect::<Vec<_>>();
513
514                if all_processors.len() < count.get() {
515                    // Not enough processors to satisfy request.
516                    return None;
517                }
518
519                all_processors
520                    .choose_multiple(&mut rng(), count.get())
521                    .cloned()
522                    .collect_vec()
523            }
524            MemoryRegionSelector::PreferSame => {
525                // We will start decrementing it to zero.
526                let count = count.get();
527
528                // We shuffle the memory regions and sort them by size, so if there are memory
529                // regions with different numbers of candidates we will prefer the ones with more,
530                // although we will consider all memory regions with at least 'count' candidates
531                // as equal in sort order to avoid needlessly preferring giant memory regions.
532                let mut remaining_memory_regions = candidates.keys().copied().collect_vec();
533                remaining_memory_regions.shuffle(&mut rng());
534                remaining_memory_regions.sort_unstable_by_key(|x| {
535                    candidates
536                        .get(x)
537                        .expect("region must exist - we just got it from there")
538                        .len()
539                        // Clamp the length to `count` to treat all larger regions equally.
540                        .min(count)
541                });
542                // We want to start with the largest memory regions first.
543                remaining_memory_regions.reverse();
544
545                let mut remaining_memory_regions = VecDeque::from(remaining_memory_regions);
546
547                let mut processors: Vec<Processor> = Vec::with_capacity(count);
548
549                while processors.len() < count {
550                    let memory_region = remaining_memory_regions.pop_front()?;
551
552                    let processors_in_region = candidates.get(&memory_region).expect(
553                        "we picked an existing key from an existing HashSet - the values must exist",
554                    );
555
556                    // There might not be enough to fill the request, which is fine.
557                    let choose_count = count.min(processors_in_region.len());
558
559                    let region_processors = processors_in_region
560                        .choose_multiple(&mut rng(), choose_count)
561                        .cloned();
562
563                    processors.extend(region_processors);
564                }
565
566                processors
567            }
568            MemoryRegionSelector::RequireSame => {
569                // We filter out memory regions that do not have enough candidates and pick a
570                // random one from the remaining, then picking a random `count` processors.
571                let qualifying_memory_regions = candidates
572                    .iter()
573                    .filter_map(|(region, processors)| {
574                        if processors.len() < count.get() {
575                            return None;
576                        }
577
578                        Some(region)
579                    })
580                    .collect_vec();
581
582                let memory_region = qualifying_memory_regions.choose(&mut rng())?;
583
584                let processors = candidates.get(memory_region).expect(
585                    "we picked an existing key for an existing HashSet - the values must exist",
586                );
587
588                processors
589                    .choose_multiple(&mut rng(), count.get())
590                    .cloned()
591                    .collect_vec()
592            }
593            MemoryRegionSelector::PreferDifferent => {
594                // We iterate through the memory regions and prefer one from each, looping through
595                // memory regions that still have processors until we have as many as requested.
596
597                // We will start removing processors are memory regions that are used up.
598                let mut candidates = candidates;
599
600                let mut processors = Vec::with_capacity(count.get());
601
602                while processors.len() < count.get() {
603                    if candidates.is_empty() {
604                        // Not enough candidates remaining to satisfy request.
605                        return None;
606                    }
607
608                    for remaining_processors in candidates.values_mut() {
609                        let (index, processor) =
610                            remaining_processors.iter().enumerate().choose(&mut rng())?;
611
612                        let processor = processor.clone();
613
614                        remaining_processors.remove(index);
615
616                        processors.push(processor);
617
618                        if processors.len() == count.get() {
619                            break;
620                        }
621                    }
622
623                    // Remove any memory regions that have been depleted.
624                    candidates.retain(|_, remaining_processors| !remaining_processors.is_empty());
625                }
626
627                processors
628            }
629            MemoryRegionSelector::RequireDifferent => {
630                // We pick random `count` memory regions and a random processor from each.
631
632                if candidates.len() < count.get() {
633                    // Not enough memory regions to satisfy request.
634                    return None;
635                }
636
637                candidates
638                    .iter()
639                    .choose_multiple(&mut rng(), count.get())
640                    .into_iter()
641                    .map(|(_, processors)| {
642                        processors.iter().choose(&mut rng()).cloned().expect(
643                            "we are picking one item from a non-empty list - item must exist",
644                        )
645                    })
646                    .collect_vec()
647            }
648        };
649
650        Some(ProcessorSet::new(
651            NonEmpty::from_vec(processors)?,
652            self.hardware,
653        ))
654    }
655
656    /// Returns a processor set with all processors that match the configured criteria.
657    ///
658    /// If multiple alternative non-empty sets are a match, returns an arbitrary one of them.
659    /// For example, if specifying only a "same memory region" constraint, it will return all
660    /// the processors in an arbitrary memory region with at least one qualifying processor.
661    ///
662    /// Returns `None` if there were no matching processors to satisfy the request.
663    ///
664    /// # Example
665    ///
666    /// ```
667    /// use many_cpus::SystemHardware;
668    ///
669    /// // Try to get all efficiency processors, but exclude the first two
670    /// let all_processors = SystemHardware::current().processors();
671    /// let first_two: Vec<_> = all_processors.processors().iter().take(2).collect();
672    ///
673    /// let filtered_processors = SystemHardware::current()
674    ///     .processors()
675    ///     .to_builder()
676    ///     .efficiency_processors_only()
677    ///     .except(first_two)
678    ///     .take_all();
679    ///
680    /// match filtered_processors {
681    ///     Some(processors) => {
682    ///         println!(
683    ///             "Found {} efficiency processors (excluding first two)",
684    ///             processors.len()
685    ///         );
686    ///
687    ///         // Use remaining efficiency processors for background work
688    ///         let threads = processors.spawn_threads(|processor| {
689    ///             println!("Background worker on processor {}", processor.id());
690    ///             // Background processing here...
691    ///         });
692    /// # for thread in threads { thread.join().unwrap(); }
693    ///     }
694    ///     None => {
695    ///         // This can happen if all efficiency processors were excluded by the filter
696    ///         println!("No efficiency processors remaining after filtering");
697    ///     }
698    /// }
699    /// ```
700    ///
701    /// # Resource quota
702    ///
703    /// By default, the resource quota is not enforced. Call [`enforce_resource_quota()`][1]
704    /// to limit results to the quota. See the type-level documentation for more details on
705    /// resource quota handling best practices.
706    ///
707    /// [1]: ProcessorSetBuilder::enforce_resource_quota
708    #[must_use]
709    #[cfg_attr(test, mutants::skip)] // Hangs due to recursive access of OnceLock.
710    pub fn take_all(self) -> Option<ProcessorSet> {
711        let candidates = self.candidates_by_memory_region();
712
713        if candidates.is_empty() {
714            // No candidates to choose from - everything was filtered out.
715            return None;
716        }
717
718        let processors = match self.memory_region_selector {
719            MemoryRegionSelector::Any
720            | MemoryRegionSelector::PreferSame
721            | MemoryRegionSelector::PreferDifferent => {
722                // We return all processors in all memory regions because we have no strong
723                // filtering criterion we must follow - all are fine, so we return all.
724                candidates
725                    .values()
726                    .flat_map(|x| x.iter().cloned())
727                    .collect()
728            }
729            MemoryRegionSelector::RequireSame => {
730                // We return all processors in a random memory region.
731                // The candidate set only contains memory regions with at least 1 processor, so
732                // we know that all candidate memory regions are valid and we were not given a
733                // count, so even 1 processor is enough to satisfy the "all" criterion.
734                let memory_region = candidates
735                    .keys()
736                    .choose(&mut rng())
737                    .expect("we picked a random existing index - element must exist");
738
739                let processors = candidates.get(memory_region).expect(
740                    "we picked an existing key for an existing HashSet - the values must exist",
741                );
742
743                processors.clone()
744            }
745            MemoryRegionSelector::RequireDifferent => {
746                // We return a random processor from each memory region.
747                // The candidate set only contains memory regions with at least 1 processor, so
748                // we know that all candidate memory regions have enough to satisfy our needs.
749                let processors = candidates.values().map(|processors| {
750                    processors
751                        .choose(&mut rng())
752                        .cloned()
753                        .expect("we picked a random item from a non-empty list - item must exist")
754                });
755
756                processors.collect()
757            }
758        };
759
760        let processors = self.reduce_processors_until_under_quota(processors);
761
762        Some(ProcessorSet::new(
763            NonEmpty::from_vec(processors)?,
764            self.hardware,
765        ))
766    }
767
768    fn reduce_processors_until_under_quota(&self, processors: Vec<Processor>) -> Vec<Processor> {
769        let Some(max_count) = self.resource_quota_processor_count_limit() else {
770            return processors;
771        };
772
773        let mut processors = processors;
774
775        // If we picked too many, reduce until we are under quota.
776        while processors.len() > max_count {
777            processors
778                .pop()
779                .expect("guarded by len-check in loop condition");
780        }
781
782        processors
783    }
784
785    /// Takes the exact processors specified from the candidate set, returning a new
786    /// [`ProcessorSet`] containing only those processors.
787    ///
788    /// # Panics
789    ///
790    /// Panics if any of the specified processors are not present in the candidate set.
791    ///
792    /// # Example
793    ///
794    /// ```
795    /// use many_cpus::SystemHardware;
796    /// use nonempty::nonempty;
797    ///
798    /// let hw = SystemHardware::current();
799    /// let candidates = hw.processors();
800    ///
801    /// // Get the first processor from the set.
802    /// let first_processor = candidates.processors().first().clone();
803    ///
804    /// // Create a new set containing exactly that processor.
805    /// let single_processor_set = candidates
806    ///     .to_builder()
807    ///     .take_exact(nonempty![first_processor]);
808    ///
809    /// assert_eq!(single_processor_set.len(), 1);
810    /// ```
811    #[must_use]
812    pub fn take_exact(self, processors: NonEmpty<Processor>) -> ProcessorSet {
813        let candidates = self.candidates_by_memory_region();
814        let candidate_ids: HashSet<_> = candidates
815            .values()
816            .flat_map(|v| v.iter().map(Processor::id))
817            .collect();
818
819        for processor in &processors {
820            assert!(
821                candidate_ids.contains(&processor.id()),
822                "processor {} is not in the candidate set",
823                processor.id()
824            );
825        }
826
827        ProcessorSet::new(processors, self.hardware)
828    }
829
830    /// Executes the first stage filters to kick out processors purely based on their individual
831    /// characteristics. Whatever pass this filter are valid candidates for selection as long
832    /// as the next stage of filtering (the memory region logic) permits it.
833    ///
834    /// Returns candidates grouped by memory region, with each returned memory region having at
835    /// least one candidate processor.
836    fn candidates_by_memory_region(&self) -> HashMap<MemoryRegionId, Vec<Processor>> {
837        let candidates_iter = self
838            .candidate_processors()
839            .into_iter()
840            .filter_map(move |p| {
841                if self.except_indexes.contains(&p.id()) {
842                    return None;
843                }
844
845                let is_acceptable_type = match self.processor_type_selector {
846                    ProcessorTypeSelector::Any => true,
847                    ProcessorTypeSelector::Performance => {
848                        p.efficiency_class() == EfficiencyClass::Performance
849                    }
850                    ProcessorTypeSelector::Efficiency => {
851                        p.efficiency_class() == EfficiencyClass::Efficiency
852                    }
853                };
854
855                if !is_acceptable_type {
856                    return None;
857                }
858
859                Some((p.memory_region_id(), p))
860            });
861
862        let mut candidates = HashMap::new();
863        for (region, processor) in candidates_iter {
864            candidates
865                .entry(region)
866                .or_insert_with(Vec::new)
867                .push(processor);
868        }
869
870        candidates
871    }
872
873    /// Returns the processors to consider as candidates. If a source processor set was provided
874    /// (via `source_processors`), only those processors are returned. Otherwise, all
875    /// platform processors are returned.
876    fn candidate_processors(&self) -> Vec<Processor> {
877        let all = self.all_processors();
878
879        match &self.source_processor_ids {
880            Some(source_ids) => all
881                .into_iter()
882                .filter(|p| source_ids.contains(&p.id()))
883                .collect(),
884            None => all.into_iter().collect(),
885        }
886    }
887
888    fn all_processors(&self) -> NonEmpty<Processor> {
889        // Cheap conversion, reasonable to do it inline since we do not expect
890        // processor set logic to be on the hot path anyway.
891        self.hardware
892            .platform()
893            .get_all_processors()
894            .map(Processor::new)
895    }
896
897    fn resource_quota_processor_count_limit(&self) -> Option<usize> {
898        if self.obey_resource_quota {
899            let max_processor_time = self.hardware.platform().max_processor_time();
900
901            // We round down the quota to get a whole number of processors.
902            // We specifically round down because our goal with the resource quota is to never
903            // exceed it, even by a fraction, as that would cause quality of service degradation.
904            #[expect(clippy::cast_sign_loss, reason = "quota cannot be negative")]
905            #[expect(
906                clippy::cast_possible_truncation,
907                reason = "we are correctly rounding to avoid the problem"
908            )]
909            let max_processor_count = max_processor_time.floor() as usize;
910
911            // We can never restrict to less than 1 processor by quota because that would be
912            // nonsense - there is always some available processor time, so at least one
913            // processor must be usable. Therefore, we round below 1, and round down above 1.
914            Some(max_processor_count.max(1))
915        } else {
916            None
917        }
918    }
919}
920
921#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
922enum MemoryRegionSelector {
923    /// The default - memory regions are not considered in processor selection.
924    #[default]
925    Any,
926
927    /// Processors are all from the same memory region. If there are not enough processors in any
928    /// memory region, the request will fail.
929    RequireSame,
930
931    /// Processors are all from the different memory regions. If there are not enough memory
932    /// regions, the request will fail.
933    RequireDifferent,
934
935    /// Processors are ideally all from the same memory region. If there are not enough processors
936    /// in a single memory region, more memory regions will be added to the candidate set as needed,
937    /// but still keeping it to as few as possible.
938    PreferSame,
939
940    /// Processors are ideally all from the different memory regions. If there are not enough memory
941    /// regions, multiple processors from the same memory region will be returned, but still keeping
942    /// to as many different memory regions as possible to spread the processors out.
943    PreferDifferent,
944}
945
946#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
947enum ProcessorTypeSelector {
948    /// The default - all processors are valid candidates.
949    #[default]
950    Any,
951
952    /// Only performance processors (faster, energy-hungry) are valid candidates.
953    ///
954    /// Every system is guaranteed to have at least one performance processor.
955    /// If all processors are of the same performance class,
956    /// they are all considered to be performance processors.
957    Performance,
958
959    /// Only efficiency processors (slower, energy-efficient) are valid candidates.
960    ///
961    /// There is no guarantee that any efficiency processors are present on the system.
962    Efficiency,
963}
964
965#[cfg(not(miri))] // Miri cannot call platform APIs.
966#[cfg(test)]
967#[cfg_attr(coverage_nightly, coverage(off))]
968mod tests_real {
969    use new_zealand::nz;
970
971    use crate::SystemHardware;
972
973    #[test]
974    fn spawn_on_any_processor() {
975        let set = SystemHardware::current().processors();
976        let result = set.spawn_thread(move |_| 1234).join().unwrap();
977
978        assert_eq!(result, 1234);
979    }
980
981    #[test]
982    fn spawn_on_every_processor() {
983        let set = SystemHardware::current().processors();
984        let processor_count = set.len();
985
986        let join_handles = set.spawn_threads(move |_| 4567);
987
988        assert_eq!(join_handles.len(), processor_count);
989
990        for handle in join_handles {
991            let result = handle.join().unwrap();
992            assert_eq!(result, 4567);
993        }
994    }
995
996    #[test]
997    fn filter_by_memory_region() {
998        let hw = SystemHardware::current();
999
1000        // We know there is at least one memory region, so these must succeed.
1001        hw.processors()
1002            .to_builder()
1003            .same_memory_region()
1004            .take_all()
1005            .unwrap();
1006        hw.processors()
1007            .to_builder()
1008            .same_memory_region()
1009            .take(nz!(1))
1010            .unwrap();
1011        hw.processors()
1012            .to_builder()
1013            .different_memory_regions()
1014            .take_all()
1015            .unwrap();
1016        hw.processors()
1017            .to_builder()
1018            .different_memory_regions()
1019            .take(nz!(1))
1020            .unwrap();
1021    }
1022
1023    #[test]
1024    fn filter_by_efficiency_class() {
1025        let hw = SystemHardware::current();
1026
1027        // There must be at least one.
1028        hw.processors()
1029            .to_builder()
1030            .performance_processors_only()
1031            .take_all()
1032            .unwrap();
1033        hw.processors()
1034            .to_builder()
1035            .performance_processors_only()
1036            .take(nz!(1))
1037            .unwrap();
1038
1039        // There might not be any. We just try resolving it and ignore the result.
1040        // As long as it does not panic, we are good.
1041        drop(
1042            hw.processors()
1043                .to_builder()
1044                .efficiency_processors_only()
1045                .take_all(),
1046        );
1047        drop(
1048            hw.processors()
1049                .to_builder()
1050                .efficiency_processors_only()
1051                .take(nz!(1)),
1052        );
1053    }
1054
1055    #[test]
1056    fn filter_in_all() {
1057        let hw = SystemHardware::current();
1058
1059        // Ensure we use a constant starting set, in case we are running tests under constraints.
1060        let starting_set = hw.processors();
1061
1062        // If we filter in all processors, we should get all of them.
1063        let processors = starting_set
1064            .to_builder()
1065            .filter(|_| true)
1066            .take_all()
1067            .unwrap();
1068        let processor_count = starting_set.len();
1069
1070        assert_eq!(processors.len(), processor_count);
1071    }
1072
1073    #[test]
1074    fn filter_out_all() {
1075        let hw = SystemHardware::current();
1076
1077        // If we filter out all processors, there should be nothing left.
1078        assert!(
1079            hw.processors()
1080                .to_builder()
1081                .filter(|_| false)
1082                .take_all()
1083                .is_none()
1084        );
1085    }
1086
1087    #[test]
1088    fn except_all() {
1089        let hw = SystemHardware::current();
1090
1091        // Ensure we use a constant starting set, in case we are running tests under constraints.
1092        let starting_set = hw.processors();
1093
1094        // If we exclude all processors, there should be nothing left.
1095        assert!(
1096            starting_set
1097                .to_builder()
1098                .except(starting_set.processors().iter())
1099                .take_all()
1100                .is_none()
1101        );
1102    }
1103}
1104
1105#[cfg(test)]
1106#[cfg_attr(coverage_nightly, coverage(off))]
1107mod tests {
1108    use std::panic::{RefUnwindSafe, UnwindSafe};
1109
1110    use new_zealand::nz;
1111    use nonempty::nonempty;
1112    use static_assertions::assert_impl_all;
1113
1114    use super::*;
1115    use crate::fake::{HardwareBuilder, ProcessorBuilder};
1116
1117    assert_impl_all!(ProcessorSetBuilder: UnwindSafe, RefUnwindSafe);
1118
1119    /// Helper to build a `ProcessorBuilder` with the given properties.
1120    fn proc(id: u32, memory_region: u32, efficiency_class: EfficiencyClass) -> ProcessorBuilder {
1121        ProcessorBuilder::new()
1122            .id(id)
1123            .memory_region(memory_region)
1124            .efficiency_class(efficiency_class)
1125    }
1126
1127    #[test]
1128    fn smoke_test() {
1129        let hw = SystemHardware::fake(
1130            HardwareBuilder::new()
1131                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1132                .processor(proc(1, 0, EfficiencyClass::Performance)),
1133        );
1134
1135        let set = hw.processors().to_builder().take_all().unwrap();
1136        assert_eq!(set.len(), 2);
1137    }
1138
1139    #[test]
1140    fn efficiency_class_filter_take() {
1141        let hw = SystemHardware::fake(
1142            HardwareBuilder::new()
1143                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1144                .processor(proc(1, 0, EfficiencyClass::Performance)),
1145        );
1146
1147        let set = hw
1148            .processors()
1149            .to_builder()
1150            .efficiency_processors_only()
1151            .take_all()
1152            .unwrap();
1153        assert_eq!(set.len(), 1);
1154        assert_eq!(set.processors().first().id(), 0);
1155    }
1156
1157    #[test]
1158    fn efficiency_class_filter_take_all() {
1159        let hw = SystemHardware::fake(
1160            HardwareBuilder::new()
1161                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1162                .processor(proc(1, 0, EfficiencyClass::Performance)),
1163        );
1164
1165        let set = hw
1166            .processors()
1167            .to_builder()
1168            .efficiency_processors_only()
1169            .take_all()
1170            .unwrap();
1171        assert_eq!(set.len(), 1);
1172        assert_eq!(set.processors().first().id(), 0);
1173    }
1174
1175    #[test]
1176    fn take_n_processors() {
1177        let hw = SystemHardware::fake(
1178            HardwareBuilder::new()
1179                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1180                .processor(proc(1, 0, EfficiencyClass::Performance))
1181                .processor(proc(2, 0, EfficiencyClass::Efficiency)),
1182        );
1183
1184        let set = hw.processors().to_builder().take(nz!(2)).unwrap();
1185        assert_eq!(set.len(), 2);
1186    }
1187
1188    #[test]
1189    fn take_n_not_enough_processors() {
1190        // Configure only 2 processors with low quota so the request for 3 fails.
1191        let hw = SystemHardware::fake(
1192            HardwareBuilder::new()
1193                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1194                .processor(proc(1, 0, EfficiencyClass::Performance))
1195                .max_processor_time(2.0),
1196        );
1197
1198        let set = hw.processors().to_builder().take(nz!(3));
1199        assert!(set.is_none());
1200    }
1201
1202    #[test]
1203    fn take_n_not_enough_processor_time_quota() {
1204        // Configure quota of only 1.0 processor time.
1205        let hw = SystemHardware::fake(
1206            HardwareBuilder::new()
1207                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1208                .processor(proc(1, 0, EfficiencyClass::Performance))
1209                .max_processor_time(1.0),
1210        );
1211
1212        let set = hw
1213            .all_processors()
1214            .to_builder()
1215            .enforce_resource_quota()
1216            .take(nz!(2));
1217        assert!(set.is_none());
1218    }
1219
1220    #[test]
1221    fn take_n_quota_floors_to_limit() {
1222        // Configure quota of 1.5 processor time. The floor of 1.5 is 1, so requesting 2
1223        // processors should fail because the quota limit is less than the requested count.
1224        let hw = SystemHardware::fake(
1225            HardwareBuilder::new()
1226                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1227                .processor(proc(1, 0, EfficiencyClass::Performance))
1228                .max_processor_time(1.5),
1229        );
1230
1231        let set = hw
1232            .all_processors()
1233            .to_builder()
1234            .enforce_resource_quota()
1235            .take(nz!(2));
1236        assert!(set.is_none());
1237    }
1238
1239    #[test]
1240    fn take_n_exactly_at_quota_limit() {
1241        // Configure quota of exactly 2.0 processor time and request exactly 2 processors.
1242        // This tests the boundary condition where count == max_count (should succeed).
1243        let hw = SystemHardware::fake(
1244            HardwareBuilder::new()
1245                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1246                .processor(proc(1, 0, EfficiencyClass::Performance))
1247                .max_processor_time(2.0),
1248        );
1249
1250        let set = hw
1251            .all_processors()
1252            .to_builder()
1253            .enforce_resource_quota()
1254            .take(nz!(2));
1255        assert!(set.is_some());
1256        assert_eq!(set.unwrap().len(), 2);
1257    }
1258
1259    #[test]
1260    fn take_n_under_quota_succeeds() {
1261        // Configure quota of 3.0 processor time and request only 2 processors.
1262        // This tests that the quota branch passes through when count < max_count.
1263        let hw = SystemHardware::fake(
1264            HardwareBuilder::new()
1265                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1266                .processor(proc(1, 0, EfficiencyClass::Performance))
1267                .processor(proc(2, 0, EfficiencyClass::Efficiency))
1268                .max_processor_time(3.0),
1269        );
1270
1271        let set = hw
1272            .all_processors()
1273            .to_builder()
1274            .enforce_resource_quota()
1275            .take(nz!(2));
1276        assert!(set.is_some());
1277        assert_eq!(set.unwrap().len(), 2);
1278    }
1279
1280    #[test]
1281    fn take_n_quota_not_enforced_by_default() {
1282        // Configure quota of only 1.0 processor time. Since quota is not enforced by default,
1283        // we should still be able to get all processors via all_processors().to_builder().
1284        let hw = SystemHardware::fake(
1285            HardwareBuilder::new()
1286                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1287                .processor(proc(1, 0, EfficiencyClass::Performance))
1288                .max_processor_time(1.0),
1289        );
1290
1291        // Verify that hw.processors() respects the quota limit.
1292        let limited_set = hw.processors();
1293        assert_eq!(limited_set.len(), 1);
1294
1295        // Verify that all_processors().to_builder() does NOT enforce quota by default.
1296        let set = hw.all_processors().to_builder().take(nz!(2));
1297        assert_eq!(set.unwrap().len(), 2);
1298    }
1299
1300    #[test]
1301    fn take_n_quota_limit_min_1() {
1302        // Configure quota of only 0.001 processor time, which should round UP to 1.
1303        let hw = SystemHardware::fake(
1304            HardwareBuilder::new()
1305                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1306                .processor(proc(1, 0, EfficiencyClass::Performance))
1307                .max_processor_time(0.001),
1308        );
1309
1310        let set = hw
1311            .all_processors()
1312            .to_builder()
1313            .enforce_resource_quota()
1314            .take(nz!(1));
1315        assert_eq!(set.unwrap().len(), 1);
1316    }
1317
1318    #[test]
1319    fn take_all_rounds_down_quota() {
1320        // Configure quota of 1.999 which should round DOWN to 1.
1321        let hw = SystemHardware::fake(
1322            HardwareBuilder::new()
1323                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1324                .processor(proc(1, 0, EfficiencyClass::Performance))
1325                .max_processor_time(1.999),
1326        );
1327
1328        let set = hw
1329            .all_processors()
1330            .to_builder()
1331            .enforce_resource_quota()
1332            .take_all()
1333            .unwrap();
1334        assert_eq!(set.len(), 1);
1335    }
1336
1337    #[test]
1338    fn take_all_min_1_despite_quota() {
1339        // Configure quota of 0.001 which should round UP to 1 (never zero).
1340        let hw = SystemHardware::fake(
1341            HardwareBuilder::new()
1342                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1343                .processor(proc(1, 0, EfficiencyClass::Performance))
1344                .max_processor_time(0.001),
1345        );
1346
1347        let set = hw
1348            .all_processors()
1349            .to_builder()
1350            .enforce_resource_quota()
1351            .take_all()
1352            .unwrap();
1353        assert_eq!(set.len(), 1);
1354    }
1355
1356    #[test]
1357    fn take_all_not_enough_processors() {
1358        // All processors are efficiency class, so performance filter should fail.
1359        let hw = SystemHardware::fake(HardwareBuilder::new().processor(proc(
1360            0,
1361            0,
1362            EfficiencyClass::Efficiency,
1363        )));
1364
1365        let set = hw
1366            .processors()
1367            .to_builder()
1368            .performance_processors_only()
1369            .take_all();
1370        assert!(set.is_none());
1371    }
1372
1373    #[test]
1374    fn except_filter_take() {
1375        let hw = SystemHardware::fake(
1376            HardwareBuilder::new()
1377                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1378                .processor(proc(1, 0, EfficiencyClass::Performance)),
1379        );
1380
1381        let builder = hw.processors().to_builder();
1382
1383        let except_set = builder.clone().filter(|p| p.id() == 0).take_all().unwrap();
1384        assert_eq!(except_set.len(), 1);
1385
1386        let set = builder.except(except_set.processors()).take_all().unwrap();
1387        assert_eq!(set.len(), 1);
1388        assert_eq!(set.processors().first().id(), 1);
1389    }
1390
1391    #[test]
1392    fn except_filter_take_all() {
1393        let hw = SystemHardware::fake(
1394            HardwareBuilder::new()
1395                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1396                .processor(proc(1, 0, EfficiencyClass::Performance)),
1397        );
1398
1399        let builder = hw.processors().to_builder();
1400        let except_set = builder.clone().filter(|p| p.id() == 0).take_all().unwrap();
1401        assert_eq!(except_set.len(), 1);
1402
1403        let set = builder.except(except_set.processors()).take_all().unwrap();
1404        assert_eq!(set.len(), 1);
1405        assert_eq!(set.processors().first().id(), 1);
1406    }
1407
1408    #[test]
1409    fn custom_filter_take() {
1410        let hw = SystemHardware::fake(
1411            HardwareBuilder::new()
1412                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1413                .processor(proc(1, 0, EfficiencyClass::Performance)),
1414        );
1415
1416        let set = hw
1417            .processors()
1418            .to_builder()
1419            .filter(|p| p.id() == 1)
1420            .take_all()
1421            .unwrap();
1422        assert_eq!(set.len(), 1);
1423        assert_eq!(set.processors().first().id(), 1);
1424    }
1425
1426    #[test]
1427    fn custom_filter_take_all() {
1428        let hw = SystemHardware::fake(
1429            HardwareBuilder::new()
1430                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1431                .processor(proc(1, 0, EfficiencyClass::Performance)),
1432        );
1433
1434        let set = hw
1435            .processors()
1436            .to_builder()
1437            .filter(|p| p.id() == 1)
1438            .take_all()
1439            .unwrap();
1440        assert_eq!(set.len(), 1);
1441        assert_eq!(set.processors().first().id(), 1);
1442    }
1443
1444    #[test]
1445    fn same_memory_region_filter_take() {
1446        let hw = SystemHardware::fake(
1447            HardwareBuilder::new()
1448                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1449                .processor(proc(1, 1, EfficiencyClass::Performance)),
1450        );
1451
1452        let set = hw
1453            .processors()
1454            .to_builder()
1455            .same_memory_region()
1456            .take_all()
1457            .unwrap();
1458        assert_eq!(set.len(), 1);
1459    }
1460
1461    #[test]
1462    fn same_memory_region_filter_take_all() {
1463        let hw = SystemHardware::fake(
1464            HardwareBuilder::new()
1465                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1466                .processor(proc(1, 1, EfficiencyClass::Performance)),
1467        );
1468
1469        let set = hw
1470            .processors()
1471            .to_builder()
1472            .same_memory_region()
1473            .take_all()
1474            .unwrap();
1475        assert_eq!(set.len(), 1);
1476    }
1477
1478    #[test]
1479    fn different_memory_region_filter_take() {
1480        let hw = SystemHardware::fake(
1481            HardwareBuilder::new()
1482                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1483                .processor(proc(1, 0, EfficiencyClass::Efficiency))
1484                .processor(proc(2, 1, EfficiencyClass::Performance))
1485                .processor(proc(3, 1, EfficiencyClass::Performance)),
1486        );
1487
1488        let set = hw
1489            .processors()
1490            .to_builder()
1491            .different_memory_regions()
1492            .take_all()
1493            .unwrap();
1494        assert_eq!(set.len(), 2);
1495
1496        assert_ne!(
1497            set.processors().first().memory_region_id(),
1498            set.processors().last().memory_region_id()
1499        );
1500    }
1501
1502    #[test]
1503    fn different_memory_region_filter_take_all() {
1504        let hw = SystemHardware::fake(
1505            HardwareBuilder::new()
1506                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1507                .processor(proc(1, 0, EfficiencyClass::Efficiency))
1508                .processor(proc(2, 1, EfficiencyClass::Performance))
1509                .processor(proc(3, 1, EfficiencyClass::Performance)),
1510        );
1511
1512        let set = hw
1513            .processors()
1514            .to_builder()
1515            .different_memory_regions()
1516            .take_all()
1517            .unwrap();
1518        assert_eq!(set.len(), 2);
1519
1520        assert_ne!(
1521            set.processors().first().memory_region_id(),
1522            set.processors().last().memory_region_id()
1523        );
1524    }
1525
1526    #[test]
1527    fn filter_combinations() {
1528        let hw = SystemHardware::fake(
1529            HardwareBuilder::new()
1530                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1531                .processor(proc(1, 1, EfficiencyClass::Performance))
1532                .processor(proc(2, 1, EfficiencyClass::Efficiency)),
1533        );
1534
1535        let builder = hw.processors().to_builder();
1536        let except_set = builder.clone().filter(|p| p.id() == 0).take_all().unwrap();
1537        let set = builder
1538            .efficiency_processors_only()
1539            .except(except_set.processors())
1540            .different_memory_regions()
1541            .take_all()
1542            .unwrap();
1543        assert_eq!(set.len(), 1);
1544        assert_eq!(set.processors().first().id(), 2);
1545    }
1546
1547    #[test]
1548    fn same_memory_region_take_two_processors() {
1549        let hw = SystemHardware::fake(
1550            HardwareBuilder::new()
1551                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1552                .processor(proc(1, 1, EfficiencyClass::Performance))
1553                .processor(proc(2, 1, EfficiencyClass::Efficiency)),
1554        );
1555
1556        let set = hw
1557            .processors()
1558            .to_builder()
1559            .same_memory_region()
1560            .take(nz!(2))
1561            .unwrap();
1562        assert_eq!(set.len(), 2);
1563        assert!(set.processors().iter().any(|p| p.id() == 1));
1564        assert!(set.processors().iter().any(|p| p.id() == 2));
1565    }
1566
1567    #[test]
1568    fn different_memory_region_and_efficiency_class_filters() {
1569        let hw = SystemHardware::fake(
1570            HardwareBuilder::new()
1571                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1572                .processor(proc(1, 1, EfficiencyClass::Performance))
1573                .processor(proc(2, 2, EfficiencyClass::Efficiency))
1574                .processor(proc(3, 3, EfficiencyClass::Performance)),
1575        );
1576
1577        let set = hw
1578            .processors()
1579            .to_builder()
1580            .different_memory_regions()
1581            .efficiency_processors_only()
1582            .take_all()
1583            .unwrap();
1584        assert_eq!(set.len(), 2);
1585        assert!(set.processors().iter().any(|p| p.id() == 0));
1586        assert!(set.processors().iter().any(|p| p.id() == 2));
1587    }
1588
1589    #[test]
1590    fn performance_processors_but_all_efficiency() {
1591        let hw = SystemHardware::fake(HardwareBuilder::new().processor(proc(
1592            0,
1593            0,
1594            EfficiencyClass::Efficiency,
1595        )));
1596
1597        let set = hw
1598            .processors()
1599            .to_builder()
1600            .performance_processors_only()
1601            .take_all();
1602        assert!(set.is_none(), "No performance processors should be found.");
1603    }
1604
1605    #[test]
1606    fn require_different_single_region() {
1607        let hw = SystemHardware::fake(
1608            HardwareBuilder::new()
1609                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1610                .processor(proc(1, 0, EfficiencyClass::Efficiency)),
1611        );
1612
1613        let set = hw
1614            .processors()
1615            .to_builder()
1616            .different_memory_regions()
1617            .take(nz!(2));
1618        assert!(
1619            set.is_none(),
1620            "Should fail because there is not enough distinct memory regions."
1621        );
1622    }
1623
1624    #[test]
1625    fn prefer_different_memory_regions_take_all() {
1626        let hw = SystemHardware::fake(
1627            HardwareBuilder::new()
1628                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1629                .processor(proc(1, 1, EfficiencyClass::Performance))
1630                .processor(proc(2, 1, EfficiencyClass::Efficiency)),
1631        );
1632
1633        let set = hw
1634            .processors()
1635            .to_builder()
1636            .prefer_different_memory_regions()
1637            .take_all()
1638            .unwrap();
1639        assert_eq!(set.len(), 3);
1640    }
1641
1642    #[test]
1643    fn prefer_different_memory_regions_take_n() {
1644        let hw = SystemHardware::fake(
1645            HardwareBuilder::new()
1646                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1647                .processor(proc(1, 1, EfficiencyClass::Performance))
1648                .processor(proc(2, 1, EfficiencyClass::Efficiency))
1649                .processor(proc(3, 2, EfficiencyClass::Performance)),
1650        );
1651
1652        let set = hw
1653            .processors()
1654            .to_builder()
1655            .prefer_different_memory_regions()
1656            .take(nz!(2))
1657            .unwrap();
1658        assert_eq!(set.len(), 2);
1659        let regions: HashSet<_> = set
1660            .processors()
1661            .iter()
1662            .map(Processor::memory_region_id)
1663            .collect();
1664        assert_eq!(regions.len(), 2);
1665    }
1666
1667    #[test]
1668    fn prefer_same_memory_regions_take_n() {
1669        let hw = SystemHardware::fake(
1670            HardwareBuilder::new()
1671                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1672                .processor(proc(1, 1, EfficiencyClass::Performance))
1673                .processor(proc(2, 1, EfficiencyClass::Efficiency))
1674                .processor(proc(3, 2, EfficiencyClass::Performance)),
1675        );
1676
1677        let set = hw
1678            .processors()
1679            .to_builder()
1680            .prefer_same_memory_region()
1681            .take(nz!(2))
1682            .unwrap();
1683        assert_eq!(set.len(), 2);
1684        let regions: HashSet<_> = set
1685            .processors()
1686            .iter()
1687            .map(Processor::memory_region_id)
1688            .collect();
1689        assert_eq!(regions.len(), 1);
1690    }
1691
1692    #[test]
1693    fn prefer_different_memory_regions_take_n_not_enough() {
1694        let hw = SystemHardware::fake(
1695            HardwareBuilder::new()
1696                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1697                .processor(proc(1, 1, EfficiencyClass::Performance))
1698                .processor(proc(2, 1, EfficiencyClass::Efficiency))
1699                .processor(proc(3, 2, EfficiencyClass::Performance)),
1700        );
1701
1702        let set = hw
1703            .processors()
1704            .to_builder()
1705            .prefer_different_memory_regions()
1706            .take(nz!(4))
1707            .unwrap();
1708        assert_eq!(set.len(), 4);
1709    }
1710
1711    #[test]
1712    fn prefer_same_memory_regions_take_n_not_enough() {
1713        let hw = SystemHardware::fake(
1714            HardwareBuilder::new()
1715                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1716                .processor(proc(1, 1, EfficiencyClass::Performance))
1717                .processor(proc(2, 1, EfficiencyClass::Efficiency))
1718                .processor(proc(3, 2, EfficiencyClass::Performance)),
1719        );
1720
1721        let set = hw
1722            .processors()
1723            .to_builder()
1724            .prefer_same_memory_region()
1725            .take(nz!(3))
1726            .unwrap();
1727        assert_eq!(set.len(), 3);
1728        let regions: HashSet<_> = set
1729            .processors()
1730            .iter()
1731            .map(Processor::memory_region_id)
1732            .collect();
1733        assert_eq!(
1734            2,
1735            regions.len(),
1736            "should have picked to minimize memory regions (biggest first)"
1737        );
1738    }
1739
1740    #[test]
1741    fn prefer_same_memory_regions_take_n_picks_best_fit() {
1742        let hw = SystemHardware::fake(
1743            HardwareBuilder::new()
1744                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1745                .processor(proc(1, 1, EfficiencyClass::Performance))
1746                .processor(proc(2, 1, EfficiencyClass::Efficiency))
1747                .processor(proc(3, 2, EfficiencyClass::Performance)),
1748        );
1749
1750        let set = hw
1751            .processors()
1752            .to_builder()
1753            .prefer_same_memory_region()
1754            .take(nz!(2))
1755            .unwrap();
1756        assert_eq!(set.len(), 2);
1757        let regions: HashSet<_> = set
1758            .processors()
1759            .iter()
1760            .map(Processor::memory_region_id)
1761            .collect();
1762        assert_eq!(
1763            1,
1764            regions.len(),
1765            "should have picked from memory region 1 which can accommodate the preference"
1766        );
1767    }
1768
1769    #[test]
1770    fn take_any_returns_none_when_not_enough_processors() {
1771        // This tests the MemoryRegionSelector::Any branch returning None when there
1772        // are not enough processors in the candidate set to satisfy the request.
1773        let hw = SystemHardware::fake(
1774            HardwareBuilder::new()
1775                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1776                .processor(proc(1, 1, EfficiencyClass::Performance))
1777                .max_processor_time(10.0),
1778        );
1779
1780        // Request more processors than exist (3 > 2). Quota is high so the quota check passes,
1781        // but there are not enough candidates in the Any branch.
1782        let set = hw.processors().to_builder().take(nz!(3));
1783        assert!(
1784            set.is_none(),
1785            "should return None when not enough processors available"
1786        );
1787    }
1788
1789    #[test]
1790    fn take_prefer_different_returns_none_when_candidates_exhausted() {
1791        // This tests the MemoryRegionSelector::PreferDifferent branch returning None
1792        // when the round-robin through memory regions exhausts all candidates.
1793        // Configuration: 2 memory regions with 1 processor each. Request 3 processors.
1794        // The round-robin will pick 1 from each region (total 2), then have no more
1795        // candidates to pick from, so it returns None.
1796        let hw = SystemHardware::fake(
1797            HardwareBuilder::new()
1798                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1799                .processor(proc(1, 1, EfficiencyClass::Performance))
1800                .max_processor_time(10.0),
1801        );
1802
1803        // Request 3 processors with prefer_different_memory_regions.
1804        // We only have 2 total, so after 2 rounds the candidates will be exhausted.
1805        let set = hw
1806            .processors()
1807            .to_builder()
1808            .prefer_different_memory_regions()
1809            .take(nz!(3));
1810        assert!(
1811            set.is_none(),
1812            "should return None when candidates exhausted in PreferDifferent mode"
1813        );
1814    }
1815
1816    #[test]
1817    fn take_exact_returns_set_with_specified_processors() {
1818        let hw = SystemHardware::fake(
1819            HardwareBuilder::new()
1820                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1821                .processor(proc(1, 1, EfficiencyClass::Performance))
1822                .processor(proc(2, 1, EfficiencyClass::Efficiency))
1823                .max_processor_time(10.0),
1824        );
1825
1826        let candidates = hw.all_processors();
1827        let p1 = candidates.processors().first().clone();
1828
1829        let set = candidates.to_builder().take_exact(nonempty![p1.clone()]);
1830        assert_eq!(set.len(), 1);
1831        assert_eq!(set.processors().first().id(), p1.id());
1832    }
1833
1834    #[test]
1835    fn take_exact_with_multiple_processors() {
1836        let hw = SystemHardware::fake(
1837            HardwareBuilder::new()
1838                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1839                .processor(proc(1, 1, EfficiencyClass::Performance))
1840                .processor(proc(2, 1, EfficiencyClass::Efficiency))
1841                .max_processor_time(10.0),
1842        );
1843
1844        let candidates = hw.all_processors();
1845        let p0 = candidates
1846            .processors()
1847            .iter()
1848            .find(|p| p.id() == 0)
1849            .cloned()
1850            .unwrap();
1851        let p2 = candidates
1852            .processors()
1853            .iter()
1854            .find(|p| p.id() == 2)
1855            .cloned()
1856            .unwrap();
1857
1858        let set = candidates.to_builder().take_exact(nonempty![p0, p2]);
1859        assert_eq!(set.len(), 2);
1860        assert!(set.processors().iter().any(|p| p.id() == 0));
1861        assert!(set.processors().iter().any(|p| p.id() == 2));
1862    }
1863
1864    #[test]
1865    #[should_panic]
1866    fn take_exact_panics_if_processor_not_in_candidates() {
1867        let hw = SystemHardware::fake(
1868            HardwareBuilder::new()
1869                .processor(proc(0, 0, EfficiencyClass::Efficiency))
1870                .processor(proc(1, 0, EfficiencyClass::Performance))
1871                .max_processor_time(10.0),
1872        );
1873
1874        let candidates = hw.all_processors();
1875
1876        // Explicitly find the processor with ID 0 (not relying on ordering).
1877        let p0 = candidates
1878            .processors()
1879            .iter()
1880            .find(|p| p.id() == 0)
1881            .cloned()
1882            .unwrap();
1883
1884        // Filter out processor 0, then try to take_exact with it - this should panic.
1885        drop(
1886            candidates
1887                .to_builder()
1888                .filter(|p| p.id() != 0)
1889                .take_exact(nonempty![p0]),
1890        );
1891    }
1892}
1893
1894/// Fallback PAL integration tests - these test the integration between `ProcessorSetBuilder`
1895/// and the fallback platform abstraction layer.
1896#[cfg(all(test, not(miri)))] // Miri cannot call platform APIs.
1897#[cfg_attr(coverage_nightly, coverage(off))]
1898mod tests_fallback {
1899    use std::num::NonZero;
1900
1901    use new_zealand::nz;
1902
1903    use crate::SystemHardware;
1904    use crate::pal::fallback::BUILD_TARGET_PLATFORM;
1905
1906    #[test]
1907    fn builder_smoke_test() {
1908        let hw = SystemHardware::fallback();
1909        let builder = hw.processors().to_builder();
1910
1911        let set = builder.take_all().unwrap();
1912
1913        assert!(set.len() >= 1);
1914    }
1915
1916    #[test]
1917    fn take_respects_limit() {
1918        let hw = SystemHardware::fallback();
1919        let builder = hw.processors().to_builder();
1920
1921        let set = builder.take(nz!(1));
1922
1923        assert!(set.is_some());
1924        assert_eq!(set.unwrap().len(), 1);
1925    }
1926
1927    #[test]
1928    fn take_all_returns_all() {
1929        let hw = SystemHardware::fallback();
1930        let builder = hw.processors().to_builder();
1931
1932        let set = builder.take_all().unwrap();
1933
1934        let expected_count = std::thread::available_parallelism()
1935            .map(NonZero::get)
1936            .unwrap_or(1);
1937
1938        assert_eq!(set.len(), expected_count);
1939    }
1940
1941    #[test]
1942    fn performance_only_filter() {
1943        // All processors on the fallback platform are Performance class.
1944        let hw = SystemHardware::fallback();
1945        let builder = hw.processors().to_builder();
1946
1947        let set = builder.performance_processors_only().take_all().unwrap();
1948
1949        let expected_count = std::thread::available_parallelism()
1950            .map(NonZero::get)
1951            .unwrap_or(1);
1952
1953        assert_eq!(set.len(), expected_count);
1954    }
1955
1956    #[test]
1957    fn same_memory_region_filter() {
1958        // All processors on the fallback platform are in memory region 0.
1959        let hw = SystemHardware::fallback();
1960        let builder = hw.processors().to_builder();
1961
1962        let set = builder.same_memory_region().take_all().unwrap();
1963
1964        let expected_count = std::thread::available_parallelism()
1965            .map(NonZero::get)
1966            .unwrap_or(1);
1967
1968        assert_eq!(set.len(), expected_count);
1969
1970        for processor in set.processors() {
1971            assert_eq!(processor.memory_region_id(), 0);
1972        }
1973    }
1974
1975    #[test]
1976    fn except_filter() {
1977        let hw = SystemHardware::fallback();
1978        let builder = hw.processors().to_builder();
1979
1980        if BUILD_TARGET_PLATFORM.processor_count() > 1 {
1981            let except_set = builder.clone().filter(|p| p.id() == 0).take_all().unwrap();
1982            assert_eq!(except_set.len(), 1);
1983
1984            let set = builder.except(except_set.processors()).take_all().unwrap();
1985
1986            for processor in set.processors() {
1987                assert_ne!(processor.id(), 0);
1988            }
1989        }
1990    }
1991
1992    #[test]
1993    fn enforce_resource_quota() {
1994        let hw = SystemHardware::fallback();
1995        let builder = hw.all_processors().to_builder();
1996
1997        let set = builder.enforce_resource_quota().take_all().unwrap();
1998
1999        assert!(set.len() >= 1);
2000    }
2001
2002    #[test]
2003    fn where_available_for_current_thread() {
2004        use std::thread;
2005
2006        thread::spawn(|| {
2007            let hw = SystemHardware::fallback();
2008
2009            // First pin to one processor.
2010            let one = hw.processors().to_builder().take(nz!(1)).unwrap();
2011
2012            one.pin_current_thread_to();
2013
2014            // Now build a new set inheriting the affinity.
2015            let set = hw
2016                .processors()
2017                .to_builder()
2018                .where_available_for_current_thread()
2019                .take_all();
2020
2021            assert!(set.is_some());
2022            assert_eq!(set.unwrap().len(), 1);
2023        })
2024        .join()
2025        .unwrap();
2026    }
2027
2028    #[test]
2029    fn quota_not_enforced_by_default_on_builder() {
2030        let hw = SystemHardware::fallback();
2031        let builder = hw.all_processors().to_builder();
2032
2033        let set = builder.take_all().unwrap();
2034
2035        // The fallback platform reports max_processor_time == processor_count.
2036        // Since quota is NOT enforced by default, we should get all processors.
2037        let expected_count = std::thread::available_parallelism()
2038            .map(NonZero::get)
2039            .unwrap_or(1);
2040
2041        assert_eq!(set.len(), expected_count);
2042    }
2043}