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
48 changes: 47 additions & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,44 @@ impl Drop for AutoGcOnDrop<'_> {
#[must_use = "macro mode is disabled on drop; bind to a named local"]
pub struct MacroModeGuard {
vm: bun_ptr::BackRef<VirtualMachine>,
module_queue: Option<NestedModuleQueueScope>,
}

bun_opaque::opaque_ffi! {
/// `Bun::NestedModuleQueue` in ModuleLoader.cpp.
pub struct NestedModuleQueue;
}

unsafe extern "C" {
// Null when no `require()` of an ES module is in progress, or when a nested queue is already the VM's current one.
safe fn Bun__NestedModuleQueue__push(vm: &VM) -> *mut NestedModuleQueue;
safe fn Bun__NestedModuleQueue__flush(global: &JSGlobalObject) -> bool;
/// Frees `queue`, which must be the VM's current queue.
fn Bun__NestedModuleQueue__pop(vm: &VM, queue: *mut NestedModuleQueue);
}

/// While a `require()` of an ES module drains `VM::m_synchronousModuleQueue`, puts a queue in front of it for an asynchronous module load that has to finish before that drain continues. [`EventLoop::wait_for_promise`] hands what lands there to the microtask queue.
#[must_use = "the queue is popped on drop; bind to a named local"]
pub struct NestedModuleQueueScope {
vm: bun_ptr::BackRef<VM>,
queue: NonNull<NestedModuleQueue>,
}
impl NestedModuleQueueScope {
pub fn enter(vm: &VM) -> Option<Self> {
let queue = NonNull::new(Bun__NestedModuleQueue__push(vm))?;
Some(Self {
vm: bun_ptr::BackRef::from(NonNull::from(vm)),
queue,
})
}
}
impl Drop for NestedModuleQueueScope {
fn drop(&mut self) {
// SAFETY: `queue` came from `Bun__NestedModuleQueue__push` in `enter`. Every queue pushed after it (a nested scope's, a nested `require()`'s) is popped by now.
unsafe { Bun__NestedModuleQueue__pop(self.vm.get(), self.queue.as_ptr()) };
}
}

