Skip to content

Commit

Permalink
Initial support for BEAM interpreter
Browse files Browse the repository at this point in the history
  • Loading branch information
GregMefford committed Jan 1, 2025
1 parent 03e458b commit 93d0726
Show file tree
Hide file tree
Showing 10 changed files with 202 additions and 0 deletions.
135 changes: 135 additions & 0 deletions interpreter/beam/beam.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package beam // import "go.opentelemetry.io/ebpf-profiler/interpreter/beam"

// BEAM VM Unwinder support code

// The BEAM VM is an interpreter for Erlang, as well as several other languages
// that share the same bytecode, such as Elixir and Gleam.

import (
"fmt"
"regexp"
"strconv"

log "github.com/sirupsen/logrus"

"go.opentelemetry.io/ebpf-profiler/host"
"go.opentelemetry.io/ebpf-profiler/interpreter"
"go.opentelemetry.io/ebpf-profiler/libpf"
"go.opentelemetry.io/ebpf-profiler/libpf/pfelf"
"go.opentelemetry.io/ebpf-profiler/remotememory"
"go.opentelemetry.io/ebpf-profiler/reporter"
"go.opentelemetry.io/ebpf-profiler/support"
)

var (
// regex for matching the process name
beamRegex = regexp.MustCompile(`beam.smp`)
_ interpreter.Data = &beamData{}
_ interpreter.Instance = &beamInstance{}
)

type beamData struct {
version uint32
}

type beamInstance struct {
interpreter.InstanceStubs

data *beamData
rm remotememory.RemoteMemory
}

func readSymbolValue(ef *pfelf.File, name libpf.SymbolName) ([]byte, error) {
sym, err := ef.LookupSymbol(name)
if err != nil {
return nil, fmt.Errorf("symbol not found: %v", err)
}

memory := make([]byte, sym.Size)
if _, err := ef.ReadVirtualMemory(memory, int64(sym.Address)); err != nil {
return nil, fmt.Errorf("failed to read process memory at 0x%x:%v", sym.Address, err)
}

log.Infof("read symbol value %s: %s", sym.Name, memory)
return memory, nil
}
func readReleaseVersion(ef *pfelf.File) (uint32, []byte, error) {

Check failure on line 59 in interpreter/beam/beam.go

View workflow job for this annotation

GitHub Actions / Lint (amd64)

unnamedResult: consider giving a name to these results (gocritic)
otp_release, err := readSymbolValue(ef, "etp_otp_release")

Check failure on line 60 in interpreter/beam/beam.go

View workflow job for this annotation

GitHub Actions / Lint (amd64)

var-naming: don't use underscores in Go names; var otp_release should be otpRelease (revive)
if err != nil {
return 0, nil, fmt.Errorf("failed to read OTP release: %v", err)
}

// Slice off the null termination before converting
otp_major, err := strconv.Atoi(string(otp_release[:len(otp_release)-1]))

Check failure on line 66 in interpreter/beam/beam.go

View workflow job for this annotation

GitHub Actions / Lint (amd64)

var-naming: don't use underscores in Go names; var otp_major should be otpMajor (revive)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse OTP version: %v", err)
}

erts_version, err := readSymbolValue(ef, "etp_erts_version")

Check failure on line 71 in interpreter/beam/beam.go

View workflow job for this annotation

GitHub Actions / Lint (amd64)

var-naming: don't use underscores in Go names; var erts_version should be ertsVersion (revive)
if err != nil {
return 0, nil, fmt.Errorf("failed to read erts version: %v", err)
}

return uint32(otp_major), erts_version, nil

Check failure

Code scanning / CodeQL

Incorrect conversion between integer types High

Incorrect conversion of an integer with architecture-dependent bit size from
strconv.Atoi
to a lower bit size type uint32 without an upper bound check.
}

func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) {
matches := beamRegex.FindStringSubmatch(info.FileName())
if matches == nil {
return nil, nil
}
log.Infof("BEAM interpreter found: %v", matches)

ef, err := info.GetELF()
if err != nil {
return nil, err
}

otp_version, _, err := readReleaseVersion(ef)

Check failure on line 91 in interpreter/beam/beam.go

View workflow job for this annotation

GitHub Actions / Lint (amd64)

var-naming: don't use underscores in Go names; var otp_version should be otpVersion (revive)
if err != nil {
return nil, err
}

symbolName := libpf.SymbolName("process_main")
interpRanges, err := info.GetSymbolAsRanges(symbolName)
if err != nil {
return nil, err
}

if err = ebpf.UpdateInterpreterOffsets(support.ProgUnwindBEAM, info.FileID(), interpRanges); err != nil {
return nil, err
}

d := &beamData{
version: otp_version,
}

log.Infof("BEAM loaded, otp_version: %d, interpRanges: %v", otp_version, interpRanges)
//d.loadIntrospectionData()

Check failure on line 111 in interpreter/beam/beam.go

View workflow job for this annotation

GitHub Actions / Lint (amd64)

commentFormatting: put a space between `//` and comment text (gocritic)

return d, nil
}

