diff --git a/go.mod b/go.mod index b4d732604..e00da1a6d 100644 --- a/go.mod +++ b/go.mod @@ -25,15 +25,15 @@ require ( github.com/peterbourgon/ff/v3 v3.4.0 github.com/stretchr/testify v1.11.1 github.com/zeebo/xxh3 v1.1.0 - go.opentelemetry.io/collector/component v1.53.0 - go.opentelemetry.io/collector/confmap/xconfmap v0.147.0 - go.opentelemetry.io/collector/consumer/consumertest v0.147.0 - go.opentelemetry.io/collector/consumer/xconsumer v0.147.0 - go.opentelemetry.io/collector/pdata v1.53.0 - go.opentelemetry.io/collector/pdata/pprofile v0.147.0 - go.opentelemetry.io/collector/receiver v1.53.0 - go.opentelemetry.io/collector/receiver/receivertest v0.147.0 - go.opentelemetry.io/collector/receiver/xreceiver v0.147.0 + go.opentelemetry.io/collector/component v1.51.0 + go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 + go.opentelemetry.io/collector/consumer/consumertest v0.145.0 + go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 + go.opentelemetry.io/collector/pdata v1.51.0 + go.opentelemetry.io/collector/pdata/pprofile v0.145.0 + go.opentelemetry.io/collector/receiver v1.51.0 + go.opentelemetry.io/collector/receiver/receivertest v0.145.0 + go.opentelemetry.io/collector/receiver/xreceiver v0.145.0 go.opentelemetry.io/otel v1.42.0 go.opentelemetry.io/otel/metric v1.42.0 go.opentelemetry.io/proto/otlp/profiles/v1development v0.3.0 @@ -87,15 +87,15 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/collector/component/componenttest v0.147.0 // indirect - go.opentelemetry.io/collector/confmap v1.53.0 // indirect - go.opentelemetry.io/collector/consumer v1.53.0 // indirect - go.opentelemetry.io/collector/consumer/consumererror v0.147.0 // indirect - go.opentelemetry.io/collector/featuregate v1.53.0 // indirect - go.opentelemetry.io/collector/internal/componentalias v0.147.0 // indirect - go.opentelemetry.io/collector/pipeline v1.53.0 // indirect - go.opentelemetry.io/otel/sdk v1.40.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/collector/component/componenttest v0.145.0 // indirect + go.opentelemetry.io/collector/confmap v1.51.0 // indirect + go.opentelemetry.io/collector/consumer v1.51.0 // indirect + go.opentelemetry.io/collector/consumer/consumererror v0.145.0 // indirect + go.opentelemetry.io/collector/featuregate v1.51.0 // indirect + go.opentelemetry.io/collector/internal/componentalias v0.145.0 // indirect + go.opentelemetry.io/collector/pipeline v1.51.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 2e182f5d9..4dc2fc57d 100644 --- a/go.sum +++ b/go.sum @@ -134,50 +134,50 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/collector/component v1.53.0 h1:A+GU9n4eKnFVmrr7NPpbVvJ1kp985jXtachb9gy12mk= -go.opentelemetry.io/collector/component v1.53.0/go.mod h1:yqyFwDuP4JKwOFaxdqoWj25aVthtavGkSDp2K42x+YY= -go.opentelemetry.io/collector/component/componenttest v0.147.0 h1:9XTwUT87gFWScoP29GEyMjKjr0jycVon6u/EVLrw08w= -go.opentelemetry.io/collector/component/componenttest v0.147.0/go.mod h1:ph5UnCbKUeX3xBg9eSdueRnGmNB4DmhQ0KC6lTsGYTs= -go.opentelemetry.io/collector/confmap v1.53.0 h1:gp5CDXNv2Bg+Ytr3A+ZiaVg9SfNiZKbxLUo6ogfyVVE= -go.opentelemetry.io/collector/confmap v1.53.0/go.mod h1:Abi0meDEJeUNlHF2uw2whtuH10TyW2pkqH547sgmRTc= -go.opentelemetry.io/collector/confmap/xconfmap v0.147.0 h1:4FWhq/szzeYEJLLMXWsLMY5b1qYy83M7rbEBUJCHnUY= -go.opentelemetry.io/collector/confmap/xconfmap v0.147.0/go.mod h1:EHgZFJzZU88Y9A+NlKCn9EwrVHEzASEtCsHw3kv+jgI= -go.opentelemetry.io/collector/consumer v1.53.0 h1:Gyy80dX5r1Lv9lvQk8XFtUkWs1eniicOzzCQBejLseg= -go.opentelemetry.io/collector/consumer v1.53.0/go.mod h1:f5U6ibd+XpC5eOSeEYhERAQJ2a5bp1d2RzW3MFddMDM= -go.opentelemetry.io/collector/consumer/consumererror v0.147.0 h1:c4jjAEke6AEqoxalOAIEudGuN4rnnheaLWdpJXPCAPQ= -go.opentelemetry.io/collector/consumer/consumererror v0.147.0/go.mod h1:9MwE9k6xHd3TGBSAeKSmt42dwWyxwUhYqfwPUx1ZQJY= -go.opentelemetry.io/collector/consumer/consumertest v0.147.0 h1:AU3sUm2L3pezrg6hzPJAO19ZANQoCcfgbyanN0q360g= -go.opentelemetry.io/collector/consumer/consumertest v0.147.0/go.mod h1:QWGFRmeYNbKaseDTNT3a2iGDmjl+DCZnLzMP7Rjj0JM= -go.opentelemetry.io/collector/consumer/xconsumer v0.147.0 h1:XJVQc2dYyalaFXMTa4/RE+aweQTiBpw1edfwdCIJSxw= -go.opentelemetry.io/collector/consumer/xconsumer v0.147.0/go.mod h1:mtwh1VsUoGjxwdmXEzjbswH7KAGByJNCIMHmhqwXeK0= -go.opentelemetry.io/collector/featuregate v1.53.0 h1:cgjXdtl7jezWxq6V0eohe/JqjY4PBotZGb5+bTR2OJw= -go.opentelemetry.io/collector/featuregate v1.53.0/go.mod h1:PS7zY/zaCb28EqciePVwRHVhc3oKortTFXsi3I6ee4g= -go.opentelemetry.io/collector/internal/componentalias v0.147.0 h1:cC1gEQwzQnDvbELVjE3FXqgBkrsUl5JhzOT+6hISaLI= -go.opentelemetry.io/collector/internal/componentalias v0.147.0/go.mod h1:RxuMjMy1j+2jZcY1Ej0E+NC6DnoqTMEvIwRiXtk82rc= -go.opentelemetry.io/collector/internal/testutil v0.147.0 h1:DFlRxBRp23/sZnpTITK25yqe0d56yNvK+63IaWc6OsU= -go.opentelemetry.io/collector/internal/testutil v0.147.0/go.mod h1:Jkjs6rkqs973LqgZ0Fe3zrokQRKULYXPIf4HuqStiEE= -go.opentelemetry.io/collector/pdata v1.53.0 h1:DlYDbRwammEZaxDZHINx5v0n8SEOVNniPbi6FRTlVkA= -go.opentelemetry.io/collector/pdata v1.53.0/go.mod h1:LRSYGNjKXaUrZEwZv3Yl+8/zV2HmRGKXW62zB2bysms= -go.opentelemetry.io/collector/pdata/pprofile v0.147.0 h1:yQS3RBvcvRcy9N7AnJvsxmse0AxJcRqBZfwMA22xBA8= -go.opentelemetry.io/collector/pdata/pprofile v0.147.0/go.mod h1:pm9mUqHNpT1SaCkxILu4FW1BvMAelh7EKhpSKe2KJIQ= -go.opentelemetry.io/collector/pdata/testdata v0.147.0 h1:fZB5jY5F+zC/oeGYBa92IknhPQIlLSwoxDUMzhrpTP4= -go.opentelemetry.io/collector/pdata/testdata v0.147.0/go.mod h1:+AB6qTXrYEBvqrv394SEXzuWxtL9LLrnVgIjYpP9HHU= -go.opentelemetry.io/collector/pipeline v1.53.0 h1:+RrNuAmHnzldGOzCCYLJv0qTFoi9QJGrLm+MEYMozmo= -go.opentelemetry.io/collector/pipeline v1.53.0/go.mod h1:RD90NG3Jbk965Xaqym3JyHkuol4uZJjQVUkD9ddXJIs= -go.opentelemetry.io/collector/receiver v1.53.0 h1:FACspX7EMj91g8OY3twlJKzw2LKj0g5wZAXT4Ys2XRU= -go.opentelemetry.io/collector/receiver v1.53.0/go.mod h1:rhBr1+X3N9ijDBBKrVCiRMfVTUlOSWj+Gj0A6qevmoA= -go.opentelemetry.io/collector/receiver/receivertest v0.147.0 h1:t+AqCUJT0ivO1eE09f8gIqnO73UeEFqjvL/annt6rWg= -go.opentelemetry.io/collector/receiver/receivertest v0.147.0/go.mod h1:8kZCwsG8KNpWRf+2izpoY8iIOyfC2cQ2CLSZc9LgOP0= -go.opentelemetry.io/collector/receiver/xreceiver v0.147.0 h1:/KAxTban2sQhiksAu/EG+ri0mNgSxldhJ4lj/XGT+xQ= -go.opentelemetry.io/collector/receiver/xreceiver v0.147.0/go.mod h1:DCjNMipiIv59Jc/YfWFxAvgonurJET9cw3D79U1yLMc= +go.opentelemetry.io/collector/component v1.51.0 h1:btNW76MCRmpsk0ARRT5wspDXF9tvdaLd3uBtYXIiQn0= +go.opentelemetry.io/collector/component v1.51.0/go.mod h1:Zlgwh4yTLDhJglOXqiyXZ7paepTvvoijfFjLqOr/Qww= +go.opentelemetry.io/collector/component/componenttest v0.145.0 h1:ryhRrXqQybGMhz7A7t32NC8BXAFcX2o1RetgPM7vw88= +go.opentelemetry.io/collector/component/componenttest v0.145.0/go.mod h1:5uStrhUdZ0Fw3se00CPmVaRtW8o9N8kKiY76OSCWFjQ= +go.opentelemetry.io/collector/confmap v1.51.0 h1:C9YlMNkIgzuauLpUz2F7DLlWwqAmkQKNcKj1XATVWuE= +go.opentelemetry.io/collector/confmap v1.51.0/go.mod h1:uWi4b9lHfvEC2poJ2I2vXwGUREVEQTcdUguOpfqdcHM= +go.opentelemetry.io/collector/confmap/xconfmap v0.145.0 h1:ngbyfh4+SKlA+osgsak3AxUNPxVxaJTmA0Sl7VfJzwY= +go.opentelemetry.io/collector/confmap/xconfmap v0.145.0/go.mod h1:zTSK+c76NAy/tI1R3xfZjdoI04D9EYDnzAHQQwl6AmA= +go.opentelemetry.io/collector/consumer v1.51.0 h1:Ex1x/k9VEEA2DOgt/eSc2Z9KTp0I6xBSruLmrYFfIFY= +go.opentelemetry.io/collector/consumer v1.51.0/go.mod h1:Erk6qdfVj+24QTrGCpurcrF+qdUlHkb4dgMy5wJxLvY= +go.opentelemetry.io/collector/consumer/consumererror v0.145.0 h1:UtcJ0mH9D7R9sexzSGOg8VpZ+m2N93owyEnReraB8UQ= +go.opentelemetry.io/collector/consumer/consumererror v0.145.0/go.mod h1:ivpHl1CQ4xlub5NnyIOLXVwsE4p9YSR3h+47g5yiha4= +go.opentelemetry.io/collector/consumer/consumertest v0.145.0 h1:3+uMwuMHoXMAU+Z6mwCRA3AxWeL7SujcAQwqqHJ1gCc= +go.opentelemetry.io/collector/consumer/consumertest v0.145.0/go.mod h1:IFc/FeaIHQClb8KK0aVn0tFDNMc+/MmfQ+aBT1cJNeo= +go.opentelemetry.io/collector/consumer/xconsumer v0.145.0 h1:9w7KKv9lVJoHvMLC6SUJHenU/KySdEgFJXbB4JQOEsk= +go.opentelemetry.io/collector/consumer/xconsumer v0.145.0/go.mod h1:SryDCLP2ZaFeZJtA2CSksJ0XvjH8k3LmlfXvy/kC7Wc= +go.opentelemetry.io/collector/featuregate v1.51.0 h1:dxJuv/3T84dhNKp7fz5+8srHz1dhquGzDpLW4OZTFBw= +go.opentelemetry.io/collector/featuregate v1.51.0/go.mod h1:/1bclXgP91pISaEeNulRxzzmzMTm4I5Xih2SnI4HRSo= +go.opentelemetry.io/collector/internal/componentalias v0.145.0 h1:A9V5IiETzz8FCtjxjRM5gf7RE3sOtA1h8phmpQjXTZ4= +go.opentelemetry.io/collector/internal/componentalias v0.145.0/go.mod h1:sEKEAwAn45ZiXRk3T/vbkvetw14tIRd0CJIxcEx9SsQ= +go.opentelemetry.io/collector/internal/testutil v0.145.0 h1:H/KL0GH3kGqSMKxZvnQ0B0CulfO9xdTg4DZf28uV7fY= +go.opentelemetry.io/collector/internal/testutil v0.145.0/go.mod h1:YAD9EAkwh/l5asZNbEBEUCqEjoL1OKMjAMoPjPqH76c= +go.opentelemetry.io/collector/pdata v1.51.0 h1:DnDhSEuDXNdzGRB7f6oOfXpbDApwBX3tY+3K69oUrDA= +go.opentelemetry.io/collector/pdata v1.51.0/go.mod h1:GoX1bjKDR++mgFKdT7Hynv9+mdgQ1DDXbjs7/Ww209Q= +go.opentelemetry.io/collector/pdata/pprofile v0.145.0 h1:ASMKpoqokf8HhzjoeMKZf0K6UXLhufVwNXH0sSuUn5w= +go.opentelemetry.io/collector/pdata/pprofile v0.145.0/go.mod h1:a60GC7wQPhLAixWzKbbP51QLwwc+J0Cmp4SurOlhGUk= +go.opentelemetry.io/collector/pdata/testdata v0.145.0 h1:iFsxsCMtE3lnAc/5kZbhZHpRv1OMmM+O5ry46xdQHbg= +go.opentelemetry.io/collector/pdata/testdata v0.145.0/go.mod h1:0y2ERArdzqmYdJHdKLKue+AUubSEGlwK49F+23+Mbic= +go.opentelemetry.io/collector/pipeline v1.51.0 h1:GZBNW+aaOE+zufGzAkXy0OI7n1cqepEa5J+beaOpS2k= +go.opentelemetry.io/collector/pipeline v1.51.0/go.mod h1:xUrAqiebzYbrgxyoXSkk6/Y3oi5Sy3im2iCA51LwUAI= +go.opentelemetry.io/collector/receiver v1.51.0 h1:BUEHfN3HSvR3YzPzJOLOotPyJlILi2D4WkGzNPNuDlA= +go.opentelemetry.io/collector/receiver v1.51.0/go.mod h1:NrkCdesDdxt6bjSVU2J+UsQxDvOUMIe/XdhnexaqAic= +go.opentelemetry.io/collector/receiver/receivertest v0.145.0 h1:JlEM4VWvoUMkllUce7p4urPhTsxFF5amG8CkVnC22/k= +go.opentelemetry.io/collector/receiver/receivertest v0.145.0/go.mod h1:iitTZ7Z2QTkr9oi3mN0IIMXG9Y6Pn2xTX31Cyyyp4/8= +go.opentelemetry.io/collector/receiver/xreceiver v0.145.0 h1:vkWKqPX6g7FWPuZlgxAVk8N+uMg5WGh/bZINdGsIgGY= +go.opentelemetry.io/collector/receiver/xreceiver v0.145.0/go.mod h1:HlEYrvW52PWoL92jRRLzlmJ2hwWaKBzaoo6FFDZpHx4= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= diff --git a/interpreter/ruby/pyroscope.go b/interpreter/ruby/pyroscope.go new file mode 100644 index 000000000..7bd9e89ba --- /dev/null +++ b/interpreter/ruby/pyroscope.go @@ -0,0 +1,16 @@ +package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby" + +import "sync/atomic" + +var ReturnToNative = atomic.Bool{} + +func init() { + ReturnToNative.Store(true) +} + +func returnToNative() int { + if ReturnToNative.Load() { + return 1 + } + return 0 +} diff --git a/interpreter/ruby/ruby.go b/interpreter/ruby/ruby.go index b84d4acdd..9c4a56d8b 100644 --- a/interpreter/ruby/ruby.go +++ b/interpreter/ruby/ruby.go @@ -4,6 +4,7 @@ package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby" import ( + "debug/elf" "encoding/binary" "errors" "fmt" @@ -24,9 +25,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" @@ -97,18 +101,20 @@ const ( var ( // regex to identify the Ruby interpreter executable - rubyRegex = regexp.MustCompile(`^(?:.*/)?libruby(?:-.*)?\.so\.(\d)\.(\d)\.(\d)$`) + rubyRegex = regexp.MustCompile(`^(?:.*/)?libruby(?:-.*)?\.so\.(\d)\.(\d)\.(\d+)$`) // regex to extract a version from a string - rubyVersionRegex = regexp.MustCompile(`^(\d)\.(\d)\.(\d)$`) - - unknownCfunc = libpf.Intern("") - cfuncDummyFile = libpf.Intern("") - rubyGcFrame = libpf.Intern("(garbage collection)") - rubyGcRunning = libpf.Intern("(running)") - rubyGcMarking = libpf.Intern("(marking)") - rubyGcSweeping = libpf.Intern("(sweeping)") - rubyGcCompacting = libpf.Intern("(compacting)") - rubyGcDummyFile = libpf.Intern("") + rubyVersionRegex = regexp.MustCompile(`^(\d)\.(\d)\.(\d+)$`) + + unknownCfunc = libpf.Intern("") + cfuncDummyFile = libpf.Intern("") + rubyGcFrame = libpf.Intern("(garbage collection)") + rubyGcRunning = libpf.Intern("(running)") + rubyGcMarking = libpf.Intern("(marking)") + rubyGcSweeping = libpf.Intern("(sweeping)") + rubyGcCompacting = libpf.Intern("(compacting)") + rubyGcDummyFile = libpf.Intern("") + rubyJitDummyFrame = libpf.Intern("") + rubyJitDummyFile = libpf.Intern("") // compiler check to make sure the needed interfaces are satisfied _ interpreter.Data = &rubyData{} _ interpreter.Instance = &rubyInstance{} @@ -330,6 +336,8 @@ func (r *rubyData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libp Size_of_value: r.vmStructs.size_of_value, Running_ec: r.vmStructs.rb_ractor_struct.running_ec, + + Return_to_native: uint8(returnToNative()), } if err := ebpf.UpdateProcData(libpf.Ruby, pid, unsafe.Pointer(&cdata)); err != nil { @@ -345,6 +353,7 @@ 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, memPool: sync.Pool{ @@ -390,6 +399,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 @@ -402,9 +414,15 @@ type rubyInstance struct { // maxSize is the largest number we did see in the last reporting interval for size // in getRubyLineNo. maxSize atomic.Uint32 + + // prefixes added to ebpf maps for the YJIT region, cleaned up in Detach + prefixes []lpm.Prefix } func (r *rubyInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error { + for _, prefix := range r.prefixes { + _ = ebpf.DeletePidInterpreterMapping(pid, prefix) + } return ebpf.DeleteProcData(libpf.Ruby, pid) } @@ -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) } @@ -1182,6 +1209,56 @@ 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 { + start, end, found := detectYJITRegion(pr, r.r.version, mappings) + log.Debugf("YJIT region %#x-%#x %+v", start, end, found) + if !found { + for i := range mappings { + m := &mappings[i] + if m.Flags&(elf.PF_R|elf.PF_X) != elf.PF_R|elf.PF_X || m.Path.String() != "" { + continue + } + log.Debugf("YJIT mapping {Vaddr: %#x, Length: %#x, Flags: %#x}", + m.Vaddr, m.Length, uint32(m.Flags)) + } + return nil + } + + if r.procInfo.Jit_start != 0 { + if start != r.procInfo.Jit_start || end != r.procInfo.Jit_end { + log.Warnf("YJIT region changed: %#x-%#x -> %#x-%#x", + r.procInfo.Jit_start, r.procInfo.Jit_end, start, end) + } + return nil + } + + pid := pr.PID() + + size := end - start + log.Debugf("Found YJIT region %#x-%#x (size %d MiB)", start, end, size/(1024*1024)) + + prefixes, err := lpm.CalculatePrefixList(start, end) + if err != nil { + return fmt.Errorf("YJIT region lpm failure %#x/%#x: %w", start, size, err) + } + + for _, prefix := range prefixes { + if err := ebpf.UpdatePidInterpreterMapping(pid, prefix, support.ProgUnwindRuby, 0, 0); err != nil { + return err + } + } + r.prefixes = prefixes + + r.procInfo.Jit_start = start + r.procInfo.Jit_end = end + if err := ebpf.UpdateProcData(libpf.Ruby, pid, unsafe.Pointer(r.procInfo)); err != nil { + return err + } + + return nil +} + func (r *rubyInstance) GetAndResetMetrics() ([]metrics.Metric, error) { addrToStringStats := r.addrToString.ResetMetrics() diff --git a/interpreter/ruby/yjit.go b/interpreter/ruby/yjit.go new file mode 100644 index 000000000..92b94308f --- /dev/null +++ b/interpreter/ruby/yjit.go @@ -0,0 +1,131 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby" + +import ( + "debug/elf" + "fmt" + "os" + "strconv" + "strings" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/libpf/pfunsafe" + "go.opentelemetry.io/ebpf-profiler/process" +) + +func parseYJITMemArgs(pr process.Process) (execMemSize, memSize uint64) { + forEachProcessArg(pr, func(arg string) { + if v, ok := strings.CutPrefix(arg, "--yjit-exec-mem-size="); ok { + if n, err := strconv.ParseUint(v, 10, 64); err == nil { + execMemSize = n + } + } + if v, ok := strings.CutPrefix(arg, "--yjit-mem-size="); ok { + if n, err := strconv.ParseUint(v, 10, 64); err == nil { + memSize = n + } + } + }) + return execMemSize, memSize +} + +func yjitRegionSize(version uint32, execMemSize, memSize uint64) uint64 { + major := (version >> 16) & 0xff + minor := (version >> 8) & 0xff + + if major < 3 || (major == 3 && minor < 3) { + return 0 + } + + var defaultMiB uint64 + if major == 3 && minor == 3 { + defaultMiB = 48 + } else { + defaultMiB = 128 + } + + if major == 3 && minor == 3 { + if execMemSize > 0 { + return execMemSize * 1024 * 1024 + } + return defaultMiB * 1024 * 1024 + } + + if execMemSize > 0 { + return execMemSize * 1024 * 1024 + } + if memSize > 0 { + return memSize * 1024 * 1024 + } + return defaultMiB * 1024 * 1024 +} + +func determineYJITRegionSize(version uint32, pr process.Process) uint64 { + execMemSize, memSize := parseYJITMemArgs(pr) + return yjitRegionSize(version, execMemSize, memSize) +} + +// mappings contains all readable or executable mappings, meaning rw-, rwx, -wx may be there, but not -w- +func findYJITRegion(mappings []process.Mapping, expectedSize uint64) (start uint64, found bool) { + var end uint64 + _ = end + for i := range mappings { + m := &mappings[i] + if !found { + if m.Path == libpf.NullString && m.Flags == (elf.PF_R|elf.PF_X) { + found = true + start = m.Vaddr + end = start + expectedSize + } + continue + } + withinFoundRange := m.Vaddr < end && m.Vaddr+m.Length <= end + if withinFoundRange { + if m.Path != libpf.NullString { + return 0, false + } + continue + } + if m.Path == libpf.NullString && m.Flags == (elf.PF_R|elf.PF_X) { + return 0, false + } + + } + return start, found +} + +func forEachProcessArg(pr process.Process, fn func(string)) { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pr.PID())) + if err != nil || len(data) == 0 { + return + } + for len(data) > 0 { + i := 0 + for i < len(data) && data[i] != 0 { + i++ + } + if i > 0 { + fn(pfunsafe.ToString(data[:i])) + } + data = data[i:] + if len(data) > 0 { + data = data[1:] + } + } +} + +func detectYJITRegion(pr process.Process, version uint32, + mappings []process.Mapping) (start, end uint64, found bool) { + expectedSize := determineYJITRegionSize(version, pr) + if expectedSize == 0 { + return 0, 0, false + } + + start, found = findYJITRegion(mappings, expectedSize) + if !found { + return 0, 0, false + } + return start, start + expectedSize, true +} diff --git a/interpreter/ruby/yjit_test.go b/interpreter/ruby/yjit_test.go new file mode 100644 index 000000000..9c59d8ee9 --- /dev/null +++ b/interpreter/ruby/yjit_test.go @@ -0,0 +1,194 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package ruby // import "go.opentelemetry.io/ebpf-profiler/interpreter/ruby" + +import ( + "debug/elf" + "testing" + + "go.opentelemetry.io/ebpf-profiler/libpf" + "go.opentelemetry.io/ebpf-profiler/process" + + "github.com/stretchr/testify/assert" +) + +func TestYjitRegionSize(t *testing.T) { + mib := uint64(1024 * 1024) + + tests := []struct { + name string + version uint32 + execMemSize uint64 + memSize uint64 + expected uint64 + }{ + { + name: "ruby_3.2_returns_zero", + version: rubyVersion(3, 2, 0), + expected: 0, + }, + { + name: "ruby_3.3_default_48MiB", + version: rubyVersion(3, 3, 0), + expected: 48 * mib, + }, + { + name: "ruby_3.3_exec_mem_size_override", + version: rubyVersion(3, 3, 4), + execMemSize: 64, + expected: 64 * mib, + }, + { + name: "ruby_3.4_default_128MiB", + version: rubyVersion(3, 4, 0), + expected: 128 * mib, + }, + { + name: "ruby_3.4_mem_size_override", + version: rubyVersion(3, 4, 1), + memSize: 256, + expected: 256 * mib, + }, + { + name: "ruby_3.4_exec_mem_size_override", + version: rubyVersion(3, 4, 1), + execMemSize: 64, + expected: 64 * mib, + }, + { + name: "ruby_3.4_exec_mem_size_overrides_mem_size", + version: rubyVersion(3, 4, 1), + execMemSize: 64, + memSize: 256, + expected: 64 * mib, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := yjitRegionSize(tt.version, tt.execMemSize, tt.memSize) + assert.Equal(t, tt.expected, got) + }) + } +} + +const rx = elf.PF_R | elf.PF_X + +func TestFindYJITRegion(t *testing.T) { + const mib = uint64(1024 * 1024) + + tests := []struct { + name string + mappings []process.Mapping + expectedSize uint64 + wantStart uint64 + wantFound bool + }{ + { + name: "ruby33_two_rx_anon_pages", + mappings: []process.Mapping{ + {Vaddr: 0x7fa73a2ea000, Length: 0x2000, Flags: rx, Path: libpf.Intern("[vdso]")}, + {Vaddr: 0x7fa73a2ec000, Length: 0x2a000, Flags: rx, Path: libpf.Intern("/usr/lib64/ld-linux-x86-64.so.2")}, + {Vaddr: 0x7fa73a358000, Length: 0x11b000, Flags: rx, Path: libpf.NullString}, + {Vaddr: 0x7fa73a473000, Length: 0xd5000, Flags: rx, Path: libpf.NullString}, + }, + expectedSize: 48 * mib, + wantStart: 0x7fa73a358000, + wantFound: true, + }, + { + name: "one_rx_anon_gap_then_file_rx", + mappings: []process.Mapping{ + {Vaddr: 0x1000000, Length: 0x1000, Flags: rx, Path: libpf.NullString}, + {Vaddr: 0x1002000, Length: 0x1000, Flags: rx, Path: libpf.Intern("/usr/lib/libc.so")}, + }, + expectedSize: 48 * mib, + wantFound: false, + }, + { + name: "two_anon_groups_with_hole", + mappings: []process.Mapping{ + {Vaddr: 0x1000000, Length: 4 * 0x1000, Flags: rx, Path: libpf.NullString}, + {Vaddr: 0x1000000 + 8*0x1000, Length: 4 * 0x1000, Flags: rx, Path: libpf.NullString}, + }, + expectedSize: 48 * mib, + wantStart: 0x1000000, + wantFound: true, + }, + { + name: "two_anon_groups_hole_larger_than_expected", + mappings: []process.Mapping{ + {Vaddr: 0x1000000, Length: 4 * 0x1000, Flags: rx, Path: libpf.NullString}, + {Vaddr: 0x1000000 + 49*mib, Length: 4 * 0x1000, Flags: rx, Path: libpf.NullString}, + }, + expectedSize: 48 * mib, + wantFound: false, + }, + { + name: "file_rx_within_expected_range", + mappings: []process.Mapping{ + {Vaddr: 0x1000000, Length: 4 * 0x1000, Flags: rx, Path: libpf.NullString}, + {Vaddr: 0x1000000 + 4*0x1000, Length: 0x1000, Flags: rx, Path: libpf.Intern("/usr/lib/libc.so")}, + {Vaddr: 0x1000000 + 8*0x1000, Length: 4 * 0x1000, Flags: rx, Path: libpf.NullString}, + }, + expectedSize: 48 * mib, + wantFound: false, + }, + { + name: "single_anon_rx_mapping", + mappings: []process.Mapping{ + {Vaddr: 0x1000000, Length: 4 * 0x1000, Flags: rx, Path: libpf.NullString}, + }, + expectedSize: 48 * mib, + wantStart: 0x1000000, + wantFound: true, + }, + { + name: "part of real process with rw mappings", + mappings: []process.Mapping{ + {Vaddr: 0x400000, Length: 0x1000, Flags: 0x5, Path: libpf.Intern("/opt/ruby-3.3.10/bin/ruby")}, + {Vaddr: 0x401000, Length: 0x1000, Flags: 0x4, Path: libpf.Intern("/opt/ruby-3.3.10/bin/ruby")}, + {Vaddr: 0x402000, Length: 0x1000, Flags: 0x4, Path: libpf.Intern("/opt/ruby-3.3.10/bin/ruby")}, + {Vaddr: 0x403000, Length: 0x1000, Flags: 0x6, Path: libpf.Intern("/opt/ruby-3.3.10/bin/ruby")}, + {Vaddr: 0x7fa6f0000000, Length: 0x48000, Flags: 0x6, Path: libpf.Intern("")}, + {Vaddr: 0x7fa739c00000, Length: 0x410000, Flags: 0x5, Path: libpf.Intern("/opt/ruby-3.3.10/lib/libruby.so.3.3.10")}, + {Vaddr: 0x7fa73a010000, Length: 0x1cf000, Flags: 0x4, Path: libpf.Intern("/opt/ruby-3.3.10/lib/libruby.so.3.3.10")}, + {Vaddr: 0x7fa73a1df000, Length: 0x16000, Flags: 0x4, Path: libpf.Intern("/opt/ruby-3.3.10/lib/libruby.so.3.3.10")}, + {Vaddr: 0x7fa73a1f5000, Length: 0x5000, Flags: 0x6, Path: libpf.Intern("/opt/ruby-3.3.10/lib/libruby.so.3.3.10")}, + {Vaddr: 0x7fa73a1fa000, Length: 0x15000, Flags: 0x6, Path: libpf.Intern("")}, + {Vaddr: 0x7fa73a2e2000, Length: 0x2000, Flags: 0x6, Path: libpf.Intern("")}, + {Vaddr: 0x7fa73a2ea000, Length: 0x2000, Flags: 0x5, Path: libpf.Intern("linux-vdso.1.so")}, + {Vaddr: 0x7fa73a2ec000, Length: 0x2a000, Flags: 0x5, Path: libpf.Intern("/usr/lib64/ld-linux-x86-64.so.2")}, + {Vaddr: 0x7fa73a316000, Length: 0xc000, Flags: 0x4, Path: libpf.Intern("/usr/lib64/ld-linux-x86-64.so.2")}, + {Vaddr: 0x7fa73a322000, Length: 0x2000, Flags: 0x4, Path: libpf.Intern("/usr/lib64/ld-linux-x86-64.so.2")}, + {Vaddr: 0x7fa73a324000, Length: 0x1000, Flags: 0x6, Path: libpf.Intern("/usr/lib64/ld-linux-x86-64.so.2")}, + {Vaddr: 0x7fa73a325000, Length: 0x1000, Flags: 0x6, Path: libpf.Intern("")}, + {Vaddr: 0x7fa73a358000, Length: 0x11b000, Flags: 0x5, Path: libpf.Intern("")}, + {Vaddr: 0x7fa73a473000, Length: 0xd8000, Flags: 0x5, Path: libpf.Intern("")}, + }, + expectedSize: 48 * mib, + wantStart: 0x7fa73a358000, + wantFound: true, + }, + { + name: "16k_rx_hole_then_50mib_rx_expected_none", + mappings: []process.Mapping{ + {Vaddr: 0x1000000, Length: 4 * 0x1000, Flags: rx, Path: libpf.NullString}, + {Vaddr: 0x1000000 + 8*0x1000, Length: 50 * mib, Flags: rx, Path: libpf.NullString}, + }, + expectedSize: 48 * mib, + wantFound: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start, found := findYJITRegion(tt.mappings, tt.expectedSize) + assert.Equal(t, tt.wantFound, found) + if found { + assert.Equal(t, tt.wantStart, start) + } + }) + } +} diff --git a/support/ebpf/frametypes.h b/support/ebpf/frametypes.h index aae841d98..4026d4922 100644 --- a/support/ebpf/frametypes.h +++ b/support/ebpf/frametypes.h @@ -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 diff --git a/support/ebpf/ruby_tracer.ebpf.c b/support/ebpf/ruby_tracer.ebpf.c index 0d3a7b8c3..26fd5c313 100644 --- a/support/ebpf/ruby_tracer.ebpf.c +++ b/support/ebpf/ruby_tracer.ebpf.c @@ -270,6 +270,12 @@ 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 if (!rubyinfo->return_to_native) { + 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 @@ -446,6 +452,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; + } + } + } + for (u32 i = 0; i < FRAMES_PER_WALK_RUBY_STACK; ++i) { error = read_ruby_frame(record, rubyinfo, stack_ptr, next_unwinder); if (error != ERR_OK) @@ -453,14 +476,20 @@ 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 || !rubyinfo->return_to_native) + ? 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; } // If the next winder is native, save state and move to next unwinder - if (*next_unwinder == PROG_UNWIND_NATIVE) + if (*next_unwinder == PROG_UNWIND_NATIVE && rubyinfo->return_to_native) { goto save_state; + } } *next_unwinder = PROG_UNWIND_RUBY; diff --git a/support/ebpf/tracemgmt.h b/support/ebpf/tracemgmt.h index d9d87ffe4..d65336092 100644 --- a/support/ebpf/tracemgmt.h +++ b/support/ebpf/tracemgmt.h @@ -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; diff --git a/support/ebpf/tracer.ebpf.amd64 b/support/ebpf/tracer.ebpf.amd64 index aa665fad7..fb38323bf 100644 Binary files a/support/ebpf/tracer.ebpf.amd64 and b/support/ebpf/tracer.ebpf.amd64 differ diff --git a/support/ebpf/tracer.ebpf.arm64 b/support/ebpf/tracer.ebpf.arm64 index 6d02ca1ff..efc1bb1dd 100644 Binary files a/support/ebpf/tracer.ebpf.arm64 and b/support/ebpf/tracer.ebpf.arm64 differ diff --git a/support/ebpf/types.h b/support/ebpf/types.h index 1c30849e3..2c6666ba1 100644 --- a/support/ebpf/types.h +++ b/support/ebpf/types.h @@ -477,6 +477,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: @@ -507,6 +510,8 @@ typedef struct RubyProcInfo { // rb_ractor_struct offset: u16 running_ec; + u8 return_to_native; + } RubyProcInfo; // V8ProcInfo is a container for the data needed to build a stack trace for a V8 process. @@ -716,6 +721,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. diff --git a/support/types.go b/support/types.go index 16cf33b20..b72cf78d0 100644 --- a/support/types.go +++ b/support/types.go @@ -288,6 +288,8 @@ type RubyProcInfo struct { Current_ec_tpbase_tls_offset uint64 Current_ctx_ptr uint64 Has_objspace bool + Jit_start uint64 + Jit_end uint64 Vm_stack uint8 Vm_stack_size uint8 Cfp uint8 @@ -304,7 +306,8 @@ type RubyProcInfo struct { Cme_method_def uint8 Size_of_value uint8 Running_ec uint16 - Pad_cgo_0 [4]byte + Return_to_native uint8 + Pad_cgo_0 [3]byte } type V8ProcInfo struct { Version uint32 @@ -336,7 +339,7 @@ const ( sizeof_ApmIntProcInfo = 0x8 sizeof_DotnetProcInfo = 0x4 sizeof_PHPProcInfo = 0x18 - sizeof_RubyProcInfo = 0x30 + sizeof_RubyProcInfo = 0x48 ) const ( @@ -397,6 +400,7 @@ const ( RubyFrameTypeCmeCfunc = 0x2 RubyFrameTypeIseq = 0x3 RubyFrameTypeGc = 0x4 + RubyFrameTypeJit = 0x5 ) var MetricsTranslation = []metrics.MetricID{ diff --git a/support/types_def.go b/support/types_def.go index 12ac64a27..c74383d58 100644 --- a/support/types_def.go +++ b/support/types_def.go @@ -210,6 +210,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{