Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
260 changes: 180 additions & 80 deletions benches/benches/bevy_scene/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,159 @@ use std::{path::Path, time::Duration};

use bevy_app::App;
use bevy_asset::{
asset_value,
io::{
memory::{Dir, MemoryAssetReader},
AssetSourceBuilder, AssetSourceId,
},
AssetApp, AssetLoader, AssetServer, Assets,
Asset, AssetApp, AssetLoader, AssetServer, Assets, Handle,
};
use bevy_ecs::prelude::*;
use bevy_scene::{prelude::*, ScenePatch};
use bevy_ui::prelude::*;

criterion_group!(benches, spawn);

fn spawn(c: &mut Criterion) {
let mut group = c.benchmark_group("spawn");
group.warm_up_time(Duration::from_millis(500));
group.measurement_time(Duration::from_secs(4));
group.bench_function("ui_immediate_function_scene", |b| {
let mut app = bench_app(|_| {}, |_| {});
b.iter(move || {
app.world_mut().spawn_scene(ui()).unwrap();
});
});
group.bench_function("ui_immediate_loaded_scene", |b| {
let dir = Dir::default();
let mut app = bench_app(
|app| {
in_memory_asset_source(dir.clone(), app);
},
|app| {
app.register_asset_loader(FakeSceneLoader::new(button));
},
);

// Insert an asset that the fake loader can fake read.
dir.insert_asset_text(Path::new("button.bsn"), "");

let asset_server = app.world().resource::<AssetServer>().clone();
let handle = asset_server.load("button.bsn");

run_app_until(&mut app, || asset_server.is_loaded(&handle));

let patch = app
.world()
.resource::<Assets<ScenePatch>>()
.get(&handle)
.unwrap();
assert!(patch.resolved.is_some());

b.iter(move || {
app.world_mut().spawn_scene(ui_loaded_asset()).unwrap();
});

drop(handle);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: this drop does nothing.

});
group.bench_function("ui_raw_bundle_no_scene", |b| {
let mut app = bench_app(|_| {}, |_| {});

b.iter(move || {
app.world_mut().spawn(raw_ui());
});
});

group.bench_function("handle_template_handle", |b| {
let dir = Dir::default();
let mut app = bench_app(
|app| {
in_memory_asset_source(dir.clone(), app);
},
|app| {
app.init_asset::<EmptyAsset>();
let assets = app.world().resource::<AssetServer>();
let handles = (0..10).map(|_| assets.add(EmptyAsset)).collect::<Vec<_>>();
app.register_asset_loader(FakeSceneLoader::new(move || {
asset_handle_scene(handles.clone())
}));
},
);

dir.insert_asset_text(Path::new("a.bsn"), "");

let asset_server = app.world().resource::<AssetServer>().clone();
let handle = asset_server.load::<ScenePatch>("a.bsn");

run_app_until(&mut app, || asset_server.is_loaded(&handle));

let world = app.world_mut();
b.iter(|| {
for _ in 0..100 {
world.spawn_scene(bsn! { :"a.bsn" }).unwrap();
}
});
});

group.bench_function("handle_template_value", |b| {
let dir = Dir::default();
let mut app = bench_app(
|app| {
in_memory_asset_source(dir.clone(), app);
},
|app| {
app.register_asset_loader(FakeSceneLoader::new(asset_value_scene));
app.init_asset::<EmptyAsset>();
},
);

dir.insert_asset_text(Path::new("a.bsn"), "");

let asset_server = app.world().resource::<AssetServer>().clone();
let handle = asset_server.load::<ScenePatch>("a.bsn");

run_app_until(&mut app, || asset_server.is_loaded(&handle));

let world = app.world_mut();
b.iter(|| {
for _ in 0..100 {
world.spawn_scene(bsn! { :"a.bsn" }).unwrap();
}
});
});
group.finish();
}

#[derive(Asset, TypePath)]
struct EmptyAsset;

#[derive(Component, FromTemplate)]
#[expect(unused, reason = "this is just used for init")]
struct AssetReference(Handle<EmptyAsset>);

fn asset_value_scene() -> impl Scene {
let children = (0..10)
.map(|_| {
bsn! {AssetReference(asset_value(EmptyAsset))}
})
.collect::<Vec<_>>();
bsn! {
Children [{children}]
}
}

