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
97 changes: 32 additions & 65 deletions src/bundler/DeferredBatchTask.rs
Original file line number Diff line number Diff line change
@@ -1,84 +1,51 @@
//! This task is run once all parse and resolve tasks have been complete
//! and we have deferred onLoad plugins that we need to resume.
//! Posted to the plugins' JS thread when the scan has nothing left to do but
//! the onLoad callbacks that called `.defer()`: the runtime's dispatch arm
//! resolves their promises (`JSBundlerPlugin__drainDeferred`) and frees this.
Comment thread
robobun marked this conversation as resolved.
//!
//! It enqueues a task to be run on the JS thread which resolves the promise
//! for every onLoad callback which called `.defer()`.
//! It carries only the plugin handle. A plugin that answers without awaiting
//! its `.defer()` promise lets the pass finish, and free its `BundleV2`, while
//! this task is still queued, so nothing here may point back into the pass.
//! The handle itself outlives the task: `Bun.build` destroys it from the
//! completion task, which is posted to the same queue after this, and bake's
//! plugins live as long as the dev server.
Comment thread
robobun marked this conversation as resolved.

use core::ptr::NonNull;

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

#[derive(Default)]
pub struct DeferredBatchTask {
// Debug-only flag; zero-sized in release.
#[cfg(debug_assertions)]
running: bool,
plugins: NonNull<JSBundlerPlugin>,
}

impl bun_event_loop::Taskable for DeferredBatchTask {
const TAG: bun_event_loop::TaskTag = task_tag::BundleV2DeferredBatchTask;
/// Embedded in its `BundleV2`, which outlives the queue entry and owns
/// everything the drain would have touched; nothing to free.
unsafe fn release_unrun(_: *mut Self) {}
unsafe fn release_unrun(this: *mut Self) {
// SAFETY: fn contract — `this` is the box `schedule` queued.
unsafe { bun_core::heap::destroy(this) };
}
}

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);
}

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);
/// Bundle thread (the loop that owns `bv2`).
pub(crate) fn schedule(bv2: &mut BundleV2) {
let plugins = bv2
.plugins
.expect("a load deferred, so the pass has plugins");
let this = bun_core::heap::into_raw(Box::new(Self { plugins }));
let task = ConcurrentTask::create(Task::init(this));
if !bv2.enqueue_on_js_loop_for_plugins(task) {
// SAFETY: refused ⇒ the queue never took `this`; nothing else points at it.
unsafe { bun_core::heap::destroy(this) };
}
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;
}
/// JS thread, from the dispatch arm that owns the box.
pub fn plugins(&self) -> &JSBundlerPlugin {
// SAFETY: see the module doc — the handle is destroyed only after this
// task has run or been released.
unsafe { self.plugins.as_ref() }
}
}
13 changes: 10 additions & 3 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 @@ -53,7 +54,8 @@ pub struct Graph<'a> {
/// is "moved" into this counter (pending_items -= 1; deferred_pending += 1)
///
/// 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`
/// tasks will be run, and the count is "moved" back to `pending_items`;
/// a load answered before then moves its own back (`BundleV2::on_load`).
Comment thread
robobun marked this conversation as resolved.
pub(crate) deferred_pending: u32,

/// onResolve / onLoad requests a plugin currently holds (dispatched to its
Expand Down Expand Up @@ -243,8 +245,7 @@ impl<'a> Graph<'a> {
}
}

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

return true;
}
Expand Down Expand Up @@ -274,6 +275,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
Loading