Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions src/bundler/Graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ pub struct Graph<'a> {
///
/// When `pending_items` hits zero and there are deferred pending tasks, those
/// tasks will be run, and the count is "moved" back to `pending_items`
/// (`drain_deferred_tasks`). A load that is answered before then takes its
/// own count back (`BundleV2::on_load`). `Load::deferred` marks whose
/// counts are in here.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) deferred_pending: u32,

/// onResolve / onLoad requests a plugin currently holds (dispatched to its
Expand Down Expand Up @@ -274,6 +277,12 @@ impl<T> Default for OutstandingLink<T> {
}
}
}
impl<T> OutstandingLink<T> {
/// Dispatched to the plugin and not answered yet.
pub(crate) fn is_linked(&self) -> bool {
self.linked
}
}
pub trait OutstandingNode: Sized {
fn link(&mut self) -> &mut OutstandingLink<Self>;
}
Expand Down
183 changes: 117 additions & 66 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1199,15 +1199,21 @@ pub mod bv2_impl {
pub parse_task: bun_ptr::BackRef<ParseTask, bun_ptr::Mut>,
/// Faster path: skip the extra threadpool dispatch when the file is not found.
pub was_file: bool,
/// Defer may only be called once.
/// Defer may only be called once (JS thread).
pub called_defer: bool,
/// `.defer()`ed and not yet drained: its scan-counter unit sits in
/// `Graph::deferred_pending` (bundle thread only).
/// `.defer()`ed and neither drained nor answered yet: its
/// scan-counter unit sits in `Graph::deferred_pending` instead of
/// `pending_items` (bundle thread only).
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) deferred: bool,
/// `jsc.AnyEventLoop.Task` — intrusive node for the Mini-loop queue
/// (used by `onDefer` to notify the bundler thread when it runs
/// under a `MiniEventLoop`).
/// Intrusive Mini-loop queue node carrying the onLoad answer to
/// the bundle thread (`on_load_async`).
Comment thread
robobun marked this conversation as resolved.
Outdated
pub task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext,
/// Same, for the `.defer()` notification (`on_defer_async`). A
/// separate node because a plugin may answer right after
/// deferring, while this one is still queued; `called_defer`
/// guarantees it is enqueued at most once.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) defer_task:
bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext,
/// Links in `Graph::outstanding_loads`; bundle thread only.
pub(crate) outstanding: crate::Graph::OutstandingLink<Load>,
}
Expand All @@ -1229,6 +1235,7 @@ pub mod bv2_impl {
called_defer: false,
deferred: false,
task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext::default(),
defer_task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext::default(),
outstanding: Default::default(),
}
}
Expand Down Expand Up @@ -2120,14 +2127,6 @@ pub mod bv2_impl {
while let Some(load) = self.graph.outstanding_loads.pop() {
// SAFETY: as above.
let load = unsafe { &mut *load };
if load.deferred {
// Its unit is parked in `deferred_pending`, not `pending_items`.
load.deferred = false;
self.graph.deferred_pending -= 1;
drop(core::mem::take(&mut load.path));
drop(core::mem::take(&mut load.namespace));
continue;
}
load.value = jsc_api::JSBundler::LoadValue::Err(cancelled_msg(&load.path));
Self::on_load(load, self);
}
Expand Down Expand Up @@ -4316,16 +4315,28 @@ pub mod bv2_impl {
Ok(())
}

pub fn on_load_async(&mut self, load: &mut jsc_api::JSBundler::Load) {
// Dispatch to the loop that *owns* `BundleV2`.
// For `Bun.build` this is a Mini loop running on the bundler thread, so
// `on_load` must land there — not on the JS plugin loop — or it will
// mutate `graph` / allocate from `graph.heap` off-thread.
/// Plugin host's thread: hand a plugin request back to the loop that
/// *owns* `BundleV2`. For `Bun.build` this is a Mini loop running on
/// the bundler thread, so the callback must land there — not on the JS
/// plugin loop — or it will mutate `graph` / allocate from `graph.heap`
/// off-thread.
///
/// # Safety
/// `task_offset` is `offset_of!(C, <field>)` of an
/// `AnyTaskWithExtraContext` field of `*request` that no other hop has
/// queued (a node may sit in the Mini queue once), and `*request`
/// outlives the hop (arena-owned by this pass).
Comment thread
robobun marked this conversation as resolved.
Outdated
unsafe fn post_to_own_loop<C>(
&mut self,
request: *mut C,
on_js_loop: fn(*mut C) -> bun_event_loop::JsResult<()>,
on_mini_loop: fn(*mut C, *mut BundleV2<'static>),
task_offset: usize,
) {
match self.any_loop_mut() {
bun_event_loop::AnyEventLoop::Js { .. } => {
let ct = bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(
std::ptr::from_mut(load),
on_load_from_js_loop_raw,
request, on_js_loop,
);
let poster = self
.js_poster
Expand All @@ -4340,50 +4351,60 @@ pub mod bv2_impl {
}
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
// SAFETY: `load` is a valid &mut for the duration of the enqueue;
// the mini loop dispatches `on_load_mini` on the bundler thread.
// SAFETY: fn contract; the mini loop dispatches `on_mini_loop`
// on the bundler thread with this `BundleV2` as the extra ctx.
unsafe {
mini.enqueue_task_concurrent_with_extra_ctx::<jsc_api::JSBundler::Load, BundleV2<'static>>(
std::ptr::from_mut(load),
on_load_mini,
core::mem::offset_of!(jsc_api::JSBundler::Load, task),
mini.enqueue_task_concurrent_with_extra_ctx::<C, BundleV2<'static>>(
request,
on_mini_loop,
task_offset,
);
}
}
}
}

/// The onLoad answer is in `load.value`; run `on_load` on the owning loop.
pub fn on_load_async(&mut self, load: &mut jsc_api::JSBundler::Load) {
// SAFETY: `task` is only ever queued by this hop, which the plugin
// glue runs once per load; `load` is arena-owned by this pass.
unsafe {
self.post_to_own_loop(
std::ptr::from_mut(load),
on_load_from_js_loop_raw,
on_load_mini,
core::mem::offset_of!(jsc_api::JSBundler::Load, task),
);
}
}

/// The onLoad callback called `.defer()`; run `on_notify_defer` on the
/// owning loop. Same queue as the load's answer, so it arrives before
/// the answer exactly when it was issued before it.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub fn on_defer_async(&mut self, load: &mut jsc_api::JSBundler::Load) {
// SAFETY: `defer_task` is only ever queued by this hop, which
// `Load::called_defer` limits to once per load; `load` is
// arena-owned by this pass.
unsafe {
self.post_to_own_loop(
std::ptr::from_mut(load),
on_notify_defer_from_js_loop_raw,
on_notify_defer_mini,
core::mem::offset_of!(jsc_api::JSBundler::Load, defer_task),
);
}
}

/// The onResolve answer is in `resolve.value`; run `on_resolve` on the owning loop.
pub fn on_resolve_async(&mut self, resolve: &mut jsc_api::JSBundler::Resolve) {
// See `on_load_async` — must dispatch on the bundler's own loop.
match self.any_loop_mut() {
bun_event_loop::AnyEventLoop::Js { .. } => {
let ct = bun_event_loop::ConcurrentTask::ConcurrentTask::from_callback(
std::ptr::from_mut(resolve),
on_resolve_from_js_loop_raw,
);
let poster = self
.js_poster
.as_ref()
.expect("JS-owned bundle has a poster");
if let bun_event_loop::Posted::Refused(ct) = poster.post(ct) {
// Owning JS VM torn down mid-bundle: the hop never runs.
// SAFETY: refused ⇒ we own the task.
unsafe {
bun_event_loop::ConcurrentTask::ConcurrentTask::release_refused(ct)
};
}
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
// SAFETY: `resolve` is a valid &mut for the duration of the enqueue;
// the mini loop dispatches `on_resolve_mini` on the bundler thread.
unsafe {
mini.enqueue_task_concurrent_with_extra_ctx::<jsc_api::JSBundler::Resolve, BundleV2<'static>>(
std::ptr::from_mut(resolve),
on_resolve_mini,
core::mem::offset_of!(jsc_api::JSBundler::Resolve, task),
);
}
}
// SAFETY: as `on_load_async`.
unsafe {
self.post_to_own_loop(
std::ptr::from_mut(resolve),
on_resolve_from_js_loop_raw,
on_resolve_mini,
core::mem::offset_of!(jsc_api::JSBundler::Resolve, task),
);
}
}
}
Expand Down Expand Up @@ -4414,10 +4435,33 @@ pub mod bv2_impl {
Ok(())
}

fn on_notify_defer_mini(load: *mut jsc_api::JSBundler::Load, this: *mut BundleV2<'static>) {
// SAFETY: see `on_load_mini`.
BundleV2::on_notify_defer(unsafe { &mut *load }, unsafe { &mut *this });
}

fn on_notify_defer_from_js_loop_raw(
load: *mut jsc_api::JSBundler::Load,
) -> bun_event_loop::JsResult<()> {
// SAFETY: `load` is a valid pointer set up by `from_callback`.
let load = unsafe { &mut *load };
// SAFETY: `bv2` is a live backref set in `Load::init`.
let bv2 = unsafe { &mut *load.bv2 };
BundleV2::on_notify_defer(load, bv2);
Ok(())
}

impl<'a> BundleV2<'a> {
pub(crate) fn on_load(load: &mut jsc_api::JSBundler::Load, this: &mut BundleV2) {
this.graph.outstanding_loads.unlink(load);
load.deferred = false;
if load.deferred {
// Answered (or failed) without waiting for its `.defer()`
// promise: bring the unit back so the answer below can hand it
// to the parse task or consume it like any other load's.
Comment thread
robobun marked this conversation as resolved.
Outdated
load.deferred = false;
this.graph.deferred_pending -= 1;
this.increment_scan_counter();
}
// `Load` is arena-allocated (no Drop); free its owned heap fields on every exit path.
struct LoadDeinitGuard(*mut jsc_api::JSBundler::Load);
impl Drop for LoadDeinitGuard {
Expand Down Expand Up @@ -6938,15 +6982,22 @@ pub mod bv2_impl {
}

impl<'a> BundleV2<'a> {
pub fn on_notify_defer(&mut self) {
self.thread_lock.assert_locked();
self.graph.deferred_pending += 1;
self.decrement_scan_counter();
}

pub fn on_notify_defer_mini(load: &mut jsc_api::JSBundler::Load, this: &mut BundleV2) {
/// Owning loop: `load`'s onLoad callback called `.defer()`. Park the
/// load's scan-counter unit in `deferred_pending` so the scan can reach
/// zero without it; `drain_deferred_tasks` moves it back when it does.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) fn on_notify_defer(load: &mut jsc_api::JSBundler::Load, this: &mut BundleV2) {
this.thread_lock.assert_locked();
// A `.defer()` issued after the callback already answered (or after
// `fail_outstanding_plugin_requests`) arrives after `on_load`, which
// disposed of the unit; the promise still settles with the next
// drain, if any.
Comment thread
robobun marked this conversation as resolved.
Outdated
if !load.outstanding.is_linked() {
return;
}
debug_assert!(!load.deferred, "Load::called_defer allows one .defer()");
load.deferred = true;
this.on_notify_defer();
this.graph.deferred_pending += 1;
this.decrement_scan_counter();
}

pub(crate) fn on_parse_task_complete(
Expand Down
72 changes: 11 additions & 61 deletions src/runtime/api/JSBundler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use bun_collections::{StringMap, StringSet};
use bun_core::MutableString;
use bun_core::Output;
use bun_core::{String as BunString, ZigString};
use bun_jsc::ConcurrentTask::ConcurrentTask;
use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult};
use bun_options_types::compile_target::CompileTarget;
use bun_options_types::schema::api; // bun.schema.api
Expand Down Expand Up @@ -1409,12 +1408,11 @@ pub mod js_bundler {
///
/// Centralises the `*mut BundleV2 → &mut` deref so the C++-called thunks
/// (`JSBundlerPlugin__onResolveAsync`, `on_defer`, `…__onLoadAsync`,
/// `…__addError`, `on_notify_defer_raw`) stay safe at the call site. `bv2`
/// is the back-reference set in `Resolve::init`/`Load::init`; the
/// `BundleV2` heap allocation outlives every plugin callback (owner-
/// creates-child, single-JS-thread). The `BundleV2` storage is heap-
/// disjoint from `Resolve`/`Load`, so the returned `&mut` does not alias
/// the caller's `&mut Resolve`/`&mut Load`.
/// `…__addError`) stay safe at the call site. `bv2` is the back-reference
/// set in `Resolve::init`/`Load::init`; the `BundleV2` heap allocation
/// outlives every plugin callback (owner-creates-child, single-JS-thread).
/// The `BundleV2` storage is heap-disjoint from `Resolve`/`Load`, so the
/// returned `&mut` does not alias the caller's `&mut Resolve`/`&mut Load`.
Comment thread
robobun marked this conversation as resolved.
Outdated
#[inline]
fn bv2_mut<'a>(bv2: *mut BundleV2<'static>) -> &'a mut BundleV2<'static> {
// SAFETY: see fn doc — live backref (owner-creates-child), single
Expand Down Expand Up @@ -1503,63 +1501,15 @@ pub mod js_bundler {
bstr::BStr::new(&self.path)
);

// Notify the *bundler thread* about the deferral. This will
// decrement the pending item counter and increment the deferred
// counter. Must land on `parse_task.ctx.loop()` (the loop running
// BundleV2), which is distinct from `js_loop_for_plugins()` (the
// plugin host's JS loop) when `Bun.build` runs the bundler on its
// own Mini event loop.
// SAFETY: parse_task.ctx and bv2 are valid backrefs; `r#loop()`
// points at a live `AnyEventLoop` owned by the bundle thread /
// runtime for the duration of the bundle.
unsafe {
let ctx = (*self.parse_task).ctx.expect("ParseTask.ctx unset");
// SAFETY: write provenance from `ParseTask::init`; bundle outlives plugin.
let any_loop = ctx
.assume_mut()
.r#loop()
.expect("BundleV2.linker.loop must be set before plugins run");
match &mut *any_loop.as_ptr() {
bun_event_loop::AnyEventLoop::Js { .. } => {
let ct =
ConcurrentTask::from_callback(ctx.as_mut_ptr(), on_notify_defer_raw);
let poster = (*ctx.as_mut_ptr())
.js_poster
.as_ref()
.expect("JS-owned bundle has a poster");
if let bun_event_loop::Posted::Refused(ct) = poster.post(ct) {
// Owning JS VM torn down mid-bundle: the notify never runs.
bun_event_loop::ConcurrentTask::ConcurrentTask::release_refused(ct);
}
}
bun_event_loop::AnyEventLoop::Mini(mini) => {
// `mini.enqueueTaskConcurrentWithExtraCtx(
// Load, BundleV2, this, BundleV2.onNotifyDeferMini, .task)`
mini.enqueue_task_concurrent_with_extra_ctx::<Load, BundleV2<'static>>(
std::ptr::from_mut::<Load>(self),
on_notify_defer_mini_wrap,
core::mem::offset_of!(Load, task),
);
}
}

Ok(bv2_plugin(self.bv2).append_defer_promise())
}
// Parks this load's scan-counter unit on the loop that owns the
// bundle (`BundleV2::on_notify_defer`); the promise is settled by
// `DeferredBatchTask` once the rest of the scan has drained.
Comment thread
robobun marked this conversation as resolved.
Outdated
let bv2 = self.bv2;
bv2_mut(bv2).on_defer_async(self);
Ok(bv2_plugin(bv2).append_defer_promise())
}
}

fn on_notify_defer_raw(ctx: *mut BundleV2<'static>) -> bun_event_loop::JsResult<()> {
bv2_mut(ctx).on_notify_defer();
Ok(())
}

fn on_notify_defer_mini_wrap(load: *mut Load, ctx: *mut BundleV2<'static>) {
// SAFETY: callback contract — `load` was passed as the `Context` arg to
// `enqueue_task_concurrent_with_extra_ctx`; `ctx` is the bundle-thread
// `BundleV2` backref the mini loop's tick supplies as `ParentContext`.
BundleV2::on_notify_defer_mini(unsafe { &mut *load }, unsafe { &mut *ctx });
}

/// # Safety
/// `load` must be the live `*mut Load` previously handed to C++ via
/// `Load::dispatch`, and `global` must be the plugin's owning
Expand Down
Loading