Skip to content

Commit b8f9998

Browse files
committed
build: pre-generate USDT probe header and add bpftrace CI
Implements the approach from the #62118 review discussion to remove the build-time 'dtrace' dependency on Linux: * Commit the SystemTap-generated probe header (src/node_provider_linux.h, regenerate with tools/usdt/generate_headers.py). USDT support is now on by default on Linux whenever <sys/sdt.h> is available (systemtap-sdt-dev on Debian/Ubuntu, systemtap-sdt-devel on Fedora/RHEL) and never needs a 'dtrace' tool at build time. A committed-header drift check runs in CI. * Make native DTrace opt-in on macOS via the new ./configure --with-dtrace; FreeBSD/illumos remain unsupported pending a 'dtrace -G' link step. --without-dtrace still disables probes everywhere. The always-on <sys/sdt.h> fallback tier is gone. * Add a path-gated test-usdt job to the Linux CI workflow with a default leg that runs the end-to-end bpftrace probe test as root, and a --without-dtrace leg that pins the no-op tier. The generated header is excluded from cpplint like src/node_root_certs.h. Signed-off-by: Bryan English <bryan@bryanenglish.com>
1 parent f47939d commit b8f9998

10 files changed

Lines changed: 410 additions & 106 deletions

File tree

‎.github/workflows/test-linux.yml‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,66 @@ jobs:
8787
./tools/test.py --flaky-tests keep_retrying -p actions -j 4
8888
env:
8989
DIR: dir%20with $unusual"chars?'åß∂ƒ©∆¬…`
90+
91+
# End-to-end coverage for the diagnostics_channel USDT probes:
92+
# a real bpftrace attach against a default (USDT-enabled) build, plus
93+
# a --without-dtrace build that pins the no-op tier. Only runs when
94+
# USDT-related paths change, so ordinary PRs do not pay for it.
95+
test-usdt:
96+
name: USDT probes (${{ matrix.cfg }})
97+
runs-on: ubuntu-24.04
98+
strategy:
99+
fail-fast: false
100+
matrix:
101+
cfg: ['default', 'without-dtrace']
102+
steps:
103+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
104+
with:
105+
persist-credentials: false
106+
path: node
107+
- name: Detect USDT-related changes
108+
id: changes
109+
run: |
110+
cd node
111+
if [ "${{ github.event_name }}" != "pull_request" ]; then
112+
echo "usdt=true" >> "$GITHUB_OUTPUT"
113+
exit 0
114+
fi
115+
git fetch --no-tags origin "+${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}"
116+
FILES=$(git diff --name-only "refs/remotes/origin/${{ github.base_ref }}...HEAD")
117+
echo "$FILES"
118+
if echo "$FILES" | grep -qE '^(src/node_(usdt|provider|diagnostics_channel)\.(h|cc|d)|src/node_provider_linux\.h|lib/diagnostics_channel\.js|tools/usdt/|test/parallel/test-diagnostics-channel-usdt.*\.js|\.github/workflows/test-linux\.yml|configure\.py|node\.gyp|doc/api/diagnostics_channel\.md)'; then
119+
echo "usdt=true" >> "$GITHUB_OUTPUT"
120+
else
121+
echo "usdt=false" >> "$GITHUB_OUTPUT"
122+
fi
123+
- name: Install bpftrace and systemtap-sdt-dev
124+
if: steps.changes.outputs.usdt == 'true'
125+
run: |
126+
sudo apt-get update
127+
sudo apt-get install -y --no-install-recommends bpftrace systemtap-sdt-dev
128+
- name: Check committed probe header is in sync
129+
if: steps.changes.outputs.usdt == 'true'
130+
run: |
131+
cd node
132+
python3 tools/usdt/generate_headers.py --check
133+
- name: Configure
134+
if: steps.changes.outputs.usdt == 'true'
135+
run: |
136+
cd node
137+
./configure ${{ matrix.cfg == 'without-dtrace' && '--without-dtrace' || '' }}
138+
- name: Build
139+
if: steps.changes.outputs.usdt == 'true'
140+
run: make -C node -j4
141+
- name: USDT binding tests
142+
if: steps.changes.outputs.usdt == 'true'
143+
run: |
144+
cd node
145+
python3 tools/test.py test/parallel/test-diagnostics-channel-usdt.js
146+
- name: bpftrace end-to-end test
147+
if: steps.changes.outputs.usdt == 'true' && matrix.cfg == 'default'
148+
# Run as root: the test skips itself when not root, and bpftrace
149+
# needs root to attach (BTF is available on GH-hosted images).
150+
run: |
151+
cd node
152+
sudo -E python3 tools/test.py test/parallel/test-diagnostics-channel-usdt-bpftrace.js

