From ec114e5701839ac5bb8a97f901333b865ff96c8a Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Wed, 6 May 2026 12:40:32 +0100 Subject: [PATCH 01/12] Use monotonic add/sub counters for malloc_increase Replace the single objspace->malloc_counters.{increase,oldmalloc_increase} size_t fields with pairs of monotonically-increasing counters. Snapshots of these counters are taken at each GC, so that the live malloc_increase is computed as (malloc - malloc_at_last_gc) - (free - free_at_last_gc) We update the baselines at each GC. Minor GC's update malloc and free associated with young objects only (counters). Major GC's update based on "oldcounters" as well. The malloc/free counters are 64 bits wide which should provide ample headroom for real world programs (>500 years at 1Gb/sec allocation rate XD). We use size_t on 64-bit and uint64_t on 32-bit, wrapped by a gc_counter_t struct. However, because updating a uint64_t is a multi-instruction operation on 32 bit architectures we have to introduce a lock to the malloc_counters struct to avoid racing. We introduced 2 new macros MALLOC_COUNTERS_LOCK and MALLOC_COUNTERS_UNLOCK that use `rb_nativethread_lock_t`. The lock is initialized in rb_gc_impl_objspace_init and destroyed in rb_gc_impl_objspace_free. We chose this because it mirrors existing finalizer_lock pattern in wbcheck. On 64 bit platforms aligned 64 bit loads are atomic, and writes are already using RUBY_ATOMIC_SIZE_ADD so the locks are not needed and the macros do nothing. --- gc/default/default.c | 147 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 125 insertions(+), 22 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 9791236002f5bd..f6f234b38d6c1c 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -495,11 +495,29 @@ enum gc_mode { gc_mode_compacting, }; +#if SIZEOF_SIZE_T >= 8 +typedef size_t gc_counter_t; +#else +typedef uint64_t gc_counter_t; +#endif + +struct gc_malloc_bytes { + gc_counter_t malloc; + gc_counter_t free; + + /* Snapshots of `malloc` / `free` taken at the end of the last GC */ + gc_counter_t malloc_at_last_gc; + gc_counter_t free_at_last_gc; +}; + typedef struct rb_objspace { struct { - size_t increase; + struct gc_malloc_bytes counters; #if RGENGC_ESTIMATE_OLDMALLOC - size_t oldmalloc_increase; + struct gc_malloc_bytes oldcounters; +#endif +#if SIZEOF_SIZE_T < 8 + rb_nativethread_lock_t lock; #endif } malloc_counters; @@ -943,8 +961,69 @@ RVALUE_AGE_SET(VALUE obj, int age) } #define malloc_limit objspace->malloc_params.limit -#define malloc_increase objspace->malloc_counters.increase +#define malloc_increase gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.counters) #define malloc_allocated_size objspace->malloc_params.allocated_size + +/* We don't need a lock if the system is 64-bit, we can do native 64-bit operations in a single instruction */ +#if SIZEOF_SIZE_T >= 8 +# define MALLOC_COUNTERS_LOCK(o) ((void)0) +# define MALLOC_COUNTERS_UNLOCK(o) ((void)0) +# define MALLOC_COUNTER_ADD(field, delta) \ + RUBY_ATOMIC_SIZE_ADD((field), (size_t)(delta)) +#else +# define MALLOC_COUNTERS_LOCK(o) rb_native_mutex_lock(&(o)->malloc_counters.lock) +# define MALLOC_COUNTERS_UNLOCK(o) rb_native_mutex_unlock(&(o)->malloc_counters.lock) +/* Caller must hold MALLOC_COUNTERS_LOCK. */ +# define MALLOC_COUNTER_ADD(field, delta) ((field) += (gc_counter_t)(delta)) +#endif + +static inline int64_t +gc_malloc_counters_increase(rb_objspace_t *objspace, const struct gc_malloc_bytes *c) +{ + MALLOC_COUNTERS_LOCK(objspace); + gc_counter_t malloc_delta = c->malloc - c->malloc_at_last_gc; + gc_counter_t free_delta = c->free - c->free_at_last_gc; + MALLOC_COUNTERS_UNLOCK(objspace); + + if (malloc_delta >= free_delta) { + return (int64_t)(malloc_delta - free_delta); + } + else { + return -(int64_t)(free_delta - malloc_delta); + } +} + +static inline size_t +gc_malloc_counters_increase_unsigned(rb_objspace_t *objspace, const struct gc_malloc_bytes *c) +{ + int64_t inc = gc_malloc_counters_increase(objspace, c); + if (inc <= 0) return 0; +#if SIZEOF_SIZE_T < 8 + if ((uint64_t)inc > SIZE_MAX) return SIZE_MAX; +#endif + return (size_t)inc; +} + +static inline int64_t +gc_malloc_counters_snapshot(rb_objspace_t *objspace, struct gc_malloc_bytes *c) +{ + MALLOC_COUNTERS_LOCK(objspace); + gc_counter_t malloc_now = c->malloc; + gc_counter_t free_now = c->free; + gc_counter_t malloc_delta = malloc_now - c->malloc_at_last_gc; + gc_counter_t free_delta = free_now - c->free_at_last_gc; + c->malloc_at_last_gc = malloc_now; + c->free_at_last_gc = free_now; + MALLOC_COUNTERS_UNLOCK(objspace); + + if (malloc_delta >= free_delta) { + return (int64_t)(malloc_delta - free_delta); + } + else { + return -(int64_t)(free_delta - malloc_delta); + } +} + #define heap_pages_lomem objspace->heap_pages.range[0] #define heap_pages_himem objspace->heap_pages.range[1] #define heap_pages_freeable_pages objspace->heap_pages.freeable_pages @@ -4924,10 +5003,12 @@ gc_check_after_marks_i(st_data_t k, st_data_t v, st_data_t ptr) static void gc_marks_check(rb_objspace_t *objspace, st_foreach_callback_func *checker_func, const char *checker_name) { - size_t saved_malloc_increase = objspace->malloc_params.increase; + MALLOC_COUNTERS_LOCK(objspace); + struct gc_malloc_bytes saved_malloc = objspace->malloc_counters.counters; #if RGENGC_ESTIMATE_OLDMALLOC - size_t saved_oldmalloc_increase = objspace->malloc_counters.oldmalloc_increase; + struct gc_malloc_bytes saved_oldmalloc = objspace->malloc_counters.oldcounters; #endif + MALLOC_COUNTERS_UNLOCK(objspace); VALUE already_disabled = rb_objspace_gc_disable(objspace); objspace->rgengc.allrefs_table = objspace_allrefs(objspace); @@ -4947,10 +5028,12 @@ gc_marks_check(rb_objspace_t *objspace, st_foreach_callback_func *checker_func, objspace->rgengc.allrefs_table = 0; if (already_disabled == Qfalse) rb_objspace_gc_enable(objspace); - objspace->malloc_params.increase = saved_malloc_increase; + MALLOC_COUNTERS_LOCK(objspace); + objspace->malloc_counters.counters = saved_malloc; #if RGENGC_ESTIMATE_OLDMALLOC - objspace->malloc_counters.oldmalloc_increase = saved_oldmalloc_increase; + objspace->malloc_counters.oldcounters = saved_oldmalloc; #endif + MALLOC_COUNTERS_UNLOCK(objspace); } #endif /* RGENGC_CHECK_MODE >= 4 */ @@ -6310,11 +6393,14 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark) { gc_prof_set_malloc_info(objspace); { - size_t inc = RUBY_ATOMIC_SIZE_EXCHANGE(malloc_increase, 0); + int64_t inc = gc_malloc_counters_snapshot(objspace, &objspace->malloc_counters.counters); size_t old_limit = malloc_limit; - if (inc > malloc_limit) { - malloc_limit = (size_t)(inc * gc_params.malloc_limit_growth_factor); + /* A net-negative `inc` (more freed than malloc'd since last GC) is + * treated the same as "allocated less than malloc_limit". + * This matches what we were doing pre-monotonic counters, but is it right? */ + if (inc > 0 && (size_t)inc > malloc_limit) { + malloc_limit = (size_t)((size_t)inc * gc_params.malloc_limit_growth_factor); if (malloc_limit > gc_params.malloc_limit_max) { malloc_limit = gc_params.malloc_limit_max; } @@ -6341,7 +6427,11 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark) /* reset oldmalloc info */ #if RGENGC_ESTIMATE_OLDMALLOC if (!full_mark) { - if (objspace->malloc_counters.oldmalloc_increase > objspace->rgengc.oldmalloc_increase_limit) { + /* Don't snapshot on minor GC: oldmalloc_increase is meant to + * accumulate across minor GCs and only reset at major GC. */ + int64_t oldmalloc_increase = gc_malloc_counters_increase(objspace, &objspace->malloc_counters.oldcounters); + if (oldmalloc_increase > 0 && + (uint64_t)oldmalloc_increase > objspace->rgengc.oldmalloc_increase_limit) { gc_needs_major_flags |= GPR_FLAG_MAJOR_BY_OLDMALLOC; objspace->rgengc.oldmalloc_increase_limit = (size_t)(objspace->rgengc.oldmalloc_increase_limit * gc_params.oldmalloc_limit_growth_factor); @@ -6351,16 +6441,16 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark) } } - if (0) fprintf(stderr, "%"PRIdSIZE"\t%d\t%"PRIuSIZE"\t%"PRIuSIZE"\t%"PRIdSIZE"\n", + if (0) fprintf(stderr, "%"PRIdSIZE"\t%d\t%"PRId64"\t%"PRIuSIZE"\t%"PRIdSIZE"\n", rb_gc_count(), gc_needs_major_flags, - objspace->malloc_counters.oldmalloc_increase, + oldmalloc_increase, objspace->rgengc.oldmalloc_increase_limit, gc_params.oldmalloc_limit_max); } else { - /* major GC */ - objspace->malloc_counters.oldmalloc_increase = 0; + /* major GC: re-baseline the oldmalloc counter */ + (void)gc_malloc_counters_snapshot(objspace, &objspace->malloc_counters.oldcounters); if ((objspace->profile.latest_gc_info & GPR_FLAG_MAJOR_BY_OLDMALLOC) == 0) { objspace->rgengc.oldmalloc_increase_limit = @@ -7591,7 +7681,7 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(total_freed_pages, objspace->heap_pages.freed_pages); SET(total_allocated_objects, total_allocated_objects(objspace)); SET(total_freed_objects, total_freed_objects(objspace)); - SET(malloc_increase_bytes, malloc_increase); + SET(malloc_increase_bytes, gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.counters)); SET(malloc_increase_bytes_limit, malloc_limit); SET(minor_gc_count, objspace->profile.minor_gc_count); SET(major_gc_count, objspace->profile.major_gc_count); @@ -7603,7 +7693,7 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(old_objects, objspace->rgengc.old_objects); SET(old_objects_limit, objspace->rgengc.old_objects_limit); #if RGENGC_ESTIMATE_OLDMALLOC - SET(oldmalloc_increase_bytes, objspace->malloc_counters.oldmalloc_increase); + SET(oldmalloc_increase_bytes, gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.oldcounters)); SET(oldmalloc_increase_bytes_limit, objspace->rgengc.oldmalloc_increase_limit); #endif @@ -8044,16 +8134,22 @@ static void malloc_increase_commit(rb_objspace_t *objspace, size_t new_size, size_t old_size) { if (new_size > old_size) { - RUBY_ATOMIC_SIZE_ADD(malloc_increase, new_size - old_size); + size_t delta = new_size - old_size; + MALLOC_COUNTERS_LOCK(objspace); + MALLOC_COUNTER_ADD(objspace->malloc_counters.counters.malloc, delta); #if RGENGC_ESTIMATE_OLDMALLOC - RUBY_ATOMIC_SIZE_ADD(objspace->malloc_counters.oldmalloc_increase, new_size - old_size); + MALLOC_COUNTER_ADD(objspace->malloc_counters.oldcounters.malloc, delta); #endif + MALLOC_COUNTERS_UNLOCK(objspace); } - else { - atomic_sub_nounderflow(&malloc_increase, old_size - new_size); + else if (old_size > new_size) { + size_t delta = old_size - new_size; + MALLOC_COUNTERS_LOCK(objspace); + MALLOC_COUNTER_ADD(objspace->malloc_counters.counters.free, delta); #if RGENGC_ESTIMATE_OLDMALLOC - atomic_sub_nounderflow(&objspace->malloc_counters.oldmalloc_increase, old_size - new_size); + MALLOC_COUNTER_ADD(objspace->malloc_counters.oldcounters.free, delta); #endif + MALLOC_COUNTERS_UNLOCK(objspace); } } @@ -9390,6 +9486,10 @@ rb_gc_impl_objspace_free(void *objspace_ptr) rb_darray_free_without_gc(objspace->weak_references); +#if SIZEOF_SIZE_T < 8 + rb_native_mutex_destroy(&objspace->malloc_counters.lock); +#endif + free(objspace); } @@ -9520,6 +9620,9 @@ rb_gc_impl_objspace_init(void *objspace_ptr) objspace->flags.measure_gc = true; malloc_limit = gc_params.malloc_limit_min; +#if SIZEOF_SIZE_T < 8 + rb_native_mutex_initialize(&objspace->malloc_counters.lock); +#endif objspace->finalize_deferred_pjob = rb_postponed_job_preregister(0, gc_finalize_deferred, objspace); if (objspace->finalize_deferred_pjob == POSTPONED_JOB_HANDLE_INVALID) { rb_bug("Could not preregister postponed job for GC"); From 672ef4326c7e33bff0551a1fece8cb814c034709 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Fri, 8 May 2026 22:39:43 +0100 Subject: [PATCH 02/12] Expose monotonic malloc/free totals via GC.stat --- gc/default/default.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/gc/default/default.c b/gc/default/default.c index f6f234b38d6c1c..08d976a4e8f7b7 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7550,6 +7550,8 @@ enum gc_stat_sym { gc_stat_sym_total_freed_pages, gc_stat_sym_total_allocated_objects, gc_stat_sym_total_freed_objects, + gc_stat_sym_total_malloc_bytes, + gc_stat_sym_total_free_bytes, gc_stat_sym_malloc_increase_bytes, gc_stat_sym_malloc_increase_bytes_limit, gc_stat_sym_minor_gc_count, @@ -7600,6 +7602,8 @@ setup_gc_stat_symbols(void) S(total_freed_pages); S(total_allocated_objects); S(total_freed_objects); + S(total_malloc_bytes); + S(total_free_bytes); S(malloc_increase_bytes); S(malloc_increase_bytes_limit); S(minor_gc_count); @@ -7661,6 +7665,11 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) return SIZET2NUM(attr); \ else if (hash != Qnil) \ rb_hash_aset(hash, gc_stat_symbols[gc_stat_sym_##name], SIZET2NUM(attr)); +#define SET64(name, attr) \ + if (key == gc_stat_symbols[gc_stat_sym_##name]) \ + return ULL2NUM(attr); \ + else if (hash != Qnil) \ + rb_hash_aset(hash, gc_stat_symbols[gc_stat_sym_##name], ULL2NUM(attr)); SET(count, objspace->profile.count); SET(time, (size_t)ns_to_ms(objspace->profile.marking_time_ns + objspace->profile.sweeping_time_ns)); // TODO: UINT64T2NUM @@ -7681,6 +7690,16 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(total_freed_pages, objspace->heap_pages.freed_pages); SET(total_allocated_objects, total_allocated_objects(objspace)); SET(total_freed_objects, total_freed_objects(objspace)); + { + /* Monotonic totals; snapshot the pair under one lock so they're + * consistent with each other on 32-bit. */ + MALLOC_COUNTERS_LOCK(objspace); + uint64_t total_malloc = objspace->malloc_counters.counters.malloc; + uint64_t total_free = objspace->malloc_counters.counters.free; + MALLOC_COUNTERS_UNLOCK(objspace); + SET64(total_malloc_bytes, total_malloc); + SET64(total_free_bytes, total_free); + } SET(malloc_increase_bytes, gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.counters)); SET(malloc_increase_bytes_limit, malloc_limit); SET(minor_gc_count, objspace->profile.minor_gc_count); @@ -7706,6 +7725,7 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(total_remembered_shady_object_count, objspace->profile.total_remembered_shady_object_count); #endif /* RGENGC_PROFILE */ #undef SET +#undef SET64 if (!NIL_P(key)) { // Matched key should return above From 8f7e07bb22e0fe57e089e7342e4b39f607330ccd Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Mon, 11 May 2026 15:30:30 +0100 Subject: [PATCH 03/12] Reorder rb_gc_impl_stat to keep heap_live_slots accurate Several SETs in rb_gc_impl_stat may allocate a T_BIGNUM RVALUE when the value exceeds FIXNUM_MAX This is invisible on LP64 but trips on LLP64 Windows and ILP32 Linux where FIXNUM_MAX ~= 1.07GB. If those allocations happen *after* setting heap_live_slots then stat[:heap_live_slots] reflects a stale snapshot, and tests that assert on it fail. This commit reorders everything so every potentially-allocating SET runs first, and the slot counters are SET last. --- gc/default/default.c | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 08d976a4e8f7b7..c33edf75ddd111 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7676,20 +7676,6 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(marking_time, (size_t)ns_to_ms(objspace->profile.marking_time_ns)); SET(sweeping_time, (size_t)ns_to_ms(objspace->profile.sweeping_time_ns)); - /* implementation dependent counters */ - SET(heap_allocated_pages, rb_darray_size(objspace->heap_pages.sorted)); - SET(heap_empty_pages, objspace->empty_pages_count) - SET(heap_allocatable_bytes, objspace->heap_pages.allocatable_bytes); - SET(heap_available_slots, objspace_available_slots(objspace)); - SET(heap_live_slots, objspace_live_slots(objspace)); - SET(heap_free_slots, objspace_free_slots(objspace)); - SET(heap_final_slots, total_final_slots_count(objspace)); - SET(heap_marked_slots, objspace->marked_slots); - SET(heap_eden_pages, heap_eden_total_pages(objspace)); - SET(total_allocated_pages, objspace->heap_pages.allocated_pages); - SET(total_freed_pages, objspace->heap_pages.freed_pages); - SET(total_allocated_objects, total_allocated_objects(objspace)); - SET(total_freed_objects, total_freed_objects(objspace)); { /* Monotonic totals; snapshot the pair under one lock so they're * consistent with each other on 32-bit. */ @@ -7700,6 +7686,18 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET64(total_malloc_bytes, total_malloc); SET64(total_free_bytes, total_free); } + + /* implementation dependent counters (small / fixnum-safe) */ + SET(heap_allocated_pages, rb_darray_size(objspace->heap_pages.sorted)); + SET(heap_empty_pages, objspace->empty_pages_count) + SET(heap_allocatable_bytes, objspace->heap_pages.allocatable_bytes); + SET(heap_eden_pages, heap_eden_total_pages(objspace)); + SET(total_allocated_pages, objspace->heap_pages.allocated_pages); + SET(total_freed_pages, objspace->heap_pages.freed_pages); + /* These two may allocate Bignums on small-FIXNUM_MAX platforms — keep + * them above the slot snapshot below. */ + SET(total_allocated_objects, total_allocated_objects(objspace)); + SET(total_freed_objects, total_freed_objects(objspace)); SET(malloc_increase_bytes, gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.counters)); SET(malloc_increase_bytes_limit, malloc_limit); SET(minor_gc_count, objspace->profile.minor_gc_count); @@ -7716,6 +7714,12 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(oldmalloc_increase_bytes_limit, objspace->rgengc.oldmalloc_increase_limit); #endif + SET(heap_available_slots, objspace_available_slots(objspace)); + SET(heap_live_slots, objspace_live_slots(objspace)); + SET(heap_free_slots, objspace_free_slots(objspace)); + SET(heap_final_slots, total_final_slots_count(objspace)); + SET(heap_marked_slots, objspace->marked_slots); + #if RGENGC_PROFILE SET(total_generated_normal_object_count, objspace->profile.total_generated_normal_object_count); SET(total_generated_shady_object_count, objspace->profile.total_generated_shady_object_count); From cb3b126eee405b57bb47c64d39b0ba287d41287d Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 14 May 2026 11:12:12 +0100 Subject: [PATCH 04/12] Better feature detection for malloc counter locks --- gc/default/default.c | 135 ++++++++++++++++++++++++++++++++----------- ruby_atomic.h | 40 +++++++++++++ 2 files changed, 141 insertions(+), 34 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index c33edf75ddd111..2ddea7ab2f27b0 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -23,6 +23,7 @@ #include "ruby/ruby.h" #include "ruby/atomic.h" +#include "ruby_atomic.h" #include "ruby/debug.h" #include "ruby/thread.h" #include "ruby/util.h" @@ -498,7 +499,16 @@ enum gc_mode { #if SIZEOF_SIZE_T >= 8 typedef size_t gc_counter_t; #else -typedef uint64_t gc_counter_t; +typedef RBIMPL_ALIGNAS(8) uint64_t gc_counter_t; +#endif + +#if SIZEOF_SIZE_T >= 8 +# define MALLOC_COUNTERS_ATOMIC_NATIVE 1 +#elif defined(HAVE_GCC_ATOMIC_BUILTINS_64) || defined(_WIN32) || \ + (defined(__sun) && defined(HAVE_ATOMIC_H) && (defined(_LP64) || defined(_I32LPx))) +# define MALLOC_COUNTERS_ATOMIC_U64 1 +#else +# define MALLOC_COUNTERS_NEED_LOCK 1 #endif struct gc_malloc_bytes { @@ -516,7 +526,7 @@ typedef struct rb_objspace { #if RGENGC_ESTIMATE_OLDMALLOC struct gc_malloc_bytes oldcounters; #endif -#if SIZEOF_SIZE_T < 8 +#ifdef MALLOC_COUNTERS_NEED_LOCK rb_nativethread_lock_t lock; #endif } malloc_counters; @@ -964,27 +974,69 @@ RVALUE_AGE_SET(VALUE obj, int age) #define malloc_increase gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.counters) #define malloc_allocated_size objspace->malloc_params.allocated_size -/* We don't need a lock if the system is 64-bit, we can do native 64-bit operations in a single instruction */ -#if SIZEOF_SIZE_T >= 8 +#ifdef MALLOC_COUNTERS_NEED_LOCK +# define MALLOC_COUNTERS_LOCK(o) rb_native_mutex_lock(&(o)->malloc_counters.lock) +# define MALLOC_COUNTERS_UNLOCK(o) rb_native_mutex_unlock(&(o)->malloc_counters.lock) +#else # define MALLOC_COUNTERS_LOCK(o) ((void)0) # define MALLOC_COUNTERS_UNLOCK(o) ((void)0) -# define MALLOC_COUNTER_ADD(field, delta) \ - RUBY_ATOMIC_SIZE_ADD((field), (size_t)(delta)) +#endif + +static inline void +gc_counter_add(gc_counter_t *p, size_t delta) +{ +#if defined(MALLOC_COUNTERS_ATOMIC_NATIVE) + RUBY_ATOMIC_SIZE_ADD(*p, delta); +#elif defined(MALLOC_COUNTERS_ATOMIC_U64) + rbimpl_atomic_u64_fetch_add_relaxed((volatile rbimpl_atomic_uint64_t *)p, (uint64_t)delta); #else -# define MALLOC_COUNTERS_LOCK(o) rb_native_mutex_lock(&(o)->malloc_counters.lock) -# define MALLOC_COUNTERS_UNLOCK(o) rb_native_mutex_unlock(&(o)->malloc_counters.lock) -/* Caller must hold MALLOC_COUNTERS_LOCK. */ -# define MALLOC_COUNTER_ADD(field, delta) ((field) += (gc_counter_t)(delta)) + *p += (gc_counter_t)delta; +#endif +} + +static inline gc_counter_t +gc_counter_load_relaxed(const gc_counter_t *p) +{ +#if defined(MALLOC_COUNTERS_ATOMIC_U64) + return (gc_counter_t)rbimpl_atomic_u64_load_relaxed((const volatile rbimpl_atomic_uint64_t *)p); +#else + return *p; #endif +} + +static inline gc_counter_t +gc_counter_load_acquire(const gc_counter_t *p) +{ +#if defined(MALLOC_COUNTERS_ATOMIC_U64) + return (gc_counter_t)rbimpl_atomic_u64_load_acquire((const volatile rbimpl_atomic_uint64_t *)p); +#else + return *p; +#endif +} + +static inline void +gc_counter_store_release(gc_counter_t *p, gc_counter_t v) +{ +#if defined(MALLOC_COUNTERS_ATOMIC_U64) + rbimpl_atomic_u64_set_release((volatile rbimpl_atomic_uint64_t *)p, (uint64_t)v); +#else + *p = v; +#endif +} static inline int64_t gc_malloc_counters_increase(rb_objspace_t *objspace, const struct gc_malloc_bytes *c) { MALLOC_COUNTERS_LOCK(objspace); - gc_counter_t malloc_delta = c->malloc - c->malloc_at_last_gc; - gc_counter_t free_delta = c->free - c->free_at_last_gc; + gc_counter_t malloc_at = gc_counter_load_acquire(&c->malloc_at_last_gc); + gc_counter_t free_at = gc_counter_load_acquire(&c->free_at_last_gc); + gc_counter_t malloc_now = gc_counter_load_relaxed(&c->malloc); + gc_counter_t free_now = gc_counter_load_relaxed(&c->free); MALLOC_COUNTERS_UNLOCK(objspace); + gc_counter_t malloc_delta = malloc_now - malloc_at; + gc_counter_t free_delta = free_now - free_at; + if (malloc_delta >= free_delta) { return (int64_t)(malloc_delta - free_delta); } @@ -1008,14 +1060,17 @@ static inline int64_t gc_malloc_counters_snapshot(rb_objspace_t *objspace, struct gc_malloc_bytes *c) { MALLOC_COUNTERS_LOCK(objspace); - gc_counter_t malloc_now = c->malloc; - gc_counter_t free_now = c->free; - gc_counter_t malloc_delta = malloc_now - c->malloc_at_last_gc; - gc_counter_t free_delta = free_now - c->free_at_last_gc; - c->malloc_at_last_gc = malloc_now; - c->free_at_last_gc = free_now; + gc_counter_t malloc_now = gc_counter_load_relaxed(&c->malloc); + gc_counter_t free_now = gc_counter_load_relaxed(&c->free); + gc_counter_t malloc_at = gc_counter_load_relaxed(&c->malloc_at_last_gc); + gc_counter_t free_at = gc_counter_load_relaxed(&c->free_at_last_gc); + gc_counter_store_release(&c->malloc_at_last_gc, malloc_now); + gc_counter_store_release(&c->free_at_last_gc, free_now); MALLOC_COUNTERS_UNLOCK(objspace); + gc_counter_t malloc_delta = malloc_now - malloc_at; + gc_counter_t free_delta = free_now - free_at; + if (malloc_delta >= free_delta) { return (int64_t)(malloc_delta - free_delta); } @@ -5004,9 +5059,19 @@ static void gc_marks_check(rb_objspace_t *objspace, st_foreach_callback_func *checker_func, const char *checker_name) { MALLOC_COUNTERS_LOCK(objspace); - struct gc_malloc_bytes saved_malloc = objspace->malloc_counters.counters; + struct gc_malloc_bytes saved_malloc = { + .malloc = gc_counter_load_relaxed(&objspace->malloc_counters.counters.malloc), + .free = gc_counter_load_relaxed(&objspace->malloc_counters.counters.free), + .malloc_at_last_gc = gc_counter_load_relaxed(&objspace->malloc_counters.counters.malloc_at_last_gc), + .free_at_last_gc = gc_counter_load_relaxed(&objspace->malloc_counters.counters.free_at_last_gc), + }; #if RGENGC_ESTIMATE_OLDMALLOC - struct gc_malloc_bytes saved_oldmalloc = objspace->malloc_counters.oldcounters; + struct gc_malloc_bytes saved_oldmalloc = { + .malloc = gc_counter_load_relaxed(&objspace->malloc_counters.oldcounters.malloc), + .free = gc_counter_load_relaxed(&objspace->malloc_counters.oldcounters.free), + .malloc_at_last_gc = gc_counter_load_relaxed(&objspace->malloc_counters.oldcounters.malloc_at_last_gc), + .free_at_last_gc = gc_counter_load_relaxed(&objspace->malloc_counters.oldcounters.free_at_last_gc), + }; #endif MALLOC_COUNTERS_UNLOCK(objspace); VALUE already_disabled = rb_objspace_gc_disable(objspace); @@ -5029,9 +5094,15 @@ gc_marks_check(rb_objspace_t *objspace, st_foreach_callback_func *checker_func, if (already_disabled == Qfalse) rb_objspace_gc_enable(objspace); MALLOC_COUNTERS_LOCK(objspace); - objspace->malloc_counters.counters = saved_malloc; + gc_counter_store_release(&objspace->malloc_counters.counters.malloc, saved_malloc.malloc); + gc_counter_store_release(&objspace->malloc_counters.counters.free, saved_malloc.free); + gc_counter_store_release(&objspace->malloc_counters.counters.malloc_at_last_gc, saved_malloc.malloc_at_last_gc); + gc_counter_store_release(&objspace->malloc_counters.counters.free_at_last_gc, saved_malloc.free_at_last_gc); #if RGENGC_ESTIMATE_OLDMALLOC - objspace->malloc_counters.oldcounters = saved_oldmalloc; + gc_counter_store_release(&objspace->malloc_counters.oldcounters.malloc, saved_oldmalloc.malloc); + gc_counter_store_release(&objspace->malloc_counters.oldcounters.free, saved_oldmalloc.free); + gc_counter_store_release(&objspace->malloc_counters.oldcounters.malloc_at_last_gc, saved_oldmalloc.malloc_at_last_gc); + gc_counter_store_release(&objspace->malloc_counters.oldcounters.free_at_last_gc, saved_oldmalloc.free_at_last_gc); #endif MALLOC_COUNTERS_UNLOCK(objspace); } @@ -7677,12 +7748,8 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(sweeping_time, (size_t)ns_to_ms(objspace->profile.sweeping_time_ns)); { - /* Monotonic totals; snapshot the pair under one lock so they're - * consistent with each other on 32-bit. */ - MALLOC_COUNTERS_LOCK(objspace); - uint64_t total_malloc = objspace->malloc_counters.counters.malloc; - uint64_t total_free = objspace->malloc_counters.counters.free; - MALLOC_COUNTERS_UNLOCK(objspace); + uint64_t total_malloc = (uint64_t)gc_counter_load_relaxed(&objspace->malloc_counters.counters.malloc); + uint64_t total_free = (uint64_t)gc_counter_load_relaxed(&objspace->malloc_counters.counters.free); SET64(total_malloc_bytes, total_malloc); SET64(total_free_bytes, total_free); } @@ -8160,18 +8227,18 @@ malloc_increase_commit(rb_objspace_t *objspace, size_t new_size, size_t old_size if (new_size > old_size) { size_t delta = new_size - old_size; MALLOC_COUNTERS_LOCK(objspace); - MALLOC_COUNTER_ADD(objspace->malloc_counters.counters.malloc, delta); + gc_counter_add(&objspace->malloc_counters.counters.malloc, delta); #if RGENGC_ESTIMATE_OLDMALLOC - MALLOC_COUNTER_ADD(objspace->malloc_counters.oldcounters.malloc, delta); + gc_counter_add(&objspace->malloc_counters.oldcounters.malloc, delta); #endif MALLOC_COUNTERS_UNLOCK(objspace); } else if (old_size > new_size) { size_t delta = old_size - new_size; MALLOC_COUNTERS_LOCK(objspace); - MALLOC_COUNTER_ADD(objspace->malloc_counters.counters.free, delta); + gc_counter_add(&objspace->malloc_counters.counters.free, delta); #if RGENGC_ESTIMATE_OLDMALLOC - MALLOC_COUNTER_ADD(objspace->malloc_counters.oldcounters.free, delta); + gc_counter_add(&objspace->malloc_counters.oldcounters.free, delta); #endif MALLOC_COUNTERS_UNLOCK(objspace); } @@ -9510,7 +9577,7 @@ rb_gc_impl_objspace_free(void *objspace_ptr) rb_darray_free_without_gc(objspace->weak_references); -#if SIZEOF_SIZE_T < 8 +#ifdef MALLOC_COUNTERS_NEED_LOCK rb_native_mutex_destroy(&objspace->malloc_counters.lock); #endif @@ -9644,7 +9711,7 @@ rb_gc_impl_objspace_init(void *objspace_ptr) objspace->flags.measure_gc = true; malloc_limit = gc_params.malloc_limit_min; -#if SIZEOF_SIZE_T < 8 +#ifdef MALLOC_COUNTERS_NEED_LOCK rb_native_mutex_initialize(&objspace->malloc_counters.lock); #endif objspace->finalize_deferred_pjob = rb_postponed_job_preregister(0, gc_finalize_deferred, objspace); diff --git a/ruby_atomic.h b/ruby_atomic.h index cbcfe682ceddb9..409b9bcfd25c24 100644 --- a/ruby_atomic.h +++ b/ruby_atomic.h @@ -70,4 +70,44 @@ rbimpl_atomic_u64_set_relaxed(volatile rbimpl_atomic_uint64_t *address, uint64_t } #define ATOMIC_U64_SET_RELAXED(var, val) rbimpl_atomic_u64_set_relaxed(&(var), val) +static inline uint64_t +rbimpl_atomic_u64_fetch_add_relaxed(volatile rbimpl_atomic_uint64_t *value, uint64_t addend) +{ +#if defined(HAVE_GCC_ATOMIC_BUILTINS_64) + return __atomic_fetch_add(value, addend, __ATOMIC_RELAXED); +#elif defined(_WIN32) + return (uint64_t)InterlockedExchangeAdd64((LONG64 *)value, (LONG64)addend); +#elif defined(__sun) && defined(HAVE_ATOMIC_H) && (defined(_LP64) || defined(_I32LPx)) + return atomic_add_64_nv(value, addend) - addend; +#else + // TODO: stdatomic + uint64_t prev = *value; + *value = prev + addend; + return prev; +#endif +} +#define ATOMIC_U64_FETCH_ADD_RELAXED(var, val) rbimpl_atomic_u64_fetch_add_relaxed(&(var), val) + +static inline uint64_t +rbimpl_atomic_u64_load_acquire(const volatile rbimpl_atomic_uint64_t *value) +{ +#if defined(HAVE_GCC_ATOMIC_BUILTINS_64) + return __atomic_load_n(value, __ATOMIC_ACQUIRE); +#else + return rbimpl_atomic_u64_load_relaxed(value); +#endif +} +#define ATOMIC_U64_LOAD_ACQUIRE(var) rbimpl_atomic_u64_load_acquire(&(var)) + +static inline void +rbimpl_atomic_u64_set_release(volatile rbimpl_atomic_uint64_t *address, uint64_t value) +{ +#if defined(HAVE_GCC_ATOMIC_BUILTINS_64) + __atomic_store_n(address, value, __ATOMIC_RELEASE); +#else + rbimpl_atomic_u64_set_relaxed(address, value); +#endif +} +#define ATOMIC_U64_SET_RELEASE(var, val) rbimpl_atomic_u64_set_release(&(var), val) + #endif From ed882010ccf0979a3939af98e52f04f077858592 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 14 May 2026 12:13:11 +0100 Subject: [PATCH 05/12] Make sure we flush the cached count to update heap slots --- gc/default/default.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 2ddea7ab2f27b0..b816598268ab8d 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -7761,10 +7761,6 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(heap_eden_pages, heap_eden_total_pages(objspace)); SET(total_allocated_pages, objspace->heap_pages.allocated_pages); SET(total_freed_pages, objspace->heap_pages.freed_pages); - /* These two may allocate Bignums on small-FIXNUM_MAX platforms — keep - * them above the slot snapshot below. */ - SET(total_allocated_objects, total_allocated_objects(objspace)); - SET(total_freed_objects, total_freed_objects(objspace)); SET(malloc_increase_bytes, gc_malloc_counters_increase_unsigned(objspace, &objspace->malloc_counters.counters)); SET(malloc_increase_bytes_limit, malloc_limit); SET(minor_gc_count, objspace->profile.minor_gc_count); @@ -7781,6 +7777,9 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(oldmalloc_increase_bytes_limit, objspace->rgengc.oldmalloc_increase_limit); #endif + ractor_cache_flush_count(objspace, rb_gc_get_ractor_newobj_cache()); + SET(total_allocated_objects, total_allocated_objects(objspace)); + SET(total_freed_objects, total_freed_objects(objspace)); SET(heap_available_slots, objspace_available_slots(objspace)); SET(heap_live_slots, objspace_live_slots(objspace)); SET(heap_free_slots, objspace_free_slots(objspace)); From 8b7c4342afafdf9523c37ec8e48f192a7e8df289 Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Thu, 14 May 2026 23:54:52 +0100 Subject: [PATCH 06/12] Snapshot malloc counters at end of sweep Snapshotting at start of marking lets sweep-time frees count against the next epoch, which roughly halves GC frequency on alloc-heavy workloads. Move the snapshot to end of sweep so the next epoch starts from a clean baseline. --- gc/default/default.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index b816598268ab8d..36e81ec9d7a0d8 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -4031,6 +4031,13 @@ gc_sweep_finish(rb_objspace_t *objspace) } } + (void)gc_malloc_counters_snapshot(objspace, &objspace->malloc_counters.counters); +#if RGENGC_ESTIMATE_OLDMALLOC + if (objspace->profile.latest_gc_info & GPR_FLAG_MAJOR_MASK) { + (void)gc_malloc_counters_snapshot(objspace, &objspace->malloc_counters.oldcounters); + } +#endif + rb_gc_event_hook(0, RUBY_INTERNAL_EVENT_GC_END_SWEEP); gc_mode_transition(objspace, gc_mode_none); @@ -6464,7 +6471,7 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark) { gc_prof_set_malloc_info(objspace); { - int64_t inc = gc_malloc_counters_snapshot(objspace, &objspace->malloc_counters.counters); + int64_t inc = gc_malloc_counters_increase(objspace, &objspace->malloc_counters.counters); size_t old_limit = malloc_limit; /* A net-negative `inc` (more freed than malloc'd since last GC) is @@ -6520,9 +6527,6 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark) gc_params.oldmalloc_limit_max); } else { - /* major GC: re-baseline the oldmalloc counter */ - (void)gc_malloc_counters_snapshot(objspace, &objspace->malloc_counters.oldcounters); - if ((objspace->profile.latest_gc_info & GPR_FLAG_MAJOR_BY_OLDMALLOC) == 0) { objspace->rgengc.oldmalloc_increase_limit = (size_t)(objspace->rgengc.oldmalloc_increase_limit / ((gc_params.oldmalloc_limit_growth_factor - 1)/10 + 1)); From 4c8f07275dd39e693bf6dd932a32f7528b24d4dd Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Tue, 19 May 2026 11:37:16 +0100 Subject: [PATCH 07/12] Unify gc_counter_t on rbimpl_atomic_uint64_t --- gc/default/default.c | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 36e81ec9d7a0d8..291ff91b810002 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -496,18 +496,10 @@ enum gc_mode { gc_mode_compacting, }; -#if SIZEOF_SIZE_T >= 8 -typedef size_t gc_counter_t; -#else -typedef RBIMPL_ALIGNAS(8) uint64_t gc_counter_t; -#endif +typedef rbimpl_atomic_uint64_t gc_counter_t; -#if SIZEOF_SIZE_T >= 8 -# define MALLOC_COUNTERS_ATOMIC_NATIVE 1 -#elif defined(HAVE_GCC_ATOMIC_BUILTINS_64) || defined(_WIN32) || \ - (defined(__sun) && defined(HAVE_ATOMIC_H) && (defined(_LP64) || defined(_I32LPx))) -# define MALLOC_COUNTERS_ATOMIC_U64 1 -#else +#if !defined(HAVE_GCC_ATOMIC_BUILTINS_64) && !defined(_WIN32) && \ + !(defined(__sun) && defined(HAVE_ATOMIC_H) && (defined(_LP64) || defined(_I32LPx))) # define MALLOC_COUNTERS_NEED_LOCK 1 #endif @@ -985,42 +977,40 @@ RVALUE_AGE_SET(VALUE obj, int age) static inline void gc_counter_add(gc_counter_t *p, size_t delta) { -#if defined(MALLOC_COUNTERS_ATOMIC_NATIVE) - RUBY_ATOMIC_SIZE_ADD(*p, delta); -#elif defined(MALLOC_COUNTERS_ATOMIC_U64) - rbimpl_atomic_u64_fetch_add_relaxed((volatile rbimpl_atomic_uint64_t *)p, (uint64_t)delta); -#else +#ifdef MALLOC_COUNTERS_NEED_LOCK *p += (gc_counter_t)delta; +#else + rbimpl_atomic_u64_fetch_add_relaxed(p, (uint64_t)delta); #endif } static inline gc_counter_t gc_counter_load_relaxed(const gc_counter_t *p) { -#if defined(MALLOC_COUNTERS_ATOMIC_U64) - return (gc_counter_t)rbimpl_atomic_u64_load_relaxed((const volatile rbimpl_atomic_uint64_t *)p); -#else +#ifdef MALLOC_COUNTERS_NEED_LOCK return *p; +#else + return rbimpl_atomic_u64_load_relaxed(p); #endif } static inline gc_counter_t gc_counter_load_acquire(const gc_counter_t *p) { -#if defined(MALLOC_COUNTERS_ATOMIC_U64) - return (gc_counter_t)rbimpl_atomic_u64_load_acquire((const volatile rbimpl_atomic_uint64_t *)p); -#else +#ifdef MALLOC_COUNTERS_NEED_LOCK return *p; +#else + return rbimpl_atomic_u64_load_acquire(p); #endif } static inline void gc_counter_store_release(gc_counter_t *p, gc_counter_t v) { -#if defined(MALLOC_COUNTERS_ATOMIC_U64) - rbimpl_atomic_u64_set_release((volatile rbimpl_atomic_uint64_t *)p, (uint64_t)v); -#else +#ifdef MALLOC_COUNTERS_NEED_LOCK *p = v; +#else + rbimpl_atomic_u64_set_release(p, v); #endif } From 3002cea4db53313a810e9577bfa199d098d0d78d Mon Sep 17 00:00:00 2001 From: Matt Valentine-House Date: Wed, 20 May 2026 17:51:19 +0100 Subject: [PATCH 08/12] Fix GC Bug in imemo cdhash creation It's possible for st_init_existing_table_with_size to trigger GC. If that happens we need to ensure that the table entries count doesn't contain garbage data, or we'll try and mark random stuff --- imemo.c | 1 + 1 file changed, 1 insertion(+) diff --git a/imemo.c b/imemo.c index 4317fe0561ade2..3448a8dcd3c54f 100644 --- a/imemo.c +++ b/imemo.c @@ -129,6 +129,7 @@ VALUE rb_imemo_cdhash_new(size_t size, const struct st_hash_type *type) { struct rb_imemo_cdhash *memo = IMEMO_NEW(struct rb_imemo_cdhash, imemo_cdhash, 0); + memo->tbl.num_entries = 0; st_init_existing_table_with_size(&memo->tbl, type, size); return (VALUE)memo; } From 989cbe857677522fa8f880b71bc6c6bbe4982048 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 20 May 2026 14:18:00 -0500 Subject: [PATCH 09/12] [DOC] Doc for Pathname#ctime --- pathname_builtin.rb | 68 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 7a7befd6646202..5d58ec81dca362 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1034,7 +1034,8 @@ def binwrite(...) File.binwrite(@path, ...) end # atime -> new_time # # Returns a new Time object containing the time of the most recent - # access (read or write) to the entry represented by +self+: + # access (read or write) to the entry represented by `self`; + # see {File System Timestamps}[rdoc-ref:file/timestamps.md]: # # # Work in a temporary directory. # require 'tmpdir' @@ -1072,7 +1073,6 @@ def binwrite(...) File.binwrite(@path, ...) end # File atime: 2026-05-14 14:36:45 +0100 # Directory atime: 2026-05-14 14:36:45 +0100 # - # See {File System Timestamps}[rdoc-ref:file/timestamps.md]. def atime() File.atime(@path) end # :markup: markdown @@ -1081,7 +1081,8 @@ def atime() File.atime(@path) end # birthtime -> new_time # # Returns a new Time object containing the create time of the entry - # represented by +self+: + # represented by `self`; + # see [File System Timestamps](rdoc-ref:file/timestamps.md): # # ```ruby # # Work in a temporary directory. @@ -1122,10 +1123,67 @@ def atime() File.atime(@path) end # Directory birthtime: 2026-05-14 23:41:12 +0100 # ``` # - # See [File System Timestamps](rdoc-ref:file/timestamps.md). def birthtime() File.birthtime(@path) end - # See File.ctime. Returns last (directory entry, not file) change time. + # :markup: markdown + # + # call-seq: + # ctime -> new_time + # + # On Windows, returns the #birthtime. + # + # On other systems, + # returns a new Time object containing the time of the most recent + # metadata change to the entry represented by `self`; + # see {File System Timestamps}[rdoc-ref:file/timestamps.md]: + # + # ```ruby + # # Work in a temporary directory. + # Pathname.mktmpdir do |tmpdirpath| + # # A subdirectory therein, and its Pathname. + # dirpath = File.join(tmpdirpath, 'subdir') + # dir_pn = Pathname(dirpath) + # puts "Create directory; directory ctime established." + # dir_pn.mkdir + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # + # # A file in the subdirectory, and its Pathname. + # filepath = File.join(dirpath, 't.txt') + # file_pn = Pathname(filepath) + # puts "Create file; file ctime established; directory ctime updated." + # file_pn.write('foo') + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # puts "Write file; file ctime updated; directory ctime not updated." + # file_pn.write('bar') + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # sleep(1) + # puts "Read file; neither ctime not updated." + # file_pn.read + # puts " File ctime: #{file_pn.ctime}" + # puts " Directory ctime: #{dir_pn.ctime}" + # end + # ``` + # + # Output: + # + # ```text + # Create directory; directory ctime established. + # Directory ctime: 2026-05-20 14:05:05 -0500 + # Create file; file ctime established; directory ctime updated. + # File ctime: 2026-05-20 14:05:06 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # Write file; file ctime updated; directory ctime not updated. + # File ctime: 2026-05-20 14:05:07 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # Read file; neither ctime not updated. + # File ctime: 2026-05-20 14:05:07 -0500 + # Directory ctime: 2026-05-20 14:05:06 -0500 + # ``` + # def ctime() File.ctime(@path) end # See File.mtime. Returns last modification time. From dfef7ecdbd64a1409f6bfd1e8679d6ff9111a71f Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 20 May 2026 15:14:57 -0500 Subject: [PATCH 10/12] [DOC] Doc for Pathname.directory? --- pathname_builtin.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 5d58ec81dca362..303da2291a21c2 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1488,7 +1488,20 @@ def exist?() FileTest.exist?(@path) end # See FileTest.grpowned?. def grpowned?() FileTest.grpowned?(@path) end - # See FileTest.directory?. + # :markup: markdown + # + # call-seq: + # directory? -> true or false + # + # Returns whether the entry represented by `self` is a directory: + # + # ```ruby + # Pathname('/etc').directory? # => true + # Pathname('lib').directory? # => true + # Pathname('README.md').directory? # => false + # Pathname('nosuch').directory? # => false + # ``` + # def directory?() FileTest.directory?(@path) end # See FileTest.file?. From 63c35b40bc1b5b8125481bd05f00d84716cac688 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Wed, 20 May 2026 15:03:13 -0500 Subject: [PATCH 11/12] [DOC] Doc for Pathname#descend --- pathname_builtin.rb | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index 303da2291a21c2..51b6fd6a071d14 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -577,31 +577,32 @@ def each_filename # :yield: filename nil end - # Iterates over and yields a new Pathname object - # for each element in the given path in descending order. - # - # Pathname.new('/path/to/some/file.rb').descend {|v| p v} - # # - # # - # # - # # - # # - # - # Pathname.new('path/to/some/file.rb').descend {|v| p v} - # # - # # - # # - # # + # :markup: markdown # - # Returns an Enumerator if no block was given. + # call-seq: + # descend {|entry| ... } -> nil + # descend -> new_enumerator # - # enum = Pathname.new("/usr/bin/ruby").descend - # # ... do stuff ... - # enum.each { |e| ... } - # # yields Pathnames /, /usr, /usr/bin, and /usr/bin/ruby. + # With a block given, yields a new pathname for each successive dirname + # in the stored path; see File.dirname: # - # It doesn't access the filesystem. + # ```ruby + # # Absolute path. + # Pathname('/path/to/some/file.rb').descend {|pn| p pn } + # # + # # + # # + # # + # # + # # Relative path. + # Pathname('path/to/some/file.rb').descend {|pn| p pn } + # # + # # + # # + # # + # ``` # + # With no block given, returns a new Enumerator. def descend return to_enum(__method__) unless block_given? vs = [] From eeed9ea3c4cf36a857252d909364fdea09e24a82 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Tue, 19 May 2026 20:06:29 -0400 Subject: [PATCH 12/12] Move rb_gc_update_set_refs into gc.c The code is in gc/gc.h but it is not used outside of gc.c, so we can move it there. --- gc.c | 28 ++++++++++++++++++++++++++-- gc/gc.h | 30 ------------------------------ 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/gc.c b/gc.c index 6f4997314833ea..168087d9148fa8 100644 --- a/gc.c +++ b/gc.c @@ -3927,10 +3927,34 @@ rb_gc_update_tbl_refs(st_table *ptr) gc_update_table_refs(ptr); } +static int +rb_gc_update_set_refs_i(st_data_t key, st_data_t value, st_data_t argp, int error) +{ + if (rb_gc_location((VALUE)key) != (VALUE)key) { + return ST_REPLACE; + } + + return ST_CONTINUE; +} + +static int +rb_gc_update_set_refs_replace_i(st_data_t *key, st_data_t *value, st_data_t argp, int existing) +{ + if (rb_gc_location((VALUE)*key) != (VALUE)*key) { + *key = rb_gc_location((VALUE)*key); + } + + return ST_CONTINUE; +} + void -rb_gc_update_set_refs(st_table *ptr) +rb_gc_update_set_refs(st_table *tbl) { - gc_update_set_refs(ptr); + if (!tbl || tbl->num_entries == 0) return; + + if (st_foreach_with_replace(tbl, rb_gc_update_set_refs_i, rb_gc_update_set_refs_replace_i, 0)) { + rb_raise(rb_eRuntimeError, "hash modified during iteration"); + } } static void diff --git a/gc/gc.h b/gc/gc.h index 69e0e0b7807dcf..ea8056c6716f08 100644 --- a/gc/gc.h +++ b/gc/gc.h @@ -240,36 +240,6 @@ gc_update_table_refs(st_table *tbl) } } -static int -rb_set_foreach_replace(st_data_t key, st_data_t value, st_data_t argp, int error) -{ - if (rb_gc_location((VALUE)key) != (VALUE)key) { - return ST_REPLACE; - } - - return ST_CONTINUE; -} - -static int -rb_set_replace_ref(st_data_t *key, st_data_t *value, st_data_t argp, int existing) -{ - if (rb_gc_location((VALUE)*key) != (VALUE)*key) { - *key = rb_gc_location((VALUE)*key); - } - - return ST_CONTINUE; -} - -static void -gc_update_set_refs(st_table *tbl) -{ - if (!tbl || tbl->num_entries == 0) return; - - if (st_foreach_with_replace(tbl, rb_set_foreach_replace, rb_set_replace_ref, 0)) { - rb_raise(rb_eRuntimeError, "hash modified during iteration"); - } -} - static inline size_t xmalloc2_size(const size_t count, const size_t elsize) {