v0.1: green-thread actors, supervision, channels, benchmark

Hand-rolled context switching on mmap'd stacks with guard pages,
allocator-driven RDTSC preemption, unbounded MPSC channels, supervision
via per-slot Signal mailboxes, root supervisor as sentinel PID.

Lib + tests + benches clean check/clippy. All 29 tests pass.
Bench: smarm 3.4% over serial baseline, within 160us of tokio
current-thread on prime-counting fan-out.
This commit is contained in:
Claude
2026-05-22 05:01:51 +00:00
commit 0e9d9d7d5f
17 changed files with 1938 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
//! Allocator-driven preemption.
//!
//! A `GlobalAlloc` wrapper counts allocations. Every `ALLOC_INTERVAL`-th
//! allocation it reads RDTSC and, if the actor's timeslice has expired,
//! calls `switch_to_scheduler` to yield.
//!
//! All state is thread-local. The scheduler enables preemption on resume
//! and disables it on the return path, so the scheduler can never preempt
//! itself.
//!
//! TSC frequency is machine-dependent; `TIMESLICE_CYCLES` is a constant
//! calibrated for ~100µs on a 3 GHz CPU. A real implementation would
//! measure it at startup. For v0.1 the constant suffices.
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
const ALLOC_INTERVAL: u32 = 128;
const TIMESLICE_CYCLES: u64 = 300_000; // ≈ 100µs on a 3 GHz CPU
thread_local! {
/// While `false`, the allocator hook is a no-op.
pub static PREEMPTION_ENABLED: Cell<bool> = const { Cell::new(false) };
/// Countdown to next RDTSC check. Reset to `ALLOC_INTERVAL` on resume.
static ALLOC_COUNT: Cell<u32> = const { Cell::new(ALLOC_INTERVAL) };
/// RDTSC value written by the scheduler on every actor resume.
static TIMESLICE_START: Cell<u64> = const { Cell::new(0) };
}
/// Arm the timeslice. Called by the scheduler on every resume.
pub fn reset_timeslice() {
ALLOC_COUNT.with(|c| c.set(ALLOC_INTERVAL));
TIMESLICE_START.with(|c| c.set(rdtsc()));
}
#[inline(always)]
pub fn rdtsc() -> u64 {
unsafe {
// SAFETY: x86-64 only. `lfence` serialises the instruction stream so
// we don't measure time before prior instructions retire.
core::arch::asm!("lfence", options(nostack, nomem, preserves_flags));
core::arch::x86_64::_rdtsc()
}
}
pub struct PreemptingAllocator;
unsafe impl GlobalAlloc for PreemptingAllocator {
#[inline]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
maybe_preempt();
unsafe { System.alloc(layout) }
}
#[inline]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
#[inline]
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
maybe_preempt();
unsafe { System.alloc_zeroed(layout) }
}
#[inline]
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
maybe_preempt();
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[inline(always)]
fn maybe_preempt() {
ALLOC_COUNT.with(|c| {
let n = c.get();
if n == 0 {
c.set(ALLOC_INTERVAL);
if PREEMPTION_ENABLED.with(|e| e.get()) {
let start = TIMESLICE_START.with(|s| s.get());
if rdtsc().saturating_sub(start) > TIMESLICE_CYCLES {
// SAFETY: reachable only inside an actor (the scheduler
// sets PREEMPTION_ENABLED on resume and clears it on
// return). The scheduler stack is therefore valid.
unsafe { crate::context::switch_to_scheduler() };
}
}
} else {
c.set(n - 1);
}
});
}
// ---------------------------------------------------------------------------
// Test helpers
// ---------------------------------------------------------------------------
/// Force-expire the timeslice so the next RDTSC check preempts.
pub fn expire_timeslice_for_test() {
TIMESLICE_START.with(|c| c.set(0));
ALLOC_COUNT.with(|c| c.set(0));
}