Skip to content

Commit bff0d78

Browse files
committed
Add AsyncLocalStorage/ContinuationPreservedEmbedderData-based CLs.
Introduce custom labels stored in Node's AsyncLocalStorage. We require that Node use its async_context_frame strategy for storing and propagating this data (which is the default beginning in Node v24, and requires the --experimental-async-context-frame flag in v22 and v23). This strategy works by storing the stored value in v8's ContinuationPreservedEmbedderData, which causes the runtime itself to handle propagation. We refuse to use the previous default AsyncLocalStorage propagation strategy, which is basically similar to our prior approach, which was rejected due to being too slow. The internal v8 data structures we walk are explained in the comment on maybe_add_node_custom_labels.
1 parent cd69146 commit bff0d78

File tree

13 files changed

+494
-17
lines changed

13 files changed

+494
-17
lines changed

interpreter/customlabels/customlabels.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,22 @@ import (
1616
const (
1717
abiVersionExport = "custom_labels_abi_version"
1818
currentSetTlsExport = "custom_labels_current_set"
19+
20+
alsIdentityHashExport = "custom_labels_als_identity_hash"
21+
alsHandleExport = "custom_labels_als_handle"
1922
)
2023

2124
var dsoRegex = regexp.MustCompile(`.*/libcustomlabels.*\.so`)
25+
var nodeRegex = regexp.MustCompile(`.*/customlabels\.node`)
2226

2327
type data struct {
2428
abiVersionElfVA libpf.Address
2529
currentSetTlsAddr libpf.Address
2630

31+
hasAlsData bool
32+
alsIdentityHashAddr libpf.Address
33+
alsHandleAddr libpf.Address
34+
2735
isSharedLibrary bool
2836
}
2937

@@ -53,9 +61,13 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete
5361
// symbol.
5462
fn := info.FileName()
5563
isNativeSharedLibrary := dsoRegex.MatchString(fn)
64+
isNodeExtension := (!isNativeSharedLibrary) && nodeRegex.MatchString(fn)
65+
isSharedLibrary := isNativeSharedLibrary || isNodeExtension
5666

5767
var currentSetTlsAddr libpf.Address
58-
if isNativeSharedLibrary {
68+
var alsIdentityHashAddr, alsHandleAddr libpf.Address
69+
var hasAlsId, hasAlsData bool
70+
if isSharedLibrary {
5971
// Resolve thread info TLS export.
6072
tlsDescs, err := ef.TLSDescriptors()
6173
if err != nil {
@@ -66,6 +78,12 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete
6678
if !ok {
6779
return nil, errors.New("failed to locate TLS descriptor for custom labels")
6880
}
81+
if isNodeExtension {
82+
alsIdentityHashAddr, hasAlsId = tlsDescs[alsIdentityHashExport]
83+
if hasAlsId {
84+
alsHandleAddr, hasAlsData = tlsDescs[alsHandleExport]
85+
}
86+
}
6987
} else {
7088
offset, err := ef.LookupTLSSymbolOffset(currentSetTlsExport)
7189
if err != nil {
@@ -75,9 +93,12 @@ func Loader(_ interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interprete
7593
}
7694

7795
d := data{
78-
abiVersionElfVA: libpf.Address(abiVersionSym.Address),
79-
currentSetTlsAddr: currentSetTlsAddr,
80-
isSharedLibrary: isNativeSharedLibrary,
96+
abiVersionElfVA: libpf.Address(abiVersionSym.Address),
97+
currentSetTlsAddr: currentSetTlsAddr,
98+
isSharedLibrary: isSharedLibrary,
99+
hasAlsData: hasAlsData,
100+
alsIdentityHashAddr: alsIdentityHashAddr,
101+
alsHandleAddr: alsHandleAddr,
81102
}
82103
return &d, nil
83104
}
@@ -107,8 +128,18 @@ func (d data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID,
107128
currentSetTlsOffset = uint64(d.currentSetTlsAddr)
108129
}
109130

131+
var alsIdentityHashTlsOffset, alsHandleTlsOffset uint64
132+
if d.hasAlsData {
133+
alsIdentityHashTlsOffset = rm.Uint64(bias + d.alsIdentityHashAddr + 8)
134+
alsHandleTlsOffset = rm.Uint64(bias + d.alsHandleAddr + 8)
135+
}
136+
110137
procInfo := support.NativeCustomLabelsProcInfo{
111-
Set_tls_offset: currentSetTlsOffset,
138+
Current_set_tls_offset: currentSetTlsOffset,
139+
140+
Has_als_data: d.hasAlsData,
141+
Als_identity_hash_tls_offset: alsIdentityHashTlsOffset,
142+
Als_handle_tls_offset: alsHandleTlsOffset,
112143
}
113144
if err := ebpf.UpdateProcData(libpf.CustomLabels, pid, unsafe.Pointer(&procInfo)); err != nil {
114145
return nil, err

interpreter/customlabels/integrationtests/node_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ func TestIntegration(t *testing.T) {
6363
// is on v25 by then.
6464
"latest",
6565
"22.19.0",
66-
"20.19.5",
6766
} {
6867
name := "node-" + nodeVersion
6968
t.Run(name, func(t *testing.T) {

interpreter/customlabels/integrationtests/testdata/node-md-render/Dockerfile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ARG NODE_VERSION=20
1+
ARG NODE_VERSION=22
22
FROM node:${NODE_VERSION}
33

44
# Install build dependencies for native modules
@@ -25,5 +25,9 @@ COPY *.js ./
2525
# Expose ports for app and gdbserver
2626
EXPOSE 80
2727

28-
# Start the application
29-
CMD ["node", "index.js"]
28+
# Start the application with conditional flag for Node < 24
29+
CMD if [ $(node -v | cut -c2- | cut -d. -f1) -lt 24 ]; then \
30+
exec node --experimental-async-context-frame index.js; \
31+
else \
32+
exec node index.js; \
33+
fi

interpreter/customlabels/integrationtests/testdata/node-md-render/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"author": "The Parca Authors",
1010
"license": "Apache-2.0",
1111
"dependencies": {
12-
"@polarsignals/custom-labels": "^0.2.0",
12+
"@polarsignals/custom-labels": "^0.3.2",
1313
"marked": "^16.1.2"
1414
}
1515
}

interpreter/nodev8/v8.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ package nodev8 // import "go.opentelemetry.io/ebpf-profiler/interpreter/nodev8"
154154

155155
import (
156156
"bytes"
157+
"encoding/binary"
157158
"errors"
158159
"fmt"
159160
"io"
@@ -480,6 +481,17 @@ type v8Data struct {
480481

481482
// frametypeToName caches frametype's name
482483
frametypeToName [MaxFrameType]libpf.String
484+
485+
// isolateSym is the symbol of the v8 thread-local current isolate
486+
isolateSym libpf.Address
487+
488+
// cpedOffset is the offset of continuation_preserved_embedder_data_ in
489+
// the isolate data.
490+
cpedOffset uint32
491+
492+
// wrappedObjectOffset is the offset of a wrapped (via ObjectWrap) C++ object
493+
// in the corresponding JS object.
494+
wrappedObjectOffset uint32
483495
}
484496

485497
type v8Instance struct {
@@ -1782,6 +1794,9 @@ func (d *v8Data) Attach(ebpf interpreter.EbpfHandler, pid libpf.PID, _ libpf.Add
17821794
Codekind_shift: vms.CodeKind.FieldShift,
17831795
Codekind_mask: uint8(vms.CodeKind.FieldMask),
17841796
Codekind_baseline: vms.CodeKind.Baseline,
1797+
Isolate_sym: uint64(d.isolateSym),
1798+
Cped_offset: d.cpedOffset,
1799+
Wrapped_object_offset: d.wrappedObjectOffset,
17851800
}
17861801
if err := ebpf.UpdateProcData(libpf.V8, pid, unsafe.Pointer(&data)); err != nil {
17871802
return nil, err
@@ -2079,6 +2094,52 @@ func (d *v8Data) readIntrospectionData(ef *pfelf.File) error {
20792094
return nil
20802095
}
20812096

2097+
// loadNodeClData loads various offsets that are needed for custom labels handling.
2098+
func (d *v8Data) loadNodeClData(ef *pfelf.File) error {
2099+
offset, err := ef.LookupTLSSymbolOffset("_ZN2v88internal18g_current_isolate_E")
2100+
if err != nil {
2101+
return err
2102+
}
2103+
d.isolateSym = libpf.Address(offset)
2104+
2105+
syms, err := ef.ReadSymbols()
2106+
if err != nil {
2107+
return fmt.Errorf("failed to read symbols: %w", err)
2108+
}
2109+
2110+
sym, err := syms.LookupSymbol("_ZZ21napi_get_node_versionE7version")
2111+
if err != nil {
2112+
return fmt.Errorf("failed to lookup Node version symbol: %w", err)
2113+
}
2114+
2115+
if sym == nil {
2116+
return errors.New("Node version symbol not found")
2117+
}
2118+
2119+
if sym.Size < 12 {
2120+
return fmt.Errorf("Node version symbol size too small: %d", sym.Size)
2121+
}
2122+
2123+
versBuf := make([]byte, 12)
2124+
if _, err = ef.ReadVirtualMemory(versBuf, int64(sym.Address)); err != nil {
2125+
return fmt.Errorf("failed to read Node version data: %w", err)
2126+
}
2127+
2128+
major := binary.LittleEndian.Uint32(versBuf[0:4])
2129+
2130+
if major >= 22 {
2131+
if major < 24 {
2132+
d.cpedOffset = 576
2133+
d.wrappedObjectOffset = 24
2134+
} else {
2135+
d.cpedOffset = 632
2136+
d.wrappedObjectOffset = 32
2137+
}
2138+
return nil
2139+
}
2140+
return fmt.Errorf("Unsupported Node major version: %d", major)
2141+
}
2142+
20822143
func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpreter.Data, error) {
20832144
if !v8Regex.MatchString(info.FileName()) {
20842145
return nil, nil
@@ -2147,6 +2208,10 @@ func Loader(ebpf interpreter.EbpfHandler, info *interpreter.LoaderInfo) (interpr
21472208
}
21482209
}
21492210

2211+
if err = d.loadNodeClData(ef); err != nil {
2212+
log.Warnf("Failed to load extra data for Node.js custom labels handling: %v", err)
2213+
}
2214+
21502215
// load introspection data
21512216
if err = d.readIntrospectionData(ef); err != nil {
21522217
return nil, err

metrics/ids.go

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

metrics/metrics.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2093,11 +2093,32 @@
20932093
"field": "bpf.luajit.errors.no_proc_info",
20942094
"id": 290
20952095
},
2096+
{
2097+
"description": "Number of attempts to read Node.js custom labels",
2098+
"type": "counter",
2099+
"name": "UnwindNodeCustomLabelsAttempts",
2100+
"field": "bpf.node_custom_labels.attempts",
2101+
"id": 291
2102+
},
2103+
{
2104+
"description": "Number of successful reads of Node.js custom labels",
2105+
"type": "counter",
2106+
"name": "UnwindNodeCustomLabelsSuccesses",
2107+
"field": "bpf.node_custom_labels.successes",
2108+
"id": 292
2109+
},
2110+
{
2111+
"description": "Number of failed attempts to read Node.js custom labels",
2112+
"type": "counter",
2113+
"name": "UnwindNodeCustomLabelsFailures",
2114+
"field": "bpf.node_custom_labels.failures",
2115+
"id": 293
2116+
},
20962117
{
20972118
"description": "Number of times rtld:map_complete USDT probe was fired",
20982119
"type": "counter",
20992120
"name": "RtldMapCompleteHits",
21002121
"field": "bpf.rtld.map_complete_hits",
2101-
"id": 291
2122+
"id": 294
21022123
}
21032124
]

0 commit comments

Comments
 (0)