Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion docs/runtime/ffi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ To track when a `TypedArray` is no longer in use from JavaScript, use a [Finaliz

#### From C, Rust, Zig, etc

To track when a `TypedArray` is no longer in use from C or FFI, pass a callback and an optional context pointer to `toArrayBuffer` or `toBuffer`. Bun calls the callback later, once the garbage collector frees the underlying `ArrayBuffer` JavaScript object.
To track when a `TypedArray` is no longer in use from C or FFI, pass a callback and an optional context pointer to `toArrayBuffer` or `toBuffer`. Bun calls the callback later, once the garbage collector frees the underlying `ArrayBuffer` JavaScript object. The callback may run on a garbage-collector thread rather than the JavaScript thread, so it must not call back into JavaScript or assume thread-local state.

The expected signature is the same as in [JavaScriptCore's C API](https://developer.apple.com/documentation/javascriptcore/jstypedarraybytesdeallocator?language=objc):

Expand Down
11 changes: 9 additions & 2 deletions packages/bun-usockets/src/eventing/epoll_kqueue.c
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,8 @@ void us_loop_run(struct us_loop_t *loop) {
}
}

extern unsigned int Bun__JSC_onBeforeWait(void * _Nonnull jsc_vm, uint64_t now_ns);
extern unsigned int Bun__JSC_onBeforeWait(void * _Nonnull jsc_vm, uint64_t now_ns, int * _Nullable released_heap_access);
extern void Bun__JSC_acquireHeapAccessAfterWait(void * _Nonnull jsc_vm);

void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout, uint64_t now_ns) {
if (loop->num_polls == 0)
Expand Down Expand Up @@ -504,8 +505,9 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout
/* `now_ns` is the reading the JS side took to pick `timeout`
* (timer::All::get_timeout), reused here to rate-limit the idle sweep; 0
* if it had none to share. Nothing measures a deadline against it. */
int released_heap_access = 0;
if (will_idle_inside_event_loop && loop->data.jsc_vm)
(void) Bun__JSC_onBeforeWait(loop->data.jsc_vm, now_ns);
(void) Bun__JSC_onBeforeWait(loop->data.jsc_vm, now_ns, &released_heap_access);

/* The scavenger sweeps our heaps while we are in the kernel. Must come after
* Bun__JSC_onBeforeWait, which allocates: nothing may touch our heaps until the matching
Expand Down Expand Up @@ -541,9 +543,14 @@ void us_loop_run_bun_tick(struct us_loop_t *loop, const struct timespec* timeout
timeout);
#endif

/* Anything that stops a poll from here on scrubs this batch (us_internal_loop_update_pending_ready_polls). */
loop->current_ready_poll = 0;
/* Before anything can allocate again. */
if (handed_off)
mi_on_thread_idle_end();
/* Before anything touches the JS heap again (this may run a finished collection's epilogue, i.e. destructors). */
if (released_heap_access)
Bun__JSC_acquireHeapAccessAfterWait(loop->data.jsc_vm);

us_internal_dispatch_ready_polls(loop);
us_internal_drain_ready_polls(loop);
Expand Down
4 changes: 2 additions & 2 deletions packages/bun-usockets/src/eventing/libuv.c
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ void us_loop_free(struct us_loop_t *loop) {
us_free(loop);
}

extern unsigned int Bun__JSC_onBeforeWait(void *jsc_vm, uint64_t now_ns);
extern unsigned int Bun__JSC_onBeforeWait(void *jsc_vm, uint64_t now_ns, int *released_heap_access);

static void idle_sweep_again_cb(struct us_timer_t *t) {}

Expand All @@ -384,7 +384,7 @@ void us_loop_run(struct us_loop_t *loop) {
/* uv_update_time() above just refreshed libuv's cached monotonic clock, so
* uv_now() reads that cache rather than taking the clock again. */
const unsigned int run_again_ms =
Bun__JSC_onBeforeWait(loop->data.jsc_vm, (uint64_t) uv_now(loop->uv_loop) * 1000000ULL);
Bun__JSC_onBeforeWait(loop->data.jsc_vm, (uint64_t) uv_now(loop->uv_loop) * 1000000ULL, NULL);
if (run_again_ms) {
/* The allocator's idle sweep wants another turn even if nothing else ends
* the poll. Unref'd: it bounds the poll, it does not keep the loop alive.
Expand Down
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "63a807e88ce0f37e44c8bb6820158a74fec39939";
export const WEBKIT_VERSION = "564ac2a6cad8da6a358f3d331690e1ec65e5d67e";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
23 changes: 15 additions & 8 deletions src/jsc/GarbageCollectionController.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ impl GarbageCollectionController {
self.gc_last_heap_size.set(vm.block_bytes_allocated());
}

/// `Tag::GcRepeating` fire body: `BUN_GC_TIMER_INTERVAL` (default 1 s) in fast mode, 30 s in slow mode; drops to slow after 30 fires with no heap growth, back to fast when it grows or an idle full collection was requested.
/// `Tag::GcRepeating` fire body: `BUN_GC_TIMER_INTERVAL` (default 1 s) in fast mode, 30 s in slow mode; drops to slow after 30 fires without heap growth beyond the chatter slack, back to fast when it grows.
///
/// # Safety
/// `this` is the live per-VM controller; `vm` is the per-thread VM.
Expand All @@ -201,17 +201,24 @@ impl GarbageCollectionController {
if this.disabled.get() {
return;
}
// Timer chatter in a parked app churns a few blocks per tick; real work grows the heap by far more.
const IDLE_GROWTH_SLACK: usize = 2 * 1024 * 1024;
// Timer chatter in a parked app churns a few blocks per tick, a few MB per 30 s tick with nothing collecting in
// between (a TUI at its prompt: 70-200 KB/s); real work grows the heap by far more (one request: 15 MB and up).
const IDLE_GROWTH_SLACK_MIN: usize = 2 * 1024 * 1024;
const IDLE_GROWTH_SLACK_PER_MS: usize = 256; // 256 KB/s: 7.5 MB over the 30 s tick
let interval = this.repeat_interval();
let idle_growth_slack =
IDLE_GROWTH_SLACK_MIN.max(interval.max(0) as usize * IDLE_GROWTH_SLACK_PER_MS);
let prev_heap_size = this.gc_last_heap_size.get();
// SAFETY: per fn contract.
let vm_ref = unsafe { &*vm };
let grew = vm_ref.jsc_vm().block_bytes_allocated() > prev_heap_size + IDLE_GROWTH_SLACK;
let (full, idle_gc_due_in) = this.idle_tick(vm_ref, grew, this.repeat_interval());
let grew = vm_ref.jsc_vm().block_bytes_allocated() > prev_heap_size + idle_growth_slack;
let (full, idle_gc_due_in) = this.idle_tick(vm_ref, grew, interval);
this.perform_gc(full);
// Only growth is activity; a shrinking heap is a collection (possibly the one requested above) doing its job.
// An idle full collection proceeds at this timer's ticks in a program that runs no JS: fast ones for the next 30.
if !full && this.gc_last_heap_size.get() <= prev_heap_size {
// Growth is activity; a shrinking heap is a collection (possibly the one requested above) doing its job. Where the
// event loop cannot let an idle collection finish while parked (Windows: Bun__JSC_onBeforeWait), it proceeds at
// this timer's ticks instead, fast ones for the next 30.
let needs_ticks_to_finish = full && cfg!(windows);
if !grew && !needs_ticks_to_finish {
let ticks = this
.heap_size_didnt_change_for_repeating_timer_ticks_count
.get()
Expand Down
10 changes: 10 additions & 0 deletions src/jsc/VmHandle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,16 @@ pub unsafe extern "C" fn Bun__VmHandle__refKeepAlive(
unsafe { VmHandle::borrow_ref(r) }.add_keep_alive(kind, delta.signum());
}

/// Any thread: wake the VM's loop (no-op once closed).
///
/// # Safety
/// `r` is a live reference its holder keeps for the duration of the call.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn Bun__VmHandle__wake(r: *const Shared) {
// SAFETY: fn contract.
unsafe { VmHandle::borrow_ref(r) }.wake()
}

/// Any thread: Node's `can_call_into_js()`.
///
/// # Safety
Expand Down
12 changes: 12 additions & 0 deletions src/jsc/bindings/BunClientData.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// VM's own uses to post work to it, keep its loop alive, or ask whether it may still run
// script. retain / retainRef take a count, release gives one up; valid however long it is held.
#include "BunLoopKind.h"
#include <wtf/SharedTask.h>
struct BunVmHandleRef;
extern "C" const BunVmHandleRef* Bun__VmHandle__retain(void* bunVM); // JS thread
extern "C" const BunVmHandleRef* Bun__VmHandle__retainRef(const BunVmHandleRef*); // any thread
Expand All @@ -28,6 +29,7 @@ class EventLoopTask;
// to outlive a lock).
extern "C" void Bun__VmHandle__postAndRelease(const BunVmHandleRef*, WebCore::EventLoopTask*, BunLoopKind);
extern "C" void Bun__VmHandle__refKeepAlive(const BunVmHandleRef*, BunLoopKind, int delta);
extern "C" void Bun__VmHandle__wake(const BunVmHandleRef*); // any thread
// Node's can_call_into_js(): false once the VM's stop was requested (terminate()/exit/teardown). Any thread.
extern "C" bool Bun__VmHandle__scriptAllowed(const BunVmHandleRef*);
// The handle's state byte, so hot paths test it inline (BUN_VM_HANDLE_STATE_OPEN == bun_jsc::vm_handle::State::Open).
Expand Down Expand Up @@ -246,6 +248,16 @@ class JSVMClientData : public JSC::VM::ClientData {
// Live size of the heap as measured by the most recent collection, eden or full.
size_t heapSizeAfterLastCollection() const { return m_heapSizeAfterLastCollection.get(); }

// The VM's default (first) Zig::GlobalObject: what defaultGlobalObject(JSC::VM&) returns on threads whose thread-local
// default is not this VM's, e.g. the collector thread running a collection's end phase. gcProtect'ed for the VM's life.
JSC::JSGlobalObject* defaultGlobalObject { nullptr };

// Idle full collections GarbageCollectionController requested that have not finished (see
// Bun__JSC_onBeforeWait). Counted up on the JS thread, down at the end of each one's end phase, on either thread.
std::atomic<unsigned> idleCollectionsPending { 0 };
// Their GCRequest::didFinishEndPhase, made once (JSC__VM__collectAsyncIdle).
RefPtr<WTF::SharedTask<void()>> idleCollectionDidFinish;

void* bunVM;
// Opaque box of the Rust VmHandle for this VM: what any *other* thread uses
// to post work / ref the loop (never bunVM). Created in create(), released
Expand Down
21 changes: 20 additions & 1 deletion src/jsc/bindings/BunJSCEventLoop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,21 @@ extern "C" uint64_t us_internal_monotonic_ns(void);
// it as an atomic rather than through a plain `int`.
extern "C" std::atomic<int32_t> Bun__defaultRemainingRunsUntilSkipReleaseAccess;

extern "C" void Bun__JSC_acquireHeapAccessAfterWait(JSC::VM* _Nonnull vm)
{
vm->heap.acquireAccess();
}

// Returns in how many milliseconds the caller is to call this again even if nothing else
// wakes it (0: no need). Only the libuv loop gets anything but 0.
extern "C" unsigned int Bun__JSC_onBeforeWait(JSC::VM* _Nonnull vm, uint64_t nowNs)
//
// `releasedHeapAccess` (null where the loop cannot take access back before it dispatches: libuv): set when this gave up
// heap access for the wait, in which case Bun__JSC_acquireHeapAccessAfterWait must run before anything touches the JS
// heap. It does that while an idle collection GarbageCollectionController requested is unfinished
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
// (JSVMClientData::idleCollectionsPending): a requested collection only advances at this thread's safepoints while this
// thread holds the collector's conn, and a parked thread has none; without access, the conn goes to the collector thread,
// which finishes the collection while this one sleeps.
extern "C" unsigned int Bun__JSC_onBeforeWait(JSC::VM* _Nonnull vm, uint64_t nowNs, int* _Nullable releasedHeapAccess)
{
ASSERT(vm);
unsigned int runAgainInMs = 0;
Expand Down Expand Up @@ -120,5 +132,12 @@ extern "C" unsigned int Bun__JSC_onBeforeWait(JSC::VM* _Nonnull vm, uint64_t now
#endif
}
}
if (releasedHeapAccess && previouslyHadAccess && WebCore::clientData(*vm)->idleCollectionsPending.load()) {
// If the collector handed the conn back for a stop-the-world phase while this thread was awake, run that phase
// now rather than at the next time JS happens to run; then park without access so the rest goes on without us.
vm->heap.stopIfNecessary();
vm->heap.releaseAccess();
*releasedHeapAccess = 1;
}
return runAgainInMs;
}
4 changes: 2 additions & 2 deletions src/jsc/bindings/FormatStackTraceForJS.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ static JSValue computeErrorInfoToJSValueWithoutSkipping(JSC::VM& vm, Vector<Stac
// Error.prepareStackTrace - https://v8.dev/docs/stack-trace-api#customizing-stack-traces
if (!globalObject) {
// node:vm will use a different JSGlobalObject
globalObject = defaultGlobalObject();
globalObject = defaultGlobalObject(vm);
if (!globalObject->isInsideErrorPrepareStackTraceCallback) {
auto* errorConstructor = lexicalGlobalObject->m_errorStructure.constructor(lexicalGlobalObject);
auto prepareStackTrace = errorConstructor->getIfPropertyExists(lexicalGlobalObject, Identifier::fromString(vm, "prepareStackTrace"_s));
Expand Down Expand Up @@ -651,7 +651,7 @@ WTF::String computeErrorInfoWrapperToString(JSC::VM& vm, Vector<StackFrame>& sta

auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
// The frames only: JavaScriptCore prepends the error's name and message when the stack is read.
WTF::String result = Bun::formatStackTrace(vm, defaultGlobalObject(), nullptr, emptyString(), emptyString(), line, column, sourceURL, stackTrace, nullptr);
WTF::String result = Bun::formatStackTrace(vm, defaultGlobalObject(vm), nullptr, emptyString(), emptyString(), line, column, sourceURL, stackTrace, nullptr);
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
if (scope.exception()) {
// The onComputeErrorInfo hook cannot propagate a throw.
(void)scope.tryClearException();
Expand Down
18 changes: 16 additions & 2 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,13 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c
extern "C" void* Bun__getVM();

extern "C" void Bun__setDefaultGlobalObject(Zig::GlobalObject* globalObject);
// The thread-local default (what defaultGlobalObject() returns on this thread) and the VM's (what defaultGlobalObject(VM&)
// returns on any thread) change together.
static void setDefaultGlobalObject(JSC::VM& vm, Zig::GlobalObject* globalObject)
{
Bun__setDefaultGlobalObject(globalObject);
WebCore::clientData(vm)->defaultGlobalObject = globalObject;
}

// Declare the native functions for LazyProperty initializers
extern "C" JSC::EncodedJSValue BunObject__createBunStdin(JSC::JSGlobalObject*);
Expand Down Expand Up @@ -464,6 +471,13 @@ extern "C" size_t Bun__reported_memory_size;
// executionContextId: >-1 for workers
extern "C" bool Bun__hasStandaloneModuleGraph();

Zig::GlobalObject* defaultGlobalObject(JSC::VM& vm)
{
if (auto* clientData = WebCore::clientData(vm); clientData && clientData->defaultGlobalObject)
return static_cast<Zig::GlobalObject*>(clientData->defaultGlobalObject);
return defaultGlobalObject();
}

extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client, int32_t executionContextId, bool miniMode, bool evalMode, void* worker_ptr)
{
auto heapSize = miniMode ? JSC::HeapType::Small : JSC::HeapType::Large;
Expand Down Expand Up @@ -549,7 +563,7 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__create(void* console_client,

globalObject->setConsole(console_client);
globalObject->isThreadLocalDefaultGlobalObject = true;
Bun__setDefaultGlobalObject(globalObject);
setDefaultGlobalObject(vm, globalObject);
JSC::gcProtect(globalObject);

#ifdef FUZZILLI_ENABLED
Expand Down Expand Up @@ -678,7 +692,7 @@ extern "C" JSC::JSGlobalObject* Zig__GlobalObject__createForTestIsolation(Zig::G

globalObject->setConsole(console_client);
globalObject->isThreadLocalDefaultGlobalObject = true;
Bun__setDefaultGlobalObject(globalObject);
setDefaultGlobalObject(vm, globalObject);
JSC::gcProtect(globalObject);

// NapiEnv holds a raw Zig::GlobalObject*; deferred napi finalizers for
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,8 @@ inline Zig::GlobalObject* defaultGlobalObject()
{
return ___private___::getDefaultGlobalObject();
}
// The VM's default global from any thread (the parameterless overload is the current thread's).
Zig::GlobalObject* defaultGlobalObject(JSC::VM&);

// The Structure a LazyClassStructure constructor allocates with for this newTarget. nullptr on exception.
JSC::Structure* structureForNewTarget(JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue newTarget, JSC::LazyClassStructure Zig::GlobalObject::* classStructure);
Expand Down
15 changes: 15 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3162,8 +3162,23 @@ void JSC__VM__collectAsync(JSC::VM* vm, bool full)
void JSC__VM__collectAsyncIdle(JSC::VM* vm)
{
JSC::JSLockHolder lock(*vm);
if (!JSC::Options::useGC())
return;
auto* clientData = WebCore::clientData(*vm);
JSC::GCRequest request(JSC::CollectionScope::Full);
request.isIdle = true;
// See Bun__JSC_onBeforeWait. The end phase may run on the collector thread while the JS thread is parked, and the
// epilogue that sweeps what the collection freed runs when the JS thread takes heap access back: wake it for that. A
// request with this hook is never coalesced into another, so the count always comes back down.
if (!clientData->idleCollectionDidFinish) {
clientData->idleCollectionDidFinish = createSharedTask<void()>([vm, clientData] {
clientData->idleCollectionsPending.fetch_sub(1);
if (!vm->currentThreadIsHoldingAPILock() && clientData->vmHandle)
Bun__VmHandle__wake(clientData->vmHandle);
});
}
request.didFinishEndPhase = clientData->idleCollectionDidFinish;
clientData->idleCollectionsPending.fetch_add(1);
vm->heap.collectAsync(request);
}

Expand Down
3 changes: 3 additions & 0 deletions src/runtime/hw_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ pub(crate) fn remap_stack_frame_positions(
frames: *mut ZigStackFrame,
frames_count: usize,
) {
// The collector thread is not otherwise a Bun thread: give it the per-thread output source that a warning about a
// missing or malformed source map would write through (idempotent; no JS ever runs there).
bun_core::output::Source::configure_thread_no_js();
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
// SAFETY: `frames[..frames_count]` is a live C++ array; the method takes
// the raw ptr because it forwards to the C++-side remapper.
unsafe { vm.remap_stack_frame_positions(frames, frames_count) };
Comment thread
Jarred-Sumner marked this conversation as resolved.
Resolved
Expand Down
4 changes: 3 additions & 1 deletion src/sourcemap/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,9 @@ pub(crate) fn parse_json(source: &[u8], hint: ParseUrlResultHint) -> crate::Resu

// the allocator given to the JS parser is not respected for all parts
// of the parse, so we need to remember to reset the ast store on entry
// and on every exit path.
// and on every exit path. A source map may be parsed lazily from a thread that never ran the parser (a stack trace
// remapped from a collector thread's end phase), so make sure this thread has the stores at all.
bun_ast::initialize_store();
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
let _store_scope = DataStoreScope::new();
bun_core::scoped_log!(SourceMapLog, "parse (JSON, {} bytes)", source.len());
let parsed = match bun_parsers::json::ParsedJson::parse_json(&json_src, &mut log) {
Expand Down
Loading
Loading