impl MacroModeGuard {
/// `vm` must be the live per-thread `VirtualMachine` (the [`BackRef`]
/// invariant: the VM outlives any guard it hands out). Mutation routes
Expand All @@ -821,14 +858,18 @@ impl MacroModeGuard {
if vm_mut.macro_guard_depth == 1 {
vm_mut.enable_macro_mode();
}
Self { vm }
Self {
vm,
module_queue: NestedModuleQueueScope::enter(vm_mut.jsc_vm()),
}
}
}
impl Drop for MacroModeGuard {
#[inline]
fn drop(&mut self) {
// Per `new` contract — `vm` outlives the guard (BackRef invariant).
let vm_mut = self.vm.get().as_mut();
self.module_queue = None;
vm_mut.macro_guard_depth = vm_mut.macro_guard_depth.saturating_sub(1);
if vm_mut.macro_guard_depth == 0 {
vm_mut.disable_macro_mode();
Expand Down Expand Up @@ -1528,6 +1569,11 @@ impl VirtualMachine {
self.transpiler_store.enabled = true;
}

/// Hands what is parked in the innermost [`NestedModuleQueueScope`]'s queue to the microtask queue. `true` when there was any.
pub(crate) fn flush_nested_module_queue(&self) -> bool {
Bun__NestedModuleQueue__flush(self.global())
}

pub fn enqueue_task(&mut self, task: bun_event_loop::Task) {
// accessed here (no overlapping `&mut EventLoop`).
self.event_loop_mut().enqueue_task(task);
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/BunClientData.h
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ class JSVMClientData : public JSC::VM::ClientData {
// after every swap.
WTF::UncheckedKeyHashMap<WTF::String, RefPtr<JSC::SourceProvider>> isolationSourceProviderCache;

// The innermost queue that Bun__NestedModuleQueue__push (ModuleLoader.cpp) put on vm.m_synchronousModuleQueue, or null.
JSC::VM::SynchronousModuleQueue* nestedModuleQueue { nullptr };

JSC::DecoderStringTable* decoderStringTable() final { return m_decoderStringTable.get(); }
void setDecoderStringTable(std::span<const uint8_t>);

Expand Down
52 changes: 52 additions & 0 deletions src/jsc/bindings/ModuleLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "JSEventEmitter.h"

#include <JavaScriptCore/JSModuleLoader.h>
#include <JavaScriptCore/MicrotaskQueueInlines.h>
#include <JavaScriptCore/ModuleRegistryEntry.h>
#include <JavaScriptCore/Completion.h>
#include <JavaScriptCore/JSModuleNamespaceObject.h>
Expand Down Expand Up @@ -1234,6 +1235,57 @@ JSValue fetchESMSourceCodeAsync(

using namespace Bun;

void Bun::requeueSynchronousModuleQueueAsMicrotasks(JSC::JSGlobalObject* globalObject, JSC::VM::SynchronousModuleQueue& queue)
{
auto& vm = JSC::getVM(globalObject);
for (auto& task : queue.tasks)
globalObject->queueMicrotask(vm, task.task, task.payload, task.arg0, task.arg1, task.arg2, task.arg3);
queue.tasks.shrink(0);
}

namespace Bun {
// A queue on the vm.m_synchronousModuleQueue chain for an asynchronous module load that has to finish inside a require() of an ES module (a macro's own module), not for that require().
struct NestedModuleQueue {
WTF_DEPRECATED_MAKE_FAST_ALLOCATED(NestedModuleQueue);

public:
JSC::VM::SynchronousModuleQueue queue;
JSC::VM::SynchronousModuleQueue* outer { nullptr };
};
}

extern "C" NestedModuleQueue* Bun__NestedModuleQueue__push(JSC::VM* vm)
{
auto* current = vm->m_synchronousModuleQueue;
auto* clientData = WebCore::clientData(*vm);
if (!current || current == clientData->nestedModuleQueue)
return nullptr;
auto* nested = new NestedModuleQueue;
nested->queue.prev = current; // VM::visitAggregateImpl marks what the require() parked through this link.
nested->outer = std::exchange(clientData->nestedModuleQueue, &nested->queue);
vm->m_synchronousModuleQueue = &nested->queue;
return nested;
}

extern "C" bool Bun__NestedModuleQueue__flush(Zig::GlobalObject* globalObject)
{
auto* queue = WebCore::clientData(JSC::getVM(globalObject))->nestedModuleQueue;
if (!queue || queue->tasks.isEmpty())
return false;
Bun::requeueSynchronousModuleQueueAsMicrotasks(globalObject, *queue);
return true;
}

// What is still parked goes to the require()'s queue, where it would have gone without the nested one.
extern "C" void Bun__NestedModuleQueue__pop(JSC::VM* vm, NestedModuleQueue* nested)
{
ASSERT(vm->m_synchronousModuleQueue == &nested->queue);
nested->queue.prev->tasks.appendVector(nested->queue.tasks);
vm->m_synchronousModuleQueue = nested->queue.prev;
WebCore::clientData(*vm)->nestedModuleQueue = nested->outer;
delete nested;
}

BUN_DEFINE_HOST_FUNCTION(jsFunctionEvictIsolationSourceProviderCache, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = JSC::getVM(globalObject);
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/ModuleLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ JSValue fetchESMSourceCodeAsync(
BunString* referrer,
BunString* typeAttribute);

// Queues every task parked in `queue` as a microtask, where each goes when no require() of an ES module diverts it.
void requeueSynchronousModuleQueueAsMicrotasks(JSC::JSGlobalObject*, JSC::VM::SynchronousModuleQueue&);

JSValue fetchCommonJSModule(
Zig::GlobalObject* globalObject,
JSCommonJSModule* moduleObject,
Expand Down
5 changes: 4 additions & 1 deletion src/jsc/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,10 @@ impl EventLoop {
return Err(jsc::Stopped);
}
self.tick();
if promise.status() == PromiseStatus::Pending {
// What a nested module load parked is work for the next tick, not something to poll for.
if promise.status() == PromiseStatus::Pending
&& !self.vm_ref().flush_nested_module_queue()
{
self.auto_tick();
}
}
Expand Down
Loading
Loading