use crate::device::{
DeviceId, DeviceService, DeviceServiceStage, ServerUtilitiesHandle,
handle::{CallError, DeviceHandleSpec, ServiceCreationError},
};
use core::time::Duration;
use cubecl_environment::future::channel::oneshot;
use cubecl_environment::stream::StreamId;
use hashbrown::{HashMap, HashSet};
use std::{
any::{Any, TypeId},
boxed::Box,
cell::RefCell,
marker::PhantomData,
panic::{AssertUnwindSafe, catch_unwind},
vec::Vec,
};
use custom_channel::DeviceClient;
pub struct ChannelDeviceHandle<S: DeviceService> {
state: ChannelDeviceState,
_phantom: PhantomData<fn(S)>,
}
impl<S: DeviceService + 'static> DeviceHandleSpec<S> for ChannelDeviceHandle<S> {
const BLOCKING: bool = false;
fn insert(device_id: DeviceId, service: S) -> Result<Self, ServiceCreationError> {
let state = ChannelDeviceState::init(device_id, Some(service))?;
Ok(Self {
state,
_phantom: PhantomData,
})
}
fn new(device_id: DeviceId) -> Self {
let state = ChannelDeviceState::init::<S>(device_id, None).unwrap();
Self {
state,
_phantom: PhantomData,
}
}
fn device_id(&self) -> DeviceId {
self.state.client.runner_id().device
}
fn utilities(&self) -> ServerUtilitiesHandle {
self.state.utilities()
}
fn submit_blocking<'a, R: Send, T: FnOnce(&mut S) -> R + Send + 'a>(
&self,
task: T,
) -> Result<R, CallError> {
let state = self.state.service.clone();
let current = StreamId::current();
self.run_scoped(move || {
state.act_on(|s| {
let s = s
.downcast_mut::<S>()
.expect("State type mismatch in Thread Local Storage");
current.executes(|| task(s))
})
})
}
fn submit<T: FnOnce(&mut S) + Send + 'static>(&self, task: T) {
self.submit_inner::<_, SEND_NO_FLUSH>(task)
.expect("Can't have an error when submitting a task");
}
fn flush_queue(&self) {
if !is_device_runner_thread(self.state.client.runner_id()) {
self.state.client.flush();
}
}
fn exclusive<R: Send, T: FnOnce() -> R + Send>(&self, task: T) -> Result<R, CallError> {
let current = StreamId::current();
self.run_scoped(move || current.executes(task))
}
fn shutdown(device_id: DeviceId) {
shutdown_device(device_id);
}
}
const SEND_FLUSH: bool = true;
const SEND_NO_FLUSH: bool = false;
impl<S: DeviceService + 'static> ChannelDeviceHandle<S> {
fn submit_inner<T: FnOnce(&mut S) + Send + 'static, const FLUSH: bool>(
&self,
task: T,
) -> Result<(), CallError> {
let state = self.state.service.clone();
let current = StreamId::current();
let func_init = move || {
state.act_on(|state| {
let state = state
.downcast_mut::<S>()
.expect("State type mismatch in Thread Local Storage");
current.executes(|| task(state));
});
};
self.send::<_, FLUSH>(func_init)
}
fn run_scoped<'a, R: Send, T: FnOnce() -> R + Send + 'a>(
&self,
task: T,
) -> Result<R, CallError> {
type Outcome<R> = Result<R, Box<dyn Any + Send>>;
fn create_shim<R: Send, F: FnOnce() -> R + Send>(
slot: &mut Option<(F, oneshot::Sender<Outcome<R>>)>,
) -> impl FnOnce() + Send + 'static {
struct Ptr(*mut ());
unsafe impl Send for Ptr {}
let ptr = Ptr(slot as *mut _ as *mut ());
move || {
let _ = &ptr; let (task, sender) = unsafe {
(*(ptr.0 as *mut Option<(F, oneshot::Sender<Outcome<R>>)>))
.take()
.unwrap_unchecked()
};
let outcome = catch_unwind(AssertUnwindSafe(task));
let _ = sender.send(outcome);
}
}
let (sender, recv) = oneshot::channel();
let mut slot = Some((task, sender));
self.send::<_, SEND_FLUSH>(create_shim(&mut slot))?;
match recv.recv() {
Ok(Ok(value)) => Ok(value),
Ok(Err(payload)) => Err(CallError::from_panic(payload)),
Err(_) => Err(CallError::disconnected()),
}
}
fn send<T: FnOnce() + Send + 'static, const FLUSH: bool>(
&self,
task: T,
) -> Result<(), CallError> {
if is_device_runner_thread(self.state.client.runner_id()) {
if let Err(payload) = catch_unwind(AssertUnwindSafe(task)) {
let err = CallError::from_panic(payload);
log::warn!("Task failed: {err:?}");
return Err(err);
}
} else {
self.state.client.enqueue(task)?;
if FLUSH {
self.state.client.flush();
}
};
Ok(())
}
}
fn is_device_runner_thread(runner_key: &RunnerId) -> bool {
SERVER_THREAD.with_borrow(|state| state.as_ref() == Some(runner_key))
}
fn is_device_thread(device_id: DeviceId) -> bool {
SERVER_THREAD.with_borrow(|state| state.as_ref().is_some_and(|id| id.device == device_id))
}
std::thread_local! {
static SERVER_THREAD: RefCell<Option<RunnerId>> = const { RefCell::new(None) };
#[allow(clippy::type_complexity)]
static STATES: RefCell<HashMap<TypeId, RefCell<Box<dyn Any + 'static>>>> = RefCell::new(HashMap::new());
}
struct DeviceRunner {}
#[derive(Clone)]
struct ChannelDeviceState {
client: DeviceClient,
service: ChannelService,
}
#[derive(Clone)]
struct ChannelService {
type_id: TypeId,
utilities: ServerUtilitiesHandle,
}
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
struct RunnerId {
device: DeviceId,
stage: DeviceServiceStage,
}
struct RunnerEntry {
client: DeviceClient,
thread: std::thread::JoinHandle<()>,
}
static RUNNERS: spin::Mutex<Option<HashMap<RunnerId, RunnerEntry>>> = spin::Mutex::new(None);
static CHANNELS: spin::Mutex<Option<Registry>> = spin::Mutex::new(None);
#[derive(Default)]
struct Registry {
channels: HashMap<(RunnerId, TypeId), ChannelDeviceState>,
shutting_down: HashSet<RunnerId>,
}
const SHUTDOWN_JOIN_TIMEOUT: Duration = Duration::from_secs(30);
const SHUTDOWN_JOIN_YIELD_BUDGET: u32 = 1024;
const SHUTDOWN_JOIN_POLL: Duration = Duration::from_micros(200);
impl ChannelDeviceState {
pub fn init<S: DeviceService>(
device_id: DeviceId,
service: Option<S>,
) -> Result<Self, ServiceCreationError> {
let type_id = TypeId::of::<S>();
let runner_id = RunnerId {
device: device_id,
stage: S::stage(),
};
let key = (runner_id, type_id);
let mut guard_channel = loop {
let mut guard = CHANNELS.lock();
let shutting_down = guard
.get_or_insert_with(Registry::default)
.shutting_down
.contains(&runner_id);
if !shutting_down {
break guard;
}
if is_device_thread(runner_id.device) {
return Err(ServiceCreationError::new(
"Cannot create a device handle from a runner thread of a device that \
is shutting down."
.into(),
));
}
drop(guard);
std::thread::yield_now();
};
let channels = &mut guard_channel.get_or_insert_with(Registry::default).channels;
if let Some(existing) = channels.get(&key) {
if service.is_some() {
return Err(ServiceCreationError::new(
"Service already initialized.".into(),
));
}
return Ok(existing.clone());
}
let device_client = {
let mut guard = RUNNERS.lock();
let runners = guard.get_or_insert_with(HashMap::new);
runners
.entry(runner_id)
.or_insert_with(|| DeviceRunner::start(runner_id))
.client
.clone()
};
let (callback, recv) = oneshot::channel();
let initialize_service = move || {
STATES.with(|state| {
let mut map = match state.try_borrow_mut() {
Ok(map) => map,
Err(err) => panic!(
"The device service {:?} is already borrowed: {err}",
core::any::type_name::<S>()
),
};
if service.is_some() && map.contains_key(&type_id) {
callback.send(Err(())).unwrap();
} else {
let service = service.unwrap_or_else(|| S::init(device_id));
let utilities = service.utilities();
map.entry(type_id)
.or_insert_with(|| RefCell::new(Box::new(service)));
callback
.send(Ok(ChannelService { type_id, utilities }))
.unwrap();
}
});
};
if is_device_runner_thread(&runner_id) {
if let Err(err) = catch_unwind(AssertUnwindSafe(initialize_service)) {
return Err(ServiceCreationError::new(std::format!(
"Service initialization failed: {err:?}"
)));
};
} else {
device_client.enqueue(initialize_service).unwrap();
device_client.flush();
};
let service = recv.recv().unwrap();
let service = match service {
Ok(service) => service,
Err(_) => {
return Err(ServiceCreationError::new(
"Service already initialized.".into(),
));
}
};
let channel = Self {
client: device_client,
service,
};
channels.insert(key, channel.clone());
Ok(channel)
}
fn utilities(&self) -> ServerUtilitiesHandle {
self.service.utilities.clone()
}
}
impl ChannelService {
fn act_on<R>(&self, f: impl FnOnce(&mut Box<dyn Any + 'static>) -> R) -> R {
STATES.with_borrow(|map| {
let cell = map.get(&self.type_id).expect("Service state not found");
let mut guard = cell
.try_borrow_mut()
.expect("Service state is already borrowed");
f(&mut guard)
})
}
}
impl DeviceRunner {
pub fn start(runner_id: RunnerId) -> RunnerEntry {
let (sender_init, recv_init) = oneshot::channel();
let (client, thread) = DeviceClient::new(runner_id, move || {
SERVER_THREAD.with_borrow_mut(|cell| *cell = Some(runner_id));
sender_init.send(()).unwrap();
});
if recv_init.recv().is_err() {
panic!("Failed to synchronize device runner thread initialization");
}
RunnerEntry { client, thread }
}
}
pub(crate) fn shutdown_device(device_id: DeviceId) {
SERVER_THREAD.with_borrow(|current| {
if let Some(runner) = current {
assert_ne!(
runner.device, device_id,
"cannot shut down a device from its own runner thread"
);
}
});
let (channels, runners) = {
let mut guard_channel = CHANNELS.lock();
let registry = guard_channel.get_or_insert_with(Registry::default);
let runners: Vec<(RunnerId, RunnerEntry)> = match RUNNERS.lock().as_mut() {
Some(map) => map
.extract_if(|runner_id, _| runner_id.device == device_id)
.collect(),
None => Vec::new(),
};
let channels: Vec<ChannelDeviceState> = registry
.channels
.extract_if(|(runner_id, _), _| runner_id.device == device_id)
.map(|(_, state)| state)
.collect();
for (runner_id, _) in runners.iter() {
registry.shutting_down.insert(*runner_id);
}
(channels, runners)
};
drop(channels);
for (runner_id, runner) in runners {
runner.client.request_shutdown();
drop(runner.client);
join_runner(runner_id, runner.thread);
if let Some(registry) = CHANNELS.lock().as_mut() {
registry.shutting_down.remove(&runner_id);
}
}
wait_for_device_shutdown(device_id);
}
fn join_runner(runner_id: RunnerId, thread: std::thread::JoinHandle<()>) {
let start = std::time::Instant::now();
let mut yields: u32 = 0;
while !thread.is_finished() {
if start.elapsed() >= SHUTDOWN_JOIN_TIMEOUT {
log::warn!(
"Device runner {runner_id:?} did not stop within {SHUTDOWN_JOIN_TIMEOUT:?}, \
leaking the thread. Something still holds a client for it: a task parked in \
another device's unflushed queue, or two runners shutting each other down."
);
return;
}
if yields < SHUTDOWN_JOIN_YIELD_BUDGET {
std::thread::yield_now();
yields += 1;
} else {
std::thread::sleep(SHUTDOWN_JOIN_POLL);
}
}
if thread.join().is_err() {
log::warn!("Device runner {runner_id:?} panicked during shutdown");
}
}
fn wait_for_device_shutdown(device_id: DeviceId) {
let start = std::time::Instant::now();
loop {
let pending = CHANNELS.lock().as_ref().is_some_and(|registry| {
registry
.shutting_down
.iter()
.any(|runner_id| runner_id.device == device_id)
});
if !pending {
return;
}
if start.elapsed() >= SHUTDOWN_JOIN_TIMEOUT * 2 {
log::warn!(
"A concurrent shutdown of {device_id:?} is still in flight after \
{SHUTDOWN_JOIN_TIMEOUT:?}, returning anyway."
);
return;
}
std::thread::sleep(SHUTDOWN_JOIN_POLL);
}
}
impl<S: DeviceService> Clone for ChannelDeviceHandle<S> {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
_phantom: self._phantom,
}
}
}
mod task {
use super::*;
use core::sync::atomic::{AtomicPtr, Ordering};
use std::{
mem::{align_of, size_of},
panic::{AssertUnwindSafe, catch_unwind},
};
pub const GLOBAL_TASK_MAX_SIZE: usize = 4096;
const INLINE_TASK_MAX_SIZE: usize = 48;
#[repr(C, align(64))]
pub struct ArenaSlot {
pub data: [u8; GLOBAL_TASK_MAX_SIZE],
}
#[repr(C, align(64))]
pub struct Task {
data: [u8; INLINE_TASK_MAX_SIZE],
data_large_ptr: AtomicPtr<u8>,
fn_ptr: fn(&mut Task),
}
const _: () = {
assert!(core::mem::size_of::<ArenaSlot>() == GLOBAL_TASK_MAX_SIZE);
assert!(core::mem::size_of::<Task>() == 64);
assert!(core::mem::align_of::<Task>() == core::mem::align_of::<ArenaSlot>());
assert!(core::mem::offset_of!(Task, data) == 0);
};
impl Task {
pub fn new(large_data_ptr: *mut u8) -> Self {
Self {
data: [0u8; INLINE_TASK_MAX_SIZE],
data_large_ptr: AtomicPtr::new(large_data_ptr),
fn_ptr: |_| {},
}
}
pub fn init<F: FnOnce() + Send + 'static>(&mut self, func: F) {
let fits_inline = size_of::<F>() <= INLINE_TASK_MAX_SIZE
&& align_of::<F>() <= align_of::<ArenaSlot>();
let fits_arena = size_of::<F>() <= GLOBAL_TASK_MAX_SIZE
&& align_of::<F>() <= align_of::<ArenaSlot>();
if fits_inline {
unsafe { std::ptr::write(self.data.as_mut_ptr() as *mut F, func) };
self.fn_ptr = |task| {
let f = unsafe { std::ptr::read(task.data.as_mut_ptr() as *mut F) };
if let Err(payload) = catch_unwind(AssertUnwindSafe(f)) {
log::warn!("{:?}", CallError::from_panic(payload));
}
};
} else if fits_arena {
unsafe {
std::ptr::write(self.data_large_ptr.load(Ordering::Relaxed) as *mut F, func)
};
self.fn_ptr = |task| {
let f = unsafe {
std::ptr::read(task.data_large_ptr.load(Ordering::Relaxed) as *mut F)
};
if let Err(payload) = catch_unwind(AssertUnwindSafe(f)) {
log::warn!("{:?}", CallError::from_panic(payload));
}
};
} else {
let boxed: Box<dyn FnOnce() + Send> = Box::new(func);
self.init(boxed);
}
}
pub fn run(&mut self) {
(self.fn_ptr)(self)
}
}
}
#[allow(dead_code)]
mod normal_channel {
use super::RunnerId;
use crate::device::handle::CallError;
use alloc::boxed::Box;
use std::sync::mpsc::SyncSender;
pub const CHANNEL_MAX_TASK: usize = 32;
pub struct DeviceClient {
state: SyncSender<Box<dyn FnOnce() + Send + 'static>>,
runner_id: RunnerId,
}
impl Clone for DeviceClient {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
runner_id: self.runner_id,
}
}
}
impl DeviceClient {
pub fn runner_id(&self) -> &RunnerId {
&self.runner_id
}
pub fn new<I: FnOnce() + Send + 'static>(
runner_id: RunnerId,
init: I,
) -> (Self, std::thread::JoinHandle<()>) {
let (sender, recv) = std::sync::mpsc::sync_channel::<Box<dyn FnOnce() + Send + 'static>>(
CHANNEL_MAX_TASK,
);
let thread = std::thread::spawn(move || {
init();
while let Ok(item) = recv.recv() {
item()
}
});
(
Self {
state: sender,
runner_id,
},
thread,
)
}
pub fn request_shutdown(&self) {}
pub fn enqueue<F: FnOnce() + Send + 'static>(&self, func: F) -> Result<(), CallError> {
self.state
.send(Box::new(func))
.map_err(|_| CallError::disconnected())
}
pub fn flush(&self) {
}
}
}
mod custom_channel {
use crate::device::handle::{
CallError,
channel::{
RunnerId,
task::{ArenaSlot, GLOBAL_TASK_MAX_SIZE, Task},
},
};
use core::{
hint::spin_loop,
sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, Ordering},
time::Duration,
};
use std::{sync::Arc, vec::Vec};
pub const CHANNEL_MAX_TASK: usize = 32;
const SPIN_BUDGET_SERVER: u32 = 8192;
const YIELD_BUDGET_SERVER: u32 = 64;
const SLEEP_STEP_SERVER: Duration = Duration::from_micros(150);
const CLIENT_BUDGET_FACTOR: u32 = CHANNEL_MAX_TASK as u32 * 2u32;
const SPIN_BUDGET_CLIENT: u32 = SPIN_BUDGET_SERVER * CLIENT_BUDGET_FACTOR;
const YIELD_BUDGET_CLIENT: u32 = YIELD_BUDGET_SERVER * CLIENT_BUDGET_FACTOR;
const SLEEP_STEP_CLIENT: Duration = Duration::from_micros(75);
pub struct DeviceClient {
state: Arc<State>,
}
impl Clone for DeviceClient {
fn clone(&self) -> Self {
Self {
state: self.state.clone(),
}
}
}
impl DeviceClient {
pub fn runner_id(&self) -> &RunnerId {
&self.state.runner_id
}
pub fn new<I: FnOnce() + Send + 'static>(
runner_id: RunnerId,
init: I,
) -> (Self, std::thread::JoinHandle<()>) {
let mut server = Server::new(runner_id);
let state = server.state.clone();
let thread = std::thread::Builder::new()
.name(std::format!(
"DS{}-{}-{}",
match runner_id.stage {
crate::device::DeviceServiceStage::Upstream => "U",
crate::device::DeviceServiceStage::Downstream => "D",
},
runner_id.device.type_id,
runner_id.device.index_id
))
.spawn(move || {
init();
server.start();
})
.unwrap();
(Self { state }, thread)
}
pub fn request_shutdown(&self) {
self.state.shutdown.store(true, Ordering::Release);
}
pub fn enqueue<F: FnOnce() + Send + 'static>(&self, func: F) -> Result<(), CallError> {
let mut idle_count: u32 = 0;
loop {
let index = self.state.available_index.fetch_add(1, Ordering::Acquire) as usize;
if index >= CHANNEL_MAX_TASK {
if idle_count < SPIN_BUDGET_CLIENT {
spin_loop();
} else if idle_count < SPIN_BUDGET_CLIENT + YIELD_BUDGET_CLIENT {
std::thread::yield_now();
} else {
std::thread::sleep(SLEEP_STEP_CLIENT);
}
idle_count = idle_count.saturating_add(1);
continue;
}
self.state.init_task_at(index, func);
self.state.enqueued_count.fetch_add(1, Ordering::SeqCst);
return Ok(());
}
}
pub fn flush(&self) {
self.state.pad_with_noops();
}
}
struct State {
queue_ptr: AtomicPtr<Task>,
available_index: AtomicU32,
enqueued_count: AtomicU32,
shutdown: AtomicBool,
runner_id: RunnerId,
}
impl State {
#[inline]
fn pad_with_noops(&self) {
let index_start =
self.available_index
.fetch_add(CHANNEL_MAX_TASK as u32, Ordering::Acquire) as usize;
if index_start >= CHANNEL_MAX_TASK {
return;
}
let index_end = CHANNEL_MAX_TASK;
for index in index_start..index_end {
self.init_task_at(index, || ());
}
let actual_added = index_end - index_start;
self.enqueued_count
.fetch_add(actual_added as u32, Ordering::SeqCst);
}
fn init_task_at<F: FnOnce() + Send + 'static>(&self, index: usize, func: F) {
assert!(index < CHANNEL_MAX_TASK, "task index {index} out of bounds");
unsafe { &mut *self.queue_ptr.load(Ordering::Acquire).add(index) }.init(func);
}
}
struct TaskBuffer {
tasks: Vec<Task>,
_arena: Vec<ArenaSlot>,
}
impl TaskBuffer {
fn new() -> Self {
let mut arena: Vec<ArenaSlot> =
Vec::from_iter((0..CHANNEL_MAX_TASK).map(|_| ArenaSlot {
data: [0u8; GLOBAL_TASK_MAX_SIZE],
}));
let arena_ptr = arena.as_mut_ptr() as *mut u8;
let tasks = Vec::from_iter((0..CHANNEL_MAX_TASK).map(|index| {
Task::new(unsafe { arena_ptr.add(index * GLOBAL_TASK_MAX_SIZE) })
}));
Self {
tasks,
_arena: arena,
}
}
}
struct Server {
state: Arc<State>,
client_buf: usize,
buffers: [TaskBuffer; 2],
ready_to_execute: bool,
}
impl Server {
fn new(runner_id: RunnerId) -> Self {
let mut buffers = [TaskBuffer::new(), TaskBuffer::new()];
let state = Arc::new(State {
queue_ptr: AtomicPtr::new(buffers[0].tasks.as_mut_ptr()),
available_index: AtomicU32::new(0),
enqueued_count: AtomicU32::new(0),
shutdown: AtomicBool::new(false),
runner_id,
});
Self {
state,
client_buf: 0,
buffers,
ready_to_execute: false,
}
}
fn start(&mut self) {
let mut idle_count: u32 = 0;
loop {
if self.ready_to_execute {
self.execute_tasks();
idle_count = 0;
}
let queue_size = self.state.enqueued_count.load(Ordering::Acquire) as usize;
if queue_size >= CHANNEL_MAX_TASK {
self.fetch();
idle_count = 0;
continue;
}
if idle_count < SPIN_BUDGET_SERVER {
spin_loop();
} else {
if (self.state.shutdown.load(Ordering::Acquire)
|| Arc::strong_count(&self.state) == 1)
&& self.try_shutdown()
{
return;
}
if idle_count < SPIN_BUDGET_SERVER + YIELD_BUDGET_SERVER {
std::thread::yield_now();
} else {
std::thread::sleep(SLEEP_STEP_SERVER);
}
}
idle_count = idle_count.saturating_add(1);
}
}
fn try_shutdown(&mut self) -> bool {
let is_last_ref = Arc::strong_count(&self.state) == 1;
if is_last_ref {
core::sync::atomic::fence(Ordering::Acquire);
}
if self.state.enqueued_count.load(Ordering::Acquire) > 0 {
self.state.pad_with_noops();
return false;
}
is_last_ref
}
fn execute_tasks(&mut self) {
let server_buf = 1 - self.client_buf;
for task in &mut self.buffers[server_buf].tasks {
task.run();
}
self.ready_to_execute = false;
}
fn fetch(&mut self) {
self.client_buf = 1 - self.client_buf;
self.state.queue_ptr.store(
self.buffers[self.client_buf].tasks.as_mut_ptr(),
Ordering::Release,
);
self.ready_to_execute = true;
self.state.enqueued_count.store(0, Ordering::SeqCst);
self.state.available_index.store(0, Ordering::SeqCst);
}
}
}
#[cfg(test)]
mod tests {
use crate::device::handle::CallResultExt;
use crate::device::handle::DeviceFixture;
#[cfg(not(miri))]
use crate::device::handle::channel::custom_channel::CHANNEL_MAX_TASK;
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
struct MockService {
counter: usize,
id: DeviceId,
}
impl DeviceService for MockService {
fn init(id: DeviceId) -> Self {
Self { counter: 0, id }
}
fn utilities(&self) -> ServerUtilitiesHandle {
Arc::new(())
}
}
fn mock_fixture() -> DeviceFixture<ChannelDeviceHandle<MockService>> {
DeviceFixture::new(ChannelDeviceHandle::<MockService>::new, shutdown_device)
}
#[test]
fn test_basic_execution_and_state_persistence() {
let handle = mock_fixture();
let res = handle
.submit_blocking(|state| {
state.counter += 1;
state.counter
})
.unwrap();
let res2 = handle
.submit_blocking(|state| {
state.counter += 1;
state.counter
})
.unwrap();
assert_eq!(res, 1);
assert_eq!(res2, 2);
}
#[test]
fn test_scoped_tasks_and_lifetimes() {
let handle = mock_fixture();
let local_val = 42;
let result = handle.exclusive(|| local_val + 8).unwrap();
assert_eq!(result, 50);
let result_mut = handle
.submit_blocking(|state| {
state.counter = local_val;
state.counter
})
.unwrap();
assert_eq!(result_mut, 42);
}
#[test]
#[cfg(not(miri))]
fn test_buffer_flushing_at_limit() {
let handle = mock_fixture();
let completed_count = Arc::new(AtomicUsize::new(0));
for _ in 0..CHANNEL_MAX_TASK {
let counter = Arc::clone(&completed_count);
handle.submit(move |_| {
counter.fetch_add(1, Ordering::SeqCst);
});
}
let _ = handle.submit_blocking(|_| {});
assert_eq!(completed_count.load(Ordering::SeqCst), 32);
}
#[test]
fn test_manual_flush_for_partial_buffer() {
let handle = mock_fixture();
let (tx, rx) = oneshot::channel();
handle.submit(move |_| {
tx.send(true).unwrap();
});
handle.state.client.flush();
let received = rx
.recv_timeout(Duration::from_secs(1))
.expect("Task was not flushed and processed in time");
assert!(received);
}
#[test]
fn test_shutdown_drains_a_task_the_last_client_left_queued() {
let ran = Arc::new(AtomicUsize::new(0));
{
let handle = mock_fixture();
let counter = Arc::clone(&ran);
handle.submit(move |_state| {
counter.fetch_add(1, Ordering::SeqCst);
});
}
assert_eq!(
ran.load(Ordering::SeqCst),
1,
"the queued task must run before the runner thread exits"
);
}
#[test]
fn test_handle_created_after_shutdown_gets_a_fresh_runner() {
let device_id = {
let handle = mock_fixture();
handle.submit_blocking(|state| state.counter += 1).unwrap();
handle.device_id()
};
let handle = ChannelDeviceHandle::<MockService>::new(device_id);
let counter = handle.submit_blocking(|state| state.counter).unwrap();
assert_eq!(counter, 0, "the new runner must start from a fresh service");
drop(handle);
shutdown_device(device_id);
}
#[test]
fn test_closure_captures_are_dropped_after_execution() {
let handle = mock_fixture();
let drop_count = Arc::new(AtomicUsize::new(0));
struct DropSpy(Arc<AtomicUsize>);
impl Drop for DropSpy {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let spy = DropSpy(Arc::clone(&drop_count));
handle
.submit_blocking(move |_state| {
let _ = &spy;
})
.expect("Task execution failed");
assert_eq!(
drop_count.load(Ordering::SeqCst),
1,
"Capture was not dropped after execution"
);
}
#[test]
fn test_large_closure_uses_arena() {
let handle = mock_fixture();
let big_data = [42u8; 128]; let result = handle
.submit_blocking(move |_state| {
big_data[0] + big_data[127]
})
.unwrap();
assert_eq!(result, 84);
}
#[test]
fn test_extra_large_closure_uses_box() {
let handle = mock_fixture();
let huge_data = [7u8; 8192]; let result = handle
.submit_blocking(move |_state| huge_data[0] + huge_data[8191])
.unwrap();
assert_eq!(result, 14);
}
#[test]
fn test_large_closure_drop_is_called() {
let handle = mock_fixture();
let drop_count = Arc::new(AtomicUsize::new(0));
struct DropSpy {
counter: Arc<AtomicUsize>,
_padding: [u8; 128], }
impl Drop for DropSpy {
fn drop(&mut self) {
self.counter.fetch_add(1, Ordering::SeqCst);
}
}
let spy = DropSpy {
counter: Arc::clone(&drop_count),
_padding: [0; 128],
};
handle
.submit_blocking(move |_state| {
let _ = &spy;
})
.unwrap();
assert_eq!(drop_count.load(Ordering::SeqCst), 1);
}
#[test]
fn test_init_runs_exactly_once_under_contention() {
use alloc::vec::Vec;
use std::sync::Barrier;
use std::sync::atomic::AtomicUsize;
use std::thread;
static INIT_CALLS: AtomicUsize = AtomicUsize::new(0);
struct CountingService;
impl DeviceService for CountingService {
fn init(_: DeviceId) -> Self {
INIT_CALLS.fetch_add(1, Ordering::SeqCst);
CountingService
}
fn utilities(&self) -> ServerUtilitiesHandle {
Arc::new(())
}
}
INIT_CALLS.store(0, Ordering::SeqCst);
const THREADS: usize = 4;
let fixture =
DeviceFixture::new(ChannelDeviceHandle::<CountingService>::new, shutdown_device);
let device_id = fixture.device_id();
let barrier = Arc::new(Barrier::new(THREADS));
let mut handles = Vec::new();
for _ in 0..THREADS {
let b = barrier.clone();
handles.push(thread::spawn(move || {
b.wait();
ChannelDeviceHandle::<CountingService>::new(device_id)
}));
}
for h in handles {
let _ = h.join().unwrap();
}
assert_eq!(
INIT_CALLS.load(Ordering::SeqCst),
1,
"CountingService::init must run exactly once across {THREADS} racing callers"
);
}
#[test]
fn test_submit_blocking_panic_drops_and_returns_err() {
let handle = mock_fixture();
let drop_count = Arc::new(AtomicUsize::new(0));
struct DropSpy(Arc<AtomicUsize>);
impl Drop for DropSpy {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let spy = DropSpy(Arc::clone(&drop_count));
let result = handle.submit_blocking(move |_state| {
let _ = &spy;
panic!("boom");
});
let err = result.expect_err("panicking task must return Err");
assert_eq!(
err.message(),
Some("boom"),
"the panic message must be preserved in the CallError"
);
assert_eq!(
drop_count.load(Ordering::SeqCst),
1,
"captures must be dropped exactly once on panic"
);
let ok = handle.submit_blocking(|state| state.counter).unwrap();
assert_eq!(ok, 0);
}
#[test]
fn test_exclusive_panic_drops_and_returns_err() {
let handle = mock_fixture();
let drop_count = Arc::new(AtomicUsize::new(0));
struct DropSpy(Arc<AtomicUsize>);
impl Drop for DropSpy {
fn drop(&mut self) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let spy = DropSpy(Arc::clone(&drop_count));
let result: Result<(), _> = handle.exclusive(move || {
let _ = &spy;
panic!("boom");
});
let err = result.expect_err("panicking task must return Err");
assert_eq!(
err.message(),
Some("boom"),
"the panic message must be preserved in the CallError"
);
assert_eq!(drop_count.load(Ordering::SeqCst), 1);
let ok = handle.exclusive(|| 7).unwrap();
assert_eq!(ok, 7);
}
#[test]
fn test_submit_blocking_preserves_formatted_string_payload() {
let handle = mock_fixture();
let result = handle.submit_blocking(|_state| {
panic!("value {}", 99);
});
let err = result.expect_err("panicking task must return Err");
assert_eq!(err.message(), Some("value 99"));
}
#[test]
fn test_submit_blocking_preserves_non_string_payload() {
let handle = mock_fixture();
#[derive(Debug, PartialEq)]
struct Boom {
code: u32,
what: &'static str,
}
let result = handle.submit_blocking(|_state| {
std::panic::panic_any(Boom {
code: 7,
what: "kaboom",
});
});
let err = result.expect_err("panicking task must return Err");
assert_eq!(err.message(), None, "a non-string payload has no message");
let payload = err.into_panic().expect("the payload must be preserved");
let boom = *payload
.downcast::<Boom>()
.expect("payload must downcast to the original type");
assert_eq!(
boom,
Boom {
code: 7,
what: "kaboom",
}
);
}
#[test]
fn test_submit_blocking_preserves_scalar_payload() {
let handle = mock_fixture();
let result = handle.submit_blocking(|_state| {
std::panic::panic_any(42i32);
});
let err = result.expect_err("panicking task must return Err");
assert_eq!(err.message(), None);
let payload = err.into_panic().expect("the payload must be preserved");
assert_eq!(
*payload.downcast::<i32>().expect("payload must be an i32"),
42
);
}
#[test]
fn test_submit_blocking_preserves_index_out_of_bounds_message() {
let handle = mock_fixture();
let result = handle.submit_blocking(|_state| {
let data = [10u8, 20u8];
let idx = core::hint::black_box(5usize);
let _ = data[idx];
});
let err = result.expect_err("panicking task must return Err");
let message = err
.message()
.expect("an index panic carries a string message");
assert!(
message.contains("index out of bounds"),
"unexpected message: {message}"
);
}
#[test]
fn test_submit_blocking_preserves_unwrap_message() {
let handle = mock_fixture();
let result = handle.submit_blocking(|_state| {
let value: Result<(), &str> = Err("nope");
#[allow(clippy::unnecessary_literal_unwrap)]
value.unwrap();
});
let err = result.expect_err("panicking task must return Err");
let message = err
.message()
.expect("an unwrap panic carries a string message");
assert!(message.contains("unwrap"), "unexpected message: {message}");
}
#[test]
fn test_into_panic_can_be_resumed() {
let handle = mock_fixture();
let result = handle.submit_blocking(|_state| {
panic!("re-raise me");
});
let err = result.expect_err("panicking task must return Err");
assert_eq!(err.message(), Some("re-raise me"));
let payload = err.into_panic().expect("the payload must be preserved");
let recaught = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
std::panic::resume_unwind(payload);
}));
let payload = recaught.expect_err("resume_unwind must re-panic");
assert_eq!(
payload.downcast_ref::<&str>().copied(),
Some("re-raise me"),
"the re-raised panic must carry the original message"
);
}
#[test]
fn test_exclusive_preserves_non_string_payload() {
let handle = mock_fixture();
#[derive(Debug, PartialEq)]
struct Boom {
code: u32,
what: &'static str,
}
let result: Result<(), _> = handle.exclusive(|| {
std::panic::panic_any(Boom {
code: 9,
what: "exclusive",
});
});
let err = result.expect_err("panicking task must return Err");
assert_eq!(err.message(), None);
let payload = err.into_panic().expect("the payload must be preserved");
let boom = *payload
.downcast::<Boom>()
.expect("payload must downcast to the original type");
assert_eq!(
boom,
Boom {
code: 9,
what: "exclusive",
}
);
}
#[test]
fn test_channel_survives_repeated_panics_each_preserved() {
let handle = mock_fixture();
let first = handle.submit_blocking(|_state| panic!("first"));
assert_eq!(
first.expect_err("first panic must return Err").message(),
Some("first")
);
let second = handle.submit_blocking(|_state| panic!("second"));
assert_eq!(
second.expect_err("second panic must return Err").message(),
Some("second")
);
let counter = handle.submit_blocking(|state| state.counter).unwrap();
assert_eq!(counter, 0);
}
#[test]
fn test_unwrap_or_resume_reraises_submit_blocking_panic() {
let handle = mock_fixture();
let reraised = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handle
.submit_blocking(|_state| {
panic!("device boom");
})
.unwrap_or_resume()
}));
let payload = reraised.expect_err("the original panic must be re-raised at the caller");
assert_eq!(
payload.downcast_ref::<&str>().copied(),
Some("device boom"),
"the re-raised panic must carry the original message"
);
let counter = handle.submit_blocking(|state| state.counter).unwrap();
assert_eq!(counter, 0);
}
#[test]
fn test_unwrap_or_resume_reraises_exclusive_non_string_payload() {
let handle = mock_fixture();
#[derive(Debug, PartialEq)]
struct Boom {
code: u32,
what: &'static str,
}
let reraised = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handle
.exclusive(|| {
std::panic::panic_any(Boom {
code: 9,
what: "exclusive",
});
})
.unwrap_or_resume()
}));
let payload = reraised.expect_err("the original panic must be re-raised at the caller");
let boom = *payload
.downcast::<Boom>()
.expect("the re-raised payload must be the original object");
assert_eq!(
boom,
Boom {
code: 9,
what: "exclusive",
}
);
}
#[test]
fn test_task_init_arena_aligned_closure() {
use super::task::{ArenaSlot, GLOBAL_TASK_MAX_SIZE, Task};
#[repr(align(64))]
#[derive(Clone, Copy)]
struct A64 {
data: [u8; 128],
}
let mut arena = alloc::boxed::Box::new(ArenaSlot {
data: [0u8; GLOBAL_TASK_MAX_SIZE],
});
let arena_ptr = arena.data.as_mut_ptr();
let mut task = Task::new(arena_ptr);
let data = A64 { data: [0xCD; 128] };
task.init(move || {
let d = core::hint::black_box(data);
let _: usize = d.data.iter().map(|&b| b as usize).sum();
});
task.run();
}
#[test]
fn test_task_init_extremely_over_aligned_closure_uses_box() {
use super::task::{ArenaSlot, GLOBAL_TASK_MAX_SIZE, Task};
#[repr(align(256))]
#[derive(Clone, Copy)]
struct A256 {
data: [u8; 256],
}
let mut arena = alloc::boxed::Box::new(ArenaSlot {
data: [0u8; GLOBAL_TASK_MAX_SIZE],
});
let arena_ptr = arena.data.as_mut_ptr();
let mut task = Task::new(arena_ptr);
let data = A256 { data: [0xAA; 256] };
task.init(move || {
let d = core::hint::black_box(data);
let _: usize = d.data.iter().map(|&b| b as usize).sum();
});
task.run();
}
}