-
-
Notifications
You must be signed in to change notification settings - Fork 38.1k
diagnostics_channel: add USDT probes #62118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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 | ||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 { | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| printf("channel: %s\n", copyinstr(arg0)); | ||||||
| } | ||||||
| ' -c './out/Release/node app.js' | ||||||
| ``` | ||||||
|
|
||||||
| ### Built-in Channels | ||||||
|
|
||||||
| #### Console | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'); | ||
|
|
@@ -188,6 +196,14 @@ class ActiveChannel { | |
| } | ||
|
|
||
| publish(data) { | ||
| if (probeSemaphore === undefined) { | ||
| probeSemaphore = dc_binding.probeSemaphore ?? null; | ||
| } | ||
|
Comment on lines
+199
to
+201
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please reset |
||
| 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 { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.