Skip to content
Open
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
58 changes: 40 additions & 18 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,17 +472,7 @@ impl BlobExt for Blob {

#[cfg(not(windows))]
{
let file_read = read_file::ReadFile::create(
self.store().expect("infallible: store present").clone(),
self.offset.get(),
self.size.get(),
handler,
)
.unwrap_or_else(|e| bun_core::handle_oom(Err(e)));
let read_file_task = read_file::ReadFileTask::create_on_js_thread(
global,
bun_core::heap::into_raw(file_read),
);
let store = self.store().expect("infallible: store present").clone();

// Create the Promise only after the store has been ref()'d.
// The garbage collector runs on memory allocations
Expand All @@ -496,6 +486,26 @@ impl BlobExt for Blob {
let promise_value = unsafe { (*handler).promise.value() };
promise_value.ensure_still_alive();

let offset = self.offset.get();
let size = self.size.get();
if read_file::ReadFile::try_coalesce_fd_read(
&store,
offset,
size,
handler.cast::<c_void>(),
read_file::completion_thunk::<Handler<'_, F>>,
) {
debug!("doReadFile: coalesced onto in-flight fd reader");
return promise_value;
Comment thread
robobun marked this conversation as resolved.
}

let file_read = read_file::ReadFile::create(store.clone(), offset, size, handler)
.unwrap_or_else(|e| bun_core::handle_oom(Err(e)));
let file_read_ptr = bun_core::heap::into_raw(file_read);
read_file::ReadFile::mark_in_flight(&store, offset, size, file_read_ptr);
let read_file_task =
read_file::ReadFileTask::create_on_js_thread(global, file_read_ptr);

// SAFETY: `read_file_task` was just heap-allocated by `create_on_js_thread`.
read_file::ReadFileTask::schedule(unsafe { &mut *read_file_task });

Expand Down Expand Up @@ -688,18 +698,30 @@ impl BlobExt for Blob {
}
#[cfg(not(windows))]
{
let store = self.store().expect("infallible: store present").clone();
let offset = self.offset.get();
let size = self.size.get();
if read_file::ReadFile::try_coalesce_fd_read(
&store,
offset,
size,
ctx.cast::<c_void>(),
NewInternalReadFileHandler::<C, F>::run,
) {
return;
}
let file_read = read_file::ReadFile::create_with_ctx(
self.store().expect("infallible: store present").clone(),
store.clone(),
ctx.cast::<c_void>(),
NewInternalReadFileHandler::<C, F>::run,
self.offset.get(),
self.size.get(),
offset,
size,
)
.unwrap_or_else(|e| bun_core::handle_oom(Err(e)));
let read_file_task = read_file::ReadFileTask::create_on_js_thread(
global,
bun_core::heap::into_raw(file_read),
);
let file_read_ptr = bun_core::heap::into_raw(file_read);
read_file::ReadFile::mark_in_flight(&store, offset, size, file_read_ptr);
let read_file_task =
read_file::ReadFileTask::create_on_js_thread(global, file_read_ptr);
// SAFETY: `read_file_task` was just heap-allocated by `create_on_js_thread`.
read_file::ReadFileTask::schedule(unsafe { &mut *read_file_task });
}
Expand Down
129 changes: 108 additions & 21 deletions src/runtime/webcore/blob/read_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use crate::webcore::Lifetime;
#[cfg(not(windows))]
use crate::webcore::blob::ClosingState;
#[cfg(not(windows))]
use crate::webcore::blob::Store;
use crate::webcore::blob::store::{Bytes as ByteStore, Data, File as FileStore};
use crate::webcore::blob::{Blob, FileCloser, FileOpener, MAX_SIZE, SizeType, StoreRef};
use crate::webcore::node_types::PathOrFileDescriptor;
Expand Down Expand Up @@ -133,6 +135,14 @@

pub type ReadFileOnReadFileCallback = fn(ctx: *mut c_void, bytes: ReadFileResultType);

#[cfg(not(windows))]
pub fn completion_thunk<C: ReadFileCompletion>(ctx: *mut c_void, bytes: ReadFileResultType) {
// The JsTerminated error is intentionally swallowed.
// SAFETY: `ctx` is the `*mut C` passed through `on_complete_ctx` /
// `extra_completions`; ownership transfers per `ReadFileCompletion::run`.
let _ = unsafe { C::run(ctx.cast::<C>(), bytes) };
}

Check warning on line 144 in src/runtime/webcore/blob/read_file.rs

View check run for this annotation

Claude / Claude Code Review

completion_thunk still drops the '// TODO: propagate the exception.' line

nit: af828fc restored only one of the two why-comment lines the earlier review asked for — `// The JsTerminated error is intentionally swallowed.` is back on `completion_thunk`, but `// TODO: propagate the exception.` (present on the removed local `handler_run<C>`) is still gone. The TODO tracks outstanding work (eventual JsTerminated propagation) and shouldn't be dropped in the hoist; add it back beneath the "intentionally swallowed" line.
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

pub struct ReadFileRead {
/// Always a `Box::<[u8]>::into_raw` from the producer's read buffer
/// (`Vec::into_boxed_slice()` so layout is exactly `(ptr, len)`). Every
Expand All @@ -158,6 +168,14 @@

pub type ReadFileTask = bun_jsc::work_task::WorkTask<ReadFile>;

#[cfg(not(windows))]
thread_local! {
/// (fd-backed `Store`, offset, max_length) → in-flight `ReadFile`; see `try_coalesce_fd_read`.
static IN_FLIGHT_FD_READERS: core::cell::RefCell<
bun_collections::HashMap<(*const Store, SizeType, SizeType), *mut ReadFile>,
> = core::cell::RefCell::new(bun_collections::HashMap::new());
}

// `WorkTaskContext` fixes `run`/`then` to take `*mut Self`; the trait method
// cannot be marked `unsafe fn` and the parameter type cannot change, so the
// lint is unsatisfiable here. The pointers come from the work-pool hand-off
Expand Down Expand Up @@ -196,6 +214,8 @@
pub errno: Option<Error>,
pub on_complete_ctx: *mut c_void,
pub on_complete_callback: ReadFileOnReadFileCallback,
/// JS-thread-only; attached via `try_coalesce_fd_read`.
pub extra_completions: bun_threading::Guarded<Vec<(ReadFileOnReadFileCallback, *mut c_void)>>,
pub io_task: Option<*mut ReadFileTask>,
pub io_poll: io::Poll,
pub io_request: io::Request,
Expand Down Expand Up @@ -361,6 +381,7 @@
errno: None,
on_complete_ctx: on_read_file_context,
on_complete_callback,
extra_completions: bun_threading::Guarded::new(Vec::new()),
io_task: None,
io_poll: io::Poll::default(),
io_request: io::Request {
Expand All @@ -383,27 +404,64 @@
context: *mut C,
) -> Result<Box<ReadFile>, Error> {
// `ReadFileCompletion`
// monomorphizes per `C`, so `handler_run::<C>` calls `C::run` directly
// monomorphizes per `C`, so `completion_thunk::<C>` calls `C::run` directly
// and `on_complete_ctx` is the unwrapped `*mut C` — no extra heap box,
// nothing to leak on the `Err` path.
fn handler_run<C: ReadFileCompletion>(ctx: *mut c_void, bytes: ReadFileResultType) {
// The JsTerminated error is intentionally swallowed.
// TODO: propagate the exception.
// SAFETY: `ctx` is the `*mut C` passed unmodified through
// `on_complete_ctx`; ownership transfers per `ReadFileCompletion::run`.
let _ = unsafe { C::run(ctx.cast::<C>(), bytes) };
}
ReadFile::create_with_ctx(
store,
context.cast::<c_void>(),
handler_run::<C>,
completion_thunk::<C>,
off,
max_len,
)
}

pub const IO_TAG: io::Tag = io::Tag::ReadFile;

/// JS-thread-only. Attach to this store's in-flight reader instead of racing the fd.
#[cfg(not(windows))]
pub fn try_coalesce_fd_read(
store: &StoreRef,
offset: SizeType,
max_length: SizeType,
ctx: *mut c_void,
callback: ReadFileOnReadFileCallback,
) -> bool {
if !matches!(&store.data, Data::File(f) if f.pathlike.is_fd()) {
return false;
}
let key = (store.as_ptr().cast_const(), offset, max_length);
let Some(existing) = IN_FLIGHT_FD_READERS.with(|m| m.borrow().get(&key).copied()) else {
return false;
};
// SAFETY: `existing` was inserted on this thread before scheduling and
// is removed on this thread in `then()` before the `ReadFile` is
// dropped (`WorkTask::then` runs on the creating event loop), so the
// pointee is live. The work pool holds `&mut ReadFile` concurrently,
// so project the `Guarded` field through the raw pointer without
// materialising `&ReadFile`.
let extras = unsafe { &*core::ptr::addr_of!((*existing).extra_completions) };
extras.lock().push((callback, ctx));
true
}
Comment thread
robobun marked this conversation as resolved.

/// JS-thread-only. See `IN_FLIGHT_FD_READERS`.
#[cfg(not(windows))]
pub fn mark_in_flight(
store: &StoreRef,
offset: SizeType,
max_length: SizeType,
reader: *mut ReadFile,
) {
if matches!(&store.data, Data::File(f) if f.pathlike.is_fd()) {
IN_FLIGHT_FD_READERS.with(|m| {
m.borrow_mut()
.entry((store.as_ptr().cast_const(), offset, max_length))
.or_insert_with(|| reader);
});
}
}
Comment thread
robobun marked this conversation as resolved.

pub fn on_ready(&mut self) {
bloblog!("ReadFile.onReady");
self.task = WorkPoolTask {
Expand Down Expand Up @@ -562,31 +620,48 @@
let cb = this.on_complete_callback;
let cb_ctx = this.on_complete_ctx;

let mut this = this;

// Clear before the callbacks so a read they schedule starts fresh.
#[cfg(not(windows))]
if let Some(store) = this.store.as_ref() {
let key = (store.as_ptr().cast_const(), this.offset, this.max_length);
let self_ptr = core::ptr::from_ref::<ReadFile>(&this).cast_mut();
IN_FLIGHT_FD_READERS.with(|m| {
let mut m = m.borrow_mut();
if m.get(&key) == Some(&self_ptr) {
m.remove(&key);
}
});
}
let extras: Vec<_> = core::mem::take(&mut *this.extra_completions.lock());

if this.store.is_none() && this.system_error.is_some() {
let mut this = this;
let system_error = this.system_error.take().unwrap();
drop(this);
for (extra_cb, extra_ctx) in extras {
extra_cb(extra_ctx, ReadFileResultType::Err(system_error.clone()));
}
cb(cb_ctx, ReadFileResultType::Err(system_error));
return Ok(());
} else if this.store.is_none() {
drop(this);
if cfg!(debug_assertions) {
panic!("assertion failure - store should not be null");
}
cb(
cb_ctx,
ReadFileResultType::Err(SystemError {
code: BunString::static_("INTERNAL_ERROR").into(),
message: BunString::static_("assertion failure - store should not be null")
.into(),
syscall: BunString::static_("read").into(),
..Default::default()
}),
);
let err = SystemError {
code: BunString::static_("INTERNAL_ERROR").into(),
message: BunString::static_("assertion failure - store should not be null").into(),
syscall: BunString::static_("read").into(),
..Default::default()
};
for (extra_cb, extra_ctx) in extras {
extra_cb(extra_ctx, ReadFileResultType::Err(err.clone()));
}
cb(cb_ctx, ReadFileResultType::Err(err));
return Ok(());
}

let mut this = this;
let _store = this.store.take().unwrap();
// reshaped for borrowck — take buffer out so it survives `drop(this)`.
let buf = core::mem::take(&mut this.buffer);
Expand All @@ -597,12 +672,24 @@
drop(this);

if let Some(err) = system_error {
for (extra_cb, extra_ctx) in extras {
extra_cb(extra_ctx, ReadFileResultType::Err(err.clone()));
}
cb(cb_ctx, ReadFileResultType::Err(err));
return Ok(());
}

// The receiver takes ownership. Normalize to `Box<[u8]>` so every
// consumer can reclaim via `heap::take` with a matching layout.
for (extra_cb, extra_ctx) in extras {
extra_cb(
extra_ctx,
ReadFileResultType::Result(ReadFileRead {
buf: bun_core::heap::into_raw(buf.clone().into_boxed_slice()),
total_size,
}),
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
cb(
cb_ctx,
ReadFileResultType::Result(ReadFileRead {
Expand Down
Loading
Loading