use super::Schedule; use crate::{ resource::Resources, system::{System, ThreadLocalExecution}, }; use bevy_hecs::{ArchetypesGeneration, TypeAccess, World}; use bevy_tasks::{ComputeTaskPool, CountdownEvent, TaskPool}; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::tracing::trace; use fixedbitset::FixedBitSet; use std::ops::Range; /// Executes each schedule stage in parallel by analyzing system dependencies. /// System execution order is undefined except under the following conditions: /// * systems in earlier stages run before systems in later stages /// * in a given stage, systems that mutate [archetype+component] X cannot run before systems registered before them that read/write [archetype+component] X /// * in a given stage, systems the read [archetype+component] X cannot run before systems registered before them that write [archetype+component] X /// * in a given stage, systems that mutate resource Y cannot run before systems registered before them that read/write resource Y /// * in a given stage, systems the read resource Y cannot run before systems registered before them that write resource Y #[derive(Debug)] pub struct ParallelExecutor { stages: Vec, last_schedule_generation: usize, clear_trackers: bool, } impl Default for ParallelExecutor { fn default() -> Self { Self { stages: Default::default(), last_schedule_generation: usize::MAX, // MAX forces prepare to run the first time clear_trackers: true, } } } impl ParallelExecutor { pub fn without_tracker_clears() -> Self { Self { clear_trackers: false, ..Default::default() } } pub fn initialize(&mut self, resources: &mut Resources) { if resources.get::().is_none() { resources.insert(ComputeTaskPool(TaskPool::default())); } } pub fn run(&mut self, schedule: &mut Schedule, world: &mut World, resources: &mut Resources) { #[cfg(feature = "trace")] let schedule_span = info_span!("schedule"); #[cfg(feature = "trace")] let _schedule_guard = schedule_span.enter(); let schedule_generation = schedule.generation(); let schedule_changed = schedule.generation() != self.last_schedule_generation; if schedule_changed { self.stages.clear(); self.stages .resize_with(schedule.stage_order.len(), ExecutorStage::default); } for (stage_name, executor_stage) in schedule.stage_order.iter().zip(self.stages.iter_mut()) { #[cfg(feature = "trace")] let stage_span = info_span!("stage", name = stage_name.as_ref()); #[cfg(feature = "trace")] let _stage_guard = stage_span.enter(); if let Some(stage_systems) = schedule.stages.get_mut(stage_name) { executor_stage.run(world, resources, stage_systems, schedule_changed); } } if self.clear_trackers { world.clear_trackers(); resources.clear_trackers(); } self.last_schedule_generation = schedule_generation; } pub fn print_order(&self, schedule: &Schedule) { println!("----------------------------"); for (stage_name, executor_stage) in schedule.stage_order.iter().zip(self.stages.iter()) { println!("stage {:?}", stage_name); if let Some(stage_systems) = schedule.stages.get(stage_name) { for (i, system) in stage_systems.iter().enumerate() { println!(" {}-{}", i, system.name()); println!( " dependencies({:?})", executor_stage.system_dependencies[i] .ones() .collect::>() ); println!( " dependants({:?})", executor_stage.system_dependents[i] ); } } } } } #[derive(Debug, Clone)] pub struct ExecutorStage { /// each system's set of dependencies system_dependencies: Vec, /// count of each system's dependencies system_dependency_count: Vec, /// Countdown of finished dependencies, used to trigger the next system ready_events: Vec>, /// When a system finishes, it will decrement the countdown events of all dependents ready_events_of_dependents: Vec>, /// each system's dependents (the systems that can't run until this system has run) system_dependents: Vec>, /// stores the indices of thread local systems in this stage, which are used during stage.prepare() thread_local_system_indices: Vec, /// When archetypes change a counter is bumped - we cache the state of that counter when it was /// last read here so that we can detect when archetypes are changed last_archetypes_generation: ArchetypesGeneration, } impl Default for ExecutorStage { fn default() -> Self { Self { system_dependents: Default::default(), system_dependency_count: Default::default(), ready_events: Default::default(), ready_events_of_dependents: Default::default(), system_dependencies: Default::default(), thread_local_system_indices: Default::default(), last_archetypes_generation: ArchetypesGeneration(u64::MAX), // MAX forces prepare to run the first time } } } impl ExecutorStage { /// Sets up state to run the next "batch" of systems. Each batch contains 0..n systems and /// optionally a thread local system at the end. After this function runs, a bunch of state /// in self will be populated for systems in this batch. Returns the range of systems /// that we prepared, up to but NOT including the thread local system that MIGHT be at the end /// of the range pub fn prepare_to_next_thread_local( &mut self, world: &World, systems: &mut [Box], schedule_changed: bool, next_thread_local_index: usize, ) -> Range { // Find the first system in this batch and (if there is one) the thread local system that // ends it. let (prepare_system_start_index, last_thread_local_index) = if next_thread_local_index == 0 { (0, None) } else { // start right after the last thread local system ( self.thread_local_system_indices[next_thread_local_index - 1] + 1, Some(self.thread_local_system_indices[next_thread_local_index - 1]), ) }; let prepare_system_index_range = if let Some(index) = self .thread_local_system_indices .get(next_thread_local_index) { // if there is an upcoming thread local system, prepare up to (and including) it prepare_system_start_index..(*index + 1) } else { // if there are no upcoming thread local systems, prepare everything right now prepare_system_start_index..systems.len() }; let archetypes_generation_changed = self.last_archetypes_generation != world.archetypes_generation(); if schedule_changed || archetypes_generation_changed { // update each system's [archetype+component] access to latest world archetypes for system_index in prepare_system_index_range.clone() { systems[system_index].update(world); // Clear this so that the next block of code that populates it doesn't insert // duplicates self.system_dependents[system_index].clear(); self.system_dependencies[system_index].clear(); } // calculate dependencies between systems and build execution order let mut current_archetype_access = TypeAccess::default(); let mut current_resource_access = TypeAccess::default(); for system_index in prepare_system_index_range.clone() { let system = &systems[system_index]; let archetype_access = system.archetype_component_access(); match system.thread_local_execution() { ThreadLocalExecution::NextFlush => { let resource_access = system.resource_access(); // if any system before this one conflicts, check all systems that came before for compatibility if !current_archetype_access.is_compatible(archetype_access) || !current_resource_access.is_compatible(resource_access) { #[allow(clippy::needless_range_loop)] for earlier_system_index in prepare_system_index_range.start..system_index { let earlier_system = &systems[earlier_system_index]; // due to how prepare ranges work, previous systems should all be "NextFlush" debug_assert_eq!( earlier_system.thread_local_execution(), ThreadLocalExecution::NextFlush ); // if earlier system is incompatible, make the current system dependent if !earlier_system .archetype_component_access() .is_compatible(archetype_access) || !earlier_system .resource_access() .is_compatible(resource_access) { self.system_dependents[earlier_system_index].push(system_index); self.system_dependencies[system_index] .insert(earlier_system_index); } } } current_archetype_access.union(archetype_access); current_resource_access.union(resource_access); if let Some(last_thread_local_index) = last_thread_local_index { self.system_dependents[last_thread_local_index].push(system_index); self.system_dependencies[system_index].insert(last_thread_local_index); } } ThreadLocalExecution::Immediate => { for earlier_system_index in prepare_system_index_range.start..system_index { // treat all earlier systems as "incompatible" to ensure we run this thread local system exclusively self.system_dependents[earlier_system_index].push(system_index); self.system_dependencies[system_index].insert(earlier_system_index); } } } } // Verify that dependents are not duplicated #[cfg(debug_assertions)] for system_index in prepare_system_index_range.clone() { let mut system_dependents_set = std::collections::HashSet::new(); for dependent_system in &self.system_dependents[system_index] { let inserted = system_dependents_set.insert(*dependent_system); // This means duplicate values are in the system_dependents list // This is reproducing when archetypes change. When we fix this, we can remove // the hack below and make this a debug-only assert or remove it debug_assert!(inserted); } } // Clear the ready events lists associated with each system so we can rebuild them for ready_events_of_dependents in &mut self.ready_events_of_dependents[prepare_system_index_range.clone()] { ready_events_of_dependents.clear(); } // Now that system_dependents and system_dependencies is populated, update // system_dependency_count and ready_events for system_index in prepare_system_index_range.clone() { // Count all dependencies to update system_dependency_count assert!(!self.system_dependencies[system_index].contains(system_index)); let dependency_count = self.system_dependencies[system_index].count_ones(..); self.system_dependency_count[system_index] = dependency_count; // If dependency count > 0, allocate a ready_event self.ready_events[system_index] = match self.system_dependency_count[system_index] { 0 => None, dependency_count => Some(CountdownEvent::new(dependency_count as isize)), } } // Now that ready_events are created, we can build ready_events_of_dependents for system_index in prepare_system_index_range.clone() { for dependent_system in &self.system_dependents[system_index] { self.ready_events_of_dependents[system_index].push( self.ready_events[*dependent_system] .as_ref() .expect("A dependent task should have a non-None ready event") .clone(), ); } } } else { // Reset the countdown events for this range of systems. Resetting is required even if the // schedule didn't change self.reset_system_ready_events(prepare_system_index_range); } if let Some(index) = self .thread_local_system_indices .get(next_thread_local_index) { // if there is an upcoming thread local system, prepare up to (and NOT including) it prepare_system_start_index..(*index) } else { // if there are no upcoming thread local systems, prepare everything right now prepare_system_start_index..systems.len() } } fn reset_system_ready_events(&mut self, prepare_system_index_range: Range) { for system_index in prepare_system_index_range { let dependency_count = self.system_dependency_count[system_index]; if dependency_count > 0 { self.ready_events[system_index] .as_ref() .expect("A system with >0 dependency count should have a non-None ready event") .reset(dependency_count as isize) } } } /// Runs the non-thread-local systems in the given prepared_system_range range pub fn run_systems( &self, world: &World, resources: &Resources, systems: &mut [Box], prepared_system_range: Range, compute_pool: &TaskPool, ) { // Generate tasks for systems in the given range and block until they are complete trace!("running systems {:?}", prepared_system_range); compute_pool.scope(|scope| { let start_system_index = prepared_system_range.start; let mut system_index = start_system_index; for system in &mut systems[prepared_system_range] { trace!( "prepare {} {} with {} dependents and {} dependencies", system_index, system.name(), self.system_dependents[system_index].len(), self.system_dependencies[system_index].count_ones(..) ); // This event will be awaited, preventing the task from starting until all // our dependencies finish running let ready_event = &self.ready_events[system_index]; // Clear any dependencies on systems before this range of systems. We know at this // point everything before start_system_index is finished, and our ready_event did // not exist to be decremented until we started processing this range if start_system_index != 0 { if let Some(ready_event) = ready_event.as_ref() { for dependency in self.system_dependencies[system_index].ones() { if dependency < start_system_index { ready_event.decrement(); } } } } let world_ref = &*world; let resources_ref = &*resources; let trigger_events = &self.ready_events_of_dependents[system_index]; // Verify that any dependent task has a > 0 count. If a dependent task has > 0 // count, then the current system we are starting now isn't blocking it from running // as it should be. Failure here implies the sync primitives are not matching the // intended schedule. This likely compiles out if trace/asserts are disabled but // make it explicitly debug-only anyways #[cfg(debug_assertions)] { let dependent_systems = &self.system_dependents[system_index]; debug_assert_eq!(trigger_events.len(), dependent_systems.len()); for (trigger_event, dependent_system_index) in trigger_events.iter().zip(dependent_systems) { debug_assert!( *dependent_system_index < start_system_index || trigger_event.get() > 0 ); } } // Spawn the task scope.spawn(async move { // Wait until our dependencies are done if let Some(ready_event) = ready_event { ready_event.listen().await; } // Execute the system - in a scope to ensure the system lock is dropped before // triggering dependents { #[cfg(feature = "trace")] let system_span = info_span!("system", name = system.name().as_ref()); #[cfg(feature = "trace")] let _system_guard = system_span.enter(); system.run(world_ref, resources_ref); } // Notify dependents that this task is done for trigger_event in trigger_events { trigger_event.decrement(); } }); system_index += 1; } }); } pub fn run( &mut self, world: &mut World, resources: &mut Resources, systems: &mut [Box], schedule_changed: bool, ) { let start_archetypes_generation = world.archetypes_generation(); let compute_pool = resources.get_cloned::().unwrap(); // if the schedule has changed, clear executor state / fill it with new defaults // This is mostly zeroing out a bunch of arrays parallel to the systems array. They will get // repopulated by prepare_to_next_thread_local() calls if schedule_changed { self.system_dependencies.clear(); self.system_dependencies .resize_with(systems.len(), || FixedBitSet::with_capacity(systems.len())); self.system_dependency_count.clear(); self.system_dependency_count.resize(systems.len(), 0); self.thread_local_system_indices = Vec::new(); self.system_dependents.clear(); self.system_dependents.resize(systems.len(), Vec::new()); self.ready_events.resize(systems.len(), None); self.ready_events_of_dependents .resize(systems.len(), Vec::new()); for (system_index, system) in systems.iter().enumerate() { if system.thread_local_execution() == ThreadLocalExecution::Immediate { #[cfg(feature = "trace")] let system_span = info_span!("system", name = system.name().as_ref()); #[cfg(feature = "trace")] let _system_guard = system_span.enter(); self.thread_local_system_indices.push(system_index); } } } // index of next thread local system in thread_local_system_indices. (always incremented by one // when prepare_to_next_thread_local is called. (We prepared up to index 0 above) let mut next_thread_local_index = 0; { // Prepare all system up to and including the first thread local system. This will return // the range of systems to run, up to but NOT including the next thread local let prepared_system_range = self.prepare_to_next_thread_local( world, systems, schedule_changed, next_thread_local_index, ); // Run everything up to the thread local system self.run_systems( world, resources, systems, prepared_system_range, &*compute_pool, ); } loop { // Bail if we have no more thread local systems if next_thread_local_index >= self.thread_local_system_indices.len() { break; } // Run the thread local system at the end of the range of systems we just processed let thread_local_system_index = self.thread_local_system_indices[next_thread_local_index]; { // if a thread local system is ready to run, run it exclusively on the main thread let system = systems[thread_local_system_index].as_mut(); #[cfg(feature = "trace")] let system_span = info_span!("thread_local_system", name = system.name().as_ref()); #[cfg(feature = "trace")] let _system_guard = system_span.enter(); system.run(world, resources); system.run_thread_local(world, resources); } // Now that the previous thread local system has run, time to advance to the next one next_thread_local_index += 1; // Prepare all systems up to and including the next thread local system. This will // return the range of systems to run, up to but NOT including the next thread local let run_ready_system_index_range = self.prepare_to_next_thread_local( world, systems, schedule_changed, next_thread_local_index, ); self.run_systems( world, resources, systems, run_ready_system_index_range, &*compute_pool, ); } // "flush" for system in systems.iter_mut() { match system.thread_local_execution() { ThreadLocalExecution::NextFlush => { #[cfg(feature = "trace")] let system_span = info_span!("system", name = system.name().as_ref()); #[cfg(feature = "trace")] let _system_guard = system_span.enter(); system.run_thread_local(world, resources); } ThreadLocalExecution::Immediate => { /* already ran */ } } } // If world's archetypes_generation is the same as it was before running any systems then // we can assume that all systems have correct archetype accesses. if start_archetypes_generation == world.archetypes_generation() { self.last_archetypes_generation = world.archetypes_generation(); } } } #[cfg(test)] mod tests { use super::ParallelExecutor; use crate::{ resource::{Res, ResMut, Resources}, schedule::Schedule, system::{IntoSystem, IntoThreadLocalSystem, Query}, Commands, }; use bevy_hecs::{Entity, World}; use bevy_tasks::{ComputeTaskPool, TaskPool}; use fixedbitset::FixedBitSet; use parking_lot::Mutex; use std::{collections::HashSet, sync::Arc}; #[derive(Default)] struct CompletedSystems { completed_systems: Arc>>, } #[test] fn cross_stage_archetype_change_prepare() { let mut world = World::new(); let mut resources = Resources::default(); resources.insert(ComputeTaskPool(TaskPool::default())); let mut schedule = Schedule::default(); schedule.add_stage("PreArchetypeChange"); schedule.add_stage("PostArchetypeChange"); fn insert(commands: &mut Commands) { commands.spawn((1u32,)); } fn read(query: Query<&u32>, entities: Query) { for entity in &mut entities.iter() { // query.get() does a "system permission check" that will fail if the entity is from a // new archetype which hasnt been "prepared yet" query.get_component::(entity).unwrap(); } assert_eq!(1, entities.iter().count()); } schedule.add_system_to_stage("PreArchetypeChange", insert.system()); schedule.add_system_to_stage("PostArchetypeChange", read.system()); let mut executor = ParallelExecutor::default(); schedule.initialize(&mut world, &mut resources); executor.run(&mut schedule, &mut world, &mut resources); } #[test] fn intra_stage_archetype_change_prepare() { let mut world = World::new(); let mut resources = Resources::default(); resources.insert(ComputeTaskPool(TaskPool::default())); let mut schedule = Schedule::default(); schedule.add_stage("update"); fn insert(world: &mut World, _resources: &mut Resources) { world.spawn((1u32,)); } fn read(query: Query<&u32>, entities: Query) { for entity in &mut entities.iter() { // query.get() does a "system permission check" that will fail if the entity is from a // new archetype which hasnt been "prepared yet" query.get_component::(entity).unwrap(); } assert_eq!(1, entities.iter().count()); } schedule.add_system_to_stage("update", insert.thread_local_system()); schedule.add_system_to_stage("update", read.system()); schedule.initialize(&mut world, &mut resources); let mut executor = ParallelExecutor::default(); executor.run(&mut schedule, &mut world, &mut resources); } #[test] fn schedule() { let mut world = World::new(); let mut resources = Resources::default(); resources.insert(ComputeTaskPool(TaskPool::default())); resources.insert(CompletedSystems::default()); resources.insert(1.0f64); resources.insert(2isize); world.spawn((1.0f32,)); world.spawn((1u32, 1u64)); world.spawn((2u32,)); let mut schedule = Schedule::default(); schedule.add_stage("A"); // component queries schedule.add_stage("B"); // thread local schedule.add_stage("C"); // resources // A system names const READ_U32_SYSTEM_NAME: &str = "read_u32"; const WRITE_FLOAT_SYSTEM_NAME: &str = "write_float"; const READ_U32_WRITE_U64_SYSTEM_NAME: &str = "read_u32_write_u64"; const READ_U64_SYSTEM_NAME: &str = "read_u64"; // B system names const WRITE_U64_SYSTEM_NAME: &str = "write_u64"; const THREAD_LOCAL_SYSTEM_SYSTEM_NAME: &str = "thread_local_system"; const WRITE_F32_SYSTEM_NAME: &str = "write_f32"; // C system names const READ_F64_RES_SYSTEM_NAME: &str = "read_f64_res"; const READ_ISIZE_RES_SYSTEM_NAME: &str = "read_isize_res"; const READ_ISIZE_WRITE_F64_RES_SYSTEM_NAME: &str = "read_isize_write_f64_res"; const WRITE_F64_RES_SYSTEM_NAME: &str = "write_f64_res"; // A systems fn read_u32(completed_systems: Res, _query: Query<&u32>) { let mut completed_systems = completed_systems.completed_systems.lock(); completed_systems.insert(READ_U32_SYSTEM_NAME); } fn write_float(completed_systems: Res, _query: Query<&f32>) { let mut completed_systems = completed_systems.completed_systems.lock(); completed_systems.insert(WRITE_FLOAT_SYSTEM_NAME); } fn read_u32_write_u64( completed_systems: Res, _query: Query<(&u32, &mut u64)>, ) { let mut completed_systems = completed_systems.completed_systems.lock(); assert!(!completed_systems.contains(READ_U64_SYSTEM_NAME)); completed_systems.insert(READ_U32_WRITE_U64_SYSTEM_NAME); } fn read_u64(completed_systems: Res, _query: Query<&u64>) { let mut completed_systems = completed_systems.completed_systems.lock(); assert!(completed_systems.contains(READ_U32_WRITE_U64_SYSTEM_NAME)); assert!(!completed_systems.contains(WRITE_U64_SYSTEM_NAME)); completed_systems.insert(READ_U64_SYSTEM_NAME); } schedule.add_system_to_stage("A", read_u32.system()); schedule.add_system_to_stage("A", write_float.system()); schedule.add_system_to_stage("A", read_u32_write_u64.system()); schedule.add_system_to_stage("A", read_u64.system()); // B systems fn write_u64(completed_systems: Res, _query: Query<&mut u64>) { let mut completed_systems = completed_systems.completed_systems.lock(); assert!(completed_systems.contains(READ_U64_SYSTEM_NAME)); assert!(!completed_systems.contains(THREAD_LOCAL_SYSTEM_SYSTEM_NAME)); assert!(!completed_systems.contains(WRITE_F32_SYSTEM_NAME)); completed_systems.insert(WRITE_U64_SYSTEM_NAME); } fn thread_local_system(_world: &mut World, resources: &mut Resources) { let completed_systems = resources.get::().unwrap(); let mut completed_systems = completed_systems.completed_systems.lock(); assert!(completed_systems.contains(WRITE_U64_SYSTEM_NAME)); assert!(!completed_systems.contains(WRITE_F32_SYSTEM_NAME)); completed_systems.insert(THREAD_LOCAL_SYSTEM_SYSTEM_NAME); } fn write_f32(completed_systems: Res, _query: Query<&mut f32>) { let mut completed_systems = completed_systems.completed_systems.lock(); assert!(completed_systems.contains(WRITE_U64_SYSTEM_NAME)); assert!(completed_systems.contains(THREAD_LOCAL_SYSTEM_SYSTEM_NAME)); assert!(!completed_systems.contains(READ_F64_RES_SYSTEM_NAME)); completed_systems.insert(WRITE_F32_SYSTEM_NAME); } schedule.add_system_to_stage("B", write_u64.system()); schedule.add_system_to_stage("B", thread_local_system.thread_local_system()); schedule.add_system_to_stage("B", write_f32.system()); // C systems fn read_f64_res(completed_systems: Res, _f64_res: Res) { let mut completed_systems = completed_systems.completed_systems.lock(); assert!(completed_systems.contains(WRITE_F32_SYSTEM_NAME)); assert!(!completed_systems.contains(READ_ISIZE_WRITE_F64_RES_SYSTEM_NAME)); assert!(!completed_systems.contains(WRITE_F64_RES_SYSTEM_NAME)); completed_systems.insert(READ_F64_RES_SYSTEM_NAME); } fn read_isize_res(completed_systems: Res, _isize_res: Res) { let mut completed_systems = completed_systems.completed_systems.lock(); completed_systems.insert(READ_ISIZE_RES_SYSTEM_NAME); } fn read_isize_write_f64_res( completed_systems: Res, _isize_res: Res, _f64_res: ResMut, ) { let mut completed_systems = completed_systems.completed_systems.lock(); assert!(completed_systems.contains(READ_F64_RES_SYSTEM_NAME)); assert!(!completed_systems.contains(WRITE_F64_RES_SYSTEM_NAME)); completed_systems.insert(READ_ISIZE_WRITE_F64_RES_SYSTEM_NAME); } fn write_f64_res(completed_systems: Res, _f64_res: ResMut) { let mut completed_systems = completed_systems.completed_systems.lock(); assert!(completed_systems.contains(READ_F64_RES_SYSTEM_NAME)); assert!(completed_systems.contains(READ_ISIZE_WRITE_F64_RES_SYSTEM_NAME)); completed_systems.insert(WRITE_F64_RES_SYSTEM_NAME); } schedule.add_system_to_stage("C", read_f64_res.system()); schedule.add_system_to_stage("C", read_isize_res.system()); schedule.add_system_to_stage("C", read_isize_write_f64_res.system()); schedule.add_system_to_stage("C", write_f64_res.system()); schedule.initialize(&mut world, &mut resources); fn run_executor_and_validate( executor: &mut ParallelExecutor, schedule: &mut Schedule, world: &mut World, resources: &mut Resources, ) { executor.run(schedule, world, resources); assert_eq!( executor.stages[0].system_dependents, vec![vec![], vec![], vec![3], vec![]] ); assert_eq!( executor.stages[1].system_dependents, vec![vec![1], vec![2], vec![]] ); assert_eq!( executor.stages[2].system_dependents, vec![vec![2, 3], vec![], vec![3], vec![]] ); let stage_0_len = executor.stages[0].system_dependencies.len(); let mut read_u64_deps = FixedBitSet::with_capacity(stage_0_len); read_u64_deps.insert(2); assert_eq!( executor.stages[0].system_dependencies, vec![ FixedBitSet::with_capacity(stage_0_len), FixedBitSet::with_capacity(stage_0_len), FixedBitSet::with_capacity(stage_0_len), read_u64_deps, ] ); let stage_1_len = executor.stages[1].system_dependencies.len(); let mut thread_local_deps = FixedBitSet::with_capacity(stage_1_len); thread_local_deps.insert(0); let mut write_f64_deps = FixedBitSet::with_capacity(stage_1_len); write_f64_deps.insert(1); assert_eq!( executor.stages[1].system_dependencies, vec![ FixedBitSet::with_capacity(stage_1_len), thread_local_deps, write_f64_deps ] ); let stage_2_len = executor.stages[2].system_dependencies.len(); let mut read_isize_write_f64_res_deps = FixedBitSet::with_capacity(stage_2_len); read_isize_write_f64_res_deps.insert(0); let mut write_f64_res_deps = FixedBitSet::with_capacity(stage_2_len); write_f64_res_deps.insert(0); write_f64_res_deps.insert(2); assert_eq!( executor.stages[2].system_dependencies, vec![ FixedBitSet::with_capacity(stage_2_len), FixedBitSet::with_capacity(stage_2_len), read_isize_write_f64_res_deps, write_f64_res_deps ] ); let completed_systems = resources.get::().unwrap(); assert_eq!( completed_systems.completed_systems.lock().len(), 11, "completed_systems should have been incremented once for each system" ); } // Stress test the "clean start" case for _ in 0..1000 { let mut executor = ParallelExecutor::default(); run_executor_and_validate(&mut executor, &mut schedule, &mut world, &mut resources); resources .get::() .unwrap() .completed_systems .lock() .clear(); } // Stress test the "continue running" case let mut executor = ParallelExecutor::default(); run_executor_and_validate(&mut executor, &mut schedule, &mut world, &mut resources); for _ in 0..1000 { // run again (with completed_systems reset) to ensure executor works correctly across runs resources .get::() .unwrap() .completed_systems .lock() .clear(); run_executor_and_validate(&mut executor, &mut schedule, &mut world, &mut resources); } } }