Automatic cleanup of one-shot systems#24114
Conversation
|
Strongly in favor of this core idea! |
| /// The cached `SystemId` as an `Entity`. | ||
| pub entity: Entity, | ||
| pub struct CachedSystemHandle<S> { | ||
| strong: Arc<StrongSystemHandle>, |
There was a problem hiding this comment.
Why use this for cached systems? The Arcs are extra overhead, and most use cases of cached systems will never want to despawn them.
| IntoSystem::into_system(system), | ||
| ))) | ||
| .id(); | ||
| SystemHandle::Weak(SystemId::from_entity(entity)) |
There was a problem hiding this comment.
World::register_system and Commands::register_system returning the same type but having different behavior will be very surprising! Users might get used to the cleanup from the World version, then use Commands somewhere and introduce a leak.
I also expect we'll have cases that won't need automatic cleanup, and it would be nice to avoid the overhead of Arcs in those cases.
Could we expose separate methods on World to register automatically-cleaned and manually-cleaned systems, with different names and returning different types? Then the method on Commands could correspond directly to a method on World.
There was a problem hiding this comment.
I think switching Commands::register_system back to returning SystemId instead of SystemHandle would be the simplest fix here; users will still be able to wrap SystemIds into SystemHandles themselves, but do so explicitly. Thoughts?
There was a problem hiding this comment.
I think switching
Commands::register_systemback to returningSystemIdinstead ofSystemHandlewould be the simplest fix here; users will still be able to wrapSystemIds intoSystemHandles themselves, but do so explicitly. Thoughts?
That might help, although those are both impl Into<SystemId> and could be passed directly to run_system without changes.
I do also think we'll want to expose a method on World that returns a manually-cleaned system, rather than having to tell users that world.commands().register_system(s); world.flush(); is the simplest way to do it.
So I would still vote for giving them different names, even if we don't offer the manually-cleaned version on World. But I also have no authority here and am happy to be overridden :).
There was a problem hiding this comment.
Oh, and I hope I don't sound negative on the overall idea. Automatic cleanup seems like a powerful pattern! I just think it should be decoupled from register_system a bit, since register_system is useful without automatic cleanup. (And automatic cleanup will be useful for other kinds of entities!)
| pub enum SystemHandle<I: SystemInput = (), O = ()> { | ||
| /// A strong handle keeps the system entity alive as long as the handle | ||
| /// (and any clones of it) exist. | ||
| Strong(Arc<StrongSystemHandle>), |
There was a problem hiding this comment.
I see how we got here, but looking at the whole thing together: Why even store the system as an entity in the World? As long as we're allocating Arcs, why not just use Arc<Mutex<dyn System>>? That gets cleaned up automatically without needing a channel or anything.
There was a problem hiding this comment.
I think there's some apprehension to requiring locking, as noted in #24072. That PR works as-is however in case we want to explore that option instead.
There was a problem hiding this comment.
I think there's some apprehension to requiring locking, as noted in #24072. That PR works as-is however in case we want to explore that option instead.
Oh, I just meant that client code could use Arc themselves instead of using registered systems. Once you have an Arc that you can share, there's no need to store anything in the World!
... Hmm, I bet we could get rid of the locking there if we needed. The reason we don't have to lock today is that the system is owned by the World, so the &mut World gives us access to the system. But we could simulate that with a SyncUnsafeCell that has a safety requirement that it only be accessed by code with &mut World for the right world! We'd still need synchronization to set the WorldId, but that would only be on the first run.
Something like:
struct SystemArcInner<S: ?Sized> {
world_id: OnceLock<WorldId>,
running: SyncUnsafeCell<bool>,
system: SyncUnsafeCell<S>,
}
impl<S: System> SystemArcInner {
fn run(&self, world: &mut World) {
// This takes locks the first time it's run, but is an ordinary load afterwards
let world_id = self.world_id.get_or_init(|| {
// This is safe because `Once` ensures only one thread runs an initializer
unsafe { &mut *self.system.get() }.initialize(world);
world.id()
});
assert_eq!(world.id(), world_id);
// Claim the `running` lock
// This is safe because we only access `running` when holding `&mut World` for *this* `World`
let running = unsafe { &mut *self.running.get() };
assert!(!*running);
*running = true;
// This is safe because we have the `running` lock
let system = unsafe { &mut *self.system.get() };
system.run(world);
// Release the `running` lock
// Note that this needs a new reference to `running` in case of reentrancy!
// TODO: This should be in a `Drop` impl so it runs on unwind
unsafe { *self.running.get() = false };
}
}
Objective
#24087 introduces scene templating for
SystemIds, however it can result in a memory leak if a scene is re-constructed multiple times:#24087 (comment)
#24087 (comment)
Essentially, we need a way to connect the lifetime of the registered system to the lifetime of the scene.
Solution
Introducing:
SystemHandles.Similar to
bevy_asset::Handles,SystemHandle's customDropimplementation enqueues the registered system entity into a message channel. The systemdespawn_unused_registered_systemspulls from the other end of this message channel and despawns the registered system entities.World::register_systemfamily of functions return strong handles, whileCommands::register_systemreturns weak handles (due to the limitation of world access).Testing
despawn_unused_registered_systemsdoes its jobdespawn_unused_registered_systemsFuture work
SystemIdscene templating #24087 will use this PR as a baseNoCleanupmarker component that's attachable to registered system entities, and filter out entities with this component insidedespawn_unused_registered_systemsstd.