#[cfg(doc)]
use crate::topology::support::DiscoverySupport;
use crate::{
cpu::cpuset::CpuSet,
errors::{self},
ffi::{int, string::LibcString, transparent::AsNewtype},
info::TextualInfo,
topology::{Topology, editor::TopologyEditor},
};
use derive_more::Display;
use hwlocality_sys::hwloc_info_s;
#[allow(unused)]
#[cfg(test)]
use similar_asserts::assert_eq;
use std::{
ffi::{c_int, c_uint},
iter::FusedIterator,
num::NonZeroUsize,
ops::Deref,
ptr,
};
use thiserror::Error;
impl Topology {
#[doc(alias = "hwloc_cpukinds_get_nr")]
pub fn num_cpu_kinds(&self) -> Result<NonZeroUsize, NoData> {
let count = errors::call_hwloc_positive_or_minus1("hwloc_cpukinds_get_nr", || unsafe {
hwlocality_sys::hwloc_cpukinds_get_nr(self.as_ptr(), 0)
})
.expect("All known failure cases are prevented by API design");
NonZeroUsize::new(int::expect_usize(count)).ok_or(NoData)
}
#[doc(alias = "hwloc_cpukinds_get_info")]
pub fn cpu_kinds(
&self,
) -> Result<
impl DoubleEndedIterator<Item = CpuKind<'_>> + Clone + ExactSizeIterator + FusedIterator,
NoData,
> {
let num_cpu_kinds = usize::from(self.num_cpu_kinds()?);
Ok((0..num_cpu_kinds).map(move |kind_index| unsafe { self.cpu_kind(kind_index) }))
}
unsafe fn cpu_kind(&self, kind_index: usize) -> CpuKind<'_> {
let kind_index =
c_uint::try_from(kind_index).expect("Should not happen if API contract is honored");
let mut cpuset = CpuSet::new();
let mut efficiency = c_int::MIN;
let mut nr_infos: c_uint = 0;
let mut infos = ptr::null_mut();
errors::call_hwloc_zero_or_minus1("hwloc_cpukinds_get_info", || unsafe {
hwlocality_sys::hwloc_cpukinds_get_info(
self.as_ptr(),
kind_index,
cpuset.as_mut_ptr(),
&raw mut efficiency,
&raw mut nr_infos,
&raw mut infos,
0,
)
})
.expect("All documented failure cases are prevented by API design");
let efficiency = match efficiency {
-1 => None,
other => {
let positive = c_uint::try_from(other).expect("Unexpected CpuEfficiency value");
Some(int::expect_usize(positive))
}
};
let infos = if infos.is_null() {
#[cfg(not(tarpaulin_include))]
assert_eq!(
nr_infos, 0,
"hwloc pretended to yield {nr_infos} infos but provided a null infos pointer"
);
&[]
} else {
#[allow(clippy::missing_docs_in_private_items)]
type Element = TextualInfo;
let infos_len = int::expect_usize(nr_infos);
int::assert_slice_len::<Element>(infos_len);
unsafe { std::slice::from_raw_parts::<Element>(infos.as_newtype(), infos_len) }
};
CpuKind {
cpuset,
efficiency,
infos,
}
}
#[doc(alias = "hwloc_cpukinds_get_by_cpuset")]
pub fn cpu_kind_from_set(
&self,
set: impl Deref<Target = CpuSet>,
) -> Result<CpuKind<'_>, FromSetError> {
fn polymorphized<'self_>(
self_: &'self_ Topology,
set: &CpuSet,
) -> Result<CpuKind<'self_>, FromSetError> {
let error = |problem| Err(FromSetError(set.clone(), problem));
match self_.cpu_kinds() {
Ok(kinds) => {
let mut intersects = false;
for kind in kinds {
if kind.cpuset.includes(set) {
return Ok(kind);
} else if kind.cpuset.intersects(set) {
intersects = true;
}
}
if intersects {
error(FromSetProblem::PartiallyIncluded)
} else {
error(FromSetProblem::NotIncluded)
}
}
Err(NoData) => error(FromSetProblem::NotIncluded),
}
}
polymorphized(self, &set)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct CpuKind<'topology> {
pub cpuset: CpuSet,
pub efficiency: Option<CpuEfficiency>,
pub infos: &'topology [TextualInfo],
}
impl TopologyEditor<'_> {
#[allow(clippy::collection_is_never_read)]
#[doc(alias = "hwloc_cpukinds_register")]
pub fn register_cpu_kind<'infos>(
&mut self,
cpuset: impl Deref<Target = CpuSet>,
forced_efficiency: Option<CpuEfficiency>,
infos: impl IntoIterator<Item = (&'infos str, &'infos str)>,
) -> Result<(), RegisterError> {
unsafe fn polymorphized(
self_: &mut TopologyEditor<'_>,
cpuset: &CpuSet,
forced_efficiency: Option<CpuEfficiency>,
raw_infos: Vec<hwloc_info_s>,
) -> Result<(), RegisterError> {
if cpuset.is_empty() {
return Err(RegisterError::NoCPUs);
}
let complete_cpuset = self_.topology().complete_cpuset();
if !complete_cpuset.includes(cpuset) {
return Err(RegisterError::UnknownCPUs(cpuset - complete_cpuset));
}
let forced_efficiency = if let Some(eff) = forced_efficiency {
c_int::try_from(eff).map_err(|_| RegisterError::ExcessiveEfficiency(eff))?
} else {
-1
};
let infos_ptr = raw_infos.as_ptr();
#[cfg(not(tarpaulin_include))]
let num_infos =
c_uint::try_from(raw_infos.len()).map_err(|_| RegisterError::TooManyInfos)?;
errors::call_hwloc_zero_or_minus1("hwloc_cpukinds_register", || unsafe {
hwlocality_sys::hwloc_cpukinds_register(
self_.topology_mut_ptr(),
cpuset.as_ptr(),
forced_efficiency,
num_infos,
infos_ptr,
0,
)
})
.expect("All known failure cases are prevented by API design");
Ok(())
}
let input_infos = infos.into_iter();
let mut infos = Vec::new();
let mut raw_infos = Vec::new();
if let Some(infos_len) = input_infos.size_hint().1 {
infos.reserve(infos_len);
raw_infos.reserve(infos_len);
}
for (name, value) in input_infos {
let new_string =
|s: &str| LibcString::new(s).map_err(|_| RegisterError::InfoContainsNul);
let (name, value) = (new_string(name)?, new_string(value)?);
raw_infos.push(unsafe { TextualInfo::borrow_raw(&name, &value) });
infos.push((name, value));
}
unsafe { polymorphized(self, &cpuset, forced_efficiency, raw_infos) }
}
}
pub type CpuEfficiency = usize;
#[derive(Copy, Clone, Debug, Default, Error, Eq, Hash, PartialEq)]
#[error("no information about CPU kinds was found")]
pub struct NoData;
#[derive(Clone, Debug, Error, Eq, Hash, PartialEq)]
#[error("CPU set {0} {1}")]
pub struct FromSetError(pub CpuSet, pub FromSetProblem);
#[allow(missing_copy_implementations)]
#[derive(Clone, Debug, Display, Eq, Hash, PartialEq)]
pub enum FromSetProblem {
#[display("is only partially included in some CPU kind")]
PartiallyIncluded,
#[display("isn't part of any known CPU kind")]
NotIncluded,
}
#[derive(Clone, Debug, Error, Eq, Hash, PartialEq)]
pub enum RegisterError {
#[error("CPU kind forced_efficiency value {0} is too high for hwloc")]
ExcessiveEfficiency(CpuEfficiency),
#[error("CPU kind textual info can't contain the NUL char")]
InfoContainsNul,
#[error("hwloc can't handle that much CPU kind textual info")]
TooManyInfos,
#[error("cannot register a CPU kind with an empty cpuset")]
NoCPUs,
#[error("CPUs {0} are not part of the topology")]
UnknownCPUs(CpuSet),
}
impl From<CpuEfficiency> for RegisterError {
fn from(value: CpuEfficiency) -> Self {
Self::ExcessiveEfficiency(value)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
cpu::cpuset::CpuSet,
strategies::{any_string, topology_related_set},
};
use proptest::{collection::SizeRange, prelude::*};
#[allow(unused)]
#[cfg(test)]
use similar_asserts::assert_eq;
use std::{collections::HashSet, sync::OnceLock};
fn cpu_kinds() -> Result<&'static [CpuKind<'static>], NoData> {
static CPU_KINDS: OnceLock<Result<Vec<CpuKind<'static>>, NoData>> = OnceLock::new();
match CPU_KINDS.get_or_init(|| Topology::test_instance().cpu_kinds().map(Vec::from_iter)) {
Ok(v) => Ok(v.as_slice()),
Err(NoData) => Err(NoData),
}
}
#[test]
fn check_cpu_kinds() {
let topology = Topology::test_instance();
if let Ok(kinds) = cpu_kinds() {
let expected_num_kinds = topology
.num_cpu_kinds()
.expect("If there are kinds, num_cpu_kinds should return Ok");
let mut seen_kinds = 0;
let mut seen_cpus = CpuSet::new();
let mut last_efficiency: Option<Option<CpuEfficiency>> = None;
for kind in kinds {
assert!(!kind.cpuset.is_empty());
assert!(!kind.cpuset.intersects(&seen_cpus));
seen_cpus |= &kind.cpuset;
if let Some(last_efficiency) = last_efficiency {
assert_eq!(kind.efficiency.is_some(), last_efficiency.is_some());
}
if let Some(efficiency) = kind.efficiency {
assert!(efficiency >= last_efficiency.map_or(0, Option::unwrap));
assert!(efficiency < expected_num_kinds.get());
}
last_efficiency = Some(kind.efficiency);
let mut seen_info = HashSet::new();
for info in kind.infos {
assert!(seen_info.insert(info.name()));
}
seen_kinds += 1;
}
assert_eq!(seen_kinds, expected_num_kinds.get());
assert!(seen_cpus.includes(topology.cpuset()));
assert!(topology.complete_cpuset().includes(&seen_cpus));
} else {
topology
.num_cpu_kinds()
.expect_err("If there are no kinds, num_cpu_kinds should return Err(NoData)");
}
}
fn cpuset_and_kind() -> impl Strategy<Value = (CpuSet, Result<CpuKind<'static>, FromSetError>)>
{
let Ok(kinds) = cpu_kinds() else {
return topology_related_set(Topology::complete_cpuset)
.prop_map(|set| {
(
set.clone(),
Err(FromSetError(set, FromSetProblem::NotIncluded)),
)
})
.boxed();
};
prop_oneof![
2 => {
prop::sample::select(kinds).prop_flat_map(|main_kind| {
let main_cpus = main_kind.cpuset.iter_set().collect::<Vec<_>>();
let num_main_cpus = main_cpus.len();
prop::sample::subsequence(main_cpus, 1..=num_main_cpus).prop_map(move |cpus| {
(cpus.into_iter().collect::<CpuSet>(), Ok(main_kind.clone()))
})
})
},
3 => {
topology_related_set(Topology::complete_cpuset)
.prop_map(move |set| {
enum MatchKind<'a> {
None,
Inclusion(&'a CpuKind<'static>),
Intersection,
}
let mut match_kind = MatchKind::None;
for kind in kinds {
if kind.cpuset.includes(&set) {
match_kind = MatchKind::Inclusion(kind);
break;
} else if kind.cpuset.intersects(&set) {
match_kind = MatchKind::Intersection;
break;
}
}
match match_kind {
MatchKind::None => (set.clone(), Err(FromSetError(set, FromSetProblem::NotIncluded))),
MatchKind::Inclusion(kind) => (set, Ok(kind.clone())),
MatchKind::Intersection => (set.clone(), Err(FromSetError(set, FromSetProblem::PartiallyIncluded))),
}
})
}
]
.boxed()
}
proptest! {
#[test]
fn cpu_kind_from_set((set, expected_result) in cpuset_and_kind()) {
prop_assert_eq!(Topology::test_instance().cpu_kind_from_set(&set), expected_result);
}
}
fn forced_efficiency() -> impl Strategy<Value = Option<CpuEfficiency>> {
let max_valid = c_int::MAX as usize;
prop_oneof![
1 => Just(None),
3 => (0..=max_valid).prop_map(Some),
1 => ((max_valid+1)..usize::MAX).prop_map(Some)
]
}
fn infos() -> impl Strategy<Value = Vec<(String, String)>> {
let keep_nul = prop_oneof![
4 => Just(false),
1 => Just(true)
];
keep_nul.prop_flat_map(|keep_nul| {
let string = move || {
any_string().prop_map(move |s| {
if keep_nul {
s
} else {
s.chars().filter(|&c| c != '\0').collect()
}
})
};
prop::collection::vec((string(), string()), SizeRange::default())
})
}
proptest! {
#[test]
fn register_cpu_kind(
cpuset in topology_related_set(Topology::complete_cpuset),
forced_efficiency in forced_efficiency(),
infos in infos(),
) {
let mut topology = Topology::test_instance().clone();
let initial_kinds = cpu_kinds();
let result = topology.edit(|editor| {
editor.register_cpu_kind(&cpuset, forced_efficiency, infos.iter().map(|(name, value)| (name.as_str(), value.as_str())))
});
let excessive_efficiency = forced_efficiency.is_some_and(|eff| eff > c_int::MAX as usize);
let info_contains_nul = infos.iter().any(|(name, value)| name.contains('\0') || value.contains('\0'));
let no_cpus = cpuset.is_empty();
let too_many_infos = infos.len() > c_uint::MAX as usize;
let complete_cpuset = Topology::test_instance().complete_cpuset();
let unknown_cpus = !complete_cpuset.includes(&cpuset);
prop_assert_eq!(result.is_err(), excessive_efficiency || info_contains_nul || too_many_infos || unknown_cpus || no_cpus);
if let Err(e) = result {
match e {
RegisterError::ExcessiveEfficiency(eff) => {
prop_assert!(excessive_efficiency);
prop_assert_eq!(forced_efficiency.unwrap(), eff);
prop_assert_eq!(e, RegisterError::from(eff));
}
RegisterError::InfoContainsNul => prop_assert!(info_contains_nul),
RegisterError::NoCPUs => prop_assert!(no_cpus),
RegisterError::TooManyInfos => prop_assert!(too_many_infos),
RegisterError::UnknownCPUs(unknown) => {
prop_assert!(unknown_cpus);
prop_assert_eq!(unknown, cpuset - complete_cpuset);
}
}
return Ok(());
}
let mut shrunk_kinds = Vec::new();
let mut new_kinds = Vec::new();
struct NewKind {
cpuset: CpuSet,
old_infos: &'static [TextualInfo],
}
let mut new_cpuset = cpuset.clone();
if let Ok(initial_kinds) = initial_kinds {
for kind in initial_kinds {
if kind.cpuset.intersects(&cpuset) {
if kind.cpuset != cpuset {
let mut shrunk_kind = kind.clone();
shrunk_kind.cpuset = &kind.cpuset - &cpuset;
shrunk_kinds.push(shrunk_kind);
}
new_kinds.push(NewKind {
cpuset: (&kind.cpuset) & (&cpuset),
old_infos: kind.infos,
});
new_cpuset -= &kind.cpuset;
} else {
shrunk_kinds.push(kind.clone());
}
}
}
if !new_cpuset.is_empty() {
new_kinds.push(NewKind {
cpuset: new_cpuset,
old_infos: &[],
})
}
let mut shrunk_kinds = shrunk_kinds.into_iter().peekable();
let mut new_kinds = new_kinds.into_iter().fuse();
for kind in topology.cpu_kinds().unwrap() {
if let Some(next_shrunk_kind) = shrunk_kinds.peek() {
if kind.cpuset == next_shrunk_kind.cpuset {
prop_assert_eq!(kind.infos, next_shrunk_kind.infos);
shrunk_kinds.next();
continue;
}
}
let expected_kind = new_kinds.next().unwrap();
prop_assert_eq!(kind.cpuset, expected_kind.cpuset);
for info in kind.infos {
if !expected_kind.old_infos.iter().any(|old| old.name() == info.name()) {
let info_name = info.name().to_str().unwrap();
let info_value = info.value().to_str().unwrap();
prop_assert!(
infos.iter().any(|(name, value)| name == info_name && value == info_value)
);
}
}
}
prop_assert!(shrunk_kinds.next().is_none());
prop_assert!(new_kinds.next().is_none());
}
}
}