feat: support heap profiling in contentTracing (#51178)

* feat: support heap profiling in `contentTracing`

* chore: backport crrev.com/c/7603976 to fix DCHECK failure

* fix: heap profiling test flakes (#51224)
This commit is contained in:
Niklas Wenzel
2026-04-29 20:29:46 +02:00
committed by GitHub
parent 86b483af5b
commit 7e0499d55d
16 changed files with 562 additions and 3 deletions

View File

@@ -458,8 +458,10 @@ source_set("electron_lib") {
"//components/certificate_transparency",
"//components/compose:buildflags",
"//components/embedder_support:user_agent",
"//components/heap_profiling/multi_process",
"//components/input",
"//components/language/core/browser",
"//components/memory_system",
"//components/net_log",
"//components/network_hints/browser",
"//components/network_hints/common:mojo_bindings",

View File

@@ -124,4 +124,65 @@ Returns `Promise<Object>` - Resolves with an object containing the `value` and `
Get the maximum usage across processes of trace buffer as a percentage of the
full state.
### `contentTracing.enableHeapProfiling([options])` _Experimental_
<!--
```YAML history
added:
- pr-url: https://github.com/electron/electron/pull/50826
```
-->
* `options` ([EnableHeapProfilingOptions](structures/enable-heap-profiling-options.md)) (optional)
Returns `Promise<void>` - Resolves once heap profiling has been enabled.
Enable [heap profiling](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/memory-infra/heap_profiler.md)
for MemoryInfra traces. Equivalent to the `--memlog` switch in Chrome.
Only takes effect if the `disabled-by-default-memory-infra` category is included.
Needs to be called before `contentTracing.startRecording()`.
Usage:
```js
const { contentTracing } = require('electron')
async function recordTrace () {
await contentTracing.enableHeapProfiling()
await contentTracing.startRecording({
included_categories: ['disabled-by-default-memory-infra'],
excluded_categories: ['*'],
memory_dump_config: {
triggers: [
{ mode: 'detailed', periodic_interval_ms: 1000 }
]
}
})
await new Promise(resolve => setTimeout(resolve, 5000))
const filePath = await contentTracing.stopRecording()
}
```
To view the recorded heap dumps:
1. Download the breakpad symbols for your Electron version from the Electron GitHub
[releases](https://github.com/electron/electron/releases)
2. Clone the [Electron source code](../development/build-instructions-gn.md)
3. In your Chromium checkout for Electron, run this command to symbolicate the heap dump:
```bash
python3 third_party/catapult/tracing/bin/symbolize_trace --use-breakpad-symbols --breakpad-symbols-directory /path/to/breakpad_symbols /path/to/trace.json
```
4. Open the symbolicated trace in `chrome://tracing` (the Perfetto UI does not support memory dumps
yet)
5. Click on one of the `M` symbols
6. Click on a `` triple bar icon (e.g., in the `malloc` column)
<img src="../images/viewing-heap-dumps.png" alt="Screenshot showing how to view a heapdump in Chromium's tracing view" />
[trace viewer]: https://chromium.googlesource.com/catapult/+/HEAD/tracing/README.md

View File

@@ -0,0 +1,26 @@
# EnableHeapProfilingOptions Object
* `mode` string (optional) - Controls which processes are profiled. Equivalent to `--memlog` in
Chrome. Default is `all`.
* `all` - Profile all processes.
* `browser` - Profile only the browser process.
* `gpu` - Profile only the GPU process.
* `minimal` - Profile only the browser and GPU processes.
* `renderer-sampling` - Profile at most 1 renderer process. Each renderer process has a fixed
probability of being profiled when the renderer process is started or, for existing processes,
when heap profiling is enabled.
* `all-renderers` - Profile all renderer processes.
* `utility-sampling` - Each utility process has a fixed probability of being profiled.
* `all-utilities` - Profile all utility processes.
* `utility-and-browser` - Profile all utility processes and the browser process.
* `samplingRate` number (optional) - Controls the sampling interval in bytes. The lower the
interval, the more precise the profile is. However it comes at the cost of performance. Default
is `100000` (100KB). That is enough to observe allocation sites that make allocations >500KB
total, where total equals to a single allocation size times the number of such allocations at the
same call site. Equivalent to `--memlog-sampling-rate` in Chrome. Must be an integer between
`1000` and `10000000`.
* `stackMode` string (optional) - Controls the type of metadata recorded for each allocation.
Equivalent to `--memlog-stack-mode` in Chrome. Default is `native`.
* `native` - Instruction addresses from unwinding the stack.
* `native-with-thread-names` - Instruction addresses from unwinding the stack. Includes the thread
name as the first frame.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -90,6 +90,7 @@ auto_filenames = {
"docs/api/structures/custom-scheme.md",
"docs/api/structures/desktop-capturer-source.md",
"docs/api/structures/display.md",
"docs/api/structures/enable-heap-profiling-options.md",
"docs/api/structures/extension-info.md",
"docs/api/structures/extension.md",
"docs/api/structures/file-filter.md",

View File

@@ -172,3 +172,4 @@ cherry-pick-7687618.patch
patch_osr_control_screen_info.patch
cherry-pick-cve-2026-6920.patch
fix_make_macos_text_replacement_work_on_contenteditable.patch
fix-dcheck-failure-when-starting-heap-profiler-for-renderer.patch

View File

@@ -0,0 +1,50 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: JunHo Seo <junho0924.seo@lge.com>
Date: Wed, 25 Feb 2026 19:59:16 -0800
Subject: Fix DCHECK failure when starting heap profiler for renderer.
Backports https://crrev.com/c/7603976.
Heap profiling for the renderer is currently started in
OnRenderProcessHostCreated(). However, at that point, base::Process::Pid
may not yet be valid, which can trigger a DCHECK(IsValid()) failure.
In addition, even if the DCHECK does not fire, the PID might still be
zero, causing renderer profiling data to be aggregated incorrectly.
To address this issue, this CL moves the heap profiler startup to
OnRenderProcessLaunched().
Bug: N/A
Change-Id: If1ba076dcf59d84b875a0b09544df9fde0dee83a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7603976
Commit-Queue: JunHo Seo <junho0924.seo@lge.com>
Reviewed-by: Joe Mason <joenotcharles@google.com>
Reviewed-by: Rohit Rao <rohitrao@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1590612}
diff --git a/components/heap_profiling/multi_process/client_connection_manager.cc b/components/heap_profiling/multi_process/client_connection_manager.cc
index 6a4c109d8f31a4658e60f49d15c4a62c9d272775..93c3b4da047e05ca622a4d759effeb0709c619a9 100644
--- a/components/heap_profiling/multi_process/client_connection_manager.cc
+++ b/components/heap_profiling/multi_process/client_connection_manager.cc
@@ -260,7 +260,7 @@ void ClientConnectionManager::StartProfilingNonRendererChild(
std::move(started_profiling_closure)));
}
-void ClientConnectionManager::OnRenderProcessHostCreated(
+void ClientConnectionManager::OnRenderProcessLaunched(
content::RenderProcessHost* host) {
if (ShouldProfileNewRenderer(host)) {
StartProfilingRenderer(host, base::DoNothing());
diff --git a/components/heap_profiling/multi_process/client_connection_manager.h b/components/heap_profiling/multi_process/client_connection_manager.h
index 80f34e3dbbcbd09645bb1c2b35b91cdaa74226ba..d298bbdc75ba5a3c857e4c04a6b65920f9161cea 100644
--- a/components/heap_profiling/multi_process/client_connection_manager.h
+++ b/components/heap_profiling/multi_process/client_connection_manager.h
@@ -100,7 +100,7 @@ class ClientConnectionManager
started_profiling_closure);
// content::RenderProcessHostCreationObserver
- void OnRenderProcessHostCreated(content::RenderProcessHost* host) override;
+ void OnRenderProcessLaunched(content::RenderProcessHost* host) override;
// RenderProcessHostObserver:
// RenderProcessHostDestroyed() corresponds to death of an underlying

View File

@@ -11,11 +11,14 @@
#include "base/command_line.h"
#include "base/containers/extend.h"
#include "base/files/file_util.h"
#include "base/no_destructor.h"
#include "base/strings/string_split.h"
#include "components/services/heap_profiling/public/cpp/profiling_client.h"
#include "content/public/common/buildflags.h"
#include "electron/buildflags/buildflags.h"
#include "electron/fuses.h"
#include "extensions/common/constants.h"
#include "mojo/public/cpp/bindings/binder_map.h"
#include "pdf/buildflags.h"
#include "shell/common/options_switches.h"
#include "shell/common/process_util.h"
@@ -227,4 +230,18 @@ bool ElectronContentClient::IsFilePickerAllowedForCrossOriginSubframe(
#endif
}
void ElectronContentClient::ExposeInterfacesToBrowser(
scoped_refptr<base::SequencedTaskRunner> io_task_runner,
mojo::BinderMap* binders) {
// Sets up the client side of the multi-process heap profiler service.
binders->Add<heap_profiling::mojom::ProfilingClient>(
[](mojo::PendingReceiver<heap_profiling::mojom::ProfilingClient>
receiver) {
static base::NoDestructor<heap_profiling::ProfilingClient>
profiling_client;
profiling_client->BindToInterface(std::move(receiver));
},
io_task_runner);
}
} // namespace electron

View File

@@ -36,6 +36,9 @@ class ElectronContentClient : public content::ContentClient {
std::vector<media::CdmHostFilePath>* cdm_host_file_paths) override;
bool IsFilePickerAllowedForCrossOriginSubframe(
const url::Origin& origin) override;
void ExposeInterfacesToBrowser(
scoped_refptr<base::SequencedTaskRunner> io_task_runner,
mojo::BinderMap* binders) override;
};
} // namespace electron

