Repository navigation
Conversation
Each node:fs operation publishes to its own TracingChannel, named fs.<operation>. The sync, callback, and promise forms of an operation share a channel, and the event context carries the form in `api`, the caller's arguments in `args`, and the leading arguments by name. The events are published from JavaScript with the TracingChannel helpers, so all events of a call share one context object, stores bound with bindStore() reach the callback and nested operations, and the error event is always followed by end or asyncEnd. Each traced function starts with a check that calls the function again inside the trace when the channel has subscribers. The whole body then runs inside the start scope, so requests capture the bound stores, and captured function references still publish. Assisted-by: claude:opus-5.5 Signed-off-by: Tim Fish <tim@timfish.uk>
timfish
force-pushed
the
fs-diagnostics-channel
branch
from
October 7, 2026 18:21
f73bebb to
f7a0dc8
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #66575 +/- ##
==========================================
+ Coverage 90.41% 90.44% +0.03%
==========================================
Files 791 791
Lines 276382 276833 +451
Branches 53076 53335 +259
==========================================
+ Hits 249877 250392 +515
+ Misses 16901 16848 -53
+ Partials 9604 9593 -11
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds a
diagnostics_channelTracingChannel for eachnode:fsoperation. It is a different approach to #65370, based on the review feedback there.Each operation publishes to
tracing:fs.<operation>:*, for exampletracing:fs.stat:start. The sync, callback and promise forms of an operation share a channel, so a subscriber tofs.readFileseesfs.readFile(),fs.readFileSync(),fsPromises.readFile()andfilehandle.readFile(). The context hasapi('sync','callback'or'promise'),args(a copy of the arguments as passed), and the leading arguments by name, such aspath,fdanddest. The docs have the full list.The events are published from JS with
traceSync(),traceCallback()andtracePromise(), so all events of one call share one context object,erroris always followed byendorasyncEnd,asyncStarthasresult, andbindStore()works. Buffers and the other arguments are passed by reference inargs. ThereadFileandwriteFileone-round-trip paths,fsPromises.readFile(),readFileSync()with an encoding,existsSync()and the FileHandle methods all publish.Each traced function starts with one line, for example
if (shouldTraceFs('stat')) return traceFsCallback('stat', stat, this, arguments);. When the channel has subscribers, the helper calls the same function again inside the trace, and a flag makes that inner call skip the check. The whole body then runs inside thestartscope. This matters forbindStore(): theFSReqCallbackor binding promise must be created inside the scope, because a request created before it does not carry the store to the callback. Nested fs calls also run inside the parent's store. Because the check is in the function body, captured references and ESM named imports still publish. When nothing subscribes, the rest of the body is the same as on main.Calls with invalid arguments publish
start,errorandend. Calls that a VFS handler serves also publish.fsPromises.glob(),fs.watch(),fs.watchFile()anddir.read()do not publish. An operation that uses other fs operations publishes those too, inside its own events. For example,fs.readFileSync()without an encoding publishesfs.open,fs.readandfs.closeinsidefs.readFile.With plain
compare.js, the async and stream benchmarks inbenchmark/fsdo not change, but some short sync benchmarks are slower. With 15 runs and no subscribers,fstatSyncwas about 5% slower,statSyncabout 3%, and failingreadFileSynccalls 5% to 9%. Most of this is the cost ofTracingChannel.hasSubscribersbefore V8 optimizes it. Many of these benchmarks do 10,000 calls in a new process, so most checks run while the getter is still in the interpreter or in Sparkplug. There, the getter callsBoundedChannel.hasSubscribersandChannel.hasSubscribersfor all five channels, which costs 60 to 100 ns per check. Once it is optimised, it costs about 3 ns. Next to a 300 to 900 ns sync call, the difference shows.Once
hasSubscribershas been optimized, calling it becomes insignificant apart from sync calls that throw. These are 1-3% slower. When an exception passes through a function that runs as Sparkplug code, V8 finds the bytecode offset with a linear walk from the start of the function (Code::GetBytecodeOffsetForBaselinePC, fromIsolate::ThrowandLookupExceptionHandlerInTable). So any code before the binding call makes each throw slower. A singleif (x.active !== 0) return;costs about 50 ns per failingopenSync()there. I did not find a way around this.I did not change the benchmarks to warm the
hasSubscribersgetters. A follow-up could makeTracingChannel.hasSubscriberscheaper before optimisation.The new tests cover the event order, the shared context,
bindStore()across callbacks, promises and nested calls, and the FileHandle methods. A coverage test fails if a public fs function does not publish, or publishes more than once.Fixes: #65330
Refs: #65370
CC: @nodejs/diagnostics
This PR was partly AI generated but everything was reviewed by a water based human