#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::future::Future;
use std::pin::pin;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll, Waker};
use actor_kit::{
ActorBuilder, ActorScheduler, MailboxConfig, Message, MessagePayload, Priority, SchedulerConfig,
};
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
struct Counting;
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
fn allocations() -> usize {
ALLOCATIONS.load(Ordering::Relaxed)
}
fn block_once<F: Future>(fut: F) -> F::Output {
let mut fut = pin!(fut);
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
match fut.as_mut().poll(&mut cx) {
Poll::Ready(output) => output,
Poll::Pending => panic!("uncontended send must complete on its first poll"),
}
}
fn fixture() -> (Arc<ActorScheduler>, actor_kit::ActorHandle) {
let mut cfg = SchedulerConfig::new().workers(1);
cfg.mailbox_config = MailboxConfig::new(4096);
let scheduler = Arc::new(ActorScheduler::new(cfg));
let handle = ActorBuilder::new()
.name("alloc-probe")
.spawn(&scheduler)
.unwrap();
scheduler.set_actor_running(&handle.id()).unwrap();
(scheduler, handle)
}
fn msg(i: u8) -> Message {
Message {
sender: None,
payload: MessagePayload::Custom(vec![i]),
priority: Priority::Normal,
}
}
#[test]
fn tell_path_allocation_profile() {
let (scheduler, handle) = fixture();
for i in 0..128u8 {
block_once(scheduler.send(handle.id(), msg(i))).unwrap();
}
let window = |scheduler: &Arc<ActorScheduler>, handle: &actor_kit::ActorHandle| {
let before = allocations();
const ITERATIONS: usize = 100;
for i in 0..ITERATIONS {
block_once(scheduler.send(handle.id(), msg(i as u8))).unwrap();
}
allocations() - before
};
let window_a = window(&scheduler, &handle);
let window_b = window(&scheduler, &handle);
assert!(
window_a >= 100,
"each tell must allocate ≥ 1 (the mailbox Vec clone): got {window_a}/100"
);
assert!(
window_a <= 210,
"steady-state tell allocates ~2/call (Vec clone + injector slot); \
got {window_a}/100 — a regression slipped in"
);
assert!(
(window_a as isize - window_b as isize).abs() <= 2,
"allocation count must be stable across windows (crossbeam block \
boundaries add ±1): {window_a} vs {window_b}"
);
let mailbox = scheduler.registry().get_mailbox(&handle.id()).unwrap();
let message = msg(255); let before = allocations();
mailbox.try_send(message).unwrap();
assert_eq!(
allocations(),
before,
"mailbox try_send happy path must not allocate"
);
}
#[test]
fn allocation_counter_sanity() {
let before = allocations();
std::hint::black_box(format!("fresh-{before}"));
assert!(
allocations() > before,
"format! must allocate (counter sanity check)"
);
}