src: internalize v8::ConvertableToTraceFormat in traces

`v8::ConvertableToTraceFormat` is only available in legacy V8 tracing
API and no longer supported in perfetto. This internalize
`node::tracing::TracedValue` and `v8::ConvertableToTraceFormat` by
defining specialized trace argument classes.

The newly defined structured trace argument classes can be easily
converted to `perfetto::TracedValue` by perfetto traced value protocol.
For example, when adding perfetto support, `CastTracedValue` will be a
no-op and these classes can add a new conversion method like:

```cpp
class Foo {
  void WriteIntoTrace(TracedValue context) const {
    auto dict = std::move(context).WriteDictionary();
    dict->Add("key", 42);
    dict->Add("foo", "bar");
    dict->Add("member", member_);
  }
};
```

PR-URL: https://github.com/nodejs/node/pull/57866
Refs: https://github.com/nodejs/diagnostics/issues/654
Refs: 9ddf987d48/include/perfetto/tracing/traced_value.h (L46)
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
Chengzhong Wu 2025-04-13 18:43:24 +01:00 committed by Node.js GitHub Bot
parent f4413175b4
commit 324b262544
7 changed files with 157 additions and 62 deletions

View File

@ -107,6 +107,18 @@ void AsyncWrap::EmitPromiseResolve(Environment* env, double async_id) {
env->async_hooks_promise_resolve_function());
}
void AsyncWrap::EmitTraceAsyncStart() const {
if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(
TRACING_CATEGORY_NODE1(async_hooks))) {
tracing::AsyncWrapArgs data(env()->execution_async_id(),
get_trigger_async_id());
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(TRACING_CATEGORY_NODE1(async_hooks),
provider_names[provider_type()],
static_cast<int64_t>(get_async_id()),
"data",
tracing::CastTracedValue(data));
}
}
void AsyncWrap::EmitTraceEventBefore() {
switch (provider_type()) {
@ -601,27 +613,7 @@ void AsyncWrap::AsyncReset(Local<Object> resource, double execution_async_id) {
}
}
switch (provider_type()) {
#define V(PROVIDER) \
case PROVIDER_ ## PROVIDER: \
if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED( \
TRACING_CATEGORY_NODE1(async_hooks))) { \
auto data = tracing::TracedValue::Create(); \
data->SetInteger("executionAsyncId", \
static_cast<int64_t>(env()->execution_async_id())); \
data->SetInteger("triggerAsyncId", \
static_cast<int64_t>(get_trigger_async_id())); \
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1( \
TRACING_CATEGORY_NODE1(async_hooks), \
#PROVIDER, static_cast<int64_t>(get_async_id()), \
"data", std::move(data)); \
} \
break;
NODE_ASYNC_PROVIDER_TYPES(V)
#undef V
default:
UNREACHABLE();
}
EmitTraceAsyncStart();
context_frame_.Reset(isolate, async_context_frame::current(isolate));

View File

@ -178,6 +178,7 @@ class AsyncWrap : public BaseObject {
void EmitDestroy(bool from_gc = false);
void EmitTraceAsyncStart() const;
void EmitTraceEventBefore();
static void EmitTraceEventAfter(ProviderType type, double async_id);
void EmitTraceEventDestroy();

View File

@ -894,18 +894,12 @@ Environment::Environment(IsolateData* isolate_data,
if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(
TRACING_CATEGORY_NODE1(environment)) != 0) {
auto traced_value = tracing::TracedValue::Create();
traced_value->BeginArray("args");
for (const std::string& arg : args) traced_value->AppendString(arg);
traced_value->EndArray();
traced_value->BeginArray("exec_args");
for (const std::string& arg : exec_args) traced_value->AppendString(arg);
traced_value->EndArray();
tracing::EnvironmentArgs traced_value(args, exec_args);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(TRACING_CATEGORY_NODE1(environment),
"Environment",
this,
"args",
std::move(traced_value));
tracing::CastTracedValue(traced_value));
}
if (options_->permission) {

View File

@ -38,31 +38,11 @@ class NodeTraceStateObserver
TRACE_EVENT_METADATA1(
"__metadata", "thread_name", "name", "JavaScriptMainThread");
auto trace_process = tracing::TracedValue::Create();
trace_process->BeginDictionary("versions");
#define V(key) \
trace_process->SetString(#key, per_process::metadata.versions.key.c_str());
NODE_VERSIONS_KEYS(V)
#undef V
trace_process->EndDictionary();
trace_process->SetString("arch", per_process::metadata.arch.c_str());
trace_process->SetString("platform",
per_process::metadata.platform.c_str());
trace_process->BeginDictionary("release");
trace_process->SetString("name",
per_process::metadata.release.name.c_str());
#if NODE_VERSION_IS_LTS
trace_process->SetString("lts", per_process::metadata.release.lts.c_str());
#endif
trace_process->EndDictionary();
TRACE_EVENT_METADATA1(
"__metadata", "node", "process", std::move(trace_process));
tracing::ProcessMeta trace_process;
TRACE_EVENT_METADATA1("__metadata",
"node",
"process",
tracing::CastTracedValue(trace_process));
// This only runs the first time tracing is enabled
controller_->RemoveTraceStateObserver(this);
}

View File

@ -9,10 +9,10 @@
#include <unicode/utypes.h>
#endif
#include <cmath>
#include <cstdio>
#include <sstream>
#include <string>
#include "node_metadata.h"
#include "util.h"
#if defined(_STLP_VENDOR_CSTD)
// STLPort doesn't import fpclassify into the std namespace.
@ -218,5 +218,48 @@ void TracedValue::AppendAsTraceFormat(std::string* out) const {
*out += root_is_array_ ? ']' : '}';
}
std::unique_ptr<v8::ConvertableToTraceFormat> EnvironmentArgs::Cast() const {
auto traced_value = tracing::TracedValue::Create();
traced_value->BeginArray("args");
for (const std::string& arg : args_) traced_value->AppendString(arg);
traced_value->EndArray();
traced_value->BeginArray("exec_args");
for (const std::string& arg : exec_args_) traced_value->AppendString(arg);
traced_value->EndArray();
return traced_value;
}
std::unique_ptr<v8::ConvertableToTraceFormat> AsyncWrapArgs::Cast() const {
auto data = tracing::TracedValue::Create();
data->SetInteger("executionAsyncId", execution_async_id_);
data->SetInteger("triggerAsyncId", trigger_async_id_);
return data;
}
std::unique_ptr<v8::ConvertableToTraceFormat> ProcessMeta::Cast() const {
auto trace_process = tracing::TracedValue::Create();
trace_process->BeginDictionary("versions");
#define V(key) \
trace_process->SetString(#key, per_process::metadata.versions.key.c_str());
NODE_VERSIONS_KEYS(V)
#undef V
trace_process->EndDictionary();
trace_process->SetString("arch", per_process::metadata.arch.c_str());
trace_process->SetString("platform", per_process::metadata.platform.c_str());
trace_process->BeginDictionary("release");
trace_process->SetString("name", per_process::metadata.release.name.c_str());
#if NODE_VERSION_IS_LTS
trace_process->SetString("lts", per_process::metadata.release.lts.c_str());
#endif
trace_process->EndDictionary();
return trace_process;
}
} // namespace tracing
} // namespace node

