diff --git a/benches/barbershop.rs b/benches/barbershop.rs index a8ab06fd9727a13bc90ecd91980ff4f48a11762c..9d239f801d0b5830e9b02277fa7d1a7c5894e26e 100644 --- a/benches/barbershop.rs +++ b/benches/barbershop.rs @@ -1,7 +1,8 @@ -use simcore_rs::{Time, SimContext, Facility, simulation}; +use simcore_rs::{Time, SimContext, Facility, simulation, Process}; use rand::{distributions::Uniform, rngs::SmallRng, SeedableRng, Rng}; use criterion::{Criterion, BenchmarkId, criterion_group, criterion_main, BatchSize}; use std::time::Duration; +use std::rc::Rc; const SEED_A : u64 = 100000; const SEED_S : u64 = 200000; @@ -22,17 +23,15 @@ fn barbershop(c: &mut Criterion) { &stop_time, |b, &stop_time| b.iter_batched( - || { - (SmallRng::from_rng(&mut rng_a).unwrap(), - SmallRng::from_rng(&mut rng_s).unwrap()) - }, - |(rng_a, rng_s)| { - let shop = BarberShop { - stop_time, rng_a, rng_s, joe: &Facility::new() - }; - - simulation(|sim| shop.actions(sim)); + || BarberShop { + stop_time, + rng_a: SmallRng::from_rng(&mut rng_a).unwrap(), + rng_s: SmallRng::from_rng(&mut rng_s).unwrap(), + joe: Rc::new(Facility::new()) }, + |shop| simulation( + |sim| Process::new(shop.actions(sim)) + ), BatchSize::SmallInput ) ); @@ -47,16 +46,17 @@ criterion_main!(benches); /* *************************** Barbershop Example *************************** */ /// Barbershop process. -struct BarberShop<'j> { - stop_time: Time, rng_a: SmallRng, rng_s: SmallRng, joe: &'j Facility +struct BarberShop { + stop_time: Time, rng_a: SmallRng, rng_s: SmallRng, joe: Rc<Facility> } -impl<'j> BarberShop<'j> { - async fn actions(self, sim: SimContext<'j>) { +impl BarberShop { + async fn actions(self, sim: SimContext<'_>) { // unpack the barber shop structure for easier access let Self { stop_time, mut rng_a, mut rng_s, joe } = self; + let joe2 = joe.clone(); // activate a process to generate the customers sim.activate(async move { @@ -69,7 +69,8 @@ impl<'j> BarberShop<'j> { while sim.now() < stop_time { // activate the next customer sim.activate(Customer { - joe, rng: SmallRng::from_seed(rng_s.gen()) + joe: joe.clone(), + rng: SmallRng::from_seed(rng_s.gen()) }.actions(sim)); // wait some time before activating the next customer sim.advance(rng_a.sample(dist)).await; @@ -80,14 +81,14 @@ impl<'j> BarberShop<'j> { sim.advance(self.stop_time).await; // finish processing the queue (no more customers arrive) - joe.seize().await; + joe2.seize().await; } } /// Customer process with access to the barber and a random number generator. -struct Customer<'j> { joe: &'j Facility, rng: SmallRng } +struct Customer { joe: Rc<Facility>, rng: SmallRng } -impl Customer<'_> { +impl Customer { pub async fn actions(mut self, sim: SimContext<'_>) { // access the barber self.joe.seize().await; diff --git a/src/bin/barbershop.rs b/src/bin/barbershop.rs index bceb8b328e559585be9deb03caf2ff453aaa98d6..d90e2eaedd76082b6e66e09717d74328bdf53e02 100644 --- a/src/bin/barbershop.rs +++ b/src/bin/barbershop.rs @@ -1,4 +1,4 @@ -use simcore_rs::{Time, SimContext, Facility, RandomVar, simulation}; +use simcore_rs::{Process, Time, SimContext, Facility, RandomVar, simulation}; use rand::{distributions::Uniform, rngs::SmallRng, SeedableRng, Rng}; // helper constants @@ -16,7 +16,7 @@ fn main() { let rv = &RandomVar::new(); // the main process - simulation(|sim| async move { + simulation(|sim| Process::new(async move { // activate a process to generate the customers sim.activate(async move { let dist = Uniform::new(12.0, 24.0); @@ -28,7 +28,9 @@ fn main() { while sim.now() < STOP_TIME { // activate the next customer sim.activate(Customer { - joe, rv, rng: SmallRng::from_seed(rng_s.gen()) + joe: joe.clone(), + rv: rv.clone(), + rng: SmallRng::from_seed(rng_s.gen()) }.actions(sim)); // wait some time before activating the next customer @@ -41,16 +43,16 @@ fn main() { // finish processing the queue (no more customers arrive) joe.seize().await; - }); + })); println!("Stats: {:.3}", rv); } /// Customer process with access to the barber and a random number generator. -struct Customer<'j> { joe: &'j Facility, rv: &'j RandomVar, rng: SmallRng } +struct Customer<'c> { joe: &'c Facility, rv: &'c RandomVar, rng: SmallRng } -impl Customer<'_> { - pub async fn actions(mut self, sim: SimContext<'_>) { +impl<'c> Customer<'c> { + pub async fn actions(mut self, sim: SimContext<'c>) { // access the barber and record the time for the report let time = sim.now(); self.joe.seize().await; diff --git a/src/lib.rs b/src/lib.rs index 359e6fdac942fe19bd3ac23853943471e1c1305d..8abc45c797655117c0d2a341ac97abbeb36d505d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,43 +35,31 @@ struct Scheduler<'s> { pub struct SimContext<'s> { handle: *const Scheduler<'s> } /// Performs a single simulation run. -/// Input is a function that takes a simulation context and returns the future -/// that is used to initialize the first process. -pub fn simulation<'s,G,F>(main: G) - where G: FnOnce(SimContext<'s>) -> F, - F: Future<Output = ()> + 's -{ +/// +/// Input is a function that takes a simulation context and returns the first +/// process. It would be better if the function only needed to return the future +/// needed to initialize the first process, but then the function signature gets +/// more complicated and is harder to explain in a paper. +pub fn simulation<'s>(main: impl FnOnce(SimContext<'s>) -> Process<'s>) { // (this function's signature could be better but there seems to be a // bug with higher-order trait-bounds right now that prevents me from // referring to the same lifetime in multiple type parameters) - use std::task::{Waker, RawWaker}; // create a fresh scheduler and a handle to it let sched = Scheduler::new(); let sim = SimContext { handle: &sched }; // construct a custom context to pass into the poll()-method - let event_waker = Box::leak(Box::new(StateEventWaker { - process: Process::default(), - context: sim - })); - let waker = unsafe { - Waker::from_raw(RawWaker::new( - event_waker as *const _ as *const (), - &WAKER_VTABLE - )) - }; + let event_waker = StateEventWaker::new(sim); + let waker = unsafe { event_waker.as_waker() }; let mut cx = Context::from_waker(&waker); // evaluate the passed function for the main process and schedule it - let root = Process::new(main(sim)); + let root = main(sim); sched.schedule(root.clone()); // pop processes until empty or the main process terminates while let Some(process) = sched.next_event() { - // register the next process with the context - event_waker.process = process.clone(); - if process.poll(&mut cx).is_ready() && process == root { break; } } @@ -225,23 +213,85 @@ impl Ord for NextEvent<'_> { /* **************************** specialized waker *************************** */ -#[derive(Clone)] /// Complex waker that is used to implement state events. -struct StateEventWaker<'s> { +/// +/// This is the shallow version that is created on the stack of the function +/// running the event loop. It assumes that the stored process is the currently +/// active one and creates the deep version when it is cloned. +struct StateEventWaker<'s> { context: SimContext<'s> } + +/// Deep version of the complex waker used to implement state events. +/// +/// This version actually holds a process as well as a simulation context on the +/// heap. Dropping the waker releases the memory. +struct StateEventWakerDeep<'s> { process: Process<'s>, context: SimContext<'s> } -impl StateEventWaker<'_> { +impl<'s> StateEventWaker<'s> { + /// Creates a new (shallow) waker using only the simulation context. + fn new(sim: SimContext<'s>) -> Self { + StateEventWaker { context: sim } + } + + /// Constructs a new waker using only a reference. + /// + /// This function is unsafe because it is up to the user to ensure that the + /// waker doesn't outlive the reference. + unsafe fn as_waker(&self) -> task::Waker { + task::Waker::from_raw( + task::RawWaker::new( + self as *const _ as *const (), + &WAKER_VTABLE + ) + ) + } + + /// Creates a deep waker from a reference. + /// + /// This function is safe because it allocates memory on the heap. + fn deep_waker(&self) -> task::RawWaker { + StateEventWakerDeep::raw_waker( + self.context, self.context.active() + ) + } + unsafe fn clone(this: *const ()) -> task::RawWaker { - let waker = &*(this as *const Self); + (&*(this as *const Self)).deep_waker() + } + + unsafe fn wake(_this: *const ()) { + unreachable!("attempting to awake the active process") + } + + unsafe fn wake_by_ref(_this: *const ()) { + unreachable!("attempting to awake the active process") + } + + unsafe fn drop(_this: *const ()) { + // memory released in the main event loop + } +} + +impl<'s> StateEventWakerDeep<'s> { + /// Constructs a raw waker from a simulation context and a process. + fn raw_waker(sim: SimContext<'s>, process: Process<'s>) -> task::RawWaker { + let waker = Box::leak(Box::new( + Self { process, context: sim } + )); task::RawWaker::new( - Box::into_raw(Box::new(waker.clone())) as *const (), - &WAKER_VTABLE + waker as *const _ as *const (), + &WAKER_DEEP_VTABLE ) } + unsafe fn clone(this: *const ()) -> task::RawWaker { + let waker = &*(this as *const Self); + Self::raw_waker(waker.context, waker.process.clone()) + } + unsafe fn wake(this: *const ()) { let waker = Box::from_raw(this as *const Self as *mut Self); waker.context.reactivate(waker.process); @@ -265,6 +315,14 @@ static WAKER_VTABLE: task::RawWakerVTable = task::RawWakerVTable::new( StateEventWaker::drop ); +/// Virtual function table for the deep waker. +static WAKER_DEEP_VTABLE: task::RawWakerVTable = task::RawWakerVTable::new( + StateEventWakerDeep::clone, + StateEventWakerDeep::wake, + StateEventWakerDeep::wake_by_ref, + StateEventWakerDeep::drop +); + /* *************************** specialized futures ************************** */ /// Future that blocks on the first call and returns on the second one.