Skip to content
72 changes: 16 additions & 56 deletions src/bundler/DeferredBatchTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
//! for every onLoad callback which called `.defer()`.

use crate::BundleV2;
use crate::bundle_v2::JSBundlerPlugin;
// Task is `(tag: u8, ptr: *mut ())` owned by bun_event_loop;
// runtime owns the match-loop. See PORTING.md §Dispatch.
use bun_event_loop::ConcurrentTask::ConcurrentTask;
use bun_event_loop::{Task, task_tag};
use core::ptr::NonNull;

/// Embedded in the pass as `BundleV2::drain_defer_task`.
#[derive(Default)]
pub struct DeferredBatchTask {
// Debug-only flag; zero-sized in release.
#[cfg(debug_assertions)]
running: bool,
/// `BundleV2::plugins`, copied by `schedule`: `run_on_js_thread` runs on
/// the plugins' thread, which must not reach into the pass.
Comment thread
robobun marked this conversation as resolved.
Outdated
plugins: Option<NonNull<JSBundlerPlugin>>,
}

impl bun_event_loop::Taskable for DeferredBatchTask {
Expand All @@ -25,60 +28,17 @@ impl bun_event_loop::Taskable for DeferredBatchTask {
}

impl DeferredBatchTask {
pub(crate) fn init(&mut self) {
// Kept as `&mut self` (not `-> Self`) — this struct is embedded
// by value in BundleV2 (recovered via container_of in `get_bundle_v2`), so
// it is reset in place, never separately constructed.
#[cfg(debug_assertions)]
debug_assert!(!self.running);
// No Drop / no owned fields — pure reset.
let _ = core::mem::take(self);
/// Bundle thread.
pub(crate) fn schedule(bv2: &mut BundleV2<'_>) {
bv2.drain_defer_task.plugins = bv2.plugins;
let task = ConcurrentTask::create(Task::init(std::ptr::from_mut::<Self>(
&mut bv2.drain_defer_task,
)));
bv2.enqueue_on_js_loop_for_plugins(task);
}

pub(crate) fn get_bundle_v2(&mut self) -> &mut BundleV2<'static> {
// SAFETY: `self` is always the `drain_defer_task` field of a live `BundleV2`;
// this struct is never instantiated standalone. Lifetime erased to 'static;
// callers must not outlive the owning bundle.
unsafe {
&mut *bun_core::from_field_ptr!(
BundleV2<'static>,
drain_defer_task,
std::ptr::from_mut::<Self>(self)
)
}
}

pub(crate) fn schedule(&mut self) {
#[cfg(debug_assertions)]
{
debug_assert!(!self.running);
self.running = false;
}
let task = ConcurrentTask::create(Task::init(std::ptr::from_mut::<Self>(self)));

self.get_bundle_v2().enqueue_on_js_loop_for_plugins(task);
}

pub fn run_on_js_thread(&mut self) {
// `deinit` only resets
// the debug `running` flag; nothing follows `drain_deferred`, so
// resetting the flag afterwards covers both paths.
{
let bv2 = self.get_bundle_v2();
let rejected = bv2.completion.map(|c| c.result_is_err()).unwrap_or(false);
// The void result is discarded — see
// `Plugin::drain_deferred` for the exception-scope note.
bv2.plugins_mut().expect("plugins").drain_deferred(rejected);
}
self.deinit();
}

// Not `impl Drop` — this struct is an intrusive field of `BundleV2`
// and `deinit` is a debug-flag reset, not resource teardown.
fn deinit(&mut self) {
#[cfg(debug_assertions)]
{
self.running = false;
}
/// Plugins' JS thread.
pub fn run_on_js_thread(&self) {
JSBundlerPlugin::opaque_mut(self.plugins.expect("plugins").as_ptr()).drain_deferred();
}
}
4 changes: 2 additions & 2 deletions src/bundler/Graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use bun_ast::server_component_boundary;
use bun_collections::MultiArrayList;
use enum_map::EnumMap;

use crate::DeferredBatchTask::DeferredBatchTask;
use crate::IndexStringMap::IndexStringMap;
use crate::PathToSourceIndexMap::PathToSourceIndexMap;
use crate::options;
Expand Down Expand Up @@ -243,8 +244,7 @@ impl<'a> Graph<'a> {
}
}

transpiler.drain_defer_task.init();
transpiler.drain_defer_task.schedule();
DeferredBatchTask::schedule(transpiler);

return true;
}
Expand Down
91 changes: 36 additions & 55 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,15 +205,6 @@ impl<'a> BundleV2<'a> {
self.plugins.map(|p| unsafe { p.as_ref() })
}

/// Mutable projection of the `plugins` backref for FFI calls that take
/// `*mut` (`drain_deferred`). The pointee is disjoint from `self` storage.
#[inline]
pub(crate) fn plugins_mut(&mut self) -> Option<&mut JSBundlerPlugin> {
// SAFETY: BACKREF — see `plugins_ref`. `&mut self` ensures no other
// `&JSBundlerPlugin` projection from this `BundleV2` overlaps.
self.plugins.map(|mut p| unsafe { p.as_mut() })
}

/// Mutable projection of the `bun_watcher` backref for `Watcher::add_file`.
/// Centralises the two open-coded `unsafe { ptr.as_mut() }` sites so the
/// liveness/exclusivity argument lives in one place.
Expand Down Expand Up @@ -738,7 +729,7 @@ pub mod bv2_impl {
kind: u8,
);
#[link_name = "JSBundlerPlugin__drainDeferred"]
safe fn JSBundlerPlugin__drainDeferred(this: &mut Plugin, rejected: bool);
safe fn JSBundlerPlugin__drainDeferred(this: &mut Plugin);
#[link_name = "JSBundlerPlugin__hasOnBeforeParsePlugins"]
safe fn JSBundlerPlugin__hasOnBeforeParsePlugins(this: &Plugin) -> i32;
// `ctx`/`args`/`result` are opaque cookies the C++ side round-trips
Expand All @@ -765,8 +756,8 @@ pub mod bv2_impl {
/// only bundler caller (`DeferredBatchTask::run_on_js_thread`)
/// ignores failures, so the void FFI call is the observable
/// behaviour at this tier.
pub(crate) fn drain_deferred(&mut self, rejected: bool) {
JSBundlerPlugin__drainDeferred(self, rejected)
pub(crate) fn drain_deferred(&mut self) {
JSBundlerPlugin__drainDeferred(self)
}

#[inline]
Expand Down Expand Up @@ -1083,7 +1074,11 @@ pub mod bv2_impl {
/// are the real lower-tier `bun_event_loop` types, so `dispatch()` /
/// `run_on_js_thread()` are implemented inherently (no T6 hook).
pub struct Resolve {
/// Only for the pass's own loop (`dispatch()`, the answer);
/// `run_on_js_thread` runs on the plugins' thread and uses `plugins`.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub bv2: *mut BundleV2<'static>,
/// `BundleV2::plugins` as of `init`.
pub(crate) plugins: Option<core::ptr::NonNull<Plugin>>,
pub import_record: MiniImportRecord,
pub value: ResolveValue,
/// `jsc.AnyEventLoop.Task` — intrusive node for the Mini-loop queue.
Expand All @@ -1096,6 +1091,7 @@ pub mod bv2_impl {
fn default() -> Self {
Self {
bv2: core::ptr::null_mut(),
plugins: None,
import_record: MiniImportRecord::default(),
value: ResolveValue::Pending,
task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext::default(),
Expand All @@ -1116,6 +1112,7 @@ pub mod bv2_impl {
// SAFETY: lifetime erased — Resolve is owned by the dispatch
// chain and never outlives `bv2`.
bv2: std::ptr::from_mut::<BundleV2<'_>>(bv2).cast::<BundleV2<'static>>(),
plugins: bv2.plugins,
import_record: record,
value: ResolveValue::Pending,
task: bun_event_loop::AnyTaskWithExtraContext::AnyTaskWithExtraContext::default(),
Expand Down Expand Up @@ -1143,25 +1140,19 @@ pub mod bv2_impl {
bv2.enqueue_on_js_loop_for_plugins(task);
}
}
/// Plugins' JS thread.
pub fn run_on_js_thread(&mut self) {
let kind = self.import_record.kind;
// reshaped for borrowck — capture the erased self
// pointer before borrowing fields immutably for the FFI call.
let self_ptr = std::ptr::from_mut::<Self>(self).cast::<core::ffi::c_void>();
// SAFETY: `bv2` is a valid backref set by `init`; the plugin
// storage is disjoint from `self`, so the `&mut JSBundlerPlugin`
// returned by `plugins_mut()` does not alias the
// `&self.import_record.*` borrows below.
unsafe { &mut *self.bv2 }
.plugins_mut()
.expect("plugins")
.match_on_resolve(
&self.import_record.specifier,
&self.import_record.namespace,
&self.import_record.source_file,
self_ptr,
kind,
);
Plugin::opaque_mut(self.plugins.expect("plugins").as_ptr()).match_on_resolve(
&self.import_record.specifier,
&self.import_record.namespace,
&self.import_record.source_file,
self_ptr,
kind,
);
}
}

Expand Down Expand Up @@ -1190,7 +1181,10 @@ pub mod bv2_impl {

/// Task driving an onLoad plugin invocation for one source file.
pub struct Load {
/// See `Resolve::bv2`.
pub bv2: *mut BundleV2<'static>,
/// `BundleV2::plugins` as of `init`.
pub(crate) plugins: Option<core::ptr::NonNull<Plugin>>,
pub(crate) source_index: bun_ast::Index,
pub(crate) default_loader: Loader,
pub path: Box<[u8]>,
Expand Down Expand Up @@ -1219,6 +1213,7 @@ pub mod bv2_impl {
.unwrap_or(Loader::Js);
Self {
bv2: std::ptr::from_mut::<BundleV2<'_>>(bv2).cast::<BundleV2<'static>>(),
plugins: bv2.plugins,
parse_task: bun_ptr::BackRef::new_mut(parse),
source_index: parse.source_index,
default_loader,
Expand Down Expand Up @@ -1278,26 +1273,20 @@ pub mod bv2_impl {
bv2.enqueue_on_js_loop_for_plugins(concurrent_task);
}
}
/// Plugins' JS thread.
pub fn run_on_js_thread(&mut self) {
let is_server_side = self.bake_graph() != crate::bake_types::Graph::Client;
let default_loader = self.default_loader;
// reshaped for borrowck — capture the erased self
// pointer before borrowing fields immutably for the FFI call.
let self_ptr = std::ptr::from_mut::<Self>(self).cast::<core::ffi::c_void>();
// SAFETY: `bv2` is a valid backref set by `init`; the plugin
// storage is disjoint from `self`, so the `&mut JSBundlerPlugin`
// returned by `plugins_mut()` does not alias the
// `&self.path` / `&self.namespace` borrows below.
unsafe { &mut *self.bv2 }
.plugins_mut()
.expect("plugins")
.match_on_load(
&self.path,
&self.namespace,
self_ptr,
default_loader,
is_server_side,
);
Plugin::opaque_mut(self.plugins.expect("plugins").as_ptr()).match_on_load(
&self.path,
&self.namespace,
self_ptr,
default_loader,
is_server_side,
);
}
}
impl bun_event_loop::Taskable for Load {
Expand Down Expand Up @@ -1420,13 +1409,11 @@ pub mod bv2_impl {

/// CYCLEBREAK GENUINE: `JSBundleCompletionTask` — the
/// concrete struct lives in `bun_runtime` (its fields name `Config`/
/// `Plugin`/`HTMLBundle::Route`). The bundler reads exactly two things
/// from it (whether the result is an error, and the concurrent-task
/// `Plugin`/`HTMLBundle::Route`). The bundler needs exactly two things
/// from it (whether it was cancelled, and the concurrent-task
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
/// enqueue), so the high tier hands the bundler an erased owner +
/// `&'static` vtable pair (same shape as [`DevServerHandle`]).
pub struct CompletionDispatch {
/// Whether the completion result is an error.
pub result_is_err: unsafe fn(core::ptr::NonNull<super::JSBundleCompletionTask>) -> bool,
/// Whether the VM that owns the plugins is shutting down: stop
/// waiting for their answers and fail the build (any thread).
pub is_cancelled: unsafe fn(core::ptr::NonNull<super::JSBundleCompletionTask>) -> bool,
Expand All @@ -1443,22 +1430,16 @@ pub mod bv2_impl {
pub vtable: &'static CompletionDispatch,
}
// SAFETY: erased `*mut JSBundleCompletionTask` backref — set by the JS
// thread, read by the bundle thread; `enqueue_task_concurrent` is the only
// cross-thread call and it goes through `jsc::EventLoop`'s lock-free queue.
// thread, read by the bundle thread; the two cross-thread calls are an
// atomic load (`is_cancelled`) and a push onto `jsc::EventLoop`'s
// lock-free queue (`enqueue_task_concurrent`).
Comment thread
robobun marked this conversation as resolved.
unsafe impl Send for CompletionHandle {}
// Intentionally not `Sync`: the opaque owner (`JSBundleCompletionTask`)
// is modeled as `!Sync`, and this wrapper exposes `result_is_err(&self)`
// in addition to the lock-free enqueue path, so blanket `&CompletionHandle`
// sharing across threads is not justified. The handle only needs to *move*
// to the bundle thread (`Send`), not be shared. If a cross-thread `&` ever
// is modeled as `!Sync`, and the handle only needs to *move* to the
// bundle thread (`Send`), not be shared. If a cross-thread `&` ever
Comment thread
robobun marked this conversation as resolved.
// becomes necessary, split out an enqueue-only wrapper and make only that
// type `Sync`.
impl CompletionHandle {
#[inline]
pub(crate) fn result_is_err(&self) -> bool {
// SAFETY: vtable contract.
unsafe { (self.vtable.result_is_err)(self.owner) }
}
#[inline]
pub(crate) fn is_cancelled(&self) -> bool {
// SAFETY: vtable contract.
Expand Down
8 changes: 2 additions & 6 deletions src/jsc/bindings/JSBundlerPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ extern "C" void JSBundlerPlugin__setConfig(Bun::JSBundlerPlugin* plugin, void* c
plugin->plugin.config = config;
}

extern "C" void JSBundlerPlugin__drainDeferred(Bun::JSBundlerPlugin* pluginObject, bool rejected)
extern "C" void JSBundlerPlugin__drainDeferred(Bun::JSBundlerPlugin* pluginObject)
{
auto* globalObject = pluginObject->globalObject();
MarkedArgumentBuffer arguments;
Expand All @@ -676,11 +676,7 @@ extern "C" void JSBundlerPlugin__drainDeferred(Bun::JSBundlerPlugin* pluginObjec
auto scope = DECLARE_THROW_SCOPE(vm);
for (auto promiseValue : arguments) {
JSPromise* promise = uncheckedDowncast<JSPromise>(promiseValue);
if (rejected) {
promise->reject(vm, JSC::jsUndefined());
} else {
promise->resolve(globalObject, vm, JSC::jsUndefined());
}
promise->resolve(globalObject, vm, JSC::jsUndefined());
RETURN_IF_EXCEPTION(scope, );
}
RETURN_IF_EXCEPTION(scope, );
Expand Down
1 change: 0 additions & 1 deletion src/runtime/api/js_bundle_completion_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,6 @@ fn from_completion_handle<'a>(c: NonNull<Bv2OpaqueCompletion>) -> &'a JSBundleCo
}

static COMPLETION_VTABLE: dispatch::CompletionDispatch = dispatch::CompletionDispatch {
result_is_err: |c| matches!(from_completion_handle(c).result, BundleV2Result::Err(_)),
is_cancelled: |c| {
from_completion_handle(c)
.cancelled
Expand Down
Loading
Loading