Skip to content

Commit 8914bd2

Browse files
committed
Handle JIT frames
1 parent 6a58b5e commit 8914bd2

9 files changed

Lines changed: 149 additions & 2 deletions

File tree

interpreter/ruby/ruby.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ import (
2525
"go.opentelemetry.io/ebpf-profiler/libpf"
2626
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
2727
"go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe"
28+
"go.opentelemetry.io/ebpf-profiler/lpm"
2829
"go.opentelemetry.io/ebpf-profiler/metrics"
2930
npsr "go.opentelemetry.io/ebpf-profiler/nopanicslicereader"
31+
"go.opentelemetry.io/ebpf-profiler/process"
3032
"go.opentelemetry.io/ebpf-profiler/remotememory"
33+
"go.opentelemetry.io/ebpf-profiler/reporter"
3134
"go.opentelemetry.io/ebpf-profiler/successfailurecounter"
3235
"go.opentelemetry.io/ebpf-profiler/support"
3336
"go.opentelemetry.io/ebpf-profiler/util"
@@ -88,6 +91,8 @@ var (
8891

8992
unknownCfunc = libpf.Intern("UNKNOWN CFUNC")
9093
cfuncDummyFile = libpf.Intern("<cfunc>")
94+
rubyJitDummyFrame = libpf.Intern("UNKNOWN JIT CODE")
95+
rubyJitDummyFile = libpf.Intern("<jitted code>")
9196
// compiler check to make sure the needed interfaces are satisfied
9297
_ interpreter.Data = &rubyData{}
9398
_ interpreter.Instance = &rubyInstance{}
@@ -295,8 +300,11 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp
295300
return &rubyInstance{
296301
r: r,
297302
rm: rm,
303+
procInfo: &cdata,
298304
globalSymbolsAddr: r.globalSymbolsAddr + bias,
299305
addrToString: addrToString,
306+
mappings: make(map[process.Mapping]*uint32),
307+
prefixes: make(map[lpm.Prefix]*uint32),
300308
memPool: sync.Pool{
301309
New: func() any {
302310
buf := make([]byte, 512)
@@ -335,6 +343,9 @@ type rubyInstance struct {
335343
r *rubyData
336344
rm remotememory.RemoteMemory
337345

346+
// Store the procinfo so we can update it if mappings are updated
347+
procInfo *support.RubyProcInfo
348+
338349
// globalSymbolsAddr is the offset of the global symbol table, for looking up ruby symbolic ids
339350
globalSymbolsAddr libpf.Address
340351

@@ -347,6 +358,13 @@ type rubyInstance struct {
347358
// maxSize is the largest number we did see in the last reporting interval for size
348359
// in getRubyLineNo.
349360
maxSize atomic.Uint32
361+
362+
// mappings is indexed by the Mapping to its generation
363+
mappings map[process.Mapping]*uint32
364+
// prefixes is indexed by the prefix added to ebpf maps (to be cleaned up) to its generation
365+
prefixes map[lpm.Prefix]*uint32
366+
// mappingGeneration is the current generation (so old entries can be pruned)
367+
mappingGeneration uint32
350368
}
351369

352370
func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error {
@@ -984,6 +1002,15 @@ func (r *rubyInstance) Symbolize(frame *host.Frame, frames *libpf.Frames) error
9841002

9851003
case support.RubyFrameTypeIseq:
9861004
iseqBody = libpf.Address(frameAddr)
1005+
case support.RubyFrameTypeJit:
1006+
label := rubyJitDummyFrame
1007+
frames.Append(&libpf.Frame{
1008+
Type: libpf.RubyFrame,
1009+
FunctionName: label,
1010+
SourceFile: rubyJitDummyFile,
1011+
SourceLine: 0,
1012+
})
1013+
return nil
9871014
default:
9881015
return fmt.Errorf("Unable to get CME or ISEQ from frame address (%d : %04x)", frameAddrType, frameFlags)
9891016
}
@@ -1121,6 +1148,92 @@ func profileFrameFullLabel(classPath, label, baseLabel, methodName libpf.String,
11211148
return libpf.Intern(profileLabel)
11221149
}
11231150

1151+
func (r *rubyInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler,
1152+
_ reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping) error {
1153+
var jitMapping *process.Mapping
1154+
1155+
pid := pr.PID()
1156+
jitFound := false
1157+
r.mappingGeneration++
1158+
1159+
log.Debugf("Synchronizing ruby mappings")
1160+
1161+
for idx := range mappings {
1162+
m := &mappings[idx]
1163+
if !m.IsExecutable() || !m.IsAnonymous() {
1164+
continue
1165+
}
1166+
// If prctl is allowed, ruby should label the memory region
1167+
// always prefer that
1168+
if strings.Contains(m.Path.String(), "jit_reserve_addr_space") {
1169+
jitMapping = m
1170+
jitFound = true
1171+
}
1172+
// Use the first executable anon region we find if it isn't labeled
1173+
// If we find more, prefer ones earlier in memory or larger in size
1174+
if !jitFound && (jitMapping == nil || m.Vaddr < jitMapping.Vaddr || m.Length > jitMapping.Length) {
1175+
// Don't set jitFound here as it is a heuristic, we aren't sure
1176+
// could be on a system without linux config flag to allow prctl to label memoy
1177+
jitMapping = m
1178+
}
1179+
1180+
if _, exists := r.mappings[*m]; exists {
1181+
*r.mappings[*m] = r.mappingGeneration
1182+
continue
1183+
}
1184+
1185+
// Generate a new uint32 pointer which is shared for mapping and the prefixes it owns
1186+
// so updating the mapping above will reflect to prefixes also.
1187+
mappingGeneration := r.mappingGeneration
1188+
r.mappings[*m] = &mappingGeneration
1189+
1190+
// Just assume all anonymous and executable mappings are Ruby for now
1191+
log.Debugf("Enabling Ruby interpreter for %#x/%#x", m.Vaddr, m.Length)
1192+
1193+
prefixes, err := lpm.CalculatePrefixList(m.Vaddr, m.Vaddr+m.Length)
1194+
if err != nil {
1195+
return fmt.Errorf("new anonymous mapping lpm failure %#x/%#x", m.Vaddr, m.Length)
1196+
}
1197+
1198+
for _, prefix := range prefixes {
1199+
_, exists := r.prefixes[prefix]
1200+
if !exists {
1201+
err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindRuby, 0, 0)
1202+
if err != nil {
1203+
return err
1204+
}
1205+
}
1206+
r.prefixes[prefix] = &mappingGeneration
1207+
}
1208+
}
1209+
if jitMapping != nil && (r.procInfo.Jit_start != jitMapping.Vaddr || r.procInfo.Jit_end != jitMapping.Vaddr+jitMapping.Length) {
1210+
r.procInfo.Jit_start = jitMapping.Vaddr
1211+
r.procInfo.Jit_end = jitMapping.Vaddr + jitMapping.Length
1212+
if err := ebpf.UpdateProcData(libpf.Ruby, pr.PID(), unsafe.Pointer(r.procInfo)); err != nil {
1213+
return err
1214+
}
1215+
log.Debugf("Added jit mapping %08x ruby proc info, %08x", r.procInfo.Jit_start, r.procInfo.Jit_end)
1216+
}
1217+
// Remove prefixes not seen
1218+
for prefix, generationPtr := range r.prefixes {
1219+
if *generationPtr == r.mappingGeneration {
1220+
continue
1221+
}
1222+
log.Debugf("Delete Ruby prefix %#v", prefix)
1223+
_ = ebpf.DeletePidInterpreterMapping(pid, prefix)
1224+
delete(r.prefixes, prefix)
1225+
}
1226+
for m, generationPtr := range r.mappings {
1227+
if *generationPtr == r.mappingGeneration {
1228+
continue
1229+
}
1230+
log.Debugf("Disabling Ruby for %#x/%#x", m.Vaddr, m.Length)
1231+
delete(r.mappings, m)
1232+
}
1233+
1234+
return nil
1235+
}
1236+
11241237
func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) {
11251238
addrToStringStats := r.addrToString.ResetMetrics()
11261239

support/ebpf/ruby_tracer.ebpf.c

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ static EBPF_INLINE ErrorCode read_ruby_frame(
247247
// continue unwinding Ruby VM frames. Due to this issue, the ordering of Ruby and native
248248
// frames will almost certainly be incorrect for Ruby versions < 2.6.
249249
frame_type = FRAME_TYPE_CME_CFUNC;
250+
} else if (record->rubyUnwindState.jit_detected) {
251+
// If we detected a jit frame and are now in a cfunc, push the c frame
252+
// as we can no longer unwind native anymore
253+
frame_type = FRAME_TYPE_CME_CFUNC;
250254
} else {
251255
// We save this cfp on in the "Record" entry, and when we start the unwinder
252256
// again we'll push it so that the order is correct and the cfunc "owns" any native code we
@@ -404,6 +408,22 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
404408
record->rubyUnwindState.cfunc_saved_frame = 0;
405409
}
406410

411+
if (
412+
rubyinfo->jit_start > 0 && record->state.pc > rubyinfo->jit_start &&
413+
record->state.pc < rubyinfo->jit_end) {
414+
record->rubyUnwindState.jit_detected = true;
415+
416+
// If the first frame is a jit PC, the leaf ruby frame should be the jit "owner"
417+
// the cpu PC is also pushed as the address,
418+
// as in theory this can be used to symbolize the JIT frame later
419+
if (trace->stack_len == 0) {
420+
ErrorCode error = push_ruby(trace, 0, FRAME_TYPE_JIT, (u64)record->state.pc, 0, 0);
421+
if (error) {
422+
return error;
423+
}
424+
}
425+
}
426+
407427
UNROLL for (u32 i = 0; i < FRAMES_PER_WALK_RUBY_STACK; ++i)
408428
{
409429
error = read_ruby_frame(record, rubyinfo, stack_ptr, next_unwinder);
@@ -412,7 +432,10 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
412432

413433
if (last_stack_frame <= stack_ptr) {
414434
// We have processed all frames in the Ruby VM and can stop here.
415-
*next_unwinder = PROG_UNWIND_NATIVE;
435+
// if this process has been JIT'd, the PC is invalid and we cannot resume native unwinding so
436+
// we are done
437+
*next_unwinder = record->rubyUnwindState.jit_detected ? PROG_UNWIND_STOP : PROG_UNWIND_NATIVE;
438+
goto save_state;
416439
} else {
417440
// If we aren't at the end, advance the stack pointer to continue from the next frame
418441
stack_ptr += rubyinfo->size_of_control_frame_struct;

support/ebpf/ruby_tracer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
#define FRAME_TYPE_CME_ISEQ 1
66
#define FRAME_TYPE_CME_CFUNC 2
77
#define FRAME_TYPE_ISEQ 3
8+
#define FRAME_TYPE_JIT 5

support/ebpf/tracemgmt.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ static inline EBPF_INLINE PerCPURecord *get_pristine_per_cpu_record()
244244
record->rubyUnwindState.stack_ptr = 0;
245245
record->rubyUnwindState.last_stack_frame = 0;
246246
record->rubyUnwindState.cfunc_saved_frame = 0;
247+
record->rubyUnwindState.jit_detected = false;
247248
record->unwindersDone = 0;
248249
record->tailCalls = 0;
249250
record->ratelimitAction = RATELIMIT_ACTION_DEFAULT;

support/ebpf/tracer.ebpf.amd64

7.39 KB
Binary file not shown.

support/ebpf/tracer.ebpf.arm64

7.39 KB
Binary file not shown.

support/ebpf/types.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,9 @@ typedef struct RubyProcInfo {
487487
// current_ctx_ptr holds the address of the symbol ruby_current_execution_context_ptr.
488488
u64 current_ctx_ptr;
489489

490+
// JIT regions, for detecting if a native PC was JIT
491+
u64 jit_start, jit_end;
492+
490493
// Offsets and sizes of Ruby internal structs
491494

492495
// rb_execution_context_struct offsets:
@@ -692,6 +695,8 @@ typedef struct RubyUnwindState {
692695
void *last_stack_frame;
693696
// Frame for last cfunc before we switched to native unwinder
694697
u64 cfunc_saved_frame;
698+
// Detect if JIT code ran in the process (at any time)
699+
bool jit_detected;
695700
} RubyUnwindState;
696701

697702
// Container for additional scratch space needed by the HotSpot unwinder.

support/types.go

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

support/types_def.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ const (
199199
RubyFrameTypeCmeIseq = C.FRAME_TYPE_CME_ISEQ
200200
RubyFrameTypeCmeCfunc = C.FRAME_TYPE_CME_CFUNC
201201
RubyFrameTypeIseq = C.FRAME_TYPE_ISEQ
202+
RubyFrameTypeJit = C.FRAME_TYPE_JIT
202203
)
203204

204205
var MetricsTranslation = []metrics.MetricID{

0 commit comments

Comments
 (0)