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}