Add cached run_system API (#14920)
				
					
				
			# Objective
Working with `World` is painful due to lifetime issues and a lack of
ergonomics, so you may want to delegate to the system API. Your current
options are:
- `world.run_system_once`, which initializes the system each time it's
called (performance cost) and doesn't support `Local`. The docs
recommend users not use this method outside of diagnostic use cases like
unit tests.
- `world.run_system`, which requires you to register the system and
store the `SystemId` somewhere (made easier by implementing `FromWorld`
for a newtyped `Local`, unless you're in e.g. a custom `Command` impl).
These options work, but you're choosing between a performance cost and
an ergonomic challenge.
## Solution
Provide a cached `run_system` API that accepts an `S: IntoSystem` and
checks for a `CachedSystemId<S::System>(SystemId)` resource. If it
doesn't exist, it will register the system and save its `SystemId` in
that resource.
In other words, it hides the "save the `SystemId` in a `Local` or
`Resource`" pattern as an implementation detail.
Prior work: https://github.com/bevyengine/bevy/pull/10469.
## Testing
This approach worked in a proof-of-concept:
b34ee29531/src/util/patch/run_system_cached.rs (L35).
A new unit test was added and it passes in CI.
			
			
This commit is contained in:
		
							parent
							
								
									4682f33e0c
								
							
						
					
					
						commit
						f78856b3bd
					
				| @ -3,7 +3,9 @@ mod parallel_scope; | |||||||
| use core::panic::Location; | use core::panic::Location; | ||||||
| use std::marker::PhantomData; | use std::marker::PhantomData; | ||||||
| 
 | 
 | ||||||
| use super::{Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource}; | use super::{ | ||||||
|  |     Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource, RunSystemCachedWith, | ||||||
|  | }; | ||||||
| use crate::{ | use crate::{ | ||||||
|     self as bevy_ecs, |     self as bevy_ecs, | ||||||
|     bundle::{Bundle, InsertMode}, |     bundle::{Bundle, InsertMode}, | ||||||
| @ -758,6 +760,30 @@ impl<'w, 's> Commands<'w, 's> { | |||||||
|         SystemId::from_entity(entity) |         SystemId::from_entity(entity) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// Similar to [`Self::run_system`], but caching the [`SystemId`] in a
 | ||||||
|  |     /// [`CachedSystemId`](crate::system::CachedSystemId) resource.
 | ||||||
|  |     ///
 | ||||||
|  |     /// See [`World::register_system_cached`] for more information.
 | ||||||
|  |     pub fn run_system_cached<M: 'static, S: IntoSystem<(), (), M> + 'static>(&mut self, system: S) { | ||||||
|  |         self.run_system_cached_with(system, ()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Similar to [`Self::run_system_with_input`], but caching the [`SystemId`] in a
 | ||||||
|  |     /// [`CachedSystemId`](crate::system::CachedSystemId) resource.
 | ||||||
|  |     ///
 | ||||||
|  |     /// See [`World::register_system_cached`] for more information.
 | ||||||
|  |     pub fn run_system_cached_with< | ||||||
|  |         I: 'static + Send, | ||||||
|  |         M: 'static, | ||||||
|  |         S: IntoSystem<I, (), M> + 'static, | ||||||
|  |     >( | ||||||
|  |         &mut self, | ||||||
|  |         system: S, | ||||||
|  |         input: I, | ||||||
|  |     ) { | ||||||
|  |         self.queue(RunSystemCachedWith::new(system, input)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /// Sends a "global" [`Trigger`] without any targets. This will run any [`Observer`] of the `event` that
 |     /// Sends a "global" [`Trigger`] without any targets. This will run any [`Observer`] of the `event` that
 | ||||||
|     /// isn't scoped to specific targets.
 |     /// isn't scoped to specific targets.
 | ||||||
|     ///
 |     ///
 | ||||||
|  | |||||||
| @ -208,8 +208,8 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> { | |||||||
| /// world.run_system_once(increment); // still prints 1
 | /// world.run_system_once(increment); // still prints 1
 | ||||||
| /// ```
 | /// ```
 | ||||||
| ///
 | ///
 | ||||||
| /// If you do need systems to hold onto state between runs, use the [`World::run_system`](World::run_system)
 | /// If you do need systems to hold onto state between runs, use [`World::run_system_cached`](World::run_system_cached)
 | ||||||
| /// and run the system by their [`SystemId`](crate::system::SystemId).
 | /// or [`World::run_system`](World::run_system).
 | ||||||
| ///
 | ///
 | ||||||
| /// # Usage
 | /// # Usage
 | ||||||
| /// Typically, to test a system, or to extract specific diagnostics information from a world,
 | /// Typically, to test a system, or to extract specific diagnostics information from a world,
 | ||||||
|  | |||||||
| @ -1,8 +1,10 @@ | |||||||
|  | use crate::bundle::Bundle; | ||||||
|  | use crate::change_detection::Mut; | ||||||
| use crate::entity::Entity; | use crate::entity::Entity; | ||||||
| use crate::system::{BoxedSystem, IntoSystem}; | use crate::system::{BoxedSystem, IntoSystem, System}; | ||||||
| use crate::world::{Command, World}; | use crate::world::{Command, World}; | ||||||
| use crate::{self as bevy_ecs}; | use crate::{self as bevy_ecs}; | ||||||
| use bevy_ecs_macros::Component; | use bevy_ecs_macros::{Component, Resource}; | ||||||
| use thiserror::Error; | use thiserror::Error; | ||||||
| 
 | 
 | ||||||
| /// A small wrapper for [`BoxedSystem`] that also keeps track whether or not the system has been initialized.
 | /// A small wrapper for [`BoxedSystem`] that also keeps track whether or not the system has been initialized.
 | ||||||
| @ -102,10 +104,29 @@ impl<I, O> std::fmt::Debug for SystemId<I, O> { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// A cached [`SystemId`] distinguished by the unique function type of its system.
 | ||||||
|  | ///
 | ||||||
|  | /// This resource is inserted by [`World::register_system_cached`].
 | ||||||
|  | #[derive(Resource)] | ||||||
|  | pub struct CachedSystemId<S: System>(pub SystemId<S::In, S::Out>); | ||||||
|  | 
 | ||||||
|  | /// Creates a [`Bundle`] for a one-shot system entity.
 | ||||||
|  | fn system_bundle<I: 'static, O: 'static>(system: BoxedSystem<I, O>) -> impl Bundle { | ||||||
|  |     ( | ||||||
|  |         RegisteredSystem { | ||||||
|  |             initialized: false, | ||||||
|  |             system, | ||||||
|  |         }, | ||||||
|  |         SystemIdMarker, | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| impl World { | impl World { | ||||||
|     /// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`].
 |     /// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`].
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// It's possible to register the same systems more than once, they'll be stored separately.
 |     /// It's possible to register multiple copies of the same system by calling this function
 | ||||||
|  |     /// multiple times. If that's not what you want, consider using [`World::register_system_cached`]
 | ||||||
|  |     /// instead.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// This is different from adding systems to a [`Schedule`](crate::schedule::Schedule),
 |     /// This is different from adding systems to a [`Schedule`](crate::schedule::Schedule),
 | ||||||
|     /// because the [`SystemId`] that is returned can be used anywhere in the [`World`] to run the associated system.
 |     /// because the [`SystemId`] that is returned can be used anywhere in the [`World`] to run the associated system.
 | ||||||
| @ -122,23 +143,13 @@ impl World { | |||||||
|     /// Similar to [`Self::register_system`], but allows passing in a [`BoxedSystem`].
 |     /// Similar to [`Self::register_system`], but allows passing in a [`BoxedSystem`].
 | ||||||
|     ///
 |     ///
 | ||||||
|     ///  This is useful if the [`IntoSystem`] implementor has already been turned into a
 |     ///  This is useful if the [`IntoSystem`] implementor has already been turned into a
 | ||||||
|     /// [`System`](crate::system::System) trait object and put in a [`Box`].
 |     /// [`System`] trait object and put in a [`Box`].
 | ||||||
|     pub fn register_boxed_system<I: 'static, O: 'static>( |     pub fn register_boxed_system<I: 'static, O: 'static>( | ||||||
|         &mut self, |         &mut self, | ||||||
|         system: BoxedSystem<I, O>, |         system: BoxedSystem<I, O>, | ||||||
|     ) -> SystemId<I, O> { |     ) -> SystemId<I, O> { | ||||||
|         SystemId { |         let entity = self.spawn(system_bundle(system)).id(); | ||||||
|             entity: self |         SystemId::from_entity(entity) | ||||||
|                 .spawn(( |  | ||||||
|                     RegisteredSystem { |  | ||||||
|                         initialized: false, |  | ||||||
|                         system, |  | ||||||
|                     }, |  | ||||||
|                     SystemIdMarker, |  | ||||||
|                 )) |  | ||||||
|                 .id(), |  | ||||||
|             marker: std::marker::PhantomData, |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Removes a registered system and returns the system, if it exists.
 |     /// Removes a registered system and returns the system, if it exists.
 | ||||||
| @ -320,6 +331,89 @@ impl World { | |||||||
|         } |         } | ||||||
|         Ok(result) |         Ok(result) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /// Registers a system or returns its cached [`SystemId`].
 | ||||||
|  |     ///
 | ||||||
|  |     /// If you want to run the system immediately and you don't need its `SystemId`, see
 | ||||||
|  |     /// [`World::run_system_cached`].
 | ||||||
|  |     ///
 | ||||||
|  |     /// The first time this function is called for a particular system, it will register it and
 | ||||||
|  |     /// store its [`SystemId`] in a [`CachedSystemId`] resource for later. If you would rather
 | ||||||
|  |     /// manage the `SystemId` yourself, or register multiple copies of the same system, use
 | ||||||
|  |     /// [`World::register_system`] instead.
 | ||||||
|  |     ///
 | ||||||
|  |     /// # Limitations
 | ||||||
|  |     ///
 | ||||||
|  |     /// This function only accepts ZST (zero-sized) systems to guarantee that any two systems of
 | ||||||
|  |     /// the same type must be equal. This means that closures that capture the environment, and
 | ||||||
|  |     /// function pointers, are not accepted.
 | ||||||
|  |     ///
 | ||||||
|  |     /// If you want to access values from the environment within a system, consider passing them in
 | ||||||
|  |     /// as inputs via [`World::run_system_cached_with`]. If that's not an option, consider
 | ||||||
|  |     /// [`World::register_system`] instead.
 | ||||||
|  |     pub fn register_system_cached<I: 'static, O: 'static, M, S: IntoSystem<I, O, M> + 'static>( | ||||||
|  |         &mut self, | ||||||
|  |         system: S, | ||||||
|  |     ) -> SystemId<I, O> { | ||||||
|  |         const { | ||||||
|  |             assert!( | ||||||
|  |                 size_of::<S>() == 0, | ||||||
|  |                 "Non-ZST systems (e.g. capturing closures, function pointers) cannot be cached.", | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if !self.contains_resource::<CachedSystemId<S::System>>() { | ||||||
|  |             let id = self.register_system(system); | ||||||
|  |             self.insert_resource(CachedSystemId::<S::System>(id)); | ||||||
|  |             return id; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self.resource_scope(|world, mut id: Mut<CachedSystemId<S::System>>| { | ||||||
|  |             if let Some(mut entity) = world.get_entity_mut(id.0.entity()) { | ||||||
|  |                 if !entity.contains::<RegisteredSystem<I, O>>() { | ||||||
|  |                     entity.insert(system_bundle(Box::new(IntoSystem::into_system(system)))); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 id.0 = world.register_system(system); | ||||||
|  |             } | ||||||
|  |             id.0 | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Removes a cached system and its [`CachedSystemId`] resource.
 | ||||||
|  |     ///
 | ||||||
|  |     /// See [`World::register_system_cached`] for more information.
 | ||||||
|  |     pub fn remove_system_cached<I: 'static, O: 'static, M, S: IntoSystem<I, O, M> + 'static>( | ||||||
|  |         &mut self, | ||||||
|  |         _system: S, | ||||||
|  |     ) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>> { | ||||||
|  |         let id = self | ||||||
|  |             .remove_resource::<CachedSystemId<S::System>>() | ||||||
|  |             .ok_or(RegisteredSystemError::SystemNotCached)?; | ||||||
|  |         self.remove_system(id.0) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Runs a cached system, registering it if necessary.
 | ||||||
|  |     ///
 | ||||||
|  |     /// See [`World::register_system_cached`] for more information.
 | ||||||
|  |     pub fn run_system_cached<O: 'static, M, S: IntoSystem<(), O, M> + 'static>( | ||||||
|  |         &mut self, | ||||||
|  |         system: S, | ||||||
|  |     ) -> Result<O, RegisteredSystemError<(), O>> { | ||||||
|  |         self.run_system_cached_with(system, ()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Runs a cached system with an input, registering it if necessary.
 | ||||||
|  |     ///
 | ||||||
|  |     /// See [`World::register_system_cached`] for more information.
 | ||||||
|  |     pub fn run_system_cached_with<I: 'static, O: 'static, M, S: IntoSystem<I, O, M> + 'static>( | ||||||
|  |         &mut self, | ||||||
|  |         system: S, | ||||||
|  |         input: I, | ||||||
|  |     ) -> Result<O, RegisteredSystemError<I, O>> { | ||||||
|  |         let id = self.register_system_cached(system); | ||||||
|  |         self.run_system_with_input(id, input) | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// The [`Command`] type for [`World::run_system`] or [`World::run_system_with_input`].
 | /// The [`Command`] type for [`World::run_system`] or [`World::run_system_with_input`].
 | ||||||
| @ -353,7 +447,7 @@ pub struct RunSystemWithInput<I: 'static> { | |||||||
| pub type RunSystem = RunSystemWithInput<()>; | pub type RunSystem = RunSystemWithInput<()>; | ||||||
| 
 | 
 | ||||||
| impl RunSystem { | impl RunSystem { | ||||||
|     /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands)
 |     /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands).
 | ||||||
|     pub fn new(system_id: SystemId) -> Self { |     pub fn new(system_id: SystemId) -> Self { | ||||||
|         Self::new_with_input(system_id, ()) |         Self::new_with_input(system_id, ()) | ||||||
|     } |     } | ||||||
| @ -374,16 +468,16 @@ impl<I: 'static + Send> Command for RunSystemWithInput<I> { | |||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// The [`Command`] type for registering one shot systems from [Commands](crate::system::Commands).
 | /// The [`Command`] type for registering one shot systems from [`Commands`](crate::system::Commands).
 | ||||||
| ///
 | ///
 | ||||||
| /// This command needs an already boxed system to register, and an already spawned entity
 | /// This command needs an already boxed system to register, and an already spawned entity.
 | ||||||
| pub struct RegisterSystem<I: 'static, O: 'static> { | pub struct RegisterSystem<I: 'static, O: 'static> { | ||||||
|     system: BoxedSystem<I, O>, |     system: BoxedSystem<I, O>, | ||||||
|     entity: Entity, |     entity: Entity, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<I: 'static, O: 'static> RegisterSystem<I, O> { | impl<I: 'static, O: 'static> RegisterSystem<I, O> { | ||||||
|     /// Creates a new [Command] struct, which can be added to [Commands](crate::system::Commands)
 |     /// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands).
 | ||||||
|     pub fn new<M, S: IntoSystem<I, O, M> + 'static>(system: S, entity: Entity) -> Self { |     pub fn new<M, S: IntoSystem<I, O, M> + 'static>(system: S, entity: Entity) -> Self { | ||||||
|         Self { |         Self { | ||||||
|             system: Box::new(IntoSystem::into_system(system)), |             system: Box::new(IntoSystem::into_system(system)), | ||||||
| @ -394,12 +488,38 @@ impl<I: 'static, O: 'static> RegisterSystem<I, O> { | |||||||
| 
 | 
 | ||||||
| impl<I: 'static + Send, O: 'static + Send> Command for RegisterSystem<I, O> { | impl<I: 'static + Send, O: 'static + Send> Command for RegisterSystem<I, O> { | ||||||
|     fn apply(self, world: &mut World) { |     fn apply(self, world: &mut World) { | ||||||
|         let _ = world.get_entity_mut(self.entity).map(|mut entity| { |         if let Some(mut entity) = world.get_entity_mut(self.entity) { | ||||||
|             entity.insert(RegisteredSystem { |             entity.insert(system_bundle(self.system)); | ||||||
|                 initialized: false, |         } | ||||||
|                 system: self.system, |     } | ||||||
|             }); | } | ||||||
|         }); | 
 | ||||||
|  | /// The [`Command`] type for running a cached one-shot system from
 | ||||||
|  | /// [`Commands`](crate::system::Commands).
 | ||||||
|  | ///
 | ||||||
|  | /// See [`World::register_system_cached`] for more information.
 | ||||||
|  | pub struct RunSystemCachedWith<S: System<Out = ()>> { | ||||||
|  |     system: S, | ||||||
|  |     input: S::In, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<S: System<Out = ()>> RunSystemCachedWith<S> { | ||||||
|  |     /// Creates a new [`Command`] struct, which can be added to
 | ||||||
|  |     /// [`Commands`](crate::system::Commands).
 | ||||||
|  |     pub fn new<M>(system: impl IntoSystem<S::In, (), M, System = S>, input: S::In) -> Self { | ||||||
|  |         Self { | ||||||
|  |             system: IntoSystem::into_system(system), | ||||||
|  |             input, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl<S: System<Out = ()>> Command for RunSystemCachedWith<S> | ||||||
|  | where | ||||||
|  |     S::In: Send, | ||||||
|  | { | ||||||
|  |     fn apply(self, world: &mut World) { | ||||||
|  |         let _ = world.run_system_cached_with(self.system, self.input); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -411,6 +531,11 @@ pub enum RegisteredSystemError<I = (), O = ()> { | |||||||
|     /// Did you forget to register it?
 |     /// Did you forget to register it?
 | ||||||
|     #[error("System {0:?} was not registered")] |     #[error("System {0:?} was not registered")] | ||||||
|     SystemIdNotRegistered(SystemId<I, O>), |     SystemIdNotRegistered(SystemId<I, O>), | ||||||
|  |     /// A cached system was removed by value, but no system with its type was found.
 | ||||||
|  |     ///
 | ||||||
|  |     /// Did you forget to register it?
 | ||||||
|  |     #[error("Cached system was not found")] | ||||||
|  |     SystemNotCached, | ||||||
|     /// A system tried to run itself recursively.
 |     /// A system tried to run itself recursively.
 | ||||||
|     #[error("System {0:?} tried to run itself recursively")] |     #[error("System {0:?} tried to run itself recursively")] | ||||||
|     Recursive(SystemId<I, O>), |     Recursive(SystemId<I, O>), | ||||||
| @ -425,6 +550,7 @@ impl<I, O> std::fmt::Debug for RegisteredSystemError<I, O> { | |||||||
|             Self::SystemIdNotRegistered(arg0) => { |             Self::SystemIdNotRegistered(arg0) => { | ||||||
|                 f.debug_tuple("SystemIdNotRegistered").field(arg0).finish() |                 f.debug_tuple("SystemIdNotRegistered").field(arg0).finish() | ||||||
|             } |             } | ||||||
|  |             Self::SystemNotCached => write!(f, "SystemNotCached"), | ||||||
|             Self::Recursive(arg0) => f.debug_tuple("Recursive").field(arg0).finish(), |             Self::Recursive(arg0) => f.debug_tuple("Recursive").field(arg0).finish(), | ||||||
|             Self::SelfRemove(arg0) => f.debug_tuple("SelfRemove").field(arg0).finish(), |             Self::SelfRemove(arg0) => f.debug_tuple("SelfRemove").field(arg0).finish(), | ||||||
|         } |         } | ||||||
| @ -625,4 +751,35 @@ mod tests { | |||||||
|         let _ = world.run_system(nested_id); |         let _ = world.run_system(nested_id); | ||||||
|         assert_eq!(*world.resource::<Counter>(), Counter(5)); |         assert_eq!(*world.resource::<Counter>(), Counter(5)); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     #[test] | ||||||
|  |     fn cached_system() { | ||||||
|  |         use crate::system::RegisteredSystemError; | ||||||
|  | 
 | ||||||
|  |         fn four() -> i32 { | ||||||
|  |             4 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let mut world = World::new(); | ||||||
|  |         let old = world.register_system_cached(four); | ||||||
|  |         let new = world.register_system_cached(four); | ||||||
|  |         assert_eq!(old, new); | ||||||
|  | 
 | ||||||
|  |         let result = world.remove_system_cached(four); | ||||||
|  |         assert!(result.is_ok()); | ||||||
|  |         let new = world.register_system_cached(four); | ||||||
|  |         assert_ne!(old, new); | ||||||
|  | 
 | ||||||
|  |         let output = world.run_system(old); | ||||||
|  |         assert!(matches!( | ||||||
|  |             output, | ||||||
|  |             Err(RegisteredSystemError::SystemIdNotRegistered(x)) if x == old, | ||||||
|  |         )); | ||||||
|  |         let output = world.run_system(new); | ||||||
|  |         assert!(matches!(output, Ok(x) if x == four())); | ||||||
|  |         let output = world.run_system_cached(four); | ||||||
|  |         assert!(matches!(output, Ok(x) if x == four())); | ||||||
|  |         let output = world.run_system_cached_with(four, ()); | ||||||
|  |         assert!(matches!(output, Ok(x) if x == four())); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Ben Frankel
						Ben Frankel