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

Reflect auto registration #15030

Open
wants to merge 23 commits into
base: main
Choose a base branch
from

Conversation

eugineerd
Copy link

@eugineerd eugineerd commented Sep 3, 2024

Objective

This PR adds automatic registration of non-generic types annotated with #[derive(Reflect)].
Manually registering all reflect types can be error-prone, failing to reflect during runtime if some type is not registered. #5781 fixed this for nested types, improving ergonomics of using reflection dramatically, but top-level types still need to be registered manually.

// from #5781
#[derive(Reflect)]
struct Foo(Bar);

#[derive(Reflect)]
struct Bar(Baz);

#[derive(Reflect)]
struct Baz(usize);

fn main() {
  // ...
  app.register_type::<Foo>() // <- can still forget to add this
  // ...
}

Solution

Automatically register all types that derive Reflect. This works for all non-generic types, allowing users to just add #[derive(Reflect)] to any compatible type and not think about where to register it.

fn main() {
  // No need to manually call .register_type::<Foo>()
  App::new()
    .add_plugins(DefaultPlugins)
    .add_systems(Startup, setup)
    .run();
}

#[derive(Reflect)]
pub struct Foo {
  a: usize,
}

fn setup(type_registry: Res<AppTypeRegistry>) {
  let type_registry = type_registry.read();
  assert!(type_registry.contains(TypeId::of::<Foo>()));
}

Automatic registration can be opted-out of by adding #[reflect(no_auto_register)] reflect attribute to a type:

#[derive(Reflect)]
#[reflect(no_auto_register)]
pub struct Foo {
  a: usize,
}

Registration normally happens at app creation time, but TypeRegistry itself has a new register_derived_types method to register all collected types at any appropriate time:

#[derive(Reflect)]
struct Foo {
  name: Option<String>,
  value: i32
}

let mut type_registry = TypeRegistry::empty();
type_registry.register_derived_types();

assert!(type_registry.contains(TypeId::of::<Foo>()));

All functionality is currently feature-gated behind reflect_auto_register due to reasons explained in considerations, but maybe it should be enabled by default instead?

Testing

  • Tested by running reflect example and removing explicit type registration, both on native and on wasm32-unknown-unknown.
  • Added a doctest to register_derived_types

Impact on startup time is negligible: <40ms on debug wasm build and <25ms on debug native build for 1614 registered types .

Wasm binary size comparison. All sizes are in KB as reported by du on the modified reflection example.

auto register, KiB no auto register, KiB abs diff, KiB % diff
wasm-release && wasm-bindgen 24364 21716 2648 +10.9%
wasm-opt -Oz 14940 13968 972 +6.5%
wasm-opt -Os 16136 14896 1240 +7.7%
wasm-opt -Oz && gzip -c 5184 4944 240 +4.6%

Considerations

While automatic type registration improves ergonomics quite a bit for projects that make heavy use of it, there are also some problems:

  • Generic types can't be automatically registered. However, as long as top-level reflect types don't need generics, recursive registration can take care of the rest.
  • Wasm bundle size increase is not insignificant, and would likely increase even further as more types are added.

Update: This seems to not be as big of a problem as I thought. I tried compiling a file with 1000 reflect types with automatic registration and manual type registration, and after optimizing with wasm-opt -Oz the file with automatic registration was actually smaller than the file using manual registration (14508KB vs. 15580KB, -6.9%). Maybe because automatic registration avoids monomorphization of .register_type?
Update 2: After fixing TypeData registration, wasm file size for auto registration isn't smaller than manual .register_type registration anymore. The difference between manual and automatic registration doesn't grow with the number of registered types at least, as can be seen from the table bellow:

1000 simple structs with reflect wasm size comparison
auto register, KiB no auto register, KiB abs diff, KiB % diff
wasm-release && wasm-bindgen 30596 21644 8952 +29.26%
wasm-release && wasm-bindgen +register_type 30740 27764 2976 +9.68%
wasm-opt -Oz 16588 13928 2660 +16.04%
wasm-opt -Oz +register_type 16652 15580 1072 +6.44%
wasm-release && wasm-bindgen (manual vs auto) 30596 27764 2832 +9.26%
wasm-opt -Oz (manual vs auto) 16588 15580 1008 +6.08%

Update 3: Disabling automatic type registration for engine types and using it only for user-defined types seems to not impact wasm file size as much. I'm not sure which types exactly increase the binary size that much, but it seems to be related to registering TypeData, because skipping it produced a much smaller wasm file. Here is a table of wasm file sizes with automatic type registration for user-defined types only:

1000 simple structs with reflect wasm size comparison, no engine types
auto register, KiB no auto register, KiB abs diff, KiB % diff
wasm-release && wasm-bindgen 27944 21644 6300 +22.55%
wasm-release && wasm-bindgen +register_type 28084 27764 320 +1.14%
wasm-opt -Oz 15612 13928 1684 +10.79%
wasm-opt -Oz +register_type 15676 15580 96 +0.61%
wasm-release && wasm-bindgen (manual vs auto) 27944 27764 180 +0.64%
wasm-opt -Oz (manual vs auto) 15612 15580 32 +0.2%

@alice-i-cecile alice-i-cecile added C-Usability A targeted quality-of-life change that makes Bevy easier to use A-Reflection Runtime information about types X-Controversial There is active debate or serious implications around merging this PR M-Needs-Release-Note Work that should be called out in the blog due to impact labels Sep 3, 2024
@alice-i-cecile alice-i-cecile added the S-Needs-Review Needs reviewer attention (from anyone!) to move forward label Sep 3, 2024
@alice-i-cecile
Copy link
Member

Wasm bundle size increase is not insignificant

Is this compared to the equivalent manual registration? Can / should we feature flag this for authors who particularly care?

Copy link
Contributor

github-actions bot commented Sep 3, 2024

You added a new feature but didn't update the readme. Please run cargo run -p build-templated-pages -- update features to update it, and commit the file change.

@eugineerd
Copy link
Author

Is this compared to the equivalent manual registration

As far as I understand it, wasm-init works by exporting a bunch of symbols to final wasm and calling them from outside when wasm_init() is called for the first time. I think most of the size increase is due to these exports, but I'm not sure. In the table I just built the modified registration example and compared the .wasm file sizes using du. As the number of registered types from type_registry.iter() differs (445 without, 614 with), it is not the same as manually registering types.

@eugineerd
Copy link
Author

eugineerd commented Sep 4, 2024

Managed to reduce wasm size considerably by removing closures from automatic type registration:

All sizes are in KB as reported by du on reflection example.

wasm-release && wasm-bindgen wasm-opt -Oz wasm-opt -Os wasm-opt -Oz && gzip -c
+reflect_auto_register 22560 14332 15352 5044
default 21716 13968 14896 4944
diff +3.7% +2.5% +3% +2%

I'm not very familiar with all the internal reflection machinery, maybe this can be optimized even further?

Update: Disregard that, I misunderstood how reflection works and missed registering type data.

Copy link
Contributor

@SkiFire13 SkiFire13 left a comment

Choose a reason for hiding this comment

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

Overall the implementation looks good to me, though I guess the PR might be controversial due to e.g. maybe missing support for consoles which cart mentioned.

crates/bevy_reflect/Cargo.toml Outdated Show resolved Hide resolved
crates/bevy_reflect/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_reflect/derive/src/impls/common.rs Outdated Show resolved Hide resolved
@nixpulvis
Copy link
Contributor

IIUC, this documentation section should probably be updated: https://docs.rs/bevy_reflect/latest/bevy_reflect/#manual-registration

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Reflection Runtime information about types C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants