Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cached run_system API #14920

Merged
merged 22 commits into from
Sep 23, 2024
Merged

Conversation

benfrankel
Copy link
Contributor

@benfrankel benfrankel commented Aug 26, 2024

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: #10469.

Testing

This approach worked in a proof-of-concept: https://github.com/benfrankel/bevy_jam_template/blob/b34ee29531e3ceae287a9cc44ec6c520e83a4cdd/src/util/patch/run_system_cached.rs#L35.

A new unit test was added and it passes in CI.

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events C-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 26, 2024
@benfrankel
Copy link
Contributor Author

benfrankel commented Aug 26, 2024

Note: Now run_system_once_with is an odd one out for accepting input, system rather than system, input; and run_system_with_input is an odd one out for including the _input suffix. These are pre-existing inconsistencies that can be ironed out in a separate PR.

@janhohenheim
Copy link
Member

janhohenheim commented Aug 26, 2024

Great addition! Code looks good to me, but can we make the docs a bit more approachable? I understand them, but I doubt the average user will. Why is this useful? When should I prefer the uncached vs the cached variant? Maybe mention Local, and EventReader in particular. Further, can we link this from more places? I recall some part of the docs saying "running a system like this will not cache anything", maybe that can link to these?

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seconding the doc changes, and also requesting that this be a full replacement for the existing run_system API. The ergonomics of this are so much nicer, and we don't need yet another variant.

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 26, 2024
@benfrankel
Copy link
Contributor Author

Seconding the doc changes, and also requesting that this be a full replacement for the existing run_system API. The ergonomics of this are so much nicer, and we don't need yet another variant.

I think making this the default API (aka shortest name) would be reasonable, but I'm not sure about fully removing the existing API. Sometimes you do want to store and pass around a type-erased identifier for the system, like in an OnHit(SystemId) component. So you need to be able to run the system from the SystemId somehow.

Removing register_system in favor of register_system_cached makes sense, but then Commands::register_system would have different behavior than World::register_system. The former doesn't know whether it should spawn an entity until it checks World later, so it has to either always spawn an entity (not cached) or wait, and not be able to return a SystemId as a consequence (which mostly defeats the point).

@alice-i-cecile
Copy link
Member

Let's swap it to the default API for now then. I wouldn't mind removing Commands::register_system completely (you can just write a custom command...), but I'll see what others think.

@janhohenheim
Copy link
Member

janhohenheim commented Aug 26, 2024

Completely agree with @benfrankel here based on my experience, as register_system is nice for dynamic system shenanigans. Definitely an advanced use-case though.

@vil-mo
Copy link

vil-mo commented Aug 29, 2024

I find it strange that S and <S as IntoSystem<I, O, M>>::System would be cached twice, even though they cache the same system. You could replace the resource type with

pub struct CachedSystemId<S: System> {
    /// Cached `SystemId`.
    pub id: SystemId<S::In, S::Out>,
}

This will also allow the type level to more clearly define what the resource does and get rid of unnecessary generic parameters.

@benfrankel
Copy link
Contributor Author

Wrt making *_system_cached the default API, it's not clear what should be done for the register_* variants.

  1. register_system and register_system_cached have the same API accepting an IntoSystem and returning a SystemId, but the latter also saves the SystemId in a resource. Should register_system be fully removed and replaced with the cached version? If not, how should it be renamed? register_system_uncached doesn't seem great.
  2. register_boxed_system accepts a type-erased BoxedSystem, so there can't be a cached version with the same API. Can register_boxed_system just be removed? If not, should it be renamed to make it clear that it's not cached like register_system would be post-rename, or is it sufficient to mention it in the docs?
  3. Commands::register_system_cached doesn't exist atm because it can't check the CachedSystemId resource until the command runs with World access. As such, it can't .spawn_empty() ahead of time as there may be a pre-existing system entity, so it can't return a SystemId. Should it exist anyways but with no return value? And should Commands::register_system be renamed to register_system_uncached, or removed?

This PR is blocked on these questions. If there's no good way forward to make register_system_cached the default API, it may be best to keep the current names.

@janhohenheim
Copy link
Member

janhohenheim commented Aug 30, 2024

My two cents:

  1. After some reflection, I think the non-cached version should be removed. Using that over a command is too niche.
  2. No idea whether it should be removed or not. If we keep it, I think renaming is better for consistency. May I suggest the name force_register_* for uncached? It directly suggests that this API brings some intrigue with it.
  3. Is Commands::register_system_cached even usable without a SystemId? How do you retrieve the system? By registering it in World again? That sounds rather pointless. I would probably keep only the uncached variant and rename it. I again suggest force_register_*

These are not strong opinions, feel free to disregard.

@janhohenheim janhohenheim added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Aug 30, 2024
@benfrankel
Copy link
Contributor Author

benfrankel commented Aug 30, 2024

Okay, here's what the full rename would look like along those lines:

