mirror of
https://github.com/bevyengine/bevy
synced 2024-11-25 14:10:19 +00:00
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: <details> <summary></summary> ![image](https://user-images.githubusercontent.com/3957610/158069737-2ddf4e4a-7d5c-4ee5-8566-424a54a06723.png) </details> - `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).
This commit is contained in:
parent
fa791d6bb8
commit
54fbaf4b4c
3 changed files with 567 additions and 0 deletions
|
@ -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"
|
||||
|
||||
|
|
|
@ -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 name>
|
||||
```
|
||||
|
||||
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
|
||||
|
|
546
examples/stress_tests/transform_hierarchy.rs
Normal file
546
examples/stress_tests/transform_hierarchy.rs
Normal file
|
@ -0,0 +1,546 @@
|
|||
//! Hierarchy and transform propagation stress test.
|
||||
//!
|
||||
//! Running this example:
|
||||
//!
|
||||
//! ```
|
||||
//! cargo r --release --example transform_hierarchy -- <configuration name>
|
||||
//! ```
|
||||
//!
|
||||
//! | 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: <test configuration>\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<Time>, mut query: Query<(&mut Transform, &mut Update)>) {
|
||||
for (mut t, mut u) in query.iter_mut() {
|
||||
u.0 += time.delta_seconds() * 0.1;
|
||||
set_translation(&mut t.translation, u.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// set translation based on the angle `a`
|
||||
fn set_translation(translation: &mut Vec3, a: f32) {
|
||||
translation.x = a.cos() * 32.0;
|
||||
translation.y = a.sin() * 32.0;
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, cfg: Res<Cfg>) {
|
||||
let mut cam = OrthographicCameraBundle::new_2d();
|
||||
cam.transform.translation.z = 100.0;
|
||||
commands.spawn_bundle(cam);
|
||||
|
||||
let result = match cfg.test_case {
|
||||
TestCase::Tree {
|
||||
depth,
|
||||
branch_width,
|
||||
} => {
|
||||
let tree = gen_tree(depth, branch_width);
|
||||
spawn_tree(&tree, &mut commands, &cfg.update_filter, default())
|
||||
}
|
||||
TestCase::NonUniformTree {
|
||||
depth,
|
||||
branch_width,
|
||||
} => {
|
||||
let tree = gen_non_uniform_tree(depth, branch_width);
|
||||
spawn_tree(&tree, &mut commands, &cfg.update_filter, default())
|
||||
}
|
||||
TestCase::Humanoids { active, inactive } => {
|
||||
let mut result = InsertResult::default();
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for _ in 0..active {
|
||||
result.combine(spawn_tree(
|
||||
&HUMANOID_RIG,
|
||||
&mut commands,
|
||||
&cfg.update_filter,
|
||||
Transform::from_xyz(
|
||||
rng.gen::<f32>() * 500.0 - 250.0,
|
||||
rng.gen::<f32>() * 500.0 - 250.0,
|
||||
0.0,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
for _ in 0..inactive {
|
||||
result.combine(spawn_tree(
|
||||
&HUMANOID_RIG,
|
||||
&mut commands,
|
||||
&UpdateFilter {
|
||||
// force inactive by setting the probability < 0
|
||||
probability: -1.0,
|
||||
..cfg.update_filter
|
||||
},
|
||||
Transform::from_xyz(
|
||||
rng.gen::<f32>() * 500.0 - 250.0,
|
||||
rng.gen::<f32>() * 500.0 - 250.0,
|
||||
0.0,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
};
|
||||
|
||||
println!("\n{:#?}", result);
|
||||
}
|
||||
|
||||
/// overview of the inserted hierarchy
|
||||
#[derive(Default, Debug)]
|
||||
struct InsertResult {
|
||||
/// total number of nodes inserted
|
||||
inserted_nodes: usize,
|
||||
/// number of nodes that get updated each frame
|
||||
active_nodes: usize,
|
||||
/// maximum depth of the hierarchy tree
|
||||
maximum_depth: usize,
|
||||
}
|
||||
|
||||
impl InsertResult {
|
||||
fn combine(&mut self, rhs: Self) -> &mut Self {
|
||||
self.inserted_nodes += rhs.inserted_nodes;
|
||||
self.active_nodes += rhs.active_nodes;
|
||||
self.maximum_depth = self.maximum_depth.max(rhs.maximum_depth);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// spawns a tree defined by a parent map (excluding root)
|
||||
/// the parent map must be ordered (parent must exist before child)
|
||||
fn spawn_tree(
|
||||
parent_map: &[usize],
|
||||
commands: &mut Commands,
|
||||
update_filter: &UpdateFilter,
|
||||
root_transform: Transform,
|
||||
) -> InsertResult {
|
||||
// total count (# of nodes + root)
|
||||
let count = parent_map.len() + 1;
|
||||
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct NodeInfo {
|
||||
child_count: u32,
|
||||
depth: u32,
|
||||
}
|
||||
|
||||
// node index -> entity lookup list
|
||||
let mut ents: Vec<Entity> = Vec::with_capacity(count);
|
||||
let mut node_info: Vec<NodeInfo> = vec![default(); count];
|
||||
for (i, &parent_idx) in parent_map.iter().enumerate() {
|
||||
// assert spawn order (parent must be processed before child)
|
||||
assert!(parent_idx <= i, "invalid spawn order");
|
||||
node_info[parent_idx].child_count += 1;
|
||||
}
|
||||
|
||||
// insert root
|
||||
ents.push(
|
||||
commands
|
||||
.spawn()
|
||||
.insert(root_transform)
|
||||
.insert(GlobalTransform::default())
|
||||
.id(),
|
||||
);
|
||||
|
||||
let mut result = InsertResult::default();
|
||||
let mut rng = rand::thread_rng();
|
||||
// used to count through the number of children (used only for visual layout)
|
||||
let mut child_idx: Vec<u16> = vec![0; count];
|
||||
|
||||
// insert children
|
||||
for (current_idx, &parent_idx) in parent_map.iter().enumerate() {
|
||||
let current_idx = current_idx + 1;
|
||||
|
||||
// separation factor to visually separate children (0..1)
|
||||
let sep = child_idx[parent_idx] as f32 / node_info[parent_idx].child_count as f32;
|
||||
child_idx[parent_idx] += 1;
|
||||
|
||||
// calculate and set depth
|
||||
// this works because it's guaranteed that we have already iterated over the parent
|
||||
let depth = node_info[parent_idx].depth + 1;
|
||||
let info = &mut node_info[current_idx];
|
||||
info.depth = depth;
|
||||
|
||||
// update max depth of tree
|
||||
result.maximum_depth = result.maximum_depth.max(depth.try_into().unwrap());
|
||||
|
||||
// insert child
|
||||
let child_entity = {
|
||||
let mut cmd = commands.spawn();
|
||||
|
||||
// check whether or not to update this node
|
||||
let update = (rng.gen::<f32>() <= update_filter.probability)
|
||||
&& (depth >= update_filter.min_depth && depth <= update_filter.max_depth);
|
||||
|
||||
if update {
|
||||
cmd.insert(Update(sep));
|
||||
result.active_nodes += 1;
|
||||
}
|
||||
|
||||
let transform = {
|
||||
let mut translation = Vec3::ZERO;
|
||||
// use the same placement fn as the `update` system
|
||||
// this way the entities won't be all at (0, 0, 0) when they don't have an `Update` component
|
||||
set_translation(&mut translation, sep);
|
||||
Transform::from_translation(translation)
|
||||
};
|
||||
|
||||
// only insert the components necessary for the transform propagation
|
||||
cmd.insert(transform).insert(GlobalTransform::default());
|
||||
|
||||
cmd.id()
|
||||
};
|
||||
|
||||
commands
|
||||
.get_or_spawn(ents[parent_idx])
|
||||
.add_child(child_entity);
|
||||
|
||||
ents.push(child_entity);
|
||||
}
|
||||
|
||||
result.inserted_nodes = ents.len();
|
||||
result
|
||||
}
|
||||
|
||||
/// generate a tree `depth` levels deep, where each node has `branch_width` children
|
||||
fn gen_tree(depth: u32, branch_width: u32) -> Vec<usize> {
|
||||
// calculate the total count of branches
|
||||
let mut count: usize = 0;
|
||||
for i in 0..(depth - 1) {
|
||||
count += TryInto::<usize>::try_into(branch_width.pow(i)).unwrap();
|
||||
}
|
||||
|
||||
// the tree is built using this pattern:
|
||||
// 0, 0, 0, ... 1, 1, 1, ... 2, 2, 2, ... (count - 1)
|
||||
(0..count)
|
||||
.flat_map(|i| std::iter::repeat(i).take(branch_width.try_into().unwrap()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// recursive part of [`gen_non_uniform_tree`]
|
||||
fn add_children_non_uniform(
|
||||
tree: &mut Vec<usize>,
|
||||
parent: usize,
|
||||
mut curr_depth: u32,
|
||||
max_branch_width: u32,
|
||||
) {
|
||||
for _ in 0..max_branch_width {
|
||||
tree.push(parent);
|
||||
|
||||
curr_depth = curr_depth.checked_sub(1).unwrap();
|
||||
if curr_depth == 0 {
|
||||
return;
|
||||
}
|
||||
add_children_non_uniform(tree, tree.len(), curr_depth, max_branch_width);
|
||||
}
|
||||
}
|
||||
|
||||
/// generate a tree that has more nodes on one side that the other
|
||||
/// the deepest hierarchy path is `max_depth` and the widest branches have `max_branch_width` children
|
||||
fn gen_non_uniform_tree(max_depth: u32, max_branch_width: u32) -> Vec<usize> {
|
||||
let mut tree = Vec::new();
|
||||
add_children_non_uniform(&mut tree, 0, max_depth, max_branch_width);
|
||||
tree
|
||||
}
|
||||
|
||||
/// parent map for a decently complex humanoid rig (based on mixamo rig)
|
||||
const HUMANOID_RIG: [usize; 67] = [
|
||||
// (0: root)
|
||||
0, // 1: hips
|
||||
1, // 2: spine
|
||||
2, // 3: spine 1
|
||||
3, // 4: spine 2
|
||||
4, // 5: neck
|
||||
5, // 6: head
|
||||
6, // 7: head top
|
||||
6, // 8: left eye
|
||||
6, // 9: right eye
|
||||
4, // 10: left shoulder
|
||||
10, // 11: left arm
|
||||
11, // 12: left forearm
|
||||
12, // 13: left hand
|
||||
13, // 14: left hand thumb 1
|
||||
14, // 15: left hand thumb 2
|
||||
15, // 16: left hand thumb 3
|
||||
16, // 17: left hand thumb 4
|
||||
13, // 18: left hand index 1
|
||||
18, // 19: left hand index 2
|
||||
19, // 20: left hand index 3
|
||||
20, // 21: left hand index 4
|
||||
13, // 22: left hand middle 1
|
||||
22, // 23: left hand middle 2
|
||||
23, // 24: left hand middle 3
|
||||
24, // 25: left hand middle 4
|
||||
13, // 26: left hand ring 1
|
||||
26, // 27: left hand ring 2
|
||||
27, // 28: left hand ring 3
|
||||
28, // 29: left hand ring 4
|
||||
13, // 30: left hand pinky 1
|
||||
30, // 31: left hand pinky 2
|
||||
31, // 32: left hand pinky 3
|
||||
32, // 33: left hand pinky 4
|
||||
4, // 34: right shoulder
|
||||
34, // 35: right arm
|
||||
35, // 36: right forearm
|
||||
36, // 37: right hand
|
||||
37, // 38: right hand thumb 1
|
||||
38, // 39: right hand thumb 2
|
||||
39, // 40: right hand thumb 3
|
||||
40, // 41: right hand thumb 4
|
||||
37, // 42: right hand index 1
|
||||
42, // 43: right hand index 2
|
||||
43, // 44: right hand index 3
|
||||
44, // 45: right hand index 4
|
||||
37, // 46: right hand middle 1
|
||||
46, // 47: right hand middle 2
|
||||
47, // 48: right hand middle 3
|
||||
48, // 49: right hand middle 4
|
||||
37, // 50: right hand ring 1
|
||||
50, // 51: right hand ring 2
|
||||
51, // 52: right hand ring 3
|
||||
52, // 53: right hand ring 4
|
||||
37, // 54: right hand pinky 1
|
||||
54, // 55: right hand pinky 2
|
||||
55, // 56: right hand pinky 3
|
||||
56, // 57: right hand pinky 4
|
||||
1, // 58: left upper leg
|
||||
58, // 59: left leg
|
||||
59, // 60: left foot
|
||||
60, // 61: left toe base
|
||||
61, // 62: left toe end
|
||||
1, // 63: right upper leg
|
||||
63, // 64: right leg
|
||||
64, // 65: right foot
|
||||
65, // 66: right toe base
|
||||
66, // 67: right toe end
|
||||
];
|
Loading…
Reference in a new issue