Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 121 additions & 8 deletions interpreter/ruby/ruby.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ import (
"go.opentelemetry.io/ebpf-profiler/libpf"
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
"go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe"
"go.opentelemetry.io/ebpf-profiler/lpm"
"go.opentelemetry.io/ebpf-profiler/metrics"
npsr "go.opentelemetry.io/ebpf-profiler/nopanicslicereader"
"go.opentelemetry.io/ebpf-profiler/process"
"go.opentelemetry.io/ebpf-profiler/remotememory"
"go.opentelemetry.io/ebpf-profiler/reporter"
"go.opentelemetry.io/ebpf-profiler/successfailurecounter"
"go.opentelemetry.io/ebpf-profiler/support"
"go.opentelemetry.io/ebpf-profiler/util"
Expand Down Expand Up @@ -101,14 +104,16 @@ var (
// regex to extract a version from a string
rubyVersionRegex = regexp.MustCompile(`^(\d)\.(\d)\.(\d)$`)

unknownCfunc = libpf.Intern("<unknown cfunc>")
cfuncDummyFile = libpf.Intern("<cfunc>")
rubyGcFrame = libpf.Intern("(garbage collection)")
rubyGcRunning = libpf.Intern("(running)")
rubyGcMarking = libpf.Intern("(marking)")
rubyGcSweeping = libpf.Intern("(sweeping)")
rubyGcCompacting = libpf.Intern("(compacting)")
rubyGcDummyFile = libpf.Intern("<gc>")
unknownCfunc = libpf.Intern("<unknown cfunc>")
cfuncDummyFile = libpf.Intern("<cfunc>")
rubyGcFrame = libpf.Intern("(garbage collection)")
rubyGcRunning = libpf.Intern("(running)")
rubyGcMarking = libpf.Intern("(marking)")
rubyGcSweeping = libpf.Intern("(sweeping)")
rubyGcCompacting = libpf.Intern("(compacting)")
rubyGcDummyFile = libpf.Intern("<gc>")
rubyJitDummyFrame = libpf.Intern("<unknown jit code>")
rubyJitDummyFile = libpf.Intern("<jitted code>")
// compiler check to make sure the needed interfaces are satisfied
_ interpreter.Data = &rubyData{}
_ interpreter.Instance = &rubyInstance{}
Expand Down Expand Up @@ -345,8 +350,11 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp
return &rubyInstance{
r: r,
rm: rm,
procInfo: &cdata,
globalSymbolsAddr: r.globalSymbolsAddr + bias,
addrToString: addrToString,
mappings: make(map[process.Mapping]*uint32),
prefixes: make(map[lpm.Prefix]*uint32),
memPool: sync.Pool{
New: func() any {
buf := make([]byte, 512)
Expand Down Expand Up @@ -390,6 +398,9 @@ type rubyInstance struct {

// lastId is a cached copy index of the final entry in the global symbol table
lastId uint32
// Store the procinfo so we can update it if mappings are updated
procInfo *support.RubyProcInfo

// globalSymbolsAddr is the offset of the global symbol table, for looking up ruby symbolic ids
globalSymbolsAddr libpf.Address

Expand All @@ -402,6 +413,13 @@ type rubyInstance struct {
// maxSize is the largest number we did see in the last reporting interval for size
// in getRubyLineNo.
maxSize atomic.Uint32

// mappings is indexed by the Mapping to its generation
mappings map[process.Mapping]*uint32
// prefixes is indexed by the prefix added to ebpf maps (to be cleaned up) to its generation
prefixes map[lpm.Prefix]*uint32
// mappingGeneration is the current generation (so old entries can be pruned)
mappingGeneration uint32
}

func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error {
Expand Down Expand Up @@ -1053,6 +1071,15 @@ func (r *rubyInstance) Symbolize(ef libpf.EbpfFrame, frames *libpf.Frames, _ lib
SourceLine: 0,
})
return nil
case support.RubyFrameTypeJit:
label := rubyJitDummyFrame
frames.Append(&libpf.Frame{
Type: libpf.RubyFrame,
FunctionName: label,
SourceFile: rubyJitDummyFile,
SourceLine: 0,
})
return nil
default:
return fmt.Errorf("Unable to get CME or ISEQ from frame address (%d)", frameAddrType)
}
Expand Down Expand Up @@ -1185,6 +1212,92 @@ func profileFrameFullLabel(classPath, label, baseLabel, methodName libpf.String,
return libpf.Intern(profileLabel)
}

func (r *rubyInstance) SynchronizeMappings(ebpf interpreter.EbpfHandler,
_ reporter.ExecutableReporter, pr process.Process, mappings []process.Mapping) error {
var jitMapping *process.Mapping

pid := pr.PID()
jitFound := false
r.mappingGeneration++

log.Debugf("Synchronizing ruby mappings")

for idx := range mappings {
m := &mappings[idx]
if !m.IsExecutable() || !m.IsAnonymous() {
continue
}
// If prctl is allowed, ruby should label the memory region
// always prefer that
if strings.Contains(m.Path.String(), "jit_reserve_addr_space") {
jitMapping = m
jitFound = true
}
// Use the first executable anon region we find if it isn't labeled
// If we find more, prefer ones earlier in memory or larger in size
if !jitFound && (jitMapping == nil || m.Vaddr < jitMapping.Vaddr || m.Length > jitMapping.Length) {
// Don't set jitFound here as it is a heuristic, we aren't sure
// could be on a system without linux config flag to allow prctl to label memoy
jitMapping = m
}

if _, exists := r.mappings[*m]; exists {
*r.mappings[*m] = r.mappingGeneration
continue
}

// Generate a new uint32 pointer which is shared for mapping and the prefixes it owns
// so updating the mapping above will reflect to prefixes also.
mappingGeneration := r.mappingGeneration
r.mappings[*m] = &mappingGeneration

// Just assume all anonymous and executable mappings are Ruby for now
log.Debugf("Enabling Ruby interpreter for %#x/%#x", m.Vaddr, m.Length)

prefixes, err := lpm.CalculatePrefixList(m.Vaddr, m.Vaddr+m.Length)
if err != nil {
return fmt.Errorf("new anonymous mapping lpm failure %#x/%#x", m.Vaddr, m.Length)
}

for _, prefix := range prefixes {
_, exists := r.prefixes[prefix]
if !exists {
err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindRuby, 0, 0)
if err != nil {
return err
}
}
r.prefixes[prefix] = &mappingGeneration
}
}
if jitMapping != nil && (r.procInfo.Jit_start != jitMapping.Vaddr || r.procInfo.Jit_end != jitMapping.Vaddr+jitMapping.Length) {
r.procInfo.Jit_start = jitMapping.Vaddr
r.procInfo.Jit_end = jitMapping.Vaddr + jitMapping.Length
if err := ebpf.UpdateProcData(libpf.Ruby, pr.PID(), unsafe.Pointer(r.procInfo)); err != nil {
return err
}
log.Debugf("Added jit mapping %08x ruby proc info, %08x", r.procInfo.Jit_start, r.procInfo.Jit_end)
}
// Remove prefixes not seen
for prefix, generationPtr := range r.prefixes {
if *generationPtr == r.mappingGeneration {
continue
}
log.Debugf("Delete Ruby prefix %#v", prefix)
_ = ebpf.DeletePidInterpreterMapping(pid, prefix)
delete(r.prefixes, prefix)
}
for m, generationPtr := range r.mappings {
if *generationPtr == r.mappingGeneration {
continue
}
log.Debugf("Disabling Ruby for %#x/%#x", m.Vaddr, m.Length)
delete(r.mappings, m)
}

return nil
}

func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) {
addrToStringStats := r.addrToString.ResetMetrics()

Expand Down
1 change: 1 addition & 0 deletions support/ebpf/frametypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@
#define RUBY_FRAME_TYPE_CME_CFUNC 2
#define RUBY_FRAME_TYPE_ISEQ 3
#define RUBY_FRAME_TYPE_GC 4
#define RUBY_FRAME_TYPE_JIT 5
#endif
26 changes: 25 additions & 1 deletion support/ebpf/ruby_tracer.ebpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@ static EBPF_INLINE ErrorCode read_ruby_frame(
// continue unwinding Ruby VM frames. Due to this issue, the ordering of Ruby and native
// frames will almost certainly be incorrect for Ruby versions < 2.6.
frame_type = RUBY_FRAME_TYPE_CME_CFUNC;
} else if (record->rubyUnwindState.jit_detected) {
// If we detected a jit frame and are now in a cfunc, push the c frame
// as we can no longer unwind native anymore
frame_type = RUBY_FRAME_TYPE_CME_CFUNC;
} else {
// We save this cfp on in the "Record" entry, and when we start the unwinder
// again we'll push it so that the order is correct and the cfunc "owns" any native code we
Expand Down Expand Up @@ -447,6 +451,23 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(
record->rubyUnwindState.cfunc_saved_frame = 0;
}

if (
rubyinfo->jit_start > 0 && record->state.pc > rubyinfo->jit_start &&
record->state.pc < rubyinfo->jit_end) {
record->rubyUnwindState.jit_detected = true;

// If the first frame is a jit PC, the leaf ruby frame should be the jit "owner"
// the cpu PC is also pushed as the address,
// as in theory this can be used to symbolize the JIT frame later
if (trace->num_frames == 0) {
ErrorCode error =
push_ruby(&record->state, trace, RUBY_FRAME_TYPE_JIT, (u64)record->state.pc, 0, 0);
if (error) {
return error;
}
}
}

UNROLL for (u32 i = 0; i < FRAMES_PER_WALK_RUBY_STACK; ++i)
{
error = read_ruby_frame(record, rubyinfo, stack_ptr, next_unwinder);
Expand All @@ -455,7 +476,10 @@ static EBPF_INLINE ErrorCode walk_ruby_stack(

if (last_stack_frame <= stack_ptr) {
// We have processed all frames in the Ruby VM and can stop here.
*next_unwinder = PROG_UNWIND_NATIVE;
// if this process has been JIT'd, the PC is invalid and we cannot resume native unwinding so
// we are done
*next_unwinder = record->rubyUnwindState.jit_detected ? PROG_UNWIND_STOP : PROG_UNWIND_NATIVE;
goto save_state;
} else {
// If we aren't at the end, advance the stack pointer to continue from the next frame
stack_ptr += rubyinfo->size_of_control_frame_struct;
Expand Down
1 change: 1 addition & 0 deletions support/ebpf/tracemgmt.h
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ static inline EBPF_INLINE PerCPURecord *get_pristine_per_cpu_record()
record->rubyUnwindState.stack_ptr = 0;
record->rubyUnwindState.last_stack_frame = 0;
record->rubyUnwindState.cfunc_saved_frame = 0;
record->rubyUnwindState.jit_detected = false;
record->unwindersDone = 0;
record->tailCalls = 0;
record->ratelimitAction = RATELIMIT_ACTION_DEFAULT;
Expand Down
Binary file modified support/ebpf/tracer.ebpf.amd64
Binary file not shown.
Binary file modified support/ebpf/tracer.ebpf.arm64
Binary file not shown.
5 changes: 5 additions & 0 deletions support/ebpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,9 @@ typedef struct RubyProcInfo {

// is reading gc state from objspace supported for this version?
bool has_objspace;

// JIT regions, for detecting if a native PC was JIT
u64 jit_start, jit_end;
// Offsets and sizes of Ruby internal structs

// rb_execution_context_struct offsets:
Expand Down Expand Up @@ -703,6 +706,8 @@ typedef struct RubyUnwindState {
void *last_stack_frame;
// Frame for last cfunc before we switched to native unwinder
u64 cfunc_saved_frame;
// Detect if JIT code ran in the process (at any time)
bool jit_detected;
} RubyUnwindState;

// Container for additional scratch space needed by the HotSpot unwinder.
Expand Down
5 changes: 4 additions & 1 deletion support/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions support/types_def.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ const (
RubyFrameTypeCmeCfunc = C.RUBY_FRAME_TYPE_CME_CFUNC
RubyFrameTypeIseq = C.RUBY_FRAME_TYPE_ISEQ
RubyFrameTypeGc = C.RUBY_FRAME_TYPE_GC
RubyFrameTypeJit = C.RUBY_FRAME_TYPE_JIT
)

var MetricsTranslation = []metrics.MetricID{
Expand Down
Loading