diff --git a/Cargo.toml b/Cargo.toml index 5efa16115826e..32841e9bbf113 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -562,3 +562,9 @@ icon = "@mipmap/ic_launcher" build_targets = ["aarch64-linux-android", "armv7-linux-androideabi"] min_sdk_version = 16 target_sdk_version = 29 + +# Stress Tests +[[example]] +name = "transform_hierarchy" +path = "examples/stress_tests/transform_hierarchy.rs" + diff --git a/examples/README.md b/examples/README.md index 53d276d646d81..21b9c50c6c8f9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -65,6 +65,7 @@ git checkout v0.4.0 - [WASM](#wasm) - [Setup](#setup-2) - [Build & Run](#build--run-2) +- [Stress Tests](#stress-tests) # The Bare Minimum @@ -408,3 +409,17 @@ ruby -run -ehttpd examples/wasm To load assets, they need to be available in the folder examples/wasm/assets. Cloning this repository will set it up as a symlink on Linux and macOS, but you will need to manually move the assets on Windows. + +# Stress Tests + +These examples are used to test the performance and stability of various parts of the engine in an isolated way. + +Due to the focus on performance it's recommended to run the stress tests in release mode: + +```sh +cargo run --release --example +``` + +Example | File | Description +--- | --- | --- +`transform_hierarchy.rs` | [`stress_tests/transform_hierarchy.rs`](./stress_tests/transform_hierarchy.rs) | Various test cases for hierarchy and transform propagation performance diff --git a/examples/stress_tests/transform_hierarchy.rs b/examples/stress_tests/transform_hierarchy.rs new file mode 100644 index 0000000000000..360f992f129f6 --- /dev/null +++ b/examples/stress_tests/transform_hierarchy.rs @@ -0,0 +1,546 @@ +//! Hierarchy and transform propagation stress test. +//! +//! Running this example: +//! +//! ``` +//! cargo r --release --example transform_hierarchy -- +//! ``` +//! +//! | Configuration | Description | +//! | -------------------- | ----------------------------------------------------------------- | +//! | `large_tree` | A fairly wide and deep tree. | +//! | `wide_tree` | A shallow but very wide tree. | +//! | `deep_tree` | A deep but not very wide tree. | +//! | `chain` | A chain. 2500 levels deep. | +//! | `update_leaves` | Same as `large_tree`, but only leaves are updated. | +//! | `update_shallow` | Same as `large_tree`, but only the first few levels are updated. | +//! | `humanoids_active` | 4000 active humanoid rigs. | +//! | `humanoids_inactive` | 4000 humanoid rigs. Only 10 are active. | +//! | `humanoids_mixed` | 2000 active and 2000 inactive humanoid rigs. | + +use bevy::prelude::*; +use rand::Rng; + +/// pre-defined test configurations with name +const CONFIGS: [(&str, Cfg); 9] = [ + ( + "large_tree", + Cfg { + test_case: TestCase::NonUniformTree { + depth: 18, + branch_width: 8, + }, + update_filter: UpdateFilter { + probability: 0.5, + min_depth: 0, + max_depth: u32::MAX, + }, + }, + ), + ( + "wide_tree", + Cfg { + test_case: TestCase::Tree { + depth: 3, + branch_width: 500, + }, + update_filter: UpdateFilter { + probability: 0.5, + min_depth: 0, + max_depth: u32::MAX, + }, + }, + ), + ( + "deep_tree", + Cfg { + test_case: TestCase::NonUniformTree { + depth: 25, + branch_width: 2, + }, + update_filter: UpdateFilter { + probability: 0.5, + min_depth: 0, + max_depth: u32::MAX, + }, + }, + ), + ( + "chain", + Cfg { + test_case: TestCase::Tree { + depth: 2500, + branch_width: 1, + }, + update_filter: UpdateFilter { + probability: 0.5, + min_depth: 0, + max_depth: u32::MAX, + }, + }, + ), + ( + "update_leaves", + Cfg { + test_case: TestCase::Tree { + depth: 18, + branch_width: 2, + }, + update_filter: UpdateFilter { + probability: 0.5, + min_depth: 17, + max_depth: u32::MAX, + }, + }, + ), + ( + "update_shallow", + Cfg { + test_case: TestCase::Tree { + depth: 18, + branch_width: 2, + }, + update_filter: UpdateFilter { + probability: 0.5, + min_depth: 0, + max_depth: 8, + }, + }, + ), + ( + "humanoids_active", + Cfg { + test_case: TestCase::Humanoids { + active: 4000, + inactive: 0, + }, + update_filter: UpdateFilter { + probability: 1.0, + min_depth: 0, + max_depth: u32::MAX, + }, + }, + ), + ( + "humanoids_inactive", + Cfg { + test_case: TestCase::Humanoids { + active: 10, + inactive: 3990, + }, + update_filter: UpdateFilter { + probability: 1.0, + min_depth: 0, + max_depth: u32::MAX, + }, + }, + ), + ( + "humanoids_mixed", + Cfg { + test_case: TestCase::Humanoids { + active: 2000, + inactive: 2000, + }, + update_filter: UpdateFilter { + probability: 1.0, + min_depth: 0, + max_depth: u32::MAX, + }, + }, + ), +]; + +fn print_available_configs() { + println!("available configurations:"); + for (name, _) in CONFIGS { + println!(" {name}"); + } +} + +fn main() { + // parse cli argument and find the selected test configuration + let cfg: Cfg = match std::env::args().nth(1) { + Some(arg) => match CONFIGS.iter().find(|(name, _)| *name == arg) { + Some((name, cfg)) => { + println!("test configuration: {name}"); + cfg.clone() + } + None => { + println!("test configuration \"{arg}\" not found.\n"); + print_available_configs(); + return; + } + }, + None => { + println!("missing argument: \n"); + print_available_configs(); + return; + } + }; + + println!("\n{:#?}", cfg); + + App::new() + .insert_resource(cfg) + .add_plugins(MinimalPlugins) + .add_plugin(TransformPlugin::default()) + .add_startup_system(setup) + .add_system(update) + .run() +} + +/// test configuration +#[derive(Debug, Clone)] +struct Cfg { + /// which test case should be inserted + test_case: TestCase, + /// which entities should be updated + update_filter: UpdateFilter, +} + +#[allow(unused)] +#[derive(Debug, Clone)] +enum TestCase { + /// a uniform tree, exponentially growing with depth + Tree { + /// total depth + depth: u32, + /// number of children per node + branch_width: u32, + }, + /// a non uniform tree (one side is deeper than the other) + /// creates significantly less nodes than `TestCase::Tree` with the same parameters + NonUniformTree { + /// the maximum depth + depth: u32, + /// max number of children per node + branch_width: u32, + }, + /// one or multiple humanoid rigs + Humanoids { + /// number of active instances (uses the specified [`UpdateFilter`]) + active: u32, + /// number of inactive instances (always inactive) + inactive: u32, + }, +} + +/// a filter to restrict which nodes are updated +#[derive(Debug, Clone)] +struct UpdateFilter { + /// starting depth (inclusive) + min_depth: u32, + /// end depth (inclusive) + max_depth: u32, + /// probability of a node to get updated (evaluated at insertion time, not during update) + /// 0 (never) .. 1 (always) + probability: f32, +} + +/// update component with some per-component value +#[derive(Component)] +struct Update(f32); + +/// update positions system +fn update(time: Res