refactor: api::utilityProcessWrapper managed by cppgc (#50955)
* refactor: api::utilityProcessWrapper managed by cppgc * chore: fix lint
This commit is contained in:
parent
a39108c5a4
commit
b9e462f397
5 changed files with 232 additions and 61 deletions
|
|
@ -5,6 +5,7 @@
|
|||
#include "shell/browser/api/electron_api_utility_process.h"
|
||||
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
#include "base/files/file_util.h"
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
#include "content/public/browser/service_process_host.h"
|
||||
#include "content/public/common/result_codes.h"
|
||||
#include "gin/object_template_builder.h"
|
||||
#include "gin/persistent.h"
|
||||
#include "mojo/public/cpp/bindings/pending_receiver.h"
|
||||
#include "services/network/public/cpp/originating_process_id.h"
|
||||
#include "shell/browser/api/message_port.h"
|
||||
|
|
@ -31,11 +33,13 @@
|
|||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/gin_helper/handle.h"
|
||||
#include "shell/common/gin_helper/object_template_builder.h"
|
||||
#include "shell/common/gin_helper/wrappable_pointer_tags.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "shell/common/v8_util.h"
|
||||
#include "third_party/blink/public/common/messaging/message_port_descriptor.h"
|
||||
#include "third_party/blink/public/common/messaging/transferable_message_mojom_traits.h"
|
||||
#include "third_party/blink/public/mojom/blob/blob.mojom.h"
|
||||
#include "v8/include/cppgc/allocation.h"
|
||||
|
||||
#if BUILDFLAG(IS_POSIX)
|
||||
#include "base/posix/eintr_wrapper.h"
|
||||
|
|
@ -51,20 +55,34 @@ namespace electron {
|
|||
|
||||
namespace {
|
||||
|
||||
base::IDMap<api::UtilityProcessWrapper*, base::ProcessId>&
|
||||
GetAllUtilityProcessWrappers() {
|
||||
static base::NoDestructor<
|
||||
base::IDMap<api::UtilityProcessWrapper*, base::ProcessId>>
|
||||
s_all_utility_process_wrappers;
|
||||
return *s_all_utility_process_wrappers;
|
||||
// Maps process IDs to their UtilityProcessWrapper instances.
|
||||
struct UtilityProcessRegistry {
|
||||
void Add(base::ProcessId pid, api::UtilityProcessWrapper* wrapper) {
|
||||
map_.emplace(pid, wrapper);
|
||||
}
|
||||
void Remove(base::ProcessId pid) { map_.erase(pid); }
|
||||
api::UtilityProcessWrapper* Lookup(base::ProcessId pid) {
|
||||
auto it = map_.find(pid);
|
||||
return it != map_.end() ? it->second.Get() : nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<base::ProcessId,
|
||||
cppgc::WeakPersistent<api::UtilityProcessWrapper>>
|
||||
map_;
|
||||
};
|
||||
|
||||
UtilityProcessRegistry& GetAllUtilityProcessWrappers() {
|
||||
static base::NoDestructor<UtilityProcessRegistry> registry;
|
||||
return *registry;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace api {
|
||||
|
||||
gin::DeprecatedWrapperInfo UtilityProcessWrapper::kWrapperInfo = {
|
||||
gin::kEmbedderNativeGin};
|
||||
const gin::WrapperInfo UtilityProcessWrapper::kWrapperInfo =
|
||||
electron::MakeWrapperInfo(electron::kElectronUtilityProcess);
|
||||
|
||||
UtilityProcessWrapper::UtilityProcessWrapper(
|
||||
node::mojom::NodeServiceParamsPtr params,
|
||||
|
|
@ -76,6 +94,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
|
|||
bool create_network_observer,
|
||||
bool disclaim_responsibility)
|
||||
: create_network_observer_(create_network_observer) {
|
||||
auto& allocation_handle =
|
||||
JavascriptEnvironment::GetIsolate()->GetCppHeap()->GetAllocationHandle();
|
||||
#if BUILDFLAG(IS_WIN)
|
||||
base::win::ScopedHandle stdout_write(nullptr);
|
||||
base::win::ScopedHandle stderr_write(nullptr);
|
||||
|
|
@ -194,12 +214,13 @@ UtilityProcessWrapper::UtilityProcessWrapper(
|
|||
#endif
|
||||
.WithProcessCallback(
|
||||
base::BindOnce(&UtilityProcessWrapper::OnServiceProcessLaunch,
|
||||
weak_factory_.GetWeakPtr()))
|
||||
gin::WrapPersistent(
|
||||
weak_factory_.GetWeakCell(allocation_handle))))
|
||||
.Pass());
|
||||
|
||||
node_service_remote_.set_disconnect_with_reason_handler(
|
||||
base::BindOnce(&UtilityProcessWrapper::OnServiceProcessDisconnected,
|
||||
weak_factory_.GetWeakPtr()));
|
||||
node_service_remote_.set_disconnect_with_reason_handler(base::BindOnce(
|
||||
&UtilityProcessWrapper::OnServiceProcessDisconnected,
|
||||
gin::WrapPersistent(weak_factory_.GetWeakCell(allocation_handle))));
|
||||
|
||||
// We use a separate message pipe to support postMessage API
|
||||
// instead of the existing receiver interface so that we can
|
||||
|
|
@ -214,7 +235,8 @@ UtilityProcessWrapper::UtilityProcessWrapper(
|
|||
base::SingleThreadTaskRunner::GetCurrentDefault());
|
||||
connector_->set_incoming_receiver(this);
|
||||
connector_->set_connection_error_handler(base::BindOnce(
|
||||
&UtilityProcessWrapper::CloseConnectorPort, weak_factory_.GetWeakPtr()));
|
||||
&UtilityProcessWrapper::CloseConnectorPort,
|
||||
gin::WrapPersistent(weak_factory_.GetWeakCell(allocation_handle))));
|
||||
|
||||
params->url_loader_factory_params = CreateURLLoaderFactoryParams();
|
||||
node_service_remote_->Initialize(std::move(params),
|
||||
|
|
@ -224,7 +246,7 @@ UtilityProcessWrapper::UtilityProcessWrapper(
|
|||
network_service_gone_subscription_ =
|
||||
content::RegisterNetworkServiceProcessGoneHandler(base::BindRepeating(
|
||||
&UtilityProcessWrapper::CreateAndSendURLLoaderFactory,
|
||||
weak_factory_.GetWeakPtr()));
|
||||
gin::WrapPersistent(weak_factory_.GetWeakCell(allocation_handle))));
|
||||
}
|
||||
|
||||
UtilityProcessWrapper::~UtilityProcessWrapper() {
|
||||
|
|
@ -235,7 +257,7 @@ void UtilityProcessWrapper::OnServiceProcessLaunch(
|
|||
const base::Process& process) {
|
||||
DCHECK(node_service_remote_.is_connected());
|
||||
pid_ = process.Pid();
|
||||
GetAllUtilityProcessWrappers().AddWithID(this, pid_);
|
||||
GetAllUtilityProcessWrappers().Add(pid_, this);
|
||||
if (stdout_read_fd_ != -1)
|
||||
EmitWithoutEvent("stdout", stdout_read_fd_);
|
||||
if (stderr_read_fd_ != -1)
|
||||
|
|
@ -276,7 +298,7 @@ void UtilityProcessWrapper::HandleTermination(uint32_t exit_code) {
|
|||
#endif
|
||||
}
|
||||
EmitWithoutEvent("exit", exit_code);
|
||||
Unpin();
|
||||
keep_alive_.Clear();
|
||||
}
|
||||
|
||||
void UtilityProcessWrapper::OnServiceProcessDisconnected(
|
||||
|
|
@ -455,25 +477,25 @@ UtilityProcessWrapper::CreateURLLoaderFactoryParams() {
|
|||
}
|
||||
|
||||
// static
|
||||
raw_ptr<UtilityProcessWrapper> UtilityProcessWrapper::FromProcessId(
|
||||
UtilityProcessWrapper* UtilityProcessWrapper::FromProcessId(
|
||||
base::ProcessId pid) {
|
||||
auto* utility_process_wrapper = GetAllUtilityProcessWrappers().Lookup(pid);
|
||||
return !!utility_process_wrapper ? utility_process_wrapper : nullptr;
|
||||
return utility_process_wrapper ? utility_process_wrapper : nullptr;
|
||||
}
|
||||
|
||||
// static
|
||||
gin_helper::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
|
||||
UtilityProcessWrapper* UtilityProcessWrapper::Create(
|
||||
gin::Arguments* const args) {
|
||||
if (!Browser::Get()->is_ready()) {
|
||||
args->ThrowTypeError(
|
||||
"utilityProcess cannot be created before app is ready.");
|
||||
return {};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
gin_helper::Dictionary dict;
|
||||
if (!args->GetNext(&dict)) {
|
||||
args->ThrowTypeError("Options must be an object.");
|
||||
return {};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::u16string display_name;
|
||||
|
|
@ -488,19 +510,19 @@ gin_helper::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
|
|||
dict.Get("modulePath", ¶ms->script);
|
||||
if (dict.Has("args") && !dict.Get("args", ¶ms->args)) {
|
||||
args->ThrowTypeError("Invalid value for args");
|
||||
return {};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
gin_helper::Dictionary opts;
|
||||
if (dict.Get("options", &opts)) {
|
||||
if (opts.Has("env") && !opts.Get("env", &env_map)) {
|
||||
args->ThrowTypeError("Invalid value for env");
|
||||
return {};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (opts.Has("execArgv") && !opts.Get("execArgv", ¶ms->exec_args)) {
|
||||
args->ThrowTypeError("Invalid value for execArgv");
|
||||
return {};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
opts.Get("serviceName", &display_name);
|
||||
|
|
@ -526,17 +548,13 @@ gin_helper::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
|
|||
opts.Get("disclaim", &disclaim_responsibility);
|
||||
#endif
|
||||
}
|
||||
auto handle = gin_helper::CreateHandle(
|
||||
args->isolate(),
|
||||
new UtilityProcessWrapper(
|
||||
std::move(params), display_name, std::move(stdio), env_map,
|
||||
current_working_directory, use_plugin_helper, create_network_observer,
|
||||
disclaim_responsibility));
|
||||
handle->Pin(args->isolate());
|
||||
return handle;
|
||||
v8::Isolate* isolate = args->isolate();
|
||||
return cppgc::MakeGarbageCollected<UtilityProcessWrapper>(
|
||||
isolate->GetCppHeap()->GetAllocationHandle(), std::move(params),
|
||||
display_name, std::move(stdio), env_map, current_working_directory,
|
||||
use_plugin_helper, create_network_observer, disclaim_responsibility);
|
||||
}
|
||||
|
||||
// static
|
||||
gin::ObjectTemplateBuilder UtilityProcessWrapper::GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) {
|
||||
return gin_helper::EventEmitterMixin<
|
||||
|
|
@ -546,8 +564,17 @@ gin::ObjectTemplateBuilder UtilityProcessWrapper::GetObjectTemplateBuilder(
|
|||
.SetProperty("pid", &UtilityProcessWrapper::GetOSProcessId);
|
||||
}
|
||||
|
||||
const char* UtilityProcessWrapper::GetTypeName() {
|
||||
return "UtilityProcessWrapper";
|
||||
void UtilityProcessWrapper::Trace(cppgc::Visitor* visitor) const {
|
||||
gin::Wrappable<UtilityProcessWrapper>::Trace(visitor);
|
||||
visitor->Trace(weak_factory_);
|
||||
}
|
||||
|
||||
const gin::WrapperInfo* UtilityProcessWrapper::wrapper_info() const {
|
||||
return &kWrapperInfo;
|
||||
}
|
||||
|
||||
const char* UtilityProcessWrapper::GetHumanReadableName() const {
|
||||
return "Electron / UtilityProcess";
|
||||
}
|
||||
|
||||
} // namespace api
|
||||
|
|
|
|||
|
|
@ -10,17 +10,17 @@
|
|||
#include <string>
|
||||
|
||||
#include "base/callback_list.h"
|
||||
#include "base/containers/id_map.h"
|
||||
#include "base/environment.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/process/process_handle.h"
|
||||
#include "content/public/browser/service_process_host.h"
|
||||
#include "gin/weak_cell.h"
|
||||
#include "gin/wrappable.h"
|
||||
#include "mojo/public/cpp/bindings/message.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "shell/browser/event_emitter_mixin.h"
|
||||
#include "shell/browser/net/url_loader_network_observer.h"
|
||||
#include "shell/common/gin_helper/pinnable.h"
|
||||
#include "shell/common/gin_helper/wrappable.h"
|
||||
#include "shell/common/gc_plugin.h"
|
||||
#include "shell/common/gin_helper/self_keep_alive.h"
|
||||
#include "shell/services/node/public/mojom/node_service.mojom.h"
|
||||
#include "v8/include/v8-forward.h"
|
||||
|
||||
|
|
@ -28,11 +28,6 @@ namespace gin {
|
|||
class Arguments;
|
||||
} // namespace gin
|
||||
|
||||
namespace gin_helper {
|
||||
template <typename T>
|
||||
class Handle;
|
||||
} // namespace gin_helper
|
||||
|
||||
namespace base {
|
||||
class Process;
|
||||
} // namespace base
|
||||
|
|
@ -44,8 +39,7 @@ class Connector;
|
|||
namespace electron::api {
|
||||
|
||||
class UtilityProcessWrapper final
|
||||
: public gin_helper::DeprecatedWrappable<UtilityProcessWrapper>,
|
||||
public gin_helper::Pinnable<UtilityProcessWrapper>,
|
||||
: public gin::Wrappable<UtilityProcessWrapper>,
|
||||
public gin_helper::EventEmitterMixin<UtilityProcessWrapper>,
|
||||
private mojo::MessageReceiver,
|
||||
public node::mojom::NodeServiceClient,
|
||||
|
|
@ -54,19 +48,6 @@ class UtilityProcessWrapper final
|
|||
enum class IOHandle : size_t { STDIN = 0, STDOUT = 1, STDERR = 2 };
|
||||
enum class IOType { IO_PIPE, IO_INHERIT, IO_IGNORE };
|
||||
|
||||
~UtilityProcessWrapper() override;
|
||||
static gin_helper::Handle<UtilityProcessWrapper> Create(gin::Arguments* args);
|
||||
static raw_ptr<UtilityProcessWrapper> FromProcessId(base::ProcessId pid);
|
||||
|
||||
void Shutdown(uint32_t exit_code);
|
||||
|
||||
// gin_helper::Wrappable
|
||||
static gin::DeprecatedWrapperInfo kWrapperInfo;
|
||||
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) override;
|
||||
const char* GetTypeName() override;
|
||||
|
||||
private:
|
||||
UtilityProcessWrapper(node::mojom::NodeServiceParamsPtr params,
|
||||
std::u16string display_name,
|
||||
std::map<IOHandle, IOType> stdio,
|
||||
|
|
@ -75,6 +56,25 @@ class UtilityProcessWrapper final
|
|||
bool use_plugin_helper,
|
||||
bool create_network_observer,
|
||||
bool disclaim_responsibility);
|
||||
~UtilityProcessWrapper() override;
|
||||
|
||||
static UtilityProcessWrapper* Create(gin::Arguments* args);
|
||||
static UtilityProcessWrapper* FromProcessId(base::ProcessId pid);
|
||||
|
||||
void Shutdown(uint32_t exit_code);
|
||||
|
||||
// gin::Wrappable
|
||||
static const gin::WrapperInfo kWrapperInfo;
|
||||
static const char* GetClassName() { return "UtilityProcess"; }
|
||||
void Trace(cppgc::Visitor*) const override;
|
||||
const gin::WrapperInfo* wrapper_info() const override;
|
||||
const char* GetHumanReadableName() const override;
|
||||
|
||||
protected:
|
||||
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
|
||||
v8::Isolate* isolate) override;
|
||||
|
||||
private:
|
||||
void OnServiceProcessLaunch(const base::Process& process);
|
||||
void CloseConnectorPort();
|
||||
|
||||
|
|
@ -120,12 +120,17 @@ class UtilityProcessWrapper final
|
|||
bool create_network_observer_ = false;
|
||||
std::unique_ptr<mojo::Connector> connector_;
|
||||
blink::MessagePortDescriptor host_port_;
|
||||
GC_PLUGIN_IGNORE(
|
||||
"Context tracking of receiver is not needed in the browser process.")
|
||||
mojo::Receiver<node::mojom::NodeServiceClient> receiver_{this};
|
||||
GC_PLUGIN_IGNORE(
|
||||
"Context tracking of remote is not needed in the browser process.")
|
||||
mojo::Remote<node::mojom::NodeService> node_service_remote_;
|
||||
std::optional<electron::URLLoaderNetworkObserver>
|
||||
url_loader_network_observer_;
|
||||
base::CallbackListSubscription network_service_gone_subscription_;
|
||||
base::WeakPtrFactory<UtilityProcessWrapper> weak_factory_{this};
|
||||
gin_helper::SelfKeepAlive<UtilityProcessWrapper> keep_alive_{this};
|
||||
gin::WeakCellFactory<UtilityProcessWrapper> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace electron::api
|
||||
|
|
|
|||
|
|
@ -603,7 +603,7 @@ void ElectronBrowserMainParts::PostMainMessageLoopRun() {
|
|||
auto& process = it.GetData().GetProcess();
|
||||
if (!process.IsValid())
|
||||
continue;
|
||||
auto utility_process_wrapper =
|
||||
auto* utility_process_wrapper =
|
||||
api::UtilityProcessWrapper::FromProcessId(process.Pid());
|
||||
if (utility_process_wrapper)
|
||||
utility_process_wrapper->Shutdown(0 /* exit_code */);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ enum ElectronWrappablePointerTag : uint16_t {
|
|||
kElectronServiceWorkerContext, // electron::api::ServiceWorkerContext
|
||||
kElectronSession, // electron::api::Session
|
||||
kElectronTray, // electron::api::Tray
|
||||
kElectronUtilityProcess, // electron::api::UtilityProcessWrapper
|
||||
kElectronWebRequest, // electron::api::WebRequest
|
||||
kLastElectronPointerTag = kElectronWebRequest,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -427,4 +427,142 @@ describe('cpp heap', () => {
|
|||
expect(result.noDuplicates).to.equal(true, 'should have exactly one PowerMonitor instance');
|
||||
});
|
||||
});
|
||||
|
||||
describe('utilityProcess module', () => {
|
||||
it('should appear in heap snapshot while process is running', async () => {
|
||||
const { remotely } = await startRemoteControlApp(['--expose-internals']);
|
||||
const result = await remotely(
|
||||
async (heap: string, snapshotHelper: string, fixturePath: string) => {
|
||||
const { utilityProcess } = require('electron');
|
||||
const { once } = require('node:events');
|
||||
const { recordState } = require(heap);
|
||||
const { containsRetainingPath } = require(snapshotHelper);
|
||||
|
||||
const child = utilityProcess.fork(fixturePath);
|
||||
await once(child, 'spawn');
|
||||
|
||||
const state = recordState();
|
||||
const found = containsRetainingPath(state.snapshot, ['C++ Persistent roots', 'Electron / UtilityProcess']);
|
||||
|
||||
child.kill();
|
||||
await once(child, 'exit');
|
||||
return found;
|
||||
},
|
||||
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
||||
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
|
||||
path.join(__dirname, 'fixtures/api/utility-process/endless.js')
|
||||
);
|
||||
expect(result).to.equal(true);
|
||||
});
|
||||
|
||||
it('should be released from heap snapshot after process exits', async () => {
|
||||
const { remotely } = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
|
||||
const result = await remotely(
|
||||
async (heap: string, snapshotHelper: string, fixturePath: string) => {
|
||||
const { utilityProcess } = require('electron');
|
||||
const { once } = require('node:events');
|
||||
const { recordState } = require(heap);
|
||||
const { containsRetainingPath } = require(snapshotHelper);
|
||||
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
||||
|
||||
let child: any = utilityProcess.fork(fixturePath);
|
||||
await once(child, 'exit');
|
||||
child = null;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
}
|
||||
|
||||
const state = recordState();
|
||||
const found = containsRetainingPath(state.snapshot, ['C++ Persistent roots', 'Electron / UtilityProcess']);
|
||||
return !found;
|
||||
},
|
||||
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
||||
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
|
||||
path.join(__dirname, 'fixtures/api/utility-process/empty.js')
|
||||
);
|
||||
expect(result).to.equal(true, 'UtilityProcess should be released after exit and GC');
|
||||
});
|
||||
|
||||
it('should survive GC when JS reference is dropped but process is still running', async () => {
|
||||
const rc = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
|
||||
const result = await rc.remotely(
|
||||
async (heap: string, snapshotHelper: string, fixturePath: string) => {
|
||||
const { utilityProcess, app } = require('electron');
|
||||
const { once } = require('node:events');
|
||||
const { recordState } = require(heap);
|
||||
const { containsRetainingPath } = require(snapshotHelper);
|
||||
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
||||
|
||||
let child: any = utilityProcess.fork(fixturePath);
|
||||
await once(child, 'spawn');
|
||||
child = null;
|
||||
|
||||
// Force GC — the process should still be alive because
|
||||
// SelfKeepAlive roots the C++ wrapper.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
}
|
||||
|
||||
const state = recordState();
|
||||
const stillAlive = containsRetainingPath(state.snapshot, [
|
||||
'C++ Persistent roots',
|
||||
'Electron / UtilityProcess'
|
||||
]);
|
||||
|
||||
setTimeout(() => app.quit());
|
||||
return stillAlive;
|
||||
},
|
||||
path.join(__dirname, '../../third_party/electron_node/test/common/heap'),
|
||||
path.join(__dirname, 'lib', 'heapsnapshot-helpers.js'),
|
||||
path.join(__dirname, 'fixtures/api/utility-process/endless.js')
|
||||
);
|
||||
|
||||
const [code] = await once(rc.process, 'exit');
|
||||
expect(code).to.equal(0);
|
||||
expect(result).to.equal(true, 'UtilityProcess should survive GC while process is running');
|
||||
});
|
||||
|
||||
it('should not leak when forking multiple processes', async () => {
|
||||
const { remotely } = await startRemoteControlApp(['--js-flags=--expose-gc']);
|
||||
const result = await remotely(
|
||||
async (fixturePath: string) => {
|
||||
const { utilityProcess } = require('electron');
|
||||
const { once } = require('node:events');
|
||||
const { getCppHeapStatistics } = require('node:v8');
|
||||
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
|
||||
|
||||
async function forkAndWait() {
|
||||
const child = utilityProcess.fork(fixturePath);
|
||||
await once(child, 'exit');
|
||||
}
|
||||
|
||||
async function measure(n: number) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
await forkAndWait();
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
v8Util.requestGarbageCollectionForTesting();
|
||||
}
|
||||
return getCppHeapStatistics('brief').used_size_bytes;
|
||||
}
|
||||
|
||||
await measure(5);
|
||||
const after1 = await measure(10);
|
||||
const after2 = await measure(10);
|
||||
return { after1, after2 };
|
||||
},
|
||||
path.join(__dirname, 'fixtures/api/utility-process/empty.js')
|
||||
);
|
||||
|
||||
const growth = result.after2 - result.after1;
|
||||
expect(growth).to.be.at.most(
|
||||
result.after1 * 0.1,
|
||||
`C++ heap grew by ${growth} bytes between rounds — likely a leak`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue