#[cfg(test)]
use std::cell::Cell;
use std::{sync::Arc, thread};
use moirai_core::error::{ExecutorError, ExecutorResult, PlacementFailure};
use themis::BindError;
use super::super::types::WorkerState;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct Assignment {
pub(super) processor: u32,
pub(super) node: usize,
}
#[cfg(test)]
pub(in crate::schedule::runtime) type TestBinder =
Arc<dyn Fn(usize, u32) -> Result<(), BindError> + Send + Sync>;
#[cfg(test)]
pub(in crate::schedule::runtime) struct PinProbe {
pub(in crate::schedule::runtime) pairs: Vec<(u32, usize)>,
pub(in crate::schedule::runtime) binder: TestBinder,
pub(in crate::schedule::runtime) lifetime_owner: Box<dyn std::any::Any + Send + Sync>,
}
#[derive(Clone)]
pub(in crate::schedule::runtime) struct WorkerPin {
#[cfg(test)]
worker: usize,
processor: u32,
#[cfg(test)]
binder: Option<TestBinder>,
}
impl WorkerPin {
pub(in crate::schedule::runtime) fn bind(&self) -> Result<(), PlacementFailure> {
#[cfg(test)]
if let Some(binder) = &self.binder {
return binder(self.worker, self.processor).map_err(placement_failure);
}
themis::bind_current_thread(self.processor).map_err(placement_failure)
}
}
pub(super) struct Pinning {
assignments: Box<[Assignment]>,
#[cfg(test)]
binder: Option<TestBinder>,
#[cfg(test)]
lifetime_owner: Cell<Option<Box<dyn std::any::Any + Send + Sync>>>,
}
impl Pinning {
pub(super) fn detect(worker_count: usize) -> ExecutorResult<Self> {
let topology = themis::CpuTopology::detect().ok_or(ExecutorError::InvalidConfiguration)?;
let pairs = topology
.processor_node_pairs()
.map(|(processor, node)| (processor, node.index()));
Ok(Self {
assignments: plan(pairs, worker_count)?,
#[cfg(test)]
binder: None,
#[cfg(test)]
lifetime_owner: Cell::new(None),
})
}
#[cfg(test)]
pub(super) fn from_probe(probe: PinProbe, worker_count: usize) -> ExecutorResult<Self> {
Ok(Self {
assignments: plan(probe.pairs.iter().copied(), worker_count)?,
binder: Some(probe.binder),
lifetime_owner: Cell::new(Some(probe.lifetime_owner)),
})
}
#[cfg(test)]
pub(super) fn take_lifetime_owner(&self) -> Option<Box<dyn std::any::Any + Send + Sync>> {
self.lifetime_owner.take()
}
pub(super) fn worker_numa_nodes(&self) -> Box<[Option<usize>]> {
self.assignments
.iter()
.map(|assignment| Some(assignment.node))
.collect()
}
pub(super) fn worker_pin(&self, worker: usize) -> WorkerPin {
WorkerPin {
#[cfg(test)]
worker,
processor: self.assignments[worker].processor,
#[cfg(test)]
binder: self.binder.clone(),
}
}
pub(super) fn await_outcomes(&self, workers: &[Arc<WorkerState>]) -> ExecutorResult<()> {
for worker in workers {
while worker.placement.get().is_none() {
thread::yield_now();
}
}
for (worker, (state, assignment)) in workers.iter().zip(&self.assignments).enumerate() {
if let Some(Err(cause)) = state.placement.get() {
return Err(ExecutorError::WorkerPlacementFailed {
worker,
processor: assignment.processor,
cause: *cause,
});
}
}
Ok(())
}
}
fn plan(
pairs: impl Iterator<Item = (u32, usize)>,
worker_count: usize,
) -> ExecutorResult<Box<[Assignment]>> {
let mut processors: Vec<Assignment> = pairs
.map(|(processor, node)| Assignment { processor, node })
.collect();
processors.sort_unstable_by_key(|assignment| assignment.processor);
if processors.is_empty() {
return Err(ExecutorError::InvalidConfiguration);
}
Ok((0..worker_count)
.map(|worker| processors[worker % processors.len()])
.collect())
}
fn placement_failure(error: BindError) -> PlacementFailure {
match error {
BindError::Unsupported => PlacementFailure::Unsupported,
BindError::OutOfRange { .. } => PlacementFailure::OutOfRange,
BindError::Os { code } => PlacementFailure::Os { code },
_ => PlacementFailure::Unsupported,
}
}
pub(super) fn normalize_worker_numa_nodes(
mut worker_numa_nodes: Box<[Option<usize>]>,
) -> Box<[Option<usize>]> {
let mut represented_nodes = worker_numa_nodes.iter().copied().flatten();
let has_multiple_nodes = represented_nodes
.next()
.is_some_and(|first| represented_nodes.any(|node| node != first));
if !has_multiple_nodes {
worker_numa_nodes.fill(None);
}
worker_numa_nodes
}
#[cfg(test)]
mod tests {
use super::*;
fn processors(assignments: &[Assignment]) -> Vec<u32> {
assignments
.iter()
.map(|assignment| assignment.processor)
.collect()
}
#[test]
fn locality_pass_requires_multiple_represented_nodes() {
for assignments in [
vec![None, None, None].into_boxed_slice(),
vec![Some(0), Some(0), Some(0)].into_boxed_slice(),
vec![None, Some(3), None].into_boxed_slice(),
] {
assert_eq!(
&*normalize_worker_numa_nodes(assignments),
&[None, None, None]
);
}
let multiple = vec![Some(3), None, Some(7), Some(3)].into_boxed_slice();
assert_eq!(
&*normalize_worker_numa_nodes(multiple),
&[Some(3), None, Some(7), Some(3)]
);
}
#[test]
fn plan_sorts_by_processor_and_wraps_when_workers_exceed_processors() {
let planned = plan([(5, 1), (2, 0), (9, 1)].into_iter(), 5)
.expect("invariant: a non-empty topology plans");
assert_eq!(processors(&planned), [2, 5, 9, 2, 5]);
assert_eq!(
planned.iter().map(|a| a.node).collect::<Vec<_>>(),
[0, 1, 1, 0, 1]
);
}
#[test]
fn plan_of_an_empty_topology_is_an_invalid_configuration() {
assert_eq!(
plan(std::iter::empty(), 2).err(),
Some(ExecutorError::InvalidConfiguration)
);
}
#[test]
fn provider_errors_map_onto_the_moirai_contract() {
assert_eq!(
placement_failure(BindError::Unsupported),
PlacementFailure::Unsupported
);
assert_eq!(
placement_failure(BindError::OutOfRange { processor: 40_000 }),
PlacementFailure::OutOfRange
);
assert_eq!(
placement_failure(BindError::Os { code: 22 }),
PlacementFailure::Os { code: 22 }
);
}
}