Skip to content

Commit 6090daa

Browse files
authored
Fix hot reloads of a folder resulting in deadlocks. (#23980)
# Objective - Fixes #23954. ## Solution - Don't lock the infos RwLock inside load_folder_internal. Instead at every callsite (2 callsites), count the number of started loads with whatever locks we have. - Move the load_folder_internal calls until after we drop the `infos`. ## Testing - Added a test for this.
1 parent bafb203 commit 6090daa

3 files changed

Lines changed: 149 additions & 40 deletions

File tree

crates/bevy_asset/src/lib.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@ mod tests {
750750
use bevy_tasks::block_on;
751751
use core::{any::TypeId, time::Duration};
752752
use futures_lite::AsyncReadExt;
753+
use ron::ser::PrettyConfig;
753754
use serde::{Deserialize, Serialize};
754755
use std::path::{Path, PathBuf};
755756
use thiserror::Error;
@@ -993,6 +994,20 @@ mod tests {
993994
storage.0.extend(reader.read().cloned());
994995
}
995996

997+
/// Serializes `text` into a `CoolText` that can be loaded.
998+
///
999+
/// This doesn't support all the features of `CoolText`, so more complex scenarios may require
1000+
/// doing this manually.
1001+
pub(crate) fn serialize_as_cool_text(text: &str) -> String {
1002+
let cool_text_ron = CoolTextRon {
1003+
text: text.into(),
1004+
dependencies: vec![],
1005+
embedded_dependencies: vec![],
1006+
sub_texts: vec![],
1007+
};
1008+
ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()
1009+
}
1010+
9961011
#[test]
9971012
fn load_dependencies() {
9981013
let dir = Dir::default();
@@ -3217,4 +3232,102 @@ mod tests {
32173232
asset_server.are_direct_dependencies_loaded(app.world().resource::<MyAssetHolder>())
32183233
);
32193234
}
3235+
3236+
#[test]
3237+
fn hot_reload_folder() {
3238+
let (mut app, dir, event_sender) = create_app_with_source_event_sender();
3239+
3240+
app.init_asset::<CoolText>()
3241+
.init_asset::<SubText>()
3242+
.register_asset_loader(CoolTextLoader);
3243+
3244+
let abc_path = Path::new("dir/abc.cool.ron");
3245+
let def_path = Path::new("dir/def.cool.ron");
3246+
dir.insert_asset_text(abc_path, &serialize_as_cool_text("abc"));
3247+
dir.insert_asset_text(def_path, &serialize_as_cool_text("def"));
3248+
3249+
let asset_server = app.world().resource::<AssetServer>().clone();
3250+
3251+
let folder_handle = asset_server.load_folder("dir");
3252+
run_app_until(&mut app, |_| {
3253+
asset_server
3254+
.is_loaded_with_dependencies(&folder_handle)
3255+
.then_some(())
3256+
});
3257+
3258+
let folder = app
3259+
.world()
3260+
.resource::<Assets<LoadedFolder>>()
3261+
.get(&folder_handle)
3262+
.unwrap();
3263+
assert_eq!(folder.handles.len(), 2);
3264+
let mut handles = folder
3265+
.handles
3266+
.iter()
3267+
.cloned()
3268+
.map(UntypedHandle::typed::<CoolText>)
3269+
.collect::<Vec<_>>();
3270+
// Sort the handles so we know abc is first and def is second.
3271+
handles.sort_by_key(|handle| handle.path().unwrap().path().to_path_buf());
3272+
3273+
let abc_handle = handles[0].clone();
3274+
let def_handle = handles[1].clone();
3275+
3276+
let cool_texts = app.world().resource::<Assets<CoolText>>();
3277+
assert_eq!(cool_texts.get(&abc_handle).unwrap().text, "abc");
3278+
assert_eq!(cool_texts.get(&def_handle).unwrap().text, "def");
3279+
3280+
// Before doing any hot reloading stuff, clear out any AssetEvent messages.
3281+
app.world_mut()
3282+
.resource_mut::<Messages<AssetEvent<LoadedFolder>>>()
3283+
.clear();
3284+
3285+
// Add a new asset to the folder, and send an event to trigger hot-reloading.
3286+
let ghi_path = Path::new("dir/ghi.cool.ron");
3287+
dir.insert_asset_text(ghi_path, &serialize_as_cool_text("ghi"));
3288+
event_sender
3289+
.send_blocking(AssetSourceEvent::AddedAsset(ghi_path.to_path_buf()))
3290+
.unwrap();
3291+
3292+
run_app_until(&mut app, |world| {
3293+
for event in world
3294+
.resource_mut::<Messages<AssetEvent<LoadedFolder>>>()
3295+
.drain()
3296+
{
3297+
if let AssetEvent::LoadedWithDependencies { id } = event
3298+
&& id == folder_handle.id()
3299+
{
3300+
return Some(());
3301+
}
3302+
}
3303+
None
3304+
});
3305+
3306+
let folder = app
3307+
.world()
3308+
.resource::<Assets<LoadedFolder>>()
3309+
.get(&folder_handle)
3310+
.unwrap();
3311+
assert_eq!(folder.handles.len(), 3);
3312+
let mut handles = folder
3313+
.handles
3314+
.iter()
3315+
.cloned()
3316+
.map(UntypedHandle::typed::<CoolText>)
3317+
.collect::<Vec<_>>();
3318+
// Sort the handles so we know the order is abc, def, and ghi.
3319+
handles.sort_by_key(|handle| handle.path().unwrap().path().to_path_buf());
3320+
3321+
let new_abc_handle = handles[0].clone();
3322+
let new_def_handle = handles[1].clone();
3323+
let new_ghi_handle = handles[2].clone();
3324+
3325+
assert_eq!(new_abc_handle, abc_handle);
3326+
assert_eq!(new_def_handle, def_handle);
3327+
3328+
let cool_texts = app.world().resource::<Assets<CoolText>>();
3329+
assert_eq!(cool_texts.get(&new_abc_handle).unwrap().text, "abc");
3330+
assert_eq!(cool_texts.get(&new_def_handle).unwrap().text, "def");
3331+
assert_eq!(cool_texts.get(&new_ghi_handle).unwrap().text, "ghi");
3332+
}
32203333
}

crates/bevy_asset/src/processor/tests.rs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ use crate::{
3434
},
3535
saver::{tests::CoolTextSaver, AssetSaver},
3636
tests::{
37-
read_asset_as_string, read_meta_as_string, run_app_until, CoolText, CoolTextLoader,
38-
CoolTextRon, SubText,
37+
read_asset_as_string, read_meta_as_string, run_app_until, serialize_as_cool_text, CoolText,
38+
CoolTextLoader, CoolTextRon, SubText,
3939
},
4040
transformer::{AssetTransformer, TransformedAsset},
4141
Asset, AssetApp, AssetLoader, AssetMode, AssetPath, AssetPlugin, LoadContext,
@@ -223,20 +223,6 @@ impl<R: AssetReader> AssetReader for LockGatedReader<R> {
223223
}
224224
}
225225

226-
/// Serializes `text` into a `CoolText` that can be loaded.
227-
///
228-
/// This doesn't support all the features of `CoolText`, so more complex scenarios may require doing
229-
/// this manually.
230-
fn serialize_as_cool_text(text: &str) -> String {
231-
let cool_text_ron = CoolTextRon {
232-
text: text.into(),
233-
dependencies: vec![],
234-
embedded_dependencies: vec![],
235-
sub_texts: vec![],
236-
};
237-
ron::ser::to_string_pretty(&cool_text_ron, PrettyConfig::new().new_line("\n")).unwrap()
238-
}
239-
240226
/// Sets the transaction log for the app to a fake one to prevent touching the filesystem.
241227
fn set_fake_transaction_log(app: &mut App) {
242228
/// A dummy transaction log factory that just creates [`FakeTransactionLog`].

crates/bevy_asset/src/server/mod.rs

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,7 @@ impl AssetServer {
11421142
}
11431143
// `get_or_create_path_handle` always returns a Strong variant, so this is safe.
11441144
let index = (&handle).try_into().unwrap();
1145+
self.write_infos().stats.started_load_tasks += 1;
11451146
self.load_folder_internal(index, path);
11461147

11471148
handle
@@ -1186,8 +1187,6 @@ impl AssetServer {
11861187
Ok(())
11871188
}
11881189

1189-
self.write_infos().stats.started_load_tasks += 1;
1190-
11911190
let path = path.into_owned();
11921191
let server = self.clone();
11931192
IoTaskPool::get()
@@ -2155,45 +2154,51 @@ pub fn handle_internal_asset_events(world: &mut World) {
21552154
}
21562155
}
21572156

2158-
let reload_parent_folders = |path: &PathBuf, source: &AssetSourceId<'static>| {
2159-
for parent in path.ancestors().skip(1) {
2160-
let parent_asset_path =
2161-
AssetPath::from(parent.to_path_buf()).with_source(source.clone());
2162-
for folder_handle in infos.get_path_handles(&parent_asset_path) {
2163-
info!("Reloading folder {parent_asset_path} because the content has changed");
2164-
// `get_path_handles` only returns Strong variants, so this is safe.
2165-
let index = (&folder_handle).try_into().unwrap();
2166-
server.load_folder_internal(index, parent_asset_path.clone());
2157+
let mut folders_to_reload = Vec::default();
2158+
let mut reload_parent_folders =
2159+
|path: &PathBuf, source: &AssetSourceId<'static>, infos: &mut AssetInfos| {
2160+
let mut new_loads = 0;
2161+
for parent in path.ancestors().skip(1) {
2162+
let parent_asset_path =
2163+
AssetPath::from(parent.to_path_buf()).with_source(source.clone());
2164+
for folder_handle in infos.get_path_handles(&parent_asset_path) {
2165+
info!(
2166+
"Reloading folder {parent_asset_path} because the content has changed"
2167+
);
2168+
new_loads += 1;
2169+
folders_to_reload.push((folder_handle, parent_asset_path.clone()));
2170+
}
21672171
}
2168-
}
2169-
};
2172+
infos.stats.started_load_tasks += new_loads;
2173+
};
21702174

21712175
let mut paths_to_reload = <HashSet<_>>::default();
2172-
let mut reload_path = |path: PathBuf, source: &AssetSourceId<'static>| {
2173-
let path = AssetPath::from(path).with_source(source);
2174-
queue_ancestors(&path, &infos, &mut paths_to_reload);
2175-
paths_to_reload.insert(path);
2176-
};
2176+
let mut reload_path =
2177+
|path: PathBuf, source: &AssetSourceId<'static>, infos: &AssetInfos| {
2178+
let path = AssetPath::from(path).with_source(source);
2179+
queue_ancestors(&path, infos, &mut paths_to_reload);
2180+
paths_to_reload.insert(path);
2181+
};
21772182

21782183
let mut handle_event = |source: AssetSourceId<'static>, event: AssetSourceEvent| {
21792184
match event {
21802185
AssetSourceEvent::AddedAsset(path) => {
2181-
reload_parent_folders(&path, &source);
2182-
reload_path(path, &source);
2186+
reload_parent_folders(&path, &source, &mut infos);
2187+
reload_path(path, &source, &infos);
21832188
}
21842189
// TODO: if the asset was processed and the processed file was changed, the first modified event
21852190
// should be skipped?
21862191
AssetSourceEvent::ModifiedAsset(path) | AssetSourceEvent::ModifiedMeta(path) => {
2187-
reload_path(path, &source);
2192+
reload_path(path, &source, &infos);
21882193
}
21892194
AssetSourceEvent::RenamedFolder { old, new } => {
2190-
reload_parent_folders(&old, &source);
2191-
reload_parent_folders(&new, &source);
2195+
reload_parent_folders(&old, &source, &mut infos);
2196+
reload_parent_folders(&new, &source, &mut infos);
21922197
}
21932198
AssetSourceEvent::RemovedAsset(path)
21942199
| AssetSourceEvent::RemovedFolder(path)
21952200
| AssetSourceEvent::AddedFolder(path) => {
2196-
reload_parent_folders(&path, &source);
2201+
reload_parent_folders(&path, &source, &mut infos);
21972202
}
21982203
_ => {}
21992204
}
@@ -2223,6 +2228,11 @@ pub fn handle_internal_asset_events(world: &mut World) {
22232228
#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))]
22242229
drop(infos);
22252230

2231+
for (handle, path) in folders_to_reload {
2232+
// `get_path_handles` only returns Strong variants, so this is safe.
2233+
let index = (&handle).try_into().unwrap();
2234+
server.load_folder_internal(index, path);
2235+
}
22262236
for path in paths_to_reload {
22272237
server.reload_internal(path, true);
22282238
}

0 commit comments

Comments
 (0)