diff --git a/customlabelstest/customlabels_test.go b/customlabelstest/customlabels_test.go
index b4d48d102..e2e1c3ef1 100644
--- a/customlabelstest/customlabels_test.go
+++ b/customlabelstest/customlabels_test.go
@@ -22,7 +22,7 @@ func TestNativeCustomLabels(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- traceCh, _ := testutils.StartTracer(ctx, t, enabledTracers, r)
+ traceCh, _ := testutils.StartTracer(ctx, t, enabledTracers, r, false)
// TODO - change this to `cargo build --release --bin custom-labels-example`
// once we have the Rust workspace from upstream.
cmd := exec.Command("cargo", "build", "--release",
diff --git a/interpreter/customlabels/customlabels.go b/interpreter/customlabels/customlabels.go
index 1de827a80..7f57069b7 100644
--- a/interpreter/customlabels/customlabels.go
+++ b/interpreter/customlabels/customlabels.go
@@ -16,15 +16,21 @@ import (
)
const (
- abiVersionExport = "custom_labels_abi_version"
- tlsExport = "custom_labels_current_set"
+ abiVersionExport = "custom_labels_abi_version"
+ currentSetTlsExport = "custom_labels_current_set"
+ currentHmTlsExport = "custom_labels_async_hashmap"
)
-var dsoRegex = regexp.MustCompile(`.*/libcustomlabels.*\.so|.*/customlabels\.node`)
+var dsoRegex = regexp.MustCompile(`.*/libcustomlabels.*\.so`)
+var nodeRegex = regexp.MustCompile(`.*/customlabels\.node`)
type data struct {
- abiVersionElfVA libpf.Address
- tlsAddr libpf.Address
+ abiVersionElfVA libpf.Address
+ currentSetTlsAddr libpf.Address
+
+ hasCurrentHm bool
+ currentHmTlsAddr libpf.Address
+
isSharedLibrary bool
}
@@ -35,7 +41,6 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete
if err != nil {
return nil, err
}
-
abiVersionSym, err := ef.LookupSymbol(abiVersionExport)
if err != nil {
if errors.Is(err, pfelf.ErrSymbolNotFound) {
@@ -53,8 +58,13 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete
// global-dynamic TLS model and have to look up the TLS descriptor.
// Otherwise, assume we're the main binary and just look up the
// symbol.
- isSharedLibrary := dsoRegex.MatchString(info.FileName())
- var tlsAddr libpf.Address
+ fn := info.FileName()
+ isNativeSharedLibrary := dsoRegex.MatchString(fn)
+ isNodeExtension := (!isNativeSharedLibrary) && nodeRegex.MatchString(fn)
+ isSharedLibrary := isNativeSharedLibrary || isNodeExtension
+
+ var currentSetTlsAddr, currentHmTlsAddr libpf.Address
+ var hasCurrentHm bool
if isSharedLibrary {
// Resolve thread info TLS export.
tlsDescs, err := ef.TLSDescriptors()
@@ -62,22 +72,27 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete
return nil, errors.New("failed to extract TLS descriptors")
}
var ok bool
- tlsAddr, ok = tlsDescs[tlsExport]
+ currentSetTlsAddr, ok = tlsDescs[currentSetTlsExport]
if !ok {
return nil, errors.New("failed to locate TLS descriptor for custom labels")
}
+ if isNodeExtension {
+ currentHmTlsAddr, hasCurrentHm = tlsDescs[currentHmTlsExport]
+ }
} else {
- offset, err := ef.LookupTLSSymbolOffset(tlsExport)
+ offset, err := ef.LookupTLSSymbolOffset(currentSetTlsExport)
if err != nil {
return nil, fmt.Errorf("failed to get tls symbol offset: %w", err)
}
- tlsAddr = libpf.Address(offset)
+ currentSetTlsAddr = libpf.Address(offset)
}
d := data{
- abiVersionElfVA: libpf.Address(abiVersionSym.Address),
- tlsAddr: tlsAddr,
- isSharedLibrary: isSharedLibrary,
+ abiVersionElfVA: libpf.Address(abiVersionSym.Address),
+ currentSetTlsAddr: currentSetTlsAddr,
+ hasCurrentHm: hasCurrentHm,
+ currentHmTlsAddr: currentHmTlsAddr,
+ isSharedLibrary: isSharedLibrary,
}
return &d, nil
}
@@ -98,16 +113,25 @@ func (d data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID,
" (only 1 is supported)", abiVersion)
}
- var tlsOffset uint64
+ var currentSetTlsOffset uint64
if d.isSharedLibrary {
// Read TLS offset from the TLS descriptor
- tlsOffset = rm.Uint64(bias + d.tlsAddr + 8)
+ currentSetTlsOffset = rm.Uint64(bias + d.currentSetTlsAddr + 8)
} else {
// We're in the main executable: TLS offset is known statically.
- tlsOffset = uint64(d.tlsAddr)
+ currentSetTlsOffset = uint64(d.currentSetTlsAddr)
+ }
+
+ var currentHmTlsOffset uint64
+ if d.hasCurrentHm {
+ currentHmTlsOffset = rm.Uint64(bias + d.currentHmTlsAddr + 8)
}
- procInfo := C.NativeCustomLabelsProcInfo{tls_offset: C.u64(tlsOffset)}
+ procInfo := C.NativeCustomLabelsProcInfo{
+ current_set_tls_offset: C.u64(currentSetTlsOffset),
+ has_current_hm: C.bool(d.hasCurrentHm),
+ current_hm_tls_offset: C.u64(currentHmTlsOffset),
+ }
if err := ebpf.UpdateProcData(libpf.CustomLabels, pid, unsafe.Pointer(&procInfo)); err != nil {
return nil, err
}
diff --git a/interpreter/customlabels/integrationtests/node_test.go b/interpreter/customlabels/integrationtests/node_test.go
new file mode 100644
index 000000000..45d4795be
--- /dev/null
+++ b/interpreter/customlabels/integrationtests/node_test.go
@@ -0,0 +1,293 @@
+// Copyright 2024 The Parca Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+
+package customlabels_test
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "math/rand"
+ "net"
+ "net/http"
+ "path"
+ "path/filepath"
+ "runtime"
+ "slices"
+ "strconv"
+ "sync"
+ "testing"
+
+ "time"
+
+ "github.com/stretchr/testify/require"
+ testcontainers "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/wait"
+ "go.opentelemetry.io/ebpf-profiler/libpf"
+ "go.opentelemetry.io/ebpf-profiler/reporter"
+ "go.opentelemetry.io/ebpf-profiler/testutils"
+ tracertypes "go.opentelemetry.io/ebpf-profiler/tracer/types"
+)
+
+type symbolMap map[libpf.FrameID]string
+
+const N_WORKERS int = 8
+
+var files = []string{
+ "AUTHORS.md",
+ "CODE_OF_CONDUCT.md",
+ "CONTRIBUTING.md",
+ "INDEX.md",
+ "PUBLISHING.md",
+ "USING_ADVANCED.md",
+ "USING_PRO.md",
+ "broken.md",
+}
+
+func TestIntegration(t *testing.T) {
+ if !testutils.IsRoot() {
+ t.Skip("root privileges required")
+ }
+
+ for _, nodeVersion := range []string{
+ // As of today, node:latest is v24.6.0
+ // Eventually, it will be something where the offsets have changed,
+ // and start failing. At that point, update the list of offsets
+ // so this passes, and also add a test for the latest v24 if latest
+ // is on v25 by then.
+ "latest",
+ "22.18.0",
+ "20.19.4",
+ } {
+ name := "node-" + nodeVersion
+ t.Run(name, func(t *testing.T) {
+ ctx, cancel := context.WithCancel(context.Background())
+ t.Cleanup(cancel)
+
+ defer cancel()
+
+ cont := startContainer(ctx, t, nodeVersion)
+
+ enabledTracers, err := tracertypes.Parse("labels,v8")
+ require.NoError(t, err)
+
+ r := &mockReporter{symbols: make(symbolMap)}
+ traceCh, trc := testutils.StartTracer(ctx, t, enabledTracers, r, false)
+
+ testHTTPEndpoint(ctx, t, cont)
+ framesPerWorkerId := make(map[int]int)
+ framesPerFileName := make(map[string]int)
+
+ totalWorkloadFrames := 0
+ unlabeledWorkloadFrames := 0
+
+ timer := time.NewTimer(3 * time.Second)
+ defer timer.Stop()
+
+ for {
+ select {
+ case <-timer.C:
+ goto done
+ case trace := <-traceCh:
+ if trace == nil {
+ continue
+ }
+ ct, err := trc.TraceProcessor().ConvertTrace(trace)
+ require.NotNil(t, ct)
+ require.NoError(t, err)
+ workerId, okWid := trace.CustomLabels["workerId"]
+ filePath, okFname := trace.CustomLabels["filePath"]
+ var fileName string
+ if okFname {
+ fileName = path.Base(filePath)
+ }
+ knownWorkloadFrames := []string{
+ "lex",
+ "parse",
+ "blockTokens",
+ "readFile",
+ "readFileHandle",
+ }
+ hasWorkloadFrame := false
+ for i := range ct.FrameTypes {
+ if ct.FrameTypes[i] == libpf.V8Frame {
+ id := libpf.NewFrameID(ct.Files[i], ct.Linenos[i])
+ name := r.getFunctionName(id)
+ if slices.Contains(knownWorkloadFrames, name) {
+ hasWorkloadFrame = true
+ }
+ }
+ }
+
+ if hasWorkloadFrame {
+ totalWorkloadFrames++
+ if !(okWid && okFname) {
+ unlabeledWorkloadFrames++
+ }
+ }
+
+ if okWid {
+ val, err := strconv.Atoi(workerId)
+ require.NoError(t, err)
+
+ require.GreaterOrEqual(t, val, 0)
+ require.Less(t, val, N_WORKERS)
+
+ framesPerWorkerId[val]++
+ }
+
+ if okFname {
+ require.Contains(t, files, fileName)
+ framesPerFileName[fileName]++
+ }
+ }
+ }
+ done:
+ totalWidFrames := 0
+ // for 8 workers, each should have roughly 1/8
+ // of the labeled frames. There will be a bit of skew,
+ // so accept anything above 60% of that.
+ for i := 0; i < N_WORKERS; i++ {
+ totalWidFrames += framesPerWorkerId[i]
+ }
+ expectedWorkerAvg := float64(totalWidFrames) / float64(N_WORKERS)
+ for i := 0; i < N_WORKERS; i++ {
+ require.Less(t, expectedWorkerAvg*0.60, float64(framesPerWorkerId[i]))
+ }
+ // Each of the documents should account for some nontrivial amount of time,
+ // but since they aren't all the same length, we are less strict.
+ totalFnameFrames := 0
+ for _, v := range framesPerFileName {
+ totalFnameFrames += v
+ }
+ expectedFnameAvg := float64(totalFnameFrames) / float64(len(framesPerFileName))
+ for _, v := range framesPerFileName {
+ require.Less(t, expectedFnameAvg*0.2, float64(v))
+ }
+
+ // Really, there should be zero frames in the
+ // `marked` workload that aren't under labels,
+ // but accept a 1% slop because the unwinder
+ // isn't perfect (e.g. it might interrupt the
+ // process when the Node environment is in an
+ // undefined state)
+ require.Less(t, 100*unlabeledWorkloadFrames, totalWorkloadFrames)
+ })
+ }
+}
+
+func startContainer(ctx context.Context, t *testing.T,
+ nodeVersion string) testcontainers.Container {
+ t.Log("starting container for node version", nodeVersion)
+ //nolint:dogsled
+ _, path, _, _ := runtime.Caller(0)
+ cont, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
+ ContainerRequest: testcontainers.ContainerRequest{
+ FromDockerfile: testcontainers.FromDockerfile{
+ Context: filepath.Dir(path) + "/testdata/node-md-render/",
+ BuildArgs: map[string]*string{
+ "NODE_VERSION": &nodeVersion,
+ },
+ },
+ ExposedPorts: []string{"80/tcp"},
+ WaitingFor: wait.ForHTTP("/docs/AUTHORS.md"),
+ },
+ Started: true,
+ })
+ require.NoError(t, err)
+ return cont
+}
+
+func testHTTPEndpoint(ctx context.Context, t *testing.T, cont testcontainers.Container) {
+ const numGoroutines = 10
+ const requestsPerGoroutine = 10000
+
+ host, err := cont.Host(ctx)
+ require.NoError(t, err)
+
+ port, err := cont.MappedPort(ctx, "80")
+ require.NoError(t, err)
+
+ baseURL := "http://" + net.JoinHostPort(host, port.Port())
+
+ var wg sync.WaitGroup
+
+ var errs []error
+ for i := 0; i < numGoroutines; i++ {
+ wg.Add(1)
+ errs = append(errs, nil)
+ go func() {
+ defer wg.Done()
+
+ for j := 0; j < requestsPerGoroutine; j++ {
+ //nolint:gosec
+ file := files[rand.Intn(len(files))]
+
+ url := fmt.Sprintf("%s/docs/%s", baseURL, file)
+
+ //nolint:gosec
+ resp, err := http.Get(url)
+ if err != nil {
+ errs[i] = err
+ return
+ }
+
+ // if we don't read body to completion, the http library will kill the connection
+ // instead of reusing it, and we might run out of ports.
+ _, err = io.ReadAll(resp.Body)
+ if err != nil {
+ errs[i] = err
+ return
+ }
+
+ err = resp.Body.Close()
+ if err != nil {
+ errs[i] = err
+ return
+ }
+
+ if http.StatusOK != resp.StatusCode {
+ errs[i] = fmt.Errorf("Expected status 200 for %s", file)
+ return
+ }
+ }
+ }()
+ }
+
+ wg.Wait()
+ require.NoError(t, errors.Join(errs...))
+}
+
+type mockReporter struct {
+ mu sync.Mutex
+ symbols symbolMap
+}
+
+var _ reporter.SymbolReporter = &mockReporter{}
+
+func (m *mockReporter) ExecutableMetadata(*reporter.ExecutableMetadataArgs) {
+}
+func (m *mockReporter) FrameKnown(_ libpf.FrameID) bool { return false }
+func (m *mockReporter) ExecutableKnown(libpf.FileID) bool {
+ return false
+}
+func (m *mockReporter) FrameMetadata(args *reporter.FrameMetadataArgs) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ m.symbols[args.FrameID] = args.FunctionName
+}
+
+func (m *mockReporter) getFunctionName(frameID libpf.FrameID) string {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ return m.symbols[frameID]
+}
diff --git a/interpreter/customlabels/integrationtests/testdata/node-md-render/.gitignore b/interpreter/customlabels/integrationtests/testdata/node-md-render/.gitignore
new file mode 100644
index 000000000..a449309d5
--- /dev/null
+++ b/interpreter/customlabels/integrationtests/testdata/node-md-render/.gitignore
@@ -0,0 +1,3 @@
+docs/
+node_modules/
+package-lock.json
diff --git a/interpreter/customlabels/integrationtests/testdata/node-md-render/Dockerfile b/interpreter/customlabels/integrationtests/testdata/node-md-render/Dockerfile
new file mode 100644
index 000000000..fb2afe359
--- /dev/null
+++ b/interpreter/customlabels/integrationtests/testdata/node-md-render/Dockerfile
@@ -0,0 +1,29 @@
+ARG NODE_VERSION=20
+FROM node:${NODE_VERSION}
+
+# Install build dependencies for native modules
+RUN apt-get update && apt-get install -y git python3 make g++
+
+WORKDIR /app
+
+# Copy package files
+COPY package.json ./
+
+# Install dependencies
+RUN npm install
+
+# Clone marked repo docs at specific commit
+RUN git clone https://github.com/markedjs/marked.git /tmp/marked && \
+ cd /tmp/marked && \
+ git checkout 0a0da515346d2b3dd1662531043fa6925cb73fe3 && \
+ cp -r docs /app/docs && \
+ rm -rf /tmp/marked
+
+# Copy application code
+COPY *.js ./
+
+# Expose ports for app and gdbserver
+EXPOSE 80
+
+# Start the application with gdbserver
+CMD ["node", "index.js"]
\ No newline at end of file
diff --git a/interpreter/customlabels/integrationtests/testdata/node-md-render/index.js b/interpreter/customlabels/integrationtests/testdata/node-md-render/index.js
new file mode 100644
index 000000000..941ab0381
--- /dev/null
+++ b/interpreter/customlabels/integrationtests/testdata/node-md-render/index.js
@@ -0,0 +1,91 @@
+const http = require('http');
+const fs = require('fs/promises');
+const path = require('path');
+const cl = require('@polarsignals/custom-labels');
+const { Worker } = require('worker_threads');
+
+const PORT = process.env.PORT || 80;
+const WORKER_COUNT = 8;
+
+const begin = Date.now();
+
+function mysleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function myrand(min, max) {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+const workers = [];
+let currentWorker = 0;
+const pendingRequests = new Map();
+let requestIdCounter = 0;
+
+for (let i = 0; i < WORKER_COUNT; i++) {
+ const worker = new Worker(path.join(__dirname, 'worker.js'), {
+ workerData: { workerId: i }
+ });
+
+ worker.on('message', ({ requestId, success, html, error }) => {
+ const { res } = pendingRequests.get(requestId);
+ pendingRequests.delete(requestId);
+
+ if (success) {
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.end(html);
+ } else {
+ res.writeHead(404, { 'Content-Type': 'text/html' });
+ res.end(`
Error: ${error}
`);
+ }
+ });
+
+ worker.on('error', (error) => {
+ console.error('Worker error:', error);
+ });
+
+ workers.push(worker);
+}
+
+function generateRandomString() {
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+ let result = '';
+ for (let i = 0; i < 10; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return result;
+}
+
+function processWithWorker(filePath, res, randomLabels) {
+ const requestId = ++requestIdCounter;
+ pendingRequests.set(requestId, { res });
+
+ const worker = workers[currentWorker];
+ currentWorker = (currentWorker + 1) % WORKER_COUNT;
+ worker.postMessage({ filePath, requestId, randomLabels });
+}
+
+function startServer() {
+ const server = http.createServer((req, res) => {
+ const filePath = path.join(__dirname, req.url);
+
+ const randomLabels = {};
+ for (let i = 1; i <= 7; i++) {
+ randomLabels[`r${i}`] = generateRandomString();
+ }
+
+ const labelArgs = Array.from({length: 7}, (_, i) => [`r${i+1}`, randomLabels[`r${i+1}`]]).flat();
+
+ cl.withLabels(() => {
+ processWithWorker(filePath, res, randomLabels);
+ }, "filePath", filePath, ...labelArgs);
+ });
+
+ server.listen(PORT, () => {
+ console.log(`Server running at http://localhost:${PORT}/`);
+ });
+}
+
+startServer();
diff --git a/interpreter/customlabels/integrationtests/testdata/node-md-render/package.json b/interpreter/customlabels/integrationtests/testdata/node-md-render/package.json
new file mode 100644
index 000000000..c87be3d1c
--- /dev/null
+++ b/interpreter/customlabels/integrationtests/testdata/node-md-render/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "md-render",
+ "version": "1.0.0",
+ "description": "Test program that renders .md files on a web interface",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "The Parca Authors",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@polarsignals/custom-labels": "^0.2.0",
+ "marked": "^16.1.2"
+ }
+}
diff --git a/interpreter/customlabels/integrationtests/testdata/node-md-render/worker.js b/interpreter/customlabels/integrationtests/testdata/node-md-render/worker.js
new file mode 100644
index 000000000..bf090359a
--- /dev/null
+++ b/interpreter/customlabels/integrationtests/testdata/node-md-render/worker.js
@@ -0,0 +1,100 @@
+const { parentPort, workerData } = require('worker_threads');
+const fs = require('fs/promises');
+const cl = require('@polarsignals/custom-labels');
+
+let marked;
+(async () => {
+ const markedModule = await import('marked');
+ marked = markedModule.marked;
+})();
+
+const workerId = workerData.workerId;
+
+function mysleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function myrand(min, max) {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+function f(data) {
+ return cl.withLabels(() => marked.parse(data.toString()), "i", "6");
+}
+
+function e(data) {
+ return cl.withLabels(() => f(data), "i", "5");
+}
+
+function d(data) {
+ return cl.withLabels(() => e(data), "i", "4");
+}
+
+function c(data) {
+ return cl.withLabels(() => d(data), "i", "3");
+}
+
+function b(data) {
+ return cl.withLabels(() => c(data), "i", "2");
+}
+
+function a(data) {
+ return cl.withLabels(() => b(data), "i", "1");
+}
+
+parentPort.on('message', async ({ filePath, requestId, randomLabels }) => {
+ try {
+ await cl.withLabels(async () => {
+ let data;
+ try {
+ data = await fs.readFile(filePath);
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ parentPort.postMessage({
+ requestId,
+ success: false,
+ error: 'File not found'
+ });
+ return;
+ }
+ throw error;
+ }
+
+ const dur = myrand(0, 1000);
+ await cl.withLabels(() => mysleep(dur), "sleepDur", "" + dur);
+
+ const md = filePath.endsWith(".md");
+
+ let content;
+ if (md) {
+ content =
+ cl.withLabels(() => a(data), "i", "0");
+ } else {
+ content = data.toString();
+ }
+
+ const htmlResponse = `
+
+ ${filePath}
+
+${content}
+
+
+ `;
+
+ parentPort.postMessage({
+ requestId,
+ success: true,
+ html: htmlResponse
+ });
+ }, "workerId", `${workerId}`, "filePath", filePath, ...Object.entries(randomLabels || {}).flat());
+ } catch (error) {
+ parentPort.postMessage({
+ requestId,
+ success: false,
+ error: error.message
+ });
+ }
+});
diff --git a/interpreter/golabels/test/main_test.go b/interpreter/golabels/test/main_test.go
index 050cdd6f9..61cb1d79a 100644
--- a/interpreter/golabels/test/main_test.go
+++ b/interpreter/golabels/test/main_test.go
@@ -22,7 +22,7 @@ func TestGoLabels(t *testing.T) {
r := &testutils.MockReporter{}
enabledTracers, _ := tracertypes.Parse("")
enabledTracers.Enable(tracertypes.Labels)
- traceCh, _ := testutils.StartTracer(context.Background(), t, enabledTracers, r)
+ traceCh, _ := testutils.StartTracer(context.Background(), t, enabledTracers, r, false)
for _, tc := range [][]string{
{"./golbls_1_23.test", "123"},
{"./golbls_1_24.test", "124"},
diff --git a/interpreter/luajit/luajit_test.go b/interpreter/luajit/luajit_test.go
index 32fe370d6..635b96fdb 100644
--- a/interpreter/luajit/luajit_test.go
+++ b/interpreter/luajit/luajit_test.go
@@ -120,7 +120,7 @@ func TestIntegration(t *testing.T) {
require.NoError(t, err)
enabledTracers.Enable(tracertypes.LuaJITTracer)
r := &mockReporter{symbols: make(symbolMap)}
- traceCh, trc := testutils.StartTracer(ctx, t, enabledTracers, r)
+ traceCh, trc := testutils.StartTracer(ctx, t, enabledTracers, r, false)
var waitGroup sync.WaitGroup
defer waitGroup.Wait()
diff --git a/interpreter/nodev8/node_offsets_generated.go b/interpreter/nodev8/node_offsets_generated.go
new file mode 100644
index 000000000..69f60952e
--- /dev/null
+++ b/interpreter/nodev8/node_offsets_generated.go
@@ -0,0 +1,114 @@
+// Code generated from complete_offsets.csv; DO NOT EDIT.
+
+package nodev8
+
+// nodeOffsets holds the Node.js environment offset data for specific versions
+type nodeOffsets struct {
+ contextHandle uint32
+ nativeContext uint32
+ embedderData uint32
+ environmentPointer uint32
+ executionAsyncId uint32
+}
+
+// nodeOffsetTable maps Node.js versions to their corresponding offsets
+// Data embedded from complete_offsets.csv
+var nodeOffsetTable = map[string]nodeOffsets{
+ "v20.0.0": { 280, 31, 47, 271, 1192 },
+ "v20.1.0": { 280, 31, 47, 271, 1208 },
+ "v20.2.0": { 280, 31, 47, 271, 1208 },
+ "v20.3.0": { 280, 31, 47, 271, 1208 },
+ "v20.3.1": { 280, 31, 47, 271, 1208 },
+ "v20.4.0": { 280, 31, 47, 271, 1200 },
+ "v20.5.0": { 280, 31, 47, 271, 1200 },
+ "v20.5.1": { 280, 31, 47, 271, 1200 },
+ "v20.6.0": { 280, 31, 47, 271, 1200 },
+ "v20.6.1": { 280, 31, 47, 271, 1200 },
+ "v20.7.0": { 280, 31, 47, 271, 1200 },
+ "v20.8.0": { 280, 31, 47, 271, 1032 },
+ "v20.8.1": { 280, 31, 47, 271, 1032 },
+ "v20.9.0": { 280, 31, 47, 271, 1032 },
+ "v20.10.0": { 280, 31, 47, 271, 1032 },
+ "v20.11.0": { 280, 31, 47, 271, 1032 },
+ "v20.11.1": { 280, 31, 47, 271, 1032 },
+ "v20.12.0": { 280, 31, 47, 271, 1136 },
+ "v20.12.1": { 280, 31, 47, 271, 1136 },
+ "v20.12.2": { 280, 31, 47, 271, 1136 },
+ "v20.13.0": { 280, 31, 47, 271, 1136 },
+ "v20.13.1": { 280, 31, 47, 271, 1136 },
+ "v20.14.0": { 280, 31, 47, 271, 1136 },
+ "v20.15.0": { 280, 31, 47, 271, 1136 },
+ "v20.15.1": { 280, 31, 47, 271, 1136 },
+ "v20.16.0": { 280, 31, 47, 271, 1136 },
+ "v20.17.0": { 280, 31, 47, 271, 1136 },
+ "v20.18.0": { 280, 31, 47, 271, 1136 },
+ "v20.18.1": { 280, 31, 47, 271, 1144 },
+ "v20.18.2": { 280, 31, 47, 271, 1144 },
+ "v20.18.3": { 280, 31, 47, 271, 1144 },
+ "v20.19.0": { 280, 31, 47, 271, 1152 },
+ "v20.19.1": { 280, 31, 47, 271, 1152 },
+ "v20.19.2": { 280, 31, 47, 271, 1152 },
+ "v20.19.3": { 280, 31, 47, 271, 1152 },
+ "v20.19.4": { 280, 31, 47, 271, 1152 },
+ "v21.0.0": { 288, 31, 47, 271, 1032 },
+ "v21.1.0": { 288, 31, 47, 271, 1032 },
+ "v21.2.0": { 288, 31, 47, 271, 1032 },
+ "v21.3.0": { 288, 31, 47, 271, 1032 },
+ "v21.4.0": { 288, 31, 47, 271, 1032 },
+ "v21.5.0": { 288, 31, 47, 271, 1032 },
+ "v21.6.0": { 288, 31, 47, 271, 1032 },
+ "v21.6.1": { 288, 31, 47, 271, 1032 },
+ "v21.6.2": { 288, 31, 47, 271, 1032 },
+ "v21.7.0": { 288, 31, 47, 271, 1136 },
+ "v21.7.1": { 288, 31, 47, 271, 1136 },
+ "v21.7.2": { 288, 31, 47, 271, 1136 },
+ "v21.7.3": { 288, 31, 47, 271, 1136 },
+ "v22.0.0": { 288, 31, 47, 271, 1136 },
+ "v22.1.0": { 288, 31, 47, 271, 1136 },
+ "v22.2.0": { 288, 31, 47, 271, 1136 },
+ "v22.3.0": { 288, 31, 47, 271, 1136 },
+ "v22.4.0": { 288, 31, 47, 271, 1136 },
+ "v22.4.1": { 288, 31, 47, 271, 1136 },
+ "v22.5.0": { 288, 31, 47, 271, 1144 },
+ "v22.5.1": { 288, 31, 47, 271, 1144 },
+ "v22.6.0": { 288, 31, 47, 271, 1144 },
+ "v22.7.0": { 288, 31, 47, 271, 1144 },
+ "v22.8.0": { 288, 31, 47, 271, 1144 },
+ "v22.9.0": { 288, 31, 47, 271, 1144 },
+ "v22.10.0": { 288, 31, 47, 271, 1152 },
+ "v22.11.0": { 288, 31, 47, 271, 1152 },
+ "v22.12.0": { 288, 31, 47, 271, 1152 },
+ "v22.13.0": { 288, 31, 47, 271, 1152 },
+ "v22.13.1": { 288, 31, 47, 271, 1152 },
+ "v22.14.0": { 288, 31, 47, 271, 1152 },
+ "v22.15.0": { 288, 31, 47, 271, 1152 },
+ "v22.15.1": { 288, 31, 47, 271, 1152 },
+ "v22.16.0": { 288, 31, 47, 271, 1152 },
+ "v22.17.0": { 288, 31, 47, 271, 1152 },
+ "v22.17.1": { 288, 31, 47, 271, 1152 },
+ "v22.18.0": { 288, 31, 47, 271, 1152 },
+ "v23.0.0": { 288, 31, 47, 271, 1152 },
+ "v23.1.0": { 288, 31, 47, 271, 1152 },
+ "v23.2.0": { 288, 31, 47, 271, 1152 },
+ "v23.3.0": { 288, 31, 47, 271, 1152 },
+ "v23.4.0": { 288, 31, 47, 271, 1152 },
+ "v23.5.0": { 288, 31, 47, 271, 1152 },
+ "v23.6.0": { 288, 31, 47, 271, 1152 },
+ "v23.6.1": { 288, 31, 47, 271, 1152 },
+ "v23.7.0": { 288, 31, 47, 271, 1152 },
+ "v23.8.0": { 288, 31, 47, 271, 1152 },
+ "v23.9.0": { 288, 31, 47, 271, 1152 },
+ "v23.10.0": { 288, 31, 47, 271, 1152 },
+ "v23.11.0": { 288, 31, 47, 271, 1152 },
+ "v23.11.1": { 288, 31, 47, 271, 1152 },
+ "v24.0.0": { 336, 31, 47, 271, 1160 },
+ "v24.0.1": { 336, 31, 47, 271, 1160 },
+ "v24.0.2": { 336, 31, 47, 271, 1160 },
+ "v24.1.0": { 336, 31, 47, 271, 1160 },
+ "v24.2.0": { 336, 31, 47, 271, 1160 },
+ "v24.3.0": { 336, 31, 47, 271, 1160 },
+ "v24.4.0": { 336, 31, 47, 271, 1160 },
+ "v24.4.1": { 336, 31, 47, 271, 1160 },
+ "v24.5.0": { 336, 31, 47, 271, 1160 },
+ "v24.6.0": { 336, 31, 47, 271, 1160 },
+}
diff --git a/interpreter/nodev8/v8.go b/interpreter/nodev8/v8.go
index 28bfa4ac6..4e90f2861 100644
--- a/interpreter/nodev8/v8.go
+++ b/interpreter/nodev8/v8.go
@@ -1,3 +1,5 @@
+//go:generate go run ../../tools/csv2go.go ../../tools/complete_offsets.csv node_offsets_generated.go
+
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0
@@ -154,6 +156,7 @@ package nodev8 // import "go.opentelemetry.io/ebpf-profiler/interpreter/nodev8"
import (
"bytes"
+ "encoding/binary"
"errors"
"fmt"
"hash/fnv"
@@ -487,6 +490,13 @@ type v8Data struct {
// version contains the V8 version
version uint32
+ // Node.js environment offsets
+ contextHandleOffset uint32
+ nativeContextOffset uint32
+ embedderDataOffset uint32
+ environmentPointerOffset uint32
+ executionAsyncIdOffset uint32
+
// bytecodeSizes contains the V8 bytecode length data
bytecodeSizes []byte
@@ -495,6 +505,9 @@ type v8Data struct {
// frametypeToID caches frametype's to a hash used as its identifier
frametypeToID [MaxFrameType]libpf.AddressOrLineno
+
+ // isolateSym is the symbol of the v8 thread-local current isolate
+ isolateSym libpf.Address
}
type v8Instance struct {
@@ -1816,6 +1829,12 @@ func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Add
data := C.V8ProcInfo{
version: C.uint(d.version),
+ context_handle_offset: C.uint(d.contextHandleOffset),
+ native_context_offset: C.uint(d.nativeContextOffset),
+ embedder_data_offset: C.uint(d.embedderDataOffset),
+ environment_pointer_offset: C.uint(d.environmentPointerOffset),
+ execution_async_id_offset: C.uint(d.executionAsyncIdOffset),
+
fp_marker: mapFramePointerOffset(vms.FramePointer.Context),
fp_function: mapFramePointerOffset(vms.FramePointer.Function),
fp_bytecode_offset: mapFramePointerOffset(vms.FramePointer.BytecodeOffset),
@@ -1837,6 +1856,8 @@ func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Add
codekind_shift: C.u8(vms.CodeKind.FieldShift),
codekind_mask: C.u8(vms.CodeKind.FieldMask),
codekind_baseline: C.u8(vms.CodeKind.Baseline),
+
+ isolate_sym: C.u64(d.isolateSym),
}
if err := ebpf.UpdateProcData(libpf.V8, pid, unsafe.Pointer(&data)); err != nil {
return nil, err
@@ -2134,6 +2155,56 @@ func (d *v8Data) readIntrospectionData(ef *pfelf.File) error {
return nil
}
+// loadNodeClData loads various offsets that are needed for custom labels handling.
+func (d *v8Data) loadNodeClData(ef *pfelf.File) error {
+ offset, err := ef.LookupTLSSymbolOffset("_ZN2v88internal18g_current_isolate_E")
+ if err != nil {
+ return err
+ }
+ d.isolateSym = libpf.Address(offset)
+
+ syms, err := ef.ReadSymbols()
+ if err != nil {
+ return fmt.Errorf("failed to read symbols: %w", err)
+ }
+
+ sym, err := syms.LookupSymbol("_ZZ21napi_get_node_versionE7version")
+ if err != nil {
+ return fmt.Errorf("failed to lookup Node version symbol: %w", err)
+ }
+
+ if sym == nil {
+ return errors.New("Node version symbol not found")
+ }
+
+ if sym.Size < 12 {
+ return fmt.Errorf("Node version symbol size too small: %d", sym.Size)
+ }
+
+ versBuf := make([]byte, 12)
+ if _, err = ef.ReadVirtualMemory(versBuf, int64(sym.Address)); err != nil {
+ return fmt.Errorf("failed to read Node version data: %w", err)
+ }
+
+ major := binary.LittleEndian.Uint32(versBuf[0:4])
+ minor := binary.LittleEndian.Uint32(versBuf[4:8])
+ patch := binary.LittleEndian.Uint32(versBuf[8:12])
+
+ // Construct version string and look up Node.js environment offsets
+ nodeVersion := fmt.Sprintf("v%d.%d.%d", major, minor, patch)
+ if offsets, found := nodeOffsetTable[nodeVersion]; found {
+ d.contextHandleOffset = offsets.contextHandle
+ d.nativeContextOffset = offsets.nativeContext
+ d.embedderDataOffset = offsets.embedderData
+ d.environmentPointerOffset = offsets.environmentPointer
+ d.executionAsyncIdOffset = offsets.executionAsyncId
+ } else {
+ return fmt.Errorf("no offsets found for Node.js version %s", nodeVersion)
+ }
+
+ return nil
+}
+
func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) {
if !v8Regex.MatchString(info.FileName()) {
return nil, nil
@@ -2202,6 +2273,10 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr
}
}
+ if err = d.loadNodeClData(ef); err != nil {
+ log.Warnf("Failed to load extra data for Node.js custom labels handling: %v", err)
+ }
+
// load introspection data
if err = d.readIntrospectionData(ef); err != nil {
return nil, err
diff --git a/metrics/ids.go b/metrics/ids.go
index c444b1915..29dcde960 100644
--- a/metrics/ids.go
+++ b/metrics/ids.go
@@ -680,6 +680,57 @@ const (
// Number of times we didn't find an entry for this process in the LuaJIT process info array
IDUnwindLuaJITErrNoProcInfo = 290
+ // Number of failures to read Node.js custom labels hashmap pointer
+ IDUnwindNodeClFailedReadHmPointer = 291
+
+ // Number of failures when no Labelset found in Node.js custom labels hashmap
+ IDUnwindNodeClFailedNoLsInHm = 292
+
+ // Number of failures to read Node.js custom labels hashmap structure
+ IDUnwindNodeClFailedReadHmStruct = 293
+
+ // Number of failures to read Node.js custom labels bucket
+ IDUnwindNodeClFailedReadBucket = 294
+
+ // Number of failures to read Node.js custom labels Labelset address
+ IDUnwindNodeClFailedReadLsAddr = 295
+
+ // Number of times too many buckets encountered in Node.js custom labels hashmap
+ IDUnwindNodeClFailedTooManyBuckets = 296
+
+ // Number of times we failed to get node.js execution async id
+ IDUnwindNodeClFailedGettingId = 297
+
+ // Number of times the node.js execution async id was zero
+ IDUnwindNodeClWarnIdZero = 298
+
+ // Number of failures to get TLS symbol address for Node.js isolate
+ IDUnwindNodeAsyncIdErrGetTlsSymbol = 299
+
+ // Number of failures to read Node.js isolate pointer
+ IDUnwindNodeAsyncIdErrReadIsolate = 300
+
+ // Number of failures to read Node.js context handle
+ IDUnwindNodeAsyncIdErrReadContextHandle = 301
+
+ // Number of failures to read Node.js real context handle
+ IDUnwindNodeAsyncIdErrReadRealContextHandle = 302
+
+ // Number of failures to read Node.js native context
+ IDUnwindNodeAsyncIdErrReadNativeContext = 303
+
+ // Number of failures to read Node.js embedder data
+ IDUnwindNodeAsyncIdErrReadEmbedderData = 304
+
+ // Number of failures to read Node.js environment pointer
+ IDUnwindNodeAsyncIdErrReadEnvPtr = 305
+
+ // Number of failures to read Node.js async ID field
+ IDUnwindNodeAsyncIdErrReadIdField = 306
+
+ // Number of failures to read Node.js async ID double value
+ IDUnwindNodeAsyncIdErrReadIdDouble = 307
+
// max number of ID values, keep this as *last entry*
- IDMax = 291
+ IDMax = 308
)
diff --git a/metrics/metrics.go b/metrics/metrics.go
index ec4776cbf..31678a5b6 100644
--- a/metrics/metrics.go
+++ b/metrics/metrics.go
@@ -17,6 +17,7 @@ import (
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/ebpf-profiler/libpf"
+ "go.opentelemetry.io/ebpf-profiler/reporter"
"go.opentelemetry.io/ebpf-profiler/vc"
)
@@ -48,8 +49,14 @@ var (
metric.WithInstrumentationVersion(vc.Version()))
counters = map[MetricID]metric.Int64Counter{}
gauges = map[MetricID]metric.Int64Gauge{}
+
+ reporterImpl reporter.MetricsReporter
)
+func SetReporter(r reporter.MetricsReporter) {
+ reporterImpl = r
+}
+
func init() {
defs := GetDefinitions()
metricTypes = make(map[MetricID]MetricType, len(defs))
@@ -87,6 +94,16 @@ func init() {
// Allow for report to be overridden in the test.
var report = func() {
ctx := context.Background()
+ if reporterImpl != nil {
+ ids := make([]uint32, nMetrics)
+ values := make([]int64, nMetrics)
+
+ for i := 0; i < nMetrics; i++ {
+ ids[i] = uint32(metricsBuffer[i].ID)
+ values[i] = int64(metricsBuffer[i].Value)
+ }
+ reporterImpl.ReportMetrics(uint32(prevTimestamp), ids, values)
+ }
for i := range nMetrics {
metric := metricsBuffer[i]
switch typ := metricTypes[metric.ID]; typ {
diff --git a/metrics/metrics.json b/metrics/metrics.json
index dc0eabf08..3f263a46b 100644
--- a/metrics/metrics.json
+++ b/metrics/metrics.json
@@ -2092,5 +2092,124 @@
"name": "UnwindLuaJITErrNoProcInfo",
"field": "bpf.luajit.errors.no_proc_info",
"id": 290
+ },
+ {
+ "description": "Number of failures to read Node.js custom labels hashmap pointer",
+ "type": "counter",
+ "name": "UnwindNodeClFailedReadHmPointer",
+ "field": "bpf.nodejs_custom_labels.errors.read_hm_pointer",
+ "id": 291
+ },
+ {
+ "description": "Number of failures when no Labelset found in Node.js custom labels hashmap",
+ "type": "counter",
+ "name": "UnwindNodeClFailedNoLsInHm",
+ "field": "bpf.nodejs_custom_labels.errors.no_ls_in_hm",
+ "id": 292
+ },
+ {
+ "description": "Number of failures to read Node.js custom labels hashmap structure",
+ "type": "counter",
+ "name": "UnwindNodeClFailedReadHmStruct",
+ "field": "bpf.nodejs_custom_labels.errors.read_hm_struct",
+ "id": 293
+ },
+ {
+ "description": "Number of failures to read Node.js custom labels bucket",
+ "type": "counter",
+ "name": "UnwindNodeClFailedReadBucket",
+ "field": "bpf.nodejs_custom_labels.errors.read_bucket",
+ "id": 294
+ },
+ {
+ "description": "Number of failures to read Node.js custom labels Labelset address",
+ "type": "counter",
+ "name": "UnwindNodeClFailedReadLsAddr",
+ "field": "bpf.nodejs_custom_labels.errors.read_ls_addr",
+ "id": 295
+ },
+ {
+ "description": "Number of times too many buckets encountered in Node.js custom labels hashmap",
+ "type": "counter",
+ "name": "UnwindNodeClFailedTooManyBuckets",
+ "field": "bpf.nodejs_custom_labels.errors.too_many_buckets",
+ "id": 296
+ },
+ {
+ "description": "Number of times we failed to get node.js execution async id",
+ "type": "counter",
+ "name": "UnwindNodeClFailedGettingId",
+ "field": "bpf.nodejs_custom_labels.errors.failed_getting_id",
+ "id": 297
+ },
+ {
+ "description": "Number of times the node.js execution async id was zero",
+ "type": "counter",
+ "name": "UnwindNodeClWarnIdZero",
+ "field": "bpf.nodejs_custom_labels.warnings.id_zero",
+ "id": 298
+ },
+ {
+ "description": "Number of failures to get TLS symbol address for Node.js isolate",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrGetTlsSymbol",
+ "field": "bpf.nodejs_async_id.errors.get_tls_symbol",
+ "id": 299
+ },
+ {
+ "description": "Number of failures to read Node.js isolate pointer",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrReadIsolate",
+ "field": "bpf.nodejs_async_id.errors.read_isolate",
+ "id": 300
+ },
+ {
+ "description": "Number of failures to read Node.js context handle",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrReadContextHandle",
+ "field": "bpf.nodejs_async_id.errors.read_context_handle",
+ "id": 301
+ },
+ {
+ "description": "Number of failures to read Node.js real context handle",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrReadRealContextHandle",
+ "field": "bpf.nodejs_async_id.errors.read_real_context_handle",
+ "id": 302
+ },
+ {
+ "description": "Number of failures to read Node.js native context",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrReadNativeContext",
+ "field": "bpf.nodejs_async_id.errors.read_native_context",
+ "id": 303
+ },
+ {
+ "description": "Number of failures to read Node.js embedder data",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrReadEmbedderData",
+ "field": "bpf.nodejs_async_id.errors.read_embedder_data",
+ "id": 304
+ },
+ {
+ "description": "Number of failures to read Node.js environment pointer",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrReadEnvPtr",
+ "field": "bpf.nodejs_async_id.errors.read_env_ptr",
+ "id": 305
+ },
+ {
+ "description": "Number of failures to read Node.js async ID field",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrReadIdField",
+ "field": "bpf.nodejs_async_id.errors.read_id_field",
+ "id": 306
+ },
+ {
+ "description": "Number of failures to read Node.js async ID double value",
+ "type": "counter",
+ "name": "UnwindNodeAsyncIdErrReadIdDouble",
+ "field": "bpf.nodejs_async_id.errors.read_id_double",
+ "id": 307
}
]
diff --git a/reporter/iface.go b/reporter/iface.go
index 695b283ec..7d56260cc 100644
--- a/reporter/iface.go
+++ b/reporter/iface.go
@@ -112,3 +112,7 @@ type HostMetadataReporter interface {
ReportHostMetadataBlocking(ctx context.Context, metadataMap map[string]string,
maxRetries int, waitRetry time.Duration) error
}
+
+type MetricsReporter interface {
+ ReportMetrics(timestamp uint32, ids []uint32, values []int64)
+}
diff --git a/support/ebpf/extmaps.h b/support/ebpf/extmaps.h
index 6191e2713..46a236e88 100644
--- a/support/ebpf/extmaps.h
+++ b/support/ebpf/extmaps.h
@@ -21,6 +21,8 @@ extern bpf_map_def trace_events;
extern bpf_map_def go_procs;
extern bpf_map_def go_labels_procs;
extern bpf_map_def cl_procs;
+extern bpf_map_def v8_procs;
+extern bpf_map_def v8_cached_env_ptrs;
#if defined(TESTING_COREDUMP)
@@ -52,7 +54,6 @@ extern bpf_map_def py_procs;
extern bpf_map_def ruby_procs;
extern bpf_map_def stack_delta_page_to_info;
extern bpf_map_def unwind_info_array;
-extern bpf_map_def v8_procs;
extern bpf_map_def luajit_procs;
#endif // TESTING_COREDUMP
diff --git a/support/ebpf/interpreter_dispatcher.ebpf.c b/support/ebpf/interpreter_dispatcher.ebpf.c
index dbe89922b..2ce66f7a0 100644
--- a/support/ebpf/interpreter_dispatcher.ebpf.c
+++ b/support/ebpf/interpreter_dispatcher.ebpf.c
@@ -147,8 +147,7 @@ bpf_map_def SEC("maps") cl_procs = {
.max_entries = 128,
};
-static inline __attribute__((__always_inline__)) void *
-get_m_ptr_legacy(struct GoCustomLabelsOffsets *offs, UnwindState *state)
+static EBPF_INLINE void *get_m_ptr_legacy(struct GoCustomLabelsOffsets *offs, UnwindState *state)
{
long res;
@@ -220,8 +219,7 @@ static EBPF_INLINE void *get_m_ptr(struct GoLabelsOffsets *offs, UnwindState *st
return m_ptr_addr;
}
-static inline __attribute__((__always_inline__)) void
-maybe_add_go_custom_labels_legacy(struct pt_regs *ctx, PerCPURecord *record)
+static EBPF_INLINE void maybe_add_go_custom_labels_legacy(struct pt_regs *ctx, PerCPURecord *record)
{
u32 pid = record->trace.pid;
// The Go label extraction code is too big to fit in this program, so we need to
@@ -269,53 +267,224 @@ static EBPF_INLINE void maybe_add_go_custom_labels(struct pt_regs *ctx, PerCPURe
tail_call(ctx, PROG_GO_LABELS);
}
-static inline __attribute__((__always_inline__)) bool
-get_native_custom_labels(PerCPURecord *record, NativeCustomLabelsProcInfo *proc)
+static EBPF_INLINE u64 addr_for_tls_symbol(u64 symbol, bool dtv)
{
u64 tsd_base;
if (tsd_get_base((void **)&tsd_base) != 0) {
increment_metric(metricID_UnwindNativeCustomLabelsErrReadTsdBase);
DEBUG_PRINT("cl: failed to get TSD base for native custom labels");
- return false;
+ return 0;
}
int err;
-#if defined(__aarch64__)
- // ELF Handling For Thread-Local Storage, p.5.
- // The thread register points to a "TCB" (Thread Control Block)
- // whose first element is a pointer to a "DTV" (Dynamic Thread Vector)...
- u64 dtv_addr;
- if ((err = bpf_probe_read_user(&dtv_addr, sizeof(void *), (void *)(tsd_base)))) {
- increment_metric(metricID_UnwindNativeCustomLabelsErrReadData);
- DEBUG_PRINT("Failed to read TLS DTV addr: %d", err);
+ u64 addr;
+ if (dtv) {
+ // ELF Handling For Thread-Local Storage, p.5.
+ // The thread register points to a "TCB" (Thread Control Block)
+ // whose first element is a pointer to a "DTV" (Dynamic Thread Vector)...
+ u64 dtv_addr;
+ if ((err = bpf_probe_read_user(&dtv_addr, sizeof(void *), (void *)(tsd_base)))) {
+ increment_metric(metricID_UnwindNativeCustomLabelsErrReadData);
+ DEBUG_PRINT("Failed to read TLS DTV addr: %d", err);
+ return 0;
+ }
+ // ... and at offsite 16 in the DTV, there is a pointer to the TLS block.
+ if ((err = bpf_probe_read_user(&addr, sizeof(void *), (void *)(dtv_addr + 16)))) {
+ increment_metric(metricID_UnwindNativeCustomLabelsErrReadData);
+ DEBUG_PRINT("Failed to read main TLS block addr: %d", err);
+ return 0;
+ }
+ addr += symbol;
+ } else {
+ addr = tsd_base + symbol;
+ }
+ return addr;
+}
+
+// Converts IEEE 754 double precision floating point bits to integer value.
+// Takes the raw 64-bit representation of a double and extracts the integer value.
+// Used for extracting Node.js async IDs stored as doubles in memory (in eBPF context,
+// where we can't use the actual "double" type)
+//
+// Assumptions:
+// - Input represents a non-negative integer value in the safe range
+// stored as a double (so no negatives, nothing 2^53 or higher,
+// no NaNs, infinities, or denormals)
+static EBPF_INLINE u64 integral_double_to_int(u64 bits)
+{
+ // Extract exponent (11 bits)
+ u64 exponent = (bits >> 52) & 0x7FF;
+ // Extract mantissa (52 bits)
+ u64 mantissa = bits & 0xFFFFFFFFFFFFF;
+
+ // Handle zero case
+ if (exponent == 0 && mantissa == 0) {
+ return 0;
+ }
+
+ // Add implicit leading 1
+ mantissa |= 1ULL << 52;
+
+ // Adjust exponent (bias is 1023)
+ s64 shift = exponent - 1023;
+
+ // Shift mantissa to get integer value
+ if (shift <= 52) {
+ return mantissa >> (52 - shift);
+ } else {
+ return mantissa << (shift - 52);
+ }
+}
+
+// From https://stackoverflow.com/a/12996028/242814
+// which got it from public-domain code.
+//
+// Changing this function is a breaking ABI change!
+static u64 custom_labels_hm_hash(u64 x)
+{
+ x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9UL;
+ x = (x ^ (x >> 27)) * 0x94d049bb133111ebUL;
+ x = x ^ (x >> 31);
+ return x;
+}
+
+// Extracts the Node.js environment pointer from V8 isolate by traversing
+// through V8 internal structures: isolate -> context_handle -> real_context_handle
+// -> native_context -> embedder_data -> env_ptr
+static EBPF_INLINE bool get_node_env_ptr(V8ProcInfo *proc, u64 *env_ptr_out)
+{
+ int err;
+
+ DEBUG_PRINT("context_handle_offset=0x%x", proc->context_handle_offset);
+ DEBUG_PRINT("native_context_offset=0x%x", proc->native_context_offset);
+ DEBUG_PRINT("embedder_data_offset=0x%x", proc->embedder_data_offset);
+ DEBUG_PRINT("environment_pointer_offset=0x%x", proc->environment_pointer_offset);
+ DEBUG_PRINT("execution_async_id_offset=0x%x", proc->execution_async_id_offset);
+
+ u64 isolate_addr = addr_for_tls_symbol(proc->isolate_sym, true);
+ DEBUG_PRINT("node custom labels: isolate_addr = 0x%llx", isolate_addr);
+ if (!isolate_addr) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrGetTlsSymbol);
return false;
}
- // ... and at offsite 16 in the DTV, there is a pointer to the TLS block.
- u64 addr;
- if ((err = bpf_probe_read_user(&addr, sizeof(void *), (void *)(dtv_addr + 16)))) {
- increment_metric(metricID_UnwindNativeCustomLabelsErrReadData);
- DEBUG_PRINT("Failed to read main TLS block addr: %d", err);
+
+ u64 isolate;
+ if ((err = bpf_probe_read_user(&isolate, sizeof(void *), (void *)(isolate_addr)))) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrReadIsolate);
+ DEBUG_PRINT("Failed to read node custom labels current set pointer: %d", err);
return false;
}
- addr += proc->tls_offset;
-#else
- u64 addr = tsd_base + proc->tls_offset;
-#endif
- DEBUG_PRINT("cl: native custom labels data at 0x%llx", addr);
+ u64 context_handle_ptr = isolate + proc->context_handle_offset;
+ DEBUG_PRINT("node custom labels: context_handle_ptr = 0x%llx", context_handle_ptr);
- NativeCustomLabelsSet *p_current_set;
- if ((err = bpf_probe_read_user(&p_current_set, sizeof(void *), (void *)(addr)))) {
- increment_metric(metricID_UnwindNativeCustomLabelsErrReadData);
- DEBUG_PRINT("Failed to read custom labels current set pointer: %d", err);
+ u64 context_handle;
+ if ((err = bpf_probe_read_user(&context_handle, sizeof(void *), (void *)(context_handle_ptr)))) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrReadContextHandle);
+ DEBUG_PRINT("Failed to read node custom labels current set pointer: %d", err);
return false;
}
+ DEBUG_PRINT("node custom labels: context_handle = 0x%llx", context_handle);
- if (!p_current_set) {
- DEBUG_PRINT("Null labelset");
- record->trace.custom_labels.len = 0;
- return true;
+ u64 context_ptr = context_handle - 1;
+ DEBUG_PRINT("node custom labels: context_ptr = 0x%llx", context_ptr);
+
+ u64 real_context_handle;
+ if ((err = bpf_probe_read_user(&real_context_handle, sizeof(void *), (void *)(context_ptr)))) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrReadRealContextHandle);
+ DEBUG_PRINT("Failed to read real context handle: %d", err);
+ return false;
+ }
+ DEBUG_PRINT("node custom labels: real_context_handle = 0x%llx", real_context_handle);
+
+ u64 native_context_ptr = real_context_handle + proc->native_context_offset;
+ DEBUG_PRINT("node custom labels: native_context_ptr = 0x%llx", native_context_ptr);
+
+ u64 native_context;
+ if ((err = bpf_probe_read_user(&native_context, sizeof(void *), (void *)(native_context_ptr)))) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrReadNativeContext);
+ DEBUG_PRINT("Failed to read native context: %d", err);
+ return false;
}
+ DEBUG_PRINT("node custom labels: native_context = 0x%llx", native_context);
+
+ u64 embedder_data_ptr = native_context + proc->embedder_data_offset;
+ DEBUG_PRINT("node custom labels: embedder_data_ptr = 0x%llx", embedder_data_ptr);
+
+ u64 embedder_data;
+ if ((err = bpf_probe_read_user(&embedder_data, sizeof(void *), (void *)(embedder_data_ptr)))) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrReadEmbedderData);
+ DEBUG_PRINT("Failed to read embedder data: %d", err);
+ return false;
+ }
+ DEBUG_PRINT("node custom labels: embedder_data = 0x%llx", embedder_data);
+
+ u64 env_ptr_ptr = embedder_data + proc->environment_pointer_offset;
+ DEBUG_PRINT("node custom labels: env_ptr_ptr = 0x%llx", env_ptr_ptr);
+
+ u64 env_ptr;
+ if ((err = bpf_probe_read_user(&env_ptr, sizeof(void *), (void *)(env_ptr_ptr)))) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrReadEnvPtr);
+ DEBUG_PRINT("Failed to read id field: %d", err);
+ return false;
+ }
+ DEBUG_PRINT("node custom labels: env_ptr = 0x%llx", env_ptr);
+
+ *env_ptr_out = env_ptr;
+ return true;
+}
+
+static EBPF_INLINE bool get_node_async_id(V8ProcInfo *proc, u32 tid, u64 *out)
+{
+ int err;
+
+ u64 env_ptr;
+ // Try to get fresh env_ptr
+ if (get_node_env_ptr(proc, &env_ptr)) {
+ bpf_map_update_elem(&v8_cached_env_ptrs, &tid, &env_ptr, BPF_ANY);
+ } else {
+ // Fallback to cached value from previous successful extraction
+ u64 *cached_env_ptr = bpf_map_lookup_elem(&v8_cached_env_ptrs, &tid);
+ if (cached_env_ptr && *cached_env_ptr != 0) {
+ // TODO[btv] -- Figure out why the environment is sometimes null.
+ // It doesn't seem to matter in practice, since the environment rarely (never?)
+ // changes, so using the cached version is fine, but it's worth understanding anyway...
+ DEBUG_PRINT("node custom labels: using cached env_ptr = 0x%llx", *cached_env_ptr);
+ env_ptr = *cached_env_ptr;
+ } else {
+ DEBUG_PRINT("node custom labels: no cached env_ptr available");
+ return false;
+ }
+ }
+
+ u64 id_field_ptr = env_ptr + proc->execution_async_id_offset;
+ DEBUG_PRINT("node custom labels: id_field_ptr = 0x%llx", id_field_ptr);
+
+ u64 id_field;
+ if ((err = bpf_probe_read_user(&id_field, sizeof(void *), (void *)(id_field_ptr)))) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrReadIdField);
+ DEBUG_PRINT("Failed to read id field: %d", err);
+ return false;
+ }
+ DEBUG_PRINT("node custom labels: id_field = 0x%llx", id_field);
+
+ u64 bits;
+ if ((err = bpf_probe_read_user(&bits, sizeof(u64), (void *)(id_field)))) {
+ increment_metric(metricID_UnwindNodeAsyncIdErrReadIdDouble);
+ DEBUG_PRINT("Failed to read id double: %d", err);
+ return false;
+ }
+ u64 id = integral_double_to_int(bits);
+ DEBUG_PRINT("node custom labels: id = %lld", id);
+ *out = id;
+
+ return true;
+}
+
+static EBPF_INLINE bool
+read_labelset_into_trace(PerCPURecord *record, NativeCustomLabelsSet *p_current_set)
+{
+ int err;
NativeCustomLabelsSet current_set;
if ((err = bpf_probe_read_user(¤t_set, sizeof(current_set), p_current_set))) {
@@ -329,7 +498,6 @@ get_native_custom_labels(PerCPURecord *record, NativeCustomLabelsProcInfo *proc)
unsigned ct = 0;
CustomLabelsArray *out = &record->trace.custom_labels;
-#pragma unroll
for (int i = 0; i < MAX_CUSTOM_LABELS; i++) {
if (i >= current_set.count)
break;
@@ -364,8 +532,40 @@ get_native_custom_labels(PerCPURecord *record, NativeCustomLabelsProcInfo *proc)
return true;
}
-static inline __attribute__((__always_inline__)) void
-maybe_add_native_custom_labels(PerCPURecord *record)
+static EBPF_INLINE bool
+get_native_custom_labels(PerCPURecord *record, NativeCustomLabelsProcInfo *proc)
+{
+ int err;
+ bool is_aarch64 =
+#if defined(__aarch64__)
+ true
+#else
+ false
+#endif
+ ;
+ u64 addr = addr_for_tls_symbol(proc->current_set_tls_offset, is_aarch64);
+ if (!addr)
+ return false;
+
+ DEBUG_PRINT("cl: native custom labels data at 0x%llx", addr);
+
+ NativeCustomLabelsSet *p_current_set;
+ if ((err = bpf_probe_read_user(&p_current_set, sizeof(void *), (void *)(addr)))) {
+ increment_metric(metricID_UnwindNativeCustomLabelsErrReadData);
+ DEBUG_PRINT("Failed to read custom labels current set pointer: %d", err);
+ return false;
+ }
+
+ if (!p_current_set) {
+ DEBUG_PRINT("Null labelset");
+ record->trace.custom_labels.len = 0;
+ return true;
+ }
+
+ return read_labelset_into_trace(record, p_current_set);
+}
+
+static EBPF_INLINE void maybe_add_native_custom_labels(PerCPURecord *record)
{
u32 pid = record->trace.pid;
NativeCustomLabelsProcInfo *proc = bpf_map_lookup_elem(&cl_procs, &pid);
@@ -381,7 +581,94 @@ maybe_add_native_custom_labels(PerCPURecord *record)
increment_metric(metricID_UnwindNativeCustomLabelsAddErrors);
}
-static inline __attribute__((__always_inline__)) void maybe_add_apm_info(Trace *trace)
+static EBPF_INLINE u64 get_labelset_for_async_id(u64 hm_addr, u64 id)
+{
+ u64 h = custom_labels_hm_hash(id);
+
+ NativeCustomLabelsHm hm;
+ if (bpf_probe_read_user(&hm, sizeof(hm), (void *)hm_addr)) {
+ increment_metric(metricID_UnwindNodeClFailedReadHmStruct);
+ DEBUG_PRINT("Failed to read hashmap structure");
+ return 0;
+ }
+
+ u64 capacity = 1ULL << hm.log2_capacity;
+
+ u64 labelset_rc_addr = 0;
+ int i;
+ const int MAX_BUCKETS = 32;
+ for (i = 0; i < MAX_BUCKETS; ++i) {
+ int pos = (h + i) % capacity;
+ NativeCustomLabelsHmBucket bucket;
+ if (bpf_probe_read_user(
+ &bucket,
+ sizeof(bucket),
+ (void *)((u64)hm.buckets + pos * sizeof(NativeCustomLabelsHmBucket)))) {
+ increment_metric(metricID_UnwindNodeClFailedReadBucket);
+ DEBUG_PRINT("Failed to read bucket at position %d", pos);
+ return 0;
+ }
+
+ if (!bucket.value || bucket.key == id) {
+ labelset_rc_addr = (u64)bucket.value;
+ break;
+ }
+ }
+ if (!labelset_rc_addr) {
+ if (i == MAX_BUCKETS)
+ increment_metric(metricID_UnwindNodeClFailedTooManyBuckets);
+ return 0;
+ }
+ u64 labelset_addr;
+ if (bpf_probe_read_user(&labelset_addr, sizeof(labelset_addr), (void *)labelset_rc_addr)) {
+ increment_metric(metricID_UnwindNodeClFailedReadLsAddr);
+ DEBUG_PRINT("Failed to read labelset addr");
+ return 0;
+ }
+ return labelset_addr;
+}
+
+// TODO - combine with native?
+static EBPF_INLINE void maybe_add_node_custom_labels(PerCPURecord *record)
+{
+ u32 pid = record->trace.pid;
+ V8ProcInfo *v8_proc = bpf_map_lookup_elem(&v8_procs, &pid);
+ NativeCustomLabelsProcInfo *proc = bpf_map_lookup_elem(&cl_procs, &pid);
+ if (!v8_proc || !proc || !proc->has_current_hm) {
+ DEBUG_PRINT("cl: %d does not support node custom labels ", pid);
+ return;
+ }
+ u64 hm_ptr_addr = addr_for_tls_symbol(proc->current_hm_tls_offset, false);
+ DEBUG_PRINT("hm pointer addr: 0x%llx", hm_ptr_addr);
+
+ u64 hm_addr;
+ if (bpf_probe_read_user(&hm_addr, sizeof(hm_addr), (void *)hm_ptr_addr)) {
+ increment_metric(metricID_UnwindNodeClFailedReadHmPointer);
+ DEBUG_PRINT("Failed to read hm pointer");
+ return;
+ }
+
+ u64 id;
+ bool success = get_node_async_id(v8_proc, record->trace.tid, &id);
+ if (success) {
+ if (id == 0) {
+ increment_metric(metricID_UnwindNodeClWarnIdZero);
+ }
+ u64 labelset_addr = get_labelset_for_async_id(hm_addr, id);
+ labelset_addr = (u64)labelset_addr;
+ if (labelset_addr) {
+ DEBUG_PRINT("cl: labelset addr is 0x%llx", labelset_addr);
+ read_labelset_into_trace(record, (NativeCustomLabelsSet *)labelset_addr);
+ } else {
+ increment_metric(metricID_UnwindNodeClFailedNoLsInHm);
+ DEBUG_PRINT("cl: No labelset found in hashmap for async id %lld", id);
+ }
+ } else {
+ increment_metric(metricID_UnwindNodeClFailedGettingId);
+ }
+}
+
+static EBPF_INLINE void maybe_add_apm_info(Trace *trace)
{
u32 pid = trace->pid; // verifier needs this to be on stack on 4.15 kernel
ApmIntProcInfo *proc = bpf_map_lookup_elem(&apm_int_procs, &pid);
@@ -441,9 +728,12 @@ static EBPF_INLINE int unwind_stop(struct pt_regs *ctx)
// Do Go first since we might tail call out and back again.
// Try legacy Go custom labels first, then new Go labels implementation
+ // TODO: maybe instead of adding a per-language call here, we
+ // should have "path to CLs" be a standard part of some per-pid map?
maybe_add_go_custom_labels_legacy(ctx, record);
maybe_add_go_custom_labels(ctx, record);
maybe_add_native_custom_labels(record);
+ maybe_add_node_custom_labels(record);
maybe_add_apm_info(trace);
// If the stack is otherwise empty, push an error for that: we should
diff --git a/support/ebpf/tracer.ebpf.amd64 b/support/ebpf/tracer.ebpf.amd64
index df0dc06cc..ba2ca3d65 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 a55b33d24..0a2fbc9e0 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 8d785cf2d..565ac8424 100644
--- a/support/ebpf/types.h
+++ b/support/ebpf/types.h
@@ -346,6 +346,24 @@ enum {
// number of failures to read Go labels (upstream)
metricID_UnwindGoLabelsFailures,
+ metricID_UnwindNodeClFailedReadHmPointer,
+ metricID_UnwindNodeClFailedNoLsInHm,
+ metricID_UnwindNodeClFailedReadHmStruct,
+ metricID_UnwindNodeClFailedReadBucket,
+ metricID_UnwindNodeClFailedReadLsAddr,
+ metricID_UnwindNodeClFailedTooManyBuckets,
+ metricID_UnwindNodeClFailedGettingId,
+ metricID_UnwindNodeClWarnIdZero,
+ metricID_UnwindNodeAsyncIdErrGetTlsSymbol,
+ metricID_UnwindNodeAsyncIdErrReadIsolate,
+ metricID_UnwindNodeAsyncIdErrReadContextHandle,
+ metricID_UnwindNodeAsyncIdErrReadRealContextHandle,
+ metricID_UnwindNodeAsyncIdErrReadNativeContext,
+ metricID_UnwindNodeAsyncIdErrReadEmbedderData,
+ metricID_UnwindNodeAsyncIdErrReadEnvPtr,
+ metricID_UnwindNodeAsyncIdErrReadIdField,
+ metricID_UnwindNodeAsyncIdErrReadIdDouble,
+
//
// Metric IDs above are for counters (cumulative values)
//
@@ -530,12 +548,19 @@ typedef struct RubyProcInfo {
// V8ProcInfo is a container for the data needed to build a stack trace for a V8 process.
typedef struct V8ProcInfo {
u32 version;
+ // Node.js environment offsets from complete_offsets.csv
+ u32 context_handle_offset;
+ u32 native_context_offset;
+ u32 embedder_data_offset;
+ u32 environment_pointer_offset;
+ u32 execution_async_id_offset;
// Introspection data
u16 type_JSFunction_first, type_JSFunction_last, type_Code, type_SharedFunctionInfo;
u8 off_HeapObject_map, off_Map_instancetype, off_JSFunction_code, off_JSFunction_shared;
u8 off_Code_instruction_start, off_Code_instruction_size, off_Code_flags;
u8 fp_marker, fp_function, fp_bytecode_offset;
u8 codekind_shift, codekind_mask, codekind_baseline;
+ u64 isolate_sym;
} V8ProcInfo;
typedef struct LuaJITProcInfo {
@@ -605,6 +630,16 @@ typedef struct NativeCustomLabelsThreadLocalData {
size_t capacity;
} NativeCustomLabelsSet;
+typedef struct {
+ u64 key;
+ void *value;
+} NativeCustomLabelsHmBucket;
+
+typedef struct {
+ NativeCustomLabelsHmBucket *buckets;
+ u64 log2_capacity;
+} NativeCustomLabelsHm;
+
#define MAX_CUSTOM_LABELS 10
typedef struct CustomLabelsArray {
@@ -1062,7 +1097,9 @@ typedef struct ApmIntProcInfo {
} ApmIntProcInfo;
typedef struct NativeCustomLabelsProcInfo {
- u64 tls_offset;
+ u64 current_set_tls_offset;
+ bool has_current_hm;
+ u64 current_hm_tls_offset;
} NativeCustomLabelsProcInfo;
typedef struct GoCustomLabelsOffsets {
diff --git a/support/ebpf/v8_tracer.ebpf.c b/support/ebpf/v8_tracer.ebpf.c
index 29b0e7004..50676e605 100644
--- a/support/ebpf/v8_tracer.ebpf.c
+++ b/support/ebpf/v8_tracer.ebpf.c
@@ -39,6 +39,14 @@ bpf_map_def SEC("maps") v8_procs = {
.max_entries = 1024,
};
+// Map from thread IDs to cached Node.js environment pointers
+bpf_map_def SEC("maps") v8_cached_env_ptrs = {
+ .type = BPF_MAP_TYPE_LRU_HASH,
+ .key_size = sizeof(u32), // TID
+ .value_size = sizeof(u64), // cached environment pointer
+ .max_entries = 4096, // more threads than processes
+};
+
// Record a V8 frame
static EBPF_INLINE ErrorCode push_v8(
Trace *trace, unsigned long pointer_and_type, unsigned long delta_or_marker, bool return_address)
diff --git a/support/types.go b/support/types.go
index fea51962c..fe9967ea8 100644
--- a/support/types.go
+++ b/support/types.go
@@ -51,7 +51,7 @@ const (
const MaxFrameUnwinds = 0x100
const (
- MetricIDBeginCumulative = 0x6f
+ MetricIDBeginCumulative = 0x80
)
const (
diff --git a/testutils/helpers.go b/testutils/helpers.go
index 6995bfb67..ae6d84833 100644
--- a/testutils/helpers.go
+++ b/testutils/helpers.go
@@ -43,7 +43,7 @@ func (f MockReporter) FrameKnown(_ libpf.FrameID) bool {
func (f MockReporter) FrameMetadata(_ *reporter.FrameMetadataArgs) {}
func StartTracer(ctx context.Context, t *testing.T, et tracertypes.IncludedTracers,
- r reporter.SymbolReporter) (chan *host.Trace, *tracer.Tracer) {
+ r reporter.SymbolReporter, printBpfLogs bool) (chan *host.Trace, *tracer.Tracer) {
trc, err := tracer.NewTracer(ctx, &tracer.Config{
CollectCustomLabels: true,
Reporter: r,
@@ -57,7 +57,9 @@ func StartTracer(ctx context.Context, t *testing.T, et tracertypes.IncludedTrace
})
require.NoError(t, err)
- go readTracePipe(ctx)
+ if printBpfLogs {
+ go readTracePipe(ctx)
+ }
trc.StartPIDEventProcessor(ctx)
diff --git a/tools/complete_offsets.csv b/tools/complete_offsets.csv
new file mode 100644
index 000000000..d2d9990e4
--- /dev/null
+++ b/tools/complete_offsets.csv
@@ -0,0 +1,98 @@
+Version,Context_Handle_Offset,Native_Context_Offset,Embedder_Data_Offset,Environment_Pointer_Offset,Execution_Async_Id_Offset
+v20.0.0,280,31,47,271,1192
+v20.1.0,280,31,47,271,1208
+v20.2.0,280,31,47,271,1208
+v20.3.0,280,31,47,271,1208
+v20.3.1,280,31,47,271,1208
+v20.4.0,280,31,47,271,1200
+v20.5.0,280,31,47,271,1200
+v20.5.1,280,31,47,271,1200
+v20.6.0,280,31,47,271,1200
+v20.6.1,280,31,47,271,1200
+v20.7.0,280,31,47,271,1200
+v20.8.0,280,31,47,271,1032
+v20.8.1,280,31,47,271,1032
+v20.9.0,280,31,47,271,1032
+v20.10.0,280,31,47,271,1032
+v20.11.0,280,31,47,271,1032
+v20.11.1,280,31,47,271,1032
+v20.12.0,280,31,47,271,1136
+v20.12.1,280,31,47,271,1136
+v20.12.2,280,31,47,271,1136
+v20.13.0,280,31,47,271,1136
+v20.13.1,280,31,47,271,1136
+v20.14.0,280,31,47,271,1136
+v20.15.0,280,31,47,271,1136
+v20.15.1,280,31,47,271,1136
+v20.16.0,280,31,47,271,1136
+v20.17.0,280,31,47,271,1136
+v20.18.0,280,31,47,271,1136
+v20.18.1,280,31,47,271,1144
+v20.18.2,280,31,47,271,1144
+v20.18.3,280,31,47,271,1144
+v20.19.0,280,31,47,271,1152
+v20.19.1,280,31,47,271,1152
+v20.19.2,280,31,47,271,1152
+v20.19.3,280,31,47,271,1152
+v20.19.4,280,31,47,271,1152
+v21.0.0,288,31,47,271,1032
+v21.1.0,288,31,47,271,1032
+v21.2.0,288,31,47,271,1032
+v21.3.0,288,31,47,271,1032
+v21.4.0,288,31,47,271,1032
+v21.5.0,288,31,47,271,1032
+v21.6.0,288,31,47,271,1032
+v21.6.1,288,31,47,271,1032
+v21.6.2,288,31,47,271,1032
+v21.7.0,288,31,47,271,1136
+v21.7.1,288,31,47,271,1136
+v21.7.2,288,31,47,271,1136
+v21.7.3,288,31,47,271,1136
+v22.0.0,288,31,47,271,1136
+v22.1.0,288,31,47,271,1136
+v22.2.0,288,31,47,271,1136
+v22.3.0,288,31,47,271,1136
+v22.4.0,288,31,47,271,1136
+v22.4.1,288,31,47,271,1136
+v22.5.0,288,31,47,271,1144
+v22.5.1,288,31,47,271,1144
+v22.6.0,288,31,47,271,1144
+v22.7.0,288,31,47,271,1144
+v22.8.0,288,31,47,271,1144
+v22.9.0,288,31,47,271,1144
+v22.10.0,288,31,47,271,1152
+v22.11.0,288,31,47,271,1152
+v22.12.0,288,31,47,271,1152
+v22.13.0,288,31,47,271,1152
+v22.13.1,288,31,47,271,1152
+v22.14.0,288,31,47,271,1152
+v22.15.0,288,31,47,271,1152
+v22.15.1,288,31,47,271,1152
+v22.16.0,288,31,47,271,1152
+v22.17.0,288,31,47,271,1152
+v22.17.1,288,31,47,271,1152
+v22.18.0,288,31,47,271,1152
+v23.0.0,288,31,47,271,1152
+v23.1.0,288,31,47,271,1152
+v23.2.0,288,31,47,271,1152
+v23.3.0,288,31,47,271,1152
+v23.4.0,288,31,47,271,1152
+v23.5.0,288,31,47,271,1152
+v23.6.0,288,31,47,271,1152
+v23.6.1,288,31,47,271,1152
+v23.7.0,288,31,47,271,1152
+v23.8.0,288,31,47,271,1152
+v23.9.0,288,31,47,271,1152
+v23.10.0,288,31,47,271,1152
+v23.11.0,288,31,47,271,1152
+v23.11.1,288,31,47,271,1152
+v24.0.0,336,31,47,271,1160
+v24.0.1,336,31,47,271,1160
+v24.0.2,336,31,47,271,1160
+v24.1.0,336,31,47,271,1160
+v24.2.0,336,31,47,271,1160
+v24.3.0,336,31,47,271,1160
+v24.4.0,336,31,47,271,1160
+v24.4.1,336,31,47,271,1160
+v24.5.0,336,31,47,271,1160
+v24.6.0,336,31,47,271,1160
diff --git a/tools/csv2go.go b/tools/csv2go.go
new file mode 100644
index 000000000..aab1bd6d2
--- /dev/null
+++ b/tools/csv2go.go
@@ -0,0 +1,154 @@
+package main
+
+import (
+ "encoding/csv"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "text/template"
+)
+
+//nolint:lll
+const templateStr = `// Code generated from complete_offsets.csv; DO NOT EDIT.
+
+package nodev8
+
+// nodeOffsets holds the Node.js environment offset data for specific versions
+type nodeOffsets struct {
+ contextHandle uint32
+ nativeContext uint32
+ embedderData uint32
+ environmentPointer uint32
+ executionAsyncId uint32
+}
+
+// nodeOffsetTable maps Node.js versions to their corresponding offsets
+// Data embedded from complete_offsets.csv
+var nodeOffsetTable = map[string]nodeOffsets{
+{{- range .}}
+ "{{.Version}}": { {{.ContextHandle}}, {{.NativeContext}}, {{.EmbedderData}}, {{.EnvironmentPointer}}, {{.ExecutionAsyncId}} },
+{{- end}}
+}
+`
+
+type OffsetData struct {
+ Version string
+ ContextHandle uint32
+ NativeContext uint32
+ EmbedderData uint32
+ EnvironmentPointer uint32
+ ExecutionAsyncId uint32
+}
+
+func main() {
+ if len(os.Args) != 3 {
+ fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0])
+ os.Exit(1)
+ }
+
+ inputFile := os.Args[1]
+ outputFile := os.Args[2]
+
+ // Read CSV file
+ file, err := os.Open(inputFile)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error opening input file: %v\n", err)
+ os.Exit(1)
+ }
+
+ reader := csv.NewReader(file)
+ records, err := reader.ReadAll()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error reading CSV: %v\n", err)
+ os.Exit(1)
+ }
+
+ if len(records) < 2 {
+ fmt.Fprintf(os.Stderr, "CSV file must have at least header and one data row\n")
+ os.Exit(1)
+ }
+
+ // Skip header row
+ var data []OffsetData
+ for i := 1; i < len(records); i++ {
+ record := records[i]
+ if len(record) != 6 {
+ fmt.Fprintf(os.Stderr,
+ "Invalid record at line %d: expected 6 columns, got %d\n",
+ i+1, len(record))
+ continue
+ }
+
+ var contextHandle, nativeContext, embedderData, environmentPointer, executionAsyncId uint64
+ contextHandle, err = strconv.ParseUint(strings.TrimSpace(record[1]), 10, 32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error parsing contextHandle at line %d: %v\n", i+1, err)
+ continue
+ }
+
+ nativeContext, err = strconv.ParseUint(strings.TrimSpace(record[2]), 10, 32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr,
+ "Error parsing nativeContext at line %d: %v\n",
+ i+1, err)
+ continue
+ }
+
+ embedderData, err = strconv.ParseUint(strings.TrimSpace(record[3]), 10, 32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error parsing embedderData at line %d: %v\n", i+1, err)
+ continue
+ }
+
+ environmentPointer, err = strconv.ParseUint(strings.TrimSpace(record[4]), 10, 32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error parsing environmentPointer at line %d: %v\n", i+1, err)
+ continue
+ }
+
+ executionAsyncId, err = strconv.ParseUint(strings.TrimSpace(record[5]), 10, 32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error parsing executionAsyncId at line %d: %v\n", i+1, err)
+ continue
+ }
+
+ data = append(data, OffsetData{
+ Version: strings.TrimSpace(record[0]),
+ ContextHandle: uint32(contextHandle),
+ NativeContext: uint32(nativeContext),
+ EmbedderData: uint32(embedderData),
+ EnvironmentPointer: uint32(environmentPointer),
+ ExecutionAsyncId: uint32(executionAsyncId),
+ })
+ }
+
+ // Create output directory if needed
+ if err = os.MkdirAll(filepath.Dir(outputFile), 0o755); err != nil {
+ fmt.Fprintf(os.Stderr, "Error creating output directory: %v\n", err)
+ os.Exit(1)
+ }
+
+ // Generate Go file
+ var tmpl *template.Template
+ tmpl, err = template.New("offsets").Parse(templateStr)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error parsing template: %v\n", err)
+ os.Exit(1)
+ }
+
+ var outFile *os.File
+ outFile, err = os.Create(outputFile)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err)
+ os.Exit(1)
+ }
+
+ if err = tmpl.Execute(outFile, data); err != nil {
+ fmt.Fprintf(os.Stderr, "Error executing template: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("Generated %s from %s with %d entries\n", outputFile, inputFile, len(data))
+}
diff --git a/tracer/ebpf_integration_test.go b/tracer/ebpf_integration_test.go
index eb7bb202d..5bf7410ee 100644
--- a/tracer/ebpf_integration_test.go
+++ b/tracer/ebpf_integration_test.go
@@ -234,5 +234,5 @@ Loop:
func TestAllTracers(t *testing.T) {
_, _ = testutils.StartTracer(context.Background(), t, tracertypes.AllTracers(),
- &testutils.MockReporter{})
+ &testutils.MockReporter{}, false)
}
diff --git a/tracer/tracer.go b/tracer/tracer.go
index 582dbfc30..68b02251d 100644
--- a/tracer/tracer.go
+++ b/tracer/tracer.go
@@ -1094,6 +1094,23 @@ func (t *Tracer) StartMapMonitors(ctx context.Context, traceOutChan chan<- *host
C.metricID_UnwindDotnetErrCodeTooLarge: metrics.IDUnwindDotnetErrCodeTooLarge,
C.metricID_UnwindLuaJITAttempts: metrics.IDUnwindLuaJITAttempts,
C.metricID_UnwindLuaJITErrNoProcInfo: metrics.IDUnwindLuaJITErrNoProcInfo,
+ C.metricID_UnwindNodeClFailedReadHmPointer: metrics.IDUnwindNodeClFailedReadHmPointer,
+ C.metricID_UnwindNodeClFailedNoLsInHm: metrics.IDUnwindNodeClFailedNoLsInHm,
+ C.metricID_UnwindNodeClFailedReadHmStruct: metrics.IDUnwindNodeClFailedReadHmStruct,
+ C.metricID_UnwindNodeClFailedReadBucket: metrics.IDUnwindNodeClFailedReadBucket,
+ C.metricID_UnwindNodeClFailedReadLsAddr: metrics.IDUnwindNodeClFailedReadLsAddr,
+ C.metricID_UnwindNodeClFailedTooManyBuckets: metrics.IDUnwindNodeClFailedTooManyBuckets,
+ C.metricID_UnwindNodeClFailedGettingId: metrics.IDUnwindNodeClFailedGettingId,
+ C.metricID_UnwindNodeClWarnIdZero: metrics.IDUnwindNodeClWarnIdZero,
+ C.metricID_UnwindNodeAsyncIdErrGetTlsSymbol: metrics.IDUnwindNodeAsyncIdErrGetTlsSymbol,
+ C.metricID_UnwindNodeAsyncIdErrReadIsolate: metrics.IDUnwindNodeAsyncIdErrReadIsolate,
+ C.metricID_UnwindNodeAsyncIdErrReadContextHandle: metrics.IDUnwindNodeAsyncIdErrReadContextHandle,
+ C.metricID_UnwindNodeAsyncIdErrReadRealContextHandle: metrics.IDUnwindNodeAsyncIdErrReadRealContextHandle,
+ C.metricID_UnwindNodeAsyncIdErrReadNativeContext: metrics.IDUnwindNodeAsyncIdErrReadNativeContext,
+ C.metricID_UnwindNodeAsyncIdErrReadEmbedderData: metrics.IDUnwindNodeAsyncIdErrReadEmbedderData,
+ C.metricID_UnwindNodeAsyncIdErrReadEnvPtr: metrics.IDUnwindNodeAsyncIdErrReadEnvPtr,
+ C.metricID_UnwindNodeAsyncIdErrReadIdField: metrics.IDUnwindNodeAsyncIdErrReadIdField,
+ C.metricID_UnwindNodeAsyncIdErrReadIdDouble: metrics.IDUnwindNodeAsyncIdErrReadIdDouble,
}
// previousMetricValue stores the previously retrieved metric values to