Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/stress_tests/many_components.rs
9353 views
1
//! Stress test for large ECS worlds.
2
//!
3
//! Running this example:
4
//!
5
//! ```
6
//! cargo run --profile stress-test --example many_components [<num_entities>] [<num_components>] [<num_systems>]
7
//! ```
8
//!
9
//! `num_entities`: The number of entities in the world (must be nonnegative)
10
//! `num_components`: the number of components in the world (must be at least 10)
11
//! `num_systems`: the number of systems in the world (must be nonnegative)
12
//!
13
//! If no valid number is provided, for each argument there's a reasonable default.
14
15
use bevy::{
16
diagnostic::{
17
DiagnosticPath, DiagnosticsPlugin, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin,
18
},
19
ecs::{
20
component::{ComponentCloneBehavior, ComponentDescriptor, ComponentId, StorageType},
21
system::QueryParamBuilder,
22
world::FilteredEntityMut,
23
},
24
log::LogPlugin,
25
platform::collections::HashSet,
26
prelude::{App, In, IntoSystem, Query, Schedule, SystemParamBuilder, Update},
27
ptr::{OwningPtr, PtrMut},
28
MinimalPlugins,
29
};
30
31
use rand::prelude::{IndexedRandom, Rng, SeedableRng};
32
use rand_chacha::ChaCha8Rng;
33
use std::{alloc::Layout, mem::ManuallyDrop, num::Wrapping};
34
35
#[expect(unsafe_code, reason = "Reading dynamic components requires unsafe")]
36
// A simple system that matches against several components and does some menial calculation to create
37
// some non-trivial load.
38
fn base_system(access_components: In<Vec<ComponentId>>, mut query: Query<FilteredEntityMut>) {
39
#[cfg(feature = "trace")]
40
let _span = tracing::info_span!("base_system", components = ?access_components.0, count = query.iter().len()).entered();
41
42
for mut filtered_entity in &mut query {
43
// We calculate Faulhaber's formula mod 256 with n = value and p = exponent.
44
// See https://en.wikipedia.org/wiki/Faulhaber%27s_formula
45
// The time is takes to compute this depends on the number of entities and the values in
46
// each entity. This is to ensure that each system takes a different amount of time.
47
let mut total: Wrapping<u8> = Wrapping(0);
48
let mut exponent: u32 = 1;
49
for component_id in &access_components.0 {
50
// find the value of the component
51
let ptr = filtered_entity.get_by_id(*component_id).unwrap();
52
53
// SAFETY: All components have a u8 layout
54
let value: u8 = unsafe { *ptr.deref::<u8>() };
55
56
for i in 0..=value {
57
let mut product = Wrapping(1);
58
for _ in 1..=exponent {
59
product *= Wrapping(i);
60
}
61
total += product;
62
}
63
exponent += 1;
64
}
65
66
// we assign this value to all the components we can write to
67
for component_id in &access_components.0 {
68
if let Some(ptr) = filtered_entity.get_mut_by_id(*component_id) {
69
// SAFETY: All components have a u8 layout
70
unsafe {
71
let mut value = ptr.with_type::<u8>();
72
*value = total.0;
73
}
74
}
75
}
76
}
77
}
78
79
#[expect(unsafe_code, reason = "Using dynamic components requires unsafe")]
80
fn stress_test(num_entities: u32, num_components: u32, num_systems: u32) {
81
let mut rng = ChaCha8Rng::seed_from_u64(42);
82
let mut app = App::default();
83
let world = app.world_mut();
84
85
// register a bunch of components
86
let component_ids: Vec<ComponentId> = (1..=num_components)
87
.map(|i| {
88
world.register_component_with_descriptor(
89
// SAFETY:
90
// * We don't implement a drop function
91
// * u8 is Sync and Send
92
unsafe {
93
ComponentDescriptor::new_with_layout(
94
format!("Component{i}").to_string(),
95
StorageType::Table,
96
Layout::new::<u8>(),
97
None,
98
true, // is mutable
99
ComponentCloneBehavior::Default,
100
None,
101
)
102
},
103
)
104
})
105
.collect();
106
107
// fill the schedule with systems
108
let mut schedule = Schedule::new(Update);
109
for _ in 1..=num_systems {
110
let num_access_components = rng.random_range(1..10);
111
let access_components: Vec<ComponentId> = component_ids
112
.choose_multiple(&mut rng, num_access_components)
113
.copied()
114
.collect();
115
let system = (QueryParamBuilder::new(|builder| {
116
for &access_component in &access_components {
117
if rand::random::<bool>() {
118
builder.mut_id(access_component);
119
} else {
120
builder.ref_id(access_component);
121
}
122
}
123
}),)
124
.build_state(world)
125
.build_any_system(base_system);
126
schedule.add_systems((move || access_components.clone()).pipe(system));
127
}
128
129
// spawn a bunch of entities
130
for _ in 1..=num_entities {
131
let num_components = rng.random_range(1..10);
132
let components: Vec<ComponentId> = component_ids
133
.choose_multiple(&mut rng, num_components)
134
.copied()
135
.collect();
136
137
let mut entity = world.spawn_empty();
138
// We use `ManuallyDrop` here as we need to avoid dropping the u8's when `values` is dropped
139
// since ownership of the values is passed to the world in `insert_by_ids`.
140
// But we do want to deallocate the memory when values is dropped.
141
let mut values: Vec<ManuallyDrop<u8>> = components
142
.iter()
143
.map(|_id| ManuallyDrop::new(rng.random_range(0..255)))
144
.collect();
145
let ptrs: Vec<OwningPtr> = values
146
.iter_mut()
147
.map(|value| {
148
// SAFETY:
149
// * We don't read/write `values` binding after this and values are `ManuallyDrop`,
150
// so we have the right to drop/move the values
151
unsafe { PtrMut::from(value).promote() }
152
})
153
.collect();
154
// SAFETY:
155
// * component_id's are from the same world
156
// * `values` was initialized above, so references are valid
157
unsafe {
158
entity.insert_by_ids(&components, ptrs.into_iter());
159
}
160
}
161
162
// overwrite Update schedule in the app
163
app.add_schedule(schedule);
164
app.add_plugins(MinimalPlugins)
165
.add_plugins(DiagnosticsPlugin)
166
.add_plugins(LogPlugin::default())
167
.add_plugins(FrameTimeDiagnosticsPlugin::default())
168
.add_plugins(LogDiagnosticsPlugin::filtered(HashSet::from_iter([
169
DiagnosticPath::new("fps"),
170
])));
171
app.run();
172
}
173
174
fn main() {
175
const DEFAULT_NUM_ENTITIES: u32 = 50000;
176
const DEFAULT_NUM_COMPONENTS: u32 = 1000;
177
const DEFAULT_NUM_SYSTEMS: u32 = 800;
178
179
// take input
180
let num_entities = std::env::args()
181
.nth(1)
182
.and_then(|string| string.parse::<u32>().ok())
183
.unwrap_or_else(|| {
184
println!("No valid number of entities provided, using default {DEFAULT_NUM_ENTITIES}");
185
DEFAULT_NUM_ENTITIES
186
});
187
let num_components = std::env::args()
188
.nth(2)
189
.and_then(|string| string.parse::<u32>().ok())
190
.and_then(|n| if n >= 10 { Some(n) } else { None })
191
.unwrap_or_else(|| {
192
println!(
193
"No valid number of components provided (>= 10), using default {DEFAULT_NUM_COMPONENTS}"
194
);
195
DEFAULT_NUM_COMPONENTS
196
});
197
let num_systems = std::env::args()
198
.nth(3)
199
.and_then(|string| string.parse::<u32>().ok())
200
.unwrap_or_else(|| {
201
println!("No valid number of systems provided, using default {DEFAULT_NUM_SYSTEMS}");
202
DEFAULT_NUM_SYSTEMS
203
});
204
205
stress_test(num_entities, num_components, num_systems);
206
}
207
208