View File

@ -5,17 +5,76 @@
#ifndef SRC_TRACING_TRACED_VALUE_H_
#define SRC_TRACING_TRACED_VALUE_H_
#include "node.h"
#include "util.h"
#include "v8.h"
#include "v8-platform.h"
#include <cstddef>
#include <memory>
#include <cstdint>
#include <span>
#include <string>
namespace node {
namespace tracing {
template <typename T>
std::unique_ptr<v8::ConvertableToTraceFormat> CastTracedValue(const T& value) {
return value.Cast();
}
class EnvironmentArgs {
public:
EnvironmentArgs(std::span<const std::string> args,
std::span<const std::string> exec_args)
: args_(args), exec_args_(exec_args) {}
std::unique_ptr<v8::ConvertableToTraceFormat> Cast() const;
private:
std::span<const std::string> args_;
std::span<const std::string> exec_args_;
};
class AsyncWrapArgs {
public:
AsyncWrapArgs(int64_t execution_async_id, int64_t trigger_async_id)
: execution_async_id_(execution_async_id),
trigger_async_id_(trigger_async_id) {}
std::unique_ptr<v8::ConvertableToTraceFormat> Cast() const;
private:
int64_t execution_async_id_;
int64_t trigger_async_id_;
};
class ProcessMeta {
public:
std::unique_ptr<v8::ConvertableToTraceFormat> Cast() const;
};
// Do not use this class directly. Define a custom structured class to provide
// a conversion method so that the class can be used with both V8 legacy
// trace API and perfetto API.
//
// These classes provide a JSON-inspired way to write structed data into traces.
//
// To define how a custom class should be written into the trace, users should
// define one of the two following functions:
// - Foo::Cast(TracedValue) const
// (preferred for code which depends on perfetto directly)
//
// After defining a conversion method, the object can be used as a
// TRACE_EVENT argument:
//
// Foo foo;
// TRACE_EVENT("cat", "Event", "arg", CastTracedValue(foo));
//
// class Foo {
// std::unique_ptr<v8::ConvertableToTraceFormat> Cast() const {
// auto traced_value = tracing::TracedValue::Create();
// dict->SetInteger("key", 42);
// dict->SetString("foo", "bar");
// return traced_value;
// }
// }
class TracedValue : public v8::ConvertableToTraceFormat {
public:
~TracedValue() override = default;

View File

@ -94,3 +94,29 @@ TEST(TracedValue, EscapingArray) {
EXPECT_EQ(check, string);
}
TEST(TracedValue, EnvironmentArgs) {
std::vector<std::string> args{"a", "bb", "ccc"};
std::vector<std::string> exec_args{"--inspect", "--a-long-arg"};
node::tracing::EnvironmentArgs env_args(args, exec_args);
std::string string;
env_args.Cast()->AppendAsTraceFormat(&string);
static const char* check = "{\"args\":[\"a\",\"bb\",\"ccc\"],"
"\"exec_args\":[\"--inspect\",\"--a-long-arg\"]}";
EXPECT_EQ(check, string);
}
TEST(TracedValue, AsyncWrapArgs) {
node::tracing::AsyncWrapArgs aw_args(1, 1);
std::string string;
aw_args.Cast()->AppendAsTraceFormat(&string);
static const char* check = "{\"executionAsyncId\":1,"
"\"triggerAsyncId\":1}";
EXPECT_EQ(check, string);
}