mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
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:
2
BUILD.gn
2
BUILD.gn
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
26
docs/api/structures/enable-heap-profiling-options.md
Normal file
26
docs/api/structures/enable-heap-profiling-options.md
Normal 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.
|
||||
BIN
docs/images/viewing-heap-dumps.png
Normal file
BIN
docs/images/viewing-heap-dumps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
10
spec/fixtures/api/content-tracing/index.html
vendored
Normal file
10
spec/fixtures/api/content-tracing/index.html
vendored
Normal 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>
|
||||
3
spec/fixtures/api/content-tracing/utility.js
vendored
Normal file
3
spec/fixtures/api/content-tracing/utility.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
setInterval(() => {
|
||||
new Array(1000000).fill(0);
|
||||
}, 100);
|
||||
17
spec/fixtures/apps/remote-control/main.js
vendored
17
spec/fixtures/apps/remote-control/main.js
vendored
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user