diff options
Diffstat (limited to 'src/bun.js/event_loop.zig')
-rw-r--r-- | src/bun.js/event_loop.zig | 128 |
1 files changed, 124 insertions, 4 deletions
diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index f9f11c943..aac1e3dcd 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -220,6 +220,21 @@ pub const EventLoop = struct { start_server_on_next_tick: bool = false, defer_count: std.atomic.Atomic(usize) = std.atomic.Atomic(usize).init(0), + gc_timer: *uws.Timer = undefined, + gc_last_heap_size: usize = 0, + gc_last_heap_size_on_repeating_timer: usize = 0, + heap_size_didnt_change_for_repeating_timer_ticks_count: u8 = 0, + gc_timer_state: GCTimerState = GCTimerState.pending, + gc_repeating_timer: *uws.Timer = undefined, + gc_timer_interval: i32 = 0, + gc_repeating_timer_fast: bool = true, + + pub const GCTimerState = enum { + pending, + scheduled, + run_on_next_tick, + }; + pub const Queue = std.fifo.LinearFifo(Task, .Dynamic); pub fn tickWithCount(this: *EventLoop) u32 { @@ -333,14 +348,59 @@ pub const EventLoop = struct { var loop = this.virtual_machine.uws_event_loop.?; if (loop.num_polls > 0 or loop.active > 0) { loop.tick(); + this.processGCTimer(); // this.afterUSocketsTick(); } } + pub fn scheduleGCTimer(this: *EventLoop) void { + this.gc_timer_state = .scheduled; + this.gc_timer.set(this, onGCTimer, 16, 0); + } + + pub fn processGCTimer(this: *EventLoop) void { + var vm = this.virtual_machine.global.vm(); + const this_heap_size = vm.blockBytesAllocated(); + const prev = this.gc_last_heap_size; + + switch (this.gc_timer_state) { + .run_on_next_tick => { + if (this_heap_size > prev) { + this.scheduleGCTimer(); + this.updateGCRepeatTimer(.fast); + } else { + this.gc_timer_state = .pending; + } + vm.collectAsync(); + this.gc_last_heap_size = this_heap_size; + }, + .pending => { + if (this_heap_size > prev) { + this.updateGCRepeatTimer(.fast); + + if (this_heap_size > prev * 2) { + this.performGC(); + } else { + this.scheduleGCTimer(); + } + } + }, + .scheduled => { + if (this_heap_size > prev * 2) { + this.updateGCRepeatTimer(.fast); + this.performGC(); + } + }, + } + } + // TODO: fix this technical debt pub fn tick(this: *EventLoop) void { var ctx = this.virtual_machine; this.tickConcurrent(); + + this.processGCTimer(); + var global_vm = ctx.global.vm(); while (true) { while (this.tickWithCount() > 0) { @@ -373,10 +433,6 @@ pub const EventLoop = struct { if (loop.active > 0 or (ctx.us_loop_reference_count > 0 and !ctx.is_us_loop_entered and (loop.num_polls > 0 or this.start_server_on_next_tick))) { if (this.tickConcurrentWithCount() > 0) { this.tick(); - } else { - if ((@intCast(c_ulonglong, ctx.uws_event_loop.?.internal_loop_data.iteration_nr) % 1_000) == 1) { - _ = ctx.global.vm().runGC(true); - } } ctx.is_us_loop_entered = true; @@ -427,11 +483,75 @@ pub const EventLoop = struct { if (this.virtual_machine.uws_event_loop == null) { var actual = uws.Loop.get().?; this.virtual_machine.uws_event_loop = actual; + this.gc_timer = uws.Timer.create(actual, this); + this.gc_repeating_timer = uws.Timer.create(actual, this); + + var gc_timer_interval: i32 = 1000; + if (this.virtual_machine.bundler.env.map.get("BUN_GC_TIMER_INTERVAL")) |timer| { + if (std.fmt.parseInt(i32, timer, 10)) |parsed| { + if (parsed > 0) { + gc_timer_interval = parsed; + } + } else |_| {} + } + this.gc_repeating_timer.set(this, onGCRepeatingTimer, gc_timer_interval, gc_timer_interval); + this.gc_timer_interval = gc_timer_interval; // _ = actual.addPostHandler(*JSC.EventLoop, this, JSC.EventLoop.afterUSocketsTick); // _ = actual.addPreHandler(*JSC.VM, this.virtual_machine.global.vm(), JSC.VM.drainMicrotasks); } } + pub fn onGCTimer(timer: *uws.Timer) callconv(.C) void { + var this = timer.as(*EventLoop); + this.gc_timer_state = .run_on_next_tick; + } + + // We want to always run GC once in awhile + // But if you have a long-running instance of Bun, you don't want the + // program constantly using CPU doing GC for no reason + // + // So we have two settings for this GC timer: + // + // - Fast: GC runs every 1 second + // - Slow: GC runs every 30 seconds + // + // When the heap size is increasing, we always switch to fast mode + // When the heap size has been the same or less for 30 seconds, we switch to slow mode + pub fn updateGCRepeatTimer(this: *EventLoop, comptime setting: @Type(.EnumLiteral)) void { + if (setting == .fast and !this.gc_repeating_timer_fast) { + this.gc_repeating_timer_fast = true; + this.gc_repeating_timer.set(this, onGCRepeatingTimer, this.gc_timer_interval, this.gc_timer_interval); + this.heap_size_didnt_change_for_repeating_timer_ticks_count = 0; + } else if (setting == .slow and this.gc_repeating_timer_fast) { + this.gc_repeating_timer_fast = false; + this.gc_repeating_timer.set(this, onGCRepeatingTimer, 30_000, 30_000); + this.heap_size_didnt_change_for_repeating_timer_ticks_count = 0; + } + } + + pub fn onGCRepeatingTimer(timer: *uws.Timer) callconv(.C) void { + var this = timer.as(*EventLoop); + const prev_heap_size = this.gc_last_heap_size_on_repeating_timer; + this.performGC(); + this.gc_last_heap_size_on_repeating_timer = this.gc_last_heap_size; + if (prev_heap_size == this.gc_last_heap_size_on_repeating_timer) { + this.heap_size_didnt_change_for_repeating_timer_ticks_count +|= 1; + if (this.heap_size_didnt_change_for_repeating_timer_ticks_count >= 30) { + // make the timer interval longer + this.updateGCRepeatTimer(.slow); + } + } else { + this.heap_size_didnt_change_for_repeating_timer_ticks_count = 0; + this.updateGCRepeatTimer(.fast); + } + } + + /// Asynchronously run the garbage collector and track how much memory is now allocated + pub fn performGC(this: *EventLoop) void { + this.global.vm().collectAsync(); + this.gc_last_heap_size = this.global.vm().blockBytesAllocated(); + } + pub fn enqueueTaskConcurrent(this: *EventLoop, task: *ConcurrentTask) void { JSC.markBinding(@src()); |