many_cpus/
processor_set_builder.rs

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