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

Query by Indexed Component Value #17608

Open
wants to merge 73 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
001b227
Initial Commit
bushrat011899 Jan 30, 2025
44408ba
Update Examples
bushrat011899 Jan 30, 2025
df42b47
Address CI
bushrat011899 Jan 30, 2025
8f6069a
Create system param and expose via `App`
bushrat011899 Jan 30, 2025
9c66cd2
CI
bushrat011899 Jan 30, 2025
5b10abb
Docs
bushrat011899 Jan 30, 2025
ef82a3d
Rename `IndexComponent`
bushrat011899 Jan 30, 2025
e372f5d
Update documentation for `IndexableComponent`
bushrat011899 Jan 30, 2025
908fd04
Added documentation to `Index`
bushrat011899 Jan 30, 2025
88c9b3b
Document manual `Default` implementation
bushrat011899 Jan 30, 2025
6fbc6c3
Just `unwrap`
bushrat011899 Jan 30, 2025
14e189e
Change from `TODO` to `PERF`
bushrat011899 Jan 30, 2025
44c2a3d
Add doc-test to `QueryByIndex`
bushrat011899 Jan 30, 2025
1778616
Add documentation and tests to the `index` module
bushrat011899 Jan 30, 2025
175c6dc
Update example description
bushrat011899 Jan 30, 2025
272c979
Switch to `i8` in example
bushrat011899 Jan 30, 2025
3f0c733
Improve documentation in `index` example
bushrat011899 Jan 30, 2025
6b2fe6e
Fix CI
bushrat011899 Jan 30, 2025
1d502e0
CI
bushrat011899 Jan 30, 2025
b18de48
Added benchmarks for iterating the index and updating it.
bushrat011899 Jan 30, 2025
beed9bb
CI
bushrat011899 Jan 30, 2025
a5e189b
Switch to cloning value in `on_replace`
bushrat011899 Jan 30, 2025
ec64913
Switch to internal liveness count instead of relying on querying
bushrat011899 Jan 30, 2025
9171430
Ensure initial state valid
bushrat011899 Jan 30, 2025
c5dac6f
CI
bushrat011899 Jan 30, 2025
776e0da
Refactor to separate concerns
bushrat011899 Jan 30, 2025
ea98ec8
Use component ID combinations instead of particular IDs
bushrat011899 Jan 31, 2025
4e5a6e0
Preallocate component IDs and increase the scale of the benchmark
bushrat011899 Jan 31, 2025
4e4e190
Refactor
bushrat011899 Jan 31, 2025
6b03687
Experiment: Maybe safe now?
bushrat011899 Jan 31, 2025
bdfd4ab
Formatting
bushrat011899 Jan 31, 2025
afa0441
Handle no-value-indexed
bushrat011899 Jan 31, 2025
4b158a0
Fix docs
bushrat011899 Jan 31, 2025
e758a62
Rewrite `index` example as Conway's Game of Life
bushrat011899 Jan 31, 2025
4d8f503
Tidy `QueryBuilder` usage
bushrat011899 Jan 31, 2025
69ab442
Change index marker name to be more descriptive
bushrat011899 Jan 31, 2025
b362d2d
Fix documentation and insure `mapping` is updated correctly
bushrat011899 Jan 31, 2025
32884e1
Switch to a list of spare slots for O(1) lookup
bushrat011899 Jan 31, 2025
2266934
Added automatic setup of `index` for the user and a `warn!` that they…
bushrat011899 Jan 31, 2025
5a1428b
Dumb import issue
bushrat011899 Jan 31, 2025
e2508dc
Remove redundant `.take(...)`
bushrat011899 Feb 1, 2025
64df47a
Fix CI
bushrat011899 Feb 1, 2025
5d67b38
For real this time
bushrat011899 Feb 1, 2025
7352492
Fix example
bushrat011899 Feb 1, 2025
94567fc
Refactor and mild performance improvement
bushrat011899 Feb 1, 2025
a5b1df2
Clippy
bushrat011899 Feb 1, 2025
de65088
Implement `Clone` for `QueryState`
bushrat011899 Feb 1, 2025
b1a322d
Clippy
bushrat011899 Feb 1, 2025
a024de0
Typo
bushrat011899 Feb 2, 2025
8a28afb
Better documentation in example
bushrat011899 Feb 2, 2025
400412f
Allow example on WASM
bushrat011899 Feb 2, 2025
d886a82
Refactor to expose index settings and custom storage backends.
bushrat011899 Feb 3, 2025
adf1132
Remove redundant transformations
bushrat011899 Feb 3, 2025
5d6606e
Remove redundant documentation
bushrat011899 Feb 3, 2025
afb3ad2
Merge remote-tracking branch 'upstream/main' into IndexComponents
bushrat011899 Feb 5, 2025
a003f40
Remove `add_index` and replace it with `add_index_with_options`
bushrat011899 Feb 5, 2025
7e6c1f9
Switch to `u8` for `address_space`
bushrat011899 Feb 5, 2025
beecc59
`fmt`
bushrat011899 Feb 5, 2025
8c2538e
Clarified `with(out)_states` documentation
bushrat011899 Feb 5, 2025
2f64169
Add some documentation to `QueryByIndexState`
bushrat011899 Feb 5, 2025
8f92f3b
Update `index` example documentation
bushrat011899 Feb 5, 2025
a5160a4
Remove redundant bounds
bushrat011899 Feb 5, 2025
4264e90
Better `address_space` documentation
bushrat011899 Feb 5, 2025
62f5fdb
Better explained when `SparseSet` may be more appropriate.
bushrat011899 Feb 5, 2025
733fdd3
Improved safety documentation for `QueryLens`
bushrat011899 Feb 5, 2025
514aadf
Document possibility of memory-leak-like behaviour when indexing freq…
bushrat011899 Feb 6, 2025
c54bec9
Docs
bushrat011899 Feb 6, 2025
70dd03a
Switch default `StorageType` to `SparseSet`
bushrat011899 Feb 7, 2025
21f6b40
Fix address-space-override
bushrat011899 Feb 7, 2025
cfcb2ae
Merge remote-tracking branch 'upstream/main' into IndexComponents
bushrat011899 Feb 7, 2025
d7fe564
Formatting
bushrat011899 Feb 7, 2025
37dfdaa
Fix from upstreaming
bushrat011899 Feb 7, 2025
6eed8b7
Fix default `address_space` value
bushrat011899 Feb 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2040,6 +2040,18 @@ description = "Demonstrates the creation and utility of immutable components"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "index"
path = "examples/ecs/index.rs"
doc-scrape-examples = true
required-features = ["bevy_dev_tools"]