func (d *beamData) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, bias libpf.Address, rm remotememory.RemoteMemory) (interpreter.Instance, error) {
log.Infof("BEAM interpreter attaching")
return &beamInstance{
data: d,
rm: rm,
}, nil
}

func (r *beamInstance) Detach(ebpf interpreter.EbpfHandler, pid libpf.PID) error {

Check failure on line 124 in interpreter/beam/beam.go

View workflow job for this annotation

GitHub Actions / Lint (amd64)

unused-parameter: parameter 'ebpf' seems to be unused, consider removing or renaming it as _ (revive)
return nil
}

func (r *beamInstance) Symbolize(symbolReporter reporter.SymbolReporter, frame *host.Frame, trace *libpf.Trace) error {
if !frame.Type.IsInterpType(libpf.BEAM) {
log.Warnf("BEAM failed to symbolize")
return interpreter.ErrMismatchInterpreterType
}
log.Infof("BEAM symbolizing")
return nil
}
2 changes: 2 additions & 0 deletions libpf/frametype.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ const (
V8Frame FrameType = support.FrameMarkerV8
// DotnetFrame identifies the Dotnet interpreter frames.
DotnetFrame FrameType = support.FrameMarkerDotnet
// BEAMFrame identifies the BEAM interpreter frames.
BEAMFrame FrameType = support.FrameMarkerBEAM
// AbortFrame identifies frames that report that further unwinding was aborted due to an error.
AbortFrame FrameType = support.FrameMarkerAbort
)
Expand Down
3 changes: 3 additions & 0 deletions libpf/interpretertype.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const (
V8 InterpreterType = support.FrameMarkerV8
// Dotnet identifies the Dotnet interpreter.
Dotnet InterpreterType = support.FrameMarkerDotnet
// BEAM identifies the BEAM interpreter.
BEAM InterpreterType = support.FrameMarkerBEAM
)

// Pseudo-interpreters without a corresponding frame type.
Expand Down Expand Up @@ -64,6 +66,7 @@ var interpreterTypeToString = map[InterpreterType]string{
Perl: "perl",
V8: "v8js",
Dotnet: "dotnet",
BEAM: "beam",
APMInt: "apm-integration",
}

