From 169c092ab51c71e673688c801d9aa1a5ef087153 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Mon, 12 Oct 2026 22:42:12 +0900 Subject: [PATCH 1/2] test: warm up right after `GC.start` in `test_no_memory_leak` `test_no_memory_leak` fails on `ubuntu-latest 3.3` in CI: <2441216> was expected to be less than <2400000>. This isn't a leak. The RSS grows only once, and it doesn't depend on the number of calls. `assert_nothing_leaked_memory` runs the block right after `GC.start`. `GC.start` resets the malloc increase counters, so the next GC comes later and the block can raise the peak malloc usage. The process keeps that peak in RSS. The warm up loop doesn't run right after `GC.start`, so it doesn't reach the same peak. Run `GC.start` before the warm up loop too. With this change, the RSS growth in the block is 0 in 10 of 10 runs locally. Without it, the growth is 1.4 MB to 1.5 MB in 6 of 10 runs. Co-Authored-By: Claude Opus 5.5 --- test/fiddle/test_function.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/fiddle/test_function.rb b/test/fiddle/test_function.rb index dcfdd78f..8c140185 100644 --- a/test/fiddle/test_function.rb +++ b/test/fiddle/test_function.rb @@ -328,6 +328,9 @@ def test_no_memory_leak Fiddle::TYPE_UINTPTR_T) a = "a" n_tries = 100_000 + # Warm up in the same state as assert_nothing_leaked_memory, + # which runs the block right after GC.start. + GC.start n_tries.times do begin a + 1 From b3b6a3c702d59201bb73306e77383019ee58c782 Mon Sep 17 00:00:00 2001 From: Sutou Kouhei Date: Tue, 13 Oct 2026 06:15:56 +0900 Subject: [PATCH 2/2] DO NOT MERGE: diagnose `test_no_memory_leak` on CI Print RSS details from `/proc/self/status`, some `GC.stat` values and `malloc_stats()` at each stage of `test_no_memory_leak`. The test fails on CI with Ruby 3.1 but it doesn't fail locally. This is temporary and will be removed. Co-Authored-By: Claude Opus 5.5 --- test/fiddle/test_function.rb | 49 ++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/test/fiddle/test_function.rb b/test/fiddle/test_function.rb index 8c140185..dc6fa170 100644 --- a/test/fiddle/test_function.rb +++ b/test/fiddle/test_function.rb @@ -328,27 +328,66 @@ def test_no_memory_leak Fiddle::TYPE_UINTPTR_T) a = "a" n_tries = 100_000 + # DIAG: temporary diagnostics. Don't merge. + malloc_stats = begin + Fiddle::Function.new(Fiddle.dlopen(nil)["malloc_stats"], + [], + Fiddle::TYPE_VOID) + rescue Fiddle::DLError + nil + end + diag = lambda do |label| + status = File.read("/proc/self/status") rescue "" + rss = status.scan(/^(VmRSS|RssAnon|RssFile|RssShmem):\s+(\d+)/) + stat = GC.stat + keys = [ + :count, + :major_gc_count, + :heap_allocated_pages, + :heap_live_slots, + :malloc_increase_bytes, + :malloc_increase_bytes_limit, + :oldmalloc_increase_bytes, + ] + $stderr.puts("DIAG #{RUBY_VERSION} #{label}: " + + rss.collect {|k, v| "#{k}=#{v}kB"}.join(" ") + " " + + keys.collect {|k| "#{k}=#{stat[k]}"}.join(" ")) + if malloc_stats + $stderr.puts("DIAG malloc_stats #{label}:") + $stderr.flush + malloc_stats.call + end + end + diag.call("start") # Warm up in the same state as assert_nothing_leaked_memory, # which runs the block right after GC.start. GC.start + diag.call("after warm up GC") n_tries.times do begin a + 1 rescue TypeError end end + diag.call("after warm up") n_arguments = 1 sizeof_fiddle_generic = Fiddle::SIZEOF_VOIDP # Rough size_per_try = (sizeof_fiddle_generic * n_arguments) + (Fiddle::SIZEOF_VOIDP * (n_arguments + 1)) - assert_nothing_leaked_memory(size_per_try * n_tries) do - n_tries.times do - begin - rb_obj_frozen_p.call(a) - rescue TypeError + begin + assert_nothing_leaked_memory(size_per_try * n_tries) do + diag.call("block start") + n_tries.times do + begin + rb_obj_frozen_p.call(a) + rescue TypeError + end end + diag.call("block end") end + ensure + diag.call("after assertion") end else prep = 'r = Fiddle::Function.new(Fiddle.dlopen(nil)["rb_obj_frozen_p"], [Fiddle::TYPE_UINTPTR_T], Fiddle::TYPE_UINTPTR_T); a = "a"'