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