docs/bevy/examples/stress_tests/many_components.rs 7.7 K · 205 lines · raw · history

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 chacha20::ChaCha8Rng;
32 use rand::prelude::{IndexedRandom, RngExt, SeedableRng};
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 for (exponent, component_id) in (1_u32..).zip(access_components.0.iter()) {
49 // find the value of the component
50 let ptr = filtered_entity.get_by_id(*component_id).unwrap();
51
52 // SAFETY: All components have a u8 layout
53 let value: u8 = unsafe { *ptr.deref::<u8>() };
54
55 for i in 0..=value {
56 let mut product = Wrapping(1);
57 for _ in 1..=exponent {
58 product *= Wrapping(i);
59 }
60 total += product;
61 }
62 }
63
64 // we assign this value to all the components we can write to
65 for component_id in &access_components.0 {
66 if let Some(ptr) = filtered_entity.get_mut_by_id(*component_id) {
67 // SAFETY: All components have a u8 layout
68 unsafe {
69 let mut value = ptr.with_type::<u8>();
70 *value = total.0;
71 }
72 }
73 }
74 }
75 }
76
77 #[expect(unsafe_code, reason = "Using dynamic components requires unsafe")]
78 fn stress_test(num_entities: u32, num_components: u32, num_systems: u32) {
79 let mut rng = ChaCha8Rng::seed_from_u64(42);
80 let mut app = App::default();
81 let world = app.world_mut();
82
83 // register a bunch of components
84 let component_ids: Vec<ComponentId> = (1..=num_components)
85 .map(|i| {
86 world.register_component_with_descriptor(
87 // SAFETY:
88 // * We don't implement a drop function
89 // * u8 is Sync and Send
90 unsafe {
91 ComponentDescriptor::new_with_layout(
92 format!("Component{i}").to_string(),
93 StorageType::Table,
94 Layout::new::<u8>(),
95 None,
96 true, // is mutable
97 false, // has no summary tick
98 ComponentCloneBehavior::Default,
99 None,
100 )
101 },
102 )
103 })
104 .collect();
105
106 // fill the schedule with systems
107 let mut schedule = Schedule::new(Update);
108 for _ in 1..=num_systems {
109 let num_access_components = rng.random_range(1..10);
110 let access_components: Vec<ComponentId> = component_ids
111 .sample(&mut rng, num_access_components)
112 .copied()
113 .collect();
114 let system = (QueryParamBuilder::new(|builder| {
115 for &access_component in &access_components {
116 if rand::random::<bool>() {
117 builder.mut_id(access_component);
118 } else {
119 builder.ref_id(access_component);
120 }
121 }
122 }),)
123 .build_state(world)
124 .build_any_system(base_system);
125 schedule.add_systems((move || access_components.clone()).pipe(system));
126 }
127
128 // spawn a bunch of entities
129 for _ in 1..=num_entities {
130 let num_components = rng.random_range(1..10);
131 let components: Vec<ComponentId> = component_ids
132 .sample(&mut rng, num_components)
133 .copied()
134 .collect();
135
136 let mut entity = world.spawn_empty();
137 // We use `ManuallyDrop` here as we need to avoid dropping the u8's when `values` is dropped
138 // since ownership of the values is passed to the world in `insert_by_ids`.
139 // But we do want to deallocate the memory when values is dropped.
140 let mut values: Vec<ManuallyDrop<u8>> = components
141 .iter()
142 .map(|_id| ManuallyDrop::new(rng.random_range(0..255)))
143 .collect();
144 let ptrs: Vec<OwningPtr> = values
145 .iter_mut()
146 .map(|value| {
147 // SAFETY:
148 // * We don't read/write `values` binding after this and values are `ManuallyDrop`,
149 // so we have the right to drop/move the values
150 unsafe { PtrMut::from(value).promote() }
151 })
152 .collect();
153 // SAFETY:
154 // * component_id's are from the same world
155 // * `values` was initialized above, so references are valid
156 unsafe {
157 entity.insert_by_ids(&components, ptrs.into_iter());
158 }
159 }
160
161 // overwrite Update schedule in the app
162 app.add_schedule(schedule);
163 app.add_plugins(MinimalPlugins)
164 .add_plugins(DiagnosticsPlugin)
165 .add_plugins(LogPlugin::default())
166 .add_plugins(FrameTimeDiagnosticsPlugin::default())
167 .add_plugins(LogDiagnosticsPlugin::filtered(HashSet::from_iter([
168 DiagnosticPath::new("fps"),
169 ])));
170 app.run();
171 }
172
173 fn main() {
174 const DEFAULT_NUM_ENTITIES: u32 = 50000;
175 const DEFAULT_NUM_COMPONENTS: u32 = 1000;
176 const DEFAULT_NUM_SYSTEMS: u32 = 800;
177
178 // take input
179 let num_entities = std::env::args()
180 .nth(1)
181 .and_then(|string| string.parse::<u32>().ok())
182 .unwrap_or_else(|| {
183 println!("No valid number of entities provided, using default {DEFAULT_NUM_ENTITIES}");
184 DEFAULT_NUM_ENTITIES
185 });
186 let num_components = std::env::args()
187 .nth(2)
188 .and_then(|string| string.parse::<u32>().ok())
189 .filter(|&n| n >= 10)
190 .unwrap_or_else(|| {
191 println!(
192 "No valid number of components provided (>= 10), using default {DEFAULT_NUM_COMPONENTS}"
193 );
194 DEFAULT_NUM_COMPONENTS
195 });
196 let num_systems = std::env::args()
197 .nth(3)
198 .and_then(|string| string.parse::<u32>().ok())
199 .unwrap_or_else(|| {
200 println!("No valid number of systems provided, using default {DEFAULT_NUM_SYSTEMS}");
201 DEFAULT_NUM_SYSTEMS
202 });
203
204 stress_test(num_entities, num_components, num_systems);
205 }