Skip to main content

many_components/
many_components.rs

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
15use 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
31use chacha20::ChaCha8Rng;
32use rand::prelude::{IndexedRandom, RngExt, SeedableRng};
33use 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.
38fn 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")]
78fn 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
173fn 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}