Skip to content

Commit 7aa6d23

Browse files
committed
diagnostics_channel: add USDT probes
Fire a `dc__publish` USDT probe for every diagnostics_channel publish, passing the channel name, so tracers such as bpftrace, perf, or SystemTap can observe publish traffic with near-zero cost when nothing is attached. Probes are enabled by default on Linux; --without-dtrace disables them. The probe semaphore is exposed to JS as a Uint16Array over the native semaphore so the publish hot path can gate on one indexed load instead of a binding call. The view is read through the binding on every publish: it is created once per context, and any cached reference (for example one captured while building a startup snapshot) would be a stale copy after the snapshot is deserialized, leaving probes disabled even while a tracer is attached. On builds without USDT support the property is absent and the hot path stays branch-only. Signed-off-by: Bryan English <bryan@bryanenglish.com> Assisted-by: Pi using GLM-5.3
1 parent 4bf4c00 commit 7aa6d23

12 files changed

Lines changed: 736 additions & 6 deletions

‎configure.py‎

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

1069+
parser.add_argument('--without-dtrace',
1070+
action='store_true',
1071+
dest='without_dtrace',
1072+
default=None,
1073+
help='build without DTrace/USDT probe support')
1074+
10691075
parser.add_argument('--without-lief',
10701076
action='store_true',
10711077
dest='without_lief',
@@ -1360,6 +1366,31 @@ def B(value):
13601366
def to_utf8(s):
13611367
return s if isinstance(s, str) else s.decode("utf-8")
13621368

1369+
def has_working_dtrace_h():
1370+
"""Check whether a dtrace tool that supports -h is available.
1371+
1372+
Supported on Linux (SystemTap dtrace wrapper), macOS, FreeBSD, and
1373+
illumos/SmartOS (native DTrace). Non-Linux platforms require -xnolibs
1374+
to avoid loading standard D libraries during header generation."""
1375+
dtrace = shutil.which('dtrace')
1376+
if dtrace is None:
1377+
return False
1378+
# -xnolibs is required on macOS/FreeBSD/illumos (native DTrace) to avoid
1379+
# loading standard D libraries. Linux (SystemTap wrapper) does not
1380+
# recognise this flag, so only pass it on non-Linux platforms.
1381+
cmd = [dtrace, '-h', '-s', '/dev/stdin', '-o', '/dev/null']
1382+
if sys.platform != 'linux':
1383+
cmd.insert(2, '-xnolibs')
1384+
try:
1385+
proc = subprocess.run(
1386+
cmd,
1387+
input=b'provider _test { probe _test(); };',
1388+
capture_output=True, timeout=10)
1389+
return proc.returncode == 0
1390+
except (OSError, subprocess.TimeoutExpired) as e:
1391+
warn('dtrace probe check failed: %s' % e)
1392+
return False
1393+
13631394
def pkg_config(pkg):
13641395
"""Run pkg-config on the specified package
13651396
Returns ("-l flags", "-I flags", "-L flags", "version")
@@ -2180,6 +2211,16 @@ def configure_node(o):
21802211
print('Warning! Loading builtin modules from disk is for development')
21812212
o['variables']['node_builtin_modules_path'] = options.node_builtin_modules_path
21822213

2214+
o['variables']['node_no_usdt'] = b(options.without_dtrace)
2215+
use_dtrace = not options.without_dtrace and has_working_dtrace_h()
2216+
o['variables']['node_use_dtrace'] = b(use_dtrace)
2217+
if options.without_dtrace:
2218+
print('USDT probes: disabled (--without-dtrace)')
2219+
elif use_dtrace:
2220+
print('USDT probes: enabled (dtrace -h, semaphore support)')
2221+
else:
2222+
print('USDT probes: fallback (sys/sdt.h) or disabled')
2223+
21832224
def configure_napi(output):
21842225
version = getnapibuildversion.get_napi_version()
21852226
output['variables']['napi_build_version'] = version

‎doc/api/diagnostics_channel.md‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,6 +1529,82 @@ another async task is triggered internally which fails and then the sync part
15291529
of the function then throws and error two `error` events will be emitted, one
15301530
for the sync error and one for the async error.
15311531

1532+
### USDT probes
1533+
1534+
<!-- YAML
1535+
added: REPLACEME
1536+
-->
1537+
1538+
> Stability: 1 - Experimental
1539+
1540+
Node.js exposes a USDT (User-Level Statically Defined Tracing) probe for
1541+
diagnostics channel publish events, enabling external observability tools
1542+
such as `bpftrace`, DTrace, and `perf` to trace channel activity. Attaching a
1543+
tracer does not activate inactive channels or bypass subscriber checks in
1544+
publishers.
1545+
1546+
#### Probe: `node:dc__publish`
1547+
1548+
Fired when a message is published to a string-named diagnostics channel.
1549+
When published from native (C++) code and a tracer is attached, the probe
1550+
fires regardless of subscriber state. When published from JavaScript, the
1551+
probe fires only if the channel has active subscribers. Some built-in
1552+
modules publish from native code only when the channel has a JavaScript
1553+
subscriber (for example the `sqlite.db.query` trace channel), so a tracer
1554+
alone does not observe publishes from those modules.
1555+
1556+
* `arg0` {const char\*} The channel name (UTF-8).
1557+
* `arg1` {const void\*} An opaque pointer to the V8 message object, or `NULL`
1558+
if the published message is not a JavaScript object (e.g., a string, number,
1559+
or `null`). **Warning:** This pointer is unstable and must NOT be
1560+
dereferenced by tracing scripts. V8's garbage collector may move the
1561+
underlying object at any time. The pointer is valid only for the
1562+
duration of the probe callback and must not be stored or compared
1563+
across separate probe firings.
1564+
1565+
#### Platform support
1566+
1567+
At `./configure` time, Node.js checks for a working `dtrace` tool and
1568+
uses `dtrace -h` to generate a probe header. Pass `--without-dtrace` to
1569+
`./configure` to disable probe support entirely.
1570+
1571+
* **Linux**: Install the `systemtap-sdt-dev` package (Debian/Ubuntu) or
1572+
`systemtap-sdt-devel` (Fedora/RHEL) before building Node.js. The
1573+
SystemTap `dtrace` wrapper generates a header with semaphore support,
1574+
giving the probe zero overhead when no tracer is attached.
1575+
* **macOS**: Supported natively via DTrace. The probe instruction is
1576+
patched to a no-op by the kernel when no tracer is attached, but the
1577+
JS-to-C++ call for `emitPublishProbe` is still incurred on every
1578+
publish to a string-named channel with subscribers.
1579+
* **FreeBSD**: Supported natively via DTrace, with the same
1580+
characteristics as macOS.
1581+
* **illumos/SmartOS**: Supported natively via DTrace, with the same
1582+
characteristics as macOS.
1583+
1584+
If `dtrace` is not found but `<sys/sdt.h>` is available, the probe falls
1585+
back to always-enabled mode. On platforms where neither is available,
1586+
the probe compiles to a no-op with zero runtime overhead.
1587+
1588+
#### Example: bpftrace (Linux)
1589+
1590+
```bash
1591+
sudo bpftrace -e '
1592+
usdt:./out/Release/node:node:dc__publish {
1593+
printf("channel: %s\n", str(arg0));
1594+
}
1595+
' -c './out/Release/node app.js'
1596+
```
1597+
1598+
#### Example: DTrace (macOS/FreeBSD)
1599+
1600+
```bash
1601+
sudo dtrace -n '
1602+
node*:::dc-publish {
1603+
printf("channel: %s\n", copyinstr(arg0));
1604+
}
1605+
' -c './out/Release/node app.js'
1606+
```
1607+
15321608
### Built-in Channels
15331609

15341610
#### Console

‎lib/diagnostics_channel.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,17 @@ class ActiveChannel {
188188
}
189189

190190
publish(data) {
191+
// Read the semaphore through the binding on every publish. The view is
192+
// created once per context and wraps static native memory, but a cached
193+
// reference (for example one captured while building a startup snapshot)
194+
// would be a stale copy after the snapshot is deserialized, keeping
195+
// probes disabled even while a tracer is attached.
196+
const probeSemaphore = dc_binding.probeSemaphore;
197+
if (probeSemaphore !== undefined &&
198+
probeSemaphore[0] > 0 &&
199+
typeof this.name === 'string') {
200+
dc_binding.emitPublishProbe(this.name, data);
201+
}
191202
const subscribers = this._subscribers;
192203
for (let i = 0; i < (subscribers?.length || 0); i++) {
193204
try {

‎node.gyp‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
'node_use_dtls%': 'false',
4747
'node_use_sqlite%': 'true',
4848
'node_use_ffi%': 'false',
49+
'node_use_dtrace%': 'false',
50+
'node_no_usdt%': 'false',
4951
'node_use_v8_platform%': 'true',
5052
'node_enable_v8_vtunejit%': 'false',
5153
'node_v8_options%': '',
@@ -276,6 +278,8 @@
276278
'src/node_metadata.h',
277279
'src/node_mutex.h',
278280
'src/node_diagnostics_channel.h',
281+
'src/node_usdt.h',
282+
'src/node_provider.d',
279283
'src/node_modules.h',
280284
'src/node_object_wrap.h',
281285
'src/node_options.h',
@@ -901,6 +905,43 @@
901905
'WARNING_CFLAGS': [ '-Werror' ],
902906
},
903907
}],
908+
[ 'node_no_usdt=="true"', {
909+
'defines': [ 'NODE_NO_USDT=1' ],
910+
}],
911+
[ 'node_use_dtrace=="true"', {
912+
'defines': [ 'NODE_HAVE_DTRACE=1' ],
913+
'conditions': [
914+
[ 'OS=="linux"', {
915+
'actions': [
916+
{
917+
'action_name': 'node_dtrace_header',
918+
'inputs': [ 'src/node_provider.d' ],
919+
'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_provider.h' ],
920+
'action': [
921+
'dtrace', '-h',
922+
'-s', 'src/node_provider.d',
923+
'-o', '<(SHARED_INTERMEDIATE_DIR)/node_provider.h',
924+
],
925+
},
926+
],
927+
}, {
928+
# macOS, FreeBSD, illumos: native DTrace requires -xnolibs
929+
# to avoid loading kernel D libraries during header generation.
930+
'actions': [
931+
{
932+
'action_name': 'node_dtrace_header',
933+
'inputs': [ 'src/node_provider.d' ],
934+
'outputs': [ '<(SHARED_INTERMEDIATE_DIR)/node_provider.h' ],
935+
'action': [
936+
'dtrace', '-h', '-xnolibs',
937+
'-s', 'src/node_provider.d',
938+
'-o', '<(SHARED_INTERMEDIATE_DIR)/node_provider.h',
939+
],
940+
},
941+
],
942+
}],
943+
],
944+
}],
904945
[ 'node_builtin_modules_path!=""', {
905946
'defines': [ 'NODE_BUILTIN_MODULES_PATH="<(node_builtin_modules_path)"' ],
906947
}],

0 commit comments

Comments
 (0)