Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/test-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,83 @@ jobs:
./tools/test.py --flaky-tests keep_retrying -p actions -j 4
env:
DIR: dir%20with $unusual"chars?'åß∂ƒ©∆¬…`

# End-to-end coverage for the diagnostics_channel USDT probes:
# a real bpftrace attach against a default (USDT-enabled) build, plus
# a --without-dtrace build that pins the no-op tier. Only runs when
# USDT-related paths change, so ordinary PRs do not pay for it.
test-usdt:
name: USDT probes (${{ matrix.cfg }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
cfg: [default, without-dtrace]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
fetch-depth: 2
path: node
- name: Detect USDT-related changes
id: changes
run: |
cd node
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "usdt=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# For pull_request events, HEAD is GitHub's merge ref; HEAD^1 is the
# tip of the base branch, so no additional fetch is needed.
FILES=$(git diff --name-only HEAD^1 HEAD)
echo "$FILES"
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
echo "usdt=true" >> "$GITHUB_OUTPUT"
else
echo "usdt=false" >> "$GITHUB_OUTPUT"
fi
- name: Install Clang ${{ env.CLANG_VERSION }}
if: steps.changes.outputs.usdt == 'true'
uses: ./node/.github/actions/install-clang
with:
clang-version: ${{ env.CLANG_VERSION }}
- name: Install Rust ${{ env.RUSTC_VERSION }}
if: steps.changes.outputs.usdt == 'true'
run: |
rustup override set "$RUSTC_VERSION"
rustup --version
- name: Set up sccache
if: steps.changes.outputs.usdt == 'true' && (github.base_ref == 'main' || github.ref_name == 'main')
uses: Mozilla-Actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
with:
version: v0.17.0
- name: Install bpftrace and systemtap-sdt-dev
if: steps.changes.outputs.usdt == 'true'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends bpftrace systemtap-sdt-dev
- name: Check committed probe header is in sync
if: steps.changes.outputs.usdt == 'true'
run: |
cd node
python3 tools/usdt/generate_headers.py --check
- name: Configure
if: steps.changes.outputs.usdt == 'true'
run: |
cd node
./configure ${{ matrix.cfg == 'without-dtrace' && '--without-dtrace' || '' }}
- name: Build
if: steps.changes.outputs.usdt == 'true'
run: make -C node -j4
- name: USDT binding tests
if: steps.changes.outputs.usdt == 'true'
run: |
cd node
python3 tools/test.py test/parallel/test-diagnostics-channel-usdt.js
- name: bpftrace end-to-end test
if: steps.changes.outputs.usdt == 'true' && matrix.cfg == 'default'
# Run as root: the test skips itself when not root, and bpftrace
# needs root to attach (BTF is available on GH-hosted images).
run: |
cd node
sudo -E python3 tools/test.py test/parallel/test-diagnostics-channel-usdt-bpftrace.js
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,9 @@ LINT_CPP_ADDON_DOC_FILES_GLOB = test/addons/??_*/*.cc test/addons/??_*/*.h
LINT_CPP_ADDON_DOC_FILES = $(wildcard $(LINT_CPP_ADDON_DOC_FILES_GLOB))
LINT_CPP_EXCLUDE ?=
LINT_CPP_EXCLUDE += src/node_root_certs.h
# Generated output of the SystemTap dtrace wrapper, committed verbatim
# (regenerate with tools/usdt/generate_headers.py).
LINT_CPP_EXCLUDE += src/node_provider_linux.h
LINT_CPP_EXCLUDE += $(LINT_CPP_ADDON_DOC_FILES)
# These files were copied more or less verbatim from V8.
LINT_CPP_EXCLUDE += src/tracing/trace_event_legacy.h src/tracing/trace_event_legacy_inl.h
Expand Down
74 changes: 74 additions & 0 deletions configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,19 @@
default=None,
help='do not install the bundled Amaro (TypeScript utils)')

parser.add_argument('--with-dtrace',
action='store_true',
dest='with_dtrace',
default=None,
help='build with native DTrace/USDT probe support '
'(opt-in on macOS; Linux probes need no dtrace tool)')

parser.add_argument('--without-dtrace',
action='store_true',
dest='without_dtrace',
default=None,
help='build without DTrace/USDT probe support')

parser.add_argument('--without-lief',
action='store_true',
dest='without_lief',
Expand Down Expand Up @@ -1360,6 +1373,32 @@ def B(value):
def to_utf8(s):
return s if isinstance(s, str) else s.decode("utf-8")

def has_working_dtrace_h():
"""Check whether a dtrace tool that supports -h is available.

Supported on macOS (native DTrace). Non-Linux platforms require
-xnolibs to avoid loading standard D libraries during header generation.
Linux never needs this check: the probe header is pre-generated and
committed (see tools/usdt/generate_headers.py)."""
dtrace = shutil.which('dtrace')
if dtrace is None:
return False
# -xnolibs is required on macOS/FreeBSD/illumos (native DTrace) to avoid
# loading standard D libraries. Linux (SystemTap wrapper) does not
# recognise this flag, so only pass it on non-Linux platforms.
cmd = [dtrace, '-h', '-s', '/dev/stdin', '-o', '/dev/null']
if sys.platform != 'linux':
cmd.insert(2, '-xnolibs')
try:
proc = subprocess.run(
cmd,
input=b'provider _test { probe _test(); };',
capture_output=True, timeout=10)
return proc.returncode == 0
except (OSError, subprocess.TimeoutExpired) as e:
warn('dtrace probe check failed: %s' % e)
return False

def pkg_config(pkg):
"""Run pkg-config on the specified package
Returns ("-l flags", "-I flags", "-L flags", "version")
Expand Down Expand Up @@ -2180,6 +2219,41 @@ def configure_node(o):
print('Warning! Loading builtin modules from disk is for development')
o['variables']['node_builtin_modules_path'] = options.node_builtin_modules_path

o['variables']['node_no_usdt'] = b(options.without_dtrace)
# USDT probe support for diagnostics_channel:
#
# * Linux: on by default whenever <sys/sdt.h> is available. The probe
# header is pre-generated and committed (src/node_provider_linux.h),
# so no dtrace tool is needed at build time.
# * macOS: opt-in via --with-dtrace; needs a working `dtrace -h` at
# build time (always present with Xcode/CLT).
# * FreeBSD/illumos: not supported yet; native DTrace there requires
# a `dtrace -G` link step that is not implemented.
if options.without_dtrace:
use_dtrace = False
elif options.with_dtrace:
if flavor == 'mac':
if not has_working_dtrace_h():
raise Exception('dtrace -h is not working; cannot use --with-dtrace')
use_dtrace = True
else:
use_dtrace = False
warn('--with-dtrace is only supported on macOS. On Linux, USDT '
'probes are enabled automatically whenever <sys/sdt.h> is '
'available.')
else:
use_dtrace = False
o['variables']['node_use_dtrace'] = b(use_dtrace)
if options.without_dtrace:
print('USDT probes: disabled (--without-dtrace)')
elif flavor == 'linux':
print('USDT probes: enabled when <sys/sdt.h> is available '
'(systemtap-sdt-dev on Debian/Ubuntu)')
elif use_dtrace:
print('USDT probes: enabled (--with-dtrace, dtrace -h)')
else:
print('USDT probes: disabled (enable with --with-dtrace)')

def configure_napi(output):
version = getnapibuildversion.get_napi_version()
output['variables']['napi_build_version'] = version
Expand Down
78 changes: 78 additions & 0 deletions doc/api/diagnostics_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,84 @@ another async task is triggered internally which fails and then the sync part
of the function then throws and error two `error` events will be emitted, one
for the sync error and one for the async error.

### USDT probes

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

Node.js exposes a USDT (User-Level Statically Defined Tracing) probe for
diagnostics channel publish events, enabling external observability tools
such as `bpftrace`, DTrace, and `perf` to trace channel activity without
modifying application code or adding JavaScript subscribers.

#### Probe: `node:dc__publish`

Fired when a message is published to a string-named diagnostics channel.
When published from native (C++) code and a tracer is attached, the probe
fires regardless of subscriber state. When published from JavaScript, the
probe fires only if the channel has active subscribers.

* `arg0` {const char\*} The channel name (UTF-8).
* `arg1` {const void\*} An opaque pointer to the V8 message object, or `NULL`
if the published message is not a JavaScript object (e.g., a string, number,
or `null`). **Warning:** This pointer is unstable and must NOT be
dereferenced by tracing scripts. V8's garbage collector may move the
underlying object at any time. The pointer is valid only for the
duration of the probe callback and must not be stored or compared
across separate probe firings.

#### Platform support

USDT support is platform-gated and, on Linux, does not require a
`dtrace` tool at build time. Pass `--without-dtrace` to `./configure`
to disable probe support entirely.

* **Linux** (on by default): the probe header is pre-generated and
committed (`src/node_provider_linux.h`, regenerated with
`tools/usdt/generate_headers.py`), so only `<sys/sdt.h>` is required
at build time — install the `systemtap-sdt-dev` package
(Debian/Ubuntu) or `systemtap-sdt-devel` (Fedora/RHEL). The SystemTap
semaphore gives the probe effectively zero overhead when no tracer is
attached. When `<sys/sdt.h>` is absent, the probe silently compiles
to a no-op. A dedicated CI job runs an end-to-end bpftrace test on
Linux and verifies the committed header is in sync with
`src/node_provider.d`.
* **macOS** (opt-in): pass `--with-dtrace` to `./configure` to enable.
Requires a working `dtrace -h` at build time (always present with
Xcode/CLT). The probe instruction is patched to a no-op by the
kernel when no tracer is attached, but the JS-to-C++ call for
`emitPublishProbe` is still incurred on every publish to a
string-named channel with subscribers, which is why this tier is
opt-in.
* **FreeBSD/illumos**: not supported yet. Native DTrace there requires
a `dtrace -G` link step that is not implemented.

On platforms where probes are not available, they compile to no-ops
with zero runtime overhead.

#### Example: bpftrace (Linux)

```bash
sudo bpftrace -e '
usdt:./out/Release/node:node:dc__publish {
printf("channel: %s\n", str(arg0));
}
' -c './out/Release/node app.js'
```

#### Example: DTrace (macOS)

```bash
sudo dtrace -n '
node*:::dc__publish {
printf("channel: %s\n", copyinstr(arg0));
}
' -c './out/Release/node app.js'
```

### Built-in Channels

#### Console
Expand Down
16 changes: 16 additions & 0 deletions lib/diagnostics_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ const { triggerUncaughtException } = internalBinding('errors');
// The subscriber buffer is replaced when native channel storage grows, so it
// must always be accessed through the binding instead of cached.
const dc_binding = internalBinding('diagnostics_channel');
// The USDT probe semaphore is exposed by the binding as a Uint16Array
// view over static native memory. It must be resolved lazily rather than
// captured at module load: this module is included in the startup
// snapshot, and the view's native backing store cannot be serialized:
// a view captured while building the snapshot is detached when the
// snapshot is deserialized. `null` marks a USDT-less build after the
// first resolution so that the hot path stays branch-only in that case.
let probeSemaphore;

const { WeakReference, kEmptyObject } = require('internal/util');
const { isPromise } = require('internal/util/types');
Expand Down Expand Up @@ -188,6 +196,14 @@ class ActiveChannel {
}

publish(data) {
if (probeSemaphore === undefined) {
probeSemaphore = dc_binding.probeSemaphore ?? null;
}
if (probeSemaphore !== null &&
probeSemaphore[0] > 0 &&
typeof this.name === 'string') {
dc_binding.emitPublishProbe(this.name, data);
}
const subscribers = this._subscribers;
for (let i = 0; i < (subscribers?.length || 0); i++) {
try {
Expand Down
40 changes: 40 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
'node_use_dtls%': 'false',
'node_use_sqlite%': 'true',
'node_use_ffi%': 'false',
'node_use_dtrace%': 'false',
'node_no_usdt%': 'false',
'node_use_v8_platform%': 'true',
'node_enable_v8_vtunejit%': 'false',
'node_v8_options%': '',
Expand Down Expand Up @@ -276,6 +278,9 @@
'src/node_metadata.h',
'src/node_mutex.h',
'src/node_diagnostics_channel.h',
'src/node_usdt.h',
'src/node_provider.d',
'src/node_provider_linux.h',
'src/node_modules.h',
'src/node_object_wrap.h',
'src/node_options.h',
Expand Down Expand Up @@ -901,6 +906,14 @@
'WARNING_CFLAGS': [ '-Werror' ],
},
}],
[ 'node_no_usdt=="true"', {
'defines': [ 'NODE_NO_USDT=1' ],
}],
[ 'node_use_dtrace=="true"', {
'defines': [ 'NODE_HAVE_DTRACE=1' ],
'dependencies': [ 'node_dtrace_header' ],
'include_dirs': [ '<(SHARED_INTERMEDIATE_DIR)' ],
}],
[ 'node_builtin_modules_path!=""', {
'defines': [ 'NODE_BUILTIN_MODULES_PATH="<(node_builtin_modules_path)"' ],
}],
Expand Down Expand Up @@ -1624,6 +1637,33 @@
}],
]
}, # overlapped-checker
{
'target_name': 'node_dtrace_header',
'type': 'none',
'conditions': [
[ 'node_use_dtrace=="true"', {
'actions': [
{
# Native DTrace (macOS, opt-in via ./configure
# --with-dtrace): generate the probe header at build time.
# On Linux the probe header is pre-generated and committed
# at src/node_provider_linux.h, so no dtrace tool is
# needed there (see tools/usdt/generate_headers.py).
# -xnolibs avoids loading standard D libraries during
# header generation.
'action_name': 'node_dtrace_header',
'inputs': [ 'src/node_provider.d' ],
'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_provider.h' ],
'action': [
'dtrace', '-h', '-xnolibs',
'-s', '<@(_inputs)',
'-o', '<@(_outputs)',
],
},
],
} ],
],
}, # node_dtrace_header
{
'target_name': 'nop',
'type': 'executable',
Expand Down
Loading
Loading