[package.metadata.example.index]
name = "Indexes"
description = "Demonstrates querying by component value using indexing"
category = "ECS (Entity Component System)"
wasm = false
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved

[[example]]
name = "iter_combinations"
path = "examples/ecs/iter_combinations.rs"
Expand Down
38 changes: 38 additions & 0 deletions benches/benches/bevy_ecs/index/index_iter_indexed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use bevy_ecs::{prelude::*, system::SystemId};
use core::hint::black_box;
use glam::*;

const PLANETS: u16 = 1_000;
const SPAWNS: usize = 1_000_000;

#[derive(Component, Copy, Clone, PartialEq, Eq, Hash)]
#[component(immutable)]
struct Planet(u16);

fn find_planet_zeroes_indexed(query: QueryByIndex<Planet, &Planet>) {
let mut query = query.at(&Planet(0));
for planet in query.query().iter() {
let _ = black_box(planet);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let _ = black_box(planet);
black_box(planet);

(Nit) The let _ = isn't necessary, since black_box() forces the expression it is passed to be evaluated.

In practice, black_box serves two purposes:

  1. It prevents the compiler from making optimizations related to the value returned by black_box
  2. It forces the value passed to black_box to be calculated, even if the return value of black_box is unused

black_box() - How to use this

}
}

