Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
55 changes: 39 additions & 16 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,29 @@ impl BlobExt for Blob {
let promise_value = unsafe { (*handler).promise.value() };
promise_value.ensure_still_alive();

if read_file::ReadFile::try_coalesce_fd_read(
&store,
self.offset.get(),
self.size.get(),
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(),
self.offset.get(),
self.size.get(),
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, 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 +701,28 @@ impl BlobExt for Blob {
}
#[cfg(not(windows))]
{
let store = self.store().expect("infallible: store present").clone();
if read_file::ReadFile::try_coalesce_fd_read(
&store,
self.offset.get(),
self.size.get(),
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(),
)
.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, 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
133 changes: 112 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) };
}
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` → in-flight `ReadFile`; see `try_coalesce_fd_read`.
static IN_FLIGHT_FD_READERS: core::cell::RefCell<
bun_collections::HashMap<*const Store, *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,69 @@
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 existing =
IN_FLIGHT_FD_READERS.with(|m| m.borrow().get(&store.as_ptr().cast_const()).copied());
let Some(existing) = existing 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 fields through the raw pointer without materialising
// `&ReadFile`. `offset`/`max_length` are set at creation only.
let (existing_offset, existing_max_length, extras) = unsafe {
(
*core::ptr::addr_of!((*existing).offset),
*core::ptr::addr_of!((*existing).max_length),
&*core::ptr::addr_of!((*existing).extra_completions),
)
};
if offset != existing_offset || max_length != existing_max_length {
return false;
}
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, 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())
.or_insert_with(|| reader);
});
}
}

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

View check run for this annotation

Claude / Claude Code Review

IN_FLIGHT_FD_READERS keyed on Store alone: leading mismatched-window read blocks same-window siblings from coalescing

Because `IN_FLIGHT_FD_READERS` is keyed on `*const Store` alone and `mark_in_flight` uses `.entry().or_insert_with()`, a leading mismatched-window read (e.g. `Bun.stdin.slice(0,3).bytes()`) occupies the Store's only slot for its lifetime, so two subsequent `Bun.stdin.arrayBuffer()` calls both fail the `(offset, max_length)` check against the sliced entry, both fall through to `create+schedule`, and race on fd 0 exactly as pre-PR. Not a regression (all three raced before), but keying the map on `
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 +625,47 @@
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 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(&store.as_ptr().cast_const()) == Some(&self_ptr) {
m.remove(&store.as_ptr().cast_const());
}
});
}
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 +676,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