diff --git a/Cargo.lock b/Cargo.lock index 0b71cef861ef3f140f5b643769e510319195ad07..bd362d1de534d1e5a889868b6ef198a4424f6dde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,15 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "cc" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +dependencies = [ + "jobserver", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -245,6 +254,15 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.42" @@ -550,6 +568,7 @@ dependencies = [ name = "simcore-rs" version = "0.1.0" dependencies = [ + "cc", "criterion", "itertools 0.10.0", "rand", diff --git a/Cargo.toml b/Cargo.toml index dcce310c19f2815e01690ca5e238f3afda7b139f..3ffe8c1447f603cb07d6aa811f82ce931d79df8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,14 +20,20 @@ rayon = "1.5" criterion = { version = "0.3.4", features = ["html_reports"]} itertools = "0.10" -[[bench]] +[build-dependencies] +cc = { version = "1.0", features = ["parallel"] } + +[[example]] name = "barbershop" +bench = true harness = false -[[bench]] +[[example]] name = "ferry" +bench = true harness = false -[[bench]] +[[example]] name = "philosophers" +bench = true harness = false diff --git a/benches/barbershop.rs b/benches/barbershop.rs deleted file mode 100644 index 179410d8de3e021753f69fbd9f40ad4849f2f1bf..0000000000000000000000000000000000000000 --- a/benches/barbershop.rs +++ /dev/null @@ -1,121 +0,0 @@ -use criterion::{Criterion, BenchmarkId, criterion_group, criterion_main, - BatchSize, PlotConfiguration, AxisScale, SamplingMode}; - -mod support; - -const SLX_PATH: &'static str = "C:/Wolverine/SLX/sse.exe"; -const RANGE: u32 = 10; -const STEP: Time = 1000.0; - -fn barbershop_bench(c: &mut Criterion) { - let mut group = c.benchmark_group("Barbershop"); - - // set-up the benchmark parameters - group.confidence_level(0.99); - group.plot_config( - PlotConfiguration::default() - .summary_scale(AxisScale::Logarithmic) - ); - group.sampling_mode(SamplingMode::Linear); - - // vary in the length of the simulation run - for sim_duration in (0..RANGE).map(|c| Time::from(1 << c)*STEP) { - let duration = sim_duration.to_string(); - let args = [ - "/silent", - "/stdout", - "/noicon", - "/nowarn", - "/noxwarn", - "/#BENCH", - "slx\\barbershop.slx", - duration.as_str() - ]; - - // benchmark the SLX implementation - group.bench_function( - BenchmarkId::new("SLX", sim_duration), - |b| b.iter_custom(|iters| - support::slx_bench( - SLX_PATH, - &args, - iters as usize - ).expect("couldn't benchmark the SLX program") - ) - ); - - // benchmark the Rust implementation - group.bench_function( - BenchmarkId::new("Rust", sim_duration), - |b| b.iter_batched( - || Shared { joe: Facility::new(), wait_time: RandomVar::new() }, - |shared| simulation( - shared, - |sim| sim.process(barbershop(sim, sim_duration)) - ), - BatchSize::SmallInput - ) - ); - } - - group.finish(); -} - -criterion_group!(benches, barbershop_bench); -criterion_main!(benches); - -/* *************************** Barbershop Example *************************** */ -use simcore_rs::{Time, SimContext, Facility, RandomVar, simulation}; -use rand::{distributions::Uniform, rngs::SmallRng, SeedableRng, Rng}; - -/// Globally shared data. -struct Shared { - joe: Facility, - wait_time: RandomVar -} - -// helper constants -const SEED_A : u64 = 100000; -const SEED_S : u64 = 200000; - -/// Customer process with access to the barber and a random processing delay. -struct Customer { delay: Time } - -impl Customer { - pub async fn actions(self, sim: SimContext<'_,Shared>) { - // access the barber and record the time for the report - let arrival_time = sim.now(); - sim.shared().joe.seize().await; - sim.shared().wait_time.tabulate(sim.now() - arrival_time); - - // spend time - sim.advance(self.delay).await; - // release the barber - sim.shared().joe.release(); - } -} - -async fn barbershop(sim: SimContext<'_,Shared>, duration: Time) { - // pseudo random number generators referenced from within the main process - let mut rng_a = SmallRng::seed_from_u64(SEED_A); - let mut rng_s = SmallRng::seed_from_u64(SEED_S); - - // activate a process to generate the customers - sim.activate(async move { - let dist_a = Uniform::new(12.0, 24.0); - let dist_s = Uniform::new(12.0, 18.0); - loop { - sim.advance(rng_a.sample(dist_a)).await; - if sim.now() >= duration { return; } - sim.activate(Customer { - delay: rng_s.sample(dist_s) - }.actions(sim)); - } - }); - - // wait until the store closes - sim.advance(duration).await; - - // finish processing the queue (no more customers arrive) - sim.shared().joe.seize().await; -} diff --git a/benches/ferry.rs b/benches/ferry.rs deleted file mode 100644 index c7c46c60b9d28809139361470e17b95a1bf2db96..0000000000000000000000000000000000000000 --- a/benches/ferry.rs +++ /dev/null @@ -1,199 +0,0 @@ -use criterion::{Criterion, BenchmarkId, criterion_group, criterion_main, - BatchSize, PlotConfiguration, AxisScale, SamplingMode}; - -mod support; - -const SLX_PATH: &'static str = "C:/Wolverine/SLX/sse.exe"; -const SEED: u64 = 100000; -const RANGE: u32 = 10; -const STEP: Time = 1000.0; - -fn ferry_bench(c: &mut Criterion) { - let mut group = c.benchmark_group("Ferry"); - - // set-up the benchmark parameters - group.confidence_level(0.99); - group.plot_config( - PlotConfiguration::default() - .summary_scale(AxisScale::Logarithmic) - ); - group.sampling_mode(SamplingMode::Linear); - - // vary in the length of the simulation run - for (i, sim_duration) in (0..RANGE).map(|c| Time::from(1 << c)*STEP).enumerate() { - let duration = sim_duration.to_string(); - let args = [ - "/silent", - "/stdout", - "/noicon", - "/nowarn", - "/noxwarn", - "/#BENCH", - "slx\\ferry.slx", - duration.as_str() - ]; - - // benchmark the SLX implementation - group.bench_function( - BenchmarkId::new("SLX", sim_duration), - |b| b.iter_custom(|iters| - support::slx_bench( - SLX_PATH, - &args, - iters as usize - ).expect("couldn't benchmark the SLX program") - ) - ); - - // benchmark the Rust implementation - group.bench_function( - BenchmarkId::new("Rust", sim_duration), - |b| b.iter_batched( - || Shared { - master_rng: RefCell::new(SmallRng::seed_from_u64(SEED*((i+1) as u64))), - ferry_cargo_len: RandomVar::new(), - ferry_load_time: RandomVar::new(), - car_wait_time: RandomVar::new(), - }, - |shared| simulation( - shared, - |sim| sim.process(ferry(sim, sim_duration, 2, 4)) - ), - BatchSize::SmallInput - ) - ); - } - - group.finish(); -} - -criterion_group!(benches, ferry_bench); -criterion_main!(benches); - -/* ****************************** Ferry Example ***************************** */ -use simcore_rs::{Time, SimContext, Sender, Receiver, RandomVar, channel, simulation}; -use rand::{rngs::SmallRng, SeedableRng, Rng}; -use rand_distr::{Exp, Normal, Distribution}; -use std::cell::RefCell; - -const HARBOR_DISTANCE: Time = 10.0; -const FERRY_TIMEOUT : Time = 5.0; -const FERRY_CAPACITY : usize = 5; - -struct Shared { - master_rng: RefCell<SmallRng>, - ferry_cargo_len: RandomVar, - ferry_load_time: RandomVar, - car_wait_time: RandomVar, -} - -#[derive(Debug)] -struct Car { - arrival_time: Time, - load_duration: Time -} - -struct Pier { - rng: SmallRng, - landing_site: Sender<Car> -} - -struct Ferry { - cargo: Vec<Car>, - timeout: Time, - travel_time: Time, - piers: Vec<Receiver<Car>> -} - -impl Pier { - async fn actions(mut self, sim: SimContext<'_,Shared>) { - let arrival_delay = Exp::new(0.1).unwrap(); - let loading_delay = Normal::new(0.5, 0.2).unwrap(); - - loop { - sim.advance(arrival_delay.sample(&mut self.rng)).await; - self.landing_site.send(Car { - arrival_time: sim.now(), - load_duration: loading_delay - .sample_iter(&mut self.rng) - .find(|&val| val >= 0.0) - .unwrap() - }).await.expect("no ferries in the simulation"); - } - } -} - -impl Ferry { - async fn actions(mut self, sim: SimContext<'_,Shared>) { - loop { - for pier in self.piers.iter() { - // unload the cars - for car in self.cargo.drain(..) { - sim.advance(car.load_duration).await; - } - - let begin_loading = sim.now(); - - // wait until new cars arrive or a timeout occurs - while self.cargo.len() < self.cargo.capacity() { - if let Some(car) = sim.select( - pier.recv(), - async { sim.advance(self.timeout).await; None } - ).await { - // a car arrived in time - sim.shared().car_wait_time.tabulate(sim.now() - car.arrival_time); - sim.advance(car.load_duration).await; - self.cargo.push(car); - } else { - // the timeout has been triggered - break; - } - } - - sim.shared().ferry_load_time.tabulate(sim.now() - begin_loading); - sim.shared().ferry_cargo_len.tabulate(self.cargo.len() as f64); - - // travel to the next harbor - sim.advance(self.travel_time).await; - } - } - } -} - -async fn ferry(sim: SimContext<'_,Shared>, duration: Time, ferries: usize, harbors: usize) { - let mut ports = Vec::with_capacity(harbors); - - // create all of the harbors - for _ in 0..harbors { - let (sx, rx) = channel(); - let harbor = Pier { - rng: SmallRng::from_seed(sim.shared().master_rng.borrow_mut().gen()), - landing_site: sx - }; - sim.activate(harbor.actions(sim)); - ports.push(rx); - } - - // create all of the ferries - for i in 0..ferries { - let ferry = Ferry { - cargo: Vec::with_capacity(FERRY_CAPACITY), - timeout: FERRY_TIMEOUT, - travel_time: HARBOR_DISTANCE, - piers: ports.iter().skip(i) - .chain(ports.iter().take(i)).cloned().collect() - }; - sim.activate(ferry.actions(sim)); - } - - // await the end of the simulation - sim.advance(duration).await; - - // take cars into account that weren't picked up by a ferry - for port in ports { - for _ in 0..port.len() { - let car = port.recv().await.unwrap(); - sim.shared().car_wait_time.tabulate(sim.now() - car.arrival_time); - } - } -} diff --git a/benches/philosophers.rs b/benches/philosophers.rs deleted file mode 100644 index cb4aae7832579dc0c3c98274cea4f08a190c2707..0000000000000000000000000000000000000000 --- a/benches/philosophers.rs +++ /dev/null @@ -1,191 +0,0 @@ -use criterion::{Criterion, BenchmarkId, criterion_group, criterion_main, - PlotConfiguration, AxisScale, SamplingMode}; - -mod support; - -const SLX_PATH: &'static str = "C:/Wolverine/SLX/sse.exe"; -const SEED: u64 = 100000; -const RANGE: u32 = 10; -const STEP: u64 = 10; -const PHILOSOPHER_COUNT: usize = 5; - -fn philosopher_bench(c: &mut Criterion) { - let mut group = c.benchmark_group("Philosophers"); - - // benchmark the SLX implementation - group.confidence_level(0.99); - group.plot_config( - PlotConfiguration::default() - .summary_scale(AxisScale::Logarithmic) - ); - group.sampling_mode(SamplingMode::Linear); - - // vary in the number of performed experiments - for experiment_count in (0..RANGE).map(|c| (1 << c)*STEP) { - let count = experiment_count.to_string(); - let args = [ - "/silent", - "/stdout", - "/noicon", - "/nowarn", - "/noxwarn", - "/#BENCH", - "slx\\philosophers.slx", - count.as_str() - ]; - - // benchmark the SLX implementation - group.bench_function( - BenchmarkId::new("SLX", experiment_count), - |b| b.iter_custom(|iters| - support::slx_bench( - SLX_PATH, - &args, - iters as usize - ).expect("couldn't benchmark the SLX program") - ) - ); - - // benchmark the Rust implementation - group.bench_function( - BenchmarkId::new("Rust", experiment_count), - |b| b.iter(|| (1..=experiment_count) - .into_par_iter() - .map(|experiment_no| - simulation( - // global data - Shared { - master_rng: RefCell::new(SmallRng::seed_from_u64(SEED*experiment_no)), - sim_duration: Cell::default() - }, - // simulation entry point - |sim| sim.process(philosophers(sim, PHILOSOPHER_COUNT)) - ).sim_duration.get() - ) - .fold( - || RandomVar::new(), - |var, duration| { var.tabulate(duration); var } - ) - .reduce( - || RandomVar::new(), - |var_a, var_b| { var_a.merge(&var_b); var_a } - ) - ) - ); - } - - group.finish(); -} - -criterion_group!(benches, philosopher_bench); -criterion_main!(benches); - -/* *********************** Dining Philosophers Example ********************** */ -use simcore_rs::{SimContext, Time, RandomVar, Control, until, simulation}; -use std::{rc::Rc, cell::{RefCell, Cell}}; -use rand::{rngs::SmallRng, SeedableRng, Rng}; -use rand_distr::{Exp, Normal, Uniform, Distribution}; -use rayon::prelude::*; - -struct Shared { - master_rng: RefCell<SmallRng>, - sim_duration: Cell<Time> -} - -struct Table { - forks: Vec<Control<bool>>, - forks_held: Control<usize>, - forks_awaited: Control<usize> -} - -impl Table { - fn new(forks: usize) -> Self { - Table { - forks: (0..forks).map(|_| Control::new(false)).collect(), - forks_held: Control::default(), - forks_awaited: Control::default() - } - } - - // blocks until the requested fork is available and acquires it - async fn acquire_fork(&self, i: usize) { - let num = i % self.forks.len(); - self.forks_awaited.set(self.forks_awaited.get() + 1); - until(&self.forks[num], |fork| !fork.get()).await; - self.forks[num].set(true); - self.forks_awaited.set(self.forks_awaited.get() - 1); - self.forks_held.set(self.forks_held.get() + 1); - } - - // returns the requested fork to the table - fn release_fork(&self, i: usize) { - self.forks[i % self.forks.len()].set(false); - self.forks_held.set(self.forks_held.get() - 1); - } -} - -struct Philosopher { - table: Rc<Table>, - seat: usize, - rng: SmallRng -} - -impl Philosopher { - async fn actions(mut self, sim: SimContext<'_, Shared>) { - let thinking_duration = Exp::new(1.0).unwrap(); - let artificial_delay = Uniform::new(0.1, 0.2); - let eating_duration = Normal::new(0.5, 0.2).unwrap(); - - loop { - // spend some time pondering the nature of things - sim.advance(thinking_duration.sample(&mut self.rng)).await; - - // acquire the first fork - self.table.acquire_fork(self.seat).await; - - // introduce an artificial delay to leave room for deadlocks - sim.advance(artificial_delay.sample(&mut self.rng)).await; - - // acquire the second fork - self.table.acquire_fork(self.seat + 1).await; - - // spend some time eating - sim.advance( - eating_duration - .sample_iter(&mut self.rng) - .find(|&val| val >= 0.0) - .unwrap() - ).await; - - // release the forks - self.table.release_fork(self.seat + 1); - self.table.release_fork(self.seat); - } - } -} - -async fn philosophers(sim: SimContext<'_, Shared>, count: usize) { - let table = Rc::new(Table::new(count)); - - // create the philosopher-processes and seat them - for i in 0..count { - sim.activate(Philosopher { - table: table.clone(), - seat: i, - rng: SmallRng::from_seed(sim.shared().master_rng.borrow_mut().gen()) - }.actions(sim)); - } - - // wait for the precise configuration indicating a deadlock - // (we technically don't really have to do this, because deadlock-detection - // is automatic since it means that no processes are scheduled and the - // simulation ends; SLX doesn't have that ability, though) - until( - (&table.forks_held, &table.forks_awaited), - |(held, awaited)| - held.get() == count && awaited.get() == count - ).await; - - // tabulate the current system time - sim.shared().sim_duration.set(sim.now()); -} \ No newline at end of file diff --git a/benches/support/mod.rs b/benches/support/mod.rs deleted file mode 100644 index f38c7cd1ad9006fe54c2a3f687f526c62dd67e05..0000000000000000000000000000000000000000 --- a/benches/support/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::{process::Command, time::Duration, iter::once}; -use itertools::Itertools; - -/// Runs the SLX runtime environment once for an SLX program that measures its -/// own duration and returns it on the standard output. -/// -/// Errors during this execution are returned back to the caller. -fn slx_run_once(program: &str, arguments: &[&str], iterations: usize) -> Result<Duration, (i32, String)> { - // start the SLX runtime and wait for its return - let rc = Command::new(program) - .args(arguments) - .arg(iterations.to_string()) - .output() - .map_err(|err| (-1, format!("Failed to run the SLX program: {}", err)))?; - - if !rc.status.success() { - // check the error code and add a description to it - Err(rc.status - .code() - .map_or_else( - || (-10000, "Unknown error code".to_string()), - |code| { - (code, match code { - -10001 => "Unable to find a security key with SLX permission".to_string(), - -10002 => "The command line includes an invalid option".to_string(), - -10003 => "The source file cannot be found".to_string(), - -10004 => "The specified/implied output file cannot be written".to_string(), - -10005 => "The program contains compile-time errors".to_string(), - -10006 => "The program has terminated with a run-time error".to_string(), - -10007 => "The /genrts option failed".to_string(), - _ => format!("Unknown error code: {}", code) - }) - } - ) - ) - } else { - Ok(Duration::from_secs_f64( - (&String::from_utf8_lossy(&rc.stdout)).trim().parse().unwrap() - )) - } -} - -/// Repeats runs of an SLX program for a specified number of times and collects -/// the total (real-time) duration of those combined runs. -/// -/// The SLX program in question has to report its own real-time duration using -/// the standard output. -pub fn slx_bench(program: &str, arguments: &[&str], iterations: usize) -> Result<Duration, String> { - // try to complete the iterations in a single run of the SLX program - slx_run_once(program, arguments, iterations) - .or_else(|(code, desc)| { - if code == -10006 { - // Runtime error: this happens when the free version of SLX - // exceeds its total instance budget, either through a - // complicated scenario or too many iterations in one call. - // We can still make this work by running SLX multiple times - // with lower iteration counts and combining the timings. - // This effectively trades benchmark speed with simulation model - // size. - (1..) - .map(|i| iterations >> i) - .take_while(|&chunk_size| chunk_size > 0) - // .inspect(|chunk_size| println!("reducing the chunk size to {}", chunk_size)) - .map(|chunk_size| { - (0..(iterations - 1)/chunk_size) - .map(|_| chunk_size) - .chain(once((iterations - 1) % chunk_size + 1)) - .map(|size| - slx_run_once(program, arguments, size) - ) - .fold_ok(Duration::default(), |duration, run| duration + run) - }) - .find_map(|result| result.ok()) - .ok_or(desc) - } else { - Err(desc) - } - }) -} diff --git a/build.rs b/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..5e05786f8125a742f92cdb22ca8e2ea1d2690669 --- /dev/null +++ b/build.rs @@ -0,0 +1,106 @@ + +// compiles the ODEMx-library and the C++ examples to link against +fn main() { + // attempt to translate the library using the native platform's compiler + let result = cc::Build::new() + .cpp(true) + .define("RUST_FFI", None) + .includes(&[ + "odemx-lite/include", + "odemx-lite/external/CppLog/include" + ]) + .flag_if_supported("-std=c++11") + .files(&[ + "odemx-lite/src/base/Comparators.cpp", + "odemx-lite/src/base/Continuous.cpp", + "odemx-lite/src/base/DefaultSimulation.cpp", + "odemx-lite/src/base/Event.cpp", + "odemx-lite/src/base/ExecutionList.cpp", + "odemx-lite/src/base/Process.cpp", + "odemx-lite/src/base/Sched.cpp", + "odemx-lite/src/base/Scheduler.cpp", + "odemx-lite/src/base/Simulation.cpp", + "odemx-lite/src/base/continuous/Continuous.cpp", + "odemx-lite/src/base/continuous/DfDt.cpp", + "odemx-lite/src/base/continuous/JacobiMatrix.cpp", + "odemx-lite/src/base/continuous/Monitor.cpp", + "odemx-lite/src/base/continuous/ODEObject.cpp", + "odemx-lite/src/base/continuous/ODESolver.cpp", + "odemx-lite/src/base/continuous/Rate.cpp", + "odemx-lite/src/base/continuous/State.cpp", + "odemx-lite/src/base/continuous/StateEvent.cpp", + "odemx-lite/src/base/continuous/VariableContainer.cpp", + "odemx-lite/src/base/control/ControlBase.cpp", + "odemx-lite/src/coroutine/Coroutine.cpp", + "odemx-lite/src/coroutine/CoroutineContext.cpp", + "odemx-lite/src/coroutine/ucFiber.cpp", + "odemx-lite/src/data/buffer/SimRecordBuffer.cpp", + "odemx-lite/src/data/buffer/StatisticsBuffer.cpp", + "odemx-lite/src/data/LoggingManager.cpp", + "odemx-lite/src/data/ManagedChannels.cpp", + "odemx-lite/src/data/output/ErrorWriter.cpp", + "odemx-lite/src/data/output/GermanTime.cpp", + "odemx-lite/src/data/output/Iso8601Time.cpp", + "odemx-lite/src/data/output/OStreamReport.cpp", + "odemx-lite/src/data/output/OStreamWriter.cpp", + "odemx-lite/src/data/output/TimeFormat.cpp", + "odemx-lite/src/data/Producer.cpp", + "odemx-lite/src/data/Report.cpp", + "odemx-lite/src/data/ReportProducer.cpp", + "odemx-lite/src/data/ReportTable.cpp", + "odemx-lite/src/data/SimRecord.cpp", + "odemx-lite/src/data/SimRecordFilter.cpp", + "odemx-lite/src/protocol/Device.cpp", + "odemx-lite/src/protocol/Entity.cpp", + "odemx-lite/src/protocol/ErrorModelDraw.cpp", + "odemx-lite/src/protocol/Layer.cpp", + "odemx-lite/src/protocol/Medium.cpp", + "odemx-lite/src/protocol/Sap.cpp", + "odemx-lite/src/protocol/Service.cpp", + "odemx-lite/src/protocol/ServiceProvider.cpp", + "odemx-lite/src/protocol/Stack.cpp", + "odemx-lite/src/random/ContinuousConst.cpp", + "odemx-lite/src/random/ContinuousDist.cpp", + "odemx-lite/src/random/DiscreteConst.cpp", + "odemx-lite/src/random/DiscreteDist.cpp", + "odemx-lite/src/random/Dist.cpp", + "odemx-lite/src/random/DistContext.cpp", + "odemx-lite/src/random/Draw.cpp", + "odemx-lite/src/random/Erlang.cpp", + "odemx-lite/src/random/NegativeExponential.cpp", + "odemx-lite/src/random/Normal.cpp", + "odemx-lite/src/random/Poisson.cpp", + "odemx-lite/src/random/RandomInt.cpp", + "odemx-lite/src/random/Uniform.cpp", + "odemx-lite/src/statistics/Accumulate.cpp", + "odemx-lite/src/statistics/Count.cpp", + "odemx-lite/src/statistics/Histogram.cpp", + "odemx-lite/src/statistics/Regression.cpp", + "odemx-lite/src/statistics/Sum.cpp", + "odemx-lite/src/statistics/Tab.cpp", + "odemx-lite/src/statistics/Tally.cpp", + "odemx-lite/src/synchronization/Bin.cpp", + "odemx-lite/src/synchronization/CondQ.cpp", + "odemx-lite/src/synchronization/IMemory.cpp", + "odemx-lite/src/synchronization/Memory.cpp", + "odemx-lite/src/synchronization/ProcessQueue.cpp", + "odemx-lite/src/synchronization/Queue.cpp", + "odemx-lite/src/synchronization/Res.cpp", + "odemx-lite/src/synchronization/Timer.cpp", + "odemx-lite/src/synchronization/Wait.cpp", + "odemx-lite/src/synchronization/WaitQ.cpp", + // example scenarios + "cpp/Barbershop/main.cpp", + "cpp/Ferry/main.cpp", + "cpp/Philosophers/main.cpp", + ]) + .try_compile("odemx"); + + // there is no need to stop the build altogether if the compilation failed + if let Err(msg) = result { + println!("cargo:warning=Compiling ODEMx-lite has failed."); + println!("cargo:warning=This library will still work, \ + the C++ benchmarks will not!"); + println!("cargo:warning={}", msg); + } +} diff --git a/examples/barbershop.rs b/examples/barbershop.rs index a29aa152cf482e2a1c4392c586df57d757d53df9..e30f1c822b85be6ce2c2c152e1b899f3fb816d30 100644 --- a/examples/barbershop.rs +++ b/examples/barbershop.rs @@ -1,4 +1,4 @@ -use simcore_rs::{Time, SimContext, Facility, RandomVar, simulation}; +use simcore_rs::{Time, SimContext, Facility, RandomVar}; use rand::{distributions::Uniform, rngs::SmallRng, SeedableRng, Rng}; /// Globally shared data. @@ -8,9 +8,8 @@ struct Shared { } // helper constants -const SIM_DURATION : Time = 60.0*24.0*7.0*3.0; // simulation runs over 3 weeks -const SEED_A : u64 = 100000; -const SEED_S : u64 = 200000; +const SEED_A: u64 = 100000; +const SEED_S: u64 = 200000; /// Customer process with access to the barber and a random processing delay. struct Customer { delay: Time } @@ -40,7 +39,7 @@ async fn barbershop(sim: SimContext<'_,Shared>, duration: Time) { let dist_s = Uniform::new(12.0, 18.0); loop { sim.advance(rng_a.sample(dist_a)).await; - if sim.now() >= SIM_DURATION { return; } + if sim.now() >= duration { return; } sim.activate(Customer { delay: rng_s.sample(dist_s) }.actions(sim)); @@ -54,14 +53,107 @@ async fn barbershop(sim: SimContext<'_,Shared>, duration: Time) { sim.shared().joe.seize().await; } +#[cfg(not(test))] fn main() { - let result = simulation( + let result = simcore_rs::simulation( // global data Shared { joe: Facility::new(), wait_time: RandomVar::new() }, // simulation entry point - |sim| sim.process(barbershop(sim, SIM_DURATION)) + |sim| sim.process(barbershop(sim, 60.0*24.0*7.0*3.0)) ); // print statistics println!("wait_time: {:#.3}", result.wait_time); } + +#[cfg(test)] +criterion::criterion_main!(bench::benches); + +#[cfg(test)] +mod slx; + +#[cfg(test)] +mod bench { + use super::*; + use criterion::{Criterion, BenchmarkId, BatchSize, PlotConfiguration, + AxisScale, SamplingMode, criterion_group}; + use std::time::Duration; + + mod odemx { + use std::os::raw::c_double; + + #[link(name = "odemx", kind = "static")] + extern { + pub fn barbershop(duration: c_double); + } + } + + const SLX_PATH: &'static str = "C:/Wolverine/SLX/se64.exe"; + const RANGE: u32 = 10; + const STEP: Time = 1000.0; + + fn barbershop_bench(c: &mut Criterion) { + let mut group = c.benchmark_group("Barbershop"); + + // set-up the benchmark parameters + group.confidence_level(0.99); + group.plot_config( + PlotConfiguration::default() + .summary_scale(AxisScale::Logarithmic) + ); + group.sampling_mode(SamplingMode::Linear); + group.measurement_time(Duration::from_secs(300)); + + // vary in the length of the simulation run + for sim_duration in (0..RANGE).map(|c| Time::from(1 << c)*STEP) { + let duration = sim_duration.to_string(); + let args = [ + "/silent", + "/stdout", + "/noicon", + "/nowarn", + "/noxwarn", + "/#BENCH", + "slx\\barbershop.slx", + duration.as_str() + ]; + + // benchmark the SLX implementation + group.bench_function( + BenchmarkId::new("SLX", sim_duration), + |b| b.iter_custom(|iters| + slx::slx_bench( + SLX_PATH, + &args, + iters as usize + ).expect("couldn't benchmark the SLX program") + ) + ); + + // benchmark the C++ implementation + group.bench_function( + BenchmarkId::new("ODEMx", sim_duration), + |b| b.iter( + || unsafe { odemx::barbershop(sim_duration as _) } + ) + ); + + // benchmark the Rust implementation + group.bench_function( + BenchmarkId::new("Rust", sim_duration), + |b| b.iter_batched( + || Shared { joe: Facility::new(), wait_time: RandomVar::new() }, + |shared| simcore_rs::simulation( + shared, + |sim| sim.process(barbershop(sim, sim_duration)) + ), + BatchSize::SmallInput + ) + ); + } + + group.finish(); + } + + criterion_group!(benches, barbershop_bench); +} diff --git a/examples/ferry.rs b/examples/ferry.rs index 8067af23657d4b651d49b91c82f8bfea036a13c6..e03f638199961328ccb3ba8de22e187615c93cc2 100644 --- a/examples/ferry.rs +++ b/examples/ferry.rs @@ -1,8 +1,11 @@ -use simcore_rs::{Time, SimContext, Sender, Receiver, RandomVar, channel, simulation}; +use simcore_rs::{Time, SimContext, Sender, Receiver, RandomVar, channel}; use rand::{rngs::SmallRng, SeedableRng, Rng}; use rand_distr::{Exp, Normal, Distribution}; use std::cell::RefCell; +const SEED: u64 = 100000; +const FERRY_COUNT: usize = 2; +const HARBOR_COUNT: usize = 4; const HARBOR_DISTANCE: Time = 10.0; const FERRY_TIMEOUT : Time = 5.0; const FERRY_CAPACITY : usize = 5; @@ -122,12 +125,9 @@ async fn ferry(sim: SimContext<'_,Shared>, duration: Time, ferries: usize, harbo } } -const SEED: u64 = 100000; -const FERRY_COUNT: usize = 2; -const HARBOR_COUNT: usize = 4; - +#[cfg(not(test))] fn main() { - let result = simulation( + let result = simcore_rs::simulation( // global data Shared { master_rng: RefCell::new(SmallRng::seed_from_u64(SEED)), @@ -136,7 +136,9 @@ fn main() { car_wait_time: RandomVar::new(), }, // simulation entry point - |sim| sim.process(ferry(sim, 24.0*60.0*7.0, FERRY_COUNT, HARBOR_COUNT)) + |sim| sim.process( + ferry(sim, 24.0*60.0*7.0, FERRY_COUNT, HARBOR_COUNT) + ) ); println!("Number of harbors: {}", HARBOR_COUNT); @@ -145,3 +147,106 @@ fn main() { println!("Ferry cargo len: {:#.3}", result.ferry_cargo_len); println!("Ferry load time: {:#.3}", result.ferry_load_time); } + +#[cfg(test)] +criterion::criterion_main!(bench::benches); + +#[cfg(test)] +mod slx; + +#[cfg(test)] +mod bench { + use super::*; + use criterion::{Criterion, BenchmarkId, BatchSize, PlotConfiguration, + AxisScale, SamplingMode, criterion_group}; + use std::time::Duration; + + mod odemx { + use std::os::raw::{c_double, c_uint}; + + #[link(name = "odemx", kind = "static")] + extern { + pub fn ferry(duration: c_double, ferries: c_uint, harbors: c_uint); + } + } + + const SLX_PATH: &'static str = "C:/Wolverine/SLX/se64.exe"; + const RANGE: u32 = 10; + const STEP: Time = 1000.0; + + fn ferry_bench(c: &mut Criterion) { + let mut group = c.benchmark_group("Ferry"); + + // set-up the benchmark parameters + group.confidence_level(0.99); + group.plot_config( + PlotConfiguration::default() + .summary_scale(AxisScale::Logarithmic) + ); + group.sampling_mode(SamplingMode::Linear); + group.measurement_time(Duration::from_secs(600)); + + // vary in the length of the simulation run + for (i, sim_duration) in (0..RANGE).map(|c| Time::from(1 << c)*STEP).enumerate() { + let duration = sim_duration.to_string(); + let args = [ + "/silent", + "/stdout", + "/noicon", + "/nowarn", + "/noxwarn", + "/#BENCH", + "slx\\ferry.slx", + duration.as_str() + ]; + + // benchmark the SLX implementation + group.bench_function( + BenchmarkId::new("SLX", sim_duration), + |b| b.iter_custom(|iters| + slx::slx_bench( + SLX_PATH, + &args, + iters as usize + ).expect("couldn't benchmark the SLX program") + ) + ); + + // benchmark the C++ implementation + group.bench_function( + BenchmarkId::new("ODEMx", sim_duration), + |b| b.iter(|| unsafe { + odemx::ferry( + sim_duration as _, + FERRY_COUNT as _, + HARBOR_COUNT as _ + ) + }) + ); + + // benchmark the Rust implementation + group.bench_function( + BenchmarkId::new("Rust", sim_duration), + |b| b.iter_batched( + || Shared { + master_rng: RefCell::new(SmallRng::seed_from_u64(SEED*((i+1) as u64))), + ferry_cargo_len: RandomVar::new(), + ferry_load_time: RandomVar::new(), + car_wait_time: RandomVar::new(), + }, + |shared| simcore_rs::simulation( + shared, + |sim| sim.process( + ferry(sim, sim_duration, FERRY_COUNT, HARBOR_COUNT) + ) + ), + BatchSize::SmallInput + ) + ); + } + + group.finish(); + } + + criterion_group!(benches, ferry_bench); +} diff --git a/examples/philosophers.rs b/examples/philosophers.rs index cdcadccd9ceea17dba0818a72459dab9489610c3..c89973f078436a2efe3d829993627b464b71a6de 100644 --- a/examples/philosophers.rs +++ b/examples/philosophers.rs @@ -1,9 +1,12 @@ -use simcore_rs::{SimContext, Time, RandomVar, Control, until, simulation}; +use simcore_rs::{SimContext, Time, RandomVar, Control, until}; use std::{rc::Rc, cell::{RefCell, Cell}}; use rand::{rngs::SmallRng, SeedableRng, Rng}; use rand_distr::{Exp, Normal, Uniform, Distribution}; use rayon::prelude::*; +const PHILOSOPHER_COUNT: usize = 5; +const SEED: u64 = 100000; + struct Shared { master_rng: RefCell<SmallRng>, sim_duration: Cell<Time> @@ -81,7 +84,7 @@ impl Philosopher { } } -async fn philosophers(sim: SimContext<'_, Shared>, count: usize) { +async fn run_once(sim: SimContext<'_, Shared>, count: usize) { let table = Rc::new(Table::new(count)); // create the philosopher-processes and seat them @@ -94,9 +97,10 @@ async fn philosophers(sim: SimContext<'_, Shared>, count: usize) { } // wait for the precise configuration indicating a deadlock - // (we technically don't really have to do this, because deadlock-detection - // is automatic since it means that no processes are scheduled and the - // simulation ends; SLX doesn't have that ability, though) + // (we technically don't have to do this, because deadlocks imply that no + // processes can advance, causing the simulation to end anyways; + // SLX doesn't allow us to record the time of that event, so we can't use + // it here either) until( (&table.forks_held, &table.forks_awaited), |(held, awaited)| @@ -107,22 +111,21 @@ async fn philosophers(sim: SimContext<'_, Shared>, count: usize) { sim.shared().sim_duration.set(sim.now()); } -const SEED : u64 = 100000; -const EXPERIMENT_COUNT : u64 = 500; -const PHILOSOPHER_COUNT : usize = 5; - -fn main() { - let sim_duration = (1..=EXPERIMENT_COUNT).into_par_iter() +fn philosophers(count: usize, reruns: usize) -> RandomVar { + // use thread-based parallelism to concurrently run simulation models + (1..=reruns) + .into_par_iter() .map(|i| - simulation( + simcore_rs::simulation( // global data Shared { - master_rng: RefCell::new(SmallRng::seed_from_u64(i*SEED)), + master_rng: RefCell::new(SmallRng::seed_from_u64(i as u64 * SEED)), sim_duration: Cell::default() }, // simulation entry point - |sim| sim.process(philosophers(sim, PHILOSOPHER_COUNT)) - ).sim_duration.get() + |sim| + sim.process(run_once(sim, count)) + ).sim_duration.get() ) .fold( || RandomVar::new(), @@ -131,7 +134,100 @@ fn main() { .reduce( || RandomVar::new(), |var_a, var_b| { var_a.merge(&var_b); var_a } - ); + ) +} + +#[cfg(not(test))] +fn main() { + const EXPERIMENT_COUNT : usize = 500; + let sim_duration = philosophers(PHILOSOPHER_COUNT, EXPERIMENT_COUNT); println!("sim_duration: {:#}", sim_duration); } + +#[cfg(test)] +criterion::criterion_main!(bench::benches); + +#[cfg(test)] +mod slx; + +#[cfg(test)] +mod bench { + use super::*; + use criterion::{Criterion, BenchmarkId, PlotConfiguration, AxisScale, + SamplingMode, criterion_group}; + mod odemx { + use std::os::raw::c_uint; + + #[link(name = "odemx", kind = "static")] + extern { + pub fn philosophers(count: c_uint, reruns: c_uint); + } + } + + const SLX_PATH: &'static str = "C:/Wolverine/SLX/se64.exe"; + const RANGE: u32 = 10; + const STEP: usize = 4; + + fn philosopher_bench(c: &mut Criterion) { + let mut group = c.benchmark_group("Philosophers"); + + // set-up the benchmark parameters + group.confidence_level(0.99); + group.plot_config( + PlotConfiguration::default() + .summary_scale(AxisScale::Logarithmic) + ); + group.sampling_mode(SamplingMode::Linear); + + // vary the number of performed reruns in each experiment + for experiment_count in (0..RANGE).map(|c| (1 << c)*STEP) { + let count = experiment_count.to_string(); + let args = [ + "/silent", + "/stdout", + "/noicon", + "/nowarn", + "/noxwarn", + "/#BENCH", + "slx\\philosophers.slx", + count.as_str() + ]; + + // benchmark the SLX implementation + group.bench_function( + BenchmarkId::new("SLX", experiment_count), + |b| b.iter_custom(|iters| + slx::slx_bench( + SLX_PATH, + &args, + iters as usize + ).expect("couldn't benchmark the SLX program") + ) + ); + + // benchmark the C++ implementation + group.bench_function( + BenchmarkId::new("ODEMx", experiment_count), + |b| b.iter(|| unsafe { + odemx::philosophers( + PHILOSOPHER_COUNT as _, + experiment_count as _ + ) + }) + ); + + // benchmark the Rust implementation + group.bench_function( + BenchmarkId::new("Rust", experiment_count), + |b| b.iter(|| + philosophers(PHILOSOPHER_COUNT, experiment_count) + ) + ); + } + + group.finish(); + } + + criterion_group!(benches, philosopher_bench); +}