From 5d7776f891ec6a379870c35f39cf6c4ccf04f9f5 Mon Sep 17 00:00:00 2001 From: Trevor Turk Date: Mon, 28 Sep 2026 22:38:40 -0500 Subject: [PATCH] Don't inspect hidden classes when printing captured calls. When a GC starts inside a method defined on a hidden class, such as the singleton class of `RubyVM::FrozenCore` (which implements `->{}`, `**` hash merges and similar), `rb_frame_method_id_and_class` returns that hidden class as the owner of the GC call. Printing the report then called `rb_inspect` on it, which raises `NotImplementedError` out of the fiber switch hook and into whatever resumed the fiber. Use `rb_class_path` for hidden classes instead, which doesn't dispatch a method on the class. Fixes #7. --- ext/fiber/profiler/capture.c | 13 +++++++++++-- test/fiber/profiler/capture.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ext/fiber/profiler/capture.c b/ext/fiber/profiler/capture.c index 814f5f1..cca1f98 100644 --- a/ext/fiber/profiler/capture.c +++ b/ext/fiber/profiler/capture.c @@ -627,6 +627,15 @@ static size_t Fiber_Profiler_Capture_absolute_nesting(struct Fiber_Profiler_Capt // If a call is within this threshold of the parent call, it will be skipped when printing the call stack - it's considered inconsequential to the performance of the parent call. static const double Fiber_Profiler_Capture_SKIP_THRESHOLD = 0.98; +// Hidden classes (e.g. the singleton class of RubyVM::FrozenCore, which implements lambda literals) raise NotImplementedError from #inspect, so we use their class path instead: +static VALUE Fiber_Profiler_Capture_Call_class_inspect(struct Fiber_Profiler_Capture_Call *call) { + if (!RB_SPECIAL_CONST_P(call->klass) && RBASIC_CLASS(call->klass) == 0) { + return rb_class_path(call->klass); + } + + return rb_inspect(call->klass); +} + void Fiber_Profiler_Capture_print_tty(struct Fiber_Profiler_Capture *capture, FILE *restrict stream, double duration) { double start_time = Fiber_Profiler_Time_delta(&capture->start_time, &capture->switch_time); @@ -677,7 +686,7 @@ void Fiber_Profiler_Capture_print_tty(struct Fiber_Profiler_Capture *capture, FI fprintf(stream, "\e[31m"); } - VALUE class_inspect = rb_inspect(call->klass); + VALUE class_inspect = Fiber_Profiler_Capture_Call_class_inspect(call); const char *name = rb_id2name(call->id); struct timespec offset; @@ -731,7 +740,7 @@ void Fiber_Profiler_Capture_print_json(struct Fiber_Profiler_Capture *capture, F call->nesting = call->parent->nesting + 1; } - VALUE class_inspect = rb_inspect(call->klass); + VALUE class_inspect = Fiber_Profiler_Capture_Call_class_inspect(call); const char *name = rb_id2name(call->id); size_t nesting = Fiber_Profiler_Capture_absolute_nesting(capture, call); diff --git a/test/fiber/profiler/capture.rb b/test/fiber/profiler/capture.rb index 3b20f72..e7deca5 100644 --- a/test/fiber/profiler/capture.rb +++ b/test/fiber/profiler/capture.rb @@ -140,6 +140,32 @@ def nested(n = 100, &block) end end + with "hidden classes" do + let(:capture) {subject.new(stall_threshold: 0, filter_threshold: 0, output: output)} + + it "can report garbage collection inside methods of hidden classes" do + capture.start + + Fiber.new do + GC.stress = true + + # Lambda literals call a method on the hidden singleton class of RubyVM::FrozenCore: + ->{} + ensure + GC.stress = false + end.resume + + capture.stop + + stall = JSON.parse(output.string) + calls = stall["calls"] + expect(calls).to have_value(have_keys( + "class" => be =~ /\A#\z/, + "method" => be == "lambda", + )) + end + end + with "Process.fork" do it "should disable the profiler in the child process after fork" do capture.start