use std::{
cell::UnsafeCell,
mem::MaybeUninit,
ptr::NonNull,
sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering},
time::{Duration, Instant},
};
use moirai_core::Priority;
use super::super::task::TaskMetadata;
use super::fan_out;
pub(crate) const PRIORITY_FROM_INDEX: [Priority; Priority::Critical.index() + 1] = [
Priority::Low,
Priority::Normal,
Priority::High,
Priority::Critical,
];
pub(crate) const NO_WORKER: u32 = u32::MAX;
pub(crate) const TIMESTAMP_NOT_RECORDED: u64 = u64::MAX;
pub(crate) const TASK_STATE_BLOCK_SIZE: usize = 1024;
#[derive(Debug, Clone, Copy)]
pub(super) enum Retirement {
CompletedBefore(Instant),
Forced,
}
pub(super) struct TaskStateBlock {
published: Box<[AtomicBool]>,
states: Box<[UnsafeCell<MaybeUninit<TaskState>>]>,
}
unsafe impl Sync for TaskStateBlock {}
pub(super) struct WakerSlot {
locked: AtomicBool,
slot: UnsafeCell<Option<std::task::Waker>>,
}
impl WakerSlot {
const fn new() -> Self {
Self {
locked: AtomicBool::new(false),
slot: UnsafeCell::new(None),
}
}
fn with_slot<R>(&self, body: impl FnOnce(&mut Option<std::task::Waker>) -> R) -> R {
let mut spins = 0_usize;
while self
.locked
.compare_exchange_weak(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
if spins < 16 {
std::hint::spin_loop();
} else {
std::thread::yield_now();
}
spins += 1;
}
let slot = unsafe { &mut *self.slot.get() };
let result = body(slot);
self.locked.store(false, Ordering::Release);
result
}
}
unsafe impl Send for WakerSlot {}
unsafe impl Sync for WakerSlot {}
pub(crate) struct TaskState {
pub(crate) created_at: Instant,
pub(super) started_after_ns: AtomicU64,
pub(super) completed_after_ns: AtomicU64,
pub(super) worker_id: AtomicU32,
pub(super) waker: WakerSlot,
token_active: AtomicBool,
pub(super) priority: AtomicU8,
pub(super) cancel_requested: AtomicBool,
pub(super) cancelled: AtomicBool,
}
const _: () = assert!(size_of::<TaskState>() <= 64);
impl std::fmt::Debug for TaskState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TaskState")
.field("created_at", &self.created_at)
.field("started_after_ns", &self.started_after_ns)
.field("completed_after_ns", &self.completed_after_ns)
.field("worker_id", &self.worker_id)
.field(
"waker_registered",
&self.waker.with_slot(|slot| slot.is_some()),
)
.finish()
}
}
impl TaskState {
#[inline]
pub(super) fn new() -> Self {
Self {
created_at: Instant::now(),
started_after_ns: AtomicU64::new(TIMESTAMP_NOT_RECORDED),
completed_after_ns: AtomicU64::new(TIMESTAMP_NOT_RECORDED),
worker_id: AtomicU32::new(NO_WORKER),
waker: WakerSlot::new(),
token_active: AtomicBool::new(true),
priority: AtomicU8::new(Priority::Normal as u8),
cancel_requested: AtomicBool::new(false),
cancelled: AtomicBool::new(false),
}
}
#[inline]
pub(super) fn set_priority(&self, priority: Priority) {
self.priority.store(priority as u8, Ordering::Relaxed);
}
#[inline]
pub(super) fn priority(&self) -> Priority {
PRIORITY_FROM_INDEX[usize::from(self.priority.load(Ordering::Relaxed))]
}
#[inline]
pub(super) fn request_cancel(&self) {
self.cancel_requested.store(true, Ordering::Release);
}
#[inline]
pub(super) fn cancel_requested(&self) -> bool {
self.cancel_requested.load(Ordering::Acquire)
}
#[inline]
pub(super) fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
}
#[inline]
pub(super) fn token_active(&self) -> bool {
self.token_active.load(Ordering::Acquire)
}
#[inline]
pub(super) unsafe fn retire_token(state: NonNull<Self>) {
let flag = unsafe { &raw const (*state.as_ptr()).token_active };
unsafe { (*flag).store(false, Ordering::Release) };
}
pub(super) fn mark_cancelled(&self) {
self.cancelled.store(true, Ordering::Release);
self.mark_completed();
}
#[inline]
pub(super) fn mark_started(&self, worker_id: usize) -> u64 {
let started_after_ns = elapsed_nanos_since(self.created_at);
self.started_after_ns
.store(started_after_ns, Ordering::Release);
self.worker_id.store(
u32::try_from(worker_id).unwrap_or(NO_WORKER),
Ordering::Release,
);
started_after_ns
}
#[inline]
pub(super) fn mark_completed_since(&self, started_after_ns: u64) -> Duration {
let completed_after_ns = elapsed_nanos_since(self.created_at).max(started_after_ns);
self.completed_after_ns
.store(completed_after_ns, Ordering::Release);
let waker = self.waker.with_slot(|slot| slot.take());
if let Some(waker) = waker {
waker.wake();
}
Duration::from_nanos(completed_after_ns - started_after_ns)
}
pub(super) fn mark_completed(&self) {
let started_after_ns = self.started_after_ns.load(Ordering::Acquire);
let started_after_ns = if started_after_ns == TIMESTAMP_NOT_RECORDED {
elapsed_nanos_since(self.created_at)
} else {
started_after_ns
};
self.mark_completed_since(started_after_ns);
}
pub(super) fn register_waker(&self, waker: &std::task::Waker) {
self.waker.with_slot(|slot| fan_out::register(slot, waker));
if self.is_completed() {
let stranded = self.waker.with_slot(|slot| slot.take());
if let Some(stranded) = stranded {
stranded.wake();
}
}
}
pub(super) fn is_completed(&self) -> bool {
self.completed_after_ns.load(Ordering::Acquire) != TIMESTAMP_NOT_RECORDED
}
pub(super) fn completed_at(&self) -> Option<Instant> {
instant_from_offset(
self.created_at,
self.completed_after_ns.load(Ordering::Acquire),
)
}
pub(super) fn snapshot(&self, id: u64) -> TaskMetadata {
let worker_id = match self.worker_id.load(Ordering::Acquire) {
NO_WORKER => None,
worker_id => Some(worker_id as usize),
};
TaskMetadata {
id,
created_at: self.created_at,
started_at: instant_from_offset(
self.created_at,
self.started_after_ns.load(Ordering::Acquire),
),
completed_at: self.completed_at(),
worker_id,
priority: self.priority(),
cancelled: self.is_cancelled(),
}
}
}
impl TaskStateBlock {
pub(super) fn new() -> Self {
let published = std::iter::repeat_with(|| AtomicBool::new(false))
.take(TASK_STATE_BLOCK_SIZE)
.collect();
let states = std::iter::repeat_with(|| UnsafeCell::new(MaybeUninit::uninit()))
.take(TASK_STATE_BLOCK_SIZE)
.collect();
Self { published, states }
}
pub(super) fn get(&self, slot: usize) -> Option<&TaskState> {
if !self.published.get(slot)?.load(Ordering::Acquire) {
return None;
}
Some(unsafe { (*self.states[slot].get()).assume_init_ref() })
}
pub(super) unsafe fn insert(&self, slot: usize) -> NonNull<TaskState> {
debug_assert!(
!self.published[slot].load(Ordering::Relaxed),
"a task id registers exactly once"
);
let cell = self.states[slot].get();
unsafe { (*cell).write(TaskState::new()) };
let address = unsafe { NonNull::new_unchecked(cell.cast::<TaskState>()) };
self.published[slot].store(true, Ordering::Release);
address
}
pub(super) fn is_settled(&self, first_slot_unissued: bool, retire: Retirement) -> bool {
(0..TASK_STATE_BLOCK_SIZE)
.rev()
.all(|slot| match self.get(slot) {
None => first_slot_unissued && slot == 0,
Some(state) => {
!state.token_active()
&& state.completed_at().is_some_and(|completed| match retire {
Retirement::Forced => true,
Retirement::CompletedBefore(cutoff) => completed <= cutoff,
})
}
})
}
pub(super) fn states(&self) -> impl Iterator<Item = &TaskState> {
(0..TASK_STATE_BLOCK_SIZE).filter_map(|slot| self.get(slot))
}
}
impl Drop for TaskStateBlock {
fn drop(&mut self) {
for (published, state) in self.published.iter_mut().zip(self.states.iter_mut()) {
if *published.get_mut() {
unsafe { state.get_mut().assume_init_drop() };
}
}
}
}
impl std::fmt::Debug for TaskStateBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TaskStateBlock")
.field("slots", &self.published.len())
.field("occupied", &self.states().count())
.finish()
}
}
#[inline]
pub(crate) fn elapsed_nanos_since(origin: Instant) -> u64 {
let elapsed = origin.elapsed().as_nanos();
elapsed.min(u128::from(TIMESTAMP_NOT_RECORDED - 1)) as u64
}
pub(crate) fn instant_from_offset(origin: Instant, offset_ns: u64) -> Option<Instant> {
if offset_ns == TIMESTAMP_NOT_RECORDED {
None
} else {
origin.checked_add(Duration::from_nanos(offset_ns))
}
}
pub(crate) fn task_location(id: u64) -> (usize, usize) {
let index = usize::try_from(id).expect("task ID must fit in usize");
(index / TASK_STATE_BLOCK_SIZE, index % TASK_STATE_BLOCK_SIZE)
}
#[cfg(test)]
mod tests {
use super::{TaskState, elapsed_nanos_since};
use std::sync::atomic::Ordering;
use std::time::Duration;
#[test]
fn completion_clamps_to_recorded_start_offset() {
let state = TaskState::new();
let future_start = elapsed_nanos_since(state.created_at).saturating_add(1_000_000);
state
.started_after_ns
.store(future_start, Ordering::Release);
let elapsed = state.mark_completed_since(future_start);
let snapshot = state.snapshot(7);
assert_eq!(elapsed, Duration::ZERO);
assert_eq!(snapshot.started_at, snapshot.completed_at);
assert_eq!(snapshot.execution_duration(), Some(Duration::ZERO));
}
}