Type Old name New name
World register_system force_register_system
World register_boxed_system force_register_boxed_system
World run_system run_system_by_id
World run_system_with_input run_system_by_id_with
World remove_system remove_system_by_id
World register_system_cached register_system
World run_system_cached run_system
World run_system_cached_with run_system_with
World remove_system_cached remove_system
Commands register_system force_register_system
Commands run_system run_system_by_id
Commands run_system_with_input run_system_by_id_with
Commands run_system_cached run_system
Commands run_system_cached_with run_system_with

Notes:

  1. remove_system_by_id can't remove the CachedSystemId resource, but that should be fine because the "invalid cached ID" case is already handled by other methods.
  2. The current Commands doesn't include a remove_system, presumably because it can't return the removed system immediately, which is similar to why a cached register_system won't be included.

Is Commands::register_system_cached even usable without a SystemId? How do you retrieve the system? By registering it in World again?

You can't exactly use Res<CachedSystemId<type of foo>> as a system param, but if the CachedSystemId resources were consolidated into a single struct CachedSystems(TypeMap) resource, it would enable accessing that resource and calling some CachedSystems::get<S>(system: S) -> Option<SystemId> within a non-exclusive system. Exclusive systems can just do world.register_system(system) instead. So this may be something to consider.

Also, if CachedSystems also stored a backwards-edge map from SystemId to TypeId, it would enable remove_system_by_id to remove the cached system ID as well (see Note 1 above).

@benfrankel
Copy link
Contributor Author

benfrankel commented Sep 22, 2024

Ready for review again. Changes:

  • Disallowed size_of::<S> == 0
  • Improved docs
  • Added a unit test

Copy link
Contributor

@SkiFire13 SkiFire13 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR looks in a pretty good shape to me.

The only maybe issue I can see is that the ZST check won't work for systems created by pipe and map, even when the systems and closures involved are ZSTs. This is due to pipe and map eagerly calling into_system on their receiver, which will result in non-ZST types even when called on ZST types. The solution would be changing them to be lazy and call into_system on their receiver only when into_system is directly called on them.

@alice-i-cecile
Copy link
Member

The only maybe issue I can see is that the ZST check won't work for systems created by pipe and map, even when the systems and closures involved are ZSTs. This is due to pipe and map eagerly calling into_system on their receiver, which will result in non-ZST types even when called on ZST types. The solution would be changing them to be lazy and call into_system on their receiver only when into_system is directly called on them.

I'm happy enough to call this "fix in follow-up", but this is a good find.

@janhohenheim janhohenheim added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Sep 22, 2024
@janhohenheim
Copy link
Member

I'm happy enough to call this "fix in follow-up", but this is a good find.
Agreed! Could one of you (either @SkiFire13 or @benfrankel) write that down in an issue?

@benfrankel
Copy link
Contributor Author

Made an issue: #15373

@janhohenheim janhohenheim added this to the 0.15 milestone Sep 22, 2024
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy with the API now. Thanks for exploring this so thoroughly.

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 22, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Sep 23, 2024
@alice-i-cecile
Copy link
Member

Really lovely work; thanks for seeing this through.

Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sick! I do think as follow up we should consider flipping this around to be the "default" oneshot api.

Merged via the queue into bevyengine:main with commit f78856b Sep 23, 2024
28 checks passed
@benfrankel benfrankel deleted the system_cached branch September 23, 2024 23:56
@kaphula
Copy link

kaphula commented Sep 24, 2024

Hello,

I am testing the latest main branch of bevy now with this feature merged, but I am running into the compile time assert check for closures and function pointers even though my system should be normal?

My system definition looks like this:

pub fn start_road_building_system(
   gr: Res<GlobalResources>,
   gs: Res<GameState>,
   mut players: Query<(&mut PlayerStateECS, &HouseECS)>,
   junctions: Query<(&JunctionECS)>,
   road_paths: Query<(&NewRoadPieceECS)>,
   roads: Query<(&NewRoadECS)>,
) { ... }

And I am first using world instance to register the system as cached, then in another system, using Commands, I am calling cmd.run_system_cached(start_road_building_system).

Is there something I am missing, should not this work?

@SkiFire13
Copy link
Contributor

I think this is a bug in how the RunSystemCachedWith command is implemented (it stores the result of IntoSystem::into_system(system) and passes that one to world.run_system_cached, but that is not a ZST). I'm working on a fix, as a workaround you should be able to use cmd.queue(|world| world.run_system_cached(start_road_building_system))

github-merge-queue bot pushed a commit that referenced this pull request Sep 24, 2024
# Objective

- Fixes #15373
- Fixes
#14920 (comment)

## Solution

- Make `IntoSystem::pipe` and `IntoSystem::map` return two new
(possibly-ZST) types that implement `IntoSystem` and whose `into_system`
method return the systems that were previously being returned by
`IntoSystem::pipe` and `IntoSystem::map`
- Don't eagerly call `IntoSystem::into_system` on the argument given to
`RunSystemCachedWith::new` to avoid losing its ZST-ness

## Testing

- Added a regression test for each issue

## Migration Guide

- `IntoSystem::pipe` and `IntoSystem::map` now return `IntoPipeSystem`
and `IntoAdapterSystem` instead of `PipeSystem` and `AdapterSystem`.
Most notably these types don't implement `System` but rather only
`IntoSystem`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible C-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants