Skip to content

Commit 01aca66

Browse files
authored
limit number of open files (#22560)
# Objective - Limit the number of open files - #17377, but scoped to the `FileAssetReader`, - Fixes #14542 - Running the `bistro` scene on macOS gives errors in the log, and some texture are randomly not loaded: ``` ERROR bevy_asset::server: Encountered an I/O error while loading asset: Too many open files (os error 24) ``` ## Solution - on iOS and macOS, add a `Semaphore` to ensure we're not opening more than 128 files in parallel ## Testing - `cargo run --release --package bistro` works without errors in the log
1 parent 78febd2 commit 01aca66

File tree

2 files changed

+96
-14
lines changed

2 files changed

+96
-14
lines changed

crates/bevy_asset/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ uuid = { version = "1.13.1", default-features = false, features = [
6969
] }
7070
tracing = { version = "0.1", default-features = false }
7171

72+
[target.'cfg(not(any(target_os = "windows", target_arch = "wasm32")))'.dependencies]
73+
async-io = "2.6"
74+
7275
[target.'cfg(target_os = "android")'.dependencies]
7376
bevy_android = { path = "../bevy_android", version = "0.19.0-dev", default-features = false }
7477

crates/bevy_asset/src/io/file/file_asset.rs

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@ use crate::io::{
33
Reader, ReaderNotSeekableError, SeekableReader, Writer,
44
};
55
use async_fs::{read_dir, File};
6+
#[cfg(not(target_os = "windows"))]
7+
use async_io::Timer;
8+
#[cfg(not(target_os = "windows"))]
9+
use async_lock::{Semaphore, SemaphoreGuard};
610
use futures_lite::StreamExt;
711

812
use alloc::{borrow::ToOwned, boxed::Box};
13+
#[cfg(target_os = "windows")]
14+
use core::marker::PhantomData;
15+
#[cfg(not(target_os = "windows"))]
16+
use core::time::Duration;
17+
#[cfg(not(target_os = "windows"))]
18+
use futures_util::{future, pin_mut};
919
use std::path::Path;
1020

1121
use super::{FileAssetReader, FileAssetWriter};
@@ -16,28 +26,97 @@ impl Reader for File {
1626
}
1727
}
1828

29+
// Set to OS default limit / 2
30+
// macos & ios: 256
31+
// linux & android: 1024
32+
#[cfg(any(target_os = "macos", target_os = "ios"))]
33+
static OPEN_FILE_LIMITER: Semaphore = Semaphore::new(128);
34+
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
35+
static OPEN_FILE_LIMITER: Semaphore = Semaphore::new(512);
36+
37+
#[cfg(not(target_os = "windows"))]
38+
async fn maybe_get_semaphore<'a>() -> Option<SemaphoreGuard<'a>> {
39+
let guard_future = OPEN_FILE_LIMITER.acquire();
40+
let timeout_future = Timer::after(Duration::from_millis(500));
41+
pin_mut!(guard_future);
42+
pin_mut!(timeout_future);
43+
44+
match future::select(guard_future, timeout_future).await {
45+
future::Either::Left((guard, _)) => Some(guard),
46+
future::Either::Right((_, _)) => None,
47+
}
48+
}
49+
50+
struct GuardedFile<'a> {
51+
file: File,
52+
#[cfg(not(target_os = "windows"))]
53+
_guard: Option<SemaphoreGuard<'a>>,
54+
#[cfg(target_os = "windows")]
55+
_lifetime: PhantomData<&'a ()>,
56+
}
57+
58+
impl<'a> futures_io::AsyncRead for GuardedFile<'a> {
59+
fn poll_read(
60+
mut self: core::pin::Pin<&mut Self>,
61+
cx: &mut core::task::Context<'_>,
62+
buf: &mut [u8],
63+
) -> core::task::Poll<std::io::Result<usize>> {
64+
core::pin::Pin::new(&mut self.file).poll_read(cx, buf)
65+
}
66+
}
67+
68+
impl<'a> Reader for GuardedFile<'a> {
69+
fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
70+
self.file.seekable()
71+
}
72+
}
73+
1974
impl AssetReader for FileAssetReader {
2075
async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
76+
#[cfg(not(target_os = "windows"))]
77+
let _guard = maybe_get_semaphore().await;
78+
2179
let full_path = self.root_path.join(path);
22-
File::open(&full_path).await.map_err(|e| {
23-
if e.kind() == std::io::ErrorKind::NotFound {
24-
AssetReaderError::NotFound(full_path)
25-
} else {
26-
e.into()
27-
}
28-
})
80+
File::open(&full_path)
81+
.await
82+
.map_err(|e| {
83+
if e.kind() == std::io::ErrorKind::NotFound {
84+
AssetReaderError::NotFound(full_path)
85+
} else {
86+
e.into()
87+
}
88+
})
89+
.map(|file| GuardedFile {
90+
file,
91+
#[cfg(not(target_os = "windows"))]
92+
_guard,
93+
#[cfg(target_os = "windows")]
94+
_lifetime: PhantomData::default(),
95+
})
2996
}
3097

3198
async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
99+
#[cfg(not(target_os = "windows"))]
100+
let _guard = maybe_get_semaphore().await;
101+
32102
let meta_path = get_meta_path(path);
33103
let full_path = self.root_path.join(meta_path);
34-
File::open(&full_path).await.map_err(|e| {
35-
if e.kind() == std::io::ErrorKind::NotFound {
36-
AssetReaderError::NotFound(full_path)
37-
} else {
38-
e.into()
39-
}
40-
})
104+
File::open(&full_path)
105+
.await
106+
.map_err(|e| {
107+
if e.kind() == std::io::ErrorKind::NotFound {
108+
AssetReaderError::NotFound(full_path)
109+
} else {
110+
e.into()
111+
}
112+
})
113+
.map(|file| GuardedFile {
114+
file,
115+
#[cfg(not(target_os = "windows"))]
116+
_guard,
117+
#[cfg(target_os = "windows")]
118+
_lifetime: PhantomData::default(),
119+
})
41120
}
42121

43122
async fn read_directory<'a>(

0 commit comments

Comments
 (0)