fn asset_handle_scene(mut handles: Vec<Handle<EmptyAsset>>) -> impl Scene {
let children = handles
.drain(..)
.map(|handle| {
bsn! {AssetReference({handle.clone()})}
})
.collect::<Vec<_>>();
bsn! {
Children [{children}]
}
}

fn ui() -> impl Scene {
bsn! {
Node
Expand Down Expand Up @@ -209,88 +350,47 @@ fn run_app_until(app: &mut App, mut predicate: impl FnMut() -> bool) {
panic!("Ran out of loops to return `Some` from `predicate`");
}

fn spawn(c: &mut Criterion) {
let mut group = c.benchmark_group("spawn");
group.warm_up_time(Duration::from_millis(500));
group.measurement_time(Duration::from_secs(4));
group.bench_function("ui_immediate_function_scene", |b| {
let mut app = App::new();
app.add_plugins((bevy_asset::AssetPlugin::default(), bevy_scene::ScenePlugin));

b.iter(move || {
app.world_mut().spawn_scene(ui()).unwrap();
});
});
group.bench_function("ui_immediate_loaded_scene", |b| {
let mut app = App::new();
let dir = Dir::default();
let dir_clone = dir.clone();
app.register_asset_source(
AssetSourceId::Default,
AssetSourceBuilder::new(move || {
Box::new(MemoryAssetReader {
root: dir_clone.clone(),
})
}),
);
app.add_plugins((
bevy_app::TaskPoolPlugin::default(),
bevy_asset::AssetPlugin::default(),
bevy_scene::ScenePlugin,
));
app.finish();
app.cleanup();

// Create a fake loader to act as a ScenePatch loaded from a file.
app.register_asset_loader(FakeSceneLoader);

#[derive(TypePath)]
struct FakeSceneLoader;

impl AssetLoader for FakeSceneLoader {
type Asset = ScenePatch;
type Error = std::io::Error;
type Settings = ();

async fn load(
&self,
_reader: &mut dyn bevy_asset::io::Reader,
_settings: &Self::Settings,
load_context: &mut bevy_asset::LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
Ok(ScenePatch::load_with(load_context, button()))
}
}

// Insert an asset that the fake loader can fake read.
dir.insert_asset_text(Path::new("button.bsn"), "");

let asset_server = app.world().resource::<AssetServer>().clone();
let handle = asset_server.load("button.bsn");
assert!(app.world().get_resource::<Assets<ScenePatch>>().is_some());
fn bench_app(before: impl FnOnce(&mut App), after: impl FnOnce(&mut App)) -> App {
let mut app = App::new();
before(&mut app);
app.add_plugins((
bevy_app::TaskPoolPlugin::default(),
bevy_asset::AssetPlugin::default(),
bevy_scene::ScenePlugin,
));
after(&mut app);
app.finish();
app.cleanup();
app
}

run_app_until(&mut app, || asset_server.is_loaded(&handle));
fn in_memory_asset_source(dir: Dir, app: &mut App) {
app.register_asset_source(
AssetSourceId::Default,
AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() })),
);
}

let patch = app
.world()
.resource::<Assets<ScenePatch>>()
.get(&handle)
.unwrap();
assert!(patch.resolved.is_some());
#[derive(TypePath)]
struct FakeSceneLoader(Box<dyn Fn() -> Box<dyn Scene> + Send + Sync>);

b.iter(move || {
app.world_mut().spawn_scene(ui_loaded_asset()).unwrap();
});
impl FakeSceneLoader {
pub fn new<S: Scene>(scene_fn: impl (Fn() -> S) + Send + Sync + 'static) -> Self {
Self(Box::new(move || Box::new(scene_fn())))
}
}

drop(handle);
});
group.bench_function("ui_raw_bundle_no_scene", |b| {
let mut app = App::new();
app.add_plugins((bevy_asset::AssetPlugin::default(), bevy_scene::ScenePlugin));
impl AssetLoader for FakeSceneLoader {
type Asset = ScenePatch;
type Error = std::io::Error;
type Settings = ();

b.iter(move || {
app.world_mut().spawn(raw_ui());
});
});
group.finish();
async fn load(
&self,
_reader: &mut dyn bevy_asset::io::Reader,
_settings: &Self::Settings,
load_context: &mut bevy_asset::LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
Ok(ScenePatch::load_with(load_context, (self.0)()))
}
}
74 changes: 73 additions & 1 deletion crates/bevy_asset/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
};
use alloc::sync::Arc;
use bevy_ecs::template::{FromTemplate, SpecializeFromTemplate, Template, TemplateContext};
use bevy_platform::collections::Equivalent;
use bevy_platform::{collections::Equivalent, sync::Mutex};
use bevy_reflect::{Reflect, TypePath};
use core::{
any::TypeId,
Expand Down Expand Up @@ -208,10 +208,56 @@ impl<T: Asset> FromTemplate for Handle<T> {
type Template = HandleTemplate<T>;
}

/// A [`Template`] that produces a [`Handle`].
#[derive(Reflect)]
pub enum HandleTemplate<T: Asset> {
/// Creates a [`Handle`] by calling [`AssetServer::load`] on the given [`AssetPath`].
Path(AssetPath<'static>),
/// Creates a [`Handle`] by cloning the given [`Handle`] value.
Handle(Handle<T>),
/// Creates a [`Handle`] by adding the given asset value using [`AssetServer::add`]. This will
/// cache the resulting [`Handle`] on the template and reuse it for future template builds.
///
/// This should generally be constructed using [`HandleTemplate::value`] or [`asset_value`].
Value(ArcMutexValue<T>),
Comment thread
cart marked this conversation as resolved.
}

impl<T: Asset> HandleTemplate<T> {
/// This will create a new [`HandleTemplate`] for the given `asset` value. This makes it possible
/// to define assets "inline" in templates / scenes that produce a [`Handle`].
///
/// This supports [`Into`]
/// to automatically convert values that can become `A`.
pub fn value(value: impl Into<T>) -> Self {
HandleTemplate::Value(ArcMutexValue(Arc::new(Mutex::new(AssetOrHandle::Value(
Some(value.into()),
)))))
}
}

/// Stores an [`Arc<Mutex<AssetOrHandle<T>>>`].
///
/// This intermediary type exists largely to enable reflect(opaque).
#[derive(Reflect)]
#[reflect(opaque)]
pub struct ArcMutexValue<T: Asset>(Arc<Mutex<AssetOrHandle<T>>>);

impl<T: Asset> Clone for ArcMutexValue<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}

#[derive(Reflect)]
enum AssetOrHandle<T: Asset> {
Value(Option<T>),
Handle(Handle<T>),
}

impl<T: Asset> Default for AssetOrHandle<T> {
fn default() -> Self {
Self::Handle(Default::default())
}
}

impl<T: Asset> Default for HandleTemplate<T> {
Expand All @@ -238,16 +284,42 @@ impl<T: Asset> Template for HandleTemplate<T> {
Ok(match self {
HandleTemplate::Path(asset_path) => context.resource::<AssetServer>().load(asset_path),
HandleTemplate::Handle(handle) => handle.clone(),
HandleTemplate::Value(value) => {
// This unwrap is ok. If another caller panicked while holding this mutex, then the
// program is in an invalid state and this should panic too.
let mut value_or_handle = value.0.lock().unwrap();
Comment thread
cart marked this conversation as resolved.
match &mut *value_or_handle {
AssetOrHandle::Value(value) => {
// This unwrap is ok because AssetOrHandle::Value will always either contain a Some Value
// when it is in this state (AssetOrHandle is private).
let handle = context.resource::<AssetServer>().add(value.take().unwrap());
*value_or_handle = AssetOrHandle::Handle(handle.clone());
handle
}
AssetOrHandle::Handle(handle) => handle.clone(),
}
}
})
}

fn clone_template(&self) -> Self {
match self {
HandleTemplate::Path(asset_path) => HandleTemplate::Path(asset_path.clone()),
HandleTemplate::Handle(handle) => HandleTemplate::Handle(handle.clone()),
HandleTemplate::Value(value) => HandleTemplate::Value(value.clone()),
}
}
}

/// This will create a new [`HandleTemplate`] for the given `asset` value. This makes it possible
/// to define assets "inline" in templates / scenes that produce a [`Handle`].
///
/// This supports [`Into`]
/// to automatically convert values that can become `A`.
pub fn asset_value<I: Into<A>, A: Asset>(asset: I) -> HandleTemplate<A> {
Comment thread
cart marked this conversation as resolved.
HandleTemplate::value(asset)
}

impl<A: Asset> core::fmt::Debug for Handle<A> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let name = ShortName::of::<A>();
Expand Down
Loading