diff --git a/Makefile b/Makefile index f1a8be26462ff0..d2940c28196f95 100644 --- a/Makefile +++ b/Makefile @@ -91,9 +91,9 @@ ZIG ?= $(shell which zig 2>/dev/null || echo -e "error: Missing zig. Please make # This is easier to happen than you'd expect. # Using realpath here causes issues because clang uses clang++ as a symlink # so if that's resolved, it won't build for C++ -REAL_CC = $(shell which clang-16 2>/dev/null || which clang 2>/dev/null) -REAL_CXX = $(shell which clang++-16 2>/dev/null || which clang++ 2>/dev/null) -CLANG_FORMAT = $(shell which clang-format-16 2>/dev/null || which clang-format 2>/dev/null) +REAL_CC = $(shell which clang-18 2>/dev/null || which clang 2>/dev/null) +REAL_CXX = $(shell which clang++-18 2>/dev/null || which clang++ 2>/dev/null) +CLANG_FORMAT = $(shell which clang-format-18 2>/dev/null || which clang-format 2>/dev/null) CC = $(REAL_CC) CXX = $(REAL_CXX) @@ -117,14 +117,14 @@ CC_WITH_CCACHE = $(CCACHE_PATH) $(CC) ifeq ($(OS_NAME),darwin) # Find LLVM ifeq ($(wildcard $(LLVM_PREFIX)),) - LLVM_PREFIX = $(shell brew --prefix llvm@16) + LLVM_PREFIX = $(shell brew --prefix llvm@18) endif ifeq ($(wildcard $(LLVM_PREFIX)),) LLVM_PREFIX = $(shell brew --prefix llvm) endif ifeq ($(wildcard $(LLVM_PREFIX)),) # This is kinda ugly, but I can't find a better way to error :( - LLVM_PREFIX = $(shell echo -e "error: Unable to find llvm. Please run 'brew install llvm@16' or set LLVM_PREFIX=/path/to/llvm") + LLVM_PREFIX = $(shell echo -e "error: Unable to find llvm. Please run 'brew install llvm@18' or set LLVM_PREFIX=/path/to/llvm") endif LDFLAGS += -L$(LLVM_PREFIX)/lib @@ -164,7 +164,7 @@ CMAKE_FLAGS_WITHOUT_RELEASE = -DCMAKE_C_COMPILER=$(CC) \ -DCMAKE_OSX_DEPLOYMENT_TARGET=$(MIN_MACOS_VERSION) \ $(CMAKE_CXX_COMPILER_LAUNCHER_FLAG) \ -DCMAKE_AR=$(AR) \ - -DCMAKE_RANLIB=$(which llvm-16-ranlib 2>/dev/null || which llvm-ranlib 2>/dev/null) \ + -DCMAKE_RANLIB=$(which llvm-18-ranlib 2>/dev/null || which llvm-ranlib 2>/dev/null) \ -DCMAKE_CXX_STANDARD=20 \ -DCMAKE_C_STANDARD=17 \ -DCMAKE_CXX_STANDARD_REQUIRED=ON \ @@ -191,7 +191,7 @@ endif ifeq ($(OS_NAME),linux) LIBICONV_PATH = -AR = $(shell which llvm-ar-16 2>/dev/null || which llvm-ar 2>/dev/null || which ar 2>/dev/null) +AR = $(shell which llvm-ar-18 2>/dev/null || which llvm-ar 2>/dev/null || which ar 2>/dev/null) endif OPTIMIZATION_LEVEL=-O3 $(MARCH_NATIVE) @@ -286,7 +286,7 @@ STRIP=/usr/bin/strip endif ifeq ($(OS_NAME),linux) -STRIP=$(shell which llvm-strip 2>/dev/null || which llvm-strip-16 2>/dev/null || which strip 2>/dev/null || echo "Missing strip") +STRIP=$(shell which llvm-strip 2>/dev/null || which llvm-strip-18 2>/dev/null || which strip 2>/dev/null || echo "Missing strip") endif @@ -674,7 +674,7 @@ endif .PHONY: assert-deps assert-deps: @echo "Checking if the required utilities are available..." - @if [ $(CLANG_VERSION) -lt "15" ]; then echo -e "ERROR: clang version >=15 required, found: $(CLANG_VERSION). Install with:\n\n $(POSIX_PKG_MANAGER) install llvm@16"; exit 1; fi + @if [ $(CLANG_VERSION) -lt "18" ]; then echo -e "ERROR: clang version >=18 required, found: $(CLANG_VERSION). Install with:\n\n $(POSIX_PKG_MANAGER) install llvm@18"; exit 1; fi @cmake --version >/dev/null 2>&1 || (echo -e "ERROR: cmake is required."; exit 1) @$(PYTHON) --version >/dev/null 2>&1 || (echo -e "ERROR: python is required."; exit 1) @$(ESBUILD) --version >/dev/null 2>&1 || (echo -e "ERROR: esbuild is required."; exit 1) @@ -1261,6 +1261,7 @@ jsc-build-mac-compile: -DBUN_FAST_TLS=ON \ -DENABLE_FTL_JIT=ON \ -DUSE_BUN_JSC_ADDITIONS=ON \ + -DUSE_BUN_EVENT_LOOP=ON \ -G Ninja \ $(CMAKE_FLAGS_WITHOUT_RELEASE) \ -DPTHREAD_JIT_PERMISSIONS_API=1 \ @@ -1284,6 +1285,7 @@ jsc-build-mac-compile-lto: -DUSE_THIN_ARCHIVES=OFF \ -DBUN_FAST_TLS=ON \ -DUSE_BUN_JSC_ADDITIONS=ON \ + -DUSE_BUN_EVENT_LOOP=ON \ -DCMAKE_C_FLAGS="-flto=full" \ -DCMAKE_CXX_FLAGS="-flto=full" \ -DENABLE_FTL_JIT=ON \ @@ -1310,6 +1312,7 @@ jsc-build-mac-compile-debug: -DENABLE_MALLOC_HEAP_BREAKDOWN=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DUSE_BUN_JSC_ADDITIONS=ON \ + -DUSE_BUN_EVENT_LOOP=ON \ -DENABLE_BUN_SKIP_FAILING_ASSERTIONS=ON \ -DALLOW_LINE_AND_COLUMN_NUMBER_IN_BUILTINS=ON \ -G Ninja \ @@ -1334,6 +1337,7 @@ jsc-build-linux-compile-config: -DENABLE_BUN_SKIP_FAILING_ASSERTIONS=ON \ -DUSE_THIN_ARCHIVES=OFF \ -DUSE_BUN_JSC_ADDITIONS=ON \ + -DUSE_BUN_EVENT_LOOP=ON \ -DENABLE_FTL_JIT=ON \ -DENABLE_REMOTE_INSPECTOR=ON \ -DJSEXPORT_PRIVATE=WTF_EXPORT_DECLARATION \ @@ -1357,6 +1361,7 @@ jsc-build-linux-compile-config-debug: -DENABLE_BUN_SKIP_FAILING_ASSERTIONS=ON \ -DUSE_THIN_ARCHIVES=OFF \ -DUSE_BUN_JSC_ADDITIONS=ON \ + -DUSE_BUN_EVENT_LOOP=ON \ -DENABLE_FTL_JIT=ON \ -DENABLE_REMOTE_INSPECTOR=ON \ -DJSEXPORT_PRIVATE=WTF_EXPORT_DECLARATION \ @@ -1375,14 +1380,14 @@ jsc-build-linux-compile-config-debug: jsc-build-linux-compile-build: mkdir -p $(WEBKIT_RELEASE_DIR) && \ cd $(WEBKIT_RELEASE_DIR) && \ - CFLAGS="$(CFLAGS) -Wl,--whole-archive -ffat-lto-objects" CXXFLAGS="$(CXXFLAGS) -Wl,--whole-archive -ffat-lto-objects -DUSE_BUN_JSC_ADDITIONS=ON" \ + CFLAGS="$(CFLAGS) -Wl,--whole-archive -ffat-lto-objects" CXXFLAGS="$(CXXFLAGS) -Wl,--whole-archive -ffat-lto-objects -DUSE_BUN_JSC_ADDITIONS=ON -DUSE_BUN_EVENT_LOOP=ON" \ cmake --build $(WEBKIT_RELEASE_DIR) --config relwithdebuginfo --target jsc .PHONY: jsc-build-linux-compile-build-debug jsc-build-linux-compile-build-debug: mkdir -p $(WEBKIT_DEBUG_DIR) && \ cd $(WEBKIT_DEBUG_DIR) && \ - CFLAGS="$(CFLAGS) -Wl,--whole-archive -ffat-lto-objects" CXXFLAGS="$(CXXFLAGS) -Wl,--whole-archive -ffat-lto-objects -DUSE_BUN_JSC_ADDITIONS=ON" \ + CFLAGS="$(CFLAGS) -Wl,--whole-archive -ffat-lto-objects" CXXFLAGS="$(CXXFLAGS) -Wl,--whole-archive -ffat-lto-objects -DUSE_BUN_JSC_ADDITIONS=ON -DUSE_BUN_EVENT_LOOP=ON" \ cmake --build $(WEBKIT_DEBUG_DIR) --config Debug --target jsc diff --git a/cmake/tools/SetupWebKit.cmake b/cmake/tools/SetupWebKit.cmake index d00d3a3e96eecb..28621043068055 100644 --- a/cmake/tools/SetupWebKit.cmake +++ b/cmake/tools/SetupWebKit.cmake @@ -2,7 +2,7 @@ option(WEBKIT_VERSION "The version of WebKit to use") option(WEBKIT_LOCAL "If a local version of WebKit should be used instead of downloading") if(NOT WEBKIT_VERSION) - set(WEBKIT_VERSION 9e3b60e4a6438d20ee6f8aa5bec6b71d2b7d213f) + set(WEBKIT_VERSION e6cb36cabed465c28c7bcbb28d86e8466ea36e8d) endif() string(SUBSTRING ${WEBKIT_VERSION} 0 16 WEBKIT_VERSION_PREFIX) diff --git a/src/bun.js/api/Timer.zig b/src/bun.js/api/Timer.zig index 9b15fe9aae7b7b..f1f205598bdadd 100644 --- a/src/bun.js/api/Timer.zig +++ b/src/bun.js/api/Timer.zig @@ -28,6 +28,8 @@ const TimerHeap = heap.Intrusive(EventLoopTimer, void, EventLoopTimer.less); pub const All = struct { last_id: i32 = 1, + lock: bun.Mutex = .{}, + thread_id: std.Thread.Id, timers: TimerHeap = .{ .context = {}, }, @@ -49,7 +51,15 @@ pub const All = struct { } } = .{}, + pub fn init() @This() { + return .{ + .thread_id = std.Thread.getCurrentId(), + }; + } + pub fn insert(this: *All, timer: *EventLoopTimer) void { + this.lock.lock(); + defer this.lock.unlock(); this.timers.insert(timer); timer.state = .ACTIVE; @@ -59,12 +69,36 @@ pub const All = struct { } pub fn remove(this: *All, timer: *EventLoopTimer) void { + this.lock.lock(); + defer this.lock.unlock(); this.timers.remove(timer); timer.state = .CANCELLED; timer.heap = .{}; } + /// Remove the EventLoopTimer if necessary. + pub fn update(this: *All, timer: *EventLoopTimer, time: *const timespec) void { + this.lock.lock(); + defer this.lock.unlock(); + if (timer.state == .ACTIVE) { + this.timers.remove(timer); + } + + timer.state = .ACTIVE; + if (comptime Environment.isDebug) { + if (&timer.next == time) { + @panic("timer.next == time. For threadsafety reasons, time and timer.next must always be a different pointer."); + } + } + timer.next = time.*; + + this.timers.insert(timer); + if (Environment.isWindows) { + this.ensureUVTimer(@alignCast(@fieldParentPtr("timer", this))); + } + } + fn ensureUVTimer(this: *All, vm: *VirtualMachine) void { if (this.uv_timer.data == null) { this.uv_timer.init(vm.uvLoop()); @@ -129,15 +163,28 @@ pub const All = struct { return VirtualMachine.get().timer.last_id; } - pub fn getTimeout(this: *const All, spec: *timespec) bool { + pub fn getTimeout(this: *All, spec: *timespec, vm: *VirtualMachine) bool { if (this.active_timer_count == 0) { return false; } - if (this.timers.peek()) |min| { - const now = timespec.now(); + var now: timespec = undefined; + var has_set_now: bool = false; + while (this.timers.peek()) |min| { + if (!has_set_now) { + now = timespec.now(); + has_set_now = true; + } + switch (now.order(&min.next)) { .gt, .eq => { + // Side-effect: potentially call the StopIfNecessary timer. + if (min.tag == .WTFTimer) { + _ = this.timers.deleteMin(); + _ = min.fire(&now, vm); + continue; + } + spec.* = .{ .nsec = 0, .sec = 0 }; return true; }, @@ -146,6 +193,8 @@ pub const All = struct { return true; }, } + + unreachable; } return false; @@ -159,22 +208,40 @@ pub const All = struct { _ = &Bun__internal_drainTimers; } - pub fn drainTimers(this: *All, vm: *VirtualMachine) void { - if (this.timers.peek() == null) { - return; - } + // Getting the current time is expensive on certain platforms. + // We don't want to call it when there are no timers. + // And when we do call it, we want to be sure we only call it once. + // and we do NOT want to hold the lock while the timer is running it's code. + // This function has to be thread-safe. + fn next(this: *All, has_set_now: *bool, now: *timespec) ?*EventLoopTimer { + this.lock.lock(); + defer this.lock.unlock(); - const now = ×pec.now(); - - while (this.timers.peek()) |t| { - if (t.next.greater(now)) { - break; + if (this.timers.peek()) |timer| { + if (!has_set_now.*) { + now.* = timespec.now(); + has_set_now.* = true; + } + if (timer.next.greater(now)) { + return null; } - assert(this.timers.deleteMin().? == t); + assert(this.timers.deleteMin().? == timer); + + return timer; + } + return null; + } + + pub fn drainTimers(this: *All, vm: *VirtualMachine) void { + // Set in next(). + var now: timespec = undefined; + // Split into a separate variable to avoid increasing the size of the timespec type. + var has_set_now: bool = false; + while (this.next(&has_set_now, &now)) |t| { switch (t.fire( - now, + &now, vm, )) { .disarm => {}, @@ -467,8 +534,7 @@ pub const TimerObject = struct { switch (this.event_loop_timer.state) { .FIRED => { // If we didn't clear the setInterval, reschedule it starting from - this.event_loop_timer.next = time_before_call; - vm.timer.insert(&this.event_loop_timer); + vm.timer.update(&this.event_loop_timer, &time_before_call); if (this.has_js_ref) { this.setEnableKeepingEventLoopAlive(vm, true); @@ -478,10 +544,7 @@ pub const TimerObject = struct { }, .ACTIVE => { // The developer called timer.refresh() synchronously in the callback. - vm.timer.remove(&this.event_loop_timer); - - this.event_loop_timer.next = time_before_call; - vm.timer.insert(&this.event_loop_timer); + vm.timer.update(&this.event_loop_timer, &time_before_call); // Balance out the ref count. // the transition from "FIRED" -> "ACTIVE" caused it to increment. @@ -620,8 +683,7 @@ pub const TimerObject = struct { this.ref(); } - this.event_loop_timer.next = now; - vm.timer.insert(&this.event_loop_timer); + vm.timer.update(&this.event_loop_timer, &now); this.has_cleared_timer = false; if (this.has_js_ref) { @@ -722,7 +784,7 @@ pub const EventLoopTimer = struct { state: State = .PENDING, - tag: Tag = .TimerCallback, + tag: Tag, pub const Tag = if (Environment.isWindows) enum { TimerCallback, @@ -732,6 +794,7 @@ pub const EventLoopTimer = struct { UpgradedDuplex, DNSResolver, WindowsNamedPipe, + WTFTimer, PostgresSQLConnectionTimeout, PostgresSQLConnectionMaxLifetime, @@ -744,6 +807,7 @@ pub const EventLoopTimer = struct { .UpgradedDuplex => uws.UpgradedDuplex, .DNSResolver => DNSResolver, .WindowsNamedPipe => uws.WindowsNamedPipe, + .WTFTimer => WTFTimer, .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, }; @@ -754,6 +818,7 @@ pub const EventLoopTimer = struct { TestRunner, StatWatcherScheduler, UpgradedDuplex, + WTFTimer, DNSResolver, PostgresSQLConnectionTimeout, PostgresSQLConnectionMaxLifetime, @@ -765,6 +830,7 @@ pub const EventLoopTimer = struct { .TestRunner => JSC.Jest.TestRunner, .StatWatcherScheduler => StatWatcherScheduler, .UpgradedDuplex => uws.UpgradedDuplex, + .WTFTimer => WTFTimer, .DNSResolver => DNSResolver, .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, @@ -824,6 +890,10 @@ pub const EventLoopTimer = struct { .PostgresSQLConnectionMaxLifetime => return @as(*JSC.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("max_lifetime_timer", this))).onMaxLifetimeTimeout(), inline else => |t| { var container: *t.Type() = @alignCast(@fieldParentPtr("event_loop_timer", this)); + if (comptime t.Type() == WTFTimer) { + return container.fire(now, vm); + } + if (comptime t.Type() == TimerObject) { return container.fire(now, vm); } @@ -858,3 +928,130 @@ pub const EventLoopTimer = struct { }; const timespec = bun.timespec; + +/// A timer created by WTF code and invoked by Bun's event loop +pub const WTFTimer = struct { + /// This is WTF::RunLoop::TimerBase from WebKit + const RunLoopTimer = opaque {}; + + vm: *VirtualMachine, + run_loop_timer: *RunLoopTimer, + event_loop_timer: EventLoopTimer, + imminent: *std.atomic.Value(?*WTFTimer), + repeat: bool, + lock: bun.Mutex = .{}, + + pub usingnamespace bun.New(@This()); + + pub fn init(run_loop_timer: *RunLoopTimer, js_vm: *VirtualMachine) *WTFTimer { + const this = WTFTimer.new(.{ + .vm = js_vm, + .imminent = &js_vm.eventLoop().imminent_gc_timer, + .event_loop_timer = .{ + .next = .{ + .sec = std.math.maxInt(i64), + .nsec = 0, + }, + .tag = .WTFTimer, + .state = .CANCELLED, + }, + .run_loop_timer = run_loop_timer, + .repeat = false, + }); + + return this; + } + + pub fn run(this: *WTFTimer, vm: *VirtualMachine) void { + if (this.event_loop_timer.state == .ACTIVE) { + vm.timer.remove(&this.event_loop_timer); + } + this.runWithoutRemoving(); + } + + inline fn runWithoutRemoving(this: *const WTFTimer) void { + WTFTimer__fire(this.run_loop_timer); + } + + pub fn update(this: *WTFTimer, seconds: f64, repeat: bool) void { + // There's only one of these. + this.imminent.store(if (seconds == 0) this else null, .seq_cst); + + if (seconds == 0.0) { + return; + } + + const modf = std.math.modf(seconds); + var interval = bun.timespec.now(); + interval.sec += @intFromFloat(modf.ipart); + interval.nsec += @intFromFloat(modf.fpart * std.time.ns_per_s); + if (interval.nsec >= std.time.ns_per_s) { + interval.sec += 1; + interval.nsec -= std.time.ns_per_s; + } + + this.vm.timer.update(&this.event_loop_timer, &interval); + this.repeat = repeat; + } + + pub fn cancel(this: *WTFTimer) void { + this.lock.lock(); + defer this.lock.unlock(); + this.imminent.store(null, .monotonic); + if (this.event_loop_timer.state == .ACTIVE) { + this.vm.timer.remove(&this.event_loop_timer); + } + } + + pub fn fire(this: *WTFTimer, _: *const bun.timespec, _: *VirtualMachine) EventLoopTimer.Arm { + this.event_loop_timer.state = .FIRED; + this.imminent.store(null, .monotonic); + this.runWithoutRemoving(); + return if (this.repeat) + .{ .rearm = this.event_loop_timer.next } + else + .disarm; + } + + pub fn deinit(this: *WTFTimer) void { + this.cancel(); + this.destroy(); + } + + export fn WTFTimer__create(run_loop_timer: *RunLoopTimer) ?*anyopaque { + if (VirtualMachine.is_bundler_thread_for_bytecode_cache) { + return null; + } + + return init(run_loop_timer, VirtualMachine.get()); + } + + export fn WTFTimer__update(this: *WTFTimer, seconds: f64, repeat: bool) void { + this.update(seconds, repeat); + } + + export fn WTFTimer__deinit(this: *WTFTimer) void { + this.deinit(); + } + + export fn WTFTimer__isActive(this: *const WTFTimer) bool { + return this.event_loop_timer.state == .ACTIVE or (this.imminent.load(.monotonic) orelse return false) == this; + } + + export fn WTFTimer__cancel(this: *WTFTimer) void { + this.cancel(); + } + + export fn WTFTimer__secondsUntilTimer(this: *WTFTimer) f64 { + this.lock.lock(); + defer this.lock.unlock(); + if (this.event_loop_timer.state == .ACTIVE) { + const until = this.event_loop_timer.next.duration(&bun.timespec.now()); + const sec: f64, const nsec: f64 = .{ @floatFromInt(until.sec), @floatFromInt(until.nsec) }; + return sec + nsec / std.time.ns_per_s; + } + return std.math.inf(f64); + } + + extern fn WTFTimer__fire(this: *RunLoopTimer) void; +}; diff --git a/src/bun.js/api/bun/subprocess.zig b/src/bun.js/api/bun/subprocess.zig index a0192a0f1d7d46..28bf780f8f587a 100644 --- a/src/bun.js/api/bun/subprocess.zig +++ b/src/bun.js/api/bun/subprocess.zig @@ -2388,21 +2388,29 @@ pub const Subprocess = struct { jsc_vm.onSubprocessSpawn(subprocess.process); } - while (subprocess.hasPendingActivityNonThreadsafe()) { - if (subprocess.stdin == .buffer) { - subprocess.stdin.buffer.watch(); - } + // We cannot release heap access while JS is running + { + const old_vm = jsc_vm.uwsLoop().internal_loop_data.jsc_vm; + jsc_vm.uwsLoop().internal_loop_data.jsc_vm = null; + defer { + jsc_vm.uwsLoop().internal_loop_data.jsc_vm = old_vm; + } + while (subprocess.hasPendingActivityNonThreadsafe()) { + if (subprocess.stdin == .buffer) { + subprocess.stdin.buffer.watch(); + } - if (subprocess.stderr == .pipe) { - subprocess.stderr.pipe.watch(); - } + if (subprocess.stderr == .pipe) { + subprocess.stderr.pipe.watch(); + } - if (subprocess.stdout == .pipe) { - subprocess.stdout.pipe.watch(); - } + if (subprocess.stdout == .pipe) { + subprocess.stdout.pipe.watch(); + } - jsc_vm.tick(); - jsc_vm.eventLoop().autoTick(); + jsc_vm.tick(); + jsc_vm.eventLoop().autoTick(); + } } subprocess.updateHasPendingActivity(); diff --git a/src/bun.js/bindings/BunJSCEventLoop.cpp b/src/bun.js/bindings/BunJSCEventLoop.cpp index 30498f1be9157e..7a3cf95b2a01ee 100644 --- a/src/bun.js/bindings/BunJSCEventLoop.cpp +++ b/src/bun.js/bindings/BunJSCEventLoop.cpp @@ -5,19 +5,14 @@ extern "C" int Bun__JSC_onBeforeWait(JSC::VM* vm) { - UNUSED_PARAM(vm); - // TODO: use JSC timers, run the incremental sweeper. - // That will fix this. - // In the meantime, we're disabling this due to https://github.com/oven-sh/bun/issues/14982 - // if (vm->heap.hasAccess()) { - // vm->heap.releaseAccess(); - // return 1; - // } + if (vm->heap.hasAccess()) { + vm->heap.releaseAccess(); + return 1; + } return 0; } extern "C" void Bun__JSC_onAfterWait(JSC::VM* vm) { - UNUSED_PARAM(vm); - // vm->heap.acquireAccess(); + vm->heap.acquireAccess(); } diff --git a/src/bun.js/bindings/EventLoopTaskNoContext.cpp b/src/bun.js/bindings/EventLoopTaskNoContext.cpp new file mode 100644 index 00000000000000..473bc3f11f2c69 --- /dev/null +++ b/src/bun.js/bindings/EventLoopTaskNoContext.cpp @@ -0,0 +1,17 @@ +#include "EventLoopTaskNoContext.h" + +namespace Bun { + +WTF_MAKE_ISO_ALLOCATED_IMPL(EventLoopTaskNoContext); + +extern "C" void Bun__EventLoopTaskNoContext__performTask(EventLoopTaskNoContext* task) +{ + task->performTask(); +} + +extern "C" void* Bun__EventLoopTaskNoContext__createdInBunVm(const EventLoopTaskNoContext* task) +{ + return task->createdInBunVm(); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/EventLoopTaskNoContext.h b/src/bun.js/bindings/EventLoopTaskNoContext.h new file mode 100644 index 00000000000000..0f3491ce7676e6 --- /dev/null +++ b/src/bun.js/bindings/EventLoopTaskNoContext.h @@ -0,0 +1,35 @@ +#pragma once + +#include "ZigGlobalObject.h" +#include "root.h" + +namespace Bun { + +// Just like WebCore::EventLoopTask but does not take a ScriptExecutionContext +class EventLoopTaskNoContext { + WTF_MAKE_ISO_ALLOCATED(EventLoopTaskNoContext); + +public: + EventLoopTaskNoContext(JSC::JSGlobalObject* globalObject, Function&& task) + : m_createdInBunVm(defaultGlobalObject(globalObject)->bunVM()) + , m_task(WTFMove(task)) + { + } + + void performTask() + { + m_task(); + delete this; + } + + void* createdInBunVm() const { return m_createdInBunVm; } + +private: + void* m_createdInBunVm; + Function m_task; +}; + +extern "C" void Bun__EventLoopTaskNoContext__performTask(EventLoopTaskNoContext* task); +extern "C" void* Bun__EventLoopTaskNoContext__createdInBunVm(const EventLoopTaskNoContext* task); + +} // namespace Bun diff --git a/src/bun.js/bindings/ScriptExecutionContext.h b/src/bun.js/bindings/ScriptExecutionContext.h index 6122948614cdbb..51b49cada1f2cf 100644 --- a/src/bun.js/bindings/ScriptExecutionContext.h +++ b/src/bun.js/bindings/ScriptExecutionContext.h @@ -2,7 +2,6 @@ #include "root.h" #include "ActiveDOMObject.h" -#include "ContextDestructionObserver.h" #include #include #include @@ -33,6 +32,8 @@ class MessagePort; class ScriptExecutionContext; class EventLoopTask; +class ContextDestructionObserver; + using ScriptExecutionContextIdentifier = uint32_t; DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(ScriptExecutionContext); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 4a8338f6acc0e1..7f2d06cc72b63b 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -6171,6 +6171,11 @@ CPP_DECL void JSC__VM__setControlFlowProfiler(JSC__VM* vm, bool isEnabled) } } +CPP_DECL void JSC__VM__performOpportunisticallyScheduledTasks(JSC__VM* vm, double until) +{ + vm->performOpportunisticallyScheduledTasks(MonotonicTime::now() + Seconds(until), {}); +} + extern "C" EncodedJSValue JSC__createError(JSC::JSGlobalObject* globalObject, const BunString* str) { return JSValue::encode(JSC::createError(globalObject, str->toWTFString(BunString::ZeroCopy))); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 6f7ae454686303..f709a6b58b7640 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -6207,16 +6207,19 @@ pub const VM = extern struct { extern fn Bun__JSC_onAfterWait(vm: *VM) void; pub const ReleaseHeapAccess = struct { vm: *VM, - needs_to_release: bool, + needs_to_acquire: bool, pub fn acquire(this: *const ReleaseHeapAccess) void { - if (this.needs_to_release) { + if (this.needs_to_acquire) { Bun__JSC_onAfterWait(this.vm); } } }; + /// Temporarily give up access to the heap, allowing other work to proceed. Call acquire() on + /// the return value at scope exit. If you did not already have heap access, release and acquire + /// are both safe no-ops. pub fn releaseHeapAccess(vm: *VM) ReleaseHeapAccess { - return .{ .vm = vm, .needs_to_release = Bun__JSC_onBeforeWait(vm) != 0 }; + return .{ .vm = vm, .needs_to_acquire = Bun__JSC_onBeforeWait(vm) != 0 }; } pub fn create(heap_type: HeapType) *VM { @@ -6379,6 +6382,10 @@ pub const VM = extern struct { return cppFn("blockBytesAllocated", .{vm}); } + pub fn performOpportunisticallyScheduledTasks(vm: *VM, until: f64) void { + cppFn("performOpportunisticallyScheduledTasks", .{ vm, until }); + } + pub const Extern = [_][]const u8{ "setControlFlowProfiler", "collectAsync", diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index 66906609504224..10ba9fca8ef8c7 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -52,12 +52,14 @@ pub const ZigGlobalObject = extern struct { pub const Interface: type = NewGlobalObject(JS.VirtualMachine); pub fn create( + vm: *JSC.VirtualMachine, console: *anyopaque, context_id: i32, mini_mode: bool, eval_mode: bool, worker_ptr: ?*anyopaque, ) *JSGlobalObject { + vm.eventLoop().ensureWaker(); const global = shim.cppFn("create", .{ console, context_id, mini_mode, eval_mode, worker_ptr }); // JSC might mess with the stack size. diff --git a/src/bun.js/bindings/headers.zig b/src/bun.js/bindings/headers.zig index 2f0bbc3205a0d6..b2b777ab027ebf 100644 --- a/src/bun.js/bindings/headers.zig +++ b/src/bun.js/bindings/headers.zig @@ -313,7 +313,7 @@ pub extern fn JSC__VM__setExecutionTimeLimit(arg0: *bindings.VM, arg1: f64) void pub extern fn JSC__VM__shrinkFootprint(arg0: *bindings.VM) void; pub extern fn JSC__VM__throwError(arg0: *bindings.VM, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) void; pub extern fn JSC__VM__whenIdle(arg0: *bindings.VM, ArgFn1: ?*const fn (...) callconv(.C) void) void; - +pub extern fn JSC__VM__performOpportunisticallyScheduledTasks(arg0: *bindings.VM, arg1: f64) void; pub extern fn FFI__ptr__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void; pub extern fn Reader__u8__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void; pub extern fn Reader__u16__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void; diff --git a/src/bun.js/bindings/webcore/ContextDestructionObserver.h b/src/bun.js/bindings/webcore/ContextDestructionObserver.h index f206ad80354e16..c90a25bd0adc98 100644 --- a/src/bun.js/bindings/webcore/ContextDestructionObserver.h +++ b/src/bun.js/bindings/webcore/ContextDestructionObserver.h @@ -3,11 +3,10 @@ #pragma once #include "root.h" +#include "ScriptExecutionContext.h" namespace WebCore { -class ScriptExecutionContext; - class ContextDestructionObserver { public: diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithm.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithm.cpp index f6350fe65d4f1f..318a8e15bef4df 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithm.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithm.cpp @@ -95,12 +95,10 @@ ExceptionOr CryptoAlgorithm::getKeyLength(const CryptoAlgorithmParameter template static void dispatchAlgorithmOperation(WorkQueue& workQueue, ScriptExecutionContext& context, ResultCallbackType&& callback, CryptoAlgorithm::ExceptionCallback&& exceptionCallback, OperationType&& operation) { - context.refEventLoop(); - workQueue.dispatch( + workQueue.dispatch(context.globalObject(), [operation = WTFMove(operation), callback = WTFMove(callback), exceptionCallback = WTFMove(exceptionCallback), contextIdentifier = context.identifier()]() mutable { auto result = operation(); ScriptExecutionContext::postTaskTo(contextIdentifier, [result = crossThreadCopy(WTFMove(result)), callback = WTFMove(callback), exceptionCallback = WTFMove(exceptionCallback)](auto& context) mutable { - context.unrefEventLoop(); if (result.hasException()) { exceptionCallback(result.releaseException().code(), ""_s); return; diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithm.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithm.h index 29964198d14926..97eb9a5c8015e7 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithm.h +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithm.h @@ -35,7 +35,7 @@ #include #include #include -#include +#include "SubtleCrypto.h" #if ENABLE(WEB_CRYPTO) diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDH.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDH.cpp index a26290f7aa56e9..acb047cc722bac 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDH.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmECDH.cpp @@ -110,7 +110,7 @@ void CryptoAlgorithmECDH::deriveBits(const CryptoAlgorithmParameters& parameters // This is a special case that can't use dispatchOperation() because it bundles // the result validation and callback dispatch into unifiedCallback. - workQueue.dispatch( + workQueue.dispatch(context.globalObject(), [baseKey = WTFMove(baseKey), publicKey = ecParameters.publicKey, length, unifiedCallback = WTFMove(unifiedCallback), contextIdentifier = context.identifier()]() mutable { auto derivedKey = platformDeriveBits(downcast(baseKey.get()), downcast(*publicKey)); ScriptExecutionContext::postTaskTo(contextIdentifier, [derivedKey = WTFMove(derivedKey), length, unifiedCallback = WTFMove(unifiedCallback)](auto&) mutable { diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp index e2691e87a04c95..fb919908a2ae4d 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA1.cpp @@ -61,13 +61,10 @@ void CryptoAlgorithmSHA1::digest(Vector&& message, VectorCallback&& cal return; } - context.refEventLoop(); - - workQueue.dispatch([digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { + workQueue.dispatch(context.globalObject(), [digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { digest->addBytes(message.data(), message.size()); auto result = digest->computeHash(); - ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto& context) { - context.unrefEventLoop(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto&) { callback(result); }); }); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp index 9333c304adf380..7080ab025544a2 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA224.cpp @@ -61,12 +61,10 @@ void CryptoAlgorithmSHA224::digest(Vector&& message, VectorCallback&& c return; } - context.refEventLoop(); - workQueue.dispatch([digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { + workQueue.dispatch(context.globalObject(), [digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { digest->addBytes(message.data(), message.size()); auto result = digest->computeHash(); - ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto& context) { - context.unrefEventLoop(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto&) { callback(result); }); }); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp index c04dfc790c9110..d55e874155c64e 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA256.cpp @@ -60,12 +60,11 @@ void CryptoAlgorithmSHA256::digest(Vector&& message, VectorCallback&& c }); return; } - context.refEventLoop(); - workQueue.dispatch([digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { + + workQueue.dispatch(context.globalObject(), [digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { digest->addBytes(message.data(), message.size()); auto result = digest->computeHash(); - ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto& context) { - context.unrefEventLoop(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto&) { callback(result); }); }); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp index 0297099f985af7..a4237487d6ab9c 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA384.cpp @@ -61,13 +61,10 @@ void CryptoAlgorithmSHA384::digest(Vector&& message, VectorCallback&& c return; } - context.refEventLoop(); - - workQueue.dispatch([digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { + workQueue.dispatch(context.globalObject(), [digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { digest->addBytes(message.data(), message.size()); auto result = digest->computeHash(); - ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto& context) { - context.unrefEventLoop(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto&) { callback(result); }); }); diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp index edb1f8b492b38f..e902cfa1d5e835 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmSHA512.cpp @@ -61,13 +61,10 @@ void CryptoAlgorithmSHA512::digest(Vector&& message, VectorCallback&& c return; } - context.refEventLoop(); - workQueue.dispatch([digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { + workQueue.dispatch(context.globalObject(), [digest = WTFMove(digest), message = WTFMove(message), callback = WTFMove(callback), contextIdentifier = context.identifier()]() mutable { digest->addBytes(message.data(), message.size()); auto result = digest->computeHash(); - - ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto& context) { - context.unrefEventLoop(); + ScriptExecutionContext::postTaskTo(contextIdentifier, [callback = WTFMove(callback), result = WTFMove(result)](auto&) { callback(result); }); }); diff --git a/src/bun.js/bindings/webcrypto/PhonyWorkQueue.cpp b/src/bun.js/bindings/webcrypto/PhonyWorkQueue.cpp new file mode 100644 index 00000000000000..325ce6ad0273e6 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/PhonyWorkQueue.cpp @@ -0,0 +1,21 @@ +#include "PhonyWorkQueue.h" + +#include +#include "EventLoopTaskNoContext.h" + +namespace Bun { + +Ref PhonyWorkQueue::create(WTF::ASCIILiteral name) +{ + (void)name; + return adoptRef(*new PhonyWorkQueue); +} + +extern "C" void ConcurrentCppTask__createAndRun(EventLoopTaskNoContext* task); + +void PhonyWorkQueue::dispatch(JSC::JSGlobalObject* globalObject, WTF::Function&& function) +{ + ConcurrentCppTask__createAndRun(new EventLoopTaskNoContext(globalObject, WTFMove(function))); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/webcrypto/PhonyWorkQueue.h b/src/bun.js/bindings/webcrypto/PhonyWorkQueue.h new file mode 100644 index 00000000000000..7d225bee3d74c9 --- /dev/null +++ b/src/bun.js/bindings/webcrypto/PhonyWorkQueue.h @@ -0,0 +1,19 @@ +#pragma once + +#include "root.h" +#include +#include + +namespace Bun { + +// Work queue which really uses CppTask.Concurrent in Bun's event loop (which enqueues into a WorkPool). +// Maintained so that SubtleCrypto functions can pretend they're using a WorkQueue, even though +// WTF::WorkQueue doesn't work and we need to use Bun's equivalent. +class PhonyWorkQueue : public WTF::RefCounted { +public: + static Ref create(WTF::ASCIILiteral name); + + void dispatch(JSC::JSGlobalObject* globalObject, Function&&); +}; + +}; // namespace Bun diff --git a/src/bun.js/bindings/webcrypto/SubtleCrypto.h b/src/bun.js/bindings/webcrypto/SubtleCrypto.h index dbfee4649bf5f2..cd4f68ddb48102 100644 --- a/src/bun.js/bindings/webcrypto/SubtleCrypto.h +++ b/src/bun.js/bindings/webcrypto/SubtleCrypto.h @@ -34,7 +34,7 @@ #include #include #include -#include +#include "PhonyWorkQueue.h" namespace JSC { class ArrayBufferView; @@ -44,6 +44,8 @@ class CallFrame; namespace WebCore { +using WorkQueue = Bun::PhonyWorkQueue; + struct JsonWebKey; class BufferSource; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index 242103874ec0ca..9b5bb002848ecd 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -304,6 +304,55 @@ pub const CppTask = opaque { Bun__performTask(global, this); } }; + +pub const ConcurrentCppTask = struct { + cpp_task: *EventLoopTaskNoContext, + workpool_task: JSC.WorkPoolTask = .{ .callback = &runFromWorkpool }, + + const EventLoopTaskNoContext = opaque { + extern fn Bun__EventLoopTaskNoContext__performTask(task: *EventLoopTaskNoContext) void; + extern fn Bun__EventLoopTaskNoContext__createdInBunVm(task: *const EventLoopTaskNoContext) ?*JSC.VirtualMachine; + + /// Deallocates `this` + pub fn run(this: *EventLoopTaskNoContext) void { + Bun__EventLoopTaskNoContext__performTask(this); + } + + /// Get the VM that created this task + pub fn getVM(this: *const EventLoopTaskNoContext) ?*JSC.VirtualMachine { + return Bun__EventLoopTaskNoContext__createdInBunVm(this); + } + }; + + pub fn runFromWorkpool(task: *JSC.WorkPoolTask) void { + var this: *ConcurrentCppTask = @fieldParentPtr("workpool_task", task); + // Extract all the info we need from `this` and `cpp_task` before we call functions that + // free them + const cpp_task = this.cpp_task; + const maybe_vm = cpp_task.getVM(); + this.destroy(); + cpp_task.run(); + if (maybe_vm) |vm| { + vm.event_loop.unrefConcurrently(); + } + } + + pub usingnamespace bun.New(@This()); + + pub export fn ConcurrentCppTask__createAndRun(cpp_task: *EventLoopTaskNoContext) void { + JSC.markBinding(@src()); + if (cpp_task.getVM()) |vm| { + vm.event_loop.refConcurrently(); + } + const cpp = ConcurrentCppTask.new(.{ .cpp_task = cpp_task }); + JSC.WorkPool.schedule(&cpp.workpool_task); + } +}; + +comptime { + _ = ConcurrentCppTask.ConcurrentCppTask__createAndRun; +} + pub const JSCScheduler = struct { pub const JSCDeferredWorkTask = opaque { extern fn Bun__runDeferredWork(task: *JSCScheduler.JSCDeferredWorkTask) void; @@ -792,6 +841,7 @@ pub const EventLoop = struct { debug: Debug = .{}, entered_event_loop_count: isize = 0, concurrent_ref: std.atomic.Value(i32) = std.atomic.Value(i32).init(0), + imminent_gc_timer: std.atomic.Value(?*JSC.BunTimer.WTFTimer) = .{ .raw = null }, signal_handler: if (Environment.isPosix) ?*PosixSignalHandle else void = if (Environment.isPosix) null, @@ -1360,6 +1410,12 @@ pub const EventLoop = struct { } } + if (this.entered_event_loop_count < 2) { + if (this.imminent_gc_timer.swap(null, .seq_cst)) |timer| { + timer.run(this.virtual_machine); + } + } + var concurrent = this.concurrent_tasks.popBatch(); const count = concurrent.count; if (count == 0) @@ -1434,7 +1490,7 @@ pub const EventLoop = struct { var event_loop_sleep_timer = if (comptime Environment.isDebug) std.time.Timer.start() catch unreachable; // for the printer, this is defined: var timespec: bun.timespec = if (Environment.isDebug) .{ .sec = 0, .nsec = 0 } else undefined; - loop.tickWithTimeout(if (ctx.timer.getTimeout(×pec)) ×pec else null); + loop.tickWithTimeout(if (ctx.timer.getTimeout(×pec, ctx)) ×pec else null); if (comptime Environment.isDebug) { log("tick {}, timeout: {}", .{ bun.fmt.fmtDuration(event_loop_sleep_timer.read()), bun.fmt.fmtDuration(timespec.ns()) }); @@ -1519,7 +1575,7 @@ pub const EventLoop = struct { this.processGCTimer(); var timespec: bun.timespec = undefined; - loop.tickWithTimeout(if (ctx.timer.getTimeout(×pec)) ×pec else null); + loop.tickWithTimeout(if (ctx.timer.getTimeout(×pec, ctx)) ×pec else null); } else { loop.tickWithoutIdle(); } diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 885af89e00bfc2..9adf470d670748 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -784,7 +784,7 @@ pub const VirtualMachine = struct { entry_point: ServerEntryPoint = undefined, origin: URL = URL{}, node_fs: ?*Node.NodeFS = null, - timer: Bun.Timer.All = .{}, + timer: Bun.Timer.All, event_loop_handle: ?*PlatformEventLoop = null, pending_unref_counter: i32 = 0, preload: []const string = &[_][]const u8{}, @@ -914,6 +914,8 @@ pub const VirtualMachine = struct { return BodyValueRef.init(body, &this.body_value_hive_allocator); } + pub threadlocal var is_bundler_thread_for_bytecode_cache: bool = false; + pub fn uwsLoop(this: *const VirtualMachine) *uws.Loop { if (comptime Environment.isPosix) { if (Environment.allow_assert) { @@ -1906,6 +1908,7 @@ pub const VirtualMachine = struct { .transpiler = transpiler, .console = console, .log = log, + .timer = JSC.BunTimer.All.init(), .origin = transpiler.options.origin, .saved_source_map_table = SavedSourceMap.HashTable.init(bun.default_allocator), .source_mappings = undefined, @@ -1928,6 +1931,7 @@ pub const VirtualMachine = struct { vm.regular_event_loop.next_immediate_tasks = EventLoop.Queue.init( default_allocator, ); + vm.regular_event_loop.virtual_machine = vm; vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; @@ -1952,6 +1956,7 @@ pub const VirtualMachine = struct { VMHolder.main_thread_vm = vm; } vm.global = ZigGlobalObject.create( + vm, vm.console, if (opts.is_main_thread) -1 else std.math.maxInt(i32), false, @@ -1959,8 +1964,8 @@ pub const VirtualMachine = struct { null, ); vm.regular_event_loop.global = vm.global; - vm.regular_event_loop.virtual_machine = vm; vm.jsc = vm.global.vm(); + uws.Loop.get().internal_loop_data.jsc_vm = vm.jsc; vm.configureDebugger(opts.debugger); vm.body_value_hive_allocator = BodyValueHiveAllocator.init(bun.typedAllocator(JSC.WebCore.Body.Value)); @@ -2023,7 +2028,11 @@ pub const VirtualMachine = struct { .transpiler = transpiler, .console = console, .log = log, + + .timer = JSC.BunTimer.All.init(), + .origin = transpiler.options.origin, + .saved_source_map_table = SavedSourceMap.HashTable.init(bun.default_allocator), .source_mappings = undefined, .macros = MacroMap.init(allocator), @@ -2044,6 +2053,7 @@ pub const VirtualMachine = struct { vm.regular_event_loop.next_immediate_tasks = EventLoop.Queue.init( default_allocator, ); + vm.regular_event_loop.virtual_machine = vm; vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; @@ -2063,6 +2073,7 @@ pub const VirtualMachine = struct { vm.transpiler.macro_context = js_ast.Macro.MacroContext.init(&vm.transpiler); vm.global = ZigGlobalObject.create( + vm, vm.console, if (opts.is_main_thread) -1 else std.math.maxInt(i32), opts.smol, @@ -2070,8 +2081,8 @@ pub const VirtualMachine = struct { null, ); vm.regular_event_loop.global = vm.global; - vm.regular_event_loop.virtual_machine = vm; vm.jsc = vm.global.vm(); + uws.Loop.get().internal_loop_data.jsc_vm = vm.jsc; vm.smol = opts.smol; vm.dns_result_order = opts.dns_result_order; @@ -2177,7 +2188,10 @@ pub const VirtualMachine = struct { .transpiler = transpiler, .console = console, .log = log, + + .timer = JSC.BunTimer.All.init(), .origin = transpiler.options.origin, + .saved_source_map_table = SavedSourceMap.HashTable.init(bun.default_allocator), .source_mappings = undefined, .macros = MacroMap.init(allocator), @@ -2200,6 +2214,7 @@ pub const VirtualMachine = struct { vm.regular_event_loop.next_immediate_tasks = EventLoop.Queue.init( default_allocator, ); + vm.regular_event_loop.virtual_machine = vm; vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; @@ -2224,6 +2239,7 @@ pub const VirtualMachine = struct { vm.transpiler.macro_context = js_ast.Macro.MacroContext.init(&vm.transpiler); vm.global = ZigGlobalObject.create( + vm, vm.console, @as(i32, @intCast(worker.execution_context_id)), worker.mini, @@ -2231,8 +2247,8 @@ pub const VirtualMachine = struct { worker.cpp_worker, ); vm.regular_event_loop.global = vm.global; - vm.regular_event_loop.virtual_machine = vm; vm.jsc = vm.global.vm(); + uws.Loop.get().internal_loop_data.jsc_vm = vm.jsc; vm.transpiler.setAllocator(allocator); vm.body_value_hive_allocator = BodyValueHiveAllocator.init(bun.typedAllocator(JSC.WebCore.Body.Value)); @@ -2269,6 +2285,7 @@ pub const VirtualMachine = struct { .transpiler = transpiler, .console = console, .log = log, + .timer = JSC.BunTimer.All.init(), .origin = transpiler.options.origin, .saved_source_map_table = SavedSourceMap.HashTable.init(bun.default_allocator), .source_mappings = undefined, @@ -2290,9 +2307,11 @@ pub const VirtualMachine = struct { vm.regular_event_loop.next_immediate_tasks = EventLoop.Queue.init( default_allocator, ); + vm.regular_event_loop.virtual_machine = vm; vm.regular_event_loop.tasks.ensureUnusedCapacity(64) catch unreachable; vm.regular_event_loop.concurrent_tasks = .{}; vm.event_loop = &vm.regular_event_loop; + vm.eventLoop().ensureWaker(); vm.transpiler.macro_context = null; vm.transpiler.resolver.store_fd = opts.store_fd; @@ -2308,7 +2327,6 @@ pub const VirtualMachine = struct { vm.transpiler.macro_context = js_ast.Macro.MacroContext.init(&vm.transpiler); - vm.regular_event_loop.virtual_machine = vm; vm.smol = opts.smol; if (opts.smol) diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 146ed7e35b3763..ad5eaa15eee46e 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1664,7 +1664,7 @@ pub const ModuleLoader = struct { const heap_access = if (!disable_transpilying) jsc_vm.jsc.releaseHeapAccess() else - JSC.VM.ReleaseHeapAccess{ .vm = jsc_vm.jsc, .needs_to_release = false }; + JSC.VM.ReleaseHeapAccess{ .vm = jsc_vm.jsc, .needs_to_acquire = false }; defer heap_access.acquire(); break :brk jsc_vm.transpiler.parseMaybeReturnFileOnly( diff --git a/src/bun.js/node/node_fs_stat_watcher.zig b/src/bun.js/node/node_fs_stat_watcher.zig index eacc939591971c..7579b5f7a6221d 100644 --- a/src/bun.js/node/node_fs_stat_watcher.zig +++ b/src/bun.js/node/node_fs_stat_watcher.zig @@ -86,19 +86,17 @@ pub const StatWatcherScheduler = struct { /// Set the timer (this function is not thread safe, should be called only from the main thread) fn setTimer(this: *StatWatcherScheduler, interval: i32) void { - // if the timer is active we need to remove it - if (this.event_loop_timer.state == .ACTIVE) { - this.vm.timer.remove(&this.event_loop_timer); - } - // if the interval is 0 means that we stop the timer if (interval == 0) { + // if the timer is active we need to remove it + if (this.event_loop_timer.state == .ACTIVE) { + this.vm.timer.remove(&this.event_loop_timer); + } return; } // reschedule the timer - this.event_loop_timer.next = bun.timespec.msFromNow(interval); - this.vm.timer.insert(&this.event_loop_timer); + this.vm.timer.update(&this.event_loop_timer, &bun.timespec.msFromNow(interval)); } /// Schedule a task to set the timer in the main thread diff --git a/src/bun.js/test/jest.zig b/src/bun.js/test/jest.zig index 74f5a44ca71f2c..a4bc37efdb785c 100644 --- a/src/bun.js/test/jest.zig +++ b/src/bun.js/test/jest.zig @@ -132,12 +132,13 @@ pub const TestRunner = struct { pub fn scheduleTimeout(this: *TestRunner, milliseconds: u32) void { const then = bun.timespec.msFromNow(@intCast(milliseconds)); const vm = JSC.VirtualMachine.get(); + + this.event_loop_timer.tag = .TestRunner; if (this.event_loop_timer.state == .ACTIVE) { vm.timer.remove(&this.event_loop_timer); } this.event_loop_timer.next = then; - this.event_loop_timer.tag = .TestRunner; vm.timer.insert(&this.event_loop_timer); } diff --git a/src/bundler/bundle_v2.zig b/src/bundler/bundle_v2.zig index 9b2ca8f32c074c..066843e3231266 100644 --- a/src/bundler/bundle_v2.zig +++ b/src/bundler/bundle_v2.zig @@ -13571,6 +13571,7 @@ pub const LinkerContext = struct { .js; if (loader.isJavaScriptLike()) { + JSC.VirtualMachine.is_bundler_thread_for_bytecode_cache = true; JSC.initialize(false); var fdpath: bun.PathBuffer = undefined; var source_provider_url = try bun.String.createFormat("{s}" ++ bun.bytecode_extension, .{chunk.final_rel_path}); @@ -13915,6 +13916,7 @@ pub const LinkerContext = struct { .js; if (loader.isJavaScriptLike()) { + JSC.VirtualMachine.is_bundler_thread_for_bytecode_cache = true; JSC.initialize(false); var fdpath: bun.PathBuffer = undefined; var source_provider_url = try bun.String.createFormat("{s}" ++ bun.bytecode_extension, .{chunk.final_rel_path}); diff --git a/test/bun.lock b/test/bun.lock index 4a801e05dd97b0..4ebc6c9bced5bb 100644 --- a/test/bun.lock +++ b/test/bun.lock @@ -23,6 +23,7 @@ "axios": "1.6.8", "body-parser": "1.20.2", "comlink": "4.4.1", + "commander": "12.1.0", "devalue": "5.1.1", "es-module-lexer": "1.3.0", "esbuild": "0.18.6", @@ -892,7 +893,7 @@ "comlink": ["comlink@4.4.1", "", {}, "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q=="], - "commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], @@ -2524,6 +2525,8 @@ "webpack-cli/colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], + "webpack-cli/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], diff --git a/test/js/node/http/node-http-ref-fixture.js b/test/js/node/http/node-http-ref-fixture.js index 78ef0984296023..b5fd7d55d28ffc 100644 --- a/test/js/node/http/node-http-ref-fixture.js +++ b/test/js/node/http/node-http-ref-fixture.js @@ -1,8 +1,9 @@ import { createServer } from "http"; +const SIGNAL = process.platform === "linux" ? "SIGUSR2" : "SIGUSR1"; var server = createServer((req, res) => { res.end(); }).listen(0, async (err, hostname, port) => { - process.on("SIGUSR1", async () => { + process.on(SIGNAL, async () => { server.unref(); // check that the server is still running @@ -15,5 +16,5 @@ var server = createServer((req, res) => { if (resp.status !== 200) { process.exit(42); } - process.kill(process.pid, "SIGUSR1"); + process.kill(process.pid, SIGNAL); }); diff --git a/test/js/node/process/process-signal-handler.fixture.js b/test/js/node/process/process-signal-handler.fixture.js index 77d9d09f4c59c5..8d2990fd025e47 100644 --- a/test/js/node/process/process-signal-handler.fixture.js +++ b/test/js/node/process/process-signal-handler.fixture.js @@ -49,20 +49,21 @@ function done2() { } } -const SIGUSR1 = os.constants.signals.SIGUSR1; const SIGUSR2 = os.constants.signals.SIGUSR2; switch (process.argv.at(-1)) { case "SIGUSR1": { - process.on("SIGUSR1", function () { - checkSignal("SIGUSR1", arguments); + const signalName = process.platform === "linux" ? "SIGUSR2" : "SIGUSR1"; + const signalNumber = os.constants.signals[signalName]; + process.on(signalName, function () { + checkSignal(signalName, arguments); done(); }); - process.on("SIGUSR1", function () { - checkSignal("SIGUSR1", arguments); + process.on(signalName, function () { + checkSignal(signalName, arguments); done(); }); - raise(SIGUSR1); + raise(signalNumber); break; } case "SIGUSR2": { diff --git a/test/js/node/test/parallel/test-signal-handler.js b/test/js/node/test/parallel/test-signal-handler.js index 05ec4e7f73faf5..68c97586377214 100644 --- a/test/js/node/test/parallel/test-signal-handler.js +++ b/test/js/node/test/parallel/test-signal-handler.js @@ -30,9 +30,12 @@ if (!common.isMainThread) console.log(`process.pid: ${process.pid}`); -process.on('SIGUSR1', common.mustCall()); +// On Bun in Linux, SIGUSR1 is reserved for the GC. +// So we need to use a different signal. +const SIGNAL = process.platform === 'linux' ? 'SIGUSR2' : 'SIGUSR1'; -process.on('SIGUSR1', common.mustCall(function() { +process.on(SIGNAL, common.mustCall()); +process.on(SIGNAL, common.mustCall(function() { setTimeout(function() { console.log('End.'); process.exit(0); @@ -44,7 +47,7 @@ setInterval(function() { console.log(`running process...${++i}`); if (i === 5) { - process.kill(process.pid, 'SIGUSR1'); + process.kill(process.pid, SIGNAL); } }, 1); diff --git a/test/js/node/util/test-aborted.test.ts b/test/js/node/util/test-aborted.test.ts index d498681f317a25..11af6f00e04d5f 100644 --- a/test/js/node/util/test-aborted.test.ts +++ b/test/js/node/util/test-aborted.test.ts @@ -37,15 +37,32 @@ test("aborted works when provided a resource that was not already aborted", asyn test("aborted with gc cleanup", async () => { const ac = new AbortController(); - const abortedPromise = aborted(ac.signal, {}); + let finalized = false; + // make a FinalizationRegistry to tell us when the second argument to aborted() + // has been garbage collected + const registry = new FinalizationRegistry(() => { + finalized = true; + }); + const abortedPromise = (() => { + const gcMe = {}; + registry.register(gcMe, undefined); + const abortedPromise = aborted(ac.signal, gcMe); + return abortedPromise; + // gcMe is now out of scope and eligible to be collected + })(); + abortedPromise.then(() => { + throw new Error("this promise should never resolve"); + }); - await new Promise(resolve => setImmediate(resolve)); - Bun.gc(true); + // wait for the object to be GC'd by ticking the event loop and forcing garbage collection + while (!finalized) { + await new Promise(resolve => setImmediate(resolve)); + Bun.gc(true); + } ac.abort(); expect(ac.signal.aborted).toBe(true); expect(getEventListeners(ac.signal, "abort").length).toBe(0); - return expect(await abortedPromise).toBeUndefined(); }); test("fails with error if not provided abort signal", async () => { diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index 3068b9c507fe87..9842f190e33c21 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -241,7 +241,8 @@ describe("napi", () => { describe("napi_threadsafe_function", () => { it("keeps the event loop alive without async_work", () => { - checkSameOutput("test_promise_with_threadsafe_function", []); + const result = checkSameOutput("test_promise_with_threadsafe_function", []); + expect(result).toBe("tsfn_callback\n0\nresolved to 1234\nresolved to 1234\ntsfn_finalize_callback"); }); it("does not hang on finalize", () => { diff --git a/test/package.json b/test/package.json index c957976297aa3e..03698757183f2b 100644 --- a/test/package.json +++ b/test/package.json @@ -28,6 +28,7 @@ "axios": "1.6.8", "body-parser": "1.20.2", "comlink": "4.4.1", + "commander": "12.1.0", "devalue": "5.1.1", "es-module-lexer": "1.3.0", "esbuild": "0.18.6", diff --git a/test/regression/issue/14982/14982.test.ts b/test/regression/issue/14982/14982.test.ts new file mode 100644 index 00000000000000..c0a683d518b378 --- /dev/null +++ b/test/regression/issue/14982/14982.test.ts @@ -0,0 +1,18 @@ +import { expect, it, describe } from "bun:test"; +import { bunEnv, bunExe } from "../../../harness"; +import { join } from "path"; + +describe("issue 14982", () => { + it("does not hang in commander", async () => { + const process = Bun.spawn([bunExe(), join(__dirname, "commander-hang.fixture.ts"), "test"], { + stdin: "inherit", + stdout: "pipe", + stderr: "inherit", + cwd: __dirname, + env: bunEnv, + }); + await process.exited; + expect(process.exitCode).toBe(0); + expect(await new Response(process.stdout).text()).toBe("Test command\n"); + }, 15000); +}); diff --git a/test/regression/issue/14982/commander-hang.fixture-test.ts b/test/regression/issue/14982/commander-hang.fixture-test.ts new file mode 100644 index 00000000000000..827af4a6629102 --- /dev/null +++ b/test/regression/issue/14982/commander-hang.fixture-test.ts @@ -0,0 +1,8 @@ +import { Command } from "commander"; + +new Command("test") + .action(() => { + console.log("Test command"); + process.exit(0); + }) + .parse(); diff --git a/test/regression/issue/14982/commander-hang.fixture.ts b/test/regression/issue/14982/commander-hang.fixture.ts new file mode 100644 index 00000000000000..c29795dcbe4bcf --- /dev/null +++ b/test/regression/issue/14982/commander-hang.fixture.ts @@ -0,0 +1,4 @@ +import { program } from "commander"; + +// loads ./commander-hang.fixture-test.ts +program.name("test").command("test", "Test command").parse();