Skip to content

Tracked one-shot systems - #24165

Merged
mockersf merged 6 commits into
bevyengine:mainfrom
ItsDoot:ecs/optinoneshothandle
May 22, 2026
Merged

Tracked one-shot systems#24165
mockersf merged 6 commits into
bevyengine:mainfrom
ItsDoot:ecs/optinoneshothandle

Conversation

@ItsDoot

@ItsDoot ItsDoot commented May 7, 2026

Copy link
Copy Markdown
Contributor

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)

This was proposed basically 1:1 in #24026 (this was later changed though). The issue is that it's unclear who owns these systems, that is who is responsible for unregistering them once they are no longer needed. Given that recreating the template will spawn the system again this basically becomes a memory leak.

#24087 (comment)

Hm, are you sure this is the case even tho in build_template it only registers the system the first time its called, switching over to storing the SystemId after the first call?

If you recreate the template (e.g. you call my_scene() again) then you will create a new instance of the system. And since the system is not scoped to the scene once the scene is despawned the system entity will be leaked.

Essentially, we need a way to connect the lifetime of the registered system to the lifetime of the scene.

Solution

This is a purely additive / opt-in / backwards-compatible version of #24114

Introducing: SystemHandles

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>),
    /// A weak handle does not keep the system entity alive.
    Weak(SystemId<I, O>),
}

pub struct StrongSystemHandle {
    entity: Entity,
    drop_queue: Arc<ConcurrentQueue<<Entity>>,
}

Similar to bevy_asset::Handles,SystemHandle's custom Drop implementation enqueues the registered system entity into a concurrent queue. The system despawn_unused_registered_systems pulls from the other end of this queue and despawns the registered system entities.

World::register_tracked_system and World::register_tracked_boxed_system are the only functions that return SystemHandles.

Testing

  • Added a test to ensure that despawn_unused_registered_systems does its job
  • Added a test to ensure that the default app will automatically call despawn_unused_registered_systems

Future work

@ItsDoot ItsDoot added A-ECS Entities, components, systems, and events C-Performance A change motivated by improving speed, memory usage or compile times D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels May 7, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS May 7, 2026
@ItsDoot
ItsDoot requested a review from chescock May 7, 2026 03:41

@chescock chescock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good! I left some comments, but they're just style nits.

Comment thread crates/bevy_ecs/Cargo.toml
Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
I: SystemInput + 'static,
O: 'static,
{
let id = id.into();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to worry about this monomorphizing two versions of this function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe! Would be good to check how much of a difference the codegen is

Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
ItsDoot and others added 3 commits May 9, 2026 02:15
Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
}

// A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters,
// and so that the handle can be hashed based on its entity not its handle type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a typo? (maybe there is not, english is my second language)
// and so that the handle can be hashed based on its entity {instead of, and not} its handle type

@mockersf mockersf May 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it would work with a "," before "not"

@ItsDoot ItsDoot 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 May 17, 2026
@mockersf
mockersf added this pull request to the merge queue May 22, 2026
@mockersf
mockersf removed this pull request from the merge queue due to a manual request May 22, 2026
Comment thread crates/bevy_ecs/src/system/system_registry.rs Outdated
@mockersf
mockersf enabled auto-merge May 22, 2026 10:40
@mockersf
mockersf added this pull request to the merge queue May 22, 2026
Merged via the queue into bevyengine:main with commit b5f5a42 May 22, 2026
38 checks passed
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in ECS May 22, 2026
@alice-i-cecile alice-i-cecile added this to the 0.19 milestone Jun 1, 2026
}

/// Registers a system and returns a tracked [`SystemHandle`] so it can later
/// be called by [`World::run_system`]. The system entity will be automatically

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I didn't see a conversion defined for from SystemHandle into SystemId. Maybe From<&SystemHandle> is needed, too.

And I think Command::run_system* should also accept a SystemHandle.

ickshonpe pushed a commit to ickshonpe/bevy that referenced this pull request Jun 7, 2026
# Objective

- The `SystemHandle` was introduced in bevyengine#24165. But it didn't come with
full `run_system` support.

## Solution

- Implement `From<SystemHandle<I, O>>` and `From<&SystemHandle<I, O>>`
for `SystemId<I, O>` to make it available for `run_system`. Using
`SystemHandle::entity()` and `SystemId::from_entity()`.
- Let `Commands::run_system*` and `commands::command::run_system*` to
accept `impl Into<SystemId>`

## Testing

- Add a test
`system::system_registry::tests::run_system_with_owned_system_handle`,
to run a `SystemHandle`.

---

## Showcase

```rust
world.run_system(handle)

// or borrowing from a reference
world.run_system(&handle)
```
mockersf added a commit that referenced this pull request Jun 10, 2026
# Objective

#24087 introduces scene templating for `SystemId`s, however it can
result in a memory leak if a scene is re-constructed multiple times:

#24087 (comment)
> This was proposed basically 1:1 in #24026 (this was later changed
though). The issue is that it's unclear who owns these systems, that is
who is responsible for unregistering them once they are no longer
needed. Given that recreating the template will spawn the system again
this basically becomes a memory leak.

#24087 (comment)
> > Hm, are you sure this is the case even tho in `build_template` it
only registers the system the first time its called, switching over to
storing the SystemId after the first call?
>
> If you recreate the template (e.g. you call `my_scene()` again) then
you will create a new instance of the system. And since the system is
not scoped to the scene once the scene is despawned the system entity
will be leaked.

Essentially, we need a way to connect the lifetime of the registered
system to the lifetime of the scene.

## Solution

This is a purely additive / opt-in / backwards-compatible version of
#24114

Introducing: `SystemHandle`s

```rust
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>),
    /// A weak handle does not keep the system entity alive.
    Weak(SystemId<I, O>),
}

pub struct StrongSystemHandle {
    entity: Entity,
    drop_queue: Arc<ConcurrentQueue<<Entity>>,
}
```

Similar to `bevy_asset::Handle`s,`SystemHandle`'s custom `Drop`
implementation enqueues the registered system entity into a concurrent
queue. The system `despawn_unused_registered_systems` pulls from the
other end of this queue and despawns the registered system entities.

`World::register_tracked_system` and
`World::register_tracked_boxed_system` are the only functions that
return `SystemHandle`s.

## Testing

- Added a test to ensure that `despawn_unused_registered_systems` does
its job
- Added a test to ensure that the default app will automatically call
`despawn_unused_registered_systems`

## Future work

- #24087 will use this PR as a base

---------

Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
mockersf pushed a commit that referenced this pull request Jun 10, 2026
# Objective

- The `SystemHandle` was introduced in #24165. But it didn't come with
full `run_system` support.

## Solution

- Implement `From<SystemHandle<I, O>>` and `From<&SystemHandle<I, O>>`
for `SystemId<I, O>` to make it available for `run_system`. Using
`SystemHandle::entity()` and `SystemId::from_entity()`.
- Let `Commands::run_system*` and `commands::command::run_system*` to
accept `impl Into<SystemId>`

## Testing

- Add a test
`system::system_registry::tests::run_system_with_owned_system_handle`,
to run a `SystemHandle`.

---

## Showcase

```rust
world.run_system(handle)

// or borrowing from a reference
world.run_system(&handle)
```
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-Performance A change motivated by improving speed, memory usage or compile times D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants