Repository navigation
Conversation
dalehamel
force-pushed
the
dale/ruby-native-resume-cfunc-owner
branch
from
October 3, 2026 03:23
d780c1d to
9d5f568
Compare
Native resume defers a cfunc frame until the next rb_vm_exec, so the cfunc owns the native frames unwound in between. That is only right for the cfunc that called into that VM loop, which sits directly below the loop's base (VM_FRAME_FLAG_FINISH) frame. When a sample lands inside a C method, that method's native frames were unwound before the Ruby unwinder ran. Deferring it attached it to the next native segment, shifted every later cfunc by one segment, and dropped the outermost Ruby segment. At the top level no later rb_vm_exec exists, so every Ruby frame was lost. Push such cfuncs inline. A cfunc on top of the VM stack with at most one native frame above the first rb_vm_exec is still deferred: it is not running, because its block has returned and the inner rb_vm_exec is finishing. A running C method always adds at least two frames, its own function and the VM call helper reached through an indirect call. Once the last Ruby frame is pushed, mark the Ruby unwinder done so a further rb_vm_exec frame cannot push that frame again, and push a still-deferred bottom cfunc instead of dropping it. The tracer blobs are rebuilt in otel/opentelemetry-ebpf-profiler-dev@sha256:585409de39191c201f91a8e6ec5294556c8f21c7d2ce0b0f96e838f27e399f82. unwind_ruby grows from 1298 to 1381 instructions. Found while investigating open-telemetry#1934. Signed-off-by: Dale Hamel <dale.hamel@shopify.com>
dalehamel
force-pushed
the
dale/ruby-native-resume-cfunc-owner
branch
from
October 3, 2026 03:23
9d5f568 to
fd5fcb6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fork-local scope
Important
This draft targets the Shopify fork branch
dale/ruby41-support, not upstreammain, and is independent of #89/#90. The regenerated coredump goldens are in stacked draft #92. Until that lands, this PR's coredump test jobs are expected to fail on exactly the 18 Ruby cases #92 updates.With native resume enabled (the default), samples taken inside a C method lose Ruby frames. I found this while investigating open-telemetry#1934. It's separate from that issue's GC-offset fix (#89).
Cause
Native resume defers a cfunc frame until the next
rb_vm_exec, so the cfunc owns the native frames unwound in between. That's only right for the cfunc that called into that VM loop, which sits directly below the loop's base (VM_FRAME_FLAG_FINISH) frame.When a sample lands inside a C method, that method's native frames were unwound before the Ruby unwinder ran. Deferring it attaches it to the next native segment, shifts every later cfunc by one segment, and drops the outermost Ruby segment. At the top level there's no later
rb_vm_exec, so every Ruby frame is lost. That's the Ruby 3.3+Process.clock_gettimesymptom in open-telemetry#1934.Change
Range#each→<=>.rb_vm_execis finishing. A running C method always adds at least two native frames, its own function and the VM call helper reached through an indirect call.rb_vm_execframe can't push that frame again. A still-deferred bottom cfunc is pushed instead of dropped.VM_FRAME_FLAG_FINISHis0x0020from Ruby 2.6 through master. Theskip_native_resume, JIT, and pre-2.6 paths are unchanged.unwind_rubygrows from 1,298 to 1,381 instructions on both architectures.Cherry-picking
ruby_tracer.ebpf.candtypes.hmerge cleanly onto upstreammainandv0.0.202636. Onlysupport/ebpf/tracer.ebpf.{amd64,arm64}conflict. Rebuild them withmake amd64 -C support/ebpf && make arm64 -C support/ebpf, ideally inotel/opentelemetry-ebpf-profiler-dev, rather than taking either side. Without the rebuild, the embedded BPF doesn't contain the fix.Validation
Live profiling on arm64 (kernel 6.8), Ruby 3.3.12 with default native resume, same base before and after:
hot.rb)<main><main><main>(1..n).each { |i| s += i }(1..n).each { |i| s += i.to_s.size }In the "after" column, every remaining sample is an abort inside a libruby PLT stub. That's an arm64 native-unwinder gap, unrelated to this change and fixed separately in #93. With both changes, every sample in both loops is complete.
Known limit
In the window between pushing a block's frame and entering its VM loop, the bottom Ruby frames are still dropped, as before. This never occurred in about 9,500 samples of block-heavy loops.