-
Notifications
You must be signed in to change notification settings - Fork 296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial support for BEAM (Erlang/Elixir) #289
base: main
Are you sure you want to change the base?
Conversation
Typically JIT is on mmaped anonymous memory. You will need to add hooks to call your unwinder for this memory mappings. For a generic catch it all example, see the If you can extract the exact memory area where JIT code exists directly from the VM, you can refer to
The native winder will not have heuristic for it. You need to implement the code to hook your unwinder for the memory areas where JIT code is at (see above). After that you'll need to have eBPF code that actually unwinds the JIT code. It might be simple if the JIT frame layout is frame pointer based, see e.g. v8 unwinder https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/main/support/ebpf/v8_tracer.ebpf.c, or highly complicated if there is a custom frame layout, see e.g. hotspot unwinder https://github.com/open-telemetry/opentelemetry-ebpf-profiler/blob/main/support/ebpf/hotspot_tracer.ebpf.c. The unwinder will need to collect the extra needed by the symolization in the next step.
Once the unwinding is done, the core will code interpreter plugins symolization code which will need to extract the symbol data from the target process. Again, see some examples how its done for the
Correct, you will need to implement both the unwinding and symbolization yourself. Depending on the VM internals, this can be highly complicated and extensive work that is needed to cover all the corner cases within the ebpf constraints. |
Aha! Thanks, this was the connection I was missing. I was thinking that since I can't statically know about all the JIT code that might be generated in the future, I can't possibly add it all to the maps, but I believe the BEAM does have ways to pretty easily locate the memory of all the JITted code, so I'll dig into that and the v8 example. The BEAM does use frame pointers, so I believe it should be relatively straightforward to figure out. Thanks for the tips! ❤️ 🚀 |
The range for the interpreter looks suspiciously small - only 128 bytes. This can be valid, if its just a small stub but guaranteed to be on stack for interpreter frames. Alternatively, this could be a function doing something else that is not necessarily on stack when executing interpreted code. You might want to double check which functions are on stack when executing interpreted code. If it can be a set of multiple functions (e.g. several functions with same signature tailcalling each other -- compiler can convert call to jump), you need to extract the range that covers all of these. It would become a problem if these functions are not contiguously in the executable area.
No problem. But in short, you'll need to manually extract those areas and then call You can also provide little bit of context data for each memory area. This could be useful if there's some auxiliary data connected to each memory area the unwinder needs.
Nice! Then
You're welcome. Looking forward to the BEAM support! Thank you for working on this! |
FYI: I have opened a open-telemetry/semantic-conventions#1735 with OTel semconv to add a type for beam. |
93d0726
to
caa65c3
Compare
return 0, nil, fmt.Errorf("failed to read erts version: %v", err) | ||
} | ||
|
||
return uint32(otpMajor), ertsVersion, nil |
Check failure
Code scanning / CodeQL
Incorrect conversion between integer types
To get log lines using the |
Ah, thanks! I wasn't sure how to make that work, which is why I did it this way instead, which did work when I put it in one of the other eBPF programs, but I don't see anything coming from my program still. |
With #145 things changed a bit and I missed that part. Sorry that I have missed this one in the first place. Here are steps I used to generate output to
I missed, that |
I spent some more time today to make progress on this, and was able to get past where I was stuck before, and now I am able to see that my eBPF unwinder is running (not sure yet if it's doing the correct thing, but it's doing something) and my In the output form the
Is this because of the PR you mentioned opening to add that as a supported type in the OTEL spec, and it hasn't yet been pulled into DevFiler? Or is there just somewhere in the Thanks again for all your help! |
Yes - devfiler uses a filter on frame types. This might change in the future. For the mean time and to enable you to continue your work, I have created a version of devfiler that handles beam frame types:
Working on live processes can be tricky and reproducing edge cases can be hard. For that reason, I recommend looking into coredump. With the tool coredump one can import a core dump of a process and run all the Go and eBPF code in user space just like a regular Go test. Please feel free to ping me, if you need help. |
Sorry it took so long to make some time to actually give it a try, but when I tried running the custom
I'm planning to try to figure out how to get the core dump testing thing working, though, so not blocking at the moment. I just wanted to see whether it got me further along with the path I was on before, having a version of |
sorry - this was not supposed to happen 🙏 |
91d71e1
to
f8f3cb5
Compare
f8f3cb5
to
ea28f8a
Compare
Note
The first commit in the PR is so that this branch stacks on top of #288 while I work on this one.
I have begun to work on support for BEAM languages like Erlang and Elixir, and wanted to open this PR early as a draft, so that I can get any feedback you may have to help the process go more smoothly. I don't have much experience with Go or eBPF, so any feedback you have is very welcome. What I have so far is mostly based on digging through the existing support for other languages as well as the BEAM / OTP source code, and trying to understand how all the parts fit together.
I have also been digging through the BEAM / OTP source code, and also the
gdb
scripts that it includes for working directly with the memory image of a running system or core dump.So far, I am able to see the logs from my Go code coming through, and confirming that it's working correctly as far as loading and attaching the interpreter support, like this:
However, I can't seem to get any tracing logs out of the eBPF program, so I suspect that it's never being run. If I modify the
native
eBPF script to write the same kind of log there, I can confirm that I'm seeing it in/sys/kernel/tracing/trace_pipe
after doing the following to narrow down the logs I want to see, but the same doesn't work for mybeam
program:I was thinking that this was because OTP 27 includes a JIT, so the interpreter might never be used, but I am also not seeing any frames for the native JIT code executing, so I'd love any advice you may have there in terms of how I might go about troubleshooting that. Maybe the native unwinder is just missing some heuristic that's needed for the way the ASMJIT / BEAMJIT works? I'm not clear on how the profiler resolves symbols for JIT code or how those should show up in
devfiler
, so maybe it is working and I just don't know how to use to the tool... 😅 But from what I can tell, I don't think the frames are showing up there for anything but the C code for Erlang itself (and built-in C functions). I was expecting to be able to see which Erlang code was running, for example.I also tried building OTP 27 with the JIT disabled to confirm my theory that it just wasn't working, but it behaved the same (though with a different memory address showing for the
interpRanges
, which confirms that it really did build a different set of code).