Skip to content

Commit adfff81

Browse files
committed
Handle JIT frames
1 parent ab9c0fc commit adfff81

7 files changed

Lines changed: 154 additions & 9 deletions

File tree

interpreter/ruby/ruby.go

Lines changed: 118 additions & 8 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"
@@ -104,14 +107,16 @@ var (
104107
// regex to extract a version from a string
105108
rubyVersionRegex = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
106109

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>")
110+
unknownCfunc = libpf.Intern("<unknown cfunc>")
111+
cfuncDummyFile = libpf.Intern("<cfunc>")
112+
rubyGcFrame = libpf.Intern("(garbage collection)")
113+
rubyGcRunning = libpf.Intern("(running)")
114+
rubyGcMarking = libpf.Intern("(marking)")
115+
rubyGcSweeping = libpf.Intern("(sweeping)")
116+
rubyGcCompacting = libpf.Intern("(compacting)")
117+
rubyGcDummyFile = libpf.Intern("<gc>")
118+
rubyJitDummyFrame = libpf.Intern("<unknown jit code>")
119+
rubyJitDummyFile = libpf.Intern("<jitted code>")
115120
// compiler check to make sure the needed interfaces are satisfied
116121
_ interpreter.Data = &rubyData{}
117122
_ interpreter.Instance = &rubyInstance{}
@@ -376,6 +381,8 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp
376381
procInfo: &cdata,
377382
globalSymbolsAddr: r.globalSymbolsAddr + bias,
378383
addrToString: addrToString,
384+
mappings: make(map[process.RawMapping]*uint32),
385+
prefixes: make(map[lpm.Prefix]*uint32),
379386
memPool: sync.Pool{
380387
New: func() any {
381388
buf := make([]byte, 512)
@@ -425,6 +432,7 @@ type rubyInstance struct {
425432

426433
// lastId is a cached copy index of the final entry in the global symbol table
427434
lastId uint32
435+
428436
// globalSymbolsAddr is the offset of the global symbol table, for looking up ruby symbolic ids
429437
globalSymbolsAddr libpf.Address
430438

@@ -437,6 +445,13 @@ type rubyInstance struct {
437445
// maxSize is the largest number we did see in the last reporting interval for size
438446
// in getRubyLineNo.
439447
maxSize atomic.Uint32
448+
449+
// mappings is indexed by the Mapping to its generation
450+
mappings map[process.RawMapping]*uint32
451+
// prefixes is indexed by the prefix added to ebpf maps (to be cleaned up) to its generation
452+
prefixes map[lpm.Prefix]*uint32
453+
// mappingGeneration is the current generation (so old entries can be pruned)
454+
mappingGeneration uint32
440455
}
441456

442457
func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error {
@@ -1115,6 +1130,15 @@ func (r *rubyInstance) Symbolize(ef libpf.EbpfFrame, frames *libpf.Frames, _ lib
11151130
SourceLine: 0,
11161131
})
11171132
return nil
1133+
case support.RubyFrameTypeJit:
1134+
label := rubyJitDummyFrame
1135+
frames.Append(&libpf.Frame{
1136+
Type: libpf.RubyFrame,
1137+
FunctionName: label,
1138+
SourceFile: rubyJitDummyFile,
1139+
SourceLine: 0,
1140+
})
1141+
return nil
11181142
default:
11191143
return fmt.Errorf("Unable to get CME or ISEQ from frame address (%d)", frameAddrType)
11201144
}
@@ -1244,6 +1268,92 @@ func profileFrameFullLabel(classPath, label, baseLabel, methodName libpf.String,
12441268
return libpf.Intern(profileLabel)
12451269
}
12461270

1271+
func (r *rubyInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler,
1272+
_ reporter.ExecutableReporter, pr process.Process, mappings []process.RawMapping) error {
1273+
var jitMapping *process.RawMapping
1274+
1275+
pid := pr.PID()
1276+
jitFound := false
1277+
r.mappingGeneration++
1278+
1279+
log.Debugf("Synchronizing ruby mappings")
1280+
1281+
for idx := range mappings {
1282+
m := &mappings[idx]
1283+
if !m.IsExecutable() || !m.IsAnonymous() {
1284+
continue
1285+
}
1286+
// If prctl is allowed, ruby should label the memory region
1287+
// always prefer that
1288+
if strings.Contains(m.Path, "jit_reserve_addr_space") {
1289+
jitMapping = m
1290+
jitFound = true
1291+
}
1292+
// Use the first executable anon region we find if it isn't labeled
1293+
// If we find more, prefer ones earlier in memory or larger in size
1294+
if !jitFound && (jitMapping == nil || m.Vaddr < jitMapping.Vaddr || m.Length > jitMapping.Length) {
1295+
// Don't set jitFound here as it is a heuristic, we aren't sure
1296+
// could be on a system without linux config flag to allow prctl to label memoy
1297+
jitMapping = m
1298+
}
1299+
1300+
if _, exists := r.mappings[*m]; exists {
1301+
*r.mappings[*m] = r.mappingGeneration
1302+
continue
1303+
}
1304+
1305+
// Generate a new uint32 pointer which is shared for mapping and the prefixes it owns
1306+
// so updating the mapping above will reflect to prefixes also.
1307+
mappingGeneration := r.mappingGeneration
1308+
r.mappings[*m] = &mappingGeneration
1309+
1310+
// Just assume all anonymous and executable mappings are Ruby for now
1311+
log.Debugf("Enabling Ruby interpreter for %#x/%#x", m.Vaddr, m.Length)
1312+
1313+
prefixes, err := lpm.CalculatePrefixList(m.Vaddr, m.Vaddr+m.Length)
1314+
if err != nil {
1315+
return fmt.Errorf("new anonymous mapping lpm failure %#x/%#x", m.Vaddr, m.Length)
1316+
}
1317+
1318+
for _, prefix := range prefixes {
1319+
_, exists := r.prefixes[prefix]
1320+
if !exists {
1321+
err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindRuby, 0, 0)
1322+
if err != nil {
1323+
return err
1324+
}
1325+
}
1326+
r.prefixes[prefix] = &mappingGeneration
1327+
}
1328+
}
1329+
if jitMapping != nil && (r.procInfo.Jit_start != jitMapping.Vaddr || r.procInfo.Jit_end != jitMapping.Vaddr+jitMapping.Length) {
1330+
r.procInfo.Jit_start = jitMapping.Vaddr
1331+
r.procInfo.Jit_end = jitMapping.Vaddr + jitMapping.Length
1332+
if err := ebpf.UpdateProcData(libpf.Ruby, pr.PID(), unsafe.Pointer(r.procInfo)); err != nil {
1333+
return err
1334+
}
1335+
log.Debugf("Added jit mapping %08x ruby proc info, %08x", r.procInfo.Jit_start, r.procInfo.Jit_end)
1336+
}
1337+
// Remove prefixes not seen
1338+
for prefix, generationPtr := range r.prefixes {
1339+
if *generationPtr == r.mappingGeneration {
1340+
continue
1341+
}
1342+
log.Debugf("Delete Ruby prefix %#v", prefix)
1343+
_ = ebpf.DeletePidInterpreterMapping(pid, prefix)
1344+
delete(r.prefixes, prefix)
1345+
}
1346+
for m, generationPtr := range r.mappings {
1347+
if *generationPtr == r.mappingGeneration {
1348+
continue
1349+
}
1350+
log.Debugf("Disabling Ruby for %#x/%#x", m.Vaddr, m.Length)
1351+
delete(r.mappings, m)
1352+
}
1353+
1354+
return nil
1355+
}
1356+
12471357
func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) {
12481358
addrToStringStats := r.addrToString.ResetMetrics()
12491359

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
@@ -238,6 +238,7 @@ static inline EBPF_INLINE PerCPURecord *get_pristine_per_cpu_record()
238238
record->rubyUnwindState.stack_ptr = 0;
239239
record->rubyUnwindState.last_stack_frame = 0;
240240
record->rubyUnwindState.cfunc_saved_frame = 0;
241+
record->rubyUnwindState.jit_detected = false;
241242
record->unwindersDone = 0;
242243
record->tailCalls = 0;
243244
record->ratelimitAction = RATELIMIT_ACTION_DEFAULT;

support/ebpf/types.h

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

489489
// is reading gc state from objspace supported for this version?
490490
bool has_objspace;
491+
492+
// JIT regions, for detecting if a native PC was JIT
493+
u64 jit_start, jit_end;
491494
// Offsets and sizes of Ruby internal structs
492495

493496
// rb_execution_context_struct offsets:
@@ -734,6 +737,8 @@ typedef struct RubyUnwindState {
734737
void *last_stack_frame;
735738
// Frame for last cfunc before we switched to native unwinder
736739
u64 cfunc_saved_frame;
740+
// Detect if JIT code ran in the process (at any time)
741+
bool jit_detected;
737742
} RubyUnwindState;
738743

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

support/types.go

Lines changed: 3 additions & 0 deletions
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
@@ -205,6 +205,7 @@ const (
205205
RubyFrameTypeCmeCfunc = C.RUBY_FRAME_TYPE_CME_CFUNC
206206
RubyFrameTypeIseq = C.RUBY_FRAME_TYPE_ISEQ
207207
RubyFrameTypeGc = C.RUBY_FRAME_TYPE_GC
208+
RubyFrameTypeJit = C.RUBY_FRAME_TYPE_JIT
208209
)
209210

210211
var MetricsTranslation = []metrics.MetricID{

0 commit comments

Comments
 (0)