diff --git a/cmake/sources/ZigSources.txt b/cmake/sources/ZigSources.txt index 7fc059629f..7ffe24fafb 100644 --- a/cmake/sources/ZigSources.txt +++ b/cmake/sources/ZigSources.txt @@ -73,6 +73,10 @@ src/bun.js/api/server/StaticRoute.zig src/bun.js/api/server/WebSocketServerContext.zig src/bun.js/api/streams.classes.zig src/bun.js/api/Timer.zig +src/bun.js/api/Timer/EventLoopTimer.zig +src/bun.js/api/Timer/ImmediateObject.zig +src/bun.js/api/Timer/TimeoutObject.zig +src/bun.js/api/Timer/TimerObjectInternals.zig src/bun.js/api/TOMLObject.zig src/bun.js/api/UnsafeObject.zig src/bun.js/bindgen_test.zig diff --git a/src/bun.js/api/Timer.zig b/src/bun.js/api/Timer.zig index 7e21663a13..df132e86a9 100644 --- a/src/bun.js/api/Timer.zig +++ b/src/bun.js/api/Timer.zig @@ -5,13 +5,9 @@ const VirtualMachine = JSC.VirtualMachine; const JSValue = JSC.JSValue; const JSError = bun.JSError; const JSGlobalObject = JSC.JSGlobalObject; -const Debugger = JSC.Debugger; const Environment = bun.Environment; const uv = bun.windows.libuv; -const api = bun.api; -const StatWatcherScheduler = @import("../node/node_fs_stat_watcher.zig").StatWatcherScheduler; const Timer = @This(); -const DNSResolver = @import("./bun/dns_resolver.zig").DNSResolver; /// TimeoutMap is map of i32 to nullable Timeout structs /// i32 is exposed to JavaScript and can be used with clearTimeout, clearInterval, etc. @@ -548,698 +544,11 @@ pub const All = struct { } }; -const uws = bun.uws; +pub const EventLoopTimer = @import("./Timer/EventLoopTimer.zig"); -pub const TimeoutObject = struct { - const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); - pub const ref = RefCount.ref; - pub const deref = RefCount.deref; - - pub const js = JSC.Codegen.JSTimeout; - pub const toJS = js.toJS; - pub const fromJS = js.fromJS; - pub const fromJSDirect = js.fromJSDirect; - - ref_count: RefCount, - event_loop_timer: EventLoopTimer = .{ - .next = .{}, - .tag = .TimeoutObject, - }, - internals: TimerObjectInternals, - - pub fn init( - globalThis: *JSGlobalObject, - id: i32, - kind: Kind, - interval: u31, - callback: JSValue, - arguments: JSValue, - ) JSValue { - // internals are initialized by init() - const timeout = bun.new(TimeoutObject, .{ .ref_count = .init(), .internals = undefined }); - const js_value = timeout.toJS(globalThis); - defer js_value.ensureStillAlive(); - timeout.internals.init( - js_value, - globalThis, - id, - kind, - interval, - callback, - arguments, - ); - - if (globalThis.bunVM().isInspectorEnabled()) { - Debugger.didScheduleAsyncCall( - globalThis, - .DOMTimer, - ID.asyncID(.{ .id = id, .kind = kind.big() }), - kind != .setInterval, - ); - } - - return js_value; - } - - fn deinit(this: *TimeoutObject) void { - this.internals.deinit(); - bun.destroy(this); - } - - pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) !*TimeoutObject { - _ = callFrame; - return globalObject.throw("Timeout is not constructible", .{}); - } - - pub fn toPrimitive(this: *TimeoutObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.toPrimitive(); - } - - pub fn doRef(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doRef(globalThis, callFrame.this()); - } - - pub fn doUnref(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doUnref(globalThis, callFrame.this()); - } - - pub fn doRefresh(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doRefresh(globalThis, callFrame.this()); - } - - pub fn hasRef(this: *TimeoutObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.hasRef(); - } - - pub fn finalize(this: *TimeoutObject) void { - this.internals.finalize(); - } - - pub fn getDestroyed(this: *TimeoutObject, globalThis: *JSGlobalObject) JSValue { - _ = globalThis; - return .jsBoolean(this.internals.getDestroyed()); - } - - pub fn close(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) JSValue { - this.internals.cancel(globalThis.bunVM()); - return callFrame.this(); - } - - pub fn get_onTimeout(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { - return TimeoutObject.js.callbackGetCached(thisValue).?; - } - - pub fn set_onTimeout(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { - TimeoutObject.js.callbackSetCached(thisValue, globalThis, value); - } - - pub fn get_idleTimeout(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { - return TimeoutObject.js.idleTimeoutGetCached(thisValue).?; - } - - pub fn set_idleTimeout(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { - TimeoutObject.js.idleTimeoutSetCached(thisValue, globalThis, value); - } - - pub fn get_repeat(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { - return TimeoutObject.js.repeatGetCached(thisValue).?; - } - - pub fn set_repeat(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { - TimeoutObject.js.repeatSetCached(thisValue, globalThis, value); - } - - pub fn dispose(this: *TimeoutObject, globalThis: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - this.internals.cancel(globalThis.bunVM()); - return .js_undefined; - } -}; - -pub const ImmediateObject = struct { - const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); - pub const ref = RefCount.ref; - pub const deref = RefCount.deref; - - pub const js = JSC.Codegen.JSImmediate; - pub const toJS = js.toJS; - pub const fromJS = js.fromJS; - pub const fromJSDirect = js.fromJSDirect; - - ref_count: RefCount, - event_loop_timer: EventLoopTimer = .{ - .next = .{}, - .tag = .ImmediateObject, - }, - internals: TimerObjectInternals, - - pub fn init( - globalThis: *JSGlobalObject, - id: i32, - callback: JSValue, - arguments: JSValue, - ) JSValue { - // internals are initialized by init() - const immediate = bun.new(ImmediateObject, .{ .ref_count = .init(), .internals = undefined }); - const js_value = immediate.toJS(globalThis); - defer js_value.ensureStillAlive(); - immediate.internals.init( - js_value, - globalThis, - id, - .setImmediate, - 0, - callback, - arguments, - ); - - if (globalThis.bunVM().isInspectorEnabled()) { - Debugger.didScheduleAsyncCall( - globalThis, - .DOMTimer, - ID.asyncID(.{ .id = id, .kind = .setImmediate }), - true, - ); - } - - return js_value; - } - - fn deinit(this: *ImmediateObject) void { - this.internals.deinit(); - bun.destroy(this); - } - - pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) !*ImmediateObject { - _ = callFrame; - return globalObject.throw("Immediate is not constructible", .{}); - } - - /// returns true if an exception was thrown - pub fn runImmediateTask(this: *ImmediateObject, vm: *VirtualMachine) bool { - return this.internals.runImmediateTask(vm); - } - - pub fn toPrimitive(this: *ImmediateObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.toPrimitive(); - } - - pub fn doRef(this: *ImmediateObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doRef(globalThis, callFrame.this()); - } - - pub fn doUnref(this: *ImmediateObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.doUnref(globalThis, callFrame.this()); - } - - pub fn hasRef(this: *ImmediateObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - return this.internals.hasRef(); - } - - pub fn finalize(this: *ImmediateObject) void { - this.internals.finalize(); - } - - pub fn getDestroyed(this: *ImmediateObject, globalThis: *JSGlobalObject) JSValue { - _ = globalThis; - return .jsBoolean(this.internals.getDestroyed()); - } - - pub fn dispose(this: *ImmediateObject, globalThis: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { - this.internals.cancel(globalThis.bunVM()); - return .js_undefined; - } -}; - -/// Data that TimerObject and ImmediateObject have in common -pub const TimerObjectInternals = struct { - /// Identifier for this timer that is exposed to JavaScript (by `+timer`) - id: i32 = -1, - interval: u31 = 0, - strong_this: JSC.Strong.Optional = .empty, - flags: Flags = .{}, - - const Flags = packed struct(u32) { - /// Whenever a timer is inserted into the heap (which happen on creation or refresh), the global - /// epoch is incremented and the new epoch is set on the timer. For timers created by - /// JavaScript, the epoch is used to break ties between timers scheduled for the same - /// millisecond. This ensures that if you set two timers for the same amount of time, and - /// refresh the first one, the first one will fire last. This mimics Node.js's behavior where - /// the refreshed timer will be inserted at the end of a list, which makes it fire later. - epoch: u25 = 0, - - kind: Kind = .setTimeout, - - // we do not allow the timer to be refreshed after we call clearInterval/clearTimeout - has_cleared_timer: bool = false, - is_keeping_event_loop_alive: bool = false, - - // if they never access the timer by integer, don't create a hashmap entry. - has_accessed_primitive: bool = false, - - has_js_ref: bool = true, - - /// Set to `true` only during execution of the JavaScript function so that `_destroyed` can be - /// false during the callback, even though the `state` will be `FIRED`. - in_callback: bool = false, - }; - - fn eventLoopTimer(this: *TimerObjectInternals) *EventLoopTimer { - switch (this.flags.kind) { - .setImmediate => { - const parent: *ImmediateObject = @fieldParentPtr("internals", this); - assert(parent.event_loop_timer.tag == .ImmediateObject); - return &parent.event_loop_timer; - }, - .setTimeout, .setInterval => { - const parent: *TimeoutObject = @fieldParentPtr("internals", this); - assert(parent.event_loop_timer.tag == .TimeoutObject); - return &parent.event_loop_timer; - }, - } - } - - fn ref(this: *TimerObjectInternals) void { - switch (this.flags.kind) { - .setImmediate => @as(*ImmediateObject, @fieldParentPtr("internals", this)).ref(), - .setTimeout, .setInterval => @as(*TimeoutObject, @fieldParentPtr("internals", this)).ref(), - } - } - - fn deref(this: *TimerObjectInternals) void { - switch (this.flags.kind) { - .setImmediate => @as(*ImmediateObject, @fieldParentPtr("internals", this)).deref(), - .setTimeout, .setInterval => @as(*TimeoutObject, @fieldParentPtr("internals", this)).deref(), - } - } - - extern "c" fn Bun__JSTimeout__call(globalObject: *JSC.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue) bool; - - /// returns true if an exception was thrown - pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool { - if (this.flags.has_cleared_timer or - // unref'd setImmediate callbacks should only run if there are things keeping the event - // loop alive other than setImmediates - (!this.flags.is_keeping_event_loop_alive and !vm.isEventLoopAliveExcludingImmediates())) - { - this.deref(); - return false; - } - - const timer = this.strong_this.get() orelse { - if (Environment.isDebug) { - @panic("TimerObjectInternals.runImmediateTask: this_object is null"); - } - return false; - }; - const globalThis = vm.global; - this.strong_this.deinit(); - this.eventLoopTimer().state = .FIRED; - this.setEnableKeepingEventLoopAlive(vm, false); - - vm.eventLoop().enter(); - const callback = ImmediateObject.js.callbackGetCached(timer).?; - const arguments = ImmediateObject.js.argumentsGetCached(timer).?; - this.ref(); - const exception_thrown = this.run(globalThis, timer, callback, arguments, this.asyncID(), vm); - this.deref(); - - if (this.eventLoopTimer().state == .FIRED) { - this.deref(); - } - - vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown); - - return exception_thrown; - } - - pub fn asyncID(this: *const TimerObjectInternals) u64 { - return ID.asyncID(.{ .id = this.id, .kind = this.flags.kind.big() }); - } - - pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *JSC.VirtualMachine) EventLoopTimer.Arm { - const id = this.id; - const kind = this.flags.kind.big(); - const async_id: ID = .{ .id = id, .kind = kind }; - const has_been_cleared = this.eventLoopTimer().state == .CANCELLED or this.flags.has_cleared_timer or vm.scriptExecutionStatus() != .running; - - this.eventLoopTimer().state = .FIRED; - - const globalThis = vm.global; - const this_object = this.strong_this.get().?; - - const callback: JSValue, const arguments: JSValue, var idle_timeout: JSValue, var repeat: JSValue = switch (kind) { - .setImmediate => .{ - ImmediateObject.js.callbackGetCached(this_object).?, - ImmediateObject.js.argumentsGetCached(this_object).?, - .js_undefined, - .js_undefined, - }, - .setTimeout, .setInterval => .{ - TimeoutObject.js.callbackGetCached(this_object).?, - TimeoutObject.js.argumentsGetCached(this_object).?, - TimeoutObject.js.idleTimeoutGetCached(this_object).?, - TimeoutObject.js.repeatGetCached(this_object).?, - }, - }; - - if (has_been_cleared or !callback.toBoolean()) { - if (vm.isInspectorEnabled()) { - Debugger.didCancelAsyncCall(globalThis, .DOMTimer, ID.asyncID(async_id)); - } - this.setEnableKeepingEventLoopAlive(vm, false); - this.flags.has_cleared_timer = true; - this.strong_this.deinit(); - this.deref(); - - return .disarm; - } - - var time_before_call: timespec = undefined; - - if (kind != .setInterval) { - this.strong_this.clearWithoutDeallocation(); - } else { - time_before_call = timespec.msFromNow(this.interval); - } - this_object.ensureStillAlive(); - - vm.eventLoop().enter(); - { - // Ensure it stays alive for this scope. - this.ref(); - defer this.deref(); - - _ = this.run(globalThis, this_object, callback, arguments, ID.asyncID(async_id), vm); - - switch (kind) { - .setTimeout, .setInterval => { - idle_timeout = TimeoutObject.js.idleTimeoutGetCached(this_object).?; - repeat = TimeoutObject.js.repeatGetCached(this_object).?; - }, - else => {}, - } - - const is_timer_done = is_timer_done: { - // Node doesn't drain microtasks after each timer callback. - if (kind == .setInterval) { - if (!this.shouldRescheduleTimer(repeat, idle_timeout)) { - break :is_timer_done true; - } - switch (this.eventLoopTimer().state) { - .FIRED => { - // If we didn't clear the setInterval, reschedule it starting from - vm.timer.update(this.eventLoopTimer(), &time_before_call); - - if (this.flags.has_js_ref) { - this.setEnableKeepingEventLoopAlive(vm, true); - } - - // The ref count doesn't change. It wasn't decremented. - }, - .ACTIVE => { - // The developer called timer.refresh() synchronously in the callback. - vm.timer.update(this.eventLoopTimer(), &time_before_call); - - // Balance out the ref count. - // the transition from "FIRED" -> "ACTIVE" caused it to increment. - this.deref(); - }, - else => { - break :is_timer_done true; - }, - } - } else { - if (kind == .setTimeout and !repeat.isNull()) { - if (idle_timeout.getNumber()) |num| { - if (num != -1) { - this.convertToInterval(globalThis, this_object, repeat); - break :is_timer_done false; - } - } - } - - if (this.eventLoopTimer().state == .FIRED) { - break :is_timer_done true; - } - } - - break :is_timer_done false; - }; - - if (is_timer_done) { - this.setEnableKeepingEventLoopAlive(vm, false); - // The timer will not be re-entered into the event loop at this point. - this.deref(); - } - } - vm.eventLoop().exit(); - - return .disarm; - } - - fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer: JSValue, repeat: JSValue) void { - bun.debugAssert(this.flags.kind == .setTimeout); - - const vm = global.bunVM(); - - const new_interval: u31 = if (repeat.getNumber()) |num| if (num < 1 or num > std.math.maxInt(u31)) 1 else @intFromFloat(num) else 1; - - // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613 - TimeoutObject.js.idleTimeoutSetCached(timer, global, repeat); - this.strong_this.set(global, timer); - this.flags.kind = .setInterval; - this.interval = new_interval; - this.reschedule(timer, vm); - } - - pub fn run(this: *TimerObjectInternals, globalThis: *JSC.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue, async_id: u64, vm: *JSC.VirtualMachine) bool { - if (vm.isInspectorEnabled()) { - Debugger.willDispatchAsyncCall(globalThis, .DOMTimer, async_id); - } - - defer { - if (vm.isInspectorEnabled()) { - Debugger.didDispatchAsyncCall(globalThis, .DOMTimer, async_id); - } - } - - // Bun__JSTimeout__call handles exceptions. - this.flags.in_callback = true; - defer this.flags.in_callback = false; - return Bun__JSTimeout__call(globalThis, timer, callback, arguments); - } - - pub fn init( - this: *TimerObjectInternals, - timer: JSValue, - global: *JSGlobalObject, - id: i32, - kind: Kind, - interval: u31, - callback: JSValue, - arguments: JSValue, - ) void { - const vm = global.bunVM(); - this.* = .{ - .id = id, - .flags = .{ .kind = kind, .epoch = vm.timer.epoch }, - .interval = interval, - }; - - if (kind == .setImmediate) { - ImmediateObject.js.argumentsSetCached(timer, global, arguments); - ImmediateObject.js.callbackSetCached(timer, global, callback); - const parent: *ImmediateObject = @fieldParentPtr("internals", this); - vm.enqueueImmediateTask(parent); - this.setEnableKeepingEventLoopAlive(vm, true); - // ref'd by event loop - parent.ref(); - } else { - TimeoutObject.js.argumentsSetCached(timer, global, arguments); - TimeoutObject.js.callbackSetCached(timer, global, callback); - TimeoutObject.js.idleTimeoutSetCached(timer, global, JSC.jsNumber(interval)); - TimeoutObject.js.repeatSetCached(timer, global, if (kind == .setInterval) JSC.jsNumber(interval) else .null); - - // this increments the refcount - this.reschedule(timer, vm); - } - - this.strong_this.set(global, timer); - } - - pub fn doRef(this: *TimerObjectInternals, _: *JSC.JSGlobalObject, this_value: JSValue) JSValue { - this_value.ensureStillAlive(); - - const did_have_js_ref = this.flags.has_js_ref; - this.flags.has_js_ref = true; - - // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L256 - // and - // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L685-L687 - if (!did_have_js_ref and !this.flags.has_cleared_timer) { - this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), true); - } - - return this_value; - } - - pub fn doRefresh(this: *TimerObjectInternals, globalObject: *JSC.JSGlobalObject, this_value: JSValue) JSValue { - // Immediates do not have a refresh function, and our binding generator should not let this - // function be reached even if you override the `this` value calling a Timeout object's - // `refresh` method - assert(this.flags.kind != .setImmediate); - - // setImmediate does not support refreshing and we do not support refreshing after cleanup - if (this.id == -1 or this.flags.kind == .setImmediate or this.flags.has_cleared_timer) { - return this_value; - } - - this.strong_this.set(globalObject, this_value); - this.reschedule(this_value, VirtualMachine.get()); - - return this_value; - } - - pub fn doUnref(this: *TimerObjectInternals, _: *JSC.JSGlobalObject, this_value: JSValue) JSValue { - this_value.ensureStillAlive(); - - const did_have_js_ref = this.flags.has_js_ref; - this.flags.has_js_ref = false; - - if (did_have_js_ref) { - this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), false); - } - - return this_value; - } - - pub fn cancel(this: *TimerObjectInternals, vm: *VirtualMachine) void { - this.setEnableKeepingEventLoopAlive(vm, false); - this.flags.has_cleared_timer = true; - - if (this.flags.kind == .setImmediate) return; - - const was_active = this.eventLoopTimer().state == .ACTIVE; - - this.eventLoopTimer().state = .CANCELLED; - this.strong_this.deinit(); - - if (was_active) { - vm.timer.remove(this.eventLoopTimer()); - this.deref(); - } - } - - fn shouldRescheduleTimer(this: *TimerObjectInternals, repeat: JSValue, idle_timeout: JSValue) bool { - if (this.flags.kind == .setInterval and repeat.isNull()) return false; - if (idle_timeout.getNumber()) |num| { - if (num == -1) return false; - } - return true; - } - - pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachine) void { - if (this.flags.kind == .setImmediate) return; - - const idle_timeout = TimeoutObject.js.idleTimeoutGetCached(timer).?; - const repeat = TimeoutObject.js.repeatGetCached(timer).?; - - // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L612 - if (!this.shouldRescheduleTimer(repeat, idle_timeout)) return; - - const now = timespec.msFromNow(this.interval); - const was_active = this.eventLoopTimer().state == .ACTIVE; - if (was_active) { - vm.timer.remove(this.eventLoopTimer()); - } else { - this.ref(); - } - - vm.timer.update(this.eventLoopTimer(), &now); - this.flags.has_cleared_timer = false; - - if (this.flags.has_js_ref) { - this.setEnableKeepingEventLoopAlive(vm, true); - } - } - - fn setEnableKeepingEventLoopAlive(this: *TimerObjectInternals, vm: *VirtualMachine, enable: bool) void { - if (this.flags.is_keeping_event_loop_alive == enable) { - return; - } - this.flags.is_keeping_event_loop_alive = enable; - switch (this.flags.kind) { - .setTimeout, .setInterval => vm.timer.incrementTimerRef(if (enable) 1 else -1), - - // setImmediate has slightly different event loop logic - .setImmediate => vm.timer.incrementImmediateRef(if (enable) 1 else -1), - } - } - - pub fn hasRef(this: *TimerObjectInternals) JSValue { - return JSValue.jsBoolean(this.flags.is_keeping_event_loop_alive); - } - - pub fn toPrimitive(this: *TimerObjectInternals) bun.JSError!JSValue { - if (!this.flags.has_accessed_primitive) { - this.flags.has_accessed_primitive = true; - const vm = VirtualMachine.get(); - try vm.timer.maps.get(this.flags.kind).put(bun.default_allocator, this.id, this.eventLoopTimer()); - } - return JSValue.jsNumber(this.id); - } - - /// This is the getter for `_destroyed` on JS Timeout and Immediate objects - pub fn getDestroyed(this: *TimerObjectInternals) bool { - if (this.flags.has_cleared_timer) { - return true; - } - if (this.flags.in_callback) { - return false; - } - return switch (this.eventLoopTimer().state) { - .ACTIVE, .PENDING => false, - .FIRED, .CANCELLED => true, - }; - } - - pub fn finalize(this: *TimerObjectInternals) void { - this.strong_this.deinit(); - this.deref(); - } - - pub fn deinit(this: *TimerObjectInternals) void { - this.strong_this.deinit(); - const vm = VirtualMachine.get(); - const kind = this.flags.kind; - - if (this.eventLoopTimer().state == .ACTIVE) { - vm.timer.remove(this.eventLoopTimer()); - } - - if (this.flags.has_accessed_primitive) { - const map = vm.timer.maps.get(kind); - if (map.orderedRemove(this.id)) { - // If this array gets large, let's shrink it down - // Array keys are i32 - // Values are 1 ptr - // Therefore, 12 bytes per entry - // So if you created 21,000 timers and accessed them by ID, you'd be using 252KB - const allocated_bytes = map.capacity() * @sizeOf(TimeoutMap.Data); - const used_bytes = map.count() * @sizeOf(TimeoutMap.Data); - if (allocated_bytes - used_bytes > 256 * 1024) { - map.shrinkAndFree(bun.default_allocator, map.count() + 8); - } - } - } - - this.setEnableKeepingEventLoopAlive(vm, false); - switch (kind) { - .setImmediate => (@as(*ImmediateObject, @fieldParentPtr("internals", this))).ref_count.assertNoRefs(), - .setTimeout, .setInterval => (@as(*TimeoutObject, @fieldParentPtr("internals", this))).ref_count.assertNoRefs(), - } - } -}; +pub const TimeoutObject = @import("./Timer/TimeoutObject.zig"); +pub const ImmediateObject = @import("./Timer/ImmediateObject.zig"); +pub const TimerObjectInternals = @import("./Timer/TimerObjectInternals.zig"); pub const Kind = enum(u2) { setTimeout = 0, @@ -1275,235 +584,6 @@ pub const ID = extern struct { const assert = bun.assert; const heap = bun.io.heap; -pub const EventLoopTimer = struct { - /// The absolute time to fire this timer next. - next: timespec, - state: State = .PENDING, - tag: Tag, - /// Internal heap fields. - heap: heap.IntrusiveField(EventLoopTimer) = .{}, - - pub fn initPaused(tag: Tag) EventLoopTimer { - return .{ - .next = .{}, - .tag = tag, - }; - } - - pub fn less(_: void, a: *const EventLoopTimer, b: *const EventLoopTimer) bool { - const sec_order = std.math.order(a.next.sec, b.next.sec); - if (sec_order != .eq) return sec_order == .lt; - - // collapse sub-millisecond precision for JavaScript timers - const maybe_a_internals = a.jsTimerInternals(); - const maybe_b_internals = b.jsTimerInternals(); - var a_ns = a.next.nsec; - var b_ns = b.next.nsec; - if (maybe_a_internals != null) a_ns = std.time.ns_per_ms * @divTrunc(a_ns, std.time.ns_per_ms); - if (maybe_b_internals != null) b_ns = std.time.ns_per_ms * @divTrunc(b_ns, std.time.ns_per_ms); - - const order = std.math.order(a_ns, b_ns); - if (order == .eq) { - if (maybe_a_internals) |a_internals| { - if (maybe_b_internals) |b_internals| { - // We expect that the epoch will overflow sometimes. - // If it does, we would ideally like timers with an epoch from before the - // overflow to be sorted *before* timers with an epoch from after the overflow - // (even though their epoch will be numerically *larger*). - // - // Wrapping subtraction gives us a distance that is consistent even if one - // epoch has overflowed and the other hasn't. If the distance from a to b is - // small, it's likely that b is really newer than a, so we consider a less than - // b. If the distance from a to b is large (greater than half the u25 range), - // it's more likely that b is older than a so the true distance is from b to a. - return b_internals.flags.epoch -% a_internals.flags.epoch < std.math.maxInt(u25) / 2; - } - } - } - return order == .lt; - } - - pub const Tag = if (Environment.isWindows) enum { - TimerCallback, - TimeoutObject, - ImmediateObject, - TestRunner, - StatWatcherScheduler, - UpgradedDuplex, - DNSResolver, - WindowsNamedPipe, - WTFTimer, - PostgresSQLConnectionTimeout, - PostgresSQLConnectionMaxLifetime, - ValkeyConnectionTimeout, - ValkeyConnectionReconnect, - SubprocessTimeout, - DevServerSweepSourceMaps, - DevServerMemoryVisualizerTick, - - pub fn Type(comptime T: Tag) type { - return switch (T) { - .TimerCallback => TimerCallback, - .TimeoutObject => TimeoutObject, - .ImmediateObject => ImmediateObject, - .TestRunner => JSC.Jest.TestRunner, - .StatWatcherScheduler => StatWatcherScheduler, - .UpgradedDuplex => uws.UpgradedDuplex, - .DNSResolver => DNSResolver, - .WindowsNamedPipe => uws.WindowsNamedPipe, - .WTFTimer => WTFTimer, - .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, - .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, - .SubprocessTimeout => JSC.Subprocess, - .ValkeyConnectionReconnect => JSC.API.Valkey, - .ValkeyConnectionTimeout => JSC.API.Valkey, - .DevServerSweepSourceMaps, - .DevServerMemoryVisualizerTick, - => bun.bake.DevServer, - }; - } - } else enum { - TimerCallback, - TimeoutObject, - ImmediateObject, - TestRunner, - StatWatcherScheduler, - UpgradedDuplex, - WTFTimer, - DNSResolver, - PostgresSQLConnectionTimeout, - PostgresSQLConnectionMaxLifetime, - ValkeyConnectionTimeout, - ValkeyConnectionReconnect, - SubprocessTimeout, - DevServerSweepSourceMaps, - DevServerMemoryVisualizerTick, - - pub fn Type(comptime T: Tag) type { - return switch (T) { - .TimerCallback => TimerCallback, - .TimeoutObject => TimeoutObject, - .ImmediateObject => ImmediateObject, - .TestRunner => JSC.Jest.TestRunner, - .StatWatcherScheduler => StatWatcherScheduler, - .UpgradedDuplex => uws.UpgradedDuplex, - .WTFTimer => WTFTimer, - .DNSResolver => DNSResolver, - .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, - .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, - .ValkeyConnectionTimeout => JSC.API.Valkey, - .ValkeyConnectionReconnect => JSC.API.Valkey, - .SubprocessTimeout => JSC.Subprocess, - .DevServerSweepSourceMaps, - .DevServerMemoryVisualizerTick, - => bun.bake.DevServer, - }; - } - }; - - const TimerCallback = struct { - callback: *const fn (*TimerCallback) Arm, - ctx: *anyopaque, - event_loop_timer: EventLoopTimer, - }; - - pub const State = enum { - /// The timer is waiting to be enabled. - PENDING, - - /// The timer is active and will fire at the next time. - ACTIVE, - - /// The timer has been cancelled and will not fire. - CANCELLED, - - /// The timer has fired and the callback has been called. - FIRED, - }; - - /// If self was created by set{Immediate,Timeout,Interval}, get a pointer to the common data - /// for all those kinds of timers - fn jsTimerInternals(self: anytype) switch (@TypeOf(self)) { - *EventLoopTimer => ?*TimerObjectInternals, - *const EventLoopTimer => ?*const TimerObjectInternals, - else => |T| @compileError("wrong type " ++ @typeName(T) ++ " passed to jsTimerInternals"), - } { - switch (self.tag) { - inline .TimeoutObject, .ImmediateObject => |tag| { - const parent: switch (@TypeOf(self)) { - *EventLoopTimer => *tag.Type(), - *const EventLoopTimer => *const tag.Type(), - else => unreachable, - } = @fieldParentPtr("event_loop_timer", self); - return &parent.internals; - }, - else => return null, - } - } - - fn ns(self: *const EventLoopTimer) u64 { - return self.next.ns(); - } - - pub const Arm = union(enum) { - rearm: timespec, - disarm, - }; - - pub fn fire(this: *EventLoopTimer, now: *const timespec, vm: *VirtualMachine) Arm { - switch (this.tag) { - .PostgresSQLConnectionTimeout => return @as(*api.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(), - .PostgresSQLConnectionMaxLifetime => return @as(*api.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("max_lifetime_timer", this))).onMaxLifetimeTimeout(), - .ValkeyConnectionTimeout => return @as(*api.Valkey, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(), - .ValkeyConnectionReconnect => return @as(*api.Valkey, @alignCast(@fieldParentPtr("reconnect_timer", this))).onReconnectTimer(), - .DevServerMemoryVisualizerTick => return bun.bake.DevServer.emitMemoryVisualizerMessageTimer(this, now), - .DevServerSweepSourceMaps => return bun.bake.DevServer.SourceMapStore.sweepWeakRefs(this, now), - inline else => |t| { - if (@FieldType(t.Type(), "event_loop_timer") != EventLoopTimer) { - @compileError(@typeName(t.Type()) ++ " has wrong type for 'event_loop_timer'"); - } - var container: *t.Type() = @alignCast(@fieldParentPtr("event_loop_timer", this)); - if (comptime t.Type() == TimeoutObject or t.Type() == ImmediateObject) { - return container.internals.fire(now, vm); - } - - if (comptime t.Type() == WTFTimer) { - return container.fire(now, vm); - } - - if (comptime t.Type() == StatWatcherScheduler) { - return container.timerCallback(); - } - if (comptime t.Type() == uws.UpgradedDuplex) { - return container.onTimeout(); - } - if (Environment.isWindows) { - if (comptime t.Type() == uws.WindowsNamedPipe) { - return container.onTimeout(); - } - } - - if (comptime t.Type() == JSC.Jest.TestRunner) { - container.onTestTimeout(now, vm); - return .disarm; - } - - if (comptime t.Type() == DNSResolver) { - return container.checkTimeouts(now, vm); - } - - if (comptime t.Type() == JSC.Subprocess) { - return container.timeoutCallback(); - } - - return container.callback(container); - }, - } - } - - pub fn deinit(_: *EventLoopTimer) void {} -}; - const timespec = bun.timespec; /// A timer created by WTF code and invoked by Bun's event loop diff --git a/src/bun.js/api/Timer/EventLoopTimer.zig b/src/bun.js/api/Timer/EventLoopTimer.zig new file mode 100644 index 0000000000..232949cb4a --- /dev/null +++ b/src/bun.js/api/Timer/EventLoopTimer.zig @@ -0,0 +1,247 @@ +const EventLoopTimer = @This(); + +/// The absolute time to fire this timer next. +next: timespec, +state: State = .PENDING, +tag: Tag, +/// Internal heap fields. +heap: bun.io.heap.IntrusiveField(EventLoopTimer) = .{}, + +pub fn initPaused(tag: Tag) EventLoopTimer { + return .{ + .next = .{}, + .tag = tag, + }; +} + +pub fn less(_: void, a: *const EventLoopTimer, b: *const EventLoopTimer) bool { + const sec_order = std.math.order(a.next.sec, b.next.sec); + if (sec_order != .eq) return sec_order == .lt; + + // collapse sub-millisecond precision for JavaScript timers + const maybe_a_internals = a.jsTimerInternals(); + const maybe_b_internals = b.jsTimerInternals(); + var a_ns = a.next.nsec; + var b_ns = b.next.nsec; + if (maybe_a_internals != null) a_ns = std.time.ns_per_ms * @divTrunc(a_ns, std.time.ns_per_ms); + if (maybe_b_internals != null) b_ns = std.time.ns_per_ms * @divTrunc(b_ns, std.time.ns_per_ms); + + const order = std.math.order(a_ns, b_ns); + if (order == .eq) { + if (maybe_a_internals) |a_internals| { + if (maybe_b_internals) |b_internals| { + // We expect that the epoch will overflow sometimes. + // If it does, we would ideally like timers with an epoch from before the + // overflow to be sorted *before* timers with an epoch from after the overflow + // (even though their epoch will be numerically *larger*). + // + // Wrapping subtraction gives us a distance that is consistent even if one + // epoch has overflowed and the other hasn't. If the distance from a to b is + // small, it's likely that b is really newer than a, so we consider a less than + // b. If the distance from a to b is large (greater than half the u25 range), + // it's more likely that b is older than a so the true distance is from b to a. + return b_internals.flags.epoch -% a_internals.flags.epoch < std.math.maxInt(u25) / 2; + } + } + } + return order == .lt; +} + +pub const Tag = if (Environment.isWindows) enum { + TimerCallback, + TimeoutObject, + ImmediateObject, + TestRunner, + StatWatcherScheduler, + UpgradedDuplex, + DNSResolver, + WindowsNamedPipe, + WTFTimer, + PostgresSQLConnectionTimeout, + PostgresSQLConnectionMaxLifetime, + ValkeyConnectionTimeout, + ValkeyConnectionReconnect, + SubprocessTimeout, + DevServerSweepSourceMaps, + DevServerMemoryVisualizerTick, + + pub fn Type(comptime T: Tag) type { + return switch (T) { + .TimerCallback => TimerCallback, + .TimeoutObject => TimeoutObject, + .ImmediateObject => ImmediateObject, + .TestRunner => JSC.Jest.TestRunner, + .StatWatcherScheduler => StatWatcherScheduler, + .UpgradedDuplex => uws.UpgradedDuplex, + .DNSResolver => DNSResolver, + .WindowsNamedPipe => uws.WindowsNamedPipe, + .WTFTimer => WTFTimer, + .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, + .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, + .SubprocessTimeout => JSC.Subprocess, + .ValkeyConnectionReconnect => JSC.API.Valkey, + .ValkeyConnectionTimeout => JSC.API.Valkey, + .DevServerSweepSourceMaps, + .DevServerMemoryVisualizerTick, + => bun.bake.DevServer, + }; + } +} else enum { + TimerCallback, + TimeoutObject, + ImmediateObject, + TestRunner, + StatWatcherScheduler, + UpgradedDuplex, + WTFTimer, + DNSResolver, + PostgresSQLConnectionTimeout, + PostgresSQLConnectionMaxLifetime, + ValkeyConnectionTimeout, + ValkeyConnectionReconnect, + SubprocessTimeout, + DevServerSweepSourceMaps, + DevServerMemoryVisualizerTick, + + pub fn Type(comptime T: Tag) type { + return switch (T) { + .TimerCallback => TimerCallback, + .TimeoutObject => TimeoutObject, + .ImmediateObject => ImmediateObject, + .TestRunner => JSC.Jest.TestRunner, + .StatWatcherScheduler => StatWatcherScheduler, + .UpgradedDuplex => uws.UpgradedDuplex, + .WTFTimer => WTFTimer, + .DNSResolver => DNSResolver, + .PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection, + .PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection, + .ValkeyConnectionTimeout => JSC.API.Valkey, + .ValkeyConnectionReconnect => JSC.API.Valkey, + .SubprocessTimeout => JSC.Subprocess, + .DevServerSweepSourceMaps, + .DevServerMemoryVisualizerTick, + => bun.bake.DevServer, + }; + } +}; + +const TimerCallback = struct { + callback: *const fn (*TimerCallback) Arm, + ctx: *anyopaque, + event_loop_timer: EventLoopTimer, +}; + +pub const State = enum { + /// The timer is waiting to be enabled. + PENDING, + + /// The timer is active and will fire at the next time. + ACTIVE, + + /// The timer has been cancelled and will not fire. + CANCELLED, + + /// The timer has fired and the callback has been called. + FIRED, +}; + +/// If self was created by set{Immediate,Timeout,Interval}, get a pointer to the common data +/// for all those kinds of timers +pub fn jsTimerInternals(self: anytype) switch (@TypeOf(self)) { + *EventLoopTimer => ?*TimerObjectInternals, + *const EventLoopTimer => ?*const TimerObjectInternals, + else => |T| @compileError("wrong type " ++ @typeName(T) ++ " passed to jsTimerInternals"), +} { + switch (self.tag) { + inline .TimeoutObject, .ImmediateObject => |tag| { + const parent: switch (@TypeOf(self)) { + *EventLoopTimer => *tag.Type(), + *const EventLoopTimer => *const tag.Type(), + else => unreachable, + } = @fieldParentPtr("event_loop_timer", self); + return &parent.internals; + }, + else => return null, + } +} + +fn ns(self: *const EventLoopTimer) u64 { + return self.next.ns(); +} + +pub const Arm = union(enum) { + rearm: timespec, + disarm, +}; + +pub fn fire(this: *EventLoopTimer, now: *const timespec, vm: *VirtualMachine) Arm { + switch (this.tag) { + .PostgresSQLConnectionTimeout => return @as(*api.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(), + .PostgresSQLConnectionMaxLifetime => return @as(*api.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("max_lifetime_timer", this))).onMaxLifetimeTimeout(), + .ValkeyConnectionTimeout => return @as(*api.Valkey, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(), + .ValkeyConnectionReconnect => return @as(*api.Valkey, @alignCast(@fieldParentPtr("reconnect_timer", this))).onReconnectTimer(), + .DevServerMemoryVisualizerTick => return bun.bake.DevServer.emitMemoryVisualizerMessageTimer(this, now), + .DevServerSweepSourceMaps => return bun.bake.DevServer.SourceMapStore.sweepWeakRefs(this, now), + inline else => |t| { + if (@FieldType(t.Type(), "event_loop_timer") != EventLoopTimer) { + @compileError(@typeName(t.Type()) ++ " has wrong type for 'event_loop_timer'"); + } + var container: *t.Type() = @alignCast(@fieldParentPtr("event_loop_timer", this)); + if (comptime t.Type() == TimeoutObject or t.Type() == ImmediateObject) { + return container.internals.fire(now, vm); + } + + if (comptime t.Type() == WTFTimer) { + return container.fire(now, vm); + } + + if (comptime t.Type() == StatWatcherScheduler) { + return container.timerCallback(); + } + if (comptime t.Type() == uws.UpgradedDuplex) { + return container.onTimeout(); + } + if (Environment.isWindows) { + if (comptime t.Type() == uws.WindowsNamedPipe) { + return container.onTimeout(); + } + } + + if (comptime t.Type() == JSC.Jest.TestRunner) { + container.onTestTimeout(now, vm); + return .disarm; + } + + if (comptime t.Type() == DNSResolver) { + return container.checkTimeouts(now, vm); + } + + if (comptime t.Type() == JSC.Subprocess) { + return container.timeoutCallback(); + } + + return container.callback(container); + }, + } +} + +pub fn deinit(_: *EventLoopTimer) void {} + +const timespec = bun.timespec; + +/// A timer created by WTF code and invoked by Bun's event loop +const WTFTimer = @import("../../WTFTimer.zig"); +const VirtualMachine = JSC.VirtualMachine; +const TimerObjectInternals = @import("../Timer.zig").TimerObjectInternals; +const TimeoutObject = @import("../Timer.zig").TimeoutObject; +const ImmediateObject = @import("../Timer.zig").ImmediateObject; +const StatWatcherScheduler = @import("../../node/node_fs_stat_watcher.zig").StatWatcherScheduler; +const DNSResolver = @import("../bun/dns_resolver.zig").DNSResolver; + +const bun = @import("bun"); +const std = @import("std"); +const Environment = bun.Environment; +const JSC = bun.JSC; + +const uws = bun.uws; +const api = JSC.API; diff --git a/src/bun.js/api/Timer/ImmediateObject.zig b/src/bun.js/api/Timer/ImmediateObject.zig new file mode 100644 index 0000000000..d695be5bc2 --- /dev/null +++ b/src/bun.js/api/Timer/ImmediateObject.zig @@ -0,0 +1,104 @@ +const ImmediateObject = @This(); + +const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); +pub const ref = RefCount.ref; +pub const deref = RefCount.deref; + +pub const js = JSC.Codegen.JSImmediate; +pub const toJS = js.toJS; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; + +ref_count: RefCount, +event_loop_timer: EventLoopTimer = .{ + .next = .{}, + .tag = .ImmediateObject, +}, +internals: TimerObjectInternals, + +pub fn init( + globalThis: *JSGlobalObject, + id: i32, + callback: JSValue, + arguments: JSValue, +) JSValue { + // internals are initialized by init() + const immediate = bun.new(ImmediateObject, .{ .ref_count = .init(), .internals = undefined }); + const js_value = immediate.toJS(globalThis); + defer js_value.ensureStillAlive(); + immediate.internals.init( + js_value, + globalThis, + id, + .setImmediate, + 0, + callback, + arguments, + ); + + if (globalThis.bunVM().isInspectorEnabled()) { + Debugger.didScheduleAsyncCall( + globalThis, + .DOMTimer, + ID.asyncID(.{ .id = id, .kind = .setImmediate }), + true, + ); + } + + return js_value; +} + +fn deinit(this: *ImmediateObject) void { + this.internals.deinit(); + bun.destroy(this); +} + +pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) !*ImmediateObject { + _ = callFrame; + return globalObject.throw("Immediate is not constructible", .{}); +} + +/// returns true if an exception was thrown +pub fn runImmediateTask(this: *ImmediateObject, vm: *VirtualMachine) bool { + return this.internals.runImmediateTask(vm); +} + +pub fn toPrimitive(this: *ImmediateObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.toPrimitive(); +} + +pub fn doRef(this: *ImmediateObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doRef(globalThis, callFrame.this()); +} + +pub fn doUnref(this: *ImmediateObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doUnref(globalThis, callFrame.this()); +} + +pub fn hasRef(this: *ImmediateObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.hasRef(); +} + +pub fn finalize(this: *ImmediateObject) void { + this.internals.finalize(); +} + +pub fn getDestroyed(this: *ImmediateObject, globalThis: *JSGlobalObject) JSValue { + _ = globalThis; + return .jsBoolean(this.internals.getDestroyed()); +} + +pub fn dispose(this: *ImmediateObject, globalThis: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + this.internals.cancel(globalThis.bunVM()); + return .js_undefined; +} + +const bun = @import("bun"); +const JSC = bun.JSC; +const VirtualMachine = JSC.VirtualMachine; +const TimerObjectInternals = @import("../Timer.zig").TimerObjectInternals; +const Debugger = @import("../../Debugger.zig"); +const ID = @import("../Timer.zig").ID; +const EventLoopTimer = @import("../Timer.zig").EventLoopTimer; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; diff --git a/src/bun.js/api/Timer/TimeoutObject.zig b/src/bun.js/api/Timer/TimeoutObject.zig new file mode 100644 index 0000000000..7e69cea0c6 --- /dev/null +++ b/src/bun.js/api/Timer/TimeoutObject.zig @@ -0,0 +1,134 @@ +const TimeoutObject = @This(); + +const RefCount = bun.ptr.RefCount(@This(), "ref_count", deinit, .{}); +pub const ref = RefCount.ref; +pub const deref = RefCount.deref; + +pub const js = JSC.Codegen.JSTimeout; +pub const toJS = js.toJS; +pub const fromJS = js.fromJS; +pub const fromJSDirect = js.fromJSDirect; + +ref_count: RefCount, +event_loop_timer: EventLoopTimer = .{ + .next = .{}, + .tag = .TimeoutObject, +}, +internals: TimerObjectInternals, + +pub fn init( + globalThis: *JSGlobalObject, + id: i32, + kind: Kind, + interval: u31, + callback: JSValue, + arguments: JSValue, +) JSValue { + // internals are initialized by init() + const timeout = bun.new(TimeoutObject, .{ .ref_count = .init(), .internals = undefined }); + const js_value = timeout.toJS(globalThis); + defer js_value.ensureStillAlive(); + timeout.internals.init( + js_value, + globalThis, + id, + kind, + interval, + callback, + arguments, + ); + + if (globalThis.bunVM().isInspectorEnabled()) { + Debugger.didScheduleAsyncCall( + globalThis, + .DOMTimer, + ID.asyncID(.{ .id = id, .kind = kind.big() }), + kind != .setInterval, + ); + } + + return js_value; +} + +fn deinit(this: *TimeoutObject) void { + this.internals.deinit(); + bun.destroy(this); +} + +pub fn constructor(globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) !*TimeoutObject { + _ = callFrame; + return globalObject.throw("Timeout is not constructible", .{}); +} + +pub fn toPrimitive(this: *TimeoutObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.toPrimitive(); +} + +pub fn doRef(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doRef(globalThis, callFrame.this()); +} + +pub fn doUnref(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doUnref(globalThis, callFrame.this()); +} + +pub fn doRefresh(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.doRefresh(globalThis, callFrame.this()); +} + +pub fn hasRef(this: *TimeoutObject, _: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + return this.internals.hasRef(); +} + +pub fn finalize(this: *TimeoutObject) void { + this.internals.finalize(); +} + +pub fn getDestroyed(this: *TimeoutObject, globalThis: *JSGlobalObject) JSValue { + _ = globalThis; + return .jsBoolean(this.internals.getDestroyed()); +} + +pub fn close(this: *TimeoutObject, globalThis: *JSGlobalObject, callFrame: *JSC.CallFrame) JSValue { + this.internals.cancel(globalThis.bunVM()); + return callFrame.this(); +} + +pub fn get_onTimeout(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { + return TimeoutObject.js.callbackGetCached(thisValue).?; +} + +pub fn set_onTimeout(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { + TimeoutObject.js.callbackSetCached(thisValue, globalThis, value); +} + +pub fn get_idleTimeout(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { + return TimeoutObject.js.idleTimeoutGetCached(thisValue).?; +} + +pub fn set_idleTimeout(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { + TimeoutObject.js.idleTimeoutSetCached(thisValue, globalThis, value); +} + +pub fn get_repeat(_: *TimeoutObject, thisValue: JSValue, _: *JSGlobalObject) JSValue { + return TimeoutObject.js.repeatGetCached(thisValue).?; +} + +pub fn set_repeat(_: *TimeoutObject, thisValue: JSValue, globalThis: *JSGlobalObject, value: JSValue) void { + TimeoutObject.js.repeatSetCached(thisValue, globalThis, value); +} + +pub fn dispose(this: *TimeoutObject, globalThis: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue { + this.internals.cancel(globalThis.bunVM()); + return .js_undefined; +} + +const bun = @import("bun"); +const JSC = bun.JSC; +const TimerObjectInternals = @import("../Timer.zig").TimerObjectInternals; +const Debugger = @import("../../Debugger.zig"); +const ID = @import("../Timer.zig").ID; +const Kind = @import("../Timer.zig").Kind; +const EventLoopTimer = @import("../Timer.zig").EventLoopTimer; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; diff --git a/src/bun.js/api/Timer/TimerObjectInternals.zig b/src/bun.js/api/Timer/TimerObjectInternals.zig new file mode 100644 index 0000000000..cc3db48996 --- /dev/null +++ b/src/bun.js/api/Timer/TimerObjectInternals.zig @@ -0,0 +1,487 @@ +/// Data that TimerObject and ImmediateObject have in common +const TimerObjectInternals = @This(); + +/// Identifier for this timer that is exposed to JavaScript (by `+timer`) +id: i32 = -1, +interval: u31 = 0, +strong_this: JSC.Strong.Optional = .empty, +flags: Flags = .{}, + +const Flags = packed struct(u32) { + /// Whenever a timer is inserted into the heap (which happen on creation or refresh), the global + /// epoch is incremented and the new epoch is set on the timer. For timers created by + /// JavaScript, the epoch is used to break ties between timers scheduled for the same + /// millisecond. This ensures that if you set two timers for the same amount of time, and + /// refresh the first one, the first one will fire last. This mimics Node.js's behavior where + /// the refreshed timer will be inserted at the end of a list, which makes it fire later. + epoch: u25 = 0, + + kind: Kind = .setTimeout, + + // we do not allow the timer to be refreshed after we call clearInterval/clearTimeout + has_cleared_timer: bool = false, + is_keeping_event_loop_alive: bool = false, + + // if they never access the timer by integer, don't create a hashmap entry. + has_accessed_primitive: bool = false, + + has_js_ref: bool = true, + + /// Set to `true` only during execution of the JavaScript function so that `_destroyed` can be + /// false during the callback, even though the `state` will be `FIRED`. + in_callback: bool = false, +}; + +fn eventLoopTimer(this: *TimerObjectInternals) *EventLoopTimer { + switch (this.flags.kind) { + .setImmediate => { + const parent: *ImmediateObject = @fieldParentPtr("internals", this); + assert(parent.event_loop_timer.tag == .ImmediateObject); + return &parent.event_loop_timer; + }, + .setTimeout, .setInterval => { + const parent: *TimeoutObject = @fieldParentPtr("internals", this); + assert(parent.event_loop_timer.tag == .TimeoutObject); + return &parent.event_loop_timer; + }, + } +} + +fn ref(this: *TimerObjectInternals) void { + switch (this.flags.kind) { + .setImmediate => @as(*ImmediateObject, @fieldParentPtr("internals", this)).ref(), + .setTimeout, .setInterval => @as(*TimeoutObject, @fieldParentPtr("internals", this)).ref(), + } +} + +fn deref(this: *TimerObjectInternals) void { + switch (this.flags.kind) { + .setImmediate => @as(*ImmediateObject, @fieldParentPtr("internals", this)).deref(), + .setTimeout, .setInterval => @as(*TimeoutObject, @fieldParentPtr("internals", this)).deref(), + } +} + +extern "c" fn Bun__JSTimeout__call(globalObject: *JSC.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue) bool; + +/// returns true if an exception was thrown +pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool { + if (this.flags.has_cleared_timer or + // unref'd setImmediate callbacks should only run if there are things keeping the event + // loop alive other than setImmediates + (!this.flags.is_keeping_event_loop_alive and !vm.isEventLoopAliveExcludingImmediates())) + { + this.deref(); + return false; + } + + const timer = this.strong_this.get() orelse { + if (Environment.isDebug) { + @panic("TimerObjectInternals.runImmediateTask: this_object is null"); + } + return false; + }; + const globalThis = vm.global; + this.strong_this.deinit(); + this.eventLoopTimer().state = .FIRED; + this.setEnableKeepingEventLoopAlive(vm, false); + + vm.eventLoop().enter(); + const callback = ImmediateObject.js.callbackGetCached(timer).?; + const arguments = ImmediateObject.js.argumentsGetCached(timer).?; + this.ref(); + const exception_thrown = this.run(globalThis, timer, callback, arguments, this.asyncID(), vm); + this.deref(); + + if (this.eventLoopTimer().state == .FIRED) { + this.deref(); + } + + vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown); + + return exception_thrown; +} + +pub fn asyncID(this: *const TimerObjectInternals) u64 { + return ID.asyncID(.{ .id = this.id, .kind = this.flags.kind.big() }); +} + +pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *JSC.VirtualMachine) EventLoopTimer.Arm { + const id = this.id; + const kind = this.flags.kind.big(); + const async_id: ID = .{ .id = id, .kind = kind }; + const has_been_cleared = this.eventLoopTimer().state == .CANCELLED or this.flags.has_cleared_timer or vm.scriptExecutionStatus() != .running; + + this.eventLoopTimer().state = .FIRED; + + const globalThis = vm.global; + const this_object = this.strong_this.get().?; + + const callback: JSValue, const arguments: JSValue, var idle_timeout: JSValue, var repeat: JSValue = switch (kind) { + .setImmediate => .{ + ImmediateObject.js.callbackGetCached(this_object).?, + ImmediateObject.js.argumentsGetCached(this_object).?, + .js_undefined, + .js_undefined, + }, + .setTimeout, .setInterval => .{ + TimeoutObject.js.callbackGetCached(this_object).?, + TimeoutObject.js.argumentsGetCached(this_object).?, + TimeoutObject.js.idleTimeoutGetCached(this_object).?, + TimeoutObject.js.repeatGetCached(this_object).?, + }, + }; + + if (has_been_cleared or !callback.toBoolean()) { + if (vm.isInspectorEnabled()) { + Debugger.didCancelAsyncCall(globalThis, .DOMTimer, ID.asyncID(async_id)); + } + this.setEnableKeepingEventLoopAlive(vm, false); + this.flags.has_cleared_timer = true; + this.strong_this.deinit(); + this.deref(); + + return .disarm; + } + + var time_before_call: timespec = undefined; + + if (kind != .setInterval) { + this.strong_this.clearWithoutDeallocation(); + } else { + time_before_call = timespec.msFromNow(this.interval); + } + this_object.ensureStillAlive(); + + vm.eventLoop().enter(); + { + // Ensure it stays alive for this scope. + this.ref(); + defer this.deref(); + + _ = this.run(globalThis, this_object, callback, arguments, ID.asyncID(async_id), vm); + + switch (kind) { + .setTimeout, .setInterval => { + idle_timeout = TimeoutObject.js.idleTimeoutGetCached(this_object).?; + repeat = TimeoutObject.js.repeatGetCached(this_object).?; + }, + else => {}, + } + + const is_timer_done = is_timer_done: { + // Node doesn't drain microtasks after each timer callback. + if (kind == .setInterval) { + if (!this.shouldRescheduleTimer(repeat, idle_timeout)) { + break :is_timer_done true; + } + switch (this.eventLoopTimer().state) { + .FIRED => { + // If we didn't clear the setInterval, reschedule it starting from + vm.timer.update(this.eventLoopTimer(), &time_before_call); + + if (this.flags.has_js_ref) { + this.setEnableKeepingEventLoopAlive(vm, true); + } + + // The ref count doesn't change. It wasn't decremented. + }, + .ACTIVE => { + // The developer called timer.refresh() synchronously in the callback. + vm.timer.update(this.eventLoopTimer(), &time_before_call); + + // Balance out the ref count. + // the transition from "FIRED" -> "ACTIVE" caused it to increment. + this.deref(); + }, + else => { + break :is_timer_done true; + }, + } + } else { + if (kind == .setTimeout and !repeat.isNull()) { + if (idle_timeout.getNumber()) |num| { + if (num != -1) { + this.convertToInterval(globalThis, this_object, repeat); + break :is_timer_done false; + } + } + } + + if (this.eventLoopTimer().state == .FIRED) { + break :is_timer_done true; + } + } + + break :is_timer_done false; + }; + + if (is_timer_done) { + this.setEnableKeepingEventLoopAlive(vm, false); + // The timer will not be re-entered into the event loop at this point. + this.deref(); + } + } + vm.eventLoop().exit(); + + return .disarm; +} + +fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer: JSValue, repeat: JSValue) void { + bun.debugAssert(this.flags.kind == .setTimeout); + + const vm = global.bunVM(); + + const new_interval: u31 = if (repeat.getNumber()) |num| if (num < 1 or num > std.math.maxInt(u31)) 1 else @intFromFloat(num) else 1; + + // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613 + TimeoutObject.js.idleTimeoutSetCached(timer, global, repeat); + this.strong_this.set(global, timer); + this.flags.kind = .setInterval; + this.interval = new_interval; + this.reschedule(timer, vm); +} + +pub fn run(this: *TimerObjectInternals, globalThis: *JSC.JSGlobalObject, timer: JSValue, callback: JSValue, arguments: JSValue, async_id: u64, vm: *JSC.VirtualMachine) bool { + if (vm.isInspectorEnabled()) { + Debugger.willDispatchAsyncCall(globalThis, .DOMTimer, async_id); + } + + defer { + if (vm.isInspectorEnabled()) { + Debugger.didDispatchAsyncCall(globalThis, .DOMTimer, async_id); + } + } + + // Bun__JSTimeout__call handles exceptions. + this.flags.in_callback = true; + defer this.flags.in_callback = false; + return Bun__JSTimeout__call(globalThis, timer, callback, arguments); +} + +pub fn init( + this: *TimerObjectInternals, + timer: JSValue, + global: *JSGlobalObject, + id: i32, + kind: Kind, + interval: u31, + callback: JSValue, + arguments: JSValue, +) void { + const vm = global.bunVM(); + this.* = .{ + .id = id, + .flags = .{ .kind = kind, .epoch = vm.timer.epoch }, + .interval = interval, + }; + + if (kind == .setImmediate) { + ImmediateObject.js.argumentsSetCached(timer, global, arguments); + ImmediateObject.js.callbackSetCached(timer, global, callback); + const parent: *ImmediateObject = @fieldParentPtr("internals", this); + vm.enqueueImmediateTask(parent); + this.setEnableKeepingEventLoopAlive(vm, true); + // ref'd by event loop + parent.ref(); + } else { + TimeoutObject.js.argumentsSetCached(timer, global, arguments); + TimeoutObject.js.callbackSetCached(timer, global, callback); + TimeoutObject.js.idleTimeoutSetCached(timer, global, JSC.jsNumber(interval)); + TimeoutObject.js.repeatSetCached(timer, global, if (kind == .setInterval) JSC.jsNumber(interval) else .null); + + // this increments the refcount + this.reschedule(timer, vm); + } + + this.strong_this.set(global, timer); +} + +pub fn doRef(this: *TimerObjectInternals, _: *JSC.JSGlobalObject, this_value: JSValue) JSValue { + this_value.ensureStillAlive(); + + const did_have_js_ref = this.flags.has_js_ref; + this.flags.has_js_ref = true; + + // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L256 + // and + // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L685-L687 + if (!did_have_js_ref and !this.flags.has_cleared_timer) { + this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), true); + } + + return this_value; +} + +pub fn doRefresh(this: *TimerObjectInternals, globalObject: *JSC.JSGlobalObject, this_value: JSValue) JSValue { + // Immediates do not have a refresh function, and our binding generator should not let this + // function be reached even if you override the `this` value calling a Timeout object's + // `refresh` method + assert(this.flags.kind != .setImmediate); + + // setImmediate does not support refreshing and we do not support refreshing after cleanup + if (this.id == -1 or this.flags.kind == .setImmediate or this.flags.has_cleared_timer) { + return this_value; + } + + this.strong_this.set(globalObject, this_value); + this.reschedule(this_value, VirtualMachine.get()); + + return this_value; +} + +pub fn doUnref(this: *TimerObjectInternals, _: *JSC.JSGlobalObject, this_value: JSValue) JSValue { + this_value.ensureStillAlive(); + + const did_have_js_ref = this.flags.has_js_ref; + this.flags.has_js_ref = false; + + if (did_have_js_ref) { + this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), false); + } + + return this_value; +} + +pub fn cancel(this: *TimerObjectInternals, vm: *VirtualMachine) void { + this.setEnableKeepingEventLoopAlive(vm, false); + this.flags.has_cleared_timer = true; + + if (this.flags.kind == .setImmediate) return; + + const was_active = this.eventLoopTimer().state == .ACTIVE; + + this.eventLoopTimer().state = .CANCELLED; + this.strong_this.deinit(); + + if (was_active) { + vm.timer.remove(this.eventLoopTimer()); + this.deref(); + } +} + +fn shouldRescheduleTimer(this: *TimerObjectInternals, repeat: JSValue, idle_timeout: JSValue) bool { + if (this.flags.kind == .setInterval and repeat.isNull()) return false; + if (idle_timeout.getNumber()) |num| { + if (num == -1) return false; + } + return true; +} + +pub fn reschedule(this: *TimerObjectInternals, timer: JSValue, vm: *VirtualMachine) void { + if (this.flags.kind == .setImmediate) return; + + const idle_timeout = TimeoutObject.js.idleTimeoutGetCached(timer).?; + const repeat = TimeoutObject.js.repeatGetCached(timer).?; + + // https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L612 + if (!this.shouldRescheduleTimer(repeat, idle_timeout)) return; + + const now = timespec.msFromNow(this.interval); + const was_active = this.eventLoopTimer().state == .ACTIVE; + if (was_active) { + vm.timer.remove(this.eventLoopTimer()); + } else { + this.ref(); + } + + vm.timer.update(this.eventLoopTimer(), &now); + this.flags.has_cleared_timer = false; + + if (this.flags.has_js_ref) { + this.setEnableKeepingEventLoopAlive(vm, true); + } +} + +fn setEnableKeepingEventLoopAlive(this: *TimerObjectInternals, vm: *VirtualMachine, enable: bool) void { + if (this.flags.is_keeping_event_loop_alive == enable) { + return; + } + this.flags.is_keeping_event_loop_alive = enable; + switch (this.flags.kind) { + .setTimeout, .setInterval => vm.timer.incrementTimerRef(if (enable) 1 else -1), + + // setImmediate has slightly different event loop logic + .setImmediate => vm.timer.incrementImmediateRef(if (enable) 1 else -1), + } +} + +pub fn hasRef(this: *TimerObjectInternals) JSValue { + return JSValue.jsBoolean(this.flags.is_keeping_event_loop_alive); +} + +pub fn toPrimitive(this: *TimerObjectInternals) bun.JSError!JSValue { + if (!this.flags.has_accessed_primitive) { + this.flags.has_accessed_primitive = true; + const vm = VirtualMachine.get(); + try vm.timer.maps.get(this.flags.kind).put(bun.default_allocator, this.id, this.eventLoopTimer()); + } + return JSValue.jsNumber(this.id); +} + +/// This is the getter for `_destroyed` on JS Timeout and Immediate objects +pub fn getDestroyed(this: *TimerObjectInternals) bool { + if (this.flags.has_cleared_timer) { + return true; + } + if (this.flags.in_callback) { + return false; + } + return switch (this.eventLoopTimer().state) { + .ACTIVE, .PENDING => false, + .FIRED, .CANCELLED => true, + }; +} + +pub fn finalize(this: *TimerObjectInternals) void { + this.strong_this.deinit(); + this.deref(); +} + +pub fn deinit(this: *TimerObjectInternals) void { + this.strong_this.deinit(); + const vm = VirtualMachine.get(); + const kind = this.flags.kind; + + if (this.eventLoopTimer().state == .ACTIVE) { + vm.timer.remove(this.eventLoopTimer()); + } + + if (this.flags.has_accessed_primitive) { + const map = vm.timer.maps.get(kind); + if (map.orderedRemove(this.id)) { + // If this array gets large, let's shrink it down + // Array keys are i32 + // Values are 1 ptr + // Therefore, 12 bytes per entry + // So if you created 21,000 timers and accessed them by ID, you'd be using 252KB + const allocated_bytes = map.capacity() * @sizeOf(TimeoutMap.Data); + const used_bytes = map.count() * @sizeOf(TimeoutMap.Data); + if (allocated_bytes - used_bytes > 256 * 1024) { + map.shrinkAndFree(bun.default_allocator, map.count() + 8); + } + } + } + + this.setEnableKeepingEventLoopAlive(vm, false); + switch (kind) { + .setImmediate => (@as(*ImmediateObject, @fieldParentPtr("internals", this))).ref_count.assertNoRefs(), + .setTimeout, .setInterval => (@as(*TimeoutObject, @fieldParentPtr("internals", this))).ref_count.assertNoRefs(), + } +} + +const bun = @import("bun"); +const std = @import("std"); +const JSC = bun.JSC; +const VirtualMachine = JSC.VirtualMachine; +const TimeoutObject = @import("../Timer.zig").TimeoutObject; +const ImmediateObject = @import("../Timer.zig").ImmediateObject; +const Debugger = @import("../../Debugger.zig"); +const timespec = bun.timespec; +const Environment = bun.Environment; +const ID = @import("../Timer.zig").ID; +const TimeoutMap = @import("../Timer.zig").TimeoutMap; +const Kind = @import("../Timer.zig").Kind; +const EventLoopTimer = @import("../Timer.zig").EventLoopTimer; +const JSValue = JSC.JSValue; +const JSGlobalObject = JSC.JSGlobalObject; +const assert = bun.assert;