From 6816e8efcff3be75f8020cd1b0ea57d3cd664bbc Mon Sep 17 00:00:00 2001 From: Adam Hess Date: Thu, 12 Oct 2023 11:15:53 -0700 Subject: [PATCH] Free everything at shutdown when the RUBY_FREE_ON_SHUTDOWN environment variable is set, manually free memory at shutdown. Co-authored-by: Nobuyoshi Nakada Co-authored-by: Peter Zhu --- builtin.c | 6 +++ common.mk | 2 + cont.c | 6 +++ encoding.c | 18 ++++++++ error.c | 8 ++++ gc.c | 36 ++++++++++++++++ internal/cont.h | 3 ++ internal/encoding.h | 3 ++ internal/error.h | 3 ++ internal/random.h | 1 + internal/symbol.h | 3 ++ internal/transcode.h | 3 ++ internal/vm.h | 5 +++ iseq.c | 6 +++ iseq.h | 2 + load.c | 7 ++++ marshal.c | 16 +++++++- miniinit.c | 7 ++++ missing/setproctitle.c | 1 + random.c | 6 +++ ruby.c | 5 +++ symbol.c | 10 +++++ test/ruby/test_rubyoptions.rb | 6 +++ transcode.c | 22 ++++++++++ variable.c | 27 ++++++++++++ variable.h | 2 + vm.c | 77 ++++++++++++++++++++++++++++++++--- 27 files changed, 285 insertions(+), 6 deletions(-) diff --git a/builtin.c b/builtin.c index b725d8b968c8a3..4fc2ab43d1477f 100644 --- a/builtin.c +++ b/builtin.c @@ -57,6 +57,12 @@ rb_load_with_builtin_functions(const char *feature_name, const struct rb_builtin #endif +void +rb_free_loaded_builtin_table(void) +{ + // do nothing +} + void Init_builtin(void) { diff --git a/common.mk b/common.mk index 928eb0a02bf214..c2bf43194ac7bf 100644 --- a/common.mk +++ b/common.mk @@ -18700,6 +18700,7 @@ vm.$(OBJEXT): $(top_srcdir)/internal/compar.h vm.$(OBJEXT): $(top_srcdir)/internal/compile.h vm.$(OBJEXT): $(top_srcdir)/internal/compilers.h vm.$(OBJEXT): $(top_srcdir)/internal/cont.h +vm.$(OBJEXT): $(top_srcdir)/internal/encoding.h vm.$(OBJEXT): $(top_srcdir)/internal/error.h vm.$(OBJEXT): $(top_srcdir)/internal/eval.h vm.$(OBJEXT): $(top_srcdir)/internal/fixnum.h @@ -18721,6 +18722,7 @@ vm.$(OBJEXT): $(top_srcdir)/internal/string.h vm.$(OBJEXT): $(top_srcdir)/internal/struct.h vm.$(OBJEXT): $(top_srcdir)/internal/symbol.h vm.$(OBJEXT): $(top_srcdir)/internal/thread.h +vm.$(OBJEXT): $(top_srcdir)/internal/transcode.h vm.$(OBJEXT): $(top_srcdir)/internal/variable.h vm.$(OBJEXT): $(top_srcdir)/internal/vm.h vm.$(OBJEXT): $(top_srcdir)/internal/warnings.h diff --git a/cont.c b/cont.c index 6bafc07ca88fe3..88c726927cab9d 100644 --- a/cont.c +++ b/cont.c @@ -275,6 +275,12 @@ struct rb_fiber_struct { static struct fiber_pool shared_fiber_pool = {NULL, NULL, 0, 0, 0, 0}; +void +rb_free_shared_fiber_pool(void) +{ + xfree(shared_fiber_pool.allocations); +} + static ID fiber_initialize_keywords[3] = {0}; /* diff --git a/encoding.c b/encoding.c index 8bfab731770fdb..1ede6a677cb989 100644 --- a/encoding.c +++ b/encoding.c @@ -71,6 +71,24 @@ static struct enc_table { st_table *names; } global_enc_table; +static int +enc_names_free_i(st_data_t name, st_data_t idx, st_data_t args) +{ + ruby_xfree((void *)name); + return ST_DELETE; +} + +void +rb_free_global_enc_table(void) +{ + for (size_t i = 0; i < ENCODING_LIST_CAPA; i++) { + xfree((void *)global_enc_table.list[i].enc); + } + + st_foreach(global_enc_table.names, enc_names_free_i, (st_data_t)0); + st_free_table(global_enc_table.names); +} + static rb_encoding *global_enc_ascii, *global_enc_utf_8, *global_enc_us_ascii; diff --git a/error.c b/error.c index 81dce83093b507..e481dbaee42eb4 100644 --- a/error.c +++ b/error.c @@ -2693,6 +2693,14 @@ syntax_error_with_path(VALUE exc, VALUE path, VALUE *mesg, rb_encoding *enc) static st_table *syserr_tbl; +void +rb_free_warning(void) +{ + st_free_table(warning_categories.id2enum); + st_free_table(warning_categories.enum2id); + st_free_table(syserr_tbl); +} + static VALUE set_syserr(int n, const char *name) { diff --git a/gc.c b/gc.c index 08e4a2cd3ee88c..ce5ae34dc80cfc 100644 --- a/gc.c +++ b/gc.c @@ -4582,6 +4582,35 @@ force_chain_object(st_data_t key, st_data_t val, st_data_t arg) bool rb_obj_is_main_ractor(VALUE gv); +void +rb_objspace_free_objects(rb_objspace_t *objspace) +{ + for (size_t i = 0; i < heap_allocated_pages; i++) { + struct heap_page *page = heap_pages_sorted[i]; + short stride = page->slot_size; + + uintptr_t p = (uintptr_t)page->start; + uintptr_t pend = p + page->total_slots * stride; + for (; p < pend; p += stride) { + VALUE vp = (VALUE)p; + switch (BUILTIN_TYPE(vp)) { + case T_DATA: { + if (rb_obj_is_mutex(vp) || rb_obj_is_thread(vp)) { + rb_data_free(objspace, vp); + } + break; + } + case T_ARRAY: + obj_free(objspace, vp); + break; + default: + break; + } + } + } +} + + void rb_objspace_call_finalizer(rb_objspace_t *objspace) { @@ -4648,7 +4677,14 @@ rb_objspace_call_finalizer(rb_objspace_t *objspace) make_io_zombie(objspace, vp); } break; + case T_SYMBOL: + case T_ARRAY: + case T_NONE: + break; default: + if (rb_free_on_exit) { + obj_free(objspace, vp); + } break; } if (poisoned) { diff --git a/internal/cont.h b/internal/cont.h index c3b091668a87b8..3c2528a02a6e77 100644 --- a/internal/cont.h +++ b/internal/cont.h @@ -22,6 +22,9 @@ void rb_jit_cont_init(void); void rb_jit_cont_each_iseq(rb_iseq_callback callback, void *data); void rb_jit_cont_finish(void); +/* vm.c */ +void rb_free_shared_fiber_pool(void); + // Copy locals from the current execution to the specified fiber. VALUE rb_fiber_inherit_storage(struct rb_execution_context_struct *ec, struct rb_fiber_struct *fiber); diff --git a/internal/encoding.h b/internal/encoding.h index a3b81bd3881436..11ffa6d83d955f 100644 --- a/internal/encoding.h +++ b/internal/encoding.h @@ -29,4 +29,7 @@ void rb_enc_set_base(const char *name, const char *orig); int rb_enc_set_dummy(int index); PUREFUNC(int rb_data_is_encoding(VALUE obj)); +/* vm.c */ +void rb_free_global_enc_table(void); + #endif /* INTERNAL_ENCODING_H */ diff --git a/internal/error.h b/internal/error.h index 5fee4689297b2e..7e41f134d7c78c 100644 --- a/internal/error.h +++ b/internal/error.h @@ -169,6 +169,9 @@ VALUE rb_syserr_new_path_in(const char *func_name, int n, VALUE path); #endif RUBY_SYMBOL_EXPORT_END +/* vm.c */ +void rb_free_warning(void); + static inline void rb_raise_cstr_i(VALUE etype, VALUE mesg) { diff --git a/internal/random.h b/internal/random.h index 231e2d5d7e8e8f..127b908e163f05 100644 --- a/internal/random.h +++ b/internal/random.h @@ -12,5 +12,6 @@ /* random.c */ int ruby_fill_random_bytes(void *, size_t, int); +void rb_free_default_rand_key(void); #endif /* INTERNAL_RANDOM_H */ diff --git a/internal/symbol.h b/internal/symbol.h index 30c81ea004169b..e7730cb70fd08b 100644 --- a/internal/symbol.h +++ b/internal/symbol.h @@ -32,6 +32,9 @@ ID rb_make_temporary_id(size_t n); void rb_gc_free_dsymbol(VALUE); int rb_static_id_valid_p(ID id); +/* vm.c */ +void rb_free_static_symid_str(void); + #if __has_builtin(__builtin_constant_p) #define rb_sym_intern_ascii_cstr(ptr) \ (__builtin_constant_p(ptr) ? \ diff --git a/internal/transcode.h b/internal/transcode.h index 9922332ea9c076..ce4f2341be889f 100644 --- a/internal/transcode.h +++ b/internal/transcode.h @@ -17,4 +17,7 @@ extern VALUE rb_cEncodingConverter; size_t rb_econv_memsize(rb_econv_t *); +/* vm.c */ +void rb_free_transcoder_table(void); + #endif /* INTERNAL_TRANSCODE_H */ diff --git a/internal/vm.h b/internal/vm.h index d10c14eb4df1e4..f491b5e2809cd5 100644 --- a/internal/vm.h +++ b/internal/vm.h @@ -83,6 +83,11 @@ void rb_check_stack_overflow(void); extern uint64_t rb_vm_insns_count; #endif +extern bool rb_free_on_exit; + +/* miniinit.c and builtin.c */ +void rb_free_loaded_builtin_table(void); + /* vm_insnhelper.c */ VALUE rb_equal_opt(VALUE obj1, VALUE obj2); VALUE rb_eql_opt(VALUE obj1, VALUE obj2); diff --git a/iseq.c b/iseq.c index d1547410973f82..b68c23040418b5 100644 --- a/iseq.c +++ b/iseq.c @@ -3422,6 +3422,12 @@ typedef struct insn_data_struct { } insn_data_t; static insn_data_t insn_data[VM_INSTRUCTION_SIZE/2]; +void +rb_free_encoded_insn_data(void) +{ + st_free_table(encoded_insn_data); +} + void rb_vm_encoded_insn_data_table_init(void) { diff --git a/iseq.h b/iseq.h index 5d17bb5d5b984d..210c79906df047 100644 --- a/iseq.h +++ b/iseq.h @@ -329,6 +329,8 @@ VALUE rb_iseq_local_variables(const rb_iseq_t *iseq); attr_index_t rb_estimate_iv_count(VALUE klass, const rb_iseq_t * initialize_iseq); +void rb_free_encoded_insn_data(void); + RUBY_SYMBOL_EXPORT_END #endif /* RUBY_ISEQ_H */ diff --git a/load.c b/load.c index 780edf27a74844..716b5af98f6171 100644 --- a/load.c +++ b/load.c @@ -360,6 +360,13 @@ loaded_features_index_clear_i(st_data_t key, st_data_t val, st_data_t arg) return ST_DELETE; } +void +rb_free_loaded_features_index(rb_vm_t *vm) +{ + st_foreach(vm->loaded_features_index, loaded_features_index_clear_i, 0); + st_free_table(vm->loaded_features_index); +} + static st_table * get_loaded_features_index(rb_vm_t *vm) { diff --git a/marshal.c b/marshal.c index 2774db07fdad21..1cd71efd54fe31 100644 --- a/marshal.c +++ b/marshal.c @@ -2517,6 +2517,20 @@ Init_marshal(void) rb_define_const(rb_mMarshal, "MINOR_VERSION", INT2FIX(MARSHAL_MINOR)); } +static int +free_compat_i(st_data_t key, st_data_t value, st_data_t _) +{ + xfree((marshal_compat_t *)value); + return ST_CONTINUE; +} + +static void +free_compat_allocator_table(void *data) +{ + st_foreach(data, free_compat_i, 0); + st_free_table(data); +} + static st_table * compat_allocator_table(void) { @@ -2525,7 +2539,7 @@ compat_allocator_table(void) #undef RUBY_UNTYPED_DATA_WARNING #define RUBY_UNTYPED_DATA_WARNING 0 compat_allocator_tbl_wrapper = - Data_Wrap_Struct(0, mark_marshal_compat_t, 0, compat_allocator_tbl); + Data_Wrap_Struct(0, mark_marshal_compat_t, free_compat_allocator_table, compat_allocator_tbl); rb_gc_register_mark_object(compat_allocator_tbl_wrapper); return compat_allocator_tbl; } diff --git a/miniinit.c b/miniinit.c index 2a14a0d1c5c667..bb22d6d9f5bf33 100644 --- a/miniinit.c +++ b/miniinit.c @@ -49,3 +49,10 @@ Init_enc(void) } #include "mini_builtin.c" + +void +rb_free_loaded_builtin_table(void) +{ + if (loaded_builtin_table) + st_free_table(loaded_builtin_table); +} diff --git a/missing/setproctitle.c b/missing/setproctitle.c index 811829c0600476..6a8532381803a0 100644 --- a/missing/setproctitle.c +++ b/missing/setproctitle.c @@ -135,6 +135,7 @@ compat_init_setproctitle(int argc, char *argv[]) } #ifndef HAVE_SETPROCTITLE + void setproctitle(const char *fmt, ...) { diff --git a/random.c b/random.c index 4b5b7ab6c475e0..5cd2d917a4d584 100644 --- a/random.c +++ b/random.c @@ -157,6 +157,12 @@ rand_start(rb_random_mt_t *r) static rb_ractor_local_key_t default_rand_key; +void +rb_free_default_rand_key(void) +{ + xfree(default_rand_key); +} + static void default_rand_mark(void *ptr) { diff --git a/ruby.c b/ruby.c index a161bcb3d62227..db25e1bc33aafe 100644 --- a/ruby.c +++ b/ruby.c @@ -1754,6 +1754,11 @@ ruby_opt_init(ruby_cmdline_options_t *opt) "environment variables RUBY_GC_HEAP_%d_INIT_SLOTS"); } + if (getenv("RUBY_FREE_ON_EXIT")) { + rb_warn("Free on exit is experimental and may be unstable"); + rb_free_on_exit = true; + } + #if USE_RJIT // rb_call_builtin_inits depends on RubyVM::RJIT.enabled? if (opt->rjit.on) diff --git a/symbol.c b/symbol.c index 6a0c926db8a91e..2c2ab4380c5598 100644 --- a/symbol.c +++ b/symbol.c @@ -531,6 +531,16 @@ register_sym(rb_symbols_t *symbols, VALUE str, VALUE sym) } } +void +rb_free_static_symid_str(void) +{ + GLOBAL_SYMBOLS_ENTER(symbols) + { + st_free_table(symbols->str_sym); + } + GLOBAL_SYMBOLS_LEAVE(); +} + static void unregister_sym(rb_symbols_t *symbols, VALUE str, VALUE sym) { diff --git a/test/ruby/test_rubyoptions.rb b/test/ruby/test_rubyoptions.rb index 16cff9754a7ef9..eb10ed6aec7ed5 100644 --- a/test/ruby/test_rubyoptions.rb +++ b/test/ruby/test_rubyoptions.rb @@ -1227,4 +1227,10 @@ def test_null_script omit "#{IO::NULL} is not a character device" unless File.chardev?(IO::NULL) assert_in_out_err([IO::NULL], success: true) end + + def test_free_on_exit_env_var + env = {"RUBY_FREE_ON_EXIT"=>"1"} + assert_ruby_status([env, "-e;"]) + assert_in_out_err([env, "-W"], "", [], /Free on exit is experimental and may be unstable/) + end end diff --git a/transcode.c b/transcode.c index 090680f4a08059..892814ee3ba09e 100644 --- a/transcode.c +++ b/transcode.c @@ -181,6 +181,28 @@ typedef struct { static st_table *transcoder_table; +static int +free_inner_transcode_i(st_data_t key, st_data_t val, st_data_t arg) +{ + xfree((void *)val); + return ST_DELETE; +} + +static int +free_transcode_i(st_data_t key, st_data_t val, st_data_t arg) +{ + st_foreach((void *)val, free_inner_transcode_i, 0); + st_free_table((void *)val); + return ST_DELETE; +} + +void +rb_free_transcoder_table(void) +{ + st_foreach(transcoder_table, free_transcode_i, 0); + st_free_table(transcoder_table); +} + static transcoder_entry_t * make_transcoder_entry(const char *sname, const char *dname) { diff --git a/variable.c b/variable.c index 4c2bc5c97e9c1f..de3a3561f86caa 100644 --- a/variable.c +++ b/variable.c @@ -438,6 +438,33 @@ struct rb_global_entry { bool ractor_local; }; +static enum rb_id_table_iterator_result +free_global_entry_i(ID key, VALUE val, void *arg) +{ + struct rb_global_entry *entry = (struct rb_global_entry *)val; + if (entry->var->counter == 1) { + ruby_xfree(entry->var); + } + else { + entry->var->counter--; + } + ruby_xfree(entry); + return ID_TABLE_DELETE; +} + +void +rb_free_rb_global_tbl(void) +{ + rb_id_table_foreach(rb_global_tbl, free_global_entry_i, 0); + rb_id_table_free(rb_global_tbl); +} + +void +rb_free_generic_iv_tbl_(void) +{ + st_free_table(generic_iv_tbl_); +} + static struct rb_global_entry* rb_find_global_entry(ID id) { diff --git a/variable.h b/variable.h index 12ada661082491..1abc89ed99f711 100644 --- a/variable.h +++ b/variable.h @@ -33,5 +33,7 @@ int rb_ivar_generic_ivtbl_lookup(VALUE obj, struct gen_ivtbl **); shape_id_t rb_generic_shape_id(VALUE obj); #endif +void rb_free_rb_global_tbl(void); +void rb_free_generic_iv_tbl_(void); #endif /* RUBY_TOPLEVEL_VARIABLE_H */ diff --git a/vm.c b/vm.c index 9abb3ba9f91fc5..294ee389aaa81d 100644 --- a/vm.c +++ b/vm.c @@ -16,6 +16,7 @@ #include "internal/compile.h" #include "internal/cont.h" #include "internal/error.h" +#include "internal/encoding.h" #include "internal/eval.h" #include "internal/gc.h" #include "internal/inits.h" @@ -25,6 +26,7 @@ #include "internal/ruby_parser.h" #include "internal/symbol.h" #include "internal/thread.h" +#include "internal/transcode.h" #include "internal/vm.h" #include "internal/sanitizers.h" #include "internal/variable.h" @@ -2055,6 +2057,13 @@ short ruby_vm_redefined_flag[BOP_LAST_]; static st_table *vm_opt_method_def_table = 0; static st_table *vm_opt_mid_table = 0; +void +rb_free_vm_opt_tables(void) +{ + st_free_table(vm_opt_method_def_table); + st_free_table(vm_opt_mid_table); +} + static int vm_redefinition_check_flag(VALUE klass) { @@ -2973,6 +2982,9 @@ free_loading_table_entry(st_data_t key, st_data_t value, st_data_t arg) return ST_DELETE; } +void rb_free_loaded_features_index(rb_vm_t *vm); +void rb_objspace_free_objects(void *objspace); + int ruby_vm_destruct(rb_vm_t *vm) { @@ -2980,13 +2992,57 @@ ruby_vm_destruct(rb_vm_t *vm) if (vm) { rb_thread_t *th = vm->ractor.main_thread; - struct rb_objspace *objspace = vm->objspace; - vm->ractor.main_thread = NULL; + VALUE *stack = th->ec->vm_stack; + if (rb_free_on_exit) { + rb_free_default_rand_key(); + rb_free_encoded_insn_data(); + rb_free_global_enc_table(); + rb_free_loaded_builtin_table(); + + rb_free_shared_fiber_pool(); + rb_free_static_symid_str(); + rb_free_transcoder_table(); + rb_free_vm_opt_tables(); + rb_free_warning(); + rb_free_rb_global_tbl(); + rb_free_loaded_features_index(vm); + rb_ractor_t *r = vm->ractor.main_ractor; + xfree(r->sync.recv_queue.baskets); + xfree(r->sync.takers_queue.baskets); + + rb_id_table_free(vm->negative_cme_table); + st_free_table(vm->overloaded_cme_table); + + rb_id_table_free(RCLASS(rb_mRubyVMFrozenCore)->m_tbl); + + rb_shape_t *cursor = rb_shape_get_root_shape(); + rb_shape_t *end = rb_shape_get_shape_by_id(GET_SHAPE_TREE()->next_shape_id); + while (cursor < end) { + // 0x1 == SINGLE_CHILD_P + if (cursor->edges && !(((uintptr_t)cursor->edges) & 0x1)) + rb_id_table_free(cursor->edges); + cursor += 1; + } + + xfree(GET_SHAPE_TREE()); + + st_free_table(vm->static_ext_inits); + st_free_table(vm->ensure_rollback_table); + + ruby_xfree(vm->postponed_job_buffer); + st_free_table(vm->defined_module_hash); - if (th) { - rb_fiber_reset_root_local_storage(th); - thread_free(th); + rb_id_table_free(vm->constant_cache); } + else { + if (th) { + rb_fiber_reset_root_local_storage(th); + thread_free(th); + } + } + + struct rb_objspace *objspace = vm->objspace; + rb_vm_living_threads_init(vm); ruby_vm_run_at_exit_hooks(vm); if (vm->loading_table) { @@ -3000,6 +3056,15 @@ ruby_vm_destruct(rb_vm_t *vm) } RB_ALTSTACK_FREE(vm->main_altstack); if (objspace) { + if (rb_free_on_exit) { + rb_objspace_free_objects(objspace); + rb_free_generic_iv_tbl_(); + if (th) { + xfree(th->ractor); + xfree(stack); + ruby_mimfree(th); + } + } rb_objspace_free(objspace); } rb_native_mutex_destroy(&vm->workqueue_lock); @@ -4198,6 +4263,8 @@ rb_ruby_debug_ptr(void) return &cr->debug; } +bool rb_free_on_exit = false; + /* iseq.c */ VALUE rb_insn_operand_intern(const rb_iseq_t *iseq, VALUE insn, int op_no, VALUE op,