mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
843 lines
27 KiB
Zig
843 lines
27 KiB
Zig
const std = @import("std");
|
|
const bun = @import("root").bun;
|
|
const JSC = bun.JSC;
|
|
const VirtualMachine = JSC.VirtualMachine;
|
|
const JSValue = JSC.JSValue;
|
|
const JSGlobalObject = JSC.JSGlobalObject;
|
|
const Debugger = JSC.Debugger;
|
|
const Environment = bun.Environment;
|
|
const Async = @import("async");
|
|
const uv = bun.windows.libuv;
|
|
const StatWatcherScheduler = @import("../node/node_fs_stat_watcher.zig").StatWatcherScheduler;
|
|
const Timer = @This();
|
|
|
|
/// TimeoutMap is map of i32 to nullable Timeout structs
|
|
/// i32 is exposed to JavaScript and can be used with clearTimeout, clearInterval, etc.
|
|
/// When Timeout is null, it means the tasks have been scheduled but not yet executed.
|
|
/// Timeouts are enqueued as a task to be run on the next tick of the task queue
|
|
/// The task queue runs after the event loop tasks have been run
|
|
/// Therefore, there is a race condition where you cancel the task after it has already been enqueued
|
|
/// In that case, it shouldn't run. It should be skipped.
|
|
pub const TimeoutMap = std.AutoArrayHashMapUnmanaged(
|
|
i32,
|
|
*EventLoopTimer,
|
|
);
|
|
|
|
const TimerHeap = heap.Intrusive(EventLoopTimer, void, EventLoopTimer.less);
|
|
|
|
pub const All = struct {
|
|
last_id: i32 = 1,
|
|
timers: TimerHeap = .{
|
|
.context = {},
|
|
},
|
|
timer_ref: bun.Async.KeepAlive = .{},
|
|
active_timer_count: i32 = 0,
|
|
uv_timer: if (Environment.isWindows) uv.Timer else void =
|
|
if (Environment.isWindows) std.mem.zeroes(uv.Timer) else {},
|
|
|
|
// We split up the map here to avoid storing an extra "repeat" boolean
|
|
maps: struct {
|
|
setTimeout: TimeoutMap = .{},
|
|
setInterval: TimeoutMap = .{},
|
|
setImmediate: TimeoutMap = .{},
|
|
|
|
pub inline fn get(this: *@This(), kind: Kind) *TimeoutMap {
|
|
return switch (kind) {
|
|
.setTimeout => &this.setTimeout,
|
|
.setInterval => &this.setInterval,
|
|
.setImmediate => &this.setImmediate,
|
|
};
|
|
}
|
|
} = .{},
|
|
|
|
pub fn insert(this: *All, timer: *EventLoopTimer) void {
|
|
this.timers.insert(timer);
|
|
timer.state = .ACTIVE;
|
|
|
|
if (Environment.isWindows) {
|
|
this.ensureUVTimer(@alignCast(@fieldParentPtr("timer", this)));
|
|
}
|
|
}
|
|
|
|
pub fn remove(this: *All, timer: *EventLoopTimer) void {
|
|
this.timers.remove(timer);
|
|
|
|
timer.state = .CANCELLED;
|
|
timer.heap = .{};
|
|
}
|
|
|
|
fn ensureUVTimer(this: *All, vm: *VirtualMachine) void {
|
|
if (this.uv_timer.data == null) {
|
|
this.uv_timer.init(vm.uvLoop());
|
|
this.uv_timer.data = vm;
|
|
this.uv_timer.unref();
|
|
}
|
|
|
|
if (this.timers.peek()) |timer| {
|
|
uv.uv_update_time(vm.uvLoop());
|
|
const now = timespec.now();
|
|
const wait = if (timer.next.greater(&now))
|
|
timer.next.duration(&now)
|
|
else
|
|
timespec{ .nsec = 0, .sec = 0 };
|
|
|
|
this.uv_timer.start(wait.ms(), 0, &onUVTimer);
|
|
|
|
if (this.active_timer_count > 0) {
|
|
this.uv_timer.ref();
|
|
} else {
|
|
this.uv_timer.unref();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn onUVTimer(uv_timer_t: *uv.Timer) callconv(.C) void {
|
|
const all: *All = @fieldParentPtr("uv_timer", uv_timer_t);
|
|
const vm: *VirtualMachine = @alignCast(@fieldParentPtr("timer", all));
|
|
all.drainTimers(vm);
|
|
all.ensureUVTimer(vm);
|
|
}
|
|
|
|
pub fn incrementTimerRef(this: *All, delta: i32) void {
|
|
const vm: *JSC.VirtualMachine = @alignCast(@fieldParentPtr("timer", this));
|
|
|
|
const old = this.active_timer_count;
|
|
const new = old + delta;
|
|
|
|
if (comptime Environment.isDebug) {
|
|
assert(new >= 0);
|
|
}
|
|
|
|
this.active_timer_count = new;
|
|
|
|
if (comptime Environment.isPosix) {
|
|
if (new > 0) {
|
|
this.timer_ref.ref(vm);
|
|
} else {
|
|
this.timer_ref.unref(vm);
|
|
}
|
|
} else if (Environment.isWindows) {
|
|
if (old <= 0 and new > 0) {
|
|
this.uv_timer.ref();
|
|
} else if (old > 0 and new <= 0) {
|
|
this.uv_timer.unref();
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn getNextID() callconv(.C) i32 {
|
|
VirtualMachine.get().timer.last_id +%= 1;
|
|
return VirtualMachine.get().timer.last_id;
|
|
}
|
|
|
|
pub fn getTimeout(this: *const All, spec: *timespec) bool {
|
|
if (this.active_timer_count == 0) {
|
|
return false;
|
|
}
|
|
|
|
if (this.timers.peek()) |min| {
|
|
const now = timespec.now();
|
|
switch (now.order(&min.next)) {
|
|
.gt, .eq => {
|
|
spec.* = .{ .nsec = 0, .sec = 0 };
|
|
return true;
|
|
},
|
|
.lt => {
|
|
spec.* = min.next.duration(&now);
|
|
return true;
|
|
},
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export fn Bun__internal_drainTimers(vm: *VirtualMachine) callconv(.C) void {
|
|
drainTimers(&vm.timer, vm);
|
|
}
|
|
|
|
comptime {
|
|
_ = &Bun__internal_drainTimers;
|
|
}
|
|
|
|
pub fn drainTimers(this: *All, vm: *VirtualMachine) void {
|
|
if (this.timers.peek() == null) {
|
|
return;
|
|
}
|
|
|
|
const now = ×pec.now();
|
|
|
|
while (this.timers.peek()) |t| {
|
|
if (t.next.greater(now)) {
|
|
break;
|
|
}
|
|
|
|
assert(this.timers.deleteMin().? == t);
|
|
|
|
switch (t.fire(
|
|
now,
|
|
vm,
|
|
)) {
|
|
.disarm => {},
|
|
.rearm => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn set(
|
|
id: i32,
|
|
globalThis: *JSGlobalObject,
|
|
callback: JSValue,
|
|
interval: i32,
|
|
arguments_array_or_zero: JSValue,
|
|
repeat: bool,
|
|
) !JSC.JSValue {
|
|
JSC.markBinding(@src());
|
|
var vm = globalThis.bunVM();
|
|
|
|
const kind: Kind = if (repeat) .setInterval else .setTimeout;
|
|
|
|
// setImmediate(foo)
|
|
if (kind == .setTimeout and interval == 0) {
|
|
const timer_object, const timer_js = TimerObject.init(globalThis, vm, id, .setImmediate, 0, callback, arguments_array_or_zero);
|
|
timer_object.ref();
|
|
vm.enqueueImmediateTask(JSC.Task.init(timer_object));
|
|
if (vm.isInspectorEnabled()) {
|
|
Debugger.didScheduleAsyncCall(globalThis, .DOMTimer, ID.asyncID(.{ .id = id, .kind = kind }), !repeat);
|
|
}
|
|
return timer_js;
|
|
}
|
|
|
|
const timer_object, const timer_js = TimerObject.init(globalThis, vm, id, kind, interval, callback, arguments_array_or_zero);
|
|
_ = timer_object; // autofix
|
|
|
|
if (vm.isInspectorEnabled()) {
|
|
Debugger.didScheduleAsyncCall(globalThis, .DOMTimer, ID.asyncID(.{ .id = id, .kind = kind }), !repeat);
|
|
}
|
|
|
|
return timer_js;
|
|
}
|
|
|
|
pub fn setImmediate(
|
|
globalThis: *JSGlobalObject,
|
|
callback: JSValue,
|
|
arguments: JSValue,
|
|
) callconv(.C) JSValue {
|
|
JSC.markBinding(@src());
|
|
const id = globalThis.bunVM().timer.last_id;
|
|
globalThis.bunVM().timer.last_id +%= 1;
|
|
|
|
const interval: i32 = 0;
|
|
|
|
const wrappedCallback = callback.withAsyncContextIfNeeded(globalThis);
|
|
|
|
return set(id, globalThis, wrappedCallback, interval, arguments, false) catch
|
|
return JSValue.jsUndefined();
|
|
}
|
|
|
|
comptime {
|
|
if (!JSC.is_bindgen) {
|
|
@export(setImmediate, .{ .name = "Bun__Timer__setImmediate" });
|
|
}
|
|
}
|
|
|
|
pub fn setTimeout(
|
|
globalThis: *JSGlobalObject,
|
|
callback: JSValue,
|
|
countdown: JSValue,
|
|
arguments: JSValue,
|
|
) callconv(.C) JSValue {
|
|
JSC.markBinding(@src());
|
|
const id = globalThis.bunVM().timer.last_id;
|
|
globalThis.bunVM().timer.last_id +%= 1;
|
|
|
|
const interval: i32 = @max(
|
|
countdown.coerce(i32, globalThis),
|
|
// It must be 1 at minimum or setTimeout(cb, 0) will seemingly hang
|
|
1,
|
|
);
|
|
|
|
const wrappedCallback = callback.withAsyncContextIfNeeded(globalThis);
|
|
|
|
return set(id, globalThis, wrappedCallback, interval, arguments, false) catch
|
|
return JSValue.jsUndefined();
|
|
}
|
|
pub fn setInterval(
|
|
globalThis: *JSGlobalObject,
|
|
callback: JSValue,
|
|
countdown: JSValue,
|
|
arguments: JSValue,
|
|
) callconv(.C) JSValue {
|
|
JSC.markBinding(@src());
|
|
const id = globalThis.bunVM().timer.last_id;
|
|
globalThis.bunVM().timer.last_id +%= 1;
|
|
|
|
const wrappedCallback = callback.withAsyncContextIfNeeded(globalThis);
|
|
|
|
// We don't deal with nesting levels directly
|
|
// but we do set the minimum timeout to be 1ms for repeating timers
|
|
const interval: i32 = @max(
|
|
countdown.coerce(i32, globalThis),
|
|
1,
|
|
);
|
|
return set(id, globalThis, wrappedCallback, interval, arguments, true) catch
|
|
return JSValue.jsUndefined();
|
|
}
|
|
|
|
pub fn clearTimer(timer_id_value: JSValue, globalThis: *JSGlobalObject, repeats: bool) void {
|
|
JSC.markBinding(@src());
|
|
|
|
const kind: Kind = if (repeats) .setInterval else .setTimeout;
|
|
var vm = globalThis.bunVM();
|
|
var map = vm.timer.maps.get(kind);
|
|
|
|
const timer: *TimerObject = brk: {
|
|
if (timer_id_value.isAnyInt()) {
|
|
if (map.fetchSwapRemove(timer_id_value.coerce(i32, globalThis))) |entry| {
|
|
// Don't forget to check the type tag.
|
|
// When we start using this list of timers for more things
|
|
// It would be a weird situation, security-wise, if we were to let
|
|
// the user cancel a timer that was of a different type.
|
|
if (entry.value.tag == .TimerObject) {
|
|
break :brk @as(
|
|
*TimerObject,
|
|
@fieldParentPtr("event_loop_timer", entry.value),
|
|
);
|
|
}
|
|
}
|
|
|
|
break :brk null;
|
|
}
|
|
|
|
break :brk TimerObject.fromJS(timer_id_value);
|
|
} orelse return;
|
|
|
|
timer.cancel(vm);
|
|
}
|
|
|
|
pub fn clearTimeout(
|
|
globalThis: *JSGlobalObject,
|
|
id: JSValue,
|
|
) callconv(.C) JSValue {
|
|
JSC.markBinding(@src());
|
|
clearTimer(id, globalThis, false);
|
|
return JSValue.jsUndefined();
|
|
}
|
|
pub fn clearInterval(
|
|
globalThis: *JSGlobalObject,
|
|
id: JSValue,
|
|
) callconv(.C) JSValue {
|
|
JSC.markBinding(@src());
|
|
clearTimer(id, globalThis, true);
|
|
return JSValue.jsUndefined();
|
|
}
|
|
|
|
const Shimmer = @import("../bindings/shimmer.zig").Shimmer;
|
|
|
|
pub const shim = Shimmer("Bun", "Timer", @This());
|
|
pub const name = "Bun__Timer";
|
|
pub const include = "";
|
|
pub const namespace = shim.namespace;
|
|
|
|
pub const Export = shim.exportFunctions(.{
|
|
.setTimeout = setTimeout,
|
|
.setInterval = setInterval,
|
|
.clearTimeout = clearTimeout,
|
|
.clearInterval = clearInterval,
|
|
.getNextID = getNextID,
|
|
});
|
|
|
|
comptime {
|
|
if (!JSC.is_bindgen) {
|
|
@export(setTimeout, .{ .name = Export[0].symbol_name });
|
|
@export(setInterval, .{ .name = Export[1].symbol_name });
|
|
@export(clearTimeout, .{ .name = Export[2].symbol_name });
|
|
@export(clearInterval, .{ .name = Export[3].symbol_name });
|
|
@export(getNextID, .{ .name = Export[4].symbol_name });
|
|
}
|
|
}
|
|
};
|
|
|
|
const uws = bun.uws;
|
|
|
|
pub const TimerObject = struct {
|
|
id: i32 = -1,
|
|
kind: Kind = .setTimeout,
|
|
interval: i32 = 0,
|
|
// 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,
|
|
|
|
strong_this: JSC.Strong = .{},
|
|
|
|
has_js_ref: bool = true,
|
|
ref_count: u32 = 1,
|
|
|
|
event_loop_timer: EventLoopTimer = .{
|
|
.next = .{},
|
|
.tag = .TimerObject,
|
|
},
|
|
|
|
pub usingnamespace JSC.Codegen.JSTimeout;
|
|
pub usingnamespace bun.NewRefCounted(@This(), deinit);
|
|
|
|
extern "C" fn Bun__JSTimeout__call(encodedTimeoutValue: JSValue, globalObject: *JSC.JSGlobalObject) void;
|
|
|
|
pub fn runImmediateTask(this: *TimerObject, vm: *VirtualMachine) void {
|
|
if (this.has_cleared_timer) {
|
|
this.deref();
|
|
return;
|
|
}
|
|
|
|
const this_object = this.strong_this.get() orelse {
|
|
if (Environment.isDebug) {
|
|
@panic("TimerObject.runImmediateTask: this_object is null");
|
|
}
|
|
return;
|
|
};
|
|
const globalThis = this.strong_this.globalThis.?;
|
|
this.strong_this.deinit();
|
|
this.event_loop_timer.state = .FIRED;
|
|
|
|
vm.eventLoop().enter();
|
|
{
|
|
this.ref();
|
|
defer this.deref();
|
|
|
|
run(this_object, globalThis, this.asyncID(), vm);
|
|
|
|
if (this.event_loop_timer.state == .FIRED) {
|
|
this.deref();
|
|
}
|
|
}
|
|
vm.eventLoop().exit();
|
|
}
|
|
|
|
pub fn asyncID(this: *const TimerObject) u64 {
|
|
return ID.asyncID(.{ .id = this.id, .kind = this.kind });
|
|
}
|
|
|
|
pub fn fire(this: *TimerObject, _: *const timespec, vm: *JSC.VirtualMachine) EventLoopTimer.Arm {
|
|
const id = this.id;
|
|
const kind = this.kind;
|
|
const has_been_cleared = this.event_loop_timer.state == .CANCELLED or this.has_cleared_timer or vm.scriptExecutionStatus() != .running;
|
|
|
|
this.event_loop_timer.state = .FIRED;
|
|
this.event_loop_timer.heap = .{};
|
|
|
|
if (has_been_cleared) {
|
|
if (vm.isInspectorEnabled()) {
|
|
if (this.strong_this.globalThis) |globalThis| {
|
|
Debugger.didCancelAsyncCall(globalThis, .DOMTimer, ID.asyncID(.{ .id = id, .kind = kind }));
|
|
}
|
|
}
|
|
|
|
this.has_cleared_timer = true;
|
|
this.strong_this.deinit();
|
|
this.deref();
|
|
|
|
return .disarm;
|
|
}
|
|
|
|
const globalThis = this.strong_this.globalThis.?;
|
|
const this_object = this.strong_this.get().?;
|
|
var time_before_call: timespec = undefined;
|
|
|
|
if (kind != .setInterval) {
|
|
this.strong_this.clear();
|
|
} 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();
|
|
|
|
run(this_object, globalThis, ID.asyncID(.{ .id = id, .kind = kind }), vm);
|
|
|
|
var is_timer_done = false;
|
|
|
|
// Node doesn't drain microtasks after each timer callback.
|
|
if (kind == .setInterval) {
|
|
switch (this.event_loop_timer.state) {
|
|
.FIRED => {
|
|
// If we didn't clear the setInterval, reschedule it starting from
|
|
this.event_loop_timer.next = time_before_call;
|
|
vm.timer.insert(&this.event_loop_timer);
|
|
|
|
if (this.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.remove(&this.event_loop_timer);
|
|
|
|
this.event_loop_timer.next = time_before_call;
|
|
vm.timer.insert(&this.event_loop_timer);
|
|
|
|
// Balance out the ref count.
|
|
// the transition from "FIRED" -> "ACTIVE" caused it to increment.
|
|
this.deref();
|
|
},
|
|
else => {
|
|
is_timer_done = true;
|
|
},
|
|
}
|
|
} else if (this.event_loop_timer.state == .FIRED) {
|
|
is_timer_done = true;
|
|
}
|
|
|
|
if (is_timer_done) {
|
|
if (this.is_keeping_event_loop_alive) {
|
|
this.is_keeping_event_loop_alive = false;
|
|
|
|
switch (this.kind) {
|
|
.setTimeout, .setInterval => {
|
|
vm.timer.incrementTimerRef(-1);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
// The timer will not be re-entered into the event loop at this point.
|
|
this.deref();
|
|
}
|
|
}
|
|
vm.eventLoop().exit();
|
|
|
|
return .disarm;
|
|
}
|
|
|
|
pub fn run(this_object: JSC.JSValue, globalThis: *JSC.JSGlobalObject, async_id: u64, vm: *JSC.VirtualMachine) void {
|
|
if (vm.isInspectorEnabled()) {
|
|
Debugger.willDispatchAsyncCall(globalThis, .DOMTimer, async_id);
|
|
}
|
|
|
|
defer {
|
|
if (vm.isInspectorEnabled()) {
|
|
Debugger.didDispatchAsyncCall(globalThis, .DOMTimer, async_id);
|
|
}
|
|
}
|
|
|
|
// Bun__JSTimeout__call handles exceptions.
|
|
Bun__JSTimeout__call(this_object, globalThis);
|
|
}
|
|
|
|
pub fn init(globalThis: *JSGlobalObject, vm: *VirtualMachine, id: i32, kind: Kind, interval: i32, callback: JSValue, arguments: JSValue) struct { *TimerObject, JSValue } {
|
|
var timer = TimerObject.new(.{
|
|
.id = id,
|
|
.kind = kind,
|
|
.interval = interval,
|
|
});
|
|
var timer_js = timer.toJS(globalThis);
|
|
timer_js.ensureStillAlive();
|
|
if (arguments != .zero)
|
|
TimerObject.argumentsSetCached(timer_js, globalThis, arguments);
|
|
TimerObject.callbackSetCached(timer_js, globalThis, callback);
|
|
timer_js.ensureStillAlive();
|
|
timer.strong_this.set(globalThis, timer_js);
|
|
if (kind != .setImmediate) {
|
|
timer.reschedule(vm);
|
|
}
|
|
return .{ timer, timer_js };
|
|
}
|
|
|
|
pub fn doRef(this: *TimerObject, _: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue {
|
|
const this_value = callframe.this();
|
|
this_value.ensureStillAlive();
|
|
|
|
const did_have_js_ref = this.has_js_ref;
|
|
this.has_js_ref = true;
|
|
|
|
if (!did_have_js_ref) {
|
|
this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), true);
|
|
}
|
|
|
|
return this_value;
|
|
}
|
|
|
|
pub fn doRefresh(this: *TimerObject, globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue {
|
|
const this_value = callframe.this();
|
|
|
|
// setImmediate does not support refreshing and we do not support refreshing after cleanup
|
|
if (this.id == -1 or this.kind == .setImmediate or this.has_cleared_timer) {
|
|
return this_value;
|
|
}
|
|
|
|
this.strong_this.set(globalObject, this_value);
|
|
this.reschedule(VirtualMachine.get());
|
|
|
|
return this_value;
|
|
}
|
|
|
|
pub fn doUnref(this: *TimerObject, _: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) JSValue {
|
|
const this_value = callframe.this();
|
|
this_value.ensureStillAlive();
|
|
|
|
const did_have_js_ref = this.has_js_ref;
|
|
this.has_js_ref = false;
|
|
|
|
if (did_have_js_ref) {
|
|
this.setEnableKeepingEventLoopAlive(JSC.VirtualMachine.get(), false);
|
|
}
|
|
|
|
return this_value;
|
|
}
|
|
|
|
pub fn cancel(this: *TimerObject, vm: *VirtualMachine) void {
|
|
this.setEnableKeepingEventLoopAlive(vm, false);
|
|
this.has_cleared_timer = true;
|
|
|
|
if (this.kind == .setImmediate) return;
|
|
|
|
const was_active = this.event_loop_timer.state == .ACTIVE;
|
|
|
|
this.event_loop_timer.state = .CANCELLED;
|
|
this.strong_this.deinit();
|
|
|
|
if (was_active) {
|
|
vm.timer.remove(&this.event_loop_timer);
|
|
this.deref();
|
|
}
|
|
}
|
|
|
|
pub fn reschedule(this: *TimerObject, vm: *VirtualMachine) void {
|
|
if (this.kind == .setImmediate) return;
|
|
|
|
const now = timespec.msFromNow(this.interval);
|
|
const was_active = this.event_loop_timer.state == .ACTIVE;
|
|
if (was_active) {
|
|
vm.timer.remove(&this.event_loop_timer);
|
|
} else {
|
|
this.ref();
|
|
}
|
|
|
|
this.event_loop_timer.next = now;
|
|
vm.timer.insert(&this.event_loop_timer);
|
|
this.has_cleared_timer = false;
|
|
|
|
if (this.has_js_ref) {
|
|
this.setEnableKeepingEventLoopAlive(vm, true);
|
|
}
|
|
}
|
|
|
|
fn setEnableKeepingEventLoopAlive(this: *TimerObject, vm: *VirtualMachine, enable: bool) void {
|
|
if (this.is_keeping_event_loop_alive == enable) {
|
|
return;
|
|
}
|
|
this.is_keeping_event_loop_alive = enable;
|
|
|
|
switch (this.kind) {
|
|
.setTimeout, .setInterval => {
|
|
vm.timer.incrementTimerRef(if (enable) 1 else -1);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
pub fn hasRef(this: *TimerObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSValue {
|
|
return JSValue.jsBoolean(this.is_keeping_event_loop_alive);
|
|
}
|
|
pub fn toPrimitive(this: *TimerObject, _: *JSC.JSGlobalObject, _: *JSC.CallFrame) JSValue {
|
|
if (!this.has_accessed_primitive) {
|
|
this.has_accessed_primitive = true;
|
|
const vm = VirtualMachine.get();
|
|
vm.timer.maps.get(this.kind).put(bun.default_allocator, this.id, &this.event_loop_timer) catch bun.outOfMemory();
|
|
}
|
|
return JSValue.jsNumber(this.id);
|
|
}
|
|
|
|
pub fn finalize(this: *TimerObject) void {
|
|
this.strong_this.deinit();
|
|
this.deref();
|
|
}
|
|
|
|
pub fn deinit(this: *TimerObject) void {
|
|
this.strong_this.deinit();
|
|
const vm = VirtualMachine.get();
|
|
|
|
if (this.event_loop_timer.state == .ACTIVE) {
|
|
vm.timer.remove(&this.event_loop_timer);
|
|
}
|
|
|
|
if (this.has_accessed_primitive) {
|
|
const map = vm.timer.maps.get(this.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);
|
|
this.destroy();
|
|
}
|
|
};
|
|
|
|
pub const Kind = enum(u32) {
|
|
setTimeout,
|
|
setInterval,
|
|
setImmediate,
|
|
};
|
|
|
|
// this is sized to be the same as one pointer
|
|
pub const ID = extern struct {
|
|
id: i32,
|
|
|
|
kind: Kind = Kind.setTimeout,
|
|
|
|
pub inline fn asyncID(this: ID) u64 {
|
|
return @bitCast(this);
|
|
}
|
|
|
|
pub fn repeats(this: ID) bool {
|
|
return this.kind == .setInterval;
|
|
}
|
|
};
|
|
|
|
const assert = bun.assert;
|
|
const heap = bun.io.heap;
|
|
|
|
pub const EventLoopTimer = struct {
|
|
/// The absolute time to fire this timer next.
|
|
next: timespec,
|
|
|
|
/// Internal heap fields.
|
|
heap: heap.IntrusiveField(EventLoopTimer) = .{},
|
|
|
|
state: State = .PENDING,
|
|
|
|
tag: Tag = .TimerCallback,
|
|
|
|
pub const Tag = if (Environment.isWindows) enum {
|
|
TimerCallback,
|
|
TimerObject,
|
|
TestRunner,
|
|
StatWatcherScheduler,
|
|
UpgradedDuplex,
|
|
WindowsNamedPipe,
|
|
|
|
pub fn Type(comptime T: Tag) type {
|
|
return switch (T) {
|
|
.TimerCallback => TimerCallback,
|
|
.TimerObject => TimerObject,
|
|
.TestRunner => JSC.Jest.TestRunner,
|
|
.StatWatcherScheduler => StatWatcherScheduler,
|
|
.UpgradedDuplex => uws.UpgradedDuplex,
|
|
.WindowsNamedPipe => uws.WindowsNamedPipe,
|
|
};
|
|
}
|
|
} else enum {
|
|
TimerCallback,
|
|
TimerObject,
|
|
TestRunner,
|
|
StatWatcherScheduler,
|
|
UpgradedDuplex,
|
|
|
|
pub fn Type(comptime T: Tag) type {
|
|
return switch (T) {
|
|
.TimerCallback => TimerCallback,
|
|
.TimerObject => TimerObject,
|
|
.TestRunner => JSC.Jest.TestRunner,
|
|
.StatWatcherScheduler => StatWatcherScheduler,
|
|
.UpgradedDuplex => uws.UpgradedDuplex,
|
|
};
|
|
}
|
|
};
|
|
|
|
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,
|
|
};
|
|
|
|
fn less(_: void, a: *const EventLoopTimer, b: *const EventLoopTimer) bool {
|
|
const order = a.next.order(&b.next);
|
|
if (order == .eq) {
|
|
if (a.tag == .TimerObject and b.tag == .TimerObject) {
|
|
const a_timer: *const TimerObject = @fieldParentPtr("event_loop_timer", a);
|
|
const b_timer: *const TimerObject = @fieldParentPtr("event_loop_timer", b);
|
|
return a_timer.id < b_timer.id;
|
|
}
|
|
|
|
if (b.tag == .TimerObject) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return order == .lt;
|
|
}
|
|
|
|
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) {
|
|
inline else => |t| {
|
|
var container: *t.Type() = @alignCast(@fieldParentPtr("event_loop_timer", this));
|
|
if (comptime t.Type() == TimerObject) {
|
|
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;
|
|
}
|
|
|
|
return container.callback(container);
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn deinit(_: *EventLoopTimer) void {}
|
|
};
|
|
|
|
const timespec = bun.timespec;
|