diff --git a/include/mruby/gc.h b/include/mruby/gc.h index 076c34126..f3024f88c 100644 --- a/include/mruby/gc.h +++ b/include/mruby/gc.h @@ -56,6 +56,9 @@ typedef struct mrb_gc { mrb_bool generational :1; /* generational GC mode */ mrb_bool full :1; /* major GC mode */ mrb_bool out_of_memory :1; /* out-of-memory error occurred */ + size_t step_limit; /* 0=unlimited, >0=absolute step cap */ + size_t malloc_increase; /* malloc bytes since last GC cycle */ + size_t malloc_threshold; /* 0=disabled, >0=bytes to trigger GC */ #ifdef MRB_GC_FIXED_ARENA struct RBasic *arena[MRB_GC_ARENA_SIZE]; /* GC protection array */ diff --git a/src/gc.c b/src/gc.c index f5757ddf9..b4c2395db 100644 --- a/src/gc.c +++ b/src/gc.c @@ -224,6 +224,17 @@ mrb_realloc_simple(mrb_state *mrb, void *p, size_t len) p2 = mrb_basic_alloc_func(p, len); } + if (p2 && len > 0) { + mrb->gc.malloc_increase += len; + if (mrb->gc.malloc_threshold > 0 && + mrb->gc.malloc_increase >= mrb->gc.malloc_threshold && + mrb->gc.state == MRB_GC_STATE_ROOT && + !mrb->gc.disabled && !mrb->gc.iterating) { + mrb->gc.malloc_increase = 0; + mrb_incremental_gc(mrb); + } + } + return p2; } @@ -1311,6 +1322,9 @@ incremental_gc_step(mrb_state *mrb, mrb_gc *gc) { size_t limit = 0, result = 0; limit = (GC_STEP_SIZE/100) * gc->step_ratio; + if (gc->step_limit > 0 && limit > gc->step_limit) { + limit = gc->step_limit; + } while (result < limit) { result += incremental_gc(mrb, gc, limit); if (gc->state == MRB_GC_STATE_ROOT) @@ -1364,6 +1378,7 @@ mrb_incremental_gc(mrb_state *mrb) } if (gc->state == MRB_GC_STATE_ROOT) { + gc->malloc_increase = 0; mrb_assert(gc->live >= gc->live_after_mark); gc->threshold = (gc->live_after_mark/100) * gc->interval_ratio; if (gc->threshold < GC_STEP_SIZE) { @@ -1612,6 +1627,44 @@ gc_step_ratio_set(mrb_state *mrb, mrb_value obj) return mrb_nil_value(); } +static mrb_value +gc_step_limit_get(mrb_state *mrb, mrb_value obj) +{ + return mrb_int_value(mrb, (mrb_int)mrb->gc.step_limit); +} + +static mrb_value +gc_step_limit_set(mrb_state *mrb, mrb_value obj) +{ + mrb_int limit; + + mrb_get_args(mrb, "i", &limit); + if (limit < 0) { + mrb_raise(mrb, E_ARGUMENT_ERROR, "step_limit must be non-negative"); + } + mrb->gc.step_limit = (size_t)limit; + return mrb_int_value(mrb, limit); +} + +static mrb_value +gc_malloc_threshold_get(mrb_state *mrb, mrb_value obj) +{ + return mrb_int_value(mrb, (mrb_int)mrb->gc.malloc_threshold); +} + +static mrb_value +gc_malloc_threshold_set(mrb_state *mrb, mrb_value obj) +{ + mrb_int threshold; + + mrb_get_args(mrb, "i", &threshold); + if (threshold < 0) { + mrb_raise(mrb, E_ARGUMENT_ERROR, "malloc_threshold must be non-negative"); + } + mrb->gc.malloc_threshold = (size_t)threshold; + return mrb_int_value(mrb, threshold); +} + static void change_gen_gc_mode(mrb_state *mrb, mrb_gc *gc, mrb_bool enable) { @@ -1740,6 +1793,9 @@ gc_stat(mrb_state *mrb, mrb_value self) mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "state")), mrb_int_value(mrb, (mrb_int)gc->state)); mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "generational")), mrb_bool_value(gc->generational)); mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "full")), mrb_bool_value(gc->full)); + mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "step_limit")), mrb_int_value(mrb, (mrb_int)gc->step_limit)); + mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "malloc_increase")), mrb_int_value(mrb, (mrb_int)gc->malloc_increase)); + mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "malloc_threshold")), mrb_int_value(mrb, (mrb_int)gc->malloc_threshold)); #ifdef MRB_GC_STATS mrb_hash_set(mrb, hash, mrb_symbol_value(mrb_intern_lit(mrb, "total")), mrb_int_value(mrb, (mrb_int)gc->gc_total_count)); @@ -1773,6 +1829,10 @@ mrb_init_gc(mrb_state *mrb) mrb_define_class_method_id(mrb, gc, MRB_SYM_E(interval_ratio), gc_interval_ratio_set, MRB_ARGS_REQ(1)); mrb_define_class_method_id(mrb, gc, MRB_SYM(step_ratio), gc_step_ratio_get, MRB_ARGS_NONE()); mrb_define_class_method_id(mrb, gc, MRB_SYM_E(step_ratio), gc_step_ratio_set, MRB_ARGS_REQ(1)); + mrb_define_class_method_id(mrb, gc, MRB_SYM(step_limit), gc_step_limit_get, MRB_ARGS_NONE()); + mrb_define_class_method_id(mrb, gc, MRB_SYM_E(step_limit), gc_step_limit_set, MRB_ARGS_REQ(1)); + mrb_define_class_method_id(mrb, gc, MRB_SYM(malloc_threshold), gc_malloc_threshold_get, MRB_ARGS_NONE()); + mrb_define_class_method_id(mrb, gc, MRB_SYM_E(malloc_threshold), gc_malloc_threshold_set, MRB_ARGS_REQ(1)); mrb_define_class_method_id(mrb, gc, MRB_SYM_E(generational_mode), gc_generational_mode_set, MRB_ARGS_REQ(1)); mrb_define_class_method_id(mrb, gc, MRB_SYM(generational_mode), gc_generational_mode_get, MRB_ARGS_NONE()); } diff --git a/test/t/gc.rb b/test/t/gc.rb index 4b800e945..3ea6b7ef8 100644 --- a/test/t/gc.rb +++ b/test/t/gc.rb @@ -33,6 +33,56 @@ assert('GC.step_ratio=') do end end +assert('GC.step_limit=') do + origin = GC.step_limit + begin + assert_equal 0, origin # default: unlimited + assert_equal 512, (GC.step_limit = 512) + assert_equal 512, GC.step_limit + assert_equal 0, (GC.step_limit = 0) # back to unlimited + ensure + GC.step_limit = origin + end +end + +assert('GC.step_limit - GC completes with small limit') do + origin = GC.step_limit + begin + GC.step_limit = 64 + # GC should still complete even with a small step limit + GC.start + assert_true GC.stat[:live] > 0 + ensure + GC.step_limit = origin + end +end + +assert('GC.malloc_threshold=') do + origin = GC.malloc_threshold + begin + assert_equal 0, origin # default: disabled + assert_equal 65536, (GC.malloc_threshold = 65536) + assert_equal 65536, GC.malloc_threshold + assert_equal 0, (GC.malloc_threshold = 0) # back to disabled + ensure + GC.malloc_threshold = origin + end +end + +assert('GC.malloc_threshold - triggers GC on large allocations') do + origin = GC.malloc_threshold + begin + GC.malloc_threshold = 4096 + GC.start # reset malloc_increase + # allocate large strings to exceed threshold + 100.times { "x" * 1024 } + stat = GC.stat + assert_true stat[:malloc_increase] >= 0 + ensure + GC.malloc_threshold = origin + end +end + assert('GC.generational_mode=') do origin = GC.generational_mode begin