Skip to content

Reset the class of each captured call - #10

Merged
samuel-williams-shopify merged 2 commits into
socketry:mainfrom
trevorturk:reset-call-class
Sep 29, 2026
Merged

samuel-williams-shopify merged 2 commits into
socketry:mainfrom
trevorturk:reset-call-class

Conversation

@trevorturk

Copy link
Copy Markdown

Found while investigating #7.

Fiber_Profiler_Capture_Call_initialize resets id but not klass. When there's no method frame, as with a block defined outside a method or a GC outside a method, rb_frame_method_id_and_class returns false and leaves klass untouched. So these calls report whatever class their slot held: false for a fresh slot, or an earlier call's class for a reused one.

Once the earlier call is gone, the capture no longer marks that class, so it may have been freed or moved. After GC.compact I've seen reports name unrelated objects, including strings whose quotes produced invalid JSON.

Reproduction:

require "fiber/profiler"
require "stringio"
require "json"

class Foo
	def bar = baz
	def baz = nil
end

output = StringIO.new
capture = Fiber::Profiler::Capture.new(stall_threshold: 0.001, output: output)
capture.start

Fiber.new do
	Foo.new.bar
	[1, 2].each {sleep 0.005}
end.resume

capture.stop

JSON.parse(output.string)["calls"].each do |call|
	puts call.values_at("path", "line", "class", "method").join(" ")
end

On main (Ruby 4.0.7 and 3.4.10), the block on line 16 is reported as Foo, left over from the earlier Foo#baz call:

stale.rb 15 false (null)
stale.rb 16 Foo (null)
stale.rb 16 Kernel sleep
stale.rb 16 false (null)
stale.rb 16 Kernel sleep

This change initializes klass to nil along with id, so both block calls are reported as nil, or <unknown> with #8.

The added test runs a top-level block (inside sus, every it block is itself within a method). It fails before the change, reporting ["false", "String", "Kernel", "false", "Kernel"], and passes after. It doesn't depend on the label, so it works with or without #8. bundle exec sus: 41/41 on Ruby 4.0.7 (YJIT) and 3.4.10.

`Fiber_Profiler_Capture_Call_initialize` reset `id` but not `klass`, and
`rb_frame_method_id_and_class` leaves `klass` untouched when there is no
method frame (e.g. a block defined outside of a method, or GC outside of
a method). Such calls reported whatever class was left in their slot:
`false` for a fresh slot, or the class of an earlier call for a reused
one. That earlier class is no longer marked by the capture, so it may
have been freed or moved, and the report can show an unrelated object.

Initialize `klass` to `nil` so these calls report `nil` instead.
@samuel-williams-shopify
samuel-williams-shopify merged commit 3912b0f into socketry:main Sep 29, 2026
20 checks passed
@samuel-williams-shopify

Copy link
Copy Markdown
Contributor

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants