Files
bun.sh/src/bun.js/api/Timer/TimerObjectInternals.zig

488 lines
18 KiB
Zig

/// 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) catch return true;
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;