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