mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Split up Timer.zig into more files (#20465)
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
247
src/bun.js/api/Timer/EventLoopTimer.zig
Normal file
247
src/bun.js/api/Timer/EventLoopTimer.zig
Normal file
@@ -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;
|
||||
104
src/bun.js/api/Timer/ImmediateObject.zig
Normal file
104
src/bun.js/api/Timer/ImmediateObject.zig
Normal file
@@ -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;
|
||||
134
src/bun.js/api/Timer/TimeoutObject.zig
Normal file
134
src/bun.js/api/Timer/TimeoutObject.zig
Normal file
@@ -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;
|
||||
487
src/bun.js/api/Timer/TimerObjectInternals.zig
Normal file
487
src/bun.js/api/Timer/TimerObjectInternals.zig
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user