Skip to content

Commit c1086a5

Browse files
committed
Handle JIT frames
1 parent 63d24dc commit c1086a5

7 files changed

Lines changed: 158 additions & 10 deletions

File tree

interpreter/ruby/ruby.go

Lines changed: 121 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ import (
2424
"go.opentelemetry.io/ebpf-profiler/libpf"
2525
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
2626
"go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe"
27+
"go.opentelemetry.io/ebpf-profiler/lpm"
2728
"go.opentelemetry.io/ebpf-profiler/metrics"
2829
npsr "go.opentelemetry.io/ebpf-profiler/nopanicslicereader"
30+
"go.opentelemetry.io/ebpf-profiler/process"
2931
"go.opentelemetry.io/ebpf-profiler/remotememory"
32+
"go.opentelemetry.io/ebpf-profiler/reporter"
3033
"go.opentelemetry.io/ebpf-profiler/successfailurecounter"
3134
"go.opentelemetry.io/ebpf-profiler/support"
3235
"go.opentelemetry.io/ebpf-profiler/util"
@@ -101,14 +104,16 @@ var (
101104
// regex to extract a version from a string
102105
rubyVersionRegex = regexp.MustCompile(`^(\d)\.(\d)\.(\d)$`)
103106

104-
unknownCfunc = libpf.Intern("<unknown cfunc>")
105-
cfuncDummyFile = libpf.Intern("<cfunc>")
106-
rubyGcFrame = libpf.Intern("(garbage collection)")
107-
rubyGcRunning = libpf.Intern("(running)")
108-
rubyGcMarking = libpf.Intern("(marking)")
109-
rubyGcSweeping = libpf.Intern("(sweeping)")
110-
rubyGcCompacting = libpf.Intern("(compacting)")
111-
rubyGcDummyFile = libpf.Intern("<gc>")
107+
unknownCfunc = libpf.Intern("<unknown cfunc>")
108+
cfuncDummyFile = libpf.Intern("<cfunc>")
109+
rubyGcFrame = libpf.Intern("(garbage collection)")
110+
rubyGcRunning = libpf.Intern("(running)")
111+
rubyGcMarking = libpf.Intern("(marking)")
112+
rubyGcSweeping = libpf.Intern("(sweeping)")
113+
rubyGcCompacting = libpf.Intern("(compacting)")
114+
rubyGcDummyFile = libpf.Intern("<gc>")
115+
rubyJitDummyFrame = libpf.Intern("<unknown jit code>")
116+
rubyJitDummyFile = libpf.Intern("<jitted code>")
112117
// compiler check to make sure the needed interfaces are satisfied
113118
_ interpreter.Data = &rubyData{}
114119
_ interpreter.Instance = &rubyInstance{}
@@ -345,8 +350,11 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp
345350
return &rubyInstance{
346351
r: r,
347352
rm: rm,
353+
procInfo: &cdata,
348354
globalSymbolsAddr: r.globalSymbolsAddr + bias,
349355
addrToString: addrToString,
356+
mappings: make(map[process.Mapping]*uint32),
357+
prefixes: make(map[lpm.Prefix]*uint32),
350358
memPool: sync.Pool{
351359
New: func() any {
352360
buf := make([]byte, 512)
@@ -390,6 +398,9 @@ type rubyInstance struct {
390398

391399
// lastId is a cached copy index of the final entry in the global symbol table
392400
lastId uint32
401+
// Store the procinfo so we can update it if mappings are updated
402+
procInfo *support.RubyProcInfo
403+
393404
// globalSymbolsAddr is the offset of the global symbol table, for looking up ruby symbolic ids
394405
globalSymbolsAddr libpf.Address
395406

@@ -402,6 +413,13 @@ type rubyInstance struct {
402413
// maxSize is the largest number we did see in the last reporting interval for size
403414
// in getRubyLineNo.
404415
maxSize atomic.Uint32
416+
417+
// mappings is indexed by the Mapping to its generation
418+
mappings map[process.Mapping]*uint32
419+
// prefixes is indexed by the prefix added to ebpf maps (to be cleaned up) to its generation
420+
prefixes map[lpm.Prefix]*uint32
421+
// mappingGeneration is the current generation (so old entries can be pruned)
422+
mappingGeneration uint32
405423
}
406424

407425
func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error {
@@ -1053,6 +1071,15 @@ func (r *rubyInstance) Symbolize(ef libpf.EbpfFrame, frames *libpf.Frames, _ lib
10531071
SourceLine: 0,
10541072
})
10551073
return nil
1074+
case support.RubyFrameTypeJit:
1075+
label := rubyJitDummyFrame
1076+
frames.Append(&libpf.Frame{
1077+
Type: libpf.RubyFrame,
1078+
FunctionName: label,
1079+
SourceFile: rubyJitDummyFile,
1080+
SourceLine: 0,
1081+
})
1082+
return nil
10561083
default:
10571084
return fmt.Errorf("Unable to get CME or ISEQ from frame address (%d)", frameAddrType)
10581085
}
@@ -1182,6 +1209,92 @@ func profileFrameFullLabel(classPath, label, baseLabel, methodName libpf.String,
11821209
return libpf.Intern(profileLabel)
11831210
}
11841211

1212+
func (r *rubyInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler,
1213+
_ reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping) error {
1214+
var jitMapping *process.Mapping
1215+
1216+
pid := pr.PID()
1217+
jitFound := false
1218+
r.mappingGeneration++
1219+
1220+
log.Debugf("Synchronizing ruby mappings")
1221+
1222+
for idx := range mappings {
1223+
m := &mappings[idx]
1224+
if !m.IsExecutable() || !m.IsAnonymous() {
1225+
continue
1226+
}
1227+
// If prctl is allowed, ruby should label the memory region
1228+
// always prefer that
1229+
if strings.Contains(m.Path.String(), "jit_reserve_addr_space") {
1230+
jitMapping = m
1231+
jitFound = true
1232+
}
1233+
// Use the first executable anon region we find if it isn't labeled
1234+
// If we find more, prefer ones earlier in memory or larger in size
1235+
if !jitFound && (jitMapping == nil || m.Vaddr < jitMapping.Vaddr || m.Length > jitMapping.Length) {
1236+
// Don't set jitFound here as it is a heuristic, we aren't sure
1237+
// could be on a system without linux config flag to allow prctl to label memoy
1238+
jitMapping = m
1239+
}
1240+
1241+
if _, exists := r.mappings[*m]; exists {
1242+
*r.mappings[*m] = r.mappingGeneration
1243+
continue
1244+
}
1245+
1246+
// Generate a new uint32 pointer which is shared for mapping and the prefixes it owns
1247+
// so updating the mapping above will reflect to prefixes also.
1248+
mappingGeneration := r.mappingGeneration
1249+
r.mappings[*m] = &mappingGeneration
1250+
1251+
// Just assume all anonymous and executable mappings are Ruby for now
1252+
log.Debugf("Enabling Ruby interpreter for %#x/%#x", m.Vaddr, m.Length)
1253+
1254+
prefixes, err := lpm.CalculatePrefixList(m.Vaddr, m.Vaddr+m.Length)
1255+
if err != nil {
1256+
return fmt.Errorf("new anonymous mapping lpm failure %#x/%#x", m.Vaddr, m.Length)
1257+
}
1258+
1259+
for _, prefix := range prefixes {
1260+
_, exists := r.prefixes[prefix]
1261+
if !exists {
1262+
err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindRuby, 0, 0)
1263+
if err != nil {
1264+
return err
1265+
}
1266+
}
1267+
r.prefixes[prefix] = &mappingGeneration
1268+
}
1269+
}
1270+
if jitMapping != nil && (r.procInfo.Jit_start != jitMapping.Vaddr || r.procInfo.Jit_end != jitMapping.Vaddr+jitMapping.Length) {
1271+
r.procInfo.Jit_start = jitMapping.Vaddr
1272+
r.procInfo.Jit_end = jitMapping.Vaddr + jitMapping.Length
1273+
if err := ebpf.UpdateProcData(libpf.Ruby, pr.PID(), unsafe.Pointer(r.procInfo)); err != nil {
1274+
return err
1275+
}
1276+
log.Debugf("Added jit mapping %08x ruby proc info, %08x", r.procInfo.Jit_start, r.procInfo.Jit_end)
1277+
}
1278+
// Remove prefixes not seen
1279+
for prefix, generationPtr := range r.prefixes {
1280+
if *generationPtr == r.mappingGeneration {
1281+
continue
1282+
}
1283+
log.Debugf("Delete Ruby prefix %#v", prefix)
1284+
_ = ebpf.DeletePidInterpreterMapping(pid, prefix)
1285+
delete(r.prefixes, prefix)
1286+
}
1287+
for m, generationPtr := range r.mappings {
1288+
if *generationPtr == r.mappingGeneration {
1289+
continue
1290+
}
1291+
log.Debugf("Disabling Ruby for %#x/%#x", m.Vaddr, m.Length)
1292+
delete(r.mappings, m)
1293+
}
1294+
1295+
return nil
1296+
}
1297+
11851298
func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) {
11861299
addrToStringStats := r.addrToString.ResetMetrics()
11871300

support/ebpf/frametypes.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,5 @@
5454
#define RUBY_FRAME_TYPE_CME_CFUNC 2
5555
#define RUBY_FRAME_TYPE_ISEQ 3
5656
#define RUBY_FRAME_TYPE_GC 4
57+
#define RUBY_FRAME_TYPE_JIT 5
5758
#endif

support/ebpf/ruby_tracer.ebpf.c

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,10 @@ static EBPF_INLINE ErrorCode read_ruby_frame(
270270
// continue unwinding Ruby VM frames. Due to this issue, the ordering of Ruby and native
271271
// frames will almost certainly be incorrect for Ruby versions < 2.6.
272272
frame_type = RUBY_FRAME_TYPE_CME_CFUNC;
273+
} else if (record->rubyUnwindState.jit_detected) {
274+
// If we detected a jit frame and are now in a cfunc, push the c frame
275+
// as we can no longer unwind native anymore
276+
frame_type = RUBY_FRAME_TYPE_CME_CFUNC;
273277
} else {
274278
// We save this cfp on in the "Record" entry, and when we start the unwinder
275279
// again we'll push it so that the order is correct and the cfunc "owns" any native code we
@@ -446,14 +450,34 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
446450
record->rubyUnwindState.cfunc_saved_frame = 0;
447451
}
448452

453+
if (
454+
rubyinfo->jit_start > 0 && record->state.pc > rubyinfo->jit_start &&
455+
record->state.pc < rubyinfo->jit_end) {
456+
record->rubyUnwindState.jit_detected = true;
457+
458+
// If the first frame is a jit PC, the leaf ruby frame should be the jit "owner"
459+
// the cpu PC is also pushed as the address,
460+
// as in theory this can be used to symbolize the JIT frame later
461+
if (trace->num_frames == 0) {
462+
ErrorCode error =
463+
push_ruby(&record->state, trace, RUBY_FRAME_TYPE_JIT, (u64)record->state.pc, 0, 0);
464+
if (error) {
465+
return error;
466+
}
467+
}
468+
}
469+
449470
for (u32 i = 0; i < FRAMES_PER_WALK_RUBY_STACK; ++i) {
450471
error = read_ruby_frame(record, rubyinfo, stack_ptr, next_unwinder);
451472
if (error != ERR_OK)
452473
return error;
453474

454475
if (last_stack_frame <= stack_ptr) {
455476
// We have processed all frames in the Ruby VM and can stop here.
456-
*next_unwinder = PROG_UNWIND_NATIVE;
477+
// if this process has been JIT'd, the PC is invalid and we cannot resume native unwinding so
478+
// we are done
479+
*next_unwinder = record->rubyUnwindState.jit_detected ? PROG_UNWIND_STOP : PROG_UNWIND_NATIVE;
480+
goto save_state;
457481
} else {
458482
// If we aren't at the end, advance the stack pointer to continue from the next frame
459483
stack_ptr += rubyinfo->size_of_control_frame_struct;

support/ebpf/tracemgmt.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ static inline EBPF_INLINE PerCPURecord *get_pristine_per_cpu_record()
231231
record->rubyUnwindState.stack_ptr = 0;
232232
record->rubyUnwindState.last_stack_frame = 0;
233233
record->rubyUnwindState.cfunc_saved_frame = 0;
234+
record->rubyUnwindState.jit_detected = false;
234235
record->unwindersDone = 0;
235236
record->tailCalls = 0;
236237
record->ratelimitAction = RATELIMIT_ACTION_DEFAULT;

support/ebpf/types.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,9 @@ typedef struct RubyProcInfo {
476476

477477
// is reading gc state from objspace supported for this version?
478478
bool has_objspace;
479+
480+
// JIT regions, for detecting if a native PC was JIT
481+
u64 jit_start, jit_end;
479482
// Offsets and sizes of Ruby internal structs
480483

481484
// rb_execution_context_struct offsets:
@@ -715,6 +718,8 @@ typedef struct RubyUnwindState {
715718
void *last_stack_frame;
716719
// Frame for last cfunc before we switched to native unwinder
717720
u64 cfunc_saved_frame;
721+
// Detect if JIT code ran in the process (at any time)
722+
bool jit_detected;
718723
} RubyUnwindState;
719724

720725
// 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
@@ -210,6 +210,7 @@ const (
210210
RubyFrameTypeCmeCfunc = C.RUBY_FRAME_TYPE_CME_CFUNC
211211
RubyFrameTypeIseq = C.RUBY_FRAME_TYPE_ISEQ
212212
RubyFrameTypeGc = C.RUBY_FRAME_TYPE_GC
213+
RubyFrameTypeJit = C.RUBY_FRAME_TYPE_JIT
213214
)
214215

215216
var MetricsTranslation = []metrics.MetricID{

0 commit comments

Comments
 (0)