Skip to content

Commit

Permalink
Track source location in change detection (bevyengine#14034)
Browse files Browse the repository at this point in the history
# Objective

- Make it possible to know *what* changed your component or resource.
- Common need when debugging, when you want to know the last code
location that mutated a value in the ECS.
- This feature would be very useful for the editor alongside system
stepping.

## Solution

- Adds the caller location to column data.
- Mutations now `track_caller` all the way up to the public API.
- Commands that invoke these functions immediately call
`Location::caller`, and pass this into the functions, instead of the
functions themselves attempting to get the caller. This would not work
for commands which are deferred, as the commands are executed by the
scheduler, not the user's code.

## Testing

- The `component_change_detection` example now shows where the component
was mutated:

```
2024-07-28T06:57:48.946022Z  INFO component_change_detection: Entity { index: 1, generation: 1 }: New value: MyComponent(0.0)
2024-07-28T06:57:49.004371Z  INFO component_change_detection: Entity { index: 1, generation: 1 }: New value: MyComponent(1.0)
2024-07-28T06:57:49.012738Z  WARN component_change_detection: Change detected!
        -> value: Ref(MyComponent(1.0))
        -> added: false
        -> changed: true
        -> changed by: examples/ecs/component_change_detection.rs:36:23
```

- It's also possible to inspect change location from a debugger:
<img width="608" alt="image"
src="https://github.com/user-attachments/assets/c90ecc7a-0462-457a-80ae-42e7f5d346b4">


---

## Changelog

- Added source locations to ECS change detection behind the
`track_change_detection` flag.

## Migration Guide

- Added `changed_by` field to many internal ECS functions used with
change detection when the `track_change_detection` feature flag is
enabled. Use Location::caller() to provide the source of the function
call.

---------

Co-authored-by: BD103 <[email protected]>
Co-authored-by: Gino Valente <[email protected]>
  • Loading branch information
3 people authored Jul 30, 2024
1 parent adb4709 commit 9575b20
Show file tree
Hide file tree
Showing 21 changed files with 957 additions and 164 deletions.
14 changes: 9 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@ ios_simulator = ["bevy_internal/ios_simulator"]
# Enable built in global state machines
bevy_state = ["bevy_internal/bevy_state"]

# Enables source location tracking for change detection, which can assist with debugging
track_change_detection = ["bevy_internal/track_change_detection"]

# Enable function reflection
reflect_functions = ["bevy_internal/reflect_functions"]

Expand Down Expand Up @@ -1611,13 +1614,14 @@ category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "component_change_detection"
path = "examples/ecs/component_change_detection.rs"
name = "change_detection"
path = "examples/ecs/change_detection.rs"
doc-scrape-examples = true
required-features = ["track_change_detection"]

[package.metadata.example.component_change_detection]
name = "Component Change Detection"
description = "Change detection on components"
[package.metadata.example.change_detection]
name = "Change Detection"
description = "Change detection on components and resources"
category = "ECS (Entity Component System)"
wasm = false

Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ categories = ["game-engines", "data-structures"]
rust-version = "1.77.0"

[features]
default = ["bevy_reflect"]
trace = []
multi_threaded = ["bevy_tasks/multi_threaded", "arrayvec"]
bevy_debug_stepping = []
default = ["bevy_reflect"]
serialize = ["dep:serde"]
track_change_detection = []

[dependencies]
bevy_ptr = { path = "../bevy_ptr", version = "0.15.0-dev" }
Expand Down
50 changes: 45 additions & 5 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ use crate::{

use bevy_ptr::{ConstNonNull, OwningPtr};
use bevy_utils::{all_tuples, HashMap, HashSet, TypeIdMap};
#[cfg(feature = "track_change_detection")]
use std::panic::Location;
use std::ptr::NonNull;

/// The `Bundle` trait enables insertion and removal of [`Component`]s from an entity.
Expand Down Expand Up @@ -401,6 +403,7 @@ impl BundleInfo {
table_row: TableRow,
change_tick: Tick,
bundle: T,
#[cfg(feature = "track_change_detection")] caller: &'static Location<'static>,
) {
// NOTE: get_components calls this closure on each component in "bundle order".
// bundle_info.component_ids are also in "bundle order"
Expand All @@ -417,10 +420,22 @@ impl BundleInfo {
let status = unsafe { bundle_component_status.get_status(bundle_component) };
match status {
ComponentStatus::Added => {
column.initialize(table_row, component_ptr, change_tick);
column.initialize(
table_row,
component_ptr,
change_tick,
#[cfg(feature = "track_change_detection")]
caller,
);
}
ComponentStatus::Mutated => {
column.replace(table_row, component_ptr, change_tick);
column.replace(
table_row,
component_ptr,
change_tick,
#[cfg(feature = "track_change_detection")]
caller,
);
}
}
}
Expand All @@ -429,7 +444,13 @@ impl BundleInfo {
// SAFETY: If component_id is in self.component_ids, BundleInfo::new requires that
// a sparse set exists for the component.
unsafe { sparse_sets.get_mut(component_id).debug_checked_unwrap() };
sparse_set.insert(entity, component_ptr, change_tick);
sparse_set.insert(
entity,
component_ptr,
change_tick,
#[cfg(feature = "track_change_detection")]
caller,
);
}
}
bundle_component += 1;
Expand Down Expand Up @@ -664,6 +685,7 @@ impl<'w> BundleInserter<'w> {
entity: Entity,
location: EntityLocation,
bundle: T,
#[cfg(feature = "track_change_detection")] caller: &'static core::panic::Location<'static>,
) -> EntityLocation {
let bundle_info = self.bundle_info.as_ref();
let add_bundle = self.add_bundle.as_ref();
Expand Down Expand Up @@ -706,6 +728,8 @@ impl<'w> BundleInserter<'w> {
location.table_row,
self.change_tick,
bundle,
#[cfg(feature = "track_change_detection")]
caller,
);

(archetype, location)
Expand Down Expand Up @@ -744,6 +768,8 @@ impl<'w> BundleInserter<'w> {
result.table_row,
self.change_tick,
bundle,
#[cfg(feature = "track_change_detection")]
caller,
);

(new_archetype, new_location)
Expand Down Expand Up @@ -823,6 +849,8 @@ impl<'w> BundleInserter<'w> {
move_result.new_row,
self.change_tick,
bundle,
#[cfg(feature = "track_change_detection")]
caller,
);

(new_archetype, new_location)
Expand Down Expand Up @@ -919,6 +947,7 @@ impl<'w> BundleSpawner<'w> {
&mut self,
entity: Entity,
bundle: T,
#[cfg(feature = "track_change_detection")] caller: &'static Location<'static>,
) -> EntityLocation {
// SAFETY: We do not make any structural changes to the archetype graph through self.world so these pointers always remain valid
let bundle_info = self.bundle_info.as_ref();
Expand All @@ -941,6 +970,8 @@ impl<'w> BundleSpawner<'w> {
table_row,
self.change_tick,
bundle,
#[cfg(feature = "track_change_detection")]
caller,
);
entities.set(entity.index(), location);
location
Expand Down Expand Up @@ -969,11 +1000,20 @@ impl<'w> BundleSpawner<'w> {
/// # Safety
/// `T` must match this [`BundleInfo`]'s type
#[inline]
pub unsafe fn spawn<T: Bundle>(&mut self, bundle: T) -> Entity {
pub unsafe fn spawn<T: Bundle>(
&mut self,
bundle: T,
#[cfg(feature = "track_change_detection")] caller: &'static Location<'static>,
) -> Entity {
let entity = self.entities().alloc();
// SAFETY: entity is allocated (but non-existent), `T` matches this BundleInfo's type
unsafe {
self.spawn_non_existent(entity, bundle);
self.spawn_non_existent(
entity,
bundle,
#[cfg(feature = "track_change_detection")]
caller,
);
}
entity
}
Expand Down
Loading

0 comments on commit 9575b20

Please sign in to comment.