Skip to content

Commit 5abeea0

Browse files
committed
diagnostics_channel: add USDT probes
Adds USDT probes that are fired for every diagnostics_channel publish.
1 parent 1647288 commit 5abeea0

11 files changed

+645
-7
lines changed

configure.py

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

951+
parser.add_argument('--without-dtrace',
952+
action='store_true',
953+
dest='without_dtrace',
954+
default=None,
955+
help='build without DTrace/USDT probe support')
956+
951957
parser.add_argument('--without-lief',
952958
action='store_true',
953959
dest='without_lief',
@@ -1240,6 +1246,31 @@ def B(value):
12401246
def to_utf8(s):
12411247
return s if isinstance(s, str) else s.decode("utf-8")
12421248

1249+
def has_working_dtrace_h():
1250+
"""Check whether a dtrace tool that supports -h is available.
1251+
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."""
1255+
dtrace = shutil.which('dtrace')
1256+
if dtrace is None:
1257+
return False
1258+
# -xnolibs is required on macOS/FreeBSD/illumos (native DTrace) to avoid
1259+
# loading standard D libraries. Linux (SystemTap wrapper) does not
1260+
# recognise this flag, so only pass it on non-Linux platforms.
1261+
cmd = [dtrace, '-h', '-s', '/dev/stdin', '-o', '/dev/null']
1262+
if sys.platform != 'linux':
1263+
cmd.insert(2, '-xnolibs')
1264+
try:
1265+
proc = subprocess.run(
1266+
cmd,
1267+
input=b'provider _test { probe _test(); };',
1268+
capture_output=True, timeout=10)
1269+
return proc.returncode == 0
1270+
except (OSError, subprocess.TimeoutExpired) as e:
1271+
warn('dtrace probe check failed: %s' % e)
1272+
return False
1273+
12431274
def pkg_config(pkg):
12441275
"""Run pkg-config on the specified package
12451276
Returns ("-l flags", "-I flags", "-L flags", "version")
@@ -1976,6 +2007,16 @@ def configure_node(o):
19762007
print('Warning! Loading builtin modules from disk is for development')
19772008
o['variables']['node_builtin_modules_path'] = options.node_builtin_modules_path
19782009

2010+
o['variables']['node_no_usdt'] = b(options.without_dtrace)
2011+
use_dtrace = not options.without_dtrace and has_working_dtrace_h()
2012+
o['variables']['node_use_dtrace'] = b(use_dtrace)
2013+
if options.without_dtrace:
2014+
print('USDT probes: disabled (--without-dtrace)')
2015+
elif use_dtrace:
2016+
print('USDT probes: enabled (dtrace -h, semaphore support)')
2017+
else:
2018+
print('USDT probes: fallback (sys/sdt.h) or disabled')
2019+
19792020
def configure_napi(output):
19802021
version = getnapibuildversion.get_napi_version()
19812022
output['variables']['napi_build_version'] = version

doc/api/diagnostics_channel.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,78 @@ another async task is triggered internally which fails and then the sync part
11121112
of the function then throws and error two `error` events will be emitted, one
11131113
for the sync error and one for the async error.
11141114

1115+
### USDT probes
1116+
1117+
<!-- YAML
1118+
added: REPLACEME
1119+
-->
1120+
1121+
> Stability: 1 - Experimental
1122+
1123+
Node.js exposes a USDT (User-Level Statically Defined Tracing) probe for
1124+
diagnostics channel publish events, enabling external observability tools
1125+
such as `bpftrace`, DTrace, and `perf` to trace channel activity without
1126+
modifying application code or adding JavaScript subscribers.
1127+
1128+
#### Probe: `node:dc__publish`
1129+
1130+
Fired when a message is published to a string-named diagnostics channel.
1131+
When published from native (C++) code and a tracer is attached, the probe
1132+
fires regardless of subscriber state. When published from JavaScript, the
1133+
probe fires only if the channel has active subscribers.
1134+
1135+
* `arg0` {const char\*} The channel name (UTF-8).
1136+
* `arg1` {const void\*} An opaque pointer to the V8 message object, or `NULL`
1137+
if the published message is not a JavaScript object (e.g., a string, number,
1138+
or `null`). **Warning:** This pointer is unstable and must NOT be
1139+
dereferenced by tracing scripts. V8's garbage collector may move the
1140+
underlying object at any time. The pointer is valid only for the
1141+
duration of the probe callback and must not be stored or compared
1142+
across separate probe firings.
1143+
1144+
#### Platform support
1145+
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.
1166+
1167+
#### Example: bpftrace (Linux)
1168+
1169+
```bash
1170+
sudo bpftrace -e '
1171+
usdt:./out/Release/node:node:dc__publish {
1172+
printf("channel: %s\n", str(arg0));
1173+
}
1174+
' -c './out/Release/node app.js'
1175+
```
1176+
1177+
#### Example: DTrace (macOS/FreeBSD)
1178+
1179+
```bash
1180+
sudo dtrace -n '
1181+
node*:::dc__publish {
1182+
printf("channel: %s\n", copyinstr(arg0));
1183+
}
1184+
' -c './out/Release/node app.js'
1185+
```
1186+
11151187
### Built-in Channels
11161188

11171189
#### Console

lib/diagnostics_channel.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ const {
3232
const { triggerUncaughtException } = internalBinding('errors');
3333

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

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

@@ -158,6 +163,9 @@ class ActiveChannel {
158163
}
159164

160165
publish(data) {
166+
if (hasUSDT && probeSemaphore[0] > 0 && typeof this.name === 'string') {
167+
dc_binding.emitPublishProbe(this.name, data);
168+
}
161169
const subscribers = this._subscribers;
162170
for (let i = 0; i < (subscribers?.length || 0); i++) {
163171
try {

node.gyp

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
'node_use_openssl%': 'true',
3838
'node_use_quic%': 'false',
3939
'node_use_sqlite%': 'true',
40+
'node_use_dtrace%': 'false',
41+
'node_no_usdt%': 'false',
4042
'node_use_v8_platform%': 'true',
4143
'node_v8_options%': '',
4244
'node_write_snapshot_as_string_literals': 'true',
@@ -272,6 +274,8 @@
272274
'src/node_metadata.h',
273275
'src/node_mutex.h',
274276
'src/node_diagnostics_channel.h',
277+
'src/node_usdt.h',
278+
'src/node_provider.d',
275279
'src/node_modules.h',
276280
'src/node_object_wrap.h',
277281
'src/node_options.h',
@@ -947,6 +951,43 @@
947951
'WARNING_CFLAGS': [ '-Werror' ],
948952
},
949953
}],
954+
[ 'node_no_usdt=="true"', {
955+
'defines': [ 'NODE_NO_USDT=1' ],
956+
}],
957+
[ 'node_use_dtrace=="true"', {
958+
'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+
],
990+
}],
950991
[ 'node_builtin_modules_path!=""', {
951992
'defines': [ 'NODE_BUILTIN_MODULES_PATH="<(node_builtin_modules_path)"' ]
952993
}],

0 commit comments

Comments
 (0)