Skip to content

Commit c60c4de

Browse files
committed
fix: event loop GC via threshold-based gc_check_trigger_export
After removing gc_check_trigger from timer ticks (caused SIGSEGV from register-held values being missed by conservative stack scanner), GC had no trigger point for long-running services. Memory grew unbounded. This adds gc_check_trigger_export() — an extern "C" wrapper around the existing threshold-based gc_check_trigger — and calls it from the main event loop AFTER both timer ticks have returned. At this point, all live JS values are safely stored in module globals, closure boxes, or timer root lists (not in registers), so the conservative scanner finds them. The threshold check (arena >= 64MB or malloc objects >= 10k) ensures GC only fires when needed, not on every 10ms loop tick. Verified: RSS stable at ~90MB (vs 40MB/min growth without, and SIGSEGV with the timer-tick approach).
1 parent 33561c2 commit c60c4de

3 files changed

Lines changed: 26 additions & 1 deletion

File tree

crates/perry-codegen/src/module_init.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,11 +798,21 @@ impl crate::codegen::Compiler {
798798
let has_work = builder.ins().icmp(IntCC::NotEqual, any_pending, zero_i32);
799799
builder.ins().brif(has_work, loop_body, &[], loop_exit, &[]);
800800

801-
// loop_body: tick both timer queues, sleep 10ms, jump back
801+
// loop_body: tick both timer queues, then GC, sleep 10ms, jump back
802802
builder.switch_to_block(loop_body);
803803
builder.seal_block(loop_body);
804804
builder.ins().call(int_tick_ref, &[]);
805805
builder.ins().call(cb_tick_ref, &[]);
806+
807+
// GC safe point: timer callbacks have returned, so all live JS values
808+
// are stored in module globals, closure boxes, or timer root lists —
809+
// NOT in registers. Uses threshold-based check (not unconditional
810+
// collection) to avoid the overhead of running GC on every loop tick.
811+
if let Some(&gc_trigger_func) = self.extern_funcs.get("gc_check_trigger_export") {
812+
let gc_ref = self.module.declare_func_in_func(gc_trigger_func, builder.func);
813+
builder.ins().call(gc_ref, &[]);
814+
}
815+
806816
let ten_ms = builder.ins().f64const(10.0);
807817
builder.ins().call(sleep_ref, &[ten_ms]);
808818
builder.ins().jump(loop_header, &[]);

crates/perry-codegen/src/runtime_decls.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10710,6 +10710,14 @@ impl Compiler {
1071010710
self.extern_funcs.insert(Cow::Borrowed("gc"), func_id);
1071110711
}
1071210712

10713+
// gc_check_trigger_export() -> void
10714+
// Threshold-based GC trigger for the event loop (only collects if needed)
10715+
{
10716+
let sig = self.module.make_signature();
10717+
let func_id = self.module.declare_function("gc_check_trigger_export", Linkage::Import, &sig)?;
10718+
self.extern_funcs.insert(Cow::Borrowed("gc_check_trigger_export"), func_id);
10719+
}
10720+
1071310721
// js_gc_register_global_root(ptr: i64) -> void
1071410722
{
1071510723
let mut sig = self.module.make_signature();

crates/perry-runtime/src/gc.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,13 @@ pub extern "C" fn js_gc_collect() {
259259
gc_collect_inner();
260260
}
261261

262+
/// Threshold-based GC trigger (safe for use from the event loop).
263+
/// Only runs collection if arena or malloc thresholds are exceeded.
264+
#[no_mangle]
265+
pub extern "C" fn gc_check_trigger_export() {
266+
gc_check_trigger();
267+
}
268+
262269
/// Main GC collection
263270
fn gc_collect_inner() {
264271
let start = std::time::Instant::now();

0 commit comments

Comments
 (0)