Skip to content

Commit 2a1015d

Browse files
committed
Refactor JIT FP walk to per-iteration unwind and fix JIT region detection
Restructure the eBPF JIT frame pointer walk: instead of a separate loop before the main CFP walk, advance the native FP chain by one frame per main loop iteration. This keeps the native unwind state in lockstep with the Ruby VM stack, supporting both YJIT (1 JIT frame, exits after first iteration) and ZJIT (1 JIT frame per iseq, 1:1 with CFPs). Fix JIT region detection in SynchronizeMappings to scan all mappings (including non-executable ---p reservations) for the prctl-labeled JIT region, then only register LPM prefixes for executable pages. This ensures jit_start/jit_end cover the full reserved address range even when the r-xp committed pages don't carry the label. Also fix IsAnonymous() to recognize [anon:...] labeled mappings, and remove debug log spam from parseMappings.
1 parent 88631d0 commit 2a1015d

File tree

2 files changed

+36
-35
lines changed

2 files changed

+36
-35
lines changed

support/ebpf/ruby_tracer.ebpf.c

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ struct ruby_procs_t {
2020
// NOTE: the maximum size stack is FRAMES_PER_WALK_RUBY_STACK * calls to tail_call().
2121
#define FRAMES_PER_WALK_RUBY_STACK 32
2222

23-
// The maximum number of JIT frames to unwind via frame pointers.
24-
// YJIT creates one native frame per JIT entry (not per Ruby method),
25-
// so in practice there is typically only 1 (occasionally 2 for nested entries).
26-
#define MAX_JIT_FP_FRAMES 4
2723
// When resolving a CME, we need to traverse environment pointers until we
2824
// find IMEMO_MENT. Since we can't do a while loop, we have to bound this
2925
// the max encountered in experimentation on a production rails app is 6.
@@ -457,40 +453,25 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
457453
}
458454

459455
// Detect if the CPU PC is in the JIT region.
460-
// When frame pointers are available, walk the native FP chain through JIT frames.
461-
// When not available, push a single dummy JIT frame and skip FP walking.
456+
// When frame pointers are available, we keep the native unwind state in sync with
457+
// the Ruby VM stack by advancing the FP chain by one frame per loop iteration.
458+
// This handles both YJIT (1 JIT frame, exits after first iteration) and ZJIT
459+
// (1 JIT frame per iseq, 1:1 with CFPs, stays in sync throughout the walk).
460+
//
461+
// When frame pointers are not available, we push a single dummy JIT frame and
462+
// set jit_detected to suppress native unwinding.
462463
bool in_jit = rubyinfo->jit_start > 0 && record->state.pc >= rubyinfo->jit_start &&
463464
record->state.pc < rubyinfo->jit_end;
464465

465466
if (in_jit) {
466467
if (rubyinfo->frame_pointers_enabled) {
467-
// Walk the native FP chain through JIT frames, pushing each as a JIT frame
468-
// so it can potentially be symbolized via perf maps later.
469-
UNROLL for (int j = 0; j < MAX_JIT_FP_FRAMES; j++)
470-
{
471-
ErrorCode jit_error =
472-
push_ruby(&record->state, trace, RUBY_FRAME_TYPE_JIT, (u64)record->state.pc, 0, 0);
473-
if (jit_error) {
474-
return jit_error;
475-
}
476-
477-
if (!unwinder_unwind_frame_pointer(&record->state)) {
478-
// FP chain broken, cannot continue
479-
*next_unwinder = PROG_UNWIND_STOP;
480-
return ERR_OK;
481-
}
482-
483-
// Check if we've left the JIT region
484-
if (record->state.pc < rubyinfo->jit_start || record->state.pc >= rubyinfo->jit_end) {
485-
break;
486-
}
487-
}
488-
// After walking JIT frames, PC should be in rb_vm_exec or other native code.
489-
// Resolve the mapping for the new PC so text_section_id/offset/bias are correct.
490-
ErrorCode map_err = get_next_unwinder_after_native_frame(record, next_unwinder);
491-
if (map_err) {
492-
return map_err;
468+
// Push a leaf JIT frame with the raw machine PC for perf-map symbolization.
469+
ErrorCode jit_error =
470+
push_ruby(&record->state, trace, RUBY_FRAME_TYPE_JIT, (u64)record->state.pc, 0, 0);
471+
if (jit_error) {
472+
return jit_error;
493473
}
474+
494475
} else {
495476
// No frame pointers available: push a single dummy JIT frame.
496477
// Mark jit_detected so that cfuncs are pushed inline and end-of-stack uses
@@ -501,18 +482,38 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
501482
if (jit_error) {
502483
return jit_error;
503484
}
485+
in_jit = false;
504486
}
505487
}
506488

507489
for (u32 i = 0; i < FRAMES_PER_WALK_RUBY_STACK; ++i) {
490+
// Keep the native unwind state in sync: if the native PC is still in the JIT
491+
// region, advance it by one frame pointer to match the Ruby VM stack pop.
492+
// For YJIT this exits JIT on the first iteration. For ZJIT this pops one JIT
493+
// native frame per CFP, keeping the two stacks in lockstep.
494+
if (in_jit) {
495+
if (!unwinder_unwind_frame_pointer(&record->state)) {
496+
*next_unwinder = PROG_UNWIND_STOP;
497+
return ERR_OK;
498+
}
499+
if (record->state.pc < rubyinfo->jit_start || record->state.pc >= rubyinfo->jit_end) {
500+
// Exited the JIT region. Resolve the mapping for the post-JIT PC so that
501+
// text_section_id/offset/bias are correct for native unwinding later.
502+
in_jit = false;
503+
ErrorCode map_err = get_next_unwinder_after_native_frame(record, next_unwinder);
504+
if (map_err) {
505+
return map_err;
506+
}
507+
}
508+
}
509+
508510
error = read_ruby_frame(record, rubyinfo, stack_ptr, next_unwinder);
509511
if (error != ERR_OK)
510512
return error;
511513

512514
if (last_stack_frame <= stack_ptr) {
513515
// We have processed all frames in the Ruby VM and can stop here.
514-
// If we walked through JIT frames via FP, the state is clean and native unwinding
515-
// can continue. If JIT was detected without FP, the PC is still in the JIT region
516+
// If JIT was detected without FP, the PC is still in the JIT region
516517
// and native unwinding would fail, so we stop.
517518
*next_unwinder = record->rubyUnwindState.jit_detected ? PROG_UNWIND_STOP : PROG_UNWIND_NATIVE;
518519
goto save_state;

support/types.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)