Expand Down
9 changes: 9 additions & 0 deletions processmanager/ebpf/ebpf.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type ebpfMapsImpl struct {
phpProcs *cebpf.Map
rubyProcs *cebpf.Map
v8Procs *cebpf.Map
beamProcs *cebpf.Map
apmIntProcs *cebpf.Map

// Stackdelta and process related eBPF maps
Expand Down Expand Up @@ -199,6 +200,12 @@ func LoadMaps(ctx context.Context, maps map[string]*cebpf.Map) (EbpfHandler, err
}
impl.v8Procs = v8Procs

beamProcs, ok := maps["beam_procs"]
if !ok {
log.Fatalf("Map beam_procs is not available")
}
impl.beamProcs = beamProcs

apmIntProcs, ok := maps["apm_int_procs"]
if !ok {
log.Fatalf("Map apm_int_procs is not available")
Expand Down Expand Up @@ -294,6 +301,8 @@ func (impl *ebpfMapsImpl) getInterpreterTypeMap(typ libpf.InterpreterType) (*ceb
return impl.rubyProcs, nil
case libpf.V8:
return impl.v8Procs, nil
case libpf.BEAM:
return impl.beamProcs, nil
case libpf.APMInt:
return impl.apmIntProcs, nil
default:
Expand Down
4 changes: 4 additions & 0 deletions processmanager/execinfomanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"go.opentelemetry.io/ebpf-profiler/host"
"go.opentelemetry.io/ebpf-profiler/interpreter"
"go.opentelemetry.io/ebpf-profiler/interpreter/apmint"
"go.opentelemetry.io/ebpf-profiler/interpreter/beam"
"go.opentelemetry.io/ebpf-profiler/interpreter/dotnet"
"go.opentelemetry.io/ebpf-profiler/interpreter/hotspot"
"go.opentelemetry.io/ebpf-profiler/interpreter/nodev8"
Expand Down Expand Up @@ -124,6 +125,9 @@ func NewExecutableInfoManager(
if includeTracers.Has(types.DotnetTracer) {
interpreterLoaders = append(interpreterLoaders, dotnet.Loader)
}
if includeTracers.Has(types.BEAMTracer) {
interpreterLoaders = append(interpreterLoaders, beam.Loader)
}

interpreterLoaders = append(interpreterLoaders, apmint.Loader)

Expand Down
37 changes: 37 additions & 0 deletions support/ebpf/beam_tracer.ebpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "bpfdefs.h"
#include "tracemgmt.h"
#include "types.h"

bpf_map_def SEC("maps") beam_procs = {
.type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(pid_t),
.value_size = sizeof(BEAMProcInfo),
.max_entries = 1024,
};

SEC("perf_event/unwind_beam")
int unwind_beam(struct pt_regs *ctx) {
static const char fmt[] = "Unwinding BEAM stack";
bpf_trace_printk(fmt, sizeof(fmt));
DEBUG_PRINT("Unwinding BEAM stack");

PerCPURecord *record = get_per_cpu_record();
if (!record) {
return -1;
}

int unwinder = get_next_unwinder_after_interpreter(record);
u32 pid = record->trace.pid;

BEAMProcInfo *beaminfo = bpf_map_lookup_elem(&beam_procs, &pid);
if (!beaminfo) {
DEBUG_PRINT("No BEAM introspection data");
goto exit;
}

DEBUG_PRINT("==== unwind_beam stack_len: %d, pid: %d ====", record->trace.stack_len, record->trace.pid);

exit:
tail_call(ctx, unwinder);
return -1;
}
2 changes: 2 additions & 0 deletions support/ebpf/frametypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
#define FRAME_MARKER_PHP_JIT 0x9
// Indicates a Dotnet frame
#define FRAME_MARKER_DOTNET 0xA
// Indicates a BEAM frame
#define FRAME_MARKER_BEAM 0xB

// Indicates a frame containing information about a critical unwinding error
// that caused further unwinding to be aborted.
Expand Down
6 changes: 6 additions & 0 deletions support/ebpf/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ typedef enum TracePrograms {
PROG_UNWIND_RUBY,
PROG_UNWIND_V8,
PROG_UNWIND_DOTNET,
PROG_UNWIND_BEAM,
NUM_TRACER_PROGS,
} TracePrograms;

Expand Down Expand Up @@ -477,6 +478,11 @@ typedef struct V8ProcInfo {
u8 codekind_shift, codekind_mask, codekind_baseline;
} V8ProcInfo;

// BEAMProcInfo is a container for the data needed to build a stack trace for a BEAM process.
typedef struct BEAMProcInfo {
u32 version;
} BEAMProcInfo;

// COMM_LEN defines the maximum length we will receive for the comm of a task.
#define COMM_LEN 16

Expand Down
2 changes: 2 additions & 0 deletions support/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
FrameMarkerPerl = C.FRAME_MARKER_PERL
FrameMarkerV8 = C.FRAME_MARKER_V8
FrameMarkerDotnet = C.FRAME_MARKER_DOTNET
FrameMarkerBEAM = C.FRAME_MARKER_BEAM
FrameMarkerAbort = C.FRAME_MARKER_ABORT
)

Expand All @@ -37,6 +38,7 @@ const (
ProgUnwindPerl = C.PROG_UNWIND_PERL
ProgUnwindV8 = C.PROG_UNWIND_V8
ProgUnwindDotnet = C.PROG_UNWIND_DOTNET
ProgUnwindBEAM = C.PROG_UNWIND_BEAM
)

const (
Expand Down
2 changes: 2 additions & 0 deletions tracer/types/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const (
RubyTracer
V8Tracer
DotnetTracer
BEAMTracer

// maxTracers indicates the max. number of different tracers
maxTracers
Expand All @@ -35,6 +36,7 @@ var tracerTypeToName = map[tracerType]string{
RubyTracer: "ruby",
V8Tracer: "v8",
DotnetTracer: "dotnet",
BEAMTracer: "beam",
}

var tracerNameToType = make(map[string]tracerType, maxTracers)
Expand Down

0 comments on commit 93d0726

Please sign in to comment.