#![warn(missing_docs, missing_debug_implementations)]
use crate::{
error::BuilderErrorKind,
executor::stall::StallDetector,
io::DmaBuffer,
parking, reactor,
sys::{self, blocking::BlockingThreadPool},
task::{self, waker_fn::dummy_waker},
GlommioError, IoRequirements, IoStats, Latency, Reactor, Shares,
};
use ahash::AHashMap;
use futures_lite::pin;
use latch::{Latch, LatchState};
use log::warn;
pub use placement::{CpuSet, Placement, PoolPlacement};
use std::{
cell::RefCell,
collections::{hash_map::Entry, BinaryHeap},
fmt,
future::Future,
io,
marker::PhantomData,
mem::MaybeUninit,
ops::{Deref, DerefMut},
pin::Pin,
rc::Rc,
sync::{Arc, Mutex},
task::{Context, Poll},
thread::{Builder, JoinHandle},
time::{Duration, Instant},
};
use tracing::trace;
mod latch;
mod multitask;
mod placement;
pub mod stall;
pub(crate) const DEFAULT_EXECUTOR_NAME: &str = "unnamed";
pub(crate) const DEFAULT_PREEMPT_TIMER: Duration = Duration::from_millis(100);
pub(crate) const DEFAULT_IO_MEMORY: usize = 10 << 20;
pub(crate) const DEFAULT_RING_SUBMISSION_DEPTH: usize = 128;
type Result<T> = crate::Result<T, ()>;
#[cfg(feature = "native-tls")]
#[thread_local]
static mut LOCAL_EX: *const LocalExecutor = std::ptr::null();
#[cfg(not(feature = "native-tls"))]
scoped_tls::scoped_thread_local!(static LOCAL_EX: LocalExecutor);
#[inline(always)]
pub fn executor() -> ExecutorProxy {
ExecutorProxy {}
}
pub(crate) fn executor_id() -> Option<usize> {
#[cfg(not(feature = "native-tls"))]
{
if LOCAL_EX.is_set() {
Some(LOCAL_EX.with(|ex| ex.id))
} else {
None
}
}
#[cfg(feature = "native-tls")]
unsafe {
LOCAL_EX.as_ref().map(|ex| ex.id)
}
}
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct TaskQueueHandle {
index: usize,
}
impl TaskQueueHandle {
pub fn index(&self) -> usize {
self.index
}
}
#[derive(Debug)]
pub(crate) struct TaskQueue {
pub(crate) ex: Rc<multitask::LocalExecutor>,
active: bool,
shares: Shares,
vruntime: u64,
io_requirements: IoRequirements,
name: String,
last_adjustment: Instant,
yielded: bool,
stats: TaskQueueStats,
}
impl Ord for TaskQueue {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
other.vruntime.cmp(&self.vruntime)
}
}
impl PartialOrd for TaskQueue {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(other.vruntime.cmp(&self.vruntime))
}
}
impl PartialEq for TaskQueue {
fn eq(&self, other: &Self) -> bool {
self.vruntime == other.vruntime
}
}
impl Eq for TaskQueue {}
impl TaskQueue {
fn new<S>(
index: TaskQueueHandle,
name: S,
shares: Shares,
ioreq: IoRequirements,
) -> Rc<RefCell<Self>>
where
S: Into<String>,
{
Rc::new(RefCell::new(TaskQueue {
ex: Rc::new(multitask::LocalExecutor::new()),
active: false,
stats: TaskQueueStats::new(index, shares.reciprocal_shares()),
shares,
vruntime: 0,
io_requirements: ioreq,
name: name.into(),
last_adjustment: Instant::now(),
yielded: false,
}))
}
fn is_active(&self) -> bool {
self.active
}
fn get_task(&mut self) -> Option<multitask::Runnable> {
self.ex.get_task()
}
fn yielded(&self) -> bool {
self.yielded
}
fn prepare_to_run(&mut self, now: Instant) {
self.yielded = false;
if let Shares::Dynamic(bm) = &self.shares {
if now.saturating_duration_since(self.last_adjustment) > bm.adjustment_period() {
self.last_adjustment = now;
self.stats.reciprocal_shares = self.shares.reciprocal_shares();
}
}
}
fn account_vruntime(&mut self, delta: Duration) -> Option<u64> {
let delta_scaled = (self.stats.reciprocal_shares * (delta.as_nanos() as u64)) >> 12;
self.stats.runtime += delta;
self.stats.queue_selected += 1;
self.active = self.ex.is_active();
let vruntime = self.vruntime.checked_add(delta_scaled);
if let Some(x) = vruntime {
self.vruntime = x;
}
vruntime
}
}
pub(crate) fn bind_to_cpu_set(cpus: impl IntoIterator<Item = usize>) -> Result<()> {
let mut cpuset = nix::sched::CpuSet::new();
for cpu in cpus {
cpuset.set(cpu).map_err(|e| to_io_error!(e))?;
}
let pid = nix::unistd::Pid::from_raw(0);
nix::sched::sched_setaffinity(pid, &cpuset).map_err(|e| Into::into(to_io_error!(e)))
}
#[derive(Debug, Copy, Clone, Default)]
pub struct ExecutorStats {
executor_runtime: Duration,
total_runtime: Duration,
scheduler_runs: u64,
tasks_executed: u64,
}
impl ExecutorStats {
fn new() -> Self {
Self {
executor_runtime: Duration::from_nanos(0),
total_runtime: Duration::from_nanos(0),
scheduler_runs: 0,
tasks_executed: 0,
}
}
pub fn executor_runtime(&self) -> Duration {
self.executor_runtime
}
pub fn total_runtime(&self) -> Duration {
self.total_runtime
}
pub fn scheduler_runs(&self) -> u64 {
self.scheduler_runs
}
pub fn tasks_executed(&self) -> u64 {
self.tasks_executed
}
}
#[derive(Debug, Copy, Clone)]
pub struct TaskQueueStats {
index: TaskQueueHandle,
reciprocal_shares: u64,
queue_selected: u64,
runtime: Duration,
}
impl TaskQueueStats {
fn new(index: TaskQueueHandle, reciprocal_shares: u64) -> Self {
Self {
index,
reciprocal_shares,
runtime: Duration::from_nanos(0),
queue_selected: 0,
}
}
pub fn index(&self) -> TaskQueueHandle {
self.index
}
pub fn current_shares(&self) -> usize {
((1u64 << 22) / self.reciprocal_shares) as usize
}
pub fn runtime(&self) -> Duration {
self.runtime
}
pub fn queue_selected(&self) -> u64 {
self.queue_selected
}
pub(crate) fn take(&mut self) -> Self {
std::mem::replace(
self,
Self {
index: self.index,
reciprocal_shares: self.reciprocal_shares,
queue_selected: Default::default(),
runtime: Default::default(),
},
)
}
}
#[derive(Debug)]
struct ExecutorQueues {
active_executors: BinaryHeap<Rc<RefCell<TaskQueue>>>,
available_executors: AHashMap<usize, Rc<RefCell<TaskQueue>>>,
active_executing: Option<Rc<RefCell<TaskQueue>>>,
executor_index: usize,
default_vruntime: u64,
preempt_timer_duration: Duration,
default_preempt_timer_duration: Duration,
spin_before_park: Option<Duration>,
stats: ExecutorStats,
}
impl ExecutorQueues {
fn new(preempt_timer_duration: Duration, spin_before_park: Option<Duration>) -> Self {
ExecutorQueues {
active_executors: BinaryHeap::new(),
available_executors: AHashMap::new(),
active_executing: None,
executor_index: 1, default_vruntime: 0,
preempt_timer_duration,
default_preempt_timer_duration: preempt_timer_duration,
spin_before_park,
stats: ExecutorStats::new(),
}
}
fn reevaluate_preempt_timer(&mut self) {
self.preempt_timer_duration = self
.active_executors
.iter()
.map(|tq| match tq.borrow().io_requirements.latency_req {
Latency::NotImportant => self.default_preempt_timer_duration,
Latency::Matters(d) => d,
})
.min()
.unwrap_or(self.default_preempt_timer_duration)
}
fn maybe_activate(&mut self, queue: Rc<RefCell<TaskQueue>>) {
let mut state = queue.borrow_mut();
if !state.is_active() {
state.vruntime = self.default_vruntime + 1;
state.active = true;
drop(state);
self.active_executors.push(queue);
self.reevaluate_preempt_timer();
}
}
}
#[derive(Debug)]
pub struct ExecutorJoinHandle<T: Send + 'static>(JoinHandle<Result<T>>);
impl<T: Send + 'static> ExecutorJoinHandle<T> {
#[must_use]
pub fn thread(&self) -> &std::thread::Thread {
self.0.thread()
}
pub fn join(self) -> Result<T> {
match self.0.join() {
Err(err) => Err(GlommioError::BuilderError(BuilderErrorKind::ThreadPanic(
err,
))),
Ok(Err(err)) => Err(err),
Ok(Ok(res)) => Ok(res),
}
}
}
#[derive(Debug)]
pub struct LocalExecutorBuilder {
placement: Placement,
spin_before_park: Option<Duration>,
name: String,
io_memory: usize,
ring_depth: usize,
preempt_timer_duration: Duration,
record_io_latencies: bool,
blocking_thread_pool_placement: PoolPlacement,
detect_stalls: Option<Box<dyn stall::StallDetectionHandler + 'static>>,
}
impl LocalExecutorBuilder {
pub fn new(placement: Placement) -> LocalExecutorBuilder {
LocalExecutorBuilder {
placement: placement.clone(),
spin_before_park: None,
name: String::from(DEFAULT_EXECUTOR_NAME),
io_memory: DEFAULT_IO_MEMORY,
ring_depth: DEFAULT_RING_SUBMISSION_DEPTH,
preempt_timer_duration: DEFAULT_PREEMPT_TIMER,
record_io_latencies: false,
blocking_thread_pool_placement: PoolPlacement::from(placement),
detect_stalls: None,
}
}
#[must_use = "The builder must be built to be useful"]
pub fn spin_before_park(mut self, spin: Duration) -> LocalExecutorBuilder {
self.spin_before_park = Some(spin);
self
}
#[must_use = "The builder must be built to be useful"]
pub fn name(mut self, name: &str) -> LocalExecutorBuilder {
self.name = String::from(name);
self
}
#[must_use = "The builder must be built to be useful"]
pub fn io_memory(mut self, io_memory: usize) -> LocalExecutorBuilder {
self.io_memory = io_memory;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn ring_depth(mut self, ring_depth: usize) -> LocalExecutorBuilder {
assert!(ring_depth > 0, "ring depth should be strictly positive");
self.ring_depth = ring_depth;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn preempt_timer(mut self, dur: Duration) -> LocalExecutorBuilder {
self.preempt_timer_duration = dur;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn record_io_latencies(mut self, enabled: bool) -> LocalExecutorBuilder {
self.record_io_latencies = enabled;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn blocking_thread_pool_placement(
mut self,
placement: PoolPlacement,
) -> LocalExecutorBuilder {
self.blocking_thread_pool_placement = placement;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn detect_stalls(
mut self,
handler: Option<Box<dyn stall::StallDetectionHandler + 'static>>,
) -> Self {
self.detect_stalls = handler;
self
}
pub fn make(self) -> Result<LocalExecutor> {
let notifier = sys::new_sleep_notifier()?;
let mut cpu_set_gen = placement::CpuSetGenerator::one(self.placement)?;
let mut le = LocalExecutor::new(
notifier,
cpu_set_gen.next().cpu_binding(),
LocalExecutorConfig {
io_memory: self.io_memory,
ring_depth: self.ring_depth,
preempt_timer: self.preempt_timer_duration,
record_io_latencies: self.record_io_latencies,
spin_before_park: self.spin_before_park,
thread_pool_placement: self.blocking_thread_pool_placement,
detect_stalls: self.detect_stalls,
},
)?;
le.init();
Ok(le)
}
#[must_use = "This spawns an executor on a thread, so you may need to call \
`JoinHandle::join()` to keep the main thread alive"]
pub fn spawn<G, F, T>(self, fut_gen: G) -> Result<ExecutorJoinHandle<T>>
where
G: FnOnce() -> F + Send + 'static,
F: Future<Output = T> + 'static,
T: Send + 'static,
{
let notifier = sys::new_sleep_notifier()?;
let name = format!("{}-{}", self.name, notifier.id());
let mut cpu_set_gen = placement::CpuSetGenerator::one(self.placement)?;
let io_memory = self.io_memory;
let ring_depth = self.ring_depth;
let preempt_timer_duration = self.preempt_timer_duration;
let spin_before_park = self.spin_before_park;
let detect_stalls = self.detect_stalls;
let record_io_latencies = self.record_io_latencies;
let blocking_thread_pool_placement = self.blocking_thread_pool_placement;
Builder::new()
.name(name)
.spawn(move || {
let mut le = LocalExecutor::new(
notifier,
cpu_set_gen.next().cpu_binding(),
LocalExecutorConfig {
io_memory,
ring_depth,
preempt_timer: preempt_timer_duration,
record_io_latencies,
spin_before_park,
thread_pool_placement: blocking_thread_pool_placement,
detect_stalls,
},
)?;
le.init();
le.run(async move { Ok(fut_gen().await) })
})
.map_err(Into::into)
.map(ExecutorJoinHandle)
}
}
impl Default for LocalExecutorBuilder {
fn default() -> Self {
Self::new(Placement::Unbound)
}
}
pub struct LocalExecutorPoolBuilder {
spin_before_park: Option<Duration>,
name: String,
io_memory: usize,
ring_depth: usize,
preempt_timer_duration: Duration,
placement: PoolPlacement,
record_io_latencies: bool,
blocking_thread_pool_placement: PoolPlacement,
handler_gen: Option<Box<dyn Fn() -> Box<dyn stall::StallDetectionHandler + 'static>>>,
}
impl fmt::Debug for LocalExecutorPoolBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LocalExecutorPoolBuilder")
.field("spin_before_park", &self.spin_before_park)
.field("name", &self.name)
.field("io_memory", &self.io_memory)
.field("ring_depth", &self.ring_depth)
.field("preempt_timer_duration", &self.preempt_timer_duration)
.field("record_io_latencies", &self.record_io_latencies)
.field(
"blocking_thread_pool_placement",
&self.blocking_thread_pool_placement,
)
.finish_non_exhaustive()
}
}
impl LocalExecutorPoolBuilder {
pub fn new(placement: PoolPlacement) -> Self {
Self {
spin_before_park: None,
name: String::from(DEFAULT_EXECUTOR_NAME),
io_memory: DEFAULT_IO_MEMORY,
ring_depth: DEFAULT_RING_SUBMISSION_DEPTH,
preempt_timer_duration: DEFAULT_PREEMPT_TIMER,
placement: placement.clone(),
record_io_latencies: false,
blocking_thread_pool_placement: placement.shrink_to(1),
handler_gen: None,
}
}
#[must_use = "The builder must be built to be useful"]
pub fn spin_before_park(mut self, spin: Duration) -> Self {
self.spin_before_park = Some(spin);
self
}
#[must_use = "The builder must be built to be useful"]
pub fn name(mut self, name: &str) -> Self {
self.name = String::from(name);
self
}
#[must_use = "The builder must be built to be useful"]
pub fn io_memory(mut self, io_memory: usize) -> Self {
self.io_memory = io_memory;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn ring_depth(mut self, ring_depth: usize) -> Self {
assert!(ring_depth > 0, "ring depth should be strictly positive");
self.ring_depth = ring_depth;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn preempt_timer(mut self, dur: Duration) -> Self {
self.preempt_timer_duration = dur;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn record_io_latencies(mut self, enabled: bool) -> Self {
self.record_io_latencies = enabled;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn blocking_thread_pool_placement(mut self, placement: PoolPlacement) -> Self {
self.blocking_thread_pool_placement = placement;
self
}
#[must_use = "The builder must be built to be useful"]
pub fn detect_stalls(
mut self,
handler_gen: Option<Box<dyn Fn() -> Box<dyn stall::StallDetectionHandler + 'static>>>,
) -> Self {
self.handler_gen = handler_gen;
self
}
#[must_use = "This spawns executors on multiple threads; threads may fail to spawn or you may \
need to call `PoolThreadHandles::join_all()` to keep the main thread alive"]
pub fn on_all_shards<G, F, T>(self, fut_gen: G) -> Result<PoolThreadHandles<T>>
where
G: FnOnce() -> F + Clone + Send + 'static,
F: Future<Output = T> + 'static,
T: Send + 'static,
{
let mut handles = PoolThreadHandles::new();
let nr_shards = self.placement.executor_count();
let mut cpu_set_gen = placement::CpuSetGenerator::pool(self.placement.clone())?;
let latch = Latch::new(nr_shards);
for _ in 0..nr_shards {
match self.spawn_thread(&mut cpu_set_gen, &latch, fut_gen.clone()) {
Ok(handle) => handles.push(handle),
Err(err) => {
handles.join_all();
return Err(err);
}
}
}
Ok(handles)
}
fn spawn_thread<G, F, T>(
&self,
cpu_set_gen: &mut placement::CpuSetGenerator,
latch: &Latch,
fut_gen: G,
) -> Result<JoinHandle<Result<T>>>
where
G: FnOnce() -> F + Clone + Send + 'static,
F: Future<Output = T> + 'static,
T: Send + 'static,
{
let cpu_binding = cpu_set_gen.next().cpu_binding();
let notifier = sys::new_sleep_notifier()?;
let name = format!("{}-{}", self.name, notifier.id());
let handle = Builder::new().name(name).spawn({
let io_memory = self.io_memory;
let ring_depth = self.ring_depth;
let preempt_timer_duration = self.preempt_timer_duration;
let spin_before_park = self.spin_before_park;
let record_io_latencies = self.record_io_latencies;
let blocking_thread_pool_placement = self.blocking_thread_pool_placement.clone();
let detect_stalls = self.handler_gen.as_ref().map(|x| (*x.deref())());
let latch = Latch::clone(latch);
move || {
if latch.arrive_and_wait() == LatchState::Ready {
let mut le = LocalExecutor::new(
notifier,
cpu_binding,
LocalExecutorConfig {
io_memory,
ring_depth,
preempt_timer: preempt_timer_duration,
record_io_latencies,
spin_before_park,
thread_pool_placement: blocking_thread_pool_placement,
detect_stalls,
},
)?;
le.init();
le.run(async move { Ok(fut_gen().await) })
} else {
Err(io::Error::new(io::ErrorKind::Other, "spawn failed").into())
}
}
});
match handle {
Ok(h) => Ok(h),
Err(e) => {
latch.cancel().expect("unreachable: latch was ready");
Err(e.into())
}
}
}
}
#[derive(Debug)]
pub struct PoolThreadHandles<T> {
handles: Vec<JoinHandle<Result<T>>>,
}
impl<T> PoolThreadHandles<T> {
fn new() -> Self {
Self {
handles: Vec::new(),
}
}
fn push(&mut self, handle: JoinHandle<Result<T>>) {
self.handles.push(handle)
}
pub fn handles(&self) -> &Vec<JoinHandle<Result<T>>> {
&self.handles
}
pub fn join_all(self) -> Vec<Result<T>> {
self.handles
.into_iter()
.map(|h| {
match h.join() {
Ok(ok @ Ok(_)) => ok,
Ok(err @ Err(_)) => err,
Err(e) => Err(GlommioError::BuilderError(BuilderErrorKind::ThreadPanic(e))),
}
})
.collect::<Vec<_>>()
}
}
pub(crate) fn maybe_activate(tq: Rc<RefCell<TaskQueue>>) {
#[cfg(not(feature = "native-tls"))]
LOCAL_EX.with(|local_ex| {
let mut queues = local_ex.queues.borrow_mut();
queues.maybe_activate(tq);
});
#[cfg(feature = "native-tls")]
unsafe {
let mut queues = LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.queues
.borrow_mut();
queues.maybe_activate(tq);
};
}
pub struct LocalExecutorConfig {
pub io_memory: usize,
pub ring_depth: usize,
pub preempt_timer: Duration,
pub record_io_latencies: bool,
pub spin_before_park: Option<Duration>,
pub thread_pool_placement: PoolPlacement,
pub detect_stalls: Option<Box<dyn stall::StallDetectionHandler + 'static>>,
}
#[derive(Debug)]
pub struct LocalExecutor {
queues: Rc<RefCell<ExecutorQueues>>,
parker: parking::Parker,
id: usize,
reactor: Rc<reactor::Reactor>,
stall_detector: RefCell<Option<StallDetector>>,
}
impl LocalExecutor {
fn get_reactor(&self) -> Rc<Reactor> {
self.reactor.clone()
}
fn init(&mut self) {
let io_requirements = IoRequirements::new(Latency::NotImportant, 0);
self.queues.borrow_mut().available_executors.insert(
0,
TaskQueue::new(
Default::default(),
"default",
Shares::Static(1000),
io_requirements,
),
);
}
fn new(
notifier: Arc<sys::SleepNotifier>,
cpu_binding: Option<impl IntoIterator<Item = usize>>,
mut config: LocalExecutorConfig,
) -> Result<LocalExecutor> {
let blocking_thread =
BlockingThreadPool::new(config.thread_pool_placement, notifier.clone())?;
match cpu_binding {
Some(cpu_set) => bind_to_cpu_set(cpu_set)?,
None => config.spin_before_park = None,
}
let p = parking::Parker::new();
let queues = ExecutorQueues::new(config.preempt_timer, config.spin_before_park);
let id = notifier.id();
trace!(id = id, "Creating executor");
Ok(LocalExecutor {
queues: Rc::new(RefCell::new(queues)),
parker: p,
id,
reactor: Rc::new(reactor::Reactor::new(
notifier,
config.io_memory,
config.ring_depth,
config.record_io_latencies,
blocking_thread,
)?),
stall_detector: RefCell::new(
config
.detect_stalls
.map(|x| StallDetector::new(id, x))
.transpose()?,
),
})
}
pub fn detect_stalls(
&self,
handler: Option<Box<dyn stall::StallDetectionHandler + 'static>>,
) -> Result<()> {
self.stall_detector.replace(
handler
.map(|x| StallDetector::new(self.id, x))
.transpose()?,
);
Ok(())
}
pub fn id(&self) -> usize {
self.id
}
fn create_task_queue<S>(&self, shares: Shares, latency: Latency, name: S) -> TaskQueueHandle
where
S: Into<String>,
{
let index = {
let mut ex = self.queues.borrow_mut();
let index = ex.executor_index;
ex.executor_index += 1;
index
};
let io_requirements = IoRequirements::new(latency, index);
let tq = TaskQueue::new(TaskQueueHandle { index }, name, shares, io_requirements);
self.queues
.borrow_mut()
.available_executors
.insert(index, tq);
TaskQueueHandle { index }
}
pub fn remove_task_queue(&self, handle: TaskQueueHandle) -> Result<()> {
let mut queues = self.queues.borrow_mut();
let queue_entry = queues.available_executors.entry(handle.index);
if let Entry::Occupied(entry) = queue_entry {
let tq = entry.get();
if tq.borrow().is_active() {
return Err(GlommioError::queue_still_active(handle.index));
}
entry.remove();
return Ok(());
}
Err(GlommioError::queue_not_found(handle.index))
}
fn get_queue(&self, handle: &TaskQueueHandle) -> Option<Rc<RefCell<TaskQueue>>> {
self.queues
.borrow()
.available_executors
.get(&handle.index)
.cloned()
}
fn current_task_queue(&self) -> TaskQueueHandle {
self.queues
.borrow()
.active_executing
.as_ref()
.unwrap()
.borrow()
.stats
.index
}
fn mark_me_for_yield(&self) {
let queues = self.queues.borrow();
let mut me = queues.active_executing.as_ref().unwrap().borrow_mut();
me.yielded = true;
}
fn spawn<T>(&self, future: impl Future<Output = T>) -> multitask::Task<T> {
let tq = self
.queues
.borrow()
.active_executing
.clone() .or_else(|| self.get_queue(&TaskQueueHandle { index: 0 }))
.unwrap();
let id = self.id;
let ex = tq.borrow().ex.clone();
ex.spawn_and_run(id, tq, future)
}
fn spawn_into<T, F>(&self, future: F, handle: TaskQueueHandle) -> Result<multitask::Task<T>>
where
F: Future<Output = T>,
{
let tq = self
.get_queue(&handle)
.ok_or_else(|| GlommioError::queue_not_found(handle.index))?;
let ex = tq.borrow().ex.clone();
let id = self.id;
Ok(ex.spawn_and_schedule(id, tq, future))
}
fn preempt_timer_duration(&self) -> Duration {
self.queues.borrow().preempt_timer_duration
}
fn spin_before_park(&self) -> Option<Duration> {
self.queues.borrow().spin_before_park
}
#[inline(always)]
pub(crate) fn need_preempt(&self) -> bool {
self.reactor.need_preempt()
}
fn run_task_queues(&self) -> bool {
let mut ran = false;
loop {
self.reactor.sys.install_eventfd();
if self.need_preempt() {
break;
}
if !self.run_one_task_queue() {
return false;
} else {
ran = true;
}
}
ran
}
fn run_one_task_queue(&self) -> bool {
let mut tq = self.queues.borrow_mut();
let candidate = tq.active_executors.pop();
tq.stats.scheduler_runs += 1;
if candidate.is_none() {
return false;
}
let queue = candidate.unwrap();
tq.active_executing = Some(queue.clone());
drop(tq);
let time = {
let now = Instant::now();
let mut queue_ref = queue.borrow_mut();
queue_ref.prepare_to_run(now);
self.reactor
.inform_io_requirements(queue_ref.io_requirements);
now
};
let (runtime, tasks_executed_this_loop) = {
let detector = self.stall_detector.borrow();
let guard = detector.as_ref().map(|x| {
let queue = queue.borrow_mut();
x.enter_task_queue(
queue.stats.index,
queue.name.clone(),
time,
self.preempt_timer_duration(),
)
});
let mut tasks_executed_this_loop = 0;
loop {
let mut queue_ref = queue.borrow_mut();
if self.need_preempt() || queue_ref.yielded() {
break;
}
if let Some(r) = queue_ref.get_task() {
drop(queue_ref);
r.run();
tasks_executed_this_loop += 1;
} else {
break;
}
}
let elapsed = time.elapsed();
drop(guard);
(elapsed, tasks_executed_this_loop)
};
let (need_repush, vruntime) = {
let mut state = queue.borrow_mut();
let last_vruntime = state.account_vruntime(runtime);
(state.is_active(), last_vruntime)
};
let mut tq = self.queues.borrow_mut();
tq.active_executing = None;
tq.stats.executor_runtime += runtime;
tq.stats.tasks_executed += tasks_executed_this_loop;
let vruntime = match vruntime {
Some(x) => x,
None => {
for queue in tq.available_executors.values() {
let mut q = queue.borrow_mut();
q.vruntime = 0;
}
0
}
};
if need_repush {
tq.active_executors.push(queue);
} else {
tq.reevaluate_preempt_timer();
}
tq.default_vruntime = tq
.active_executors
.peek()
.map(|x| x.borrow().vruntime)
.unwrap_or(vruntime);
true
}
pub fn run<T>(&self, future: impl Future<Output = T>) -> T {
let run = |this: &Self| {
let waker = dummy_waker();
let cx = &mut Context::from_waker(&waker);
let spin_before_park = self.spin_before_park().unwrap_or_default();
let future = this
.spawn_into(future, TaskQueueHandle::default())
.unwrap()
.detach();
pin!(future);
let mut pre_time = Instant::now();
loop {
if let Poll::Ready(t) = future.as_mut().poll(cx) {
let cur_time = Instant::now();
this.queues.borrow_mut().stats.total_runtime += cur_time - pre_time;
break t.unwrap();
}
this.parker
.poll_io(|| Some(this.preempt_timer_duration()))
.expect("Failed to poll io! This is actually pretty bad!");
let run = this.run_task_queues();
let cur_time = Instant::now();
this.queues.borrow_mut().stats.total_runtime += cur_time - pre_time;
pre_time = cur_time;
if !run {
if let Poll::Ready(t) = future.as_mut().poll(cx) {
break t.unwrap();
} else {
while !this.reactor.spin_poll_io().unwrap() {
if pre_time.elapsed() > spin_before_park {
this.parker
.park()
.expect("Failed to park! This is actually pretty bad!");
break;
}
}
pre_time = Instant::now();
}
}
}
};
#[cfg(not(feature = "native-tls"))]
{
assert!(
!LOCAL_EX.is_set(),
"There is already an LocalExecutor running on this thread"
);
LOCAL_EX.set(self, || run(self))
}
#[cfg(feature = "native-tls")]
unsafe {
assert!(
LOCAL_EX.is_null(),
"There is already an LocalExecutor running on this thread"
);
defer!(LOCAL_EX = std::ptr::null());
LOCAL_EX = self as *const Self;
run(self)
}
}
}
impl Default for LocalExecutor {
fn default() -> Self {
LocalExecutorBuilder::new(Placement::Unbound)
.make()
.unwrap()
}
}
#[must_use = "tasks get canceled when dropped, use `.detach()` to run them in the background"]
#[derive(Debug)]
pub struct Task<T>(multitask::Task<T>);
impl<T> Task<T> {
pub fn detach(self) -> task::JoinHandle<T> {
self.0.detach()
}
pub async fn cancel(self) -> Option<T> {
self.0.cancel().await
}
}
impl<T> Future for Task<T> {
type Output = T;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(cx)
}
}
#[must_use = "scoped tasks get canceled when dropped, use a standard Task and `.detach()` to run \
them in the background"]
#[derive(Debug)]
pub struct ScopedTask<'a, T>(multitask::Task<T>, PhantomData<&'a T>);
impl<'a, T> ScopedTask<'a, T> {
pub async fn cancel(self) -> Option<T> {
self.0.cancel().await
}
}
impl<'a, T> Future for ScopedTask<'a, T> {
type Output = T;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(cx)
}
}
#[inline(always)]
pub async fn yield_if_needed() {
executor().yield_if_needed().await
}
pub fn spawn_local<T>(future: impl Future<Output = T> + 'static) -> Task<T>
where
T: 'static,
{
executor().spawn_local(future)
}
pub fn allocate_dma_buffer(size: usize) -> DmaBuffer {
executor().reactor().alloc_dma_buffer(size)
}
pub fn allocate_dma_buffer_global(size: usize) -> DmaBuffer {
DmaBuffer::new(size).unwrap()
}
pub fn spawn_local_into<T>(
future: impl Future<Output = T> + 'static,
handle: TaskQueueHandle,
) -> Result<Task<T>>
where
T: 'static,
{
executor().spawn_local_into(future, handle)
}
pub unsafe fn spawn_scoped_local<'a, T>(future: impl Future<Output = T> + 'a) -> ScopedTask<'a, T> {
executor().spawn_scoped_local(future)
}
pub unsafe fn spawn_scoped_local_into<'a, T>(
future: impl Future<Output = T> + 'a,
handle: TaskQueueHandle,
) -> Result<ScopedTask<'a, T>> {
executor().spawn_scoped_local_into(future, handle)
}
#[derive(Debug)]
pub struct ExecutorProxy {}
impl ExecutorProxy {
#[inline(always)]
pub fn need_preempt(&self) -> bool {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| local_ex.need_preempt());
#[cfg(feature = "native-tls")]
return unsafe {
LOCAL_EX
.as_ref()
.map(|ex| ex.need_preempt())
.unwrap_or_default()
};
}
#[inline(always)]
pub async fn yield_if_needed(&self) {
#[cfg(not(feature = "native-tls"))]
{
let need_yield = if LOCAL_EX.is_set() {
LOCAL_EX.with(|local_ex| {
if local_ex.need_preempt() {
local_ex.mark_me_for_yield();
true
} else {
false
}
})
} else {
false
};
if need_yield {
futures_lite::future::yield_now().await;
}
}
#[cfg(feature = "native-tls")]
unsafe {
if self.need_preempt() {
(*LOCAL_EX).mark_me_for_yield();
futures_lite::future::yield_now().await;
}
}
}
#[inline(always)]
pub async fn yield_now(&self) {
futures_lite::future::yield_now().await
}
#[inline(always)]
pub async fn yield_task_queue_now(&self) {
#[cfg(not(feature = "native-tls"))]
{
if LOCAL_EX.is_set() {
LOCAL_EX.with(|local_ex| {
local_ex.mark_me_for_yield();
})
}
futures_lite::future::yield_now().await;
}
#[cfg(feature = "native-tls")]
{
if let Some(local_ex) = unsafe { LOCAL_EX.as_ref() } {
local_ex.mark_me_for_yield();
}
futures_lite::future::yield_now().await;
}
}
#[inline(always)]
pub(crate) fn reactor(&self) -> Rc<reactor::Reactor> {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| local_ex.get_reactor());
#[cfg(feature = "native-tls")]
return unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.get_reactor()
};
}
pub fn id(&self) -> usize {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| local_ex.id());
#[cfg(feature = "native-tls")]
return unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.id()
};
}
pub fn create_task_queue(
&self,
shares: Shares,
latency: Latency,
name: &str,
) -> TaskQueueHandle {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| local_ex.create_task_queue(shares, latency, name));
#[cfg(feature = "native-tls")]
return unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.create_task_queue(shares, latency, name)
};
}
pub fn current_task_queue(&self) -> TaskQueueHandle {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| local_ex.current_task_queue());
#[cfg(feature = "native-tls")]
return unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.current_task_queue()
};
}
pub fn task_queue_stats(&self, handle: TaskQueueHandle) -> Result<TaskQueueStats> {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| match local_ex.get_queue(&handle) {
Some(x) => Ok(x.borrow_mut().stats.take()),
None => Err(GlommioError::queue_not_found(handle.index)),
});
#[cfg(feature = "native-tls")]
return match unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.get_queue(&handle)
} {
Some(x) => Ok(x.borrow_mut().stats.take()),
None => Err(GlommioError::queue_not_found(handle.index)),
};
}
pub fn all_task_queue_stats<V>(&self, mut output: V) -> V
where
V: Extend<TaskQueueStats>,
{
#[cfg(not(feature = "native-tls"))]
LOCAL_EX.with(|local_ex| {
output.extend(
local_ex
.queues
.borrow()
.available_executors
.values()
.map(|x| x.borrow_mut().stats.take()),
);
});
#[cfg(feature = "native-tls")]
output.extend(unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.queues
.borrow()
.available_executors
.values()
.map(|x| x.borrow_mut().stats.take())
});
output
}
pub fn executor_stats(&self) -> ExecutorStats {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| std::mem::take(&mut local_ex.queues.borrow_mut().stats));
#[cfg(feature = "native-tls")]
return std::mem::take(unsafe {
&mut LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.queues
.borrow_mut()
.stats
});
}
pub fn io_stats(&self) -> IoStats {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| local_ex.get_reactor().io_stats());
#[cfg(feature = "native-tls")]
return unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.get_reactor()
.io_stats()
};
}
pub fn task_queue_io_stats(&self, handle: TaskQueueHandle) -> Result<IoStats> {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| {
match local_ex.get_reactor().task_queue_io_stats(&handle) {
Some(x) => Ok(x),
None => Err(GlommioError::queue_not_found(handle.index)),
}
});
#[cfg(feature = "native-tls")]
return match unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.get_reactor()
.task_queue_io_stats(&handle)
} {
Some(x) => Ok(x),
None => Err(GlommioError::queue_not_found(handle.index)),
};
}
pub fn spawn_local<T>(&self, future: impl Future<Output = T> + 'static) -> Task<T>
where
T: 'static,
{
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| Task::<T>(local_ex.spawn(future)));
#[cfg(feature = "native-tls")]
return Task::<T>(unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.spawn(future)
});
}
pub fn spawn_local_into<T>(
&self,
future: impl Future<Output = T> + 'static,
handle: TaskQueueHandle,
) -> Result<Task<T>>
where
T: 'static,
{
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| local_ex.spawn_into(future, handle).map(Task::<T>));
#[cfg(feature = "native-tls")]
return unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.spawn_into(future, handle)
}
.map(Task::<T>);
}
pub unsafe fn spawn_scoped_local<'a, T>(
&self,
future: impl Future<Output = T> + 'a,
) -> ScopedTask<'a, T> {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| ScopedTask::<'a, T>(local_ex.spawn(future), PhantomData));
#[cfg(feature = "native-tls")]
return ScopedTask::<'a, T>(
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.spawn(future),
PhantomData,
);
}
pub unsafe fn spawn_scoped_local_into<'a, T>(
&self,
future: impl Future<Output = T> + 'a,
handle: TaskQueueHandle,
) -> Result<ScopedTask<'a, T>> {
#[cfg(not(feature = "native-tls"))]
return LOCAL_EX.with(|local_ex| {
local_ex
.spawn_into(future, handle)
.map(|x| ScopedTask::<'a, T>(x, PhantomData))
});
#[cfg(feature = "native-tls")]
return LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.spawn_into(future, handle)
.map(|x| ScopedTask::<'a, T>(x, PhantomData));
}
pub fn spawn_blocking<F, R>(&self, func: F) -> impl Future<Output = R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let result = Arc::new(Mutex::new(MaybeUninit::<R>::uninit()));
let f_inner = enclose::enclose!((result) move || {result.lock().unwrap().write(func());});
#[cfg(not(feature = "native-tls"))]
let waiter =
LOCAL_EX.with(move |local_ex| local_ex.reactor.run_blocking(Box::new(f_inner)));
#[cfg(feature = "native-tls")]
let waiter = unsafe {
LOCAL_EX
.as_ref()
.expect("this thread doesn't have a LocalExecutor running")
.reactor
.run_blocking(Box::new(f_inner))
};
async move {
let source = waiter.await;
assert!(source.collect_rw().await.is_ok());
unsafe {
let res_arc = Arc::try_unwrap(result).expect("leak");
let ret = std::mem::replace(
&mut *res_arc.lock().unwrap().deref_mut(),
MaybeUninit::<R>::uninit(),
)
.assume_init();
ret
}
}
}
}
#[cfg(test)]
mod test {
use core::mem::MaybeUninit;
use std::{
cell::Cell,
collections::HashMap,
sync::{
atomic::{AtomicUsize, Ordering},
Arc, Mutex,
},
task::Waker,
};
use futures::{
future::{join, join_all, poll_fn},
join,
};
use crate::{
enclose,
timer::{self, sleep, Timer},
SharesManager,
};
use super::*;
#[test]
fn create_and_destroy_executor() {
let mut var = Rc::new(RefCell::new(0));
let local_ex = LocalExecutor::default();
let varclone = var.clone();
local_ex.run(async move {
let mut m = varclone.borrow_mut();
*m += 10;
});
let v = Rc::get_mut(&mut var).unwrap();
let v = v.replace(0);
assert_eq!(v, 10);
}
#[test]
fn create_fail_to_bind() {
if LocalExecutorBuilder::new(Placement::Fixed(usize::MAX))
.make()
.is_ok()
{
unreachable!("Should have failed");
}
}
#[test]
fn bind_to_cpu_set_range() {
assert!(bind_to_cpu_set(vec![0, 1, 2, 3]).is_ok());
assert!(bind_to_cpu_set(0..1024).is_ok());
assert!(bind_to_cpu_set(0..1025).is_err());
}
#[test]
fn create_and_bind() {
if let Err(x) = LocalExecutorBuilder::new(Placement::Fixed(0)).make() {
panic!("got error {:?}", x);
}
}
#[test]
#[should_panic]
fn spawn_without_executor() {
let _ = LocalExecutor::default();
std::mem::drop(crate::spawn_local(async move {}));
}
#[test]
fn invalid_task_queue() {
let local_ex = LocalExecutor::default();
local_ex.run(async {
let task = crate::spawn_local_into(
async move {
unreachable!("Should not have executed this");
},
TaskQueueHandle { index: 1 },
);
if task.is_ok() {
unreachable!("Should have failed");
}
});
}
#[test]
fn ten_yielding_queues() {
let local_ex = LocalExecutor::default();
let executed_last = Rc::new(RefCell::new(0));
local_ex.run(async {
let mut joins = Vec::with_capacity(10);
for id in 1..11 {
let exec = executed_last.clone();
joins.push(crate::spawn_local(async move {
for _ in 0..10_000 {
{
let mut last = exec.borrow_mut();
assert_ne!(id, *last);
*last = id;
}
crate::executor().yield_task_queue_now().await;
}
}));
}
futures::future::join_all(joins).await;
});
}
#[test]
fn task_with_latency_requirements() {
let local_ex = LocalExecutor::default();
local_ex.run(async {
let not_latency = crate::executor().create_task_queue(
Shares::default(),
Latency::NotImportant,
"test",
);
let latency = crate::executor().create_task_queue(
Shares::default(),
Latency::Matters(Duration::from_millis(2)),
"testlat",
);
let nolat_started = Rc::new(RefCell::new(false));
let lat_status = Rc::new(RefCell::new(false));
let nolat = local_ex
.spawn_into(
crate::enclose! { (nolat_started, lat_status)
async move {
*(nolat_started.borrow_mut()) = true;
let start = Instant::now();
loop {
if *(lat_status.borrow()) {
break; }
if start.elapsed().as_secs() > 1 {
panic!("Never received preempt signal");
}
crate::yield_if_needed().await;
}
}
},
not_latency,
)
.unwrap();
let lat = local_ex
.spawn_into(
crate::enclose! { (nolat_started, lat_status)
async move {
loop {
if !(*(nolat_started.borrow())) {
crate::executor().yield_task_queue_now().await;
} else {
break;
}
}
*(lat_status.borrow_mut()) = true;
}
},
latency,
)
.unwrap();
futures::join!(nolat, lat);
});
}
#[test]
fn current_task_queue_matches() {
let local_ex = LocalExecutor::default();
local_ex.run(async {
let tq1 = crate::executor().create_task_queue(
Shares::default(),
Latency::NotImportant,
"test1",
);
let tq2 = crate::executor().create_task_queue(
Shares::default(),
Latency::NotImportant,
"test2",
);
let id1 = tq1.index;
let id2 = tq2.index;
let j0 = crate::spawn_local(async {
assert_eq!(crate::executor().current_task_queue().index, 0);
});
let j1 = crate::spawn_local_into(
async move {
assert_eq!(crate::executor().current_task_queue().index, id1);
},
tq1,
)
.unwrap();
let j2 = crate::spawn_local_into(
async move {
assert_eq!(crate::executor().current_task_queue().index, id2);
},
tq2,
)
.unwrap();
futures::join!(j0, j1, j2);
})
}
#[test]
fn task_optimized_for_throughput() {
let local_ex = LocalExecutor::default();
local_ex.run(async {
let tq1 = crate::executor().create_task_queue(
Shares::default(),
Latency::NotImportant,
"test",
);
let tq2 = crate::executor().create_task_queue(
Shares::default(),
Latency::NotImportant,
"testlat",
);
let first_started = Rc::new(RefCell::new(0));
let second_status = Rc::new(RefCell::new(0));
let first = local_ex
.spawn_into(
crate::enclose! { (first_started, second_status)
async move {
let start = Instant::now();
loop {
{
let mut count = first_started.borrow_mut();
*count += 1;
if start.elapsed().as_millis() >= 99 {
break;
}
if *count < *(second_status.borrow()) {
panic!("I was preempted but should not have been");
}
}
crate::yield_if_needed().await;
}
}
},
tq1,
)
.unwrap();
let second = local_ex
.spawn_into(
crate::enclose! { (first_started, second_status)
async move {
loop {
{
let mut count = second_status.borrow_mut();
*count += 1;
if *count < *(first_started.borrow()) {
break;
}
}
crate::executor().yield_task_queue_now().await;
}
}
},
tq2,
)
.unwrap();
futures::join!(first, second);
});
}
#[test]
fn test_detach() {
let ex = LocalExecutor::default();
ex.run(async {
crate::spawn_local(async {
loop {
crate::executor().yield_task_queue_now().await;
}
})
.detach();
Timer::new(Duration::from_micros(100)).await;
});
}
fn from_timeval(v: libc::timeval) -> Duration {
Duration::from_secs(v.tv_sec as u64) + Duration::from_micros(v.tv_usec as u64)
}
fn getrusage() -> Duration {
let mut s0 = MaybeUninit::<libc::rusage>::uninit();
let err = unsafe { libc::getrusage(libc::RUSAGE_THREAD, s0.as_mut_ptr()) };
if err != 0 {
panic!("getrusage error = {}", err);
}
let usage = unsafe { s0.assume_init() };
from_timeval(usage.ru_utime) + from_timeval(usage.ru_stime)
}
#[test]
fn test_no_spin() {
let ex = LocalExecutor::default();
let task_queue = ex.create_task_queue(
Shares::default(),
Latency::Matters(Duration::from_millis(10)),
"my_tq",
);
let start = getrusage();
ex.run(async {
crate::spawn_local_into(
async { timer::sleep(Duration::from_secs(1)).await },
task_queue,
)
.expect("failed to spawn task")
.await;
});
assert!(
getrusage() - start < Duration::from_millis(10),
"expected user time on LE is less than 10 millisecond"
);
}
#[test]
fn test_spin() {
let dur = Duration::from_secs(1);
let ex0 = LocalExecutorBuilder::default().make().unwrap();
ex0.run(async {
let ex0_ru_start = getrusage();
timer::sleep(dur).await;
let ex0_ru_finish = getrusage();
assert!(
ex0_ru_finish - ex0_ru_start < Duration::from_millis(10),
"expected user time on LE0 is less than 10 millisecond"
);
});
let ex = LocalExecutorBuilder::new(Placement::Fixed(0))
.spin_before_park(Duration::from_millis(100))
.make()
.unwrap();
ex.run(async {
let ex_ru_start = getrusage();
timer::sleep(dur).await;
let ex_ru_finish = getrusage();
assert!(
ex_ru_finish - ex_ru_start >= Duration::from_millis(50),
"expected user time on LE is much greater than 50 millisecond",
);
});
}
#[test]
fn test_runtime_stats() {
let dur = Duration::from_secs(2);
let ex0 = LocalExecutorBuilder::default().make().unwrap();
ex0.run(async {
assert!(
crate::executor().executor_stats().total_runtime() < Duration::from_nanos(10),
"expected runtime on LE {:#?} is less than 10 ns",
crate::executor().executor_stats().total_runtime()
);
let now = Instant::now();
while now.elapsed().as_millis() < 200 {}
crate::executor().yield_task_queue_now().await;
assert!(
crate::executor().executor_stats().total_runtime() >= Duration::from_millis(200),
"expected runtime on LE0 {:#?} is greater than 200 ms",
crate::executor().executor_stats().total_runtime()
);
timer::sleep(dur).await;
assert!(
crate::executor().executor_stats().total_runtime() < Duration::from_millis(400),
"expected runtime on LE0 {:#?} is not greater than 400 ms",
crate::executor().executor_stats().total_runtime()
);
});
let ex = LocalExecutorBuilder::new(Placement::Fixed(0))
.spin_before_park(Duration::from_secs(5))
.make()
.unwrap();
ex.run(async {
crate::spawn_local(async move {
assert!(
crate::executor().executor_stats().total_runtime() < Duration::from_nanos(10),
"expected runtime on LE {:#?} is less than 10 ns",
crate::executor().executor_stats().total_runtime()
);
let now = Instant::now();
while now.elapsed().as_millis() < 200 {}
crate::executor().yield_task_queue_now().await;
assert!(
crate::executor().executor_stats().total_runtime()
>= Duration::from_millis(200),
"expected runtime on LE {:#?} is greater than 200 ms",
crate::executor().executor_stats().total_runtime()
);
timer::sleep(dur).await;
assert!(
crate::executor().executor_stats().total_runtime() < Duration::from_millis(400),
"expected runtime on LE {:#?} is not greater than 400 ms",
crate::executor().executor_stats().total_runtime()
);
})
.await;
});
}
async fn work_quanta() {
let now = Instant::now();
while now.elapsed().as_millis() < 2 {}
crate::executor().yield_task_queue_now().await;
}
macro_rules! test_static_shares {
( $s1:expr, $s2:expr, $work:block ) => {
let local_ex = LocalExecutor::default();
local_ex.run(async {
let tq1 = crate::executor().create_task_queue(
Shares::Static($s1),
Latency::Matters(Duration::from_millis(1)),
"test_1",
);
let tq2 = crate::executor().create_task_queue(
Shares::Static($s2),
Latency::Matters(Duration::from_millis(1)),
"test_2",
);
let tq1_count = Rc::new(Cell::new(0));
let tq2_count = Rc::new(Cell::new(0));
let now = Instant::now();
let t1 = crate::spawn_local_into(
enclose! { (tq1_count, now) async move {
while now.elapsed().as_secs() < 5 {
$work;
tq1_count.replace(tq1_count.get() + 1);
}
}},
tq1,
)
.unwrap();
let t2 = crate::spawn_local_into(
enclose! { (tq2_count, now ) async move {
while now.elapsed().as_secs() < 5 {
$work;
tq2_count.replace(tq2_count.get() + 1);
}
}},
tq2,
)
.unwrap();
join!(t1, t2);
let expected_ratio = $s2 as f64 / (($s2 + $s1) as f64);
let actual_ratio =
tq2_count.get() as f64 / ((tq1_count.get() + tq2_count.get()) as f64);
assert!((expected_ratio - actual_ratio).abs() < 0.1);
});
};
}
#[test]
fn test_shares_high_disparity_fat_task() {
test_static_shares!(1000, 10, { work_quanta().await });
}
#[test]
fn test_shares_low_disparity_fat_task() {
test_static_shares!(1000, 1000, { work_quanta().await });
}
#[test]
fn test_allocate_dma_buffer() {
LocalExecutor::default().run(async {
let mut buffer = crate::allocate_dma_buffer(42);
assert_eq!(buffer.len(), 42);
buffer.as_bytes_mut()[0] = 12;
buffer.as_bytes_mut()[12] = 13;
assert_eq!(buffer.as_bytes_mut().len(), 42);
assert_eq!(buffer.as_bytes()[0], 12);
assert_eq!(buffer.as_bytes()[12], 13);
});
}
#[test]
fn test_allocate_dma_buffer_global() {
LocalExecutor::default().run(async {
let mut buffer = crate::allocate_dma_buffer_global(42);
assert_eq!(buffer.len(), 42);
assert_eq!(buffer.len(), 42);
buffer.as_bytes_mut()[0] = 12;
buffer.as_bytes_mut()[12] = 13;
assert_eq!(buffer.as_bytes_mut().len(), 42);
assert_eq!(buffer.as_bytes()[0], 12);
assert_eq!(buffer.as_bytes()[12], 13);
});
}
struct DynamicSharesTest {
shares: Cell<usize>,
}
impl DynamicSharesTest {
fn new() -> Rc<Self> {
Rc::new(Self {
shares: Cell::new(0),
})
}
fn tick(&self, millis: u64) {
if millis < 1000 {
self.shares.replace(1);
} else {
self.shares.replace(1000);
}
}
}
impl SharesManager for DynamicSharesTest {
fn shares(&self) -> usize {
self.shares.get()
}
fn adjustment_period(&self) -> Duration {
Duration::from_millis(1)
}
}
#[test]
fn test_dynamic_shares() {
let local_ex = LocalExecutor::default();
local_ex.run(async {
let bm = DynamicSharesTest::new();
let tq1 = crate::executor().create_task_queue(
Shares::Static(1000),
Latency::Matters(Duration::from_millis(1)),
"test_1",
);
let tq2 = crate::executor().create_task_queue(
Shares::Dynamic(bm.clone()),
Latency::Matters(Duration::from_millis(1)),
"test_2",
);
let tq1_count = Rc::new(RefCell::new(vec![0, 0]));
let tq2_count = Rc::new(RefCell::new(vec![0, 0]));
let now = Instant::now();
let t1 = crate::spawn_local_into(
enclose! { (tq1_count, now) async move {
loop {
let secs = now.elapsed().as_secs();
if secs >= 2 {
break;
}
(*tq1_count.borrow_mut())[secs as usize] += 1;
crate::executor().yield_task_queue_now().await;
}
}},
tq1,
)
.unwrap();
let t2 = crate::spawn_local_into(
enclose! { (tq2_count, now, bm) async move {
loop {
let elapsed = now.elapsed();
let secs = elapsed.as_secs();
if secs >= 2 {
break;
}
bm.tick(elapsed.as_millis() as u64);
(*tq2_count.borrow_mut())[secs as usize] += 1;
crate::executor().yield_task_queue_now().await;
}
}},
tq2,
)
.unwrap();
join!(t1, t2);
let ratios: Vec<f64> = tq1_count
.borrow()
.iter()
.zip(tq2_count.borrow().iter())
.map(|(x, y)| *y as f64 / *x as f64)
.collect();
assert!(ratios[1] > ratios[0]);
assert!(ratios[0] < 0.25);
assert!(ratios[1] > 0.50);
});
}
#[test]
fn multiple_spawn() {
LocalExecutor::default().run(async {
crate::spawn_local(async {}).detach().await;
crate::spawn_local(async {}).detach().await;
});
}
#[test]
#[should_panic(expected = "Message!")]
fn panic_is_not_list() {
LocalExecutor::default().run(async { panic!("Message!") });
}
struct TestFuture {
w: Arc<Mutex<Option<Waker>>>,
}
impl Future for TestFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut w = self.w.lock().unwrap();
match w.take() {
Some(_) => Poll::Ready(()),
None => {
*w = Some(cx.waker().clone());
Poll::Pending
}
}
}
}
#[test]
fn cross_executor_wake_by_ref() {
let w = Arc::new(Mutex::new(None));
let t = w.clone();
let fut = TestFuture { w };
let ex1 = LocalExecutorBuilder::default()
.spawn(|| async move {
fut.await;
})
.unwrap();
let ex2 = LocalExecutorBuilder::default()
.spawn(|| async move {
loop {
sleep(Duration::from_secs(1)).await;
let w = t.lock().unwrap();
if let Some(ref x) = *w {
x.wake_by_ref();
return;
}
}
})
.unwrap();
ex1.join().unwrap();
ex2.join().unwrap();
}
#[test]
fn cross_executor_wake_by_value() {
let w = Arc::new(Mutex::new(None));
let t = w.clone();
let fut = TestFuture { w };
let ex1 = LocalExecutorBuilder::default()
.spawn(|| async move {
fut.await;
})
.unwrap();
let ex2 = LocalExecutorBuilder::default()
.spawn(|| async move {
loop {
sleep(Duration::from_secs(1)).await;
let w = t.lock().unwrap();
if let Some(x) = w.clone() {
x.wake();
return;
}
}
})
.unwrap();
ex1.join().unwrap();
ex2.join().unwrap();
}
#[test]
fn cross_executor_wake_with_join_handle() {
let w = Arc::new(Mutex::new(None));
let t = w.clone();
let fut = TestFuture { w };
let ex1 = LocalExecutorBuilder::default()
.spawn(|| async move {
let x = crate::spawn_local(fut).detach();
x.await;
})
.unwrap();
let ex2 = LocalExecutorBuilder::default()
.spawn(|| async move {
loop {
sleep(Duration::from_secs(1)).await;
let w = t.lock().unwrap();
if let Some(x) = w.clone() {
x.wake();
return;
}
}
})
.unwrap();
ex1.join().unwrap();
ex2.join().unwrap();
}
#[test]
fn cross_executor_wake_early_drop() {
let w = Arc::new(Mutex::new(None));
let t = w.clone();
let fut = TestFuture { w };
let ex1 = LocalExecutorBuilder::default()
.spawn(|| async move {
let _drop = futures_lite::future::poll_once(fut).await;
})
.unwrap();
let ex2 = LocalExecutorBuilder::default()
.spawn(|| async move {
loop {
sleep(Duration::from_secs(1)).await;
let w = t.lock().unwrap();
if let Some(ref x) = *w {
x.wake_by_ref();
return;
}
}
})
.unwrap();
ex1.join().unwrap();
ex2.join().unwrap();
}
#[test]
fn cross_executor_wake_hold_waker() {
let w = Arc::new(Mutex::new(None));
let t = w.clone();
let fut = TestFuture { w };
let ex1 = LocalExecutorBuilder::default()
.spawn(|| async move {
let _drop = futures_lite::future::poll_once(fut).await;
})
.unwrap();
ex1.join().unwrap();
let ex2 = LocalExecutorBuilder::default()
.spawn(|| async move {
let w = t.lock().unwrap().clone().unwrap();
w.wake_by_ref();
})
.unwrap();
ex2.join().unwrap();
}
#[test]
fn executor_pool_builder() {
let nr_cpus = 4;
let count = Arc::new(AtomicUsize::new(0));
let handles = LocalExecutorPoolBuilder::new(PoolPlacement::Unbound(nr_cpus))
.on_all_shards({
let count = Arc::clone(&count);
|| async move { count.fetch_add(1, Ordering::Relaxed) }
})
.unwrap();
let _: std::thread::ThreadId = handles.handles[0].thread().id();
assert_eq!(nr_cpus, handles.handles().iter().count());
let mut fut_output = handles
.join_all()
.into_iter()
.map(Result::unwrap)
.collect::<Vec<_>>();
fut_output.sort_unstable();
assert_eq!(fut_output, (0..nr_cpus).collect::<Vec<_>>());
assert_eq!(nr_cpus, count.load(Ordering::Relaxed));
}
#[test]
fn executor_invalid_executor_count() {
assert!(LocalExecutorPoolBuilder::new(PoolPlacement::Unbound(0))
.on_all_shards(|| async move {})
.is_err());
}
#[test]
fn executor_pool_builder_placements() {
let cpu_set = CpuSet::online().unwrap();
assert!(!cpu_set.is_empty());
for nn in 1..2 {
let nr_execs = nn * cpu_set.len();
let mut placements = vec![
PoolPlacement::Unbound(nr_execs),
PoolPlacement::Fenced(nr_execs, cpu_set.clone()),
PoolPlacement::MaxSpread(nr_execs, None),
PoolPlacement::MaxSpread(nr_execs, Some(cpu_set.clone())),
PoolPlacement::MaxPack(nr_execs, None),
PoolPlacement::MaxPack(nr_execs, Some(cpu_set.clone())),
];
for pp in placements.drain(..) {
let ids = Arc::new(Mutex::new(HashMap::new()));
let cpus = Arc::new(Mutex::new(HashMap::new()));
let cpu_hard_bind =
!matches!(pp, PoolPlacement::Unbound(_) | PoolPlacement::Fenced(_, _));
let handles = LocalExecutorPoolBuilder::new(pp)
.on_all_shards({
let ids = Arc::clone(&ids);
let cpus = Arc::clone(&cpus);
|| async move {
ids.lock()
.unwrap()
.entry(crate::executor().id())
.and_modify(|e| *e += 1)
.or_insert(1);
let pid = nix::unistd::Pid::from_raw(0);
let cpu = nix::sched::sched_getaffinity(pid).unwrap();
cpus.lock()
.unwrap()
.entry(cpu)
.and_modify(|e| *e += 1)
.or_insert(1);
}
})
.unwrap();
assert_eq!(nr_execs, handles.handles().len());
handles
.join_all()
.into_iter()
.for_each(|r| assert!(r.is_ok()));
assert_eq!(nr_execs, ids.lock().unwrap().len());
ids.lock().unwrap().values().for_each(|v| assert_eq!(*v, 1));
if cpu_hard_bind {
assert_eq!(nr_execs, cpus.lock().unwrap().len());
cpus.lock()
.unwrap()
.values()
.for_each(|v| assert_eq!(*v, nn));
}
}
}
}
#[test]
fn executor_pool_builder_shards_limit() {
let cpu_set = CpuSet::online().unwrap();
assert!(!cpu_set.is_empty());
{
let mut placements = vec![
(false, PoolPlacement::Unbound(cpu_set.len())),
(false, PoolPlacement::Fenced(cpu_set.len(), cpu_set.clone())),
(true, PoolPlacement::MaxSpread(cpu_set.len(), None)),
(
true,
PoolPlacement::MaxSpread(cpu_set.len(), Some(cpu_set.clone())),
),
(true, PoolPlacement::MaxPack(cpu_set.len(), None)),
(
true,
PoolPlacement::MaxPack(cpu_set.len(), Some(cpu_set.clone())),
),
];
for (_shard_limited, p) in placements.drain(..) {
LocalExecutorPoolBuilder::new(p)
.on_all_shards(|| async move {})
.unwrap()
.join_all();
}
}
{
let mut placements = vec![
(false, PoolPlacement::Unbound(1 + cpu_set.len())),
(
false,
PoolPlacement::Fenced(1 + cpu_set.len(), cpu_set.clone()),
),
(true, PoolPlacement::MaxSpread(1 + cpu_set.len(), None)),
(
true,
PoolPlacement::MaxSpread(1 + cpu_set.len(), Some(cpu_set.clone())),
),
(true, PoolPlacement::MaxPack(1 + cpu_set.len(), None)),
(
true,
PoolPlacement::MaxPack(1 + cpu_set.len(), Some(cpu_set)),
),
];
for (shard_limited, p) in placements.drain(..) {
match LocalExecutorPoolBuilder::new(p).on_all_shards(|| async move {}) {
Ok(handles) => {
handles.join_all();
assert!(!shard_limited);
}
Err(_) => assert!(shard_limited),
}
}
}
}
#[test]
fn scoped_task() {
LocalExecutor::default().run(async {
let mut a = 1;
unsafe {
crate::spawn_scoped_local(async {
a = 2;
})
.await;
}
crate::executor().yield_task_queue_now().await;
assert_eq!(a, 2);
let mut a = 1;
let do_later = unsafe {
crate::spawn_scoped_local(async {
a = 2;
})
};
crate::executor().yield_task_queue_now().await;
do_later.await;
assert_eq!(a, 2);
});
}
#[test]
fn executor_pool_builder_thread_panic() {
let nr_execs = 8;
let res = LocalExecutorPoolBuilder::new(PoolPlacement::Unbound(nr_execs))
.on_all_shards(|| async move { panic!("join handle will be Err") })
.unwrap()
.join_all();
assert_eq!(nr_execs, res.len());
assert!(res.into_iter().all(|r| r.is_err()));
}
#[test]
fn executor_pool_builder_return_values() {
let nr_execs = 8;
let x = Arc::new(AtomicUsize::new(0));
let mut values = LocalExecutorPoolBuilder::new(PoolPlacement::Unbound(nr_execs))
.on_all_shards(|| async move { x.fetch_add(1, Ordering::Relaxed) })
.unwrap()
.join_all()
.into_iter()
.map(Result::unwrap)
.collect::<Vec<_>>();
values.sort_unstable();
assert_eq!(values, (0..nr_execs).collect::<Vec<_>>());
}
#[test]
fn executor_pool_builder_spawn_cancel() {
let nr_shards = 8;
let builder = LocalExecutorPoolBuilder::new(PoolPlacement::Unbound(nr_shards));
let nr_exectuted = Arc::new(AtomicUsize::new(0));
let fut_gen = {
let nr_exectuted = Arc::clone(&nr_exectuted);
|| async move {
nr_exectuted.fetch_add(1, Ordering::Relaxed);
unreachable!("should not execute")
}
};
let mut handles = PoolThreadHandles::new();
let mut cpu_set_gen = placement::CpuSetGenerator::pool(builder.placement.clone()).unwrap();
let latch = Latch::new(builder.placement.executor_count());
let ii_cxl = 2;
for ii in 0..builder.placement.executor_count() {
if ii == nr_shards - ii_cxl {
std::thread::sleep(std::time::Duration::from_millis(100));
assert!(ii_cxl <= latch.cancel().unwrap());
}
match builder.spawn_thread(&mut cpu_set_gen, &latch, fut_gen.clone()) {
Ok(handle) => handles.push(handle),
Err(_) => break,
}
}
assert_eq!(0, nr_exectuted.load(Ordering::Relaxed));
assert_eq!(nr_shards, handles.handles.len());
handles.join_all().into_iter().for_each(|s| {
assert!(format!("{}", s.unwrap_err()).contains("spawn failed"));
});
}
#[should_panic]
#[test]
fn executor_inception() {
LocalExecutor::default().run(async {
LocalExecutor::default().run(async {});
});
}
enum TaskState {
Pending(Option<Waker>),
Ready,
}
#[test]
fn wake_by_ref_refcount_underflow_with_join_handle() {
LocalExecutor::default().run(async {
let slot: Rc<RefCell<TaskState>> = Rc::new(RefCell::new(TaskState::Pending(None)));
let cloned_slot = slot.clone();
let jh = crate::spawn_local(async move {
poll_fn::<(), _>(|cx| {
let current = &mut *cloned_slot.borrow_mut();
match current {
TaskState::Pending(maybe_waker) => match maybe_waker {
Some(_) => unreachable!(),
None => {
*current = TaskState::Pending(Some(cx.waker().clone()));
Poll::Pending
}
},
TaskState::Ready => Poll::Ready(()),
}
})
.await;
})
.detach();
let jh2 = crate::spawn_local(async move {
let current = &mut *slot.borrow_mut();
match current {
TaskState::Pending(maybe_waker) => {
let waker = maybe_waker.take().unwrap();
waker.wake_by_ref();
*current = TaskState::Ready; }
TaskState::Ready => unreachable!(), }
})
.detach();
join_all(vec![jh, jh2]).await;
});
}
#[test]
fn wake_by_ref_refcount_underflow_with_sleep() {
LocalExecutor::default().run(async {
let slot: Rc<RefCell<TaskState>> = Rc::new(RefCell::new(TaskState::Pending(None)));
let cloned_slot = slot.clone();
crate::spawn_local(async move {
poll_fn::<(), _>(|cx| {
let current = &mut *cloned_slot.borrow_mut();
match current {
TaskState::Pending(maybe_waker) => match maybe_waker {
Some(_) => unreachable!(),
None => {
*current = TaskState::Pending(Some(cx.waker().clone()));
Poll::Pending
}
},
TaskState::Ready => Poll::Ready(()),
}
})
.await;
})
.detach();
crate::spawn_local(async move {
let current = &mut *slot.borrow_mut();
match current {
TaskState::Pending(maybe_waker) => {
let waker = maybe_waker.take().unwrap();
waker.wake_by_ref();
*current = TaskState::Ready;
}
TaskState::Ready => unreachable!(),
}
})
.detach();
timer::sleep(Duration::from_millis(1)).await;
});
}
#[test]
fn wake_refcount_underflow_with_join_handle() {
LocalExecutor::default().run(async {
let slot: Rc<RefCell<TaskState>> = Rc::new(RefCell::new(TaskState::Pending(None)));
let cloned_slot = slot.clone();
let jh = crate::spawn_local(async move {
poll_fn::<(), _>(|cx| {
let current = &mut *cloned_slot.borrow_mut();
match current {
TaskState::Pending(maybe_waker) => match maybe_waker {
Some(_) => unreachable!(),
None => {
*current = TaskState::Pending(Some(cx.waker().clone()));
Poll::Pending
}
},
TaskState::Ready => Poll::Ready(()),
}
})
.await;
})
.detach();
let jh2 = crate::spawn_local(async move {
let current = &mut *slot.borrow_mut();
match current {
TaskState::Pending(maybe_waker) => {
let waker = maybe_waker.take().unwrap();
waker.wake();
*current = TaskState::Ready;
}
TaskState::Ready => unreachable!(),
}
})
.detach();
join_all(vec![jh, jh2]).await;
});
}
#[test]
fn wake_refcount_underflow_with_sleep() {
LocalExecutor::default().run(async {
let slot: Rc<RefCell<TaskState>> = Rc::new(RefCell::new(TaskState::Pending(None)));
let cloned_slot = slot.clone();
crate::spawn_local(async move {
poll_fn::<(), _>(|cx| {
let current = &mut *cloned_slot.borrow_mut();
match current {
TaskState::Pending(maybe_waker) => match maybe_waker {
Some(_) => unreachable!(),
None => {
*current = TaskState::Pending(Some(cx.waker().clone()));
Poll::Pending
}
},
TaskState::Ready => Poll::Ready(()),
}
})
.await;
})
.detach();
crate::spawn_local(async move {
let current = &mut *slot.borrow_mut();
match current {
TaskState::Pending(maybe_waker) => {
let waker = maybe_waker.take().unwrap();
waker.wake();
*current = TaskState::Ready;
}
TaskState::Ready => unreachable!(),
}
})
.detach();
timer::sleep(Duration::from_millis(1)).await;
});
}
#[test]
fn blocking_function() {
LocalExecutor::default().run(async {
let started = Instant::now();
let blocking = executor().spawn_blocking(enclose!((started) move || {
let now = Instant::now();
while now.elapsed() < Duration::from_millis(100) {}
started.elapsed()
}));
let coop = enclose!((started) async move {
let now = Instant::now();
while now.elapsed() < Duration::from_millis(100) {
yield_if_needed().await;
}
started.elapsed()
});
let (blocking, coop) = join(blocking, coop).await;
assert!(blocking.as_millis() >= 100 && blocking.as_millis() < 150);
assert!(coop.as_millis() >= 100 && coop.as_millis() < 150);
});
}
#[test]
fn blocking_function_parallelism() {
LocalExecutorBuilder::new(Placement::Unbound)
.blocking_thread_pool_placement(PoolPlacement::Unbound(4))
.spawn(|| async {
let started = Instant::now();
let mut blocking = vec![];
for _ in 0..5 {
blocking.push(executor().spawn_blocking(enclose!((started) move || {
let now = Instant::now();
while now.elapsed() < Duration::from_millis(100) {}
started.elapsed()
})));
}
let mut ts = join_all(blocking.into_iter()).await;
assert_eq!(ts.len(), 5);
ts.sort_unstable();
for ts in ts.iter().take(4) {
assert!(ts.as_millis() >= 100 && ts.as_millis() < 150);
}
assert!(ts[4].as_millis() >= 200 && ts[4].as_millis() < 250);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn blocking_function_placement_independent_of_executor_placement() {
let affinity = nix::sched::sched_getaffinity(nix::unistd::Pid::from_raw(0)).unwrap();
let num_cpus_accessible_by_default = (0..nix::sched::CpuSet::count())
.map(|cpu| affinity.is_set(cpu).unwrap() as usize)
.sum::<usize>();
if num_cpus_accessible_by_default < 2 {
eprintln!(
"Insufficient CPUs available to test blocking_function_placement_independent_of_executor_placement (affinity only allows for {})",
num_cpus_accessible_by_default,
);
return;
}
let num_schedulable_cpus = LocalExecutorBuilder::new(Placement::Fixed(0))
.blocking_thread_pool_placement(PoolPlacement::Unbound(2))
.spawn(|| async {
executor()
.spawn_blocking(move || {
let pid = nix::unistd::Pid::from_raw(0);
let affinity =
nix::sched::sched_getaffinity(pid).expect("Failed to get affinity");
(0..nix::sched::CpuSet::count())
.map(|cpu| {
affinity
.is_set(cpu)
.expect("Failed to check if cpu affinity is set")
as usize
})
.sum::<usize>()
})
.await
})
.unwrap()
.join()
.unwrap();
assert!(
num_schedulable_cpus >= num_cpus_accessible_by_default,
"num schedulable {}, num cpus accessible {}",
num_schedulable_cpus,
num_cpus_accessible_by_default,
);
}
#[test]
fn blocking_pool_invalid_placement() {
let ret = LocalExecutorBuilder::new(Placement::Unbound)
.blocking_thread_pool_placement(PoolPlacement::Unbound(0))
.spawn(|| async {})
.unwrap()
.join();
assert!(ret.is_err());
}
#[test]
fn local_executor_unset() {
LocalExecutor::default().run(async {});
#[cfg(not(feature = "native-tls"))]
assert!(!LOCAL_EX.is_set());
#[cfg(feature = "native-tls")]
assert!(unsafe { LOCAL_EX.is_null() });
}
#[test]
fn local_executor_unset_when_panic() {
let res = std::panic::catch_unwind(|| {
LocalExecutor::default().run(async {
panic!("uh oh!");
});
});
assert!(res.is_err());
#[cfg(not(feature = "native-tls"))]
assert!(!LOCAL_EX.is_set());
#[cfg(feature = "native-tls")]
assert!(unsafe { LOCAL_EX.is_null() });
}
}