feat: support heap profiling in contentTracing (#51162)

* feat: support heap profiling in `contentTracing`

Co-authored-by: deepak1556 <hop2deep@gmail.com>

Co-authored-by: Niklas Wenzel <dev@nikwen.de>

* fix: heap profiling test flakes (#51224)

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Niklas Wenzel <dev@nikwen.de>
This commit is contained in:
trop[bot]
2026-04-29 11:29:42 -07:00
committed by GitHub
parent 6277ee0d38
commit f23788aeae
14 changed files with 511 additions and 3 deletions

View File

@@ -480,8 +480,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

@@ -91,6 +91,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

@@ -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"
@@ -343,6 +346,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());