@@ -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
407425func (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+
11851298func (r * rubyInstance ) GetAndResetMetrics () ([]metrics.Metric , error ) {
11861299 addrToStringStats := r .addrToString .ResetMetrics ()
11871300
0 commit comments