From 378290adb9a1d5defc343d78a18a04945782dddd Mon Sep 17 00:00:00 2001 From: Alex <3957610+CptPotato@users.noreply.github.com> Date: Mon, 21 Mar 2022 20:36:46 +0000 Subject: [PATCH] Add transform hierarchy stress test (#4170) ## Objective There recently was a discussion on Discord about a possible test case for stress-testing transform hierarchies. ## Solution Create a test case for stress testing transform propagation. *Edit:* I have scrapped my previous example and built something more functional and less focused on visuals. There are three test setups: - `TestCase::Tree` recursively creates a tree with a specified depth and branch width - `TestCase::NonUniformTree` is the same as `Tree` but omits nodes in a way that makes the tree "lean" towards one side, like this:
![image](https://user-images.githubusercontent.com/3957610/158069737-2ddf4e4a-7d5c-4ee5-8566-424a54a06723.png)
- `TestCase::Humanoids` creates one or more separate hierarchies based on the structure of common humanoid rigs - this can both insert `active` and `inactive` instances of the human rig It's possible to parameterize which parts of the hierarchy get updated (transform change) and which remain unchanged. This is based on @james7132 suggestion: There's a probability to decide which entities should remain static. On top of that these changes can be limited to a certain range in the hierarchy (min_depth..max_depth). --- Cargo.toml | 6 + examples/README.md | 15 + examples/stress_tests/transform_hierarchy.rs | 546 +++++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 examples/stress_tests/transform_hierarchy.rs diff --git a/Cargo.toml b/Cargo.toml index cc7d80db0629d8..c082d3b985a043 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -592,3 +592,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 805f058021b53d..e3cdc93a7dc2c6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -66,6 +66,7 @@ git checkout v0.4.0 - [WASM](#wasm) - [Setup](#setup-2) - [Build & Run](#build--run-2) +- [Stress Tests](#stress-tests) # The Bare Minimum @@ -420,3 +421,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 00000000000000..360f992f129f63 --- /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