View File

@@ -25,7 +25,10 @@
#include "base/strings/string_util_internal.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/profiler/process_type.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/memory_system/initializer.h"
#include "components/memory_system/parameters.h"
#include "content/public/app/initialize_mojo_core.h"
#include "content/public/common/content_switches.h"
#include "crypto/hash.h"
@@ -359,6 +362,31 @@ std::optional<int> ElectronMainDelegate::PreBrowserMain() {
return std::nullopt;
}
std::optional<int> ElectronMainDelegate::PostEarlyInitialization(
InvokedIn invoked_in) {
// Start memory observation as early as possible so it can start recording
// memory allocations.
InitializeMemorySystem();
return std::nullopt;
}
void ElectronMainDelegate::InitializeMemorySystem() {
const base::CommandLine* const command_line =
base::CommandLine::ForCurrentProcess();
const std::string process_type =
command_line->GetSwitchValueASCII(::switches::kProcessType);
// PoissonAllocationSampler is necessary for heap profiling.
memory_system::Initializer()
.SetDispatcherParameters(memory_system::DispatcherParameters::
PoissonAllocationSamplerInclusion::kEnforce,
memory_system::DispatcherParameters::
AllocationTraceRecorderInclusion::kIgnore,
process_type)
.Initialize(memory_system_);
}
std::string_view ElectronMainDelegate::GetBrowserV8SnapshotFilename() {
bool load_browser_process_specific_v8_snapshot =
IsBrowserProcess() &&

View File

@@ -9,6 +9,7 @@
#include <string>
#include <string_view>
#include "components/memory_system/memory_system.h"
#include "content/public/app/content_main_delegate.h"
namespace content {
@@ -47,6 +48,7 @@ class ElectronMainDelegate : public content::ContentMainDelegate {
void PreSandboxStartup() override;
void SandboxInitialized(const std::string& process_type) override;
std::optional<int> PreBrowserMain() override;
std::optional<int> PostEarlyInitialization(InvokedIn invoked_in) override;
content::ContentClient* CreateContentClient() override;
content::ContentBrowserClient* CreateContentBrowserClient() override;
content::ContentGpuClient* CreateContentGpuClient() override;
@@ -63,6 +65,8 @@ class ElectronMainDelegate : public content::ContentMainDelegate {
void ZygoteForked() override;
#endif
void InitializeMemorySystem();
private:
std::unique_ptr<content::ContentBrowserClient> browser_client_;
std::unique_ptr<content::ContentClient> content_client_;
@@ -70,6 +74,8 @@ class ElectronMainDelegate : public content::ContentMainDelegate {
std::unique_ptr<content::ContentRendererClient> renderer_client_;
std::unique_ptr<content::ContentUtilityClient> utility_client_;
std::unique_ptr<tracing::TracingSamplerProfiler> tracing_sampler_profiler_;
memory_system::MemorySystem memory_system_;
};
} // namespace electron

View File

@@ -12,6 +12,11 @@
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/trace_event/trace_config.h"
#if !defined(ADDRESS_SANITIZER)
#include "components/heap_profiling/multi_process/client_connection_manager.h"
#include "components/heap_profiling/multi_process/supervisor.h"
#include "components/services/heap_profiling/public/cpp/settings.h"
#endif // !defined(ADDRESS_SANITIZER)
#include "content/public/browser/tracing_controller.h"
#include "shell/browser/browser.h"
#include "shell/browser/javascript_environment.h"
@@ -141,6 +146,86 @@ v8::Local<v8::Promise> GetCategories(v8::Isolate* isolate) {
return handle;
}
#if !defined(ADDRESS_SANITIZER)
std::tuple<heap_profiling::Mode, heap_profiling::mojom::StackMode, uint32_t>
GetHeapProfilingOptions(gin::Arguments* const args) {
heap_profiling::Mode mode = heap_profiling::Mode::kAll;
heap_profiling::mojom::StackMode stack_mode =
heap_profiling::mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES;
uint32_t sampling_rate = 100000;
gin_helper::Dictionary options;
if (args->GetNext(&options)) {
std::string mode_in;
std::string stack_mode_in;
std::optional<uint32_t> sampling_rate_in;
if (options.Get("mode", &mode_in)) {
heap_profiling::Mode converted =
heap_profiling::ConvertStringToMode(mode_in);
if (converted != heap_profiling::Mode::kNone &&
converted != heap_profiling::Mode::kManual) {
mode = converted;
}
}
if (options.Get("stackMode", &stack_mode_in)) {
stack_mode = heap_profiling::ConvertStringToStackMode(stack_mode_in);
}
if (options.GetOptional("samplingRate", &sampling_rate_in) &&
sampling_rate_in && sampling_rate_in.value() >= 1000 &&
sampling_rate_in.value() <= 10000000) {
sampling_rate = sampling_rate_in.value();
}
}
return {mode, stack_mode, sampling_rate};
}
bool g_heap_profiling_started = false;
#endif // !defined(ADDRESS_SANITIZER)
v8::Local<v8::Promise> EnableHeapProfiling(gin::Arguments* const args) {
#if defined(ADDRESS_SANITIZER)
// Memory sanitizers are using large memory shadow to keep track of memory
// state. Using memlog and memory sanitizers at the same time is slowing down
// user experience, causing the browser to be barely responsive. In theory,
// memlog and memory sanitizers are compatible and can run at the same time.
return gin_helper::Promise<void>::ResolvedPromise(args->isolate());
#else
gin_helper::Promise<void> promise(args->isolate());
v8::Local<v8::Promise> handle = promise.GetHandle();
auto* supervisor = heap_profiling::Supervisor::GetInstance();
if (supervisor->HasStarted() || g_heap_profiling_started) {
promise.RejectWithErrorMessage("Heap profiling is already enabled");
return handle;
}
// HasStarted() becomes true asynchronously. We keep track of whether we have
// called Start() already to avoid calling Start() twice.
g_heap_profiling_started = true;
auto [mode, stack_mode, sampling_rate] = GetHeapProfilingOptions(args);
supervisor->SetClientConnectionManagerConstructor(
[](base::WeakPtr<heap_profiling::Controller> controller_weak_ptr,
heap_profiling::Mode mode) {
return std::make_unique<heap_profiling::ClientConnectionManager>(
controller_weak_ptr, mode);
});
supervisor->Start(mode, stack_mode, sampling_rate,
base::BindOnce(gin_helper::Promise<void>::ResolvePromise,
std::move(promise)));
return handle;
#endif // defined(ADDRESS_SANITIZER)
}
v8::Local<v8::Promise> StartTracing(
v8::Isolate* isolate,
const base::trace_event::TraceConfig& trace_config) {
@@ -206,6 +291,7 @@ void Initialize(v8::Local<v8::Object> exports,
dict.SetMethod("startRecording", &StartTracing);
dict.SetMethod("stopRecording", &StopRecording);
dict.SetMethod("getTraceBufferUsage", &GetTraceBufferUsage);
dict.SetMethod("enableHeapProfiling", &EnableHeapProfiling);
}
} // namespace

View File

@@ -1,12 +1,16 @@
import { app, contentTracing, TraceConfig, TraceCategoriesAndOptions } from 'electron/main';
import { app, contentTracing, EnableHeapProfilingOptions, TraceConfig, TraceCategoriesAndOptions } from 'electron/main';
import { expect } from 'chai';
import { once } from 'node:events';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { ifdescribe } from './lib/spec-helpers';
import { ifdescribe, ifit, startRemoteControlApp } from './lib/spec-helpers';
const isCI = !!process.env.CI;
const fixturesPath = path.resolve(__dirname, 'fixtures');
// FIXME: The tests are skipped on linux arm/arm64
ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== 'linux'))('contentTracing', () => {
@@ -162,6 +166,252 @@ ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== '
});
});
describe('enableHeapProfiling', function () {
const enableHeapProfilingTestTimeout = 120000;
this.timeout(enableHeapProfilingTestTimeout);
const checkForHeapDumps = async (options?: EnableHeapProfilingOptions | false) => {
const rc = await startRemoteControlApp([`--remote-app-timeout=${enableHeapProfilingTestTimeout}`]);
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } = await rc.remotely(
async (
htmlPath: string,
utilityProcessPath: string,
options: EnableHeapProfilingOptions | false | undefined,
isCI: boolean
) => {
const { contentTracing, BrowserWindow, utilityProcess } = require('electron');
const { once } = require('node:events');
const fs = require('node:fs');
const process = require('node:process');
const { setTimeout } = require('node:timers/promises');
const isEventWithNonEmptyHeapDumpForProcess = (event: any, pid: number) =>
event.cat === 'disabled-by-default-memory-infra' &&
event.name === 'periodic_interval' &&
event.pid === pid &&
event.args.dumps.level_of_detail === 'detailed' &&
event.args.dumps.process_mmaps?.vm_regions.length > 0 &&
typeof event.args.dumps.allocators === 'object' &&
typeof event.args.dumps.heaps_v2.allocators === 'object' &&
Object.values(event.args.dumps.allocators).some((allocator: any) => allocator.attrs.size?.value !== '0') &&
Object.values(event.args.dumps.heaps_v2.allocators).some(
(allocator: any) =>
allocator.counts.length > 0 && allocator.nodes.length > 0 && allocator.sizes.length > 0
);
const hasNonEmptyHeapDumpForProcess = (parsedTrace: any, pid: number) =>
parsedTrace.traceEvents.some((event: any) => isEventWithNonEmptyHeapDumpForProcess(event, pid));
if (options !== false) await contentTracing.enableHeapProfiling(options);
await contentTracing.startRecording({
included_categories: ['disabled-by-default-memory-infra'],
excluded_categories: ['*'],
memory_dump_config: {
triggers: [{ mode: 'detailed', periodic_interval_ms: 1000 }]
}
});
// Launch a renderer process
const window = new BrowserWindow({ show: false });
await window.webContents.loadFile(htmlPath);
// Launch a utility process
const utility = utilityProcess.fork(utilityProcessPath);
await once(utility, 'spawn');
// Collect heap dumps
// - We wait for a long time because sometimes processes take a few seconds to start sending heap dumps.
// - CI machines are slower, so we wait longer there than when running locally.
await setTimeout(isCI ? 10000 : 4000);
const path = await contentTracing.stopRecording();
const data = fs.readFileSync(path, 'utf8');
const parsed = JSON.parse(data);
const hasBrowserProcessHeapDump = hasNonEmptyHeapDumpForProcess(parsed, process.pid);
const hasRendererProcessHeapDump = hasNonEmptyHeapDumpForProcess(parsed, window.webContents.getOSProcessId());
const hasUtilityProcessHeapDump = hasNonEmptyHeapDumpForProcess(parsed, utility.pid);
global.setTimeout(() => require('electron').app.quit());
return {
hasBrowserProcessHeapDump,
hasRendererProcessHeapDump,
hasUtilityProcessHeapDump
};
},
path.join(fixturesPath, 'api', 'content-tracing', 'index.html'),
path.join(fixturesPath, 'api', 'content-tracing', 'utility.js'),
options,
isCI
);
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
return {
hasBrowserProcessHeapDump,
hasRendererProcessHeapDump,
hasUtilityProcessHeapDump
};
};
it('does not include heap dumps when enableHeapProfiling is not called', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps(false);
expect(hasBrowserProcessHeapDump).to.be.false();
expect(hasRendererProcessHeapDump).to.be.false();
expect(hasUtilityProcessHeapDump).to.be.false();
});
ifit(!process.env.IS_ASAN)(
'includes heap dumps for browser process when called with { mode: "browser" }',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({ mode: 'browser' });
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.false();
expect(hasUtilityProcessHeapDump).to.be.false();
}
);
ifit(!process.env.IS_ASAN)(
'includes heap dumps for renderer processes when called with { mode: "all-renderers" }',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({ mode: 'all-renderers' });
expect(hasBrowserProcessHeapDump).to.be.false();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.false();
}
);
ifit(!process.env.IS_ASAN)(
'includes heap dumps for utility processes when called with { mode: "all-utilities" }',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({ mode: 'all-utilities' });
expect(hasBrowserProcessHeapDump).to.be.false();
expect(hasRendererProcessHeapDump).to.be.false();
expect(hasUtilityProcessHeapDump).to.be.true();
}
);
ifit(!process.env.IS_ASAN)(
'includes heap dumps for browser, renderer, and utility processes when called with { mode: "all" }',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({ mode: 'all' });
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
}
);
ifit(!process.env.IS_ASAN)(
'includes heap dumps for browser, renderer, and utility processes when called without options',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps();
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
}
);
ifit(!process.env.IS_ASAN)('accepts valid options', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({
mode: 'all',
stackMode: 'native-with-thread-names',
samplingRate: 50000
});
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
});
ifit(!process.env.IS_ASAN)('does not crash when invalid options are passed', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({
// @ts-expect-error Invalid mode
mode: 'invalid',
// @ts-expect-error Invalid stack mode
stackMode: 'invalid',
samplingRate: -1000
});
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
});
ifit(!process.env.IS_ASAN)('does not crash when options of invalid types are passed', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({
// @ts-expect-error Invalid mode
mode: { invalid: true },
// @ts-expect-error Invalid stack mode
stackMode: 999,
// @ts-expect-error Invalid sampling rate
samplingRate: 'invalid'
});
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
});
ifit(!!process.env.IS_ASAN)('does not include heap dumps in ASAN builds', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps();
expect(hasBrowserProcessHeapDump).to.be.false();
expect(hasRendererProcessHeapDump).to.be.false();
expect(hasUtilityProcessHeapDump).to.be.false();
});
ifit(!process.env.IS_ASAN)('rejects when called multiple times', async function () {
const rc = await startRemoteControlApp();
const [firstResult, secondResult, thirdResult] = await rc.remotely(async () => {
const { contentTracing } = require('electron');
// Call twice before enabling finishes.
const firstPromise = contentTracing.enableHeapProfiling();
const secondPromise = contentTracing.enableHeapProfiling();
const [firstResult, secondResult] = await Promise.allSettled([firstPromise, secondPromise]);
// Call again after enabling finishes.
const thirdPromise = contentTracing.enableHeapProfiling();
const [thirdResult] = await Promise.allSettled([thirdPromise]);
global.setTimeout(() => require('electron').app.quit());
return [firstResult, secondResult, thirdResult];
});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
expect(firstResult.status).to.equal('fulfilled');
expect(secondResult.status).to.equal('rejected');
expect(secondResult.reason.message).to.equal('Heap profiling is already enabled');
expect(thirdResult.status).to.equal('rejected');
expect(thirdResult.reason.message).to.equal('Heap profiling is already enabled');
});
});
describe('captured events', () => {
it('include V8 samples from the main process', async function () {
this.timeout(60000);

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>

View File

@@ -0,0 +1,3 @@
setInterval(() => {
new Array(1000000).fill(0);
}, 100);

View File

@@ -8,6 +8,21 @@ const http = require('node:http');
const promises_1 = require('node:timers/promises');
const v8 = require('node:v8');
function getAutoQuitTimeout () {
const argPrefix = '--remote-app-timeout=';
const arg = process.argv.find((arg) => arg.startsWith(argPrefix));
if (arg) {
const timeout = parseInt(arg.slice(argPrefix.length), 10);
if (Number.isSafeInteger(timeout) && timeout > 0) {
return timeout;
}
}
return 30000;
}
if (app.commandLine.hasSwitch('boot-eval')) {
// eslint-disable-next-line no-eval
eval(app.commandLine.getSwitchValue('boot-eval'));
@@ -35,4 +50,4 @@ app.whenReady().then(() => {
setTimeout(() => {
process.exit(0);
}, 30000);
}, getAutoQuitTimeout());