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
77 changes: 49 additions & 28 deletions src/bundler/BundleThread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,15 @@ pub trait CompletionStruct: Node + Send + 'static {
/// while it was still queued ([`free_released_unstarted`] then frees it).
fn try_start(&mut self) -> bool;
fn free_released_unstarted(this: *mut Self);
fn complete_on_bundle_thread(&mut self);
/// Hands the build (result and log stored) back to its owner, which may
/// free `*this` as soon as it is posted. Raw pointer, not `&mut self`: no
/// reference to `*this`, here or in the caller, may be live when that
/// happens.
///
/// # Safety
/// `this` is a started build the bundle thread owns; nothing on this
/// thread touches `*this` afterwards.
Comment thread
robobun marked this conversation as resolved.
unsafe fn complete_on_bundle_thread(this: *mut Self);
fn set_result(&mut self, result: BundleV2Result);
fn set_log(&mut self, log: bun_ast::Log);
fn set_transpiler(&mut self, this: *mut BundleV2<'_>);
Expand Down Expand Up @@ -225,24 +233,25 @@ impl<C: CompletionStruct> BundleThread<C> {
break;
}
// SAFETY: queue stores non-null *mut C pushed via enqueue(); owner keeps it alive
// until complete_on_bundle_thread() signals completion — unless it
// until complete_on_bundle_thread() hands it back — unless it
// released the build while it sat here (its VM went away).
if !unsafe { (*completion).try_start() } {
C::free_released_unstarted(completion);
continue;
}
// SAFETY: as above; started ⇒ the owner waits for us.
let completion = unsafe { &mut *completion };
// SAFETY: `generation` is only read/written on this (bundle) thread.
let generation = unsafe { (*instance).generation };
// `panic = "abort"` → a Rust panic on this thread enters the
// crash-handler hook and aborts the whole process.
// No `catch_unwind` — there is nothing to catch.
match Self::generate_in_new_thread(completion, generation) {
Ok(()) => {}
Err(err) => {
completion.set_result(BundleV2Result::Err(err));
completion.complete_on_bundle_thread();
//
// SAFETY: started ⇒ `*completion` is ours until it is handed
// back, which is the last thing either path does with it; the
// reborrow for `set_result` ends before that.
unsafe {
if let Err(err) = Self::generate_in_new_thread(completion, generation) {
(*completion).set_result(BundleV2Result::Err(err));
C::complete_on_bundle_thread(completion);
}
}
has_bundled = true;
Expand All @@ -264,8 +273,12 @@ impl<C: CompletionStruct> BundleThread<C> {
}

/// This is called from `Bun.build` in JavaScript.
fn generate_in_new_thread(
completion: &mut C,
///
/// # Safety
/// `completion` is a started build the bundle thread owns. On `Ok` it has
/// been handed back (and may be gone); on `Err` the caller hands it back.
Comment thread
robobun marked this conversation as resolved.
unsafe fn generate_in_new_thread(
completion: *mut C,
generation: bun_core::Generation,
) -> Result<(), crate::Error> {
let heap = Arena::new();
Expand All @@ -277,37 +290,45 @@ impl<C: CompletionStruct> BundleThread<C> {
ast_memory_store.push();

// Allocate + configure folded — see `create_and_configure_transpiler` doc.
let transpiler = completion.create_and_configure_transpiler(bump)?;
//
// SAFETY: fn contract; the reborrow ends with the call (the result
// borrows `bump`).
let transpiler = unsafe { (*completion).create_and_configure_transpiler(bump) }?;

transpiler.resolver.generation = generation;

// Construction + run delegated — see
// `init_and_run` doc. Reborrow `transpiler` through a raw ptr so
// `completion` can be borrowed again below.
// Construction + run delegated — see `init_and_run` doc. It consumes
// the `&'a mut`; the log read and the drop below go through this.
Comment thread
robobun marked this conversation as resolved.
let transpiler_ptr: *mut Transpiler<'_> = transpiler;
let run = completion.init_and_run(
// SAFETY: `transpiler` lives in `bump` for the duration of `heap`.
unsafe { &mut *transpiler_ptr },
bump,
// `WorkPool::get()` returns `&'static ThreadPool`; pass as raw so
// the impl can hand it to `BundleV2::init` (which stores `*mut`).
std::ptr::from_ref(bun_threading::work_pool::WorkPool::get()).cast_mut(),
);
// SAFETY: fn contract; `transpiler` lives in `bump` until `heap` drops.
let run = unsafe {
(*completion).init_and_run(
&mut *transpiler_ptr,
bump,
// `WorkPool::get()` returns `&'static ThreadPool`; pass as raw so
// the impl can hand it to `BundleV2::init` (which stores `*mut`).
Comment thread
robobun marked this conversation as resolved.
std::ptr::from_ref(bun_threading::work_pool::WorkPool::get()).cast_mut(),
)
};

// Straight-line teardown: log copy
// runs on both paths; `completeOnBundleThread` only on success (the error
// path's `set_result(Err)` + complete happens in `thread_main`). The
// `deinitWithoutFreeingArena` + wait-group drain live inside `init_and_run`
// (it owns `this`).
let mut out_log = bun_ast::Log::init();
// SAFETY: `transpiler.log` is the arena-allocated `*mut Log` set up by
// `configure_bundler`; valid for the lifetime of `heap`. Raw deref so the
// `&'a mut Transpiler` consumed by `init_and_run` above is not reborrowed.
// SAFETY: `transpiler.log` was installed by `create_and_configure_transpiler`
// and is live while the build is ours; raw because `init_and_run`
// consumed the `&'a mut`.
let _ = unsafe { (*(*transpiler_ptr).log).append_to_with_recycled(&mut out_log, true) }; // logger OOM-only
completion.set_log(out_log);
// SAFETY: fn contract; the reborrow ends with the call.
unsafe { (*completion).set_log(out_log) };

if run.is_ok() {
completion.complete_on_bundle_thread();
// SAFETY: result and log are stored and no reborrow is live;
// nothing below touches `*completion` (the teardown drops only this
// thread's memory).
unsafe { C::complete_on_bundle_thread(completion) };
}

ast_memory_store.pop();
Expand Down
20 changes: 13 additions & 7 deletions src/runtime/api/js_bundle_completion_task.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,8 +561,8 @@ impl JSBundleCompletionTask {

pub(crate) fn on_complete_anytask(ctx: *mut Self) -> bun_event_loop::JsResult<()> {
crate::jsc_hooks::ActiveHandle::Bundle(NonNull::new(ctx).expect("completion")).unregister();
// For the +1 taken by `complete_on_bundle_thread` enqueue.
// SAFETY: `ctx` is the live heap allocation; `adopt` consumes the prior +1 on Drop.
// The creation ref, which `complete_on_bundle_thread` posted back to us.
// SAFETY: `ctx` is the live heap allocation; `adopt` consumes that ref on Drop.
let _drop_ref = unsafe { bun_ptr::ScopedRef::<Self>::adopt(ctx) };
// SAFETY: `ctx` is the heap::alloc allocation registered in `task`,
// dispatched exactly once per task on the JS thread. Exclusive: the
Expand Down Expand Up @@ -1114,13 +1114,19 @@ impl CompletionStruct for JSBundleCompletionTask {
Ok(())
}

fn complete_on_bundle_thread(&mut self) {
unsafe fn complete_on_bundle_thread(this: *mut Self) {
// The bundle thread's last touch of this task and of the VM's memory:
// hand it back (always queued — the VM waits for it) and stop counting.
self.bundle_loop
.store(ptr::null_mut(), core::sync::atomic::Ordering::Release);
let handle = self.loop_handle.clone();
let this = std::ptr::from_mut::<Self>(self);
// The post carries the creation ref (`on_complete_anytask` releases it),
// so the JS thread may free `*this` once it lands: take the handle out first.
//
// SAFETY: trait contract — `this` is the started build, still ours.
let handle = unsafe {
(*this)
.bundle_loop
.store(ptr::null_mut(), core::sync::atomic::Ordering::Release);
(*this).loop_handle.clone()
};
let ct = jsc::ConcurrentTask::create(jsc::Task::init(this));
let jsc::vm_handle::Posted::Queued = handle.post_task(ct) else {
unreachable!("VM handle closed with a Bun.build outstanding");
Expand Down
Loading