pub struct Benchmark(World, SystemId);

impl Benchmark {
pub fn new() -> Self {
let mut world = World::new();

world.add_index::<Planet>();

world.spawn_batch((0..PLANETS).map(Planet).cycle().take(SPAWNS));

let id = world.register_system(find_planet_zeroes_indexed);

Self(world, id)
}

#[inline(never)]
pub fn run(&mut self) {
let _ = self.0.run_system(self.1);
}
}
35 changes: 35 additions & 0 deletions benches/benches/bevy_ecs/index/index_iter_naive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use bevy_ecs::{prelude::*, system::SystemId};
use core::hint::black_box;
use glam::*;

const PLANETS: u16 = 1_000;
const SPAWNS: usize = 1_000_000;

#[derive(Component, Copy, Clone, PartialEq, Eq, Hash)]
#[component(immutable)]
struct Planet(u16);

fn find_planet_zeroes_naive(query: Query<&Planet>) {
for planet in query.iter().filter(|&&planet| planet == Planet(0)) {
let _ = black_box(planet);
}
}

pub struct Benchmark(World, SystemId);

impl Benchmark {
pub fn new() -> Self {
let mut world = World::new();

world.spawn_batch((0..PLANETS).map(Planet).cycle().take(SPAWNS));

let id = world.register_system(find_planet_zeroes_naive);

Self(world, id)
}

#[inline(never)]
Copy link
Member

Choose a reason for hiding this comment

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

Why did you choose to add #[inline(never)]? It seems counter-intuitive to me, since we're just trying to benchmark the contained run_system().

pub fn run(&mut self) {
let _ = self.0.run_system(self.1);
}
}
47 changes: 47 additions & 0 deletions benches/benches/bevy_ecs/index/index_update_indexed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use bevy_ecs::{prelude::*, system::SystemId};
use glam::*;

const PLANETS: u8 = 16;
const SPAWNS: usize = 10_000;

#[derive(Component, Copy, Clone, PartialEq, Eq, Hash)]
#[component(immutable)]
struct Planet(u8);

fn increment_planet_zeroes_indexed(
query: QueryByIndex<Planet, (Entity, &Planet)>,
mut local: Local<u8>,
mut commands: Commands,
) {
let target = Planet(*local);
let next_planet = Planet(target.0 + 1);

let mut query = query.at(&target);
for (entity, _planet) in query.query().iter() {
commands.entity(entity).insert(next_planet);
}

*local += 1;
}

pub struct Benchmark(World, SystemId);

impl Benchmark {
pub fn new() -> Self {
let mut world = World::new();

world.add_index::<Planet>();

world.spawn_batch((0..PLANETS).map(Planet).cycle().take(SPAWNS));

let id = world.register_system(increment_planet_zeroes_indexed);

Self(world, id)
}

#[inline(never)]
pub fn run(&mut self) {
let _ = self.0.run_system(self.1);
self.0.flush();
}
}
44 changes: 44 additions & 0 deletions benches/benches/bevy_ecs/index/index_update_naive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use bevy_ecs::{prelude::*, system::SystemId};
use glam::*;

const PLANETS: u8 = 16;
const SPAWNS: usize = 10_000;

#[derive(Component, Copy, Clone, PartialEq, Eq, Hash)]
#[component(immutable)]
struct Planet(u8);

fn increment_planet_zeroes_naive(
query: Query<(Entity, &Planet)>,
mut local: Local<u8>,
mut commands: Commands,
) {
let target = Planet(*local);
let next_planet = Planet(target.0 + 1);

for (entity, _planet) in query.iter().filter(|(_, planet)| **planet == target) {
commands.entity(entity).insert(next_planet);
}

*local += 1;
}

pub struct Benchmark(World, SystemId);

