 1f884de53c
			
		
	
	
		1f884de53c
		
			
		
	
	
	
	
		
			
			# Objective We currently have no benchmarks for large worlds with many entities, components and systems. Having a benchmark for a world with many components is especially useful for the performance improvements needed for relations. This is also a response to this [comment from cart](https://github.com/bevyengine/bevy/pull/14385#issuecomment-2311292546). > I'd like both a small bevy_ecs-scoped executor benchmark that generates thousands of components used by hundreds of systems. ## Solution I use dynamic components and components to construct a benchmark with 2000 components, 4000 systems, and 10000 entities. ## Some notes - ~I use a lot of random entities, which creates unpredictable performance, I should use a seeded PRNG.~ - Not entirely sure if everything is ran concurrently currently. And there are many conflicts, meaning there's probably a lot of first-come-first-serve going on. Not entirely sure if these benchmarks are very reproducible. - Maybe add some more safety comments - Also component_reads_and_writes() is about to be deprecated #16339, but there's no other way to currently do what I'm trying to do. --------- Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com> Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com>
		
			
				
	
	
		
			197 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			197 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| //! Stress test for large ECS worlds.
 | |
| //!
 | |
| //! Running this example:
 | |
| //!
 | |
| //! ```
 | |
| //! cargo run --profile stress-test --example many_components [<num_entities>] [<num_components>] [<num_systems>]
 | |
| //! ```
 | |
| //!
 | |
| //! `num_entities`: The number of entities in the world (must be nonnegative)
 | |
| //! `num_components`: the number of components in the world (must be at least 10)
 | |
| //! `num_systems`: the number of systems in the world (must be nonnegative)
 | |
| //!
 | |
| //! If no valid number is provided, for each argument there's a reasonable default.
 | |
| 
 | |
| use bevy::{
 | |
|     diagnostic::{
 | |
|         DiagnosticPath, DiagnosticsPlugin, FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin,
 | |
|     },
 | |
|     ecs::{
 | |
|         component::{ComponentDescriptor, ComponentId, StorageType},
 | |
|         system::QueryParamBuilder,
 | |
|         world::FilteredEntityMut,
 | |
|     },
 | |
|     log::LogPlugin,
 | |
|     prelude::{App, In, IntoSystem, Query, Schedule, SystemParamBuilder, Update},
 | |
|     ptr::OwningPtr,
 | |
|     MinimalPlugins,
 | |
| };
 | |
| 
 | |
| use rand::prelude::{Rng, SeedableRng, SliceRandom};
 | |
| use rand_chacha::ChaCha8Rng;
 | |
| use std::{alloc::Layout, num::Wrapping};
 | |
| 
 | |
| // A simple system that matches against several components and does some menial calculation to create
 | |
| // some non-trivial load.
 | |
| fn base_system(access_components: In<Vec<ComponentId>>, mut query: Query<FilteredEntityMut>) {
 | |
|     for mut filtered_entity in &mut query {
 | |
|         // We calculate Faulhaber's formula mod 256 with n = value and p = exponent.
 | |
|         // See https://en.wikipedia.org/wiki/Faulhaber%27s_formula
 | |
|         // The time is takes to compute this depends on the number of entities and the values in
 | |
|         // each entity. This is to ensure that each system takes a different amount of time.
 | |
|         let mut total: Wrapping<u8> = Wrapping(0);
 | |
|         let mut exponent: u32 = 1;
 | |
|         for component_id in &access_components.0 {
 | |
|             // find the value of the component
 | |
|             let ptr = filtered_entity.get_by_id(*component_id).unwrap();
 | |
| 
 | |
|             #[expect(unsafe_code)]
 | |
|             // SAFETY: All components have a u8 layout
 | |
|             let value: u8 = unsafe { *ptr.deref::<u8>() };
 | |
| 
 | |
|             for i in 0..=value {
 | |
|                 let mut product = Wrapping(1);
 | |
|                 for _ in 1..=exponent {
 | |
|                     product *= Wrapping(i);
 | |
|                 }
 | |
|                 total += product;
 | |
|             }
 | |
|             exponent += 1;
 | |
|         }
 | |
| 
 | |
|         // we assign this value to all the components we can write to
 | |
|         for component_id in &access_components.0 {
 | |
|             if let Some(ptr) = filtered_entity.get_mut_by_id(*component_id) {
 | |
|                 #[expect(unsafe_code)]
 | |
|                 // SAFETY: All components have a u8 layout
 | |
|                 unsafe {
 | |
|                     let mut value = ptr.with_type::<u8>();
 | |
|                     *value = total.0;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| fn stress_test(num_entities: u32, num_components: u32, num_systems: u32) {
 | |
|     let mut rng = ChaCha8Rng::seed_from_u64(42);
 | |
|     let mut app = App::default();
 | |
|     let world = app.world_mut();
 | |
| 
 | |
|     // register a bunch of components
 | |
|     let component_ids: Vec<ComponentId> = (1..=num_components)
 | |
|         .map(|i| {
 | |
|             world.register_component_with_descriptor(
 | |
|                 #[allow(unsafe_code)]
 | |
|                 // SAFETY:
 | |
|                 // we don't implement a drop function
 | |
|                 // u8 is Sync and Send
 | |
|                 unsafe {
 | |
|                     ComponentDescriptor::new_with_layout(
 | |
|                         format!("Component{}", i).to_string(),
 | |
|                         StorageType::Table,
 | |
|                         Layout::new::<u8>(),
 | |
|                         None,
 | |
|                         true, // is mutable
 | |
|                     )
 | |
|                 },
 | |
|             )
 | |
|         })
 | |
|         .collect();
 | |
| 
 | |
|     // fill the schedule with systems
 | |
|     let mut schedule = Schedule::new(Update);
 | |
|     for _ in 1..=num_systems {
 | |
|         let num_access_components = rng.gen_range(1..10);
 | |
|         let access_components: Vec<ComponentId> = component_ids
 | |
|             .choose_multiple(&mut rng, num_access_components)
 | |
|             .copied()
 | |
|             .collect();
 | |
|         let system = (QueryParamBuilder::new(|builder| {
 | |
|             for &access_component in &access_components {
 | |
|                 if rand::random::<bool>() {
 | |
|                     builder.mut_id(access_component);
 | |
|                 } else {
 | |
|                     builder.ref_id(access_component);
 | |
|                 }
 | |
|             }
 | |
|         }),)
 | |
|             .build_state(world)
 | |
|             .build_any_system(base_system);
 | |
|         schedule.add_systems((move || access_components.clone()).pipe(system));
 | |
|     }
 | |
| 
 | |
|     // spawn a bunch of entities
 | |
|     for _ in 1..=num_entities {
 | |
|         let num_components = rng.gen_range(1..10);
 | |
|         let components = component_ids.choose_multiple(&mut rng, num_components);
 | |
| 
 | |
|         let mut entity = world.spawn_empty();
 | |
|         for &component_id in components {
 | |
|             let value: u8 = rng.gen_range(0..255);
 | |
|             OwningPtr::make(value, |ptr| {
 | |
|                 #[allow(unsafe_code)]
 | |
|                 // SAFETY:
 | |
|                 // component_id is from the same world
 | |
|                 // value is u8, so ptr is a valid reference for component_id
 | |
|                 unsafe {
 | |
|                     entity.insert_by_id(component_id, ptr);
 | |
|                 }
 | |
|             });
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // overwrite Update schedule in the app
 | |
|     app.add_schedule(schedule);
 | |
|     app.add_plugins(MinimalPlugins)
 | |
|         .add_plugins(DiagnosticsPlugin)
 | |
|         .add_plugins(LogPlugin::default())
 | |
|         .add_plugins(FrameTimeDiagnosticsPlugin)
 | |
|         .add_plugins(LogDiagnosticsPlugin::filtered(vec![DiagnosticPath::new(
 | |
|             "fps",
 | |
|         )]));
 | |
|     app.run();
 | |
| }
 | |
| 
 | |
| #[expect(missing_docs)]
 | |
| pub fn main() {
 | |
|     const DEFAULT_NUM_ENTITIES: u32 = 50000;
 | |
|     const DEFAULT_NUM_COMPONENTS: u32 = 1000;
 | |
|     const DEFAULT_NUM_SYSTEMS: u32 = 800;
 | |
| 
 | |
|     // take input
 | |
|     let num_entities = std::env::args()
 | |
|         .nth(1)
 | |
|         .and_then(|string| string.parse::<u32>().ok())
 | |
|         .unwrap_or_else(|| {
 | |
|             println!(
 | |
|                 "No valid number of entities provided, using default {}",
 | |
|                 DEFAULT_NUM_ENTITIES
 | |
|             );
 | |
|             DEFAULT_NUM_ENTITIES
 | |
|         });
 | |
|     let num_components = std::env::args()
 | |
|         .nth(2)
 | |
|         .and_then(|string| string.parse::<u32>().ok())
 | |
|         .and_then(|n| if n >= 10 { Some(n) } else { None })
 | |
|         .unwrap_or_else(|| {
 | |
|             println!(
 | |
|                 "No valid number of components provided (>= 10), using default {}",
 | |
|                 DEFAULT_NUM_COMPONENTS
 | |
|             );
 | |
|             DEFAULT_NUM_COMPONENTS
 | |
|         });
 | |
|     let num_systems = std::env::args()
 | |
|         .nth(3)
 | |
|         .and_then(|string| string.parse::<u32>().ok())
 | |
|         .unwrap_or_else(|| {
 | |
|             println!(
 | |
|                 "No valid number of systems provided, using default {}",
 | |
|                 DEFAULT_NUM_SYSTEMS
 | |
|             );
 | |
|             DEFAULT_NUM_SYSTEMS
 | |
|         });
 | |
| 
 | |
|     stress_test(num_entities, num_components, num_systems);
 | |
| }
 |