diff --git a/lib/browser/api/app.ts b/lib/browser/api/app.ts index ff024def5e84..ba7c8ec06aad 100644 --- a/lib/browser/api/app.ts +++ b/lib/browser/api/app.ts @@ -1,11 +1,14 @@ import { Menu } from 'electron/main'; +import { EventEmitter } from 'events'; import * as fs from 'fs'; const bindings = process._linkedBinding('electron_browser_app'); const commandLine = process._linkedBinding('electron_common_command_line'); const { app } = bindings; +Object.setPrototypeOf(app, EventEmitter.prototype); + // Only one app object permitted. export default app; diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 264539be3965..bed19dbf5165 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -138,3 +138,4 @@ fix_add_macos_memory_query_fallback_to_avoid_crash.patch fix_resolve_dynamic_background_material_update_issue_on_windows_11.patch chore_restore_some_deprecated_wrapper_utility_in_gin.patch feat_add_support_for_embedder_snapshot_validation.patch +chore_add_electron_objects_to_wrappablepointertag.patch diff --git a/patches/chromium/chore_add_electron_objects_to_wrappablepointertag.patch b/patches/chromium/chore_add_electron_objects_to_wrappablepointertag.patch new file mode 100644 index 000000000000..57551dbabad1 --- /dev/null +++ b/patches/chromium/chore_add_electron_objects_to_wrappablepointertag.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: deepak1556 +Date: Wed, 20 Aug 2025 04:03:11 +0900 +Subject: chore: add electron objects to WrappablePointerTag + +Extends gin::WrappablePointerTag with tags needed for +electron objects that extend gin::Wrappable and gets +allocated on the cpp heap + +diff --git a/gin/public/wrappable_pointer_tags.h b/gin/public/wrappable_pointer_tags.h +index a507d1d837ab3ec2b2d3ae7978d9d410ab2ec2d1..a547ecacad732f73512abbe5da81483208bb9e81 100644 +--- a/gin/public/wrappable_pointer_tags.h ++++ b/gin/public/wrappable_pointer_tags.h +@@ -72,7 +72,8 @@ enum WrappablePointerTag : uint16_t { + kTextInputControllerBindings, // content::TextInputControllerBindings + kWebAXObjectProxy, // content::WebAXObjectProxy + kWrappedExceptionHandler, // extensions::WrappedExceptionHandler +- kLastPointerTag = kWrappedExceptionHandler, ++ kElectronApp, // electron::api::App ++ kLastPointerTag = kElectronApp, + }; + + static_assert(kLastPointerTag < diff --git a/shell/browser/api/electron_api_app.cc b/shell/browser/api/electron_api_app.cc index 416a5fd11e95..2db351326ab6 100644 --- a/shell/browser/api/electron_api_app.cc +++ b/shell/browser/api/electron_api_app.cc @@ -75,7 +75,6 @@ #include "shell/common/gin_converters/value_converter.h" #include "shell/common/gin_helper/dictionary.h" #include "shell/common/gin_helper/error_thrower.h" -#include "shell/common/gin_helper/handle.h" #include "shell/common/gin_helper/object_template_builder.h" #include "shell/common/language_util.h" #include "shell/common/node_includes.h" @@ -83,6 +82,8 @@ #include "shell/common/thread_restrictions.h" #include "shell/common/v8_util.h" #include "ui/gfx/image/image.h" +#include "v8/include/cppgc/allocation.h" +#include "v8/include/v8-traced-handle.h" #if BUILDFLAG(IS_WIN) #include "base/strings/utf_string_conversions.h" @@ -365,7 +366,11 @@ struct Converter { namespace electron::api { -gin::DeprecatedWrapperInfo App::kWrapperInfo = {gin::kEmbedderNativeGin}; +gin::WrapperInfo App::kWrapperInfo = {{gin::kEmbedderNativeGin}, + gin::kElectronApp}; + +// static +cppgc::Persistent App::instance_; namespace { @@ -561,7 +566,6 @@ App::App() { App::~App() { static_cast(ElectronBrowserClient::Get()) ->set_delegate(nullptr); - Browser::Get()->RemoveObserver(this); content::GpuDataManager::GetInstance()->RemoveObserver(this); content::BrowserChildProcessObserver::Remove(this); } @@ -1582,7 +1586,7 @@ void DockSetMenu(electron::api::Menu* menu) { } v8::Local App::GetDockAPI(v8::Isolate* isolate) { - if (dock_.IsEmpty()) { + if (dock_.IsEmptyThreadSafe()) { // Initialize the Dock API, the methods are bound to "dock" which exists // for the lifetime of "app" auto browser = base::Unretained(Browser::Get()); @@ -1610,7 +1614,7 @@ v8::Local App::GetDockAPI(v8::Isolate* isolate) { dock_.Reset(isolate, dock_obj.GetHandle()); } - return v8::Local::New(isolate, dock_); + return dock_.Get(isolate); } #endif @@ -1697,18 +1701,33 @@ void ConfigureHostResolver(v8::Isolate* isolate, // static App* App::Get() { - static base::NoDestructor app; - return app.get(); + CHECK_NE(instance_, nullptr); + return instance_.Get(); } // static -gin_helper::Handle App::Create(v8::Isolate* isolate) { - return gin_helper::CreateHandle(isolate, Get()); +App* App::Create(v8::Isolate* isolate) { + if (!instance_) { + instance_ = cppgc::MakeGarbageCollected( + isolate->GetCppHeap()->GetAllocationHandle()); + } + return instance_.Get(); +} + +const gin::WrapperInfo* App::wrapper_info() const { + return &kWrapperInfo; +} + +void App::Trace(cppgc::Visitor* visitor) const { + gin::Wrappable::Trace(visitor); +#if BUILDFLAG(IS_MAC) + visitor->Trace(dock_); +#endif } gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) { auto browser = base::Unretained(Browser::Get()); - return gin_helper::EventEmitterMixin::GetObjectTemplateBuilder(isolate) + return gin::Wrappable::GetObjectTemplateBuilder(isolate) .SetMethod("quit", base::BindRepeating(&Browser::Quit, browser)) .SetMethod("exit", base::BindRepeating(&Browser::Exit, browser)) .SetMethod("focus", base::BindRepeating(&Browser::Focus, browser)) @@ -1850,8 +1869,8 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) { .SetMethod("resolveProxy", &App::ResolveProxy); } -const char* App::GetTypeName() { - return "App"; +const char* App::GetHumanReadableName() const { + return "Electron / App"; } } // namespace electron::api diff --git a/shell/browser/api/electron_api_app.h b/shell/browser/api/electron_api_app.h index 5124851f632a..dad2ed76a81a 100644 --- a/shell/browser/api/electron_api_app.h +++ b/shell/browser/api/electron_api_app.h @@ -19,6 +19,7 @@ #include "content/public/browser/scoped_accessibility_mode.h" #include "crypto/crypto_buildflags.h" #include "electron/mas.h" +#include "gin/wrappable.h" #include "net/base/completion_once_callback.h" #include "net/base/completion_repeating_callback.h" #include "net/base/features.h" @@ -27,7 +28,7 @@ #include "shell/browser/browser_observer.h" #include "shell/browser/electron_browser_client.h" #include "shell/browser/event_emitter_mixin.h" -#include "shell/common/gin_helper/wrappable.h" +#include "v8/include/cppgc/persistent.h" #if BUILDFLAG(USE_NSS_CERTS) #include "shell/browser/certificate_manager_model.h" @@ -40,10 +41,13 @@ class FilePath; namespace gin_helper { class Dictionary; class ErrorThrower; -template -class Handle; } // namespace gin_helper +namespace v8 { +template +class TracedReference; +} + namespace electron { struct ProcessMetric; @@ -54,21 +58,23 @@ enum class JumpListResult : int; namespace api { -class App final : public ElectronBrowserClient::Delegate, - public gin_helper::DeprecatedWrappable, +class App final : public gin::Wrappable, + public ElectronBrowserClient::Delegate, public gin_helper::EventEmitterMixin, private BrowserObserver, private content::GpuDataManagerObserver, private content::BrowserChildProcessObserver { public: - static gin_helper::Handle Create(v8::Isolate* isolate); + static App* Create(v8::Isolate* isolate); static App* Get(); - // gin_helper::Wrappable - static gin::DeprecatedWrapperInfo kWrapperInfo; + // gin::Wrappable + static gin::WrapperInfo kWrapperInfo; + void Trace(cppgc::Visitor*) const override; gin::ObjectTemplateBuilder GetObjectTemplateBuilder( v8::Isolate* isolate) override; - const char* GetTypeName() override; + const gin::WrapperInfo* wrapper_info() const override; + const char* GetHumanReadableName() const override; #if BUILDFLAG(USE_NSS_CERTS) void OnCertificateManagerModelCreated( @@ -84,14 +90,13 @@ class App final : public ElectronBrowserClient::Delegate, static bool IsPackaged(); App(); + ~App() override; // disable copy App(const App&) = delete; App& operator=(const App&) = delete; private: - ~App() override; - // BrowserObserver: void OnBeforeQuit(bool* prevent_default) override; void OnWillQuit(bool* prevent_default) override; @@ -236,7 +241,7 @@ class App final : public ElectronBrowserClient::Delegate, bool MoveToApplicationsFolder(gin_helper::ErrorThrower, gin::Arguments* args); bool IsInApplicationsFolder(); v8::Local GetDockAPI(v8::Isolate* isolate); - v8::Global dock_; + v8::TracedReference dock_; #endif #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) @@ -277,6 +282,8 @@ class App final : public ElectronBrowserClient::Delegate, bool watch_singleton_socket_on_ready_ = false; std::unique_ptr scoped_accessibility_mode_; + + static cppgc::Persistent instance_; }; } // namespace api diff --git a/shell/common/node_bindings.cc b/shell/common/node_bindings.cc index 04648369ce8d..52186599b46a 100644 --- a/shell/common/node_bindings.cc +++ b/shell/common/node_bindings.cc @@ -377,9 +377,18 @@ bool IsAllowedOption(const std::string_view option) { "--no-experimental-global-navigator", }); + // This should be aligned with what's possible to set via the process object. + static constexpr auto unpacked_options = + base::MakeFixedFlatSet({ + "--expose-internals", + }); + if (debug_options.contains(option)) return electron::fuses::IsNodeCliInspectEnabled(); + if (unpacked_options.contains(option)) + return !electron::api::App::IsPackaged(); + return options.contains(option); } @@ -601,6 +610,7 @@ void NodeBindings::Initialize(v8::Isolate* const isolate, node::per_process::cli_options->disable_wasm_trap_handler = true; uint64_t process_flags = + node::ProcessInitializationFlags::kNoInitializeCppgc | node::ProcessInitializationFlags::kNoInitializeV8 | node::ProcessInitializationFlags::kNoInitializeNodeV8Platform; @@ -610,8 +620,7 @@ void NodeBindings::Initialize(v8::Isolate* const isolate, process_flags |= node::ProcessInitializationFlags::kEnableStdioInheritance; if (browser_env_ == BrowserEnvironment::kRenderer) - process_flags |= node::ProcessInitializationFlags::kNoInitializeCppgc | - node::ProcessInitializationFlags::kNoDefaultSignalHandling; + process_flags |= node::ProcessInitializationFlags::kNoDefaultSignalHandling; if (!fuses::IsNodeOptionsEnabled()) process_flags |= node::ProcessInitializationFlags::kDisableNodeOptionsEnv; diff --git a/spec/cpp-heap-spec.ts b/spec/cpp-heap-spec.ts new file mode 100644 index 000000000000..55713688a2ae --- /dev/null +++ b/spec/cpp-heap-spec.ts @@ -0,0 +1,45 @@ +import { expect } from 'chai'; + +import * as path from 'node:path'; + +import { startRemoteControlApp } from './lib/spec-helpers'; + +describe('cpp heap', () => { + describe('app module', () => { + it('should not allocate on every require', async () => { + const { remotely } = await startRemoteControlApp(); + const [usedBefore, usedAfter] = await remotely(async () => { + const { getCppHeapStatistics } = require('node:v8'); + const heapStatsBefore = getCppHeapStatistics('brief'); + { + const { app } = require('electron'); + console.log(app.name); + } + { + const { app } = require('electron'); + console.log(app.dock); + } + const heapStatsAfter = getCppHeapStatistics('brief'); + return [heapStatsBefore.used_size_bytes, heapStatsAfter.used_size_bytes]; + }); + expect(usedBefore).to.be.equal(usedAfter); + }); + + it('should record as node in heap snapshot', async () => { + const { remotely } = await startRemoteControlApp(['--expose-internals']); + const [nodeCount, hasPersistentParent] = await remotely(async (heap: string) => { + const { recordState } = require(heap); + const state = recordState(); + const rootNodes = state.snapshot.filter( + (node: any) => node.name === 'Electron / App' && node.type !== 'string'); + const hasParent = rootNodes.some((node: any) => node.incomingEdges.some( + (edge: any) => { + return edge.type === 'element' && edge.from.name === 'C++ Persistent roots'; + })); + return [rootNodes.length, hasParent]; + }, path.join(__dirname, '../../third_party/electron_node/test/common/heap')); + expect(nodeCount).to.equal(1); + expect(hasPersistentParent).to.be.true(); + }); + }); +});