Track source location in change detection
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.
Testing
- The
component_change_detectionexample now shows where the component was mutated:
2024-06-26T06:56:42.980958Z INFO component_change_detection: Entity { index: 1, generation: 1 } changed: Ref(MyComponent(2.0)) from: "examples/ecs/component_change_detection.rs:34:23"
Changelog
- Added source locations to ECS change detection.
Migration Guide
- Added
callerfield to many internal ECS functions used with change detection. Use Location::caller() to provide the source of the function call.
Mut is one of the hottest paths in Bevy. I'm concerned about the performance implications of introducing anything (even the cheapest ops) to it. I'd like to either see compelling evidence that this won't meaningfully affect performance, especially given that this is a "debugging tool". I'd feel much more comfortable if this was behind a feature flag.
I really like this feature and think it'll be very useful, but I'll second the request for putting it behind an off-by-default feature flag.
Yup, I assumed as much. I was hoping to get a read on whether this is even an acceptable approach to take for the feature. Before going down the path of adding feature flags.
Is there a reason you're de referencing the static reference you get from Location::caller? Couldn't you just pass the reference around?
Is there a reason you're de referencing the static reference you get from
Location::caller? Couldn't you just pass the reference around?
Location is two u32s and one &str, making it 16 bytes on 64-bit platforms. Storing a reference instead would bring it down to 8 bytes, which might have some mild performance gains because it would fit in a register.
Furthermore, why do you use core::panic::Location as compared with the std variant?
- static reference: I started with this, but was having trouble with the deref making it hard to update the field, so I switched to an owned value to get it working. I agree it should be fixed if this is going to be merged.
- using core: because it can't hurt? I wasn't sure if bevy_ecs was ever intended to be used in a no_std context, and figured it would be safer to use core for a type so ubiquitous.
The answer to most questions is probably going to be "because this is a proof of concept". 😄 I wasn't sure if this was going to be possible or if it would return any useful information, so I'm mostly just happy to see that it's doable. The useful findings from this are:
- It's possible to get line and column level tracing for change detection, which is super useful
- Changes from commands completely break this approach because commands are deferred, and the call stack can't be traced to the calling location by the time the command is actually executed.
For 2, it seems like it should be possible to get information from tracing to figure out what system the commands are being executed for. Not quite sure how to do that though, might need to replace Location with a new struct that also contains the Location as well as the current system span.
2. Changes from commands completely break this approach because commands are deferred, and the call stack can't be traced to the calling location by the time the command is actually executed.
Is it possible to find the Location in Commands::insert, then pass that forward to the actual command?
using core: because it can't hurt? I wasn't sure if bevy_ecs was ever intended to be used in a no_std context, and figured it would be safer to use core for a type so ubiquitous.
I would like this, one day :) Probably core + alloc, but still! Not a priority, but might as well do it when there's no cost to it.
Is it possible to find the Location in Commands::insert, then pass that forward to the actual command?
Yep, that would look like this (you'd also need to do the same for init_resource, insert_resource, spawn_batch etc).
Good call! That looks pretty straightforward.
Spent far to long trying to figure out why a camera transform from glb was getting reset, was correct on spawn, next frame its Identity, this found the issue in seconds. Was calling remove_parent_in_place before TransformPropagate had ran. Huge help.
This is close to finished, it just needs a system to track the caller from Commands. That's past my capabilities, but hopefully someone else is up for the task!
I've updated this to now work with commands, as well as a pass on naming, and updated the change detection example to more effectively teach these concepts.
Thanks @BD103 for the assist!
I like this and I want it. Should
Commands::addandCommand::pushbetrack_callerso that custom user commands can provide call tracing? Or would that be something user should do on the methods of their extension trait.
If we do want to support custom commands like this, it would need more documentation. I feel like this is probably follow-up PR territory.
follow-up PR territory.
Fair enough, agreed.
Is there a reason you're de referencing the static reference you get from
Location::caller? Couldn't you just pass the reference around?
Locationis twou32s and one&str, making it 16 bytes on 64-bit platforms. Storing a reference instead would bring it down to 8 bytes, which might have some mild performance gains because it would fit in a register.
at the risk of sounding incredibly pedantic: 24 bytes. &str is the pointer and the length, so it is already 16 bytes.
remember that this is a pretty magical struct that gets willed into existence by the compiler during codegen. I kinda expect it to have somewhat nonintuitive codegen properties because inspecting Location::caller() kinda... creates the thing as much as it returns a reference.
Should Commands::add and Command::push be track_caller so that custom user commands can provide call tracing?
At best that would mean storing the caller location and changing Commands::apply to take a &Location, but custom commands can already just store the location themselves in their constructor.
In either case the problem for custom commands is that insert_with_caller & friends aren't pub. So the actual question is: Do we want to make them pub?
insert_with_caller & friends aren't pub. So the actual question is: Do we want to make them pub?
I think so. This will be needed for custom impls, as you said. We can document the context around this on the with_caller methods. I wish there was a way to make this pattern nicer, but alas.
at the risk of sounding incredibly pedantic: 24 bytes.
&stris the pointer and the length, so it is already 16 bytes.
Hah, I forgot about wide pointers. Thanks!
Thank you to everyone involved with the authoring or reviewing of this PR! This work is relatively important and needs release notes! Head over to https://github.com/bevyengine/bevy-website/issues/1666 if you'd like to help out.