impl Benchmark {
pub fn new() -> Self {
let mut world = World::new();

world.spawn_batch((0..PLANETS).map(Planet).cycle().take(SPAWNS));

let id = world.register_system(increment_planet_zeroes_naive);

Self(world, id)
}

#[inline(never)]
pub fn run(&mut self) {
let _ = self.0.run_system(self.1);
self.0.flush();
}
}
38 changes: 38 additions & 0 deletions benches/benches/bevy_ecs/index/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
mod index_iter_indexed;
mod index_iter_naive;
mod index_update_indexed;
mod index_update_naive;

use criterion::{criterion_group, Criterion};

criterion_group!(benches, index_iter, index_update,);

fn index_iter(c: &mut Criterion) {
let mut group = c.benchmark_group("index_iter");
group.warm_up_time(core::time::Duration::from_millis(500));
group.measurement_time(core::time::Duration::from_secs(4));
Comment on lines +12 to +13
Copy link
Member

Choose a reason for hiding this comment

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

Is there a specific reason you chose these times, which are smaller than the default? criterion by default has a warm-up time of 3 seconds and a measurements time of 5 seconds.

group.bench_function("naive", |b| {
let mut bench = index_iter_naive::Benchmark::new();
b.iter(move || bench.run());
});
group.bench_function("indexed", |b| {
let mut bench = index_iter_indexed::Benchmark::new();
b.iter(move || bench.run());
});
group.finish();
}

fn index_update(c: &mut Criterion) {
let mut group = c.benchmark_group("index_update");
group.warm_up_time(core::time::Duration::from_millis(500));
group.measurement_time(core::time::Duration::from_secs(4));
group.bench_function("naive", |b| {
let mut bench = index_update_naive::Benchmark::new();
b.iter(move || bench.run());
});
group.bench_function("indexed", |b| {
let mut bench = index_update_indexed::Benchmark::new();
b.iter(move || bench.run());
});
group.finish();
}
2 changes: 2 additions & 0 deletions benches/benches/bevy_ecs/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod empty_archetypes;
mod entity_cloning;
mod events;
mod fragmentation;
mod index;
mod iteration;
mod observers;
mod param;
Expand All @@ -23,6 +24,7 @@ criterion_main!(
empty_archetypes::benches,
entity_cloning::benches,
events::benches,
index::benches,
iteration::benches,
fragmentation::benches,
observers::benches,
Expand Down
38 changes: 38 additions & 0 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub use bevy_derive::AppLabel;
use bevy_ecs::{
component::RequiredComponentsError,
event::{event_update_system, EventCursor},
index::IndexableComponent,
intern::Interned,
prelude::*,
schedule::{ScheduleBuildSettings, ScheduleLabel},
Expand Down Expand Up @@ -1324,6 +1325,43 @@ impl App {
self.world_mut().add_observer(observer);
self
}

/// Creates and maintains an index for the provided immutable [`Component`] `C` using observers.
///
/// This allows querying by component _value_ to be very performant, at the expense of a small
/// amount of overhead when modifying the indexed component.
///
/// See [`IndexableComponent`] for further details.
///
/// # Examples
///
/// ```rust
/// # use bevy_app::prelude::*;
/// # use bevy_ecs::prelude::*;
/// #
/// # let mut app = App::new();
/// #[derive(Component, PartialEq, Eq, Hash, Clone)]
/// #[component(immutable)]
/// enum FavoriteColor {
/// Red,
/// Green,
/// Blue,
/// }
///
/// app.add_index::<FavoriteColor>();
///
/// fn find_red_fans(mut query: QueryByIndex<FavoriteColor, Entity>) {
/// let mut lens = query.at(&FavoriteColor::Red);
/// for entity in lens.query().iter() {
/// println!("{entity:?} likes the color Red!");
/// }
/// }
/// # app.add_systems(Update, find_red_fans);
/// ```
pub fn add_index<C: IndexableComponent>(&mut self) -> &mut Self {
self.world_mut().add_index::<C>();
self
}
}

type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;
Expand Down
Loading