‎Makefile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,6 +1471,9 @@ LINT_CPP_ADDON_DOC_FILES_GLOB = test/addons/??_*/*.cc test/addons/??_*/*.h
14711471
LINT_CPP_ADDON_DOC_FILES = $(wildcard $(LINT_CPP_ADDON_DOC_FILES_GLOB))
14721472
LINT_CPP_EXCLUDE ?=
14731473
LINT_CPP_EXCLUDE += src/node_root_certs.h
1474+
# Generated output of the SystemTap dtrace wrapper, committed verbatim
1475+
# (regenerate with tools/usdt/generate_headers.py).
1476+
LINT_CPP_EXCLUDE += src/node_provider_linux.h
14741477
LINT_CPP_EXCLUDE += $(LINT_CPP_ADDON_DOC_FILES)
14751478
# These files were copied more or less verbatim from V8.
14761479
LINT_CPP_EXCLUDE += src/tracing/trace_event.h src/tracing/trace_event_common.h

‎configure.py‎

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,13 @@
948948
default=None,
949949
help='do not install the bundled Amaro (TypeScript utils)')
950950

951+
parser.add_argument('--with-dtrace',
952+
action='store_true',
953+
dest='with_dtrace',
954+
default=None,
955+
help='build with native DTrace/USDT probe support '
956+
'(opt-in on macOS; Linux probes need no dtrace tool)')
957+
951958
parser.add_argument('--without-dtrace',
952959
action='store_true',
953960
dest='without_dtrace',
@@ -1249,9 +1256,10 @@ def to_utf8(s):
12491256
def has_working_dtrace_h():
12501257
"""Check whether a dtrace tool that supports -h is available.
12511258
1252-
Supported on Linux (SystemTap dtrace wrapper), macOS, FreeBSD, and
1253-
illumos/SmartOS (native DTrace). Non-Linux platforms require -xnolibs
1254-
to avoid loading standard D libraries during header generation."""
1259+
Supported on macOS (native DTrace). Non-Linux platforms require
1260+
-xnolibs to avoid loading standard D libraries during header generation.
1261+
Linux never needs this check: the probe header is pre-generated and
1262+
committed (see tools/usdt/generate_headers.py)."""
12551263
dtrace = shutil.which('dtrace')
12561264
if dtrace is None:
12571265
return False
@@ -2008,14 +2016,39 @@ def configure_node(o):
20082016
o['variables']['node_builtin_modules_path'] = options.node_builtin_modules_path
20092017

20102018
o['variables']['node_no_usdt'] = b(options.without_dtrace)
2011-
use_dtrace = not options.without_dtrace and has_working_dtrace_h()
2019+
# USDT probe support for diagnostics_channel:
2020+
#
2021+
# * Linux: on by default whenever <sys/sdt.h> is available. The probe
2022+
# header is pre-generated and committed (src/node_provider_linux.h),
2023+
# so no dtrace tool is needed at build time.
2024+
# * macOS: opt-in via --with-dtrace; needs a working `dtrace -h` at
2025+
# build time (always present with Xcode/CLT).
2026+
# * FreeBSD/illumos: not supported yet; native DTrace there requires
2027+
# a `dtrace -G` link step that is not implemented.
2028+
if options.without_dtrace:
2029+
use_dtrace = False
2030+
elif options.with_dtrace:
2031+
if flavor == 'mac':
2032+
if not has_working_dtrace_h():
2033+
raise Exception('dtrace -h is not working; cannot use --with-dtrace')
2034+
use_dtrace = True
2035+
else:
2036+
use_dtrace = False
2037+
warn('--with-dtrace is only supported on macOS. On Linux, USDT '
2038+
'probes are enabled automatically whenever <sys/sdt.h> is '
2039+
'available.')
2040+
else:
2041+
use_dtrace = False
20122042
o['variables']['node_use_dtrace'] = b(use_dtrace)
20132043
if options.without_dtrace:
20142044
print('USDT probes: disabled (--without-dtrace)')
2045+
elif flavor == 'linux':
2046+
print('USDT probes: enabled when <sys/sdt.h> is available '
2047+
'(systemtap-sdt-dev on Debian/Ubuntu)')
20152048
elif use_dtrace:
2016-
print('USDT probes: enabled (dtrace -h, semaphore support)')
2049+
print('USDT probes: enabled (--with-dtrace, dtrace -h)')
20172050
else:
2018-
print('USDT probes: fallback (sys/sdt.h) or disabled')
2051+
print('USDT probes: disabled (enable with --with-dtrace)')
20192052

20202053
def configure_napi(output):
20212054
version = getnapibuildversion.get_napi_version()

‎doc/api/diagnostics_channel.md‎

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,26 +1143,32 @@ probe fires only if the channel has active subscribers.
11431143

11441144
#### Platform support
11451145

1146-
At `./configure` time, Node.js checks for a working `dtrace` tool and
1147-
uses `dtrace -h` to generate a probe header. Pass `--without-dtrace` to
1148-
`./configure` to disable probe support entirely.
1149-
1150-
* **Linux**: Install the `systemtap-sdt-dev` package (Debian/Ubuntu) or
1151-
`systemtap-sdt-devel` (Fedora/RHEL) before building Node.js. The
1152-
SystemTap `dtrace` wrapper generates a header with semaphore support,
1153-
giving the probe zero overhead when no tracer is attached.
1154-
* **macOS**: Supported natively via DTrace. The probe instruction is
1155-
patched to a no-op by the kernel when no tracer is attached, but the
1156-
JS-to-C++ call for `emitPublishProbe` is still incurred on every
1157-
publish to a string-named channel with subscribers.
1158-
* **FreeBSD**: Supported natively via DTrace, with the same
1159-
characteristics as macOS.
1160-
* **illumos/SmartOS**: Supported natively via DTrace, with the same
1161-
characteristics as macOS.
1162-
1163-
If `dtrace` is not found but `<sys/sdt.h>` is available, the probe falls
1164-
back to always-enabled mode. On platforms where neither is available,
1165-
the probe compiles to a no-op with zero runtime overhead.
1146+
USDT support is platform-gated and, on Linux, does not require a
1147+
`dtrace` tool at build time. Pass `--without-dtrace` to `./configure`
1148+
to disable probe support entirely.
1149+
1150+
* **Linux** (on by default): the probe header is pre-generated and
1151+
committed (`src/node_provider_linux.h`, regenerated with
1152+
`tools/usdt/generate_headers.py`), so only `<sys/sdt.h>` is required
1153+
at build time — install the `systemtap-sdt-dev` package
1154+
(Debian/Ubuntu) or `systemtap-sdt-devel` (Fedora/RHEL). The SystemTap
1155+
semaphore gives the probe effectively zero overhead when no tracer is
1156+
attached. When `<sys/sdt.h>` is absent, the probe silently compiles
1157+
to a no-op. A dedicated CI job runs an end-to-end bpftrace test on
1158+
Linux and verifies the committed header is in sync with
1159+
`src/node_provider.d`.
1160+
* **macOS** (opt-in): pass `--with-dtrace` to `./configure` to enable.
1161+
Requires a working `dtrace -h` at build time (always present with
1162+
Xcode/CLT). The probe instruction is patched to a no-op by the
1163+
kernel when no tracer is attached, but the JS-to-C++ call for
1164+
`emitPublishProbe` is still incurred on every publish to a
1165+
string-named channel with subscribers, which is why this tier is
1166+
opt-in.
1167+
* **FreeBSD/illumos**: not supported yet. Native DTrace there requires
1168+
a `dtrace -G` link step that is not implemented.
1169+
1170+
On platforms where probes are not available, they compile to no-ops
1171+
with zero runtime overhead.
11661172

11671173
#### Example: bpftrace (Linux)
11681174

@@ -1174,7 +1180,7 @@ sudo bpftrace -e '
11741180
' -c './out/Release/node app.js'
11751181
```
11761182

1177-
#### Example: DTrace (macOS/FreeBSD)
1183+
#### Example: DTrace (macOS)
11781184

11791185
```bash
11801186
sudo dtrace -n '

‎node.gyp‎

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@
276276
'src/node_diagnostics_channel.h',
277277
'src/node_usdt.h',
278278
'src/node_provider.d',
279+
'src/node_provider_linux.h',
279280
'src/node_modules.h',
280281
'src/node_object_wrap.h',
281282
'src/node_options.h',
@@ -956,37 +957,8 @@
956957
}],
957958
[ 'node_use_dtrace=="true"', {
958959
'defines': [ 'NODE_HAVE_DTRACE=1' ],
959-
'conditions': [
960-
[ 'OS=="linux"', {
961-
'actions': [
962-
{
963-
'action_name': 'node_dtrace_header',
964-
'inputs': [ 'src/node_provider.d' ],
965-
'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_provider.h' ],
966-
'action': [
967-
'dtrace', '-h',
968-
'-s', 'src/node_provider.d',
969-
'-o', '<(SHARED_INTERMEDIATE_DIR)/node_provider.h',
970-
],
971-
},
972-
],
973-
}, {
974-
# macOS, FreeBSD, illumos: native DTrace requires -xnolibs
975-
# to avoid loading kernel D libraries during header generation.
976-
'actions': [
977-
{
978-
'action_name': 'node_dtrace_header',
979-
'inputs': [ 'src/node_provider.d' ],
980-
'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_provider.h' ],
981-
'action': [
982-
'dtrace', '-h', '-xnolibs',
983-
'-s', 'src/node_provider.d',
984-
'-o', '<(SHARED_INTERMEDIATE_DIR)/node_provider.h',
985-
],
986-
},
987-
],
988-
}],
989-
],
960+
'dependencies': [ 'node_dtrace_header' ],
961+
'include_dirs': [ '<(SHARED_INTERMEDIATE_DIR)' ],
990962
}],
991963
[ 'node_builtin_modules_path!=""', {
992964
'defines': [ 'NODE_BUILTIN_MODULES_PATH="<(node_builtin_modules_path)"' ]
@@ -1566,6 +1538,33 @@
15661538
}],
15671539
]
15681540
}, # overlapped-checker
1541+
{
1542+
'target_name': 'node_dtrace_header',
1543+
'type': 'none',
1544+
'conditions': [
1545+
[ 'node_use_dtrace=="true"', {
1546+
'actions': [
1547+
{
1548+
# Native DTrace (macOS, opt-in via ./configure
1549+
# --with-dtrace): generate the probe header at build time.
1550+
# On Linux the probe header is pre-generated and committed
1551+
# at src/node_provider_linux.h, so no dtrace tool is
1552+
# needed there (see tools/usdt/generate_headers.py).
1553+
# -xnolibs avoids loading standard D libraries during
1554+
# header generation.
1555+
'action_name': 'node_dtrace_header',
1556+
'inputs': [ 'src/node_provider.d' ],
1557+
'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_provider.h' ],
1558+
'action': [
1559+
'dtrace', '-h', '-xnolibs',
1560+
'-s', '<@(_inputs)',
1561+
'-o', '<@(_outputs)',
1562+
],
1563+
},
1564+
],
1565+
} ],
1566+
],
1567+
}, # node_dtrace_header
15691568
{
15701569
'target_name': 'nop',
15711570
'type': 'executable',

‎src/node_diagnostics_channel.cc‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,15 @@
99

1010
#include <cstdint>
1111

12-
#if defined(NODE_HAVE_DTRACE) && defined(STAP_HAS_SEMAPHORES)
13-
// Definition of the USDT probe semaphore declared in the dtrace-generated
14-
// node_provider.h. STAP_HAS_SEMAPHORES is only defined by the SystemTap
15-
// dtrace wrapper (Linux), where the .probes ELF section attribute is valid.
16-
// On macOS/FreeBSD/illumos (native DTrace) there is no semaphore variable;
17-
// the kernel handles probe enabling directly. The generated header declares
18-
// The generated header declares this symbol with C++ linkage (no extern "C"
19-
// wrapper), so this definition must also use C++ linkage to ensure the
20-
// linker resolves the same mangled symbol.
21-
unsigned short node_dc__publish_semaphore
12+
#if NODE_HAVE_USDT && defined(NODE_USDT_HAVE_SEMAPHORE)
13+
// Definition of the USDT probe semaphore declared in the committed,
14+
// SystemTap-generated src/node_provider_linux.h (Linux Tier 1). The
15+
// .probes ELF section attribute is only valid there. On native DTrace
16+
// platforms there is no semaphore variable; the kernel handles probe
17+
// enabling directly. The generated header declares this symbol with
18+
// C++ linkage (no extern "C" wrapper), so this definition must also use
19+
// C++ linkage to ensure the linker resolves the same mangled symbol.
20+
unsigned short node_dc__publish_semaphore // NOLINT(runtime/int)
2221
__attribute__((section(".probes")));
2322
#endif
2423

@@ -153,7 +152,7 @@ void BindingData::SetupProbeSemaphore(Isolate* isolate,
153152
// a tracer is attached without crossing the JS/C++ boundary.
154153
auto backing = ArrayBuffer::NewBackingStore(
155154
NodeDCPublishSemaphore(),
156-
sizeof(unsigned short),
155+
sizeof(unsigned short), // NOLINT(runtime/int)
157156
[](void*, size_t, void*) {}, // no-op deleter — memory is static
158157
nullptr);
159158
Local<ArrayBuffer> ab = ArrayBuffer::New(isolate, std::move(backing));

‎src/node_provider_linux.h‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* This file is generated by tools/usdt/generate_headers.py from
3+
* src/node_provider.d using the SystemTap `dtrace` wrapper. Do not
4+
* edit it by hand.
5+
*
6+
* Regenerate with: python3 tools/usdt/generate_headers.py
7+
* The test-usdt CI job verifies that this file is in sync with
8+
* src/node_provider.d.
9+
*/
10+
11+
/* Generated by the Systemtap dtrace wrapper */
12+
13+
14+
#define _SDT_HAS_SEMAPHORES 1
15+
16+
17+
#define STAP_HAS_SEMAPHORES 1 /* deprecated */
18+
19+
20+
#include <sys/sdt.h>
21+
22+
/* NODE_DC_PUBLISH ( const char *, const void * ) */
23+
#if defined STAP_SDT_V1
24+
#define NODE_DC_PUBLISH_ENABLED() __builtin_expect (dc__publish_semaphore, 0)
25+
#define node_dc__publish_semaphore dc__publish_semaphore
26+
#else
27+
#define NODE_DC_PUBLISH_ENABLED() __builtin_expect (node_dc__publish_semaphore, 0)
28+
#endif
29+
__extension__ extern unsigned short node_dc__publish_semaphore __attribute__ ((unused)) __attribute__ ((section (".probes")));
30+
#define NODE_DC_PUBLISH(arg1, arg2) \
31+
DTRACE_PROBE2 (node, dc__publish, arg1, arg2)
32+

0 commit comments

Comments
 (0)