feat: full runtime redesign (v0.6)
Complete rewrite with improved architecture & correctness: - src/runtime.rs: Simplified task scheduling with proper state transitions - src/scheduler.rs: Decoupled from runtime, pure task queue logic - src/io.rs, src/mutex.rs: Refactored for clarity & performance - New actor model framework (src/actor.rs, src/context.rs) - Channel primitives (src/channel.rs) & process IDs (src/pid.rs) - Preemption framework (src/preempt.rs) for fair timeslicing - Expanded benchmarks & tests (multi_scheduler, primes, runtime)
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
//! Preemption hooks.
|
||||
//!
|
||||
//! Preemption is event-driven: every preemption event decrements a
|
||||
//! thread-local counter (`ALLOC_COUNT`). When the counter hits zero, we
|
||||
//! read RDTSC and, if the actor's timeslice has expired, call
|
||||
//! `switch_to_scheduler` to yield. Resetting the counter to `ALLOC_INTERVAL`
|
||||
//! amortises the RDTSC across many cheap events.
|
||||
//!
|
||||
//! Two event sources today:
|
||||
//! - `PreemptingAllocator` — heap allocations.
|
||||
//! - `smarm::check!()` — explicit preemption point for tight no-alloc
|
||||
//! loops, since stable Rust gives us no transparent way to preempt
|
||||
//! such loops (`__rust_probestack` is emitted inline by LLVM and not
|
||||
//! called at runtime).
|
||||
//!
|
||||
//! Both sources share `ALLOC_COUNT`, so the timeslice check fires at the
|
||||
//! same rate regardless of whether the actor is alloc-heavy, check-heavy,
|
||||
//! or mixed.
|
||||
//!
|
||||
//! 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) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared preemption check. Called by every preemption event source — the
|
||||
/// heap allocator today, `smarm::check!()` for tight no-alloc loops.
|
||||
/// Decrements `ALLOC_COUNT`; every `ALLOC_INTERVAL` calls reads the
|
||||
/// timeslice clock and yields if expired.
|
||||
///
|
||||
/// **Invariant**: must not be called inside a "prep-to-park" region —
|
||||
/// e.g. between registering as a channel's parked receiver and calling
|
||||
/// `park_current()`. A preemption-driven yield in that window would
|
||||
/// reach the scheduler with state=Runnable, the unparker would no-op,
|
||||
/// the actor would then park, and the wakeup would be lost. Library
|
||||
/// code that touches the parking primitives must keep its prep-to-park
|
||||
/// regions allocation-free and check!()-free.
|
||||
#[inline(always)]
|
||||
pub 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));
|
||||
}
|
||||
Reference in New Issue
Block a user