pub mod builder;
#[cfg(feature = "hwloc-2_3_0")]
pub mod editor;
pub mod export;
pub mod support;
use self::{
builder::{BuildFlags, TopologyBuilder, TypeFilter},
support::FeatureSupport,
};
#[cfg(all(feature = "hwloc-2_3_0", doc))]
use crate::topology::support::MiscSupport;
use crate::{
bitmap::{Bitmap, BitmapRef, SpecializedBitmap},
cpu::cpuset::CpuSet,
errors::{self, ForeignObjectError, RawHwlocError},
ffi::transparent::AsNewtype,
memory::nodeset::NodeSet,
object::{
TopologyObject,
depth::{Depth, NormalDepth},
types::ObjectType,
},
};
use bitflags::bitflags;
use errno::Errno;
use hwlocality_sys::{
HWLOC_DISTRIB_FLAG_REVERSE, hwloc_bitmap_s, hwloc_distrib_flags_e, hwloc_topology,
hwloc_type_filter_e,
};
use libc::EINVAL;
#[allow(unused)]
#[cfg(test)]
use similar_asserts::assert_eq;
use std::{
collections::BTreeMap,
fmt::{self, Debug, Pointer},
ops::Deref,
ptr::{self, NonNull},
sync::OnceLock,
};
use strum::IntoEnumIterator;
use thiserror::Error;
#[cfg_attr(
feature = "hwloc-2_3_0",
doc = "- [Modifying a loaded topology](#modifying-a-loaded-topology) (hwloc 2.3+)"
)]
#[cfg_attr(
feature = "hwloc-2_3_0",
doc = "- [Comparing memory node attributes for finding where to allocate on](#comparing-memory-node-attributes-for-finding-where-to-allocate-on) (hwloc 2.3+)"
)]
#[cfg_attr(
feature = "hwloc-2_4_0",
doc = "- [Kinds of CPU cores](#kinds-of-cpu-cores) (hwloc 2.4+)"
)]
#[cfg_attr(
any(doc, target_os = "linux"),
doc = "- [Linux-specific helpers](#linux-specific-helpers)"
)]
#[cfg_attr(
any(doc, all(target_os = "windows", feature = "hwloc-2_5_0")),
doc = "- [Windows-specific helpers](#windows-specific-helpers) (hwloc 2.5+)"
)]
#[doc(alias = "hwloc_topology")]
#[doc(alias = "hwloc_topology_t")]
pub struct Topology(NonNull<hwloc_topology>);
impl Topology {
#[allow(clippy::missing_errors_doc)]
pub fn new() -> Result<Self, RawHwlocError> {
TopologyBuilder::new().build()
}
#[doc(hidden)]
pub fn test_instance() -> &'static Self {
static INSTANCE: OnceLock<Topology> = OnceLock::new();
INSTANCE.get_or_init(|| {
Self::builder()
.with_flags(BuildFlags::INCLUDE_DISALLOWED)
.expect("INCLUDE_DISALLOWED should always work")
.with_common_type_filter(TypeFilter::KeepAll)
.expect("KeepAll should be a supported common type filter")
.build()
.expect("Failed to initialize main test Topology")
})
}
#[cfg(feature = "hwloc-2_3_0")]
#[doc(hidden)]
pub fn foreign_instance() -> &'static Self {
static INSTANCE: OnceLock<Topology> = OnceLock::new();
INSTANCE.get_or_init(|| Self::test_instance().clone())
}
#[doc(alias = "hwloc_topology_init")]
pub fn builder() -> TopologyBuilder {
TopologyBuilder::new()
}
#[doc(alias = "hwloc_topology_abi_check")]
pub fn is_abi_compatible(&self) -> bool {
let result = errors::call_hwloc_zero_or_minus1("hwloc_topology_abi_check", || unsafe {
hwlocality_sys::hwloc_topology_abi_check(self.as_ptr())
});
match result {
Ok(()) => true,
#[cfg(not(tarpaulin_include))]
Err(RawHwlocError {
errno: Some(Errno(EINVAL)),
..
}) => false,
#[cfg(not(tarpaulin_include))]
#[cfg(windows)]
Err(RawHwlocError { errno: None, .. }) => {
false
}
Err(raw_err) => unreachable!("Unexpected hwloc error: {raw_err}"),
}
}
#[doc(alias = "hwloc_topology_get_flags")]
pub fn build_flags(&self) -> BuildFlags {
let result = BuildFlags::from_bits_retain(unsafe {
hwlocality_sys::hwloc_topology_get_flags(self.as_ptr())
});
assert!(result.is_valid(), "hwloc returned invalid flags");
result
}
#[doc(alias = "hwloc_topology_is_thissystem")]
pub fn is_this_system(&self) -> bool {
errors::call_hwloc_bool("hwloc_topology_is_thissystem", || unsafe {
hwlocality_sys::hwloc_topology_is_thissystem(self.as_ptr())
})
.expect("Should not involve faillible syscalls")
}
#[cfg_attr(
feature = "hwloc-2_3_0",
doc = "On hwloc 2.3+, [`BuildFlags::IMPORT_SUPPORT`] may be used during topology building to"
)]
#[cfg_attr(
feature = "hwloc-2_3_0",
doc = "report the supported features of the original remote machine instead. If"
)]
#[cfg_attr(
feature = "hwloc-2_3_0",
doc = "it was successfully imported, [`MiscSupport::imported()`] will be set."
)]
#[doc(alias = "hwloc_topology_get_support")]
pub fn feature_support(&self) -> &FeatureSupport {
let ptr = errors::call_hwloc_ptr("hwloc_topology_get_support", || unsafe {
hwlocality_sys::hwloc_topology_get_support(self.as_ptr())
})
.expect("Unexpected hwloc error");
unsafe { ptr.as_ref().as_newtype() }
}
pub fn supports<Group>(
&self,
get_group: fn(&FeatureSupport) -> Option<&Group>,
check_feature: fn(&Group) -> bool,
) -> bool {
get_group(self.feature_support()).is_some_and(check_feature)
}
#[allow(clippy::missing_errors_doc)]
#[doc(alias = "hwloc_topology_get_type_filter")]
pub fn type_filter(&self, ty: ObjectType) -> Result<TypeFilter, RawHwlocError> {
let mut filter = hwloc_type_filter_e::MAX;
errors::call_hwloc_zero_or_minus1("hwloc_topology_get_type_filter", || unsafe {
hwlocality_sys::hwloc_topology_get_type_filter(
self.as_ptr(),
ty.into(),
&raw mut filter,
)
})?;
Ok(unsafe { TypeFilter::from_hwloc(filter) })
}
}
impl Topology {
#[allow(clippy::missing_docs_in_private_items)]
#[doc(alias = "hwloc_distrib")]
pub fn distribute_items(
&self,
roots: &[&TopologyObject],
num_items: usize,
max_depth: NormalDepth,
flags: DistributeFlags,
) -> Result<Vec<CpuSet>, DistributeError> {
for root in roots.iter().copied() {
if !self.contains(root) {
return Err(DistributeError::ForeignRoot(root.into()));
}
}
if num_items == 0 {
return Ok(Vec::new());
}
fn recurse<'self_>(
roots_and_cpusets: impl DoubleEndedIterator<Item = ObjSetWeightDepth<'self_>> + Clone,
num_items: usize,
max_depth: NormalDepth,
flags: DistributeFlags,
result: &mut Vec<CpuSet>,
) {
#[cfg(not(tarpaulin_include))]
debug_assert_ne!(
roots_and_cpusets.clone().count(),
0,
"Can't distribute to 0 roots"
);
#[cfg(not(tarpaulin_include))]
debug_assert_ne!(
num_items, 0,
"Shouldn't try to distribute 0 items (just don't call this function)"
);
let initial_len = result.len();
let total_weight: usize = roots_and_cpusets
.clone()
.map(|(_, _, weight, _)| weight)
.sum();
let mut given_weight = 0;
let mut given_items = 0;
let process_root = |(root, cpuset, weight, depth): ObjSetWeightDepth<'_>| {
let next_given_weight = given_weight + weight;
let next_given_items = weight_to_items(next_given_weight, total_weight, num_items);
let my_items = next_given_items - given_items;
if root.normal_arity() > 0 && my_items > 1 && depth < max_depth {
recurse(
root.normal_children().filter_map(decode_normal_obj),
my_items,
max_depth,
flags,
result,
);
} else if my_items > 0 {
for _ in 0..my_items {
result.push(cpuset.clone_target());
}
} else {
let mut iter = result.iter_mut().rev();
let last = iter.next().expect("First chunk cannot be empty");
for other in iter {
if other == last {
*other |= cpuset;
}
}
*last |= cpuset;
}
given_weight = next_given_weight;
given_items = next_given_items;
};
if flags.contains(DistributeFlags::REVERSE) {
roots_and_cpusets.rev().for_each(process_root);
} else {
roots_and_cpusets.for_each(process_root);
}
#[cfg(not(tarpaulin_include))]
debug_assert_eq!(
result.len() - initial_len,
num_items,
"This function should distribute the requested number of items"
);
}
let decoded_roots = roots.iter().copied().filter_map(|root| {
let mut root_then_ancestors = std::iter::once(root)
.chain(root.ancestors())
.skip_while(|candidate| !candidate.object_type().is_normal());
root_then_ancestors.find_map(decode_normal_obj)
});
if decoded_roots.clone().count() == 0 {
return Err(DistributeError::EmptyRoots);
}
if sets_overlap(decoded_roots.clone().map(|(_, root_set, _, _)| root_set)) {
return Err(DistributeError::OverlappingRoots);
}
let mut result = Vec::with_capacity(num_items);
recurse(decoded_roots, num_items, max_depth, flags, &mut result);
#[cfg(not(tarpaulin_include))]
debug_assert_eq!(
result.len(),
num_items,
"This function should produce one result per input item"
);
Ok(result)
}
}
#[cfg(not(tarpaulin_include))]
bitflags! {
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
#[doc(alias = "hwloc_distrib_flags_e")]
pub struct DistributeFlags: hwloc_distrib_flags_e {
const REVERSE = HWLOC_DISTRIB_FLAG_REVERSE;
}
}
crate::impl_arbitrary_for_bitflags!(DistributeFlags, hwloc_distrib_flags_e);
impl Default for DistributeFlags {
fn default() -> Self {
Self::empty()
}
}
#[derive(Clone, Debug, Eq, Error, Hash, PartialEq)]
pub enum DistributeError {
#[error("no CPU is accessible from the distribution roots")]
EmptyRoots,
#[error("distribution root {0}")]
ForeignRoot(#[from] ForeignObjectError),
#[error("distribution roots overlap with each other")]
OverlappingRoots,
}
fn weight_to_items(given_weight: usize, total_weight: usize, num_items: usize) -> usize {
#[allow(clippy::missing_docs_in_private_items)]
const TOO_LARGE: &str = "Such large inputs are not supported yet";
let cast = |x: usize| u128::try_from(x).expect(TOO_LARGE);
let numerator = cast(given_weight)
.checked_mul(cast(num_items))
.expect(TOO_LARGE);
let denominator = cast(total_weight);
let my_items = numerator / denominator + u128::from(numerator % denominator != 0);
debug_assert!(
given_weight <= total_weight,
"Cannot distribute more weight than the active root's total weight"
);
my_items
.try_into()
.expect("Cannot happen if computation is correct")
}
fn decode_normal_obj(obj: &TopologyObject) -> Option<ObjSetWeightDepth<'_>> {
debug_assert!(
obj.object_type().is_normal(),
"This function only works on normal objects"
);
let cpuset = obj.cpuset().expect("Normal objects should have cpusets");
let weight = cpuset
.weight()
.expect("Topology objects should not have infinite cpusets");
let depth = obj.depth().expect_normal();
(weight > 0).then_some((obj, cpuset, weight, depth))
}
type ObjSetWeightDepth<'object> = (
&'object TopologyObject,
BitmapRef<'object, CpuSet>,
usize,
NormalDepth,
);
fn sets_overlap(mut sets: impl Iterator<Item = impl Deref<Target = CpuSet>>) -> bool {
sets.try_fold(CpuSet::new(), |mut acc, set| {
let set: &CpuSet = &set;
if acc.intersects(set) {
None
} else {
acc |= set;
Some(acc)
}
})
.is_none()
}
impl Topology {
#[doc(alias = "hwloc_topology_get_topology_cpuset")]
pub fn cpuset(&self) -> BitmapRef<'_, CpuSet> {
unsafe {
self.topology_set(
"hwloc_topology_get_topology_cpuset",
hwlocality_sys::hwloc_topology_get_topology_cpuset,
)
}
}
#[doc(alias = "hwloc_topology_get_complete_cpuset")]
pub fn complete_cpuset(&self) -> BitmapRef<'_, CpuSet> {
unsafe {
self.topology_set(
"hwloc_topology_get_complete_cpuset",
hwlocality_sys::hwloc_topology_get_complete_cpuset,
)
}
}
#[doc(alias = "hwloc_topology_get_allowed_cpuset")]
pub fn allowed_cpuset(&self) -> BitmapRef<'_, CpuSet> {
unsafe {
self.topology_set(
"hwloc_topology_get_allowed_cpuset",
hwlocality_sys::hwloc_topology_get_allowed_cpuset,
)
}
}
#[doc(alias = "hwloc_topology_get_topology_nodeset")]
pub fn nodeset(&self) -> BitmapRef<'_, NodeSet> {
unsafe {
self.topology_set(
"hwloc_topology_get_topology_nodeset",
hwlocality_sys::hwloc_topology_get_topology_nodeset,
)
}
}
#[doc(alias = "hwloc_topology_get_complete_nodeset")]
pub fn complete_nodeset(&self) -> BitmapRef<'_, NodeSet> {
unsafe {
self.topology_set(
"hwloc_topology_get_complete_nodeset",
hwlocality_sys::hwloc_topology_get_complete_nodeset,
)
}
}
#[doc(alias = "hwloc_topology_get_allowed_nodeset")]
pub fn allowed_nodeset(&self) -> BitmapRef<'_, NodeSet> {
unsafe {
self.topology_set(
"hwloc_topology_get_allowed_nodeset",
hwlocality_sys::hwloc_topology_get_allowed_nodeset,
)
}
}
unsafe fn topology_set<'topology, Set: SpecializedBitmap>(
&'topology self,
getter_name: &'static str,
getter: unsafe extern "C" fn(*const hwloc_topology) -> *const hwloc_bitmap_s,
) -> BitmapRef<'topology, Set> {
let bitmap_ref = unsafe {
let bitmap_ptr = errors::call_hwloc_ptr(getter_name, || getter(self.as_ptr()))
.expect("According to their docs, these functions cannot return NULL");
Bitmap::borrow_from_nonnull::<'topology>(bitmap_ptr)
};
bitmap_ref.cast()
}
}
impl Topology {
pub(crate) fn as_ptr(&self) -> *const hwloc_topology {
self.0.as_ptr()
}
pub(crate) fn as_mut_ptr(&mut self) -> *mut hwloc_topology {
self.0.as_ptr()
}
pub(crate) fn contains(&self, object: &TopologyObject) -> bool {
let expected_root = self.root_object();
let actual_root = std::iter::once(object)
.chain(object.ancestors())
.last()
.expect("By definition, this iterator always has >= 1 element");
ptr::eq(expected_root, actual_root)
}
}
#[cfg(feature = "hwloc-2_3_0")]
impl Clone for Topology {
#[doc(alias = "hwloc_topology_dup")]
fn clone(&self) -> Self {
let mut clone = ptr::null_mut();
errors::call_hwloc_zero_or_minus1("hwloc_topology_dup", || unsafe {
hwlocality_sys::hwloc_topology_dup(&raw mut clone, self.as_ptr())
})
.expect("Duplicating a topology only fail with ENOMEM, which is a panic in Rust");
let clone = NonNull::new(clone).expect("Got null pointer from hwloc_topology_dup");
let result = errors::call_hwloc_zero_or_minus1("hwloc_topology_refresh", || unsafe {
hwlocality_sys::hwloc_topology_refresh(clone.as_ptr())
});
#[cfg(not(tarpaulin_include))]
if let Err(e) = result {
unsafe { hwlocality_sys::hwloc_topology_destroy(clone.as_ptr()) }
panic!(
"ERROR: Failed to refresh topology clone ({e}), so it's stuck in a state that violates Rust aliasing rules..."
);
}
Self(clone)
}
}
impl Debug for Topology {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct("Topology");
debug
.field("is_abi_compatible", &self.is_abi_compatible())
.field("build_flags", &self.build_flags())
.field("is_this_system", &self.is_this_system())
.field("feature_support", self.feature_support());
let type_filters = ObjectType::iter()
.map(|ty| {
(
format!("{ty}"),
self.type_filter(ty)
.expect("should always succeed when called with a valid type"),
)
})
.collect::<BTreeMap<_, _>>();
debug.field("type_filter", &type_filters);
let objects_per_depth = NormalDepth::iter_range(NormalDepth::MIN, self.depth())
.map(Depth::from)
.chain(Depth::VIRTUAL_DEPTHS.iter().copied())
.filter_map(|depth| {
let objs = self.objects_at_depth(depth).collect::<Vec<_>>();
(!objs.is_empty()).then_some((format!("{depth}"), objs))
})
.collect::<Vec<_>>();
debug
.field("objects_per_depth", &objects_per_depth)
.field("memory_parents_depth", &self.memory_parents_depth());
debug
.field("cpuset", &self.cpuset())
.field("complete_cpuset", &self.complete_cpuset())
.field("allowed_cpuset", &self.allowed_cpuset())
.field("nodeset", &self.nodeset())
.field("complete_nodeset", &self.complete_nodeset())
.field("allowed_nodeset", &self.allowed_nodeset());
debug.field("distances", &self.distances(None));
#[cfg(feature = "hwloc-2_3_0")]
{
debug.field("memory_attributes", &self.dump_memory_attributes());
}
#[cfg(feature = "hwloc-2_4_0")]
{
let cpu_kinds = self.cpu_kinds().into_iter().flatten().collect::<Vec<_>>();
debug.field("cpu_kinds", &cpu_kinds);
}
debug.finish()
}
}
impl Drop for Topology {
#[doc(alias = "hwloc_topology_destroy")]
fn drop(&mut self) {
unsafe { hwlocality_sys::hwloc_topology_destroy(self.as_mut_ptr()) }
}
}
impl PartialEq for Topology {
fn eq(&self, other: &Self) -> bool {
macro_rules! check_all_equal {
($($property:ident),*) => {
if (
$(
Topology::$property(self) != Topology::$property(other)
)||*
) {
return false;
}
};
}
check_all_equal!(
is_abi_compatible,
build_flags,
is_this_system,
feature_support,
memory_parents_depth,
cpuset,
complete_cpuset,
allowed_cpuset,
nodeset,
complete_nodeset,
allowed_nodeset
);
fn type_filters(
topology: &Topology,
) -> impl Iterator<Item = Result<TypeFilter, RawHwlocError>> + '_ {
ObjectType::iter().map(|ty| topology.type_filter(ty))
}
if !type_filters(self).eq(type_filters(other)) {
return false;
}
if !self.has_same_object_hierarchy(other) {
return false;
}
let same_distances = match (self.distances(None), other.distances(None)) {
(Ok(distances1), Ok(distances2)) => {
distances1.len() == distances2.len()
&& distances1
.into_iter()
.zip(&distances2)
.all(|(d1, d2)| d1.eq_modulo_topology(d2))
}
(Err(e1), Err(e2)) => e1 == e2,
(Ok(_), Err(_)) | (Err(_), Ok(_)) => false,
};
if !same_distances {
return false;
}
#[cfg(feature = "hwloc-2_3_0")]
{
if !self
.dump_memory_attributes()
.eq_modulo_topology(&other.dump_memory_attributes())
{
return false;
}
}
#[cfg(feature = "hwloc-2_4_0")]
{
let same_kinds = match (self.cpu_kinds(), other.cpu_kinds()) {
(Ok(kinds1), Ok(kinds2)) => kinds1.eq(kinds2),
(Err(e1), Err(e2)) => e1 == e2,
(Ok(_), Err(_)) | (Err(_), Ok(_)) => false,
};
if !same_kinds {
return false;
}
}
true
}
}
impl Pointer for Topology {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
<NonNull<hwloc_topology> as Pointer>::fmt(&self.0, f)
}
}
unsafe impl Send for Topology {}
unsafe impl Sync for Topology {}
#[allow(clippy::too_many_lines)]
#[cfg(test)]
mod tests {
use super::*;
use crate::ffi::PositiveInt;
use bitflags::Flags;
use proptest::prelude::*;
#[allow(unused)]
use similar_asserts::assert_eq;
use static_assertions::{assert_impl_all, assert_not_impl_any};
use std::{
collections::BTreeSet,
error::Error,
fmt::{Binary, Display, LowerExp, LowerHex, Octal, UpperExp, UpperHex},
hash::Hash,
io::{self, Read},
num::NonZeroU8,
ops::{
BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not, Sub, SubAssign,
},
panic::UnwindSafe,
};
assert_impl_all!(DistributeFlags:
Binary, BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign,
Copy, Debug, Default, Extend<DistributeFlags>, Flags,
FromIterator<DistributeFlags>, Hash, IntoIterator<Item=DistributeFlags>,
LowerHex, Not, Octal, Sized, Sub, SubAssign, Sync, UpperHex, Unpin,
UnwindSafe
);
assert_not_impl_any!(DistributeFlags:
Display, Drop, PartialOrd, Pointer, LowerExp, Read, UpperExp,
fmt::Write, io::Write
);
assert_impl_all!(Topology:
Debug, Drop, PartialEq, Pointer, Sized, Sync, Unpin, UnwindSafe
);
#[cfg(feature = "hwloc-2_3_0")]
assert_impl_all!(Topology: Clone);
assert_not_impl_any!(Topology:
Binary, Copy, Default, Deref, Display, IntoIterator, LowerExp, LowerHex,
Octal, Read, UpperExp, UpperHex, fmt::Write, io::Write
);
assert_impl_all!(DistributeError:
Clone, Error, Hash, Sized, Sync, Unpin, UnwindSafe
);
assert_not_impl_any!(DistributeError:
Binary, Copy, Default, Deref, Drop, IntoIterator,
LowerExp, LowerHex, Octal, PartialOrd, Pointer, Read,
UpperExp, UpperHex, fmt::Write, io::Write
);
#[test]
fn default_distribute_flags() {
assert_eq!(DistributeFlags::default(), DistributeFlags::empty());
}
#[cfg(feature = "hwloc-2_3_0")]
#[test]
fn clone() -> Result<(), TestCaseError> {
use crate::topology::builder::tests::DataSource;
let topology = Topology::test_instance();
let clone = topology.clone();
assert_ne!(format!("{:p}", *topology), format!("{clone:p}"));
builder::tests::check_topology(
topology,
DataSource::ThisSystem,
topology.build_flags(),
|ty| Ok(topology.type_filter(ty).unwrap()),
)?;
assert!(!topology.contains(clone.root_object()));
assert_eq!(topology, &clone);
Ok(())
}
#[allow(clippy::print_stdout, clippy::use_debug)]
#[test]
fn debug_and_self_eq() {
let topology = Topology::test_instance();
assert_eq!(topology, topology);
println!("{topology:#?}");
}
fn max_depth() -> impl Strategy<Value = NormalDepth> {
prop_oneof![
4 => (0..usize::from(Topology::test_instance().depth()))
.prop_map(|us| NormalDepth::try_from(us).unwrap()),
1 => any::<NormalDepth>()
]
}
proptest! {
#[test]
fn distribute_nowhere(num_items: NonZeroU8, max_depth in max_depth(), flags: DistributeFlags) {
let num_items = usize::from(num_items.get());
prop_assert_eq!(
Topology::test_instance().distribute_items(&[], num_items, max_depth, flags),
Err(DistributeError::EmptyRoots)
);
}
}
fn disjoint_roots() -> impl Strategy<Value = Vec<&'static TopologyObject>> {
fn normal_weight(obj: &TopologyObject) -> usize {
obj.cpuset()
.expect("normal object should have a cpuset")
.weight()
.expect("normal object should have a finite cpuset")
}
fn pick_disjoint_objects(
root: &'static TopologyObject,
num_objects: usize,
) -> impl Strategy<Value = Vec<&'static TopologyObject>> {
assert!(
root.object_type().is_normal(),
"root object should be normal"
);
assert!(num_objects <= normal_weight(root));
match num_objects {
0 => Just(Vec::new()).boxed(),
1 => {
let topology = Topology::test_instance();
let subtree_objects = topology
.normal_objects()
.filter(|obj| obj.is_in_subtree(root) || ptr::eq(*obj, root))
.collect::<Vec<_>>();
prop::sample::select(subtree_objects)
.prop_map(|obj| vec![obj])
.boxed()
}
_ => {
let degenerate_children = root
.normal_children()
.flat_map(|child| std::iter::repeat_n(child, normal_weight(child)))
.collect::<Vec<_>>();
debug_assert_eq!(degenerate_children.len(), normal_weight(root));
prop::sample::subsequence(degenerate_children, num_objects)
.prop_flat_map(|selected_degenerate| {
let mut count_per_child =
Vec::<(&'static TopologyObject, usize)>::new();
'children: for child in selected_degenerate {
if let Some((last_child, last_count)) = count_per_child.last_mut() {
if ptr::eq(child, *last_child) {
*last_count += 1;
continue 'children;
}
}
count_per_child.push((child, 1));
}
let nested_objs = count_per_child
.into_iter()
.map(|(child, count)| pick_disjoint_objects(child, count))
.collect::<Vec<_>>();
nested_objs.prop_map(|nested_objs| {
nested_objs.into_iter().flatten().collect::<Vec<_>>()
})
})
.boxed()
}
}
}
let root = Topology::test_instance().root_object();
(1..=normal_weight(root))
.prop_flat_map(|num_objects| pick_disjoint_objects(root, num_objects))
.prop_shuffle()
}
proptest! {
#[test]
fn distribute_nothing(
disjoint_roots in disjoint_roots(),
max_depth in max_depth(),
flags: DistributeFlags,
) {
prop_assert_eq!(
Topology::test_instance().distribute_items(&disjoint_roots, 0, max_depth, flags),
Ok(Vec::new())
);
}
}
fn find_possible_leaves<'output>(
roots: &[&'output TopologyObject],
max_depth: NormalDepth,
) -> Vec<&'output TopologyObject> {
let mut input = roots.to_vec();
let mut output = Vec::new();
for _ in PositiveInt::iter_range(PositiveInt::MIN, max_depth) {
let mut new_leaves = false;
for obj in input.drain(..) {
if obj.normal_arity() > 0 && obj.depth().expect_normal() < max_depth {
output.extend(obj.normal_children());
new_leaves = true;
} else {
output.push(obj);
}
}
std::mem::swap(&mut input, &mut output);
if !new_leaves {
break;
}
}
input
}
proptest! {
#[test]
fn distribute_correct(
disjoint_roots in disjoint_roots(),
max_depth in max_depth(),
num_items: NonZeroU8,
flags: DistributeFlags,
) {
let topology = Topology::test_instance();
let num_items = usize::from(num_items.get());
#[allow(clippy::cast_precision_loss)]
{
let item_sets = topology
.distribute_items(&disjoint_roots, num_items, max_depth, flags)
.unwrap();
prop_assert_eq!(item_sets.len(), num_items);
let possible_leaf_objects = find_possible_leaves(&disjoint_roots, max_depth);
let mut possible_leaf_sets = possible_leaf_objects
.iter()
.map(|obj| obj.cpuset().unwrap().clone_target())
.collect::<BTreeSet<CpuSet>>();
let mut items_per_set = Vec::<(CpuSet, usize)>::new();
for item_set in item_sets {
match items_per_set.last_mut() {
Some((last_set, count)) if last_set == &item_set => *count += 1,
_ => items_per_set.push((item_set, 1)),
}
}
for item_set in items_per_set.iter().map(|(set, _count)| set) {
if !possible_leaf_sets.contains(item_set) {
let subsets = possible_leaf_objects
.iter()
.map(|obj| obj.cpuset().unwrap())
.skip_while(|&subset| !item_set.includes(subset))
.take_while(|&subset| item_set.includes(subset))
.map(|subset| subset.clone_target())
.collect::<Vec<_>>();
let merged_set = subsets.iter().fold(CpuSet::new(), |mut acc, subset| {
acc |= subset;
acc
});
prop_assert_eq!(
item_set,
&merged_set,
"Distributed set {} is not part of expected leaf sets {:?}",
item_set,
possible_leaf_sets
);
prop_assert_eq!(
items_per_set
.iter()
.filter(|(item_set, _count)| item_set.intersects(&merged_set))
.count(),
1,
"Merging should only occurs when 1 item is shared by N leaves"
);
for subset in subsets {
prop_assert!(possible_leaf_sets.remove(&subset));
}
prop_assert!(possible_leaf_sets.insert(merged_set));
}
}
prop_assert!(!sets_overlap(items_per_set.iter().map(|(set, _count)| set)));
let total_weight = items_per_set
.iter()
.map(|(leaf_set, _count)| leaf_set.weight().unwrap())
.sum::<usize>();
for (leaf_set, items_per_leaf) in &items_per_set {
let cpu_share = leaf_set.weight().unwrap() as f64 / total_weight as f64;
let ideal_share = num_items as f64 * cpu_share;
prop_assert!((*items_per_leaf as f64 - ideal_share).abs() <= 1.0);
}
let (first_set, items_per_leaf) = items_per_set.first().unwrap();
let first_set_intersects = |root: Option<&TopologyObject>| {
prop_assert!(first_set.intersects(root.unwrap().cpuset().unwrap()));
Ok(())
};
if flags.contains(DistributeFlags::REVERSE) {
first_set_intersects(disjoint_roots.last().copied())?;
} else {
first_set_intersects(disjoint_roots.first().copied())?;
}
let cpu_share = first_set.weight().unwrap() as f64 / total_weight as f64;
let ideal_share = num_items as f64 * cpu_share;
let bias = *items_per_leaf as f64 - ideal_share;
prop_assert!(
bias >= 0.0,
"Earlier roots should get favored for item allocation"
);
}
}
}
fn overlapping_roots() -> impl Strategy<Value = Vec<&'static TopologyObject>> {
disjoint_roots().prop_flat_map(|disjoint_roots| {
let topology = Topology::test_instance();
let full_set = disjoint_roots.iter().fold(CpuSet::new(), |mut acc, root| {
acc |= root.cpuset().unwrap();
acc
});
let overlapping_pu = (0..full_set.weight().unwrap()).prop_map(move |cpu_idx| {
topology
.smallest_object_covering_cpuset(&CpuSet::from(
full_set.iter_set().nth(cpu_idx).unwrap(),
))
.unwrap()
});
let overlapping_root = overlapping_pu.prop_map(|pu| {
std::iter::once(pu)
.chain(pu.ancestors())
.find(|root| root.cpuset().is_some())
.unwrap()
});
let num_roots = disjoint_roots.len();
(Just(disjoint_roots), overlapping_root, 0..num_roots).prop_map(
|(mut disjoint_roots, overlapping_root, bad_root_idx)| {
disjoint_roots.insert(bad_root_idx, overlapping_root);
disjoint_roots
},
)
})
}
proptest! {
#[test]
fn distribute_overlapping(
overlapping_roots in overlapping_roots(),
max_depth in max_depth(),
num_items: NonZeroU8,
flags: DistributeFlags,
) {
let num_items = usize::from(num_items.get());
prop_assert_eq!(
Topology::test_instance().distribute_items(&overlapping_roots, num_items, max_depth, flags),
Err(DistributeError::OverlappingRoots)
);
}
}
#[cfg(feature = "hwloc-2_3_0")]
fn foreign_normal_object() -> impl Strategy<Value = &'static TopologyObject> {
static FOREIGNERS: OnceLock<Box<[&'static TopologyObject]>> = OnceLock::new();
let objects =
FOREIGNERS.get_or_init(|| Topology::foreign_instance().normal_objects().collect());
prop::sample::select(&objects[..])
}
#[cfg(feature = "hwloc-2_3_0")]
fn foreign_roots_and_idx() -> impl Strategy<Value = (Vec<&'static TopologyObject>, usize)> {
(disjoint_roots(), foreign_normal_object()).prop_flat_map(
|(disjoint_roots, foreign_object)| {
let num_roots = disjoint_roots.len();
(Just((disjoint_roots, foreign_object)), 0..num_roots).prop_map(
|((mut disjoint_roots, foreign_object), bad_root_idx)| {
disjoint_roots.insert(bad_root_idx, foreign_object);
(disjoint_roots, bad_root_idx)
},
)
},
)
}
#[cfg(feature = "hwloc-2_3_0")]
proptest! {
#[test]
fn distribute_foreign(
(foreign_roots, foreign_idx) in foreign_roots_and_idx(),
max_depth in max_depth(),
num_items: NonZeroU8,
flags: DistributeFlags,
) {
let num_items = usize::from(num_items.get());
let foreign_object = foreign_roots[foreign_idx];
prop_assert_eq!(
Topology::test_instance().distribute_items(&foreign_roots, num_items, max_depth, flags),
Err(DistributeError::ForeignRoot(foreign_object.into()))
);
}
}
}