Skip to content

Commit 6e1aa61

Browse files
committed
fs: add per-operation fs diagnostics channels
Add built-in node:diagnostics_channel channels for file system operations performed through node:fs and node:fs/promises. Each operation gets its own TracingChannel family named fs.<operation>, with channels tracing:fs.<operation>:start, :end, :asyncStart, :asyncEnd, and :error. The event payload carries the API (sync/callback/promise), path/dest/fd fields when applicable, plus result/error following TracingChannel conventions. Events are published from the internal shared file system layer rather than the JS wrappers, so captured function references still emit events. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 2d22505 commit 6e1aa61

6 files changed

Lines changed: 545 additions & 0 deletions

File tree

‎doc/api/diagnostics_channel.md‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,6 +1636,89 @@ diagnosticsChannel.subscribe('crypto.fips.indicator', (message) => {
16361636
});
16371637
```
16381638

1639+
#### Filesystem
1640+
1641+
> Stability: 1 - Experimental
1642+
1643+
These channels are emitted for file system operations performed through
1644+
`node:fs` and `node:fs/promises`. Each operation has its own
1645+
[`TracingChannel`][] family named `fs.<operation>`, where `<operation>` is a
1646+
stable operation name such as `open`, `read`, `write`, `stat`, `readdir`, or
1647+
`realpath`. Subscribers can use [`diagnostics_channel.tracingChannel()`][] to
1648+
subscribe to all events of a given operation at once:
1649+
1650+
```mjs
1651+
import diagnostics_channel from 'node:diagnostics_channel';
1652+
1653+
const channel = diagnostics_channel.tracingChannel('fs.open');
1654+
channel.subscribe({
1655+
start: (event) => console.log('start', event),
1656+
end: (event) => console.log('end', event),
1657+
error: (event) => console.log('error', event),
1658+
});
1659+
```
1660+
1661+
The events are published from the internal file system implementation, so they
1662+
are observed for every public `fs` operation regardless of whether the
1663+
function reference was captured before subscribing or whether the operation
1664+
uses the callback, promise, or synchronous API.
1665+
1666+
Each event carries an object with the following common fields:
1667+
1668+
* `api` {string} The API that performed the operation: `'sync'`, `'callback'`,
1669+
or `'promise'`.
1670+
* `path` {string|undefined} The path argument for path-based operations, or
1671+
the source path for operations with a destination.
1672+
* `dest` {string|undefined} The destination argument for operations that
1673+
accept one, such as `rename`, `link`, `symlink`, or `copyFile`.
1674+
* `fd` {number|undefined} The file descriptor for operations that operate on
1675+
an existing file descriptor, such as `read`, `write`, `fsync`, or `close`.
1676+
1677+
Large read/write buffers are not copied into the event payload. The `start`
1678+
and `asyncStart` events carry no `result` or `error`; the `end` and `asyncEnd`
1679+
events carry the `result` of the operation, and the `error` event carries the
1680+
`error`, following the [TracingChannel Channels][] conventions.
1681+
1682+
Operations performed through streams (`fs.createReadStream` and
1683+
`fs.createWriteStream`), most `FileHandle` methods, and the `fs.readFile`
1684+
fast path (which batches open/stat/read/close into a single background job)
1685+
are not covered by these channel families, and may not emit the full set of
1686+
events.
1687+
1688+
##### Event: `'tracing:fs.<operation>:start'`
1689+
1690+
Emitted synchronously when an operation begins, before the operation is
1691+
submitted. For synchronous operations this is followed by `end` (or `error`);
1692+
for asynchronous operations it is followed by `end` and then `asyncStart`/
1693+
`asyncEnd` (or `error`).
1694+
1695+
##### Event: `'tracing:fs.<operation>:end'`
1696+
1697+
* `result` {any} The result of the operation.
1698+
1699+
Emitted when the operation completes. For synchronous operations this carries
1700+
the operation `result`; for asynchronous operations it is emitted when the
1701+
operation is submitted and carries no `result` (the `result` is delivered on
1702+
the `asyncEnd` event).
1703+
1704+
##### Event: `'tracing:fs.<operation>:asyncStart'`
1705+
1706+
Emitted when the asynchronous work for an operation begins (when the
1707+
completion callback is invoked).
1708+
1709+
##### Event: `'tracing:fs.<operation>:asyncEnd'`
1710+
1711+
* `result` {any} The result of the operation.
1712+
1713+
Emitted when the asynchronous work for an operation completes, carrying the
1714+
operation `result`.
1715+
1716+
##### Event: `'tracing:fs.<operation>:error'`
1717+
1718+
* `error` {Error} The error that caused the operation to fail.
1719+
1720+
Emitted when an operation fails.
1721+
16391722
#### HTTP
16401723

16411724
> Stability: 1 - Experimental

‎src/env_properties.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
V(allow_bare_named_params_string, "allowBareNamedParameters") \
8888
V(allow_unknown_named_params_string, "allowUnknownNamedParameters") \
8989
V(alpn_callback_string, "ALPNCallback") \
90+
V(api_string, "api") \
9091
V(args_string, "args") \
9192
V(arguments_string, "arguments") \
9293
V(async_ids_stack_string, "async_ids_stack") \

‎src/node_file-inl.h‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ FSReqPromise<AliasedBufferT>::FSReqPromise(BindingData* binding_data,
214214
template <typename AliasedBufferT>
215215
void FSReqPromise<AliasedBufferT>::Reject(v8::Local<v8::Value> reject) {
216216
finished_ = true;
217+
PublishFSOpCompletionEvent(this, FSOperationChannel::kError, "error", reject);
217218
v8::HandleScope scope(env()->isolate());
218219
InternalCallbackScope callback_scope(this);
219220
v8::Local<v8::Value> value;
@@ -232,6 +233,8 @@ void FSReqPromise<AliasedBufferT>::Reject(v8::Local<v8::Value> reject) {
232233
template <typename AliasedBufferT>
233234
void FSReqPromise<AliasedBufferT>::Resolve(v8::Local<v8::Value> value) {
234235
finished_ = true;
236+
PublishFSOpCompletionEvent(this, FSOperationChannel::kAsyncEnd, "result",
237+
value);
235238
v8::HandleScope scope(env()->isolate());
236239
InternalCallbackScope callback_scope(this);
237240
v8::Local<v8::Value> val;
@@ -311,6 +314,7 @@ FSReqBase* GetReqWrap(const v8::FunctionCallbackInfo<v8::Value>& args,
311314
result =
312315
FSReqPromise<AliasedFloat64Array>::New(binding_data, use_bigint);
313316
}
317+
result->set_is_promise(true);
314318
}
315319
}
316320
if (result != nullptr) {
@@ -328,13 +332,40 @@ FSReqBase* AsyncDestCall(Environment* env, FSReqBase* req_wrap,
328332
Func fn, Args... fn_args) {
329333
CHECK_NOT_NULL(req_wrap);
330334
req_wrap->Init(syscall, dest, len, enc);
335+
BindingData* binding = req_wrap->binding_data();
336+
const char* api = req_wrap->is_promise() ? "promise" : "callback";
337+
FSOperationChannels* channels = nullptr;
338+
// See SyncCallAndThrowIf: instrumentation is unsafe with a pending
339+
// exception.
340+
if (binding != nullptr && !env->isolate()->HasPendingException()) {
341+
channels = &GetFSOperationChannels(binding, env, syscall);
342+
req_wrap->set_op_channels(channels);
343+
if (FSOperationChannelHasSubscribers(*channels,
344+
FSOperationChannel::kStart)) {
345+
PublishFSOperationEvent(env, *channels, FSOperationChannel::kStart,
346+
api, nullptr, req_wrap->data(), -1, nullptr,
347+
v8::Local<v8::Value>());
348+
}
349+
}
331350
int err = req_wrap->Dispatch(fn, fn_args..., after);
332351
if (err < 0) {
333352
uv_fs_t* uv_req = req_wrap->req();
334353
uv_req->result = err;
335354
uv_req->path = nullptr;
336355
after(uv_req); // after may delete req_wrap if there is an error
337356
req_wrap = nullptr;
357+
} else if (channels != nullptr &&
358+
AnyFSOperationChannelHasSubscribers(*channels)) {
359+
const char* path = req_wrap->req()->path;
360+
int fd = -1;
361+
if (OperationUsesFd(req_wrap->req()->fs_type)) fd = req_wrap->req()->file;
362+
req_wrap->set_fd(fd);
363+
// The path is captured for the completion events; it requires a copy
364+
// since the uv request is cleaned up before they fire.
365+
req_wrap->set_op_path(path == nullptr ? std::string() : path);
366+
PublishFSOperationEvent(env, *channels, FSOperationChannel::kEnd, api,
367+
path, req_wrap->data(), fd, nullptr,
368+
v8::Local<v8::Value>());
338369
}
339370
return req_wrap;
340371
}
@@ -389,7 +420,54 @@ int SyncCallAndThrowIf(Predicate should_throw,
389420
Func fn,
390421
Args... args) {
391422
env->PrintSyncTrace();
423+
BindingData* binding = Realm::GetBindingData<BindingData>(env->context());
424+
FSOperationChannels* channels = nullptr;
425+
// The instrumentation creates V8 objects and may run subscribers, neither
426+
// of which is safe with a pending exception (a multi-step operation keeps
427+
// going after a failed step to clean up, e.g. write + close).
428+
if (binding != nullptr && !env->isolate()->HasPendingException()) {
429+
channels = &GetFSOperationChannels(binding, env, req_wrap->syscall_p);
430+
if (FSOperationChannelHasSubscribers(*channels,
431+
FSOperationChannel::kStart)) {
432+
PublishFSOperationEvent(env, *channels, FSOperationChannel::kStart,
433+
"sync", req_wrap->path_p, req_wrap->dest_p, -1,
434+
nullptr, v8::Local<v8::Value>());
435+
}
436+
}
392437
int result = fn(nullptr, &(req_wrap->req), args..., nullptr);
438+
if (channels != nullptr) {
439+
if (should_throw(result)) {
440+
// The error object is only built when someone is listening; the throw
441+
// path below creates its own copy.
442+
if (FSOperationChannelHasSubscribers(*channels,
443+
FSOperationChannel::kError)) {
444+
int fd = -1;
445+
if (OperationUsesFd(req_wrap->req.fs_type)) fd = req_wrap->req.file;
446+
v8::Local<v8::Value> error = UVException(env->isolate(),
447+
result,
448+
req_wrap->syscall_p,
449+
nullptr,
450+
req_wrap->path_p,
451+
req_wrap->dest_p);
452+
PublishFSOperationEvent(env, *channels, FSOperationChannel::kError,
453+
"sync", req_wrap->path_p, req_wrap->dest_p,
454+
fd, "error", error);
455+
}
456+
} else if (FSOperationChannelHasSubscribers(*channels,
457+
FSOperationChannel::kEnd)) {
458+
int fd = -1;
459+
if (OperationUsesFd(req_wrap->req.fs_type)) fd = req_wrap->req.file;
460+
PublishFSOperationEvent(env,
461+
*channels,
462+
FSOperationChannel::kEnd,
463+
"sync",
464+
req_wrap->path_p,
465+
req_wrap->dest_p,
466+
fd,
467+
"result",
468+
v8::Integer::New(env->isolate(), result));
469+
}
470+
}
393471
if (should_throw(result)) {
394472
env->ThrowUVException(result,
395473
req_wrap->syscall_p,

‎src/node_file.cc‎

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,125 @@ using v8::Uint8Array;
9393
using v8::Undefined;
9494
using v8::Value;
9595

96+
// Event names for the built-in per-operation fs tracing channel families,
97+
// one per FSOperationChannel in node_file.h. Each operation gets its own
98+
// channel family named `tracing:fs.<operation>:<event>`.
99+
const char* const kFSOperationEventNames[kNumFSOperationChannels] = {
100+
"start",
101+
"end",
102+
"asyncStart",
103+
"asyncEnd",
104+
"error",
105+
};
106+
107+
FSOperationChannels& GetFSOperationChannels(BindingData* binding,
108+
Environment* env,
109+
const char* operation) {
110+
auto& names = binding->fs_op_channel_names_;
111+
for (size_t i = 0; i < names.size(); i++) {
112+
if (names[i] == operation) {
113+
return *binding->fs_op_channel_sets_[i];
114+
}
115+
}
116+
auto set = std::make_unique<FSOperationChannels>();
117+
for (size_t i = 0; i < kNumFSOperationChannels; i++) {
118+
std::string name = std::string("tracing:fs.") + operation + ":" +
119+
kFSOperationEventNames[i];
120+
(*set)[i] = diagnostics_channel::Channel::Get(env, name);
121+
}
122+
names.push_back(operation);
123+
binding->fs_op_channel_sets_.push_back(std::move(set));
124+
return *binding->fs_op_channel_sets_.back();
125+
}
126+
127+
void PublishFSOperationEvent(Environment* env,
128+
FSOperationChannels& channels,
129+
FSOperationChannel channel,
130+
const char* api,
131+
const char* path,
132+
const char* dest,
133+
int fd,
134+
const char* value_key,
135+
Local<Value> value) {
136+
const size_t index = static_cast<size_t>(channel);
137+
CHECK_LT(index, kNumFSOperationChannels);
138+
diagnostics_channel::Channel* ch = channels[index].get();
139+
if (ch == nullptr || !ch->HasSubscribers()) {
140+
return;
141+
}
142+
143+
Isolate* isolate = env->isolate();
144+
HandleScope scope(isolate);
145+
Local<Context> context = env->context();
146+
Local<Object> obj = Object::New(isolate);
147+
obj->Set(context,
148+
env->api_string(),
149+
ToV8Value(context, api, isolate).ToLocalChecked())
150+
.Check();
151+
if (path != nullptr && path[0] != '\0') {
152+
obj->Set(context,
153+
env->path_string(),
154+
ToV8Value(context, path, isolate).ToLocalChecked())
155+
.Check();
156+
}
157+
if (dest != nullptr && dest[0] != '\0') {
158+
obj->Set(context,
159+
env->dest_string(),
160+
ToV8Value(context, dest, isolate).ToLocalChecked())
161+
.Check();
162+
}
163+
if (fd != -1) {
164+
obj->Set(context, env->fd_string(), Integer::New(isolate, fd)).Check();
165+
}
166+
if (value_key != nullptr && !value.IsEmpty()) {
167+
obj->Set(context, OneByteString(isolate, value_key), value).Check();
168+
}
169+
ch->Publish(env, obj);
170+
}
171+
172+
void PublishFSOpCompletionEvent(FSReqBase* req_wrap,
173+
FSOperationChannel channel,
174+
const char* value_key,
175+
Local<Value> value) {
176+
FSOperationChannels* channels = req_wrap->op_channels();
177+
if (channels == nullptr ||
178+
!FSOperationChannelHasSubscribers(*channels, channel)) {
179+
return;
180+
}
181+
const char* api = req_wrap->is_promise() ? "promise" : "callback";
182+
PublishFSOperationEvent(req_wrap->env(),
183+
*channels,
184+
channel,
185+
api,
186+
req_wrap->op_path().c_str(),
187+
req_wrap->data(),
188+
req_wrap->fd(),
189+
value_key,
190+
value);
191+
}
192+
193+
// Returns true if the libuv fs request type operates on an existing file
194+
// descriptor (as opposed to taking a path). These are the request types whose
195+
// `file` field holds the input descriptor.
196+
bool OperationUsesFd(uv_fs_type fs_type) {
197+
switch (fs_type) {
198+
case UV_FS_CLOSE:
199+
case UV_FS_READ:
200+
case UV_FS_WRITE:
201+
case UV_FS_FSTAT:
202+
case UV_FS_FTRUNCATE:
203+
case UV_FS_FDATASYNC:
204+
case UV_FS_FSYNC:
205+
case UV_FS_FUTIME:
206+
case UV_FS_FCHMOD:
207+
case UV_FS_FCHOWN:
208+
case UV_FS_SENDFILE:
209+
return true;
210+
default:
211+
return false;
212+
}
213+
}
214+
96215
#ifndef S_ISDIR
97216
#define S_ISDIR(mode) (((mode)&S_IFMT) == S_IFDIR)
98217
#endif
@@ -227,6 +346,7 @@ FSReqBase::~FSReqBase() = default;
227346

228347
void FSReqBase::MemoryInfo(MemoryTracker* tracker) const {
229348
tracker->TrackField("continuation_data", continuation_data_);
349+
tracker->TrackField("op_path", op_path_);
230350
}
231351

232352
// The FileHandle object wraps a file descriptor and will close it on garbage
@@ -734,6 +854,7 @@ int FileHandle::DoShutdown(ShutdownWrap* req_wrap) {
734854
}
735855

736856
void FSReqCallback::Reject(Local<Value> reject) {
857+
PublishFSOpCompletionEvent(this, FSOperationChannel::kError, "error", reject);
737858
MakeCallback(env()->oncomplete_string(), 1, &reject);
738859
}
739860

@@ -746,6 +867,8 @@ void FSReqCallback::ResolveStatFs(const uv_statfs_t* stat) {
746867
}
747868

748869
void FSReqCallback::Resolve(Local<Value> value) {
870+
PublishFSOpCompletionEvent(this, FSOperationChannel::kAsyncEnd, "result",
871+
value);
749872
Local<Value> argv[2]{Null(env()->isolate()), value};
750873
MakeCallback(env()->oncomplete_string(),
751874
value->IsUndefined() ? 1 : arraysize(argv),
@@ -774,6 +897,10 @@ FSReqAfterScope::FSReqAfterScope(FSReqBase* wrap, uv_fs_t* req)
774897
handle_scope_(wrap->env()->isolate()),
775898
context_scope_(wrap->env()->context()) {
776899
CHECK_EQ(wrap_->req(), req);
900+
// The async work for the operation has completed; the continuation window
901+
// begins here.
902+
PublishFSOpCompletionEvent(wrap, FSOperationChannel::kAsyncStart, nullptr,
903+
Local<Value>());
777904
}
778905

779906
FSReqAfterScope::~FSReqAfterScope() {

0 commit comments

Comments
 (0)