Skip to content
Draft
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
41 changes: 41 additions & 0 deletions configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,12 @@
default=None,
help='do not install the bundled Amaro (TypeScript utils)')

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 @@ -1240,6 +1246,31 @@ 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 Linux (SystemTap dtrace wrapper), macOS, FreeBSD, and
illumos/SmartOS (native DTrace). Non-Linux platforms require -xnolibs
to avoid loading standard D libraries during header generation."""
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 @@ -1976,6 +2007,16 @@ 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)
use_dtrace = not options.without_dtrace and has_working_dtrace_h()
o['variables']['node_use_dtrace'] = b(use_dtrace)
if options.without_dtrace:
print('USDT probes: disabled (--without-dtrace)')
elif use_dtrace:
print('USDT probes: enabled (dtrace -h, semaphore support)')
else:
print('USDT probes: fallback (sys/sdt.h) or disabled')

def configure_napi(output):
version = getnapibuildversion.get_napi_version()
output['variables']['napi_build_version'] = version
Expand Down
72 changes: 72 additions & 0 deletions doc/api/diagnostics_channel.md
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,78 @@ 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

At `./configure` time, Node.js checks for a working `dtrace` tool and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have removed dtrace support long time ago. Wouldn't it require us to include a new suite for testing it on that environment?

uses `dtrace -h` to generate a probe header. Pass `--without-dtrace` to
`./configure` to disable probe support entirely.

* **Linux**: Install the `systemtap-sdt-dev` package (Debian/Ubuntu) or
`systemtap-sdt-devel` (Fedora/RHEL) before building Node.js. The
SystemTap `dtrace` wrapper generates a header with semaphore support,
giving the probe zero overhead when no tracer is attached.
* **macOS**: Supported natively via DTrace. 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.
* **FreeBSD**: Supported natively via DTrace, with the same
characteristics as macOS.
* **illumos/SmartOS**: Supported natively via DTrace, with the same
characteristics as macOS.

If `dtrace` is not found but `<sys/sdt.h>` is available, the probe falls
back to always-enabled mode. On platforms where neither is available,
the probe compiles to a no-op 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/FreeBSD)

```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
10 changes: 9 additions & 1 deletion lib/diagnostics_channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ const {
const { triggerUncaughtException } = internalBinding('errors');

const dc_binding = internalBinding('diagnostics_channel');
const { subscribers: subscriberCounts } = dc_binding;
const { subscribers: subscriberCounts, probeSemaphore } = dc_binding;
// When compiled without USDT support, probeSemaphore is undefined and
// emitPublishProbe does not exist. Capture this once at load time so that
// when USDT is absent, the hot path in publish() can skip all probe logic
// with a single boolean check.
const hasUSDT = probeSemaphore !== undefined;

const { WeakReference } = require('internal/util');

Expand Down Expand Up @@ -158,6 +163,9 @@ class ActiveChannel {
}

publish(data) {
if (hasUSDT && 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
41 changes: 41 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
'node_use_openssl%': 'true',
'node_use_quic%': 'false',
'node_use_sqlite%': 'true',
'node_use_dtrace%': 'false',
'node_no_usdt%': 'false',
'node_use_v8_platform%': 'true',
'node_v8_options%': '',
'node_write_snapshot_as_string_literals': 'true',
Expand Down Expand Up @@ -272,6 +274,8 @@
'src/node_metadata.h',
'src/node_mutex.h',
'src/node_diagnostics_channel.h',
'src/node_usdt.h',
'src/node_provider.d',
'src/node_modules.h',
'src/node_object_wrap.h',
'src/node_options.h',
Expand Down Expand Up @@ -947,6 +951,43 @@
'WARNING_CFLAGS': [ '-Werror' ],
},
}],
[ 'node_no_usdt=="true"', {
'defines': [ 'NODE_NO_USDT=1' ],
}],
[ 'node_use_dtrace=="true"', {
'defines': [ 'NODE_HAVE_DTRACE=1' ],
'conditions': [
[ 'OS=="linux"', {
'actions': [
{
'action_name': 'node_dtrace_header',
'inputs': [ 'src/node_provider.d' ],
'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_provider.h' ],
'action': [
'dtrace', '-h',
'-s', 'src/node_provider.d',
'-o', '<(SHARED_INTERMEDIATE_DIR)/node_provider.h',
],
},
],
}, {
# macOS, FreeBSD, illumos: native DTrace requires -xnolibs
# to avoid loading kernel D libraries during header generation.
'actions': [
{
'action_name': 'node_dtrace_header',
'inputs': [ 'src/node_provider.d' ],
'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_provider.h' ],
'action': [
'dtrace', '-h', '-xnolibs',
'-s', 'src/node_provider.d',
'-o', '<(SHARED_INTERMEDIATE_DIR)/node_provider.h',
],
},
],
}],
],
}],
[ 'node_builtin_modules_path!=""', {
'defines': [ 'NODE_BUILTIN_MODULES_PATH="<(node_builtin_modules_path)"' ]
}],
Expand Down
Loading