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

[Merged by Bors] - drop overwritten component data on double insert #2227

Closed
wants to merge 5 commits into from

Conversation

Frizi
Copy link
Contributor

@Frizi Frizi commented May 21, 2021

Continuing the work on reducing the safety footguns in the code, I've removed one extra UnsafeCell in favour of safe Cell usage inisde ComponentTicks. That change led to discovery of misbehaving component insert logic, where data wasn't properly dropped when overwritten. Apart from that being fixed, some method names were changed to better convey the "initialize new allocation" and "replace existing allocation" semantic.

Depends on #2221, I will rebase this PR after the dependency is merged. For now, review just the last commit.

@Frizi Frizi force-pushed the reinsert-drop branch 2 times, most recently from 9667281 to 79fb468 Compare May 21, 2021 00:38
Comment on lines +1199 to +1210
#[test]
fn insert_overwrite_drop() {
let (dropck1, dropped1) = DropCk::new_pair();
let (dropck2, dropped2) = DropCk::new_pair();
let mut world = World::default();
world.spawn().insert(dropck1).insert(dropck2);
assert_eq!(dropped1.load(Ordering::Relaxed), 1);
assert_eq!(dropped2.load(Ordering::Relaxed), 0);
drop(world);
assert_eq!(dropped1.load(Ordering::Relaxed), 1);
assert_eq!(dropped2.load(Ordering::Relaxed), 1);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here is the scenario that previously never dropped the first inserted component.

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-ECS Entities, components, systems, and events labels May 21, 2021
@bjorn3
Copy link
Contributor

bjorn3 commented May 21, 2021

Nice reduction of UnsafeCell usage!

@Frizi Frizi force-pushed the reinsert-drop branch 2 times, most recently from d1e84fd to 547fa26 Compare May 21, 2021 10:16
@NathanSWard
Copy link
Contributor

Just as a note, this will probably start to fail once #2217 gets merged (or the other way around) since ComponentTicks are no longer Copy.

pub struct ComponentTicks {
pub(crate) added: u32,
pub(crate) changed: u32,
pub(crate) changed: Cell<u32>,
Copy link
Member

Choose a reason for hiding this comment

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

I think this is actually counterintuitively a reduction in safety. Cell is !Sync, and calling set_changed() from multiple threads at the same time violates that constraint. On main we only provide mutable access to ComponentTicks when the caller has unique access to the corresponding component across all threads, so the old UnsafeCell wrapper doesn't allow invalid access in userspace. But now callers without unique access can call set_changed, which allows cross thread mutation of Cell.

Copy link
Member

@cart cart May 25, 2021

Choose a reason for hiding this comment

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

I think technically userspace is still "safe" right now with this pr because there are no public interfaces that expose &ComponentTicks directly (ex: Res<T> has an internal &ComponentTicks reference). But imo this change makes it much easier to accidentally expose unsafe behavior. Its tempting to look at Res<T> and think "we should expose set_changed() here ... its a safe api so we're fine".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see. I somehow ignored the part where things get sent across threads, and compiler let me do it because of existing pointer casts. I'll try to work around that and get rid of Cell. I still think I can end up with better placement for UnsafeCell, as I believe having it over the whole Vec wasn't correct. Safe alternative would be to use an atomic in place of cell, but It's likely to impact performance, so I'm not doing it right now.

I think at some point we would also think about using GhostCell once its implementation matures. I believe we would be able to remove a lot of unsafety that way.

Copy link
Member

Choose a reason for hiding this comment

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

Cool cool. Definitely happy to consider alternatives. For now, can you revert to the current UnsafeCell impl so we don't need to block the drop fixes on UnsafeCell changes (which appear to be orthogonal).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't want to revert it outright, as I believe a lot of those changes are still valid and benefitial. I will make it more similar to previous implementation though and will keep UnsafeCell around.

One reason is that I believe this line inside get_with_ticks is a sneaky UB, and I want to get rid of it

let ticks = &mut *self.ticks.get();

Because even though we only really use that reference in order to get the ticks at specific index, we do create a unique reference to the vector while possibly holding unique references to its parts (some ticks at other indices). It's totally possible to reallocate that vec by accident through shared reference and invalidate all the live ticks references, which should not ever be possible.

For that reason, I want to use Vec<UnsafeCell<ComponentTicks>> instead.

@Frizi
Copy link
Contributor Author

Frizi commented May 25, 2021

Okay, I believe it should be correct now. No more Cell in threaded context. There is actually a bit more of UnsafeCell around the code now, but an UB of accessing the aliased vector through unique reference got removed.

@@ -386,7 +387,7 @@ impl<T: Component> WorldQuery for &mut T {
pub struct WriteFetch<T> {
storage_type: StorageType,
table_components: NonNull<T>,
table_ticks: *mut ComponentTicks,
table_ticks: *const UnsafeCell<ComponentTicks>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Pointers are allowed to alias, so this UnsafeCell is not necessary. You can store the result of UnsafeCell::get here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can't do that without creating a reference to the wrong element. This pointer is being offset before reading, thus it represents a reference to the whole vec slice. I can't get rid of that UnsafeCell and cast that to *mut at the same time. The only correct way to do that right now requires a &UnsafeCell<ComponentTicks> to exist first. This might be changed once UnsafeCell::raw_get gets stabilised.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see.

@@ -34,7 +34,7 @@ impl TableId {
pub struct Column {
pub(crate) component_id: ComponentId,
pub(crate) data: BlobVec,
pub(crate) ticks: UnsafeCell<Vec<ComponentTicks>>,
pub(crate) ticks: Vec<UnsafeCell<ComponentTicks>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@cart
Copy link
Member

cart commented May 30, 2021

Alrighty it hurts, but I've resolved to merge this despite the additional "for loop" query iteration perf regression (in addition to the previous pr's regression). Correctness needs to come first and we can hopefully reclaim the perf when we move to static component storage.

@cart
Copy link
Member

cart commented May 30, 2021

bors r+

bors bot pushed a commit that referenced this pull request May 30, 2021
Continuing the work on reducing the safety footguns in the code, I've removed one extra `UnsafeCell` in favour of safe `Cell` usage inisde `ComponentTicks`. That change led to discovery of misbehaving component insert logic, where data wasn't properly dropped when overwritten. Apart from that being fixed, some method names were changed to better convey the "initialize new allocation" and "replace existing allocation" semantic.

Depends on #2221, I will rebase this PR after the dependency is merged. For now, review just the last commit.

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
@bors bors bot changed the title drop overwritten component data on double insert [Merged by Bors] - drop overwritten component data on double insert May 30, 2021
@bors bors bot closed this May 30, 2021
ostwilkens pushed a commit to ostwilkens/bevy that referenced this pull request Jul 27, 2021
Continuing the work on reducing the safety footguns in the code, I've removed one extra `UnsafeCell` in favour of safe `Cell` usage inisde `ComponentTicks`. That change led to discovery of misbehaving component insert logic, where data wasn't properly dropped when overwritten. Apart from that being fixed, some method names were changed to better convey the "initialize new allocation" and "replace existing allocation" semantic.

Depends on bevyengine#2221, I will rebase this PR after the dependency is merged. For now, review just the last commit.

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
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-Bug An unexpected or incorrect behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants