diff --git a/Cargo.toml b/Cargo.toml index 0fafd91be7970..0b0a5855ecae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,9 @@ ron = "0.6.2" serde = { version = "1", features = ["derive"] } # Needed to poll Task examples futures-lite = "1.11.3" +futures-timer = "3.0.2" +lazy_static = "1" +noop-waker = "0.1" [[example]] name = "hello_world" @@ -243,6 +246,10 @@ path = "examples/asset/hot_asset_reloading.rs" name = "async_compute" path = "examples/async_tasks/async_compute.rs" +[[example]] +name = "async_bench" +path = "examples/async_tasks/async_bench.rs" + # Audio [[example]] name = "audio" diff --git a/examples/README.md b/examples/README.md index 7796afdb16223..601109566b68a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -42,6 +42,7 @@ git checkout v0.4.0 - [Application](#application) - [Assets](#assets) - [Async Tasks](#async-tasks) + - [Async Task Benchmark](#async-task-benchmark) - [Audio](#audio) - [Diagnostics](#diagnostics) - [ECS (Entity Component System)](#ecs-entity-component-system) @@ -138,6 +139,12 @@ Example | File | Description --- | --- | --- `async_compute` | [`async_tasks/async_compute.rs`](async_tasks/async_compute.rs) | How to use `AsyncComputeTaskPool` to complete longer running tasks +## Async Task Benchmark + +Example | File | Description +--- | --- | --- +`async_bench` | [`async_tasks/async_bench.rs`](async_tasks/async_bench.rs) | Compare perforance between using single block_on and multiple block_on(s) when dealing with massive custom async tasks + ## Audio Example | File | Description diff --git a/examples/async_tasks/async_bench.rs b/examples/async_tasks/async_bench.rs new file mode 100644 index 0000000000000..b0316deebaea9 --- /dev/null +++ b/examples/async_tasks/async_bench.rs @@ -0,0 +1,308 @@ +use bevy::{ + app::{AppExit, ScheduleRunnerSettings}, + prelude::*, + tasks::{AsyncComputeTaskPool, Task}, +}; +use core::task::Context; +use futures_lite::{ + future::{block_on, poll_once}, + Future, +}; +use std::{ + sync::{Arc, LockResult, RwLock, RwLockReadGuard}, + task::{Poll, Waker}, + time::{Duration, Instant}, +}; + +const TASK_DURATION_SEC: f32 = 0.5; +const FPS: f64 = 120.0; +const FRAME_STEP: f64 = 1.0 / FPS; +const N_STEPS: usize = 10; +const N_TASKS: usize = 100000; + +struct FrameCounter { + pub n_frames: usize, +} + +// This example benchmarks performance of concurrent custom task handling +// Run with release build: cargo run --release --example async_bench +// Example output: +// windows: +// [no_poll_once] n_frames executed: 238, avg fps: 39.3(target:120), duration: 6.048s +// [noop_waker] n_frames executed: 161, avg fps: 25.7(target:120), duration: 6.253s +// [handle_tasks] n_frames executed: 54, avg fps: 9.4(target:120), duration: 5.743s +// [handle_tasks_par] n_frames executed: 124, avg fps: 21.5(target:120), duration: 5.767s +// [handle_tasks_par_2] n_frames executed: 90, avg fps: 15.4(target:120), duration: 5.835s +fn main() { + App::new() + .insert_resource(ScheduleRunnerSettings::run_loop(Duration::from_secs_f64( + FRAME_STEP, + ))) + .insert_resource(FrameCounter { n_frames: 0 }) + .add_plugins(MinimalPlugins) + .add_startup_system(spawn_tasks_no_poll_once) + .add_system_to_stage(CoreStage::First, count_frame) + .add_system(handle_tasks_no_poll_once) + .run(); + App::new() + .insert_resource(ScheduleRunnerSettings::run_loop(Duration::from_secs_f64( + FRAME_STEP, + ))) + .insert_resource(FrameCounter { n_frames: 0 }) + .add_plugins(MinimalPlugins) + .add_startup_system(spawn_tasks_noop_waker) + .add_system_to_stage(CoreStage::First, count_frame) + .add_system(handle_tasks_noop_waker) + .run(); + for handle_tasks_system in [handle_tasks, handle_tasks_par, handle_tasks_par_2] { + App::new() + .insert_resource(ScheduleRunnerSettings::run_loop(Duration::from_secs_f64( + FRAME_STEP, + ))) + .insert_resource(FrameCounter { n_frames: 0 }) + .add_plugins(MinimalPlugins) + .add_startup_system(spawn_tasks) + .add_system_to_stage(CoreStage::First, count_frame) + .add_system(handle_tasks_system) + .run(); + } +} + +fn spawn_tasks(mut commands: Commands, thread_pool: Res) { + for step in 0..N_STEPS { + for _i in 0..N_TASKS { + let task = thread_pool.spawn(async move { + let start_time = Instant::now(); + let duration = Duration::from_secs_f32(TASK_DURATION_SEC * (step as f32)); + while Instant::now() - start_time < duration { + futures_timer::Delay::new(Duration::from_secs_f32(0.1)).await + } + true + }); + commands.spawn().insert(task); + } + } +} + +fn count_frame(mut frame_counter: ResMut) { + frame_counter.n_frames += 1; +} + +fn handle_tasks( + mut commands: Commands, + mut transform_tasks: Query<(Entity, &mut Task)>, + mut app_exit_events: EventWriter, + time: Res