Compare commits

..

4 Commits

Author SHA1 Message Date
deepak1556
22c8b7cf17 chore: update patches 2026-04-09 13:15:05 +09:00
deepak1556
26f169d9d1 chore: cleanup macro usage 2026-04-09 13:14:58 +09:00
deepak1556
15416ef8e4 fix: crash in ELECTRON_RUN_AS_NODE 2026-04-09 13:14:52 +09:00
deepak1556
2d4e638722 feat: capture Node.js trace categories via Perfetto 2026-04-09 13:14:29 +09:00
24 changed files with 824 additions and 607 deletions

View File

@@ -151,3 +151,4 @@ fix_pulseaudio_stream_and_icon_names.patch
fix_fire_menu_popup_start_for_dynamically_created_aria_menus.patch
feat_allow_enabling_extensions_on_custom_protocols.patch
fix_initialize_com_on_desktopmedialistcapturethread_on_windows.patch
chore_register_node_as_a_dynamic_trace_category_prefix.patch

View File

@@ -0,0 +1,27 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: deepak1556 <hop2deep@gmail.com>
Date: Tue, 31 Mar 2026 09:03:39 +0900
Subject: chore: register node as a dynamic trace category prefix
This allows Node.js trace categories to be treated as dynamic Perfetto
categories in the Chromium build. Without this, the categories
must be registered in the static registry base/trace_event/builtin_categories.h
which is backed by constexpr function ValidateCategories() that
recursively validates to a depth of index + longest_category_name_length,
adding the node categories exceeds the current constexpr recursion depth
of 512 and requires additional patching to add `-fconstexpr-depth` to //base
target. Given neither the static nor the dynamic registration can be
upstreamed, the minimal of the two changes is chosen here.
diff --git a/base/trace_event/builtin_categories.h b/base/trace_event/builtin_categories.h
index 85c6f973788938b6a48a7a89e9fa803dc1030580..ae25a8188d57ff4c15e9a20e91629d585314db87 100644
--- a/base/trace_event/builtin_categories.h
+++ b/base/trace_event/builtin_categories.h
@@ -14,6 +14,7 @@ PERFETTO_DEFINE_TEST_CATEGORY_PREFIXES("cat",
"foo",
"test",
"kTest",
+ "node",
"noise",
"Testing",
"NotTesting",

View File

@@ -34,48 +34,6 @@ index 7ea6daec53a497bf867d799e041bf6ae7191ef7b..15940624940d5c629c40319f45c59282
agent_group_scheduler_compositor_task_runner =
execution_context->GetScheduler()
->ToFrameScheduler()
diff --git a/third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.cc b/third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.cc
index 936f5ebe28caa993ed5de0f7de3613fa338e263f..961ac8091aa82128e1cfb8800a7efcb80d100a05 100644
--- a/third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.cc
+++ b/third_party/blink/renderer/core/workers/threaded_worklet_messaging_proxy.cc
@@ -13,10 +13,12 @@
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/execution_context/security_context.h"
+#include "third_party/blink/renderer/core/exported/web_view_impl.h"
#include "third_party/blink/renderer/core/frame/csp/content_security_policy.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_client.h"
+#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/inspector/thread_debugger_common_impl.h"
#include "third_party/blink/renderer/core/loader/worker_fetch_context.h"
#include "third_party/blink/renderer/core/origin_trials/origin_trial_context.h"
@@ -135,6 +137,14 @@ void ThreadedWorkletMessagingProxy::Initialize(
DCHECK(csp);
LocalFrameClient* frame_client = window->GetFrame()->Client();
+ auto worklet_settings =
+ std::make_unique<WorkerSettings>(window->GetFrame()->GetSettings());
+ if (auto* web_local_frame = WebLocalFrameImpl::FromFrame(window->GetFrame())) {
+ if (auto* web_view = web_local_frame->ViewImpl()) {
+ worklet_settings->SetNodeIntegrationInWorker(
+ web_view->GetWebPreferences().node_integration_in_worker);
+ }
+ }
auto global_scope_creation_params =
std::make_unique<GlobalScopeCreationParams>(
window->Url(), mojom::blink::ScriptType::kModule, global_scope_name,
@@ -147,8 +157,7 @@ void ThreadedWorkletMessagingProxy::Initialize(
window->GetHttpsState(), worker_clients,
frame_client->CreateWorkerContentSettingsClient(),
OriginTrialContext::GetInheritedTrialFeatures(window).get(),
- base::UnguessableToken::Create(),
- std::make_unique<WorkerSettings>(window->GetFrame()->GetSettings()),
+ base::UnguessableToken::Create(), std::move(worklet_settings),
mojom::blink::V8CacheOptions::kDefault, module_responses_map,
mojo::NullRemote() /* browser_interface_broker */,
window->GetFrame()->Loader().CreateWorkerCodeCacheHost(),
diff --git a/third_party/blink/renderer/core/workers/worker_settings.cc b/third_party/blink/renderer/core/workers/worker_settings.cc
index 45680c5f6ea0c7e89ccf43eb88f8a11e3318c02e..3fa3af62f4e7ba8186441c5e3184b1c04fe32d12 100644
--- a/third_party/blink/renderer/core/workers/worker_settings.cc
@@ -113,56 +71,3 @@ index 45c60dd2c44b05fdd279f759069383479823c7f2..33a2a0337efb9a46293e11d0d09b3fc1
GenericFontFamilySettings generic_font_family_settings_;
};
diff --git a/third_party/blink/renderer/core/workers/worklet_global_scope.cc b/third_party/blink/renderer/core/workers/worklet_global_scope.cc
index b5300dea97f20d72a807543a6da0baf61d21955f..a7030c1ba6851b26c765c7b05cd26e1453866719 100644
--- a/third_party/blink/renderer/core/workers/worklet_global_scope.cc
+++ b/third_party/blink/renderer/core/workers/worklet_global_scope.cc
@@ -32,6 +32,7 @@
#include "third_party/blink/renderer/core/script/modulator.h"
#include "third_party/blink/renderer/core/workers/global_scope_creation_params.h"
#include "third_party/blink/renderer/core/workers/worker_reporting_proxy.h"
+#include "third_party/blink/renderer/core/workers/worker_settings.h"
#include "third_party/blink/renderer/core/workers/worker_thread.h"
#include "third_party/blink/renderer/core/workers/worklet_module_responses_map.h"
#include "third_party/blink/renderer/core/workers/worklet_module_tree_client.h"
@@ -110,6 +111,10 @@ WorkletGlobalScope::WorkletGlobalScope(
parent_cross_origin_isolated_capability_(
creation_params->cross_origin_isolated_capability),
parent_is_isolated_context_(creation_params->parent_is_isolated_context),
+ node_integration_in_worker_(
+ creation_params->worker_settings
+ ? creation_params->worker_settings->NodeIntegrationInWorker()
+ : false),
browser_interface_broker_proxy_(this) {
DCHECK((thread_type_ == ThreadType::kMainThread && frame_) ||
(thread_type_ == ThreadType::kOffMainThread && worker_thread_));
diff --git a/third_party/blink/renderer/core/workers/worklet_global_scope.h b/third_party/blink/renderer/core/workers/worklet_global_scope.h
index c7dd62900f0de48ab992a7c99058f5b6d98212cf..47ceea11ec9db6b67cef6945d165f46c868f4ca5 100644
--- a/third_party/blink/renderer/core/workers/worklet_global_scope.h
+++ b/third_party/blink/renderer/core/workers/worklet_global_scope.h
@@ -140,6 +140,13 @@ class CORE_EXPORT WorkletGlobalScope : public WorkerOrWorkletGlobalScope {
// Returns the WorkletToken that uniquely identifies this worklet.
virtual WorkletToken GetWorkletToken() const = 0;
+ // Electron: returns whether the creator frame had the
+ // `nodeIntegrationInWorker` web preference enabled. Copied from
+ // GlobalScopeCreationParams::worker_settings at construction time so the
+ // value is readable on the worker thread without crossing back to the
+ // main thread.
+ bool NodeIntegrationInWorker() const { return node_integration_in_worker_; }
+
// Returns the ExecutionContextToken that uniquely identifies the parent
// context that created this worklet. Note that this will always be a
// LocalFrameToken.
@@ -207,6 +214,11 @@ class CORE_EXPORT WorkletGlobalScope : public WorkerOrWorkletGlobalScope {
// TODO(crbug.com/1206150): We need a spec for this capability.
const bool parent_is_isolated_context_;
+ // Electron: snapshot of the creator frame's nodeIntegrationInWorker
+ // WebPreference, copied out of GlobalScopeCreationParams::worker_settings
+ // at construction time.
+ const bool node_integration_in_worker_;
+
// This is the interface that handles generated code cache
// requests both to fetch code cache when loading resources
// and to store generated code cache to disk.

View File

@@ -133,10 +133,10 @@ index 6fe4f0492dc1f3eaf576c8ff7866080a54cb81c1..41e8e052ff81df78ece87163b0499966
// Recreate the buffer in the constructor.
InternalFieldInfo* casted_info = static_cast<InternalFieldInfo*>(info);
diff --git a/src/env.cc b/src/env.cc
index b5cf58cc953590493beb52abf249e33e486ffc46..347ec5c42e098186ff489dff199ac5989961f6e3 100644
index 57a46c8be2e052b298ed841eed6f291d62711750..e4ffaa465a4ffe21334496c52334fcb1404f67a9 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -1765,10 +1765,10 @@ void AsyncHooks::Deserialize(Local<Context> context) {
@@ -1764,10 +1764,10 @@ void AsyncHooks::Deserialize(Local<Context> context) {
context->GetDataFromSnapshotOnce<Array>(
info_->js_execution_async_resources).ToLocalChecked();
} else {
@@ -149,7 +149,7 @@ index b5cf58cc953590493beb52abf249e33e486ffc46..347ec5c42e098186ff489dff199ac598
// The native_execution_async_resources_ field requires v8::Local<> instances
// for async calls whose resources were on the stack as JS objects when they
@@ -1808,7 +1808,7 @@ AsyncHooks::SerializeInfo AsyncHooks::Serialize(Local<Context> context,
@@ -1807,7 +1807,7 @@ AsyncHooks::SerializeInfo AsyncHooks::Serialize(Local<Context> context,
info.async_id_fields = async_id_fields_.Serialize(context, creator);
if (!js_execution_async_resources_.IsEmpty()) {
info.js_execution_async_resources = creator->AddData(
@@ -458,7 +458,7 @@ index fea0426496978c0003fe1481afcf93fc9c23edca..c9588880d05435ab9f4e23fcff74c933
CHECK(
diff --git a/src/node_contextify.cc b/src/node_contextify.cc
index 3c234205e89be7e976dae5c3fcc73ca67953e034..e66d4fcb0c064f96cdb819c783027d864fe88d12 100644
index 986a2d8da7fd04b5d4060d9c8d44c61a231dcce6..9f11d32c70366524cf3b7c1cfdfd24f31e438e7b 100644
--- a/src/node_contextify.cc
+++ b/src/node_contextify.cc
@@ -113,7 +113,7 @@ namespace {
@@ -479,7 +479,7 @@ index 3c234205e89be7e976dae5c3fcc73ca67953e034..e66d4fcb0c064f96cdb819c783027d86
PropertyAttribute attributes = PropertyAttribute::None;
bool is_declared =
@@ -1666,7 +1666,7 @@ static MaybeLocal<Function> CompileFunctionForCJSLoader(
@@ -1665,7 +1665,7 @@ static MaybeLocal<Function> CompileFunctionForCJSLoader(
bool* cache_rejected,
bool is_cjs_scope,
ScriptCompiler::CachedData* cached_data) {
@@ -533,10 +533,10 @@ index 55a0c986c5b6989ee9ce277bb6a9778abb2ad2ee..809d88f21e5572807e38132d40ee7587
READONLY_PROPERTY(target, "exitCodes", exit_codes);
diff --git a/src/node_file.cc b/src/node_file.cc
index 96aac2d86695732bf6805f2ad2168a62241b5045..547455bb5011677719a8de1f98cb447561bce6aa 100644
index c7a9648b0f83e910190dc620f4b72577ffde6c44..46cd16b535d9bd651ef733ca52ea58db7d39b09f 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -3850,7 +3850,7 @@ void BindingData::Deserialize(Local<Context> context,
@@ -3857,7 +3857,7 @@ void BindingData::Deserialize(Local<Context> context,
int index,
InternalFieldInfoBase* info) {
DCHECK_IS_SNAPSHOT_SLOT(index);

View File

@@ -14,10 +14,10 @@ We don't need to do this for zlib, as the existing gn workflow uses the same
Upstreamed at https://github.com/nodejs/node/pull/55903
diff --git a/unofficial.gni b/unofficial.gni
index bff7b0650cfe8578a044e45d0f9e352859909695..4ab316e45bd84e43a53335df60f847b17fe6c2fa 100644
index a773152813376bef1fa227c331241a1d944c9317..43f09d1e68c88d3ba3b862a1a74769f73c370894 100644
--- a/unofficial.gni
+++ b/unofficial.gni
@@ -199,7 +199,17 @@ template("node_gn_build") {
@@ -203,7 +203,17 @@ template("node_gn_build") {
configs -= [ "//build/config/gcc:symbol_visibility_hidden" ]
configs += [ "//build/config/gcc:symbol_visibility_default" ]
}
@@ -36,7 +36,7 @@ index bff7b0650cfe8578a044e45d0f9e352859909695..4ab316e45bd84e43a53335df60f847b1
if (v8_enable_i18n_support) {
deps += [ "//third_party/icu" ]
}
@@ -232,6 +242,19 @@ template("node_gn_build") {
@@ -236,6 +246,19 @@ template("node_gn_build") {
sources += node_inspector.node_inspector_sources +
node_inspector.node_inspector_generated_sources
}

View File

@@ -3,20 +3,14 @@ From: Shelley Vohr <shelley.vohr@gmail.com>
Date: Wed, 17 Apr 2024 08:17:49 -0400
Subject: build: enable perfetto
Enable perfetto by default in Node.js. Node.js disables perfetto by
default but is broken on build - they don't currently add guards for
`V8_USE_PERFETTO` and upstream only defines certain functions
on `v8::TracingController` if perfetto is disabled. Electron already
had minimal to no support for Node.js trace events, so the impact of
adding associated guards there should be relatively small.
We should upstream this as it will eventually impact Node.js as well.
Enable perfetto by default in Node.js and wire track events
through legacy shim.
diff --git a/lib/internal/constants.js b/lib/internal/constants.js
index 8d7204f6cb48f783adc4d1c1eb2de0c83b7fffe2..a154559a56bf383d3c26af523c9bb07b564ef600 100644
index 8d7204f6cb48f783adc4d1c1eb2de0c83b7fffe2..8061013fcfe3c3b02aabaca0447069423ac853b2 100644
--- a/lib/internal/constants.js
+++ b/lib/internal/constants.js
@@ -5,12 +5,15 @@ const isWindows = process.platform === 'win32';
@@ -5,12 +5,16 @@ const isWindows = process.platform === 'win32';
module.exports = {
// Alphabet chars.
CHAR_UPPERCASE_A: 65, /* A */
@@ -28,61 +22,216 @@ index 8d7204f6cb48f783adc4d1c1eb2de0c83b7fffe2..a154559a56bf383d3c26af523c9bb07b
CHAR_LOWERCASE_B: 98, /* b */
+ CHAR_UPPERCASE_E: 69, /* E */
CHAR_LOWERCASE_E: 101, /* e */
+ CHAR_UPPERCASE_I: 73, /* I */
+
CHAR_LOWERCASE_N: 110, /* n */
// Non-alphabetic chars.
diff --git a/lib/internal/http.js b/lib/internal/http.js
index f8b4fd7c4ca5a0907806c7e804de8c951675a36a..209e3bcf8be5a23ac528dcd673bed82cbad709ca 100644
index f8b4fd7c4ca5a0907806c7e804de8c951675a36a..0f924a5cc6718415226ffef5f8bc40a51043be04 100644
--- a/lib/internal/http.js
+++ b/lib/internal/http.js
@@ -11,8 +11,8 @@ const {
const { setUnrefTimeout } = require('internal/timers');
const { getCategoryEnabledBuffer, trace } = internalBinding('trace_events');
const {
- CHAR_LOWERCASE_B,
- CHAR_LOWERCASE_E,
+ CHAR_UPPERCASE_B,
+ CHAR_UPPERCASE_E,
} = require('internal/constants');
@@ -9,7 +9,7 @@ const {
} = primordials;
const { URL } = require('internal/url');
@@ -51,11 +51,13 @@ function isTraceHTTPEnabled() {
const traceEventCategory = 'node,node.http';
const { setUnrefTimeout } = require('internal/timers');
-const { getCategoryEnabledBuffer, trace } = internalBinding('trace_events');
+const { isTraceCategoryEnabled, trace } = internalBinding('trace_events');
const {
CHAR_LOWERCASE_B,
CHAR_LOWERCASE_E,
@@ -42,13 +42,11 @@ function getNextTraceEventId() {
return ++traceEventId;
}
-const httpEnabled = getCategoryEnabledBuffer('node.http');
-
function isTraceHTTPEnabled() {
- return httpEnabled[0] > 0;
+ return isTraceCategoryEnabled('node.http');
}
-const traceEventCategory = 'node,node.http';
+const traceEventCategory = 'node.http';
function traceBegin(...args) {
- trace(CHAR_LOWERCASE_B, traceEventCategory, ...args);
+ // See v8/src/builtins/builtins-trace.cc - must be uppercase for perfetto
+ trace(CHAR_UPPERCASE_B, traceEventCategory, ...args);
trace(CHAR_LOWERCASE_B, traceEventCategory, ...args);
diff --git a/lib/internal/perf/usertiming.js b/lib/internal/perf/usertiming.js
index 88bb63ead2d3d620dea6b54db043a404b484ead9..a37386bbf51b55b26a140cd81fbaf26d78015f21 100644
--- a/lib/internal/perf/usertiming.js
+++ b/lib/internal/perf/usertiming.js
@@ -13,6 +13,12 @@ const { PerformanceEntry, kSkipThrow } = require('internal/perf/performance_entr
const { now } = require('internal/perf/utils');
const { enqueue, bufferUserTiming } = require('internal/perf/observe');
const nodeTiming = require('internal/perf/nodetiming');
+const { isTraceCategoryEnabled, trace } = internalBinding('trace_events');
+const {
+ CHAR_LOWERCASE_B,
+ CHAR_LOWERCASE_E,
+ CHAR_UPPERCASE_I
+} = require('internal/constants');
const {
validateNumber,
@@ -41,6 +47,9 @@ const kDetail = Symbol('kDetail');
const markTimings = new SafeMap();
+const traceEventCategory = 'node.perf.usertiming';
+let traceEventId = 0;
+
const nodeTimingReadOnlyAttributes = new SafeSet(new SafeArrayIterator([
'nodeStart',
'v8Start',
@@ -168,6 +177,10 @@ function mark(name, options) {
const mark = new PerformanceMark(name, options);
enqueue(mark);
bufferUserTiming(mark);
+ if (isTraceCategoryEnabled(traceEventCategory)) {
+ trace(CHAR_UPPERCASE_I, traceEventCategory, name, undefined,
+ { startTime: mark.startTime });
+ }
return mark;
}
function traceEnd(...args) {
- trace(CHAR_LOWERCASE_E, traceEventCategory, ...args);
+ // See v8/src/builtins/builtins-trace.cc - must be uppercase for perfetto
+ trace(CHAR_UPPERCASE_E, traceEventCategory, ...args);
@@ -233,6 +246,13 @@ function measure(name, startOrMeasureOptions, endMark) {
const measure = createPerformanceMeasure(name, start, duration, detail);
enqueue(measure);
bufferUserTiming(measure);
+ if (isTraceCategoryEnabled(traceEventCategory)) {
+ const id = ++traceEventId;
+ trace(CHAR_LOWERCASE_B, traceEventCategory, name, id,
+ { startTime: start });
+ trace(CHAR_LOWERCASE_E, traceEventCategory, name, id,
+ { startTime: start, duration });
+ }
return measure;
}
function ipToInt(ip) {
diff --git a/lib/internal/trace_events_async_hooks.js b/lib/internal/trace_events_async_hooks.js
index a9f517ffc9e4eea5bc68997ffadc85d43dde2a52..e85bcd2f500ff3f5bbd2b25922c13cb29de50993 100644
--- a/lib/internal/trace_events_async_hooks.js
+++ b/lib/internal/trace_events_async_hooks.js
@@ -20,7 +20,7 @@ const {
// the specific C++ macros.
const kBeforeEvent = CHAR_LOWERCASE_B;
const kEndEvent = CHAR_LOWERCASE_E;
-const kTraceEventCategory = 'node,node.async_hooks';
+const kTraceEventCategory = 'node.async_hooks';
const kEnabled = Symbol('enabled');
diff --git a/lib/internal/util/debuglog.js b/lib/internal/util/debuglog.js
index 06a4f8a239855571dcc67cd81e7da7a255a9ebfd..1fa9e314ad796cdf74f718f0eb2a15530f5833d3 100644
--- a/lib/internal/util/debuglog.js
+++ b/lib/internal/util/debuglog.js
@@ -20,7 +20,7 @@ const {
CHAR_LOWERCASE_N: kTraceInstant,
} = require('internal/constants');
const { inspect, format, formatWithOptions } = require('internal/util/inspect');
-const { getCategoryEnabledBuffer, trace } = internalBinding('trace_events');
+const { isTraceCategoryEnabled, trace } = internalBinding('trace_events');
// `debugImpls` and `testEnabled` are deliberately not initialized so any call
// to `debuglog()` before `initializeDebugEnv()` is called will throw.
@@ -386,14 +386,13 @@ function debugWithTimer(set, cb) {
}
const traceCategory = `node,node.${StringPrototypeToLowerCase(set)}`;
- let traceCategoryBuffer;
let debugLogCategoryEnabled = false;
let timerFlags = kNone;
function ensureTimerFlagsAreUpdated() {
timerFlags &= ~kSkipTrace;
- if (traceCategoryBuffer[0] === 0) {
+ if (!isTraceCategoryEnabled(traceCategory)) {
timerFlags |= kSkipTrace;
}
}
@@ -467,7 +466,6 @@ function debugWithTimer(set, cb) {
}
emitWarningIfNeeded(set);
debugLogCategoryEnabled = testEnabled(set);
- traceCategoryBuffer = getCategoryEnabledBuffer(traceCategory);
timerFlags = kNone;
@@ -475,7 +473,7 @@ function debugWithTimer(set, cb) {
timerFlags |= kSkipLog;
}
- if (traceCategoryBuffer[0] === 0) {
+ if (!isTraceCategoryEnabled(traceCategory)) {
timerFlags |= kSkipTrace;
}
diff --git a/node.gyp b/node.gyp
index f5cd416b5fe7a51084bc4af9a4427a8e62599fd8..5eb70ce3820f2b82121bc102c5182ab768cbef36 100644
index f5cd416b5fe7a51084bc4af9a4427a8e62599fd8..b7072ce74354495bec49357f962f4ef2999bf727 100644
--- a/node.gyp
+++ b/node.gyp
@@ -182,7 +182,6 @@
@@ -182,9 +182,9 @@
'src/timers.cc',
'src/timer_wrap.cc',
'src/tracing/agent.cc',
- 'src/tracing/node_trace_buffer.cc',
'src/tracing/node_trace_writer.cc',
'src/tracing/trace_event.cc',
+ 'src/tracing/trace_categories.cc',
'src/tracing/traced_value.cc',
@@ -314,7 +313,6 @@
'src/tty_wrap.cc',
'src/udp_wrap.cc',
@@ -314,9 +314,9 @@
'src/tcp_wrap.h',
'src/timers.h',
'src/tracing/agent.h',
- 'src/tracing/node_trace_buffer.h',
'src/tracing/node_trace_writer.h',
'src/tracing/trace_event.h',
+ 'src/tracing/trace_categories.h',
'src/tracing/trace_event_common.h',
'src/tracing/traced_value.h',
'src/timer_wrap.h',
diff --git a/src/async_wrap.cc b/src/async_wrap.cc
index 301f77c419f178c4eea258e0896327f69389dda7..d5068a18392a6128ceee7f0146f8f9c77f9924bb 100644
--- a/src/async_wrap.cc
+++ b/src/async_wrap.cc
@@ -110,8 +110,7 @@ void AsyncWrap::EmitPromiseResolve(Environment* env, double async_id) {
}
void AsyncWrap::EmitTraceAsyncStart() const {
- if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(
- TRACING_CATEGORY_NODE1(async_hooks))) {
+ if (NODE_TRACE_CATEGORY_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),
diff --git a/src/env.cc b/src/env.cc
index fdabe48dd7776c59298f7d972286d0d2ed062752..c185d822b29c0b691bbf5f724f71f59638c6184d 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -650,8 +650,8 @@ void TrackingTraceStateObserver::UpdateTraceCategoryState() {
return;
}
- bool async_hooks_enabled = (*(TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(
- TRACING_CATEGORY_NODE1(async_hooks)))) != 0;
+ bool async_hooks_enabled =
+ NODE_TRACE_CATEGORY_ENABLED(TRACING_CATEGORY_NODE1(async_hooks));
Isolate* isolate = env_->isolate();
HandleScope handle_scope(isolate);
@@ -893,8 +893,7 @@ Environment::Environment(IsolateData* isolate_data,
time_origin_timestamp_,
MAYBE_FIELD_PTR(env_info, performance_state));
- if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(
- TRACING_CATEGORY_NODE1(environment)) != 0) {
+ if (NODE_TRACE_CATEGORY_ENABLED(TRACING_CATEGORY_NODE1(environment))) {
tracing::EnvironmentArgs traced_value(args, exec_args);
TRACE_EVENT_NESTABLE_ASYNC_BEGIN1(TRACING_CATEGORY_NODE1(environment),
"Environment",
diff --git a/src/inspector/tracing_agent.cc b/src/inspector/tracing_agent.cc
index 40c8aea35c931c46fc62b717c978eab0659645fd..348cdfb0b42aa18f352c220cea0b896c09f67753 100644
--- a/src/inspector/tracing_agent.cc
@@ -104,6 +253,156 @@ index 40c8aea35c931c46fc62b717c978eab0659645fd..348cdfb0b42aa18f352c220cea0b896c
void Flush(bool) override {
if (!json_writer_)
return;
diff --git a/src/node.cc b/src/node.cc
index 0bc086ccd1ff449c0f3fb08a972a0c45d3178f1c..ca74e83ef6f7b0e8b8496457af3813f07f52eb37 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -78,6 +78,11 @@
#include "large_pages/node_large_page.h"
+#if defined(V8_USE_PERFETTO)
+#include "perfetto/tracing/tracing.h"
+#include "tracing/trace_categories.h"
+#endif
+
#if defined(__APPLE__) || defined(__linux__) || defined(_WIN32)
#define NODE_USE_V8_WASM_TRAP_HANDLER 1
#else
@@ -1261,6 +1266,14 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
absl::SetMutexDeadlockDetectionMode(absl::OnDeadlockCycle::kIgnore);
}
+#if defined(V8_USE_PERFETTO)
+ // Register Node's Perfetto TrackEvent data source so that trace
+ // categories are available.
+ if (perfetto::Tracing::IsInitialized()) {
+ node::perfetto_track_event::TrackEvent::Register();
+ }
+#endif
+
#if NODE_USE_V8_WASM_TRAP_HANDLER
bool use_wasm_trap_handler =
!per_process::cli_options->disable_wasm_trap_handler;
diff --git a/src/node_contextify.cc b/src/node_contextify.cc
index 3c234205e89be7e976dae5c3fcc73ca67953e034..986a2d8da7fd04b5d4060d9c8d44c61a231dcce6 100644
--- a/src/node_contextify.cc
+++ b/src/node_contextify.cc
@@ -1026,8 +1026,7 @@ void ContextifyScript::New(const FunctionCallbackInfo<Value>& args) {
ContextifyScript* contextify_script = New(env, args.This());
- if (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(
- TRACING_CATEGORY_NODE2(vm, script)) != 0) {
+ if (NODE_TRACE_CATEGORY_ENABLED(TRACING_CATEGORY_NODE2(vm, script))) {
Utf8Value fn(isolate, filename);
TRACE_EVENT_BEGIN1(TRACING_CATEGORY_NODE2(vm, script),
"ContextifyScript::New",
diff --git a/src/node_dir.cc b/src/node_dir.cc
index c9173d404c79a69743fc75ddb6bba0ac9579c1ef..8ffac047a69b3900f37d712334c504a1c65c83fd 100644
--- a/src/node_dir.cc
+++ b/src/node_dir.cc
@@ -61,18 +61,25 @@ static const char* get_dir_func_name_by_type(uv_fs_type req_type) {
#define TRACE_NAME(name) "fs_dir.sync." #name
#define GET_TRACE_ENABLED \
- (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED( \
- TRACING_CATEGORY_NODE2(fs_dir, sync)) != 0)
+ NODE_TRACE_CATEGORY_ENABLED(TRACING_CATEGORY_NODE2(fs_dir, sync))
#define FS_DIR_SYNC_TRACE_BEGIN(syscall, ...) \
if (GET_TRACE_ENABLED) \
TRACE_EVENT_BEGIN(TRACING_CATEGORY_NODE2(fs_dir, sync), \
TRACE_NAME(syscall), \
##__VA_ARGS__);
+#if defined(V8_USE_PERFETTO)
+// Perfetto's TRACE_EVENT_END does not accept a name; it matches the prior
+// TRACE_EVENT_BEGIN on the same thread.
+#define FS_DIR_SYNC_TRACE_END(syscall, ...) \
+ if (GET_TRACE_ENABLED) \
+ TRACE_EVENT_END(TRACING_CATEGORY_NODE2(fs_dir, sync), ##__VA_ARGS__);
+#else
#define FS_DIR_SYNC_TRACE_END(syscall, ...) \
if (GET_TRACE_ENABLED) \
TRACE_EVENT_END(TRACING_CATEGORY_NODE2(fs_dir, sync), \
TRACE_NAME(syscall), \
##__VA_ARGS__);
+#endif
#define FS_DIR_ASYNC_TRACE_BEGIN0(fs_type, id) \
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0(TRACING_CATEGORY_NODE2(fs_dir, async), \
diff --git a/src/node_file.cc b/src/node_file.cc
index 96aac2d86695732bf6805f2ad2168a62241b5045..c7a9648b0f83e910190dc620f4b72577ffde6c44 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -147,16 +147,23 @@ static const char* get_fs_func_name_by_type(uv_fs_type req_type) {
#define TRACE_NAME(name) "fs.sync." #name
#define GET_TRACE_ENABLED \
- (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED( \
- TRACING_CATEGORY_NODE2(fs, sync)) != 0)
+ NODE_TRACE_CATEGORY_ENABLED(TRACING_CATEGORY_NODE2(fs, sync))
#define FS_SYNC_TRACE_BEGIN(syscall, ...) \
if (GET_TRACE_ENABLED) \
TRACE_EVENT_BEGIN( \
TRACING_CATEGORY_NODE2(fs, sync), TRACE_NAME(syscall), ##__VA_ARGS__);
+#if defined(V8_USE_PERFETTO)
+// Perfetto's TRACE_EVENT_END does not accept a name; it matches the prior
+// TRACE_EVENT_BEGIN on the same thread.
+#define FS_SYNC_TRACE_END(syscall, ...) \
+ if (GET_TRACE_ENABLED) \
+ TRACE_EVENT_END(TRACING_CATEGORY_NODE2(fs, sync), ##__VA_ARGS__);
+#else
#define FS_SYNC_TRACE_END(syscall, ...) \
if (GET_TRACE_ENABLED) \
TRACE_EVENT_END( \
TRACING_CATEGORY_NODE2(fs, sync), TRACE_NAME(syscall), ##__VA_ARGS__);
+#endif
#define FS_ASYNC_TRACE_BEGIN0(fs_type, id) \
TRACE_EVENT_NESTABLE_ASYNC_BEGIN0(TRACING_CATEGORY_NODE2(fs, async), \
diff --git a/src/node_internals.h b/src/node_internals.h
index 8e930a6fecd6589b858293d91b2454ea14ae7c73..a95dd02d4149a02ff40c759010e130c89ad1d848 100644
--- a/src/node_internals.h
+++ b/src/node_internals.h
@@ -315,6 +315,14 @@ class ThreadPoolWork {
const char* type_;
};
+#if defined(V8_USE_PERFETTO)
+// Perfetto categories must be single strings (not comma-separated).
+#define TRACING_CATEGORY_NODE "node"
+#define TRACING_CATEGORY_NODE1(one) "node." #one
+#define TRACING_CATEGORY_NODE2(one, two) "node." #one "." #two
+#define NODE_TRACE_CATEGORY_ENABLED(category) \
+ TRACE_EVENT_CATEGORY_ENABLED(category)
+#else
#define TRACING_CATEGORY_NODE "node"
#define TRACING_CATEGORY_NODE1(one) \
TRACING_CATEGORY_NODE "," \
@@ -323,6 +331,9 @@ class ThreadPoolWork {
TRACING_CATEGORY_NODE "," \
TRACING_CATEGORY_NODE "." #one "," \
TRACING_CATEGORY_NODE "." #one "." #two
+#define NODE_TRACE_CATEGORY_ENABLED(category) \
+ (*TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(category) != 0)
+#endif
// Functions defined in node.cc that are exposed via the bootstrapper object
diff --git a/src/node_trace_events.cc b/src/node_trace_events.cc
index 225b1465b7c97d972a38968faf6d685017a80bf0..4a53a07f4d5e79354e647ba3ff6e2e1095a5b684 100644
--- a/src/node_trace_events.cc
+++ b/src/node_trace_events.cc
@@ -127,7 +127,8 @@ static void GetCategoryEnabledBuffer(const FunctionCallbackInfo<Value>& args) {
node::Utf8Value category_name(isolate, args[0]);
const uint8_t* enabled_pointer =
- TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(category_name.out());
+ tracing::TraceEventHelper::GetCategoryGroupEnabled(
+ category_name.out());
uint8_t* enabled_pointer_cast = const_cast<uint8_t*>(enabled_pointer);
uint8_t size = sizeof(*enabled_pointer_cast);
diff --git a/src/tracing/agent.cc b/src/tracing/agent.cc
index eddcf6c3bf91b730d6ca72960e3048ceed7e7844..184e8647b2148bc597d9d3eb63f86ae99917c642 100644
--- a/src/tracing/agent.cc
@@ -297,48 +596,138 @@ index cd965d77b7859ff2edcf781a934594b5a9b6d251..fe1714ba77fddef693d37eeb8c7a196d
void Flush(bool blocking) override;
static const int kTracesPerFile = 1 << 19;
diff --git a/src/tracing/trace_categories.cc b/src/tracing/trace_categories.cc
new file mode 100644
index 0000000000000000000000000000000000000000..4abc4dc5d9ef9c2a9b2e3d85d858f4bbf5ac6432
--- /dev/null
+++ b/src/tracing/trace_categories.cc
@@ -0,0 +1,5 @@
+#include "tracing/trace_categories.h"
+
+#if defined(V8_USE_PERFETTO)
+PERFETTO_TRACK_EVENT_STATIC_STORAGE_IN_NAMESPACE(node);
+#endif
diff --git a/src/tracing/trace_categories.h b/src/tracing/trace_categories.h
new file mode 100644
index 0000000000000000000000000000000000000000..b28d4baa7bf766301c9281b80e0d29729ef9832e
--- /dev/null
+++ b/src/tracing/trace_categories.h
@@ -0,0 +1,66 @@
+#ifndef SRC_TRACING_TRACE_CATEGORIES_H_
+#define SRC_TRACING_TRACE_CATEGORIES_H_
+
+#if defined(V8_USE_PERFETTO)
+
+#ifdef BASE_TRACE_EVENT_BUILTIN_CATEGORIES_H_
+// Compiling mode where Chromium's Perfetto TrackEvent
+// is already set up (via PERFETTO_USE_CATEGORIES_FROM_NAMESPACE(base)).
+// Node trace categories (node.perf, node.async_hooks, etc.) will be treated
+// as dynamic categories.
+#else
+// Set up Node's own Perfetto TrackEvent data source with its trace categories,
+// following the same pattern V8 uses (PERFETTO_DEFINE_CATEGORIES_IN_NAMESPACE).
+#define PERFETTO_ENABLE_LEGACY_TRACE_EVENTS 1
+
+#include "perfetto/tracing/track_event.h"
+#include "perfetto/tracing/track_event_legacy.h"
+
+// Register Node.js trace categories as static Perfetto categories in the
+// 'node' namespace.
+PERFETTO_DEFINE_CATEGORIES_IN_NAMESPACE(
+ node,
+ perfetto::Category("__metadata"),
+ perfetto::Category("node"),
+ perfetto::Category("node.perf"),
+ perfetto::Category("node.perf.timerify"),
+ perfetto::Category("node.perf.usertiming"),
+ perfetto::Category("node.perf.event_loop"),
+ perfetto::Category("node.async_hooks"),
+ perfetto::Category("node.bootstrap"),
+ perfetto::Category("node.dns.native"),
+ perfetto::Category("node.environment"),
+ perfetto::Category("node.fs.async"),
+ perfetto::Category("node.fs.sync"),
+ perfetto::Category("node.fs_dir.async"),
+ perfetto::Category("node.fs_dir.sync"),
+ perfetto::Category("node.http"),
+ perfetto::Category("node.net.native"),
+ perfetto::Category("node.promises.rejections"),
+ perfetto::Category("node.realm"),
+ perfetto::Category("node.threadpoolwork.async"),
+ perfetto::Category("node.threadpoolwork.sync"),
+ perfetto::Category("node.vm.script"),
+ perfetto::Category("v8"));
+
+// Make Node's categories available through the default TrackEvent namespace
+// so that TRACE_EVENT macros work without qualification.
+PERFETTO_USE_CATEGORIES_FROM_NAMESPACE(node);
+
+// These deprecated phase constants are not defined by Perfetto's legacy shim
+// but are still exported to JavaScript by node_constants.cc.
+#ifndef TRACE_EVENT_PHASE_ENTER_CONTEXT
+#define TRACE_EVENT_PHASE_ENTER_CONTEXT ('(')
+#endif
+#ifndef TRACE_EVENT_PHASE_LEAVE_CONTEXT
+#define TRACE_EVENT_PHASE_LEAVE_CONTEXT (')')
+#endif
+#ifndef TRACE_EVENT_PHASE_LINK_IDS
+#define TRACE_EVENT_PHASE_LINK_IDS ('=')
+#endif
+
+#endif // BASE_TRACE_EVENT_BUILTIN_CATEGORIES_H_
+
+#endif // defined(V8_USE_PERFETTO)
+
+#endif // SRC_TRACING_TRACE_CATEGORIES_H_
diff --git a/src/tracing/trace_event.h b/src/tracing/trace_event.h
index a662a081dc3bf356bf93e4063fcb043e4d8df07b..c89cdfe2b2681fbf9946200a03d7d1f7bad21226 100644
index a662a081dc3bf356bf93e4063fcb043e4d8df07b..a7d0363e15a260feaaa5c7826a3b3137be531934 100644
--- a/src/tracing/trace_event.h
+++ b/src/tracing/trace_event.h
@@ -69,8 +69,16 @@ enum CategoryGroupEnabledFlags {
// for best performance when tracing is disabled.
// const uint8_t*
// TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(const char* category_group)
+#ifndef V8_USE_PERFETTO
#define TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED \
node::tracing::TraceEventHelper::GetCategoryGroupEnabled
+#else
+#define TRACE_EVENT_API_GET_CATEGORY_GROUP_ENABLED(category_group) \
+ ([](const char*) -> const uint8_t* { \
+ static uint8_t no = 0; \
+ return &no; \
+ })(category_group)
+#endif
@@ -7,13 +7,23 @@
// Get the number of times traces have been recorded. This is used to implement
// the TRACE_EVENT_IS_NEW_TRACE facility.
@@ -114,10 +122,15 @@ enum CategoryGroupEnabledFlags {
// const uint8_t* category_group_enabled,
// const char* name,
// uint64_t id)
+#ifndef V8_USE_PERFETTO
#define TRACE_EVENT_API_UPDATE_TRACE_EVENT_DURATION \
if (auto controller = \
node::tracing::TraceEventHelper::GetTracingController()) \
controller->UpdateTraceEventDuration
+#else
+#define TRACE_EVENT_API_UPDATE_TRACE_EVENT_DURATION(category_group_enabled, name, event_handle) \
+ (void)(category_group_enabled), (void)(name), (void)(event_handle)
+#endif
#include "v8-platform.h"
#include "tracing/agent.h"
-#include "trace_event_common.h"
#include <atomic>
// Adds a metadata event to the trace log. The |AppendValueAsTraceFormat| method
// on the convertable value will be called at flush time.
@@ -319,12 +332,15 @@ class TraceEventHelper {
+#if defined(V8_USE_PERFETTO)
+#include "tracing/trace_categories.h"
+#else
+#include "trace_event_common.h"
+#endif
+
// This header file defines implementation details of how the trace macros in
// trace_event_common.h collect and store trace events. Anything not
// implementation-specific should go in trace_macros_common.h instead of here.
+#if !defined(V8_USE_PERFETTO)
+// When Perfetto is enabled, all trace event macros and their internal
+// implementation are provided by Perfetto's track event legacy shim
+// (included via trace_categories.h). The following definitions are only
+// needed for the non-Perfetto tracing backend.
// The pointer returned from GetCategoryGroupEnabled() points to a
// value with zero or more of the following bits. Used in this class only.
@@ -301,6 +311,8 @@ enum CategoryGroupEnabledFlags {
INTERNAL_TRACE_EVENT_UID(ScopedContext) \
INTERNAL_TRACE_EVENT_UID(scoped_context)(context);
+#endif // !defined(V8_USE_PERFETTO)
+
namespace node {
namespace tracing {
@@ -319,15 +331,24 @@ class TraceEventHelper {
static void SetAgent(Agent* agent);
static inline const uint8_t* GetCategoryGroupEnabled(const char* group) {
+#ifndef V8_USE_PERFETTO
+#if defined(V8_USE_PERFETTO)
+ // Under Perfetto, callers should use TRACE_EVENT_CATEGORY_ENABLED()
+ // instead. This function exists only for backward compatibility with
+ // non-Perfetto builds.
+ static const uint8_t disabled = 0;
+ return &disabled;
+#else
v8::TracingController* controller = GetTracingController();
static const uint8_t disabled = 0;
if (controller == nullptr) [[unlikely]] {
@@ -346,56 +735,90 @@ index a662a081dc3bf356bf93e4063fcb043e4d8df07b..c89cdfe2b2681fbf9946200a03d7d1f7
}
return controller->GetCategoryGroupEnabled(group);
+#endif
+ return 0;
}
};
@@ -462,6 +478,7 @@ static inline uint64_t AddTraceEventImpl(
const char* scope, uint64_t id, uint64_t bind_id, int32_t num_args,
const char** arg_names, const uint8_t* arg_types,
const uint64_t* arg_values, unsigned int flags) {
+#ifndef V8_USE_PERFETTO
std::unique_ptr<v8::ConvertableToTraceFormat> arg_convertibles[2];
if (num_args > 0 && arg_types[0] == TRACE_VALUE_TYPE_CONVERTABLE) {
arg_convertibles[0].reset(reinterpret_cast<v8::ConvertableToTraceFormat*>(
@@ -478,6 +495,8 @@ static inline uint64_t AddTraceEventImpl(
+#if !defined(V8_USE_PERFETTO)
// TraceID encapsulates an ID that can either be an integer or pointer. Pointers
// are by default mangled with the Process ID so that they are unlikely to
// collide when the same pointer is used on different processes.
@@ -478,6 +499,7 @@ static inline uint64_t AddTraceEventImpl(
return controller->AddTraceEvent(phase, category_group_enabled, name, scope, id,
bind_id, num_args, arg_names, arg_types,
arg_values, arg_convertibles, flags);
+#endif
+ return 0;
}
static V8_INLINE uint64_t AddTraceEventWithTimestampImpl(
@@ -485,6 +504,7 @@ static V8_INLINE uint64_t AddTraceEventWithTimestampImpl(
const char* scope, uint64_t id, uint64_t bind_id, int32_t num_args,
const char** arg_names, const uint8_t* arg_types,
const uint64_t* arg_values, unsigned int flags, int64_t timestamp) {
+#ifndef V8_USE_PERFETTO
std::unique_ptr<v8::ConvertableToTraceFormat> arg_convertibles[2];
if (num_args > 0 && arg_types[0] == TRACE_VALUE_TYPE_CONVERTABLE) {
arg_convertibles[0].reset(reinterpret_cast<v8::ConvertableToTraceFormat*>(
@@ -501,12 +521,15 @@ static V8_INLINE uint64_t AddTraceEventWithTimestampImpl(
@@ -501,6 +523,7 @@ static V8_INLINE uint64_t AddTraceEventWithTimestampImpl(
return controller->AddTraceEventWithTimestamp(
phase, category_group_enabled, name, scope, id, bind_id, num_args,
arg_names, arg_types, arg_values, arg_convertibles, flags, timestamp);
+#endif
+ return 0;
}
static V8_INLINE void AddMetadataEventImpl(
const uint8_t* category_group_enabled, const char* name, int32_t num_args,
const char** arg_names, const uint8_t* arg_types,
const uint64_t* arg_values, unsigned int flags) {
+#ifndef V8_USE_PERFETTO
std::unique_ptr<v8::ConvertableToTraceFormat> arg_convertibles[2];
if (num_args > 0 && arg_types[0] == TRACE_VALUE_TYPE_CONVERTABLE) {
arg_convertibles[0].reset(reinterpret_cast<v8::ConvertableToTraceFormat*>(
@@ -522,6 +545,7 @@ static V8_INLINE void AddMetadataEventImpl(
return agent->GetTracingController()->AddMetadataEvent(
category_group_enabled, name, num_args, arg_names, arg_types, arg_values,
arg_convertibles, flags);
+#endif
}
@@ -716,6 +739,8 @@ class ScopedTracer {
Data data_;
};
// Define SetTraceValue for each allowed type. It stores the type and
+#endif // !defined(V8_USE_PERFETTO)
+
} // namespace tracing
} // namespace node
diff --git a/src/tracing/traced_value.h b/src/tracing/traced_value.h
index 0bc9df81d87562243817a6618641a49b602654e3..b6dd8b9a9c21051f3d385d5ecea9c50c8b8b1629 100644
--- a/src/tracing/traced_value.h
+++ b/src/tracing/traced_value.h
@@ -11,6 +11,26 @@
#include <span>
#include <string>
+#if defined(V8_USE_PERFETTO)
+#include "perfetto/tracing/traced_value.h"
+
+namespace perfetto {
+
+template <>
+struct TraceFormatTraits<
+ std::unique_ptr<v8::ConvertableToTraceFormat>> {
+ static void WriteIntoTrace(
+ TracedValue context,
+ const std::unique_ptr<v8::ConvertableToTraceFormat>& value) {
+ std::string json;
+ value->AppendAsTraceFormat(&json);
+ std::move(context).WriteString(json);
+ }
+};
+
+} // namespace perfetto
+#endif // defined(V8_USE_PERFETTO)
+
namespace node {
namespace tracing {
diff --git a/unofficial.gni b/unofficial.gni
index bff7b0650cfe8578a044e45d0f9e352859909695..a773152813376bef1fa227c331241a1d944c9317 100644
--- a/unofficial.gni
+++ b/unofficial.gni
@@ -143,7 +143,10 @@ template("node_gn_build") {
[ "node.gyp" ])
source_set("libnode") {
- configs += [ ":node_internal_config" ]
+ configs += [
+ ":node_internal_config",
+ "$node_v8_path:v8_tracing_config",
+ ]
public_configs = [
":node_external_config",
"deps/googletest:googletest_config",
@@ -173,6 +176,7 @@ template("node_gn_build") {
"//third_party/zstd:headers",
"$node_simdutf_path",
"$node_v8_path:v8_libplatform",
+ "$node_v8_path:v8_tracing",
]
cflags_cc = [

View File

@@ -20,22 +20,18 @@ index ab7dc27de3e304f6d912d5834da47e3b4eb25495..b6c0fd4ceee989dac55c7d54e52fef18
}
}
diff --git a/unofficial.gni b/unofficial.gni
index 4ab316e45bd84e43a53335df60f847b17fe6c2fa..def9a302830e493e51cc2b3588816fcbd3a1bb51 100644
index 43f09d1e68c88d3ba3b862a1a74769f73c370894..cedd2b0a0941fe66bdae479c4fc768ce3d7bc6ac 100644
--- a/unofficial.gni
+++ b/unofficial.gni
@@ -143,7 +143,10 @@ template("node_gn_build") {
[ "node.gyp" ])
source_set("libnode") {
- configs += [ ":node_internal_config" ]
+ configs += [
+ ":node_internal_config",
@@ -146,6 +146,7 @@ template("node_gn_build") {
configs += [
":node_internal_config",
"$node_v8_path:v8_tracing_config",
+ "//build/config/compiler:no_exit_time_destructors"
+ ]
]
public_configs = [
":node_external_config",
"deps/googletest:googletest_config",
@@ -364,6 +367,7 @@ template("node_gn_build") {
@@ -368,6 +369,7 @@ template("node_gn_build") {
"src/embedded_data.h",
]
include_dirs = [ "src", "tools" ]

View File

@@ -18,10 +18,10 @@ Stage 3.
Upstreamed in https://github.com/nodejs/node/pull/60364
diff --git a/src/node.cc b/src/node.cc
index b9d35e60f39d1edd910cd0fc1e57157458db93f5..4421ddd05f69e32f38d074a4cc04e4e7eac89e76 100644
index 6e7df97bfdb3bb2ff9fcbb0eba6118239018d632..2b221e84bb5e84829af8193b38eec31b57668e75 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -778,7 +778,7 @@ static ExitCode ProcessGlobalArgsInternal(std::vector<std::string>* args,
@@ -783,7 +783,7 @@ static ExitCode ProcessGlobalArgsInternal(std::vector<std::string>* args,
if (std::ranges::find(v8_args, "--no-js-source-phase-imports") ==
v8_args.end()) {

View File

@@ -12,10 +12,10 @@ This can be removed/refactored once Node.js upgrades to a version of V8
containing the above CL.
diff --git a/src/node.cc b/src/node.cc
index 0bc086ccd1ff449c0f3fb08a972a0c45d3178f1c..b9d35e60f39d1edd910cd0fc1e57157458db93f5 100644
index ca74e83ef6f7b0e8b8496457af3813f07f52eb37..6e7df97bfdb3bb2ff9fcbb0eba6118239018d632 100644
--- a/src/node.cc
+++ b/src/node.cc
@@ -1244,7 +1244,7 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
@@ -1249,7 +1249,7 @@ InitializeOncePerProcessInternal(const std::vector<std::string>& args,
result->platform_ = per_process::v8_platform.Platform();
}

View File

@@ -10,10 +10,10 @@ change, it seems to introduce an incompatibility when compiling
using clang modules. Disabling them resolves the issue.
diff --git a/unofficial.gni b/unofficial.gni
index def9a302830e493e51cc2b3588816fcbd3a1bb51..900c5e4d8a48d0725420518c923c7024518158b8 100644
index cedd2b0a0941fe66bdae479c4fc768ce3d7bc6ac..86bd7d18ca299d0866c872b52fb0508174c148d9 100644
--- a/unofficial.gni
+++ b/unofficial.gni
@@ -197,6 +197,10 @@ template("node_gn_build") {
@@ -199,6 +199,10 @@ template("node_gn_build") {
"CoreFoundation.framework",
"Security.framework",
]
@@ -24,7 +24,7 @@ index def9a302830e493e51cc2b3588816fcbd3a1bb51..900c5e4d8a48d0725420518c923c7024
}
if (is_posix) {
configs -= [ "//build/config/gcc:symbol_visibility_hidden" ]
@@ -369,6 +373,12 @@ template("node_gn_build") {
@@ -371,6 +375,12 @@ template("node_gn_build") {
include_dirs = [ "src", "tools" ]
configs += [ "//build/config/compiler:no_exit_time_destructors" ]

View File

@@ -7,10 +7,10 @@ libc++ added [[nodiscard]] to std::filesystem::copy_options operator|=
which causes build failures with -Werror.
diff --git a/src/node_file.cc b/src/node_file.cc
index 547455bb5011677719a8de1f98cb447561bce6aa..385db5fd6fe5db6bb7ff17e98309b6cd605a82d3 100644
index 46cd16b535d9bd651ef733ca52ea58db7d39b09f..7a7c71a0fcbb71e1c3dfcac7a00da207c4c3bf56 100644
--- a/src/node_file.cc
+++ b/src/node_file.cc
@@ -3460,11 +3460,11 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
@@ -3467,11 +3467,11 @@ static void CpSyncCopyDir(const FunctionCallbackInfo<Value>& args) {
auto file_copy_opts = std::filesystem::copy_options::recursive;
if (force) {

View File

@@ -89,7 +89,7 @@ index fb2af584a4ae777022c9ef8c20ada1edcbbbefdc..fe6300a5d5d2d6602a84cbd33736c213
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
diff --git a/src/env.cc b/src/env.cc
index fdabe48dd7776c59298f7d972286d0d2ed062752..b5cf58cc953590493beb52abf249e33e486ffc46 100644
index c185d822b29c0b691bbf5f724f71f59638c6184d..57a46c8be2e052b298ed841eed6f291d62711750 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -611,7 +611,7 @@ IsolateData::~IsolateData() {}

View File

@@ -19,7 +19,7 @@ index 2c95ac99be70b0750372e9c858753bf519498e3d..5ab30502fd232196739ca2b450e35cc9
Local<Module> module = obj->module_.Get(isolate);
if (module->GetStatus() < Module::kInstantiated) {
diff --git a/src/node_contextify.cc b/src/node_contextify.cc
index e66d4fcb0c064f96cdb819c783027d864fe88d12..619980b36db457ef7e476eacd446e3bf2a9a71d2 100644
index 9f11d32c70366524cf3b7c1cfdfd24f31e438e7b..3f1772b62aa0300540d25fb93012c49bce9d8134 100644
--- a/src/node_contextify.cc
+++ b/src/node_contextify.cc
@@ -460,7 +460,7 @@ ContextifyContext* ContextifyContext::Get(const PropertyCallbackInfo<T>& args) {

View File

@@ -1,2 +1,3 @@
chore_allow_customizing_microtask_policy_per_context.patch
build_warn_instead_of_abort_on_builtin_pgo_profile_mismatch.patch
src_use_legacy_trace_macros_in_perfetto_to_support_all_phases.patch

View File

@@ -0,0 +1,98 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: deepak1556 <hop2deep@gmail.com>
Date: Tue, 31 Mar 2026 07:05:17 +0900
Subject: src: use legacy trace macros in perfetto to support all phases
Replace the phase-specific SDK macros with the INTERNAL_TRACE_EVENT_ADD
and INTERNAL_TRACE_EVENT_ADD_WITH_ID legacy shim macros, which accept an
arbitrary phase character and forward it through Perfetto's legacy
compatibility layer. This supports nestable async ('b'/'e'/'n')
and counter ('C') trace event phases needed for Node.js.
Additionally:
1. Clear TRACE_EVENT_FLAG_COPY before calling Perfetto macros. Perfetto
manages string lifetimes internally via DynamicString and
TRACE_STR_COPY; passing FLAG_COPY triggers a CHECK failure in
Perfetto's legacy shim.
2. Default instant events (phase 'I') with GLOBAL scope to THREAD scope.
Under Perfetto, global-scope instant events land on Track::Global(0),
producing pid:0/tid:0 in the trace output, making them effectively
invisible in trace viewers.
diff --git a/src/builtins/builtins-trace.cc b/src/builtins/builtins-trace.cc
index c17d72d477d4c28d25e3f385d8af3c5b7024f7f7..5990e6cee1d08ba0e86059cb7f3affc878dcc632 100644
--- a/src/builtins/builtins-trace.cc
+++ b/src/builtins/builtins-trace.cc
@@ -181,37 +181,44 @@ BUILTIN(Trace) {
}
#if defined(V8_USE_PERFETTO)
- // TODO(skyostil): Use interned names to reduce trace size.
- auto trace_args = [&](perfetto::EventContext ctx) {
+ // Perfetto handles string lifetimes internally (via DynamicString and
+ // TRACE_STR_COPY), so TRACE_EVENT_FLAG_COPY must not be set — Perfetto's
+ // legacy shim CHECKs against it.
+ flags &= ~TRACE_EVENT_FLAG_COPY;
+
+ // Default instant events to thread scope under Perfetto. Without this,
+ // scope bits are 0 (GLOBAL), which puts events on Track::Global(0)
+ // resulting in pid:0/tid:0 in the trace output.
+ if (phase == TRACE_EVENT_PHASE_INSTANT) {
+ auto scope = flags & TRACE_EVENT_FLAG_SCOPE_MASK;
+ if (scope == TRACE_EVENT_SCOPE_GLOBAL) {
+ flags = (flags & ~TRACE_EVENT_FLAG_SCOPE_MASK) | TRACE_EVENT_SCOPE_THREAD;
+ }
+ }
+
+ // Use the legacy trace event macros which support all phase types
+ // (including nestable async 'b'/'e'/'n' and counter 'C' used by Node.js)
+ if (flags & TRACE_EVENT_FLAG_HAS_ID) {
if (num_args) {
MaybeUtf8 arg_contents(isolate, Cast<String>(arg_json));
- auto annotation = ctx.event()->add_debug_annotations();
- annotation->set_name(arg_name);
- annotation->set_legacy_json_value(*arg_contents);
+ INTERNAL_TRACE_EVENT_ADD_WITH_ID(
+ phase, dynamic_category, perfetto::DynamicString(*name), id, flags,
+ arg_name, TRACE_STR_COPY(*arg_contents));
+ } else {
+ INTERNAL_TRACE_EVENT_ADD_WITH_ID(
+ phase, dynamic_category, perfetto::DynamicString(*name), id, flags);
}
- if (flags & TRACE_EVENT_FLAG_HAS_ID) {
- auto legacy_event = ctx.event()->set_legacy_event();
- legacy_event->set_global_id(id);
+ } else {
+ if (num_args) {
+ MaybeUtf8 arg_contents(isolate, Cast<String>(arg_json));
+ INTERNAL_TRACE_EVENT_ADD(
+ phase, dynamic_category, perfetto::DynamicString(*name), flags,
+ arg_name, TRACE_STR_COPY(*arg_contents));
+ } else {
+ INTERNAL_TRACE_EVENT_ADD(phase, dynamic_category,
+ perfetto::DynamicString(*name), flags);
}
- };
-
- switch (phase) {
- case TRACE_EVENT_PHASE_BEGIN:
- TRACE_EVENT_BEGIN(dynamic_category, perfetto::DynamicString(*name),
- trace_args);
- break;
- case TRACE_EVENT_PHASE_END:
- TRACE_EVENT_END(dynamic_category, trace_args);
- break;
- case TRACE_EVENT_PHASE_INSTANT:
- TRACE_EVENT_INSTANT(dynamic_category, perfetto::DynamicString(*name),
- trace_args);
- break;
- default:
- THROW_NEW_ERROR_RETURN_FAILURE(
- isolate, NewTypeError(MessageTemplate::kTraceEventPhaseError));
}
-
#else // !defined(V8_USE_PERFETTO)
uint8_t arg_type;
uint64_t arg_value;

View File

@@ -529,7 +529,14 @@ NodeBindings::NodeBindings(BrowserEnvironment browser_env)
uv_loop_{InitEventLoop(browser_env, &worker_loop_)} {}
NodeBindings::~NodeBindings() {
StopPolling();
// Quit the embed thread.
embed_closed_ = true;
uv_sem_post(&embed_sem_);
WakeupEmbedThread();
// Wait for everything to be done.
uv_thread_join(&embed_thread_);
// Clear uv.
uv_sem_destroy(&embed_sem_);
@@ -540,26 +547,6 @@ NodeBindings::~NodeBindings() {
stop_and_close_uv_loop(uv_loop_);
}
void NodeBindings::StopPolling() {
if (!initialized_)
return;
// Tell the embed thread to quit.
embed_closed_ = true;
// The embed thread alternates between uv_sem_wait (waiting for UvRunOnce
// to finish) and PollEvents (waiting for I/O). Wake it from both.
uv_sem_post(&embed_sem_);
WakeupEmbedThread();
// Wait for it to exit.
uv_thread_join(&embed_thread_);
// Allow PrepareEmbedThread + StartPolling to restart.
embed_closed_ = false;
initialized_ = false;
}
node::IsolateData* NodeBindings::isolate_data(
v8::Local<v8::Context> context) const {
if (context->GetNumberOfEmbedderDataFields() <=
@@ -946,21 +933,12 @@ void NodeBindings::PrepareEmbedThread() {
if (initialized_)
return;
// The async handle and semaphore live for the lifetime of this
// NodeBindings instance (destroyed in ~NodeBindings), but the embed
// thread itself may be stopped and restarted via StopPolling /
// PrepareEmbedThread for pooled worklet contexts. Only init the
// handles once.
if (!embed_thread_prepared_) {
// Add dummy handle for libuv, otherwise libuv would quit when there is
// nothing to do.
uv_async_init(uv_loop_, dummy_uv_handle_.get(), nullptr);
// Start worker that will interrupt main loop when having uv events.
uv_sem_init(&embed_sem_, 0);
embed_thread_prepared_ = true;
}
// Add dummy handle for libuv, otherwise libuv would quit when there is
// nothing to do.
uv_async_init(uv_loop_, dummy_uv_handle_.get(), nullptr);
// Start worker that will interrupt main loop when having uv events.
uv_sem_init(&embed_sem_, 0);
uv_thread_create(&embed_thread_, EmbedThreadRunner, this);
}

View File

@@ -157,12 +157,6 @@ class NodeBindings {
// Notify embed thread to start polling after environment is loaded.
void StartPolling();
// Stop the embed thread and polling without destroying handles or the loop.
// After this call, PrepareEmbedThread + StartPolling can restart them.
// Used by pooled worklets that need to pause the embed thread during
// environment teardown but reuse the same NodeBindings for the next context.
void StopPolling();
node::IsolateData* isolate_data(v8::Local<v8::Context> context) const;
// Gets/sets the environment to wrap uv loop.
@@ -231,11 +225,6 @@ class NodeBindings {
// Indicates whether polling thread has been created.
bool initialized_ = false;
// Whether PrepareEmbedThread has initialized the semaphore and async handle.
// Unlike |initialized_|, this is never reset — the handles live until the
// destructor.
bool embed_thread_prepared_ = false;
// Indicates whether the app code has finished loading
// for ESM this is async after the module is loaded
bool app_code_loaded_ = false;

View File

@@ -25,7 +25,6 @@
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worker_global_scope.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worker_settings.h" // nogncheck
#include "third_party/blink/renderer/core/workers/worklet_global_scope.h" // nogncheck
namespace electron {
@@ -207,20 +206,11 @@ bool WorkerHasNodeIntegration(blink::ExecutionContext* ec) {
// owing to an inability to customize sandbox policies in these workers
// given that they're run out-of-process.
// Also avoid creating a Node.js environment for worklet global scope
// created on the main thread — those share the page's V8 context where
// Node is already wired up.
// created on the main thread.
if (ec->IsServiceWorkerGlobalScope() || ec->IsSharedWorkerGlobalScope() ||
ec->IsMainThreadWorkletGlobalScope())
return false;
// Off-main-thread worklets (AudioWorklet, PaintWorklet, AnimationWorklet,
// SharedStorageWorklet) have their own dedicated worker thread but do not
// derive from WorkerGlobalScope, so check for them separately and read the
// flag from WorkletGlobalScope, which copies it out of the same
// WorkerSettings as dedicated workers do.
if (auto* wlgs = blink::DynamicTo<blink::WorkletGlobalScope>(ec))
return wlgs->NodeIntegrationInWorker();
auto* wgs = blink::DynamicTo<blink::WorkerGlobalScope>(ec);
if (!wgs)
return false;
@@ -243,9 +233,9 @@ void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread(
return;
auto* current = WebWorkerObserver::GetCurrent();
if (!current)
current = WebWorkerObserver::Create();
current->WorkerScriptReadyForEvaluation(context);
if (current)
return;
WebWorkerObserver::Create()->WorkerScriptReadyForEvaluation(context);
}
void ElectronRendererClient::WillDestroyWorkerContextOnWorkerThread(

View File

@@ -10,12 +10,11 @@
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/threading/thread_local.h"
#include "gin/converter.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
#include "shell/common/node_bindings.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" // nogncheck
namespace electron {
@@ -24,23 +23,6 @@ namespace {
static base::NoDestructor<base::ThreadLocalOwnedPointer<WebWorkerObserver>>
lazy_tls;
// Returns true if `context` belongs to a worklet that runs on a thread
// pooled by Blink's WorkletThreadHolder, where the worker thread can be
// reused for multiple worklet contexts. For these scopes the
// WebWorkerObserver and its NodeBindings must outlive the v8::Context so
// the next pooled context can reuse them — Node.js cannot be re-initialized
// on the same thread (the allocator shim only loads once). See callers of
// blink::WorkletThreadHolder in third_party/blink for the authoritative
// list.
bool IsPooledWorkletContext(v8::Local<v8::Context> context) {
auto* ec = blink::ExecutionContext::From(context);
if (!ec)
return false;
return ec->IsAudioWorkletGlobalScope() || ec->IsPaintWorkletGlobalScope() ||
ec->IsAnimationWorkletGlobalScope() ||
ec->IsSharedStorageWorkletGlobalScope();
}
} // namespace
// static
@@ -66,21 +48,6 @@ WebWorkerObserver::~WebWorkerObserver() = default;
void WebWorkerObserver::WorkerScriptReadyForEvaluation(
v8::Local<v8::Context> worker_context) {
active_context_count_++;
if (environments_.empty()) {
// First context on this thread - do full Node.js initialization.
InitializeNewEnvironment(worker_context);
} else {
// Thread is being reused (AudioWorklet thread pooling). Share the
// existing Node.js environment with the new context instead of
// reinitializing, which would break existing contexts on this thread.
ShareEnvironmentWithContext(worker_context);
}
}
void WebWorkerObserver::InitializeNewEnvironment(
v8::Local<v8::Context> worker_context) {
v8::Context::Scope context_scope(worker_context);
v8::Isolate* const isolate = v8::Isolate::GetCurrent();
v8::MicrotasksScope microtasks_scope(
@@ -139,191 +106,26 @@ void WebWorkerObserver::InitializeNewEnvironment(
environments_.insert(std::move(env));
}
void WebWorkerObserver::ShareEnvironmentWithContext(
v8::Local<v8::Context> worker_context) {
v8::Context::Scope context_scope(worker_context);
v8::Isolate* const isolate = v8::Isolate::GetCurrent();
v8::MicrotasksScope microtasks_scope(
worker_context, v8::MicrotasksScope::kDoNotRunMicrotasks);
// Get the existing environment from the first context on this thread.
DCHECK(!environments_.empty());
node::Environment* env = environments_.begin()->get();
// Initialize the V8 context for Node.js use.
v8::Maybe<bool> initialized = node::InitializeContext(worker_context);
CHECK(!initialized.IsNothing() && initialized.FromJust());
// Assign the existing Node.js environment to this new context so that
// node::Environment::GetCurrent(context) returns the shared environment.
env->AssignToContext(worker_context, env->principal_realm(),
node::ContextInfo("electron_worker"));
// Get process and require from the original context to make Node.js
// APIs available in the new context.
v8::Local<v8::Context> original_context = env->context();
v8::Local<v8::Object> original_global = original_context->Global();
v8::Local<v8::Object> new_global = worker_context->Global();
v8::Local<v8::Value> process_value;
CHECK(original_global
->Get(original_context, gin::StringToV8(isolate, "process"))
.ToLocal(&process_value));
v8::Local<v8::Value> require_value;
CHECK(original_global
->Get(original_context, gin::StringToV8(isolate, "require"))
.ToLocal(&require_value));
// Set up 'global' as an alias for globalThis. Node.js bootstrapping normally
// does this during LoadEnvironment, but we skip full bootstrap for shared
// contexts.
new_global
->Set(worker_context, gin::StringToV8(isolate, "global"), new_global)
.Check();
new_global
->Set(worker_context, gin::StringToV8(isolate, "process"), process_value)
.Check();
new_global
->Set(worker_context, gin::StringToV8(isolate, "require"), require_value)
.Check();
// Copy Buffer from the original context if it exists.
v8::Local<v8::Value> buffer_value;
if (original_global->Get(original_context, gin::StringToV8(isolate, "Buffer"))
.ToLocal(&buffer_value) &&
!buffer_value->IsUndefined()) {
new_global
->Set(worker_context, gin::StringToV8(isolate, "Buffer"), buffer_value)
.Check();
}
// Restore the Blink implementations of web APIs that Node.js may
// have deleted. For first-context init this is done by the node_init script
// but we can't run that for shared contexts (it calls internalBinding).
// Instead, copy the blink-prefixed values set during first init.
for (const std::string_view key :
{"fetch", "Response", "FormData", "Request", "Headers", "EventSource"}) {
// First, check if the new context has a working Blink version.
v8::MaybeLocal<v8::Value> blink_value =
new_global->Get(worker_context, gin::StringToV8(isolate, key));
if (!blink_value.IsEmpty() && !blink_value.ToLocalChecked()->IsUndefined())
continue;
// If not, copy from the original context.
std::string blink_key = base::StrCat({"blink", key});
v8::Local<v8::Value> orig_value;
if (original_global->Get(original_context, gin::StringToV8(isolate, key))
.ToLocal(&orig_value) &&
!orig_value->IsUndefined()) {
new_global->Set(worker_context, gin::StringToV8(isolate, key), orig_value)
.Check();
}
}
}
void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
node::Environment* env = node::Environment::GetCurrent(context);
if (!env)
return;
const bool is_pooled_worklet = IsPooledWorkletContext(context);
active_context_count_--;
if (active_context_count_ == 0) {
// Last context on this thread — full cleanup.
{
v8::Context::Scope context_scope(env->context());
// Emit the "exit" event on the process object. We avoid using
// gin_helper::EmitEvent here because it goes through
// CallMethodWithArgs, which creates a node::CallbackScope. During
// worker shutdown (PrepareForShutdownOnWorkerThread), the
// CallbackScope destructor's InternalCallbackScope::Close() tries to
// process ticks and microtask checkpoints, which can SEGV because the
// worker context is being torn down by Blink.
v8::Isolate* isolate = env->isolate();
v8::HandleScope handle_scope(isolate);
v8::Local<v8::Context> ctx = env->context();
v8::Local<v8::Value> emit_v;
if (env->process_object()
->Get(ctx, gin::StringToV8(isolate, "emit"))
.ToLocal(&emit_v) &&
emit_v->IsFunction()) {
v8::Local<v8::Value> args[] = {gin::StringToV8(isolate, "exit")};
v8::TryCatch try_catch(isolate);
emit_v.As<v8::Function>()
->Call(ctx, env->process_object(), 1, args)
.FromMaybe(v8::Local<v8::Value>());
// We are mid-teardown and about to destroy the worker's
// node::Environment, so we cannot let an exception thrown by an
// 'exit' listener propagate back into Blink (it would assert in
// V8::FromJustIsNothing on the next call into V8). Log it and
// explicitly reset the TryCatch so the destructor doesn't rethrow.
if (try_catch.HasCaught()) {
if (auto message = try_catch.Message(); !message.IsEmpty()) {
std::string str;
if (gin::ConvertFromV8(isolate, message->Get(), &str))
LOG(ERROR) << "Exception thrown from worker 'exit' handler: "
<< str;
}
try_catch.Reset();
}
}
}
// Prevent UvRunOnce from using the environment after it's destroyed.
node_bindings_->set_uv_env(nullptr);
// Stop the embed thread before destroying environments. The embed
// thread's PollEvents and FreeEnvironment's uv_run both compete for
// completions on the same libuv event loop; on Windows (IOCP) this
// race can deadlock. Joining the embed thread first eliminates the
// contention so FreeEnvironment's uv_run can drain handles cleanly.
// For pooled worklets the thread is restarted in
// InitializeNewEnvironment via PrepareEmbedThread + StartPolling.
node_bindings_->StopPolling();
// Destroying the node environment will also run the uv loop.
{
util::ExplicitMicrotasksScope microtasks_scope(
context->GetMicrotaskQueue());
environments_.clear();
}
// ElectronBindings is tracking node environments.
electron_bindings_->EnvironmentDestroyed(env);
// For non-pooled worker contexts (e.g., dedicated workers) Blink does
// not reuse the worker thread, so tear down the observer completely.
//
// For pooled worklet contexts (AudioWorklet, PaintWorklet,
// AnimationWorklet, SharedStorageWorklet — see
// blink::WorkletThreadHolder) the same NodeBindings must be reused
// for the next context on the thread because Node.js cannot be
// re-initialized on the same thread. Keep the observer alive and let
// the next WorkerScriptReadyForEvaluation call
// InitializeNewEnvironment, which restarts the embed thread via
// PrepareEmbedThread + StartPolling.
if (!is_pooled_worklet) {
lazy_tls->Set(nullptr); // destroys *this; do not access members below
return;
}
} else {
// Other contexts still use the shared environment. Just unassign
// this context from the environment if it's not the primary context
// (the primary context must stay assigned because env->context()
// references it, and UvRunOnce enters that context scope).
if (context != env->context()) {
env->UnassignFromContext(context);
}
// If the destroyed context IS the primary context, we leave the env
// assigned to it. The env's PrincipalRealm holds a Global<Context>
// reference that keeps the V8 context alive even though Blink has
// torn down its side. This is safe because UvRunOnce only needs
// the V8 context scope, not Blink-side objects.
if (env) {
v8::Context::Scope context_scope(env->context());
gin_helper::EmitEvent(env->isolate(), env->process_object(), "exit");
}
// Destroying the node environment will also run the uv loop.
{
util::ExplicitMicrotasksScope microtasks_scope(
context->GetMicrotaskQueue());
base::EraseIf(environments_,
[env](auto const& item) { return item.get() == env; });
}
// ElectronBindings is tracking node environments.
electron_bindings_->EnvironmentDestroyed(env);
if (lazy_tls->Get())
lazy_tls->Set(nullptr);
}
} // namespace electron

View File

@@ -40,17 +40,9 @@ class WebWorkerObserver {
void ContextWillDestroy(v8::Local<v8::Context> context);
private:
// Full initialization for the first context on a thread.
void InitializeNewEnvironment(v8::Local<v8::Context> context);
// Share existing environment with a new context on a reused thread.
void ShareEnvironmentWithContext(v8::Local<v8::Context> context);
std::unique_ptr<NodeBindings> node_bindings_;
std::unique_ptr<ElectronBindings> electron_bindings_;
base::flat_set<std::shared_ptr<node::Environment>> environments_;
// Number of active contexts using the environment on this thread.
size_t active_context_count_ = 0;
};
} // namespace electron

View File

@@ -185,4 +185,112 @@ ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== '
expect(parsed.traceEvents.some((x: any) => x.cat === 'disabled-by-default-v8.cpu_profiler' && x.name === 'ProfileChunk')).to.be.true();
});
});
describe('node trace categories', () => {
it('captures performance.mark() as instant trace events', async function () {
const { performance } = require('node:perf_hooks');
await contentTracing.startRecording({
included_categories: ['node.perf.usertiming']
});
performance.mark('test-trace-mark');
const resultPath = await contentTracing.stopRecording();
const data = fs.readFileSync(resultPath, 'utf8');
const parsed = JSON.parse(data);
const markEvents = parsed.traceEvents.filter(
(x: any) => x.cat === 'node.perf.usertiming' && x.name === 'test-trace-mark'
);
expect(markEvents).to.have.lengthOf.at.least(1, 'should have node.perf.usertiming events for performance.mark()');
expect(markEvents[0].ph).to.equal('I', 'performance.mark() should emit instant (I) phase events');
});
it('captures performance.measure() as nestable async begin/end trace events', async function () {
const { performance } = require('node:perf_hooks');
await contentTracing.startRecording({
included_categories: ['node.perf.usertiming']
});
performance.mark('trace-measure-start');
await setTimeout(100);
performance.mark('trace-measure-end');
performance.measure('test-trace-measure', 'trace-measure-start', 'trace-measure-end');
const resultPath = await contentTracing.stopRecording();
const data = fs.readFileSync(resultPath, 'utf8');
const parsed = JSON.parse(data);
const measureEvents = parsed.traceEvents.filter(
(x: any) => x.cat === 'node.perf.usertiming' && x.name === 'test-trace-measure'
);
expect(measureEvents.some((x: any) => x.ph === 'b')).to.be.true('should have nestable async begin (b) event');
expect(measureEvents.some((x: any) => x.ph === 'e')).to.be.true('should have nestable async end (e) event');
});
it('captures node.fs.sync trace events for file operations', async function () {
await contentTracing.startRecording({
included_categories: ['node.fs.sync']
});
fs.readFileSync(__filename, 'utf8');
const resultPath = await contentTracing.stopRecording();
const data = fs.readFileSync(resultPath, 'utf8');
const parsed = JSON.parse(data);
const fsEvents = parsed.traceEvents.filter(
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.fs.sync')
);
expect(fsEvents).to.have.lengthOf.at.least(1, 'should have node.fs.sync trace events');
});
it('captures multiple node categories simultaneously', async function () {
const vm = require('node:vm');
await contentTracing.startRecording({
included_categories: ['node.async_hooks', 'node.vm.script']
});
vm.runInNewContext('1 + 1');
await fs.promises.readFile(__filename, 'utf8');
const resultPath = await contentTracing.stopRecording();
const data = fs.readFileSync(resultPath, 'utf8');
const parsed = JSON.parse(data);
const asyncHooksEvents = parsed.traceEvents.filter(
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.async_hooks')
);
const vmEvents = parsed.traceEvents.filter(
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.vm.script')
);
expect(asyncHooksEvents).to.have.lengthOf.at.least(1, 'should have node.async_hooks events');
expect(vmEvents).to.have.lengthOf.at.least(1, 'should have node.vm.script events');
});
it('captures events using wildcard category pattern node.fs.*', async function () {
await contentTracing.startRecording({
included_categories: ['node.fs.*']
});
fs.readFileSync(__filename, 'utf8');
await fs.promises.readFile(__filename, 'utf8');
const resultPath = await contentTracing.stopRecording();
const data = fs.readFileSync(resultPath, 'utf8');
const parsed = JSON.parse(data);
const syncEvents = parsed.traceEvents.filter(
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.fs.sync')
);
const asyncEvents = parsed.traceEvents.filter(
(x: any) => typeof x.cat === 'string' && x.cat.includes('node.fs.async')
);
expect(syncEvents).to.have.lengthOf.at.least(1, 'should have node.fs.sync events from wildcard pattern');
expect(asyncEvents).to.have.lengthOf.at.least(1, 'should have node.fs.async events from wildcard pattern');
});
});
});

View File

@@ -1625,27 +1625,6 @@ describe('chromium features', () => {
expect(data).to.equal('function function function function function');
});
it('AudioWorklet keeps node integration across pooled worker threads', async () => {
// Regression test for https://github.com/electron/electron/issues/41263.
// Blink pools the AudioWorklet backing thread (Chromium CL:5270028) so
// the Nth+ AudioWorklet on a page reuses the same thread; the page
// creates several AudioWorklet contexts in sequence and asserts node
// integration is wired up in every one of them.
const w = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: true,
contextIsolation: false
}
});
w.loadURL(`file://${fixturesPath}/pages/audio-worklet.html`);
const [, results] = await once(ipcMain, 'audio-worklet-result');
expect(results).to.be.an('array').with.length.greaterThan(0);
for (const r of results) expect(r).to.equal('ok');
});
describe('SharedWorker', () => {
it('can work', async () => {
const w = new BrowserWindow({ show: false });

View File

@@ -1,44 +0,0 @@
<html>
<body>
<script type="text/javascript" charset="utf-8">
const { ipcRenderer } = require('electron');
// Create a number of AudioContext + AudioWorklet pairs in sequence so
// that Blink's WorkletThreadHolder pools and reuses the underlying
// worker thread (Chromium CL:5270028). For each context we ask the
// worklet to report whether `require` is a function and post that back
// via its MessagePort. The bug being guarded is that the Nth+ pooled
// worklet would silently lose its Node.js environment, so the test
// must run enough iterations to exercise thread reuse.
const NUM_CONTEXTS = 6;
async function runOne(index) {
const audioCtx = new AudioContext();
try {
await audioCtx.audioWorklet.addModule('../workers/audio_worklet_node.js');
const node = new AudioWorkletNode(audioCtx, 'node-integration-probe');
const reply = new Promise((resolve) => {
node.port.onmessage = (e) => resolve(e.data);
});
node.port.postMessage('probe');
node.connect(audioCtx.destination);
return await reply;
} finally {
await audioCtx.close();
}
}
(async () => {
const results = [];
for (let i = 0; i < NUM_CONTEXTS; i++) {
try {
results.push(await runOne(i));
} catch (err) {
results.push(`error: ${err && err.message ? err.message : err}`);
}
}
ipcRenderer.send('audio-worklet-result', results);
})();
</script>
</body>
</html>

View File

@@ -1,28 +0,0 @@
// Reports whether the Node.js environment is wired up inside this
// AudioWorklet's global scope. Used by spec/fixtures/pages/audio-worklet.html
// to verify that nodeIntegrationInWorker keeps working when Blink reuses a
// pooled worker thread for multiple AudioWorklet contexts.
class NodeIntegrationProbeProcessor extends AudioWorkletProcessor {
constructor () {
super();
this.port.onmessage = () => {
let info;
try {
// require should be a function and `node:timers` should resolve.
const ok = typeof require === 'function' &&
typeof require('node:timers').setImmediate === 'function' &&
typeof process === 'object';
info = ok ? 'ok' : 'missing';
} catch (err) {
info = `throw: ${err && err.message ? err.message : err}`;
}
this.port.postMessage(info);
};
}
process () {
return true;
}
}
registerProcessor('node-integration-probe', NodeIntegrationProbeProcessor);