diff --git a/BUILD.gn b/BUILD.gn index 413685f6f8..074dec4746 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -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", diff --git a/docs/api/content-tracing.md b/docs/api/content-tracing.md index 67cea6c724..1217f024bb 100644 --- a/docs/api/content-tracing.md +++ b/docs/api/content-tracing.md @@ -124,4 +124,65 @@ Returns `Promise` - 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_ + + + +* `options` ([EnableHeapProfilingOptions](structures/enable-heap-profiling-options.md)) (optional) + +Returns `Promise` - 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) + +Screenshot showing how to view a heapdump in Chromium's tracing view + [trace viewer]: https://chromium.googlesource.com/catapult/+/HEAD/tracing/README.md diff --git a/docs/api/structures/enable-heap-profiling-options.md b/docs/api/structures/enable-heap-profiling-options.md new file mode 100644 index 0000000000..fff29a3666 --- /dev/null +++ b/docs/api/structures/enable-heap-profiling-options.md @@ -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. diff --git a/docs/images/viewing-heap-dumps.png b/docs/images/viewing-heap-dumps.png new file mode 100644 index 0000000000..de4eae135f Binary files /dev/null and b/docs/images/viewing-heap-dumps.png differ diff --git a/filenames.auto.gni b/filenames.auto.gni index 87434e4539..0a0298d2c1 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -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", diff --git a/patches/chromium/.patches b/patches/chromium/.patches index 26a1c0249d..87a52d2dfa 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -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 diff --git a/patches/chromium/fix-dcheck-failure-when-starting-heap-profiler-for-renderer.patch b/patches/chromium/fix-dcheck-failure-when-starting-heap-profiler-for-renderer.patch new file mode 100644 index 0000000000..2181369c16 --- /dev/null +++ b/patches/chromium/fix-dcheck-failure-when-starting-heap-profiler-for-renderer.patch @@ -0,0 +1,50 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: JunHo Seo +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 +Reviewed-by: Joe Mason +Reviewed-by: Rohit Rao +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 diff --git a/shell/app/electron_content_client.cc b/shell/app/electron_content_client.cc index ee91e67058..5c9a3a6952 100644 --- a/shell/app/electron_content_client.cc +++ b/shell/app/electron_content_client.cc @@ -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 io_task_runner, + mojo::BinderMap* binders) { + // Sets up the client side of the multi-process heap profiler service. + binders->Add( + [](mojo::PendingReceiver + receiver) { + static base::NoDestructor + profiling_client; + profiling_client->BindToInterface(std::move(receiver)); + }, + io_task_runner); +} + } // namespace electron diff --git a/shell/app/electron_content_client.h b/shell/app/electron_content_client.h index 3c38e45ea5..b23bae06a8 100644 --- a/shell/app/electron_content_client.h +++ b/shell/app/electron_content_client.h @@ -36,6 +36,9 @@ class ElectronContentClient : public content::ContentClient { std::vector* cdm_host_file_paths) override; bool IsFilePickerAllowedForCrossOriginSubframe( const url::Origin& origin) override; + void ExposeInterfacesToBrowser( + scoped_refptr io_task_runner, + mojo::BinderMap* binders) override; }; } // namespace electron diff --git a/shell/app/electron_main_delegate.cc b/shell/app/electron_main_delegate.cc index 37bef80587..318fbe6982 100644 --- a/shell/app/electron_main_delegate.cc +++ b/shell/app/electron_main_delegate.cc @@ -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 ElectronMainDelegate::PreBrowserMain() { return std::nullopt; } +std::optional 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() && diff --git a/shell/app/electron_main_delegate.h b/shell/app/electron_main_delegate.h index 20edab7910..aad6047b03 100644 --- a/shell/app/electron_main_delegate.h +++ b/shell/app/electron_main_delegate.h @@ -9,6 +9,7 @@ #include #include +#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 PreBrowserMain() override; + std::optional 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 browser_client_; std::unique_ptr content_client_; @@ -70,6 +74,8 @@ class ElectronMainDelegate : public content::ContentMainDelegate { std::unique_ptr renderer_client_; std::unique_ptr utility_client_; std::unique_ptr tracing_sampler_profiler_; + + memory_system::MemorySystem memory_system_; }; } // namespace electron diff --git a/shell/browser/api/electron_api_content_tracing.cc b/shell/browser/api/electron_api_content_tracing.cc index fe5a7de291..fb2120dcf0 100644 --- a/shell/browser/api/electron_api_content_tracing.cc +++ b/shell/browser/api/electron_api_content_tracing.cc @@ -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 GetCategories(v8::Isolate* isolate) { return handle; } +#if !defined(ADDRESS_SANITIZER) + +std::tuple +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 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 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::ResolvedPromise(args->isolate()); +#else + gin_helper::Promise promise(args->isolate()); + v8::Local 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 controller_weak_ptr, + heap_profiling::Mode mode) { + return std::make_unique( + controller_weak_ptr, mode); + }); + + supervisor->Start(mode, stack_mode, sampling_rate, + base::BindOnce(gin_helper::Promise::ResolvePromise, + std::move(promise))); + + return handle; +#endif // defined(ADDRESS_SANITIZER) +} + v8::Local StartTracing( v8::Isolate* isolate, const base::trace_event::TraceConfig& trace_config) { @@ -206,6 +291,7 @@ void Initialize(v8::Local exports, dict.SetMethod("startRecording", &StartTracing); dict.SetMethod("stopRecording", &StopRecording); dict.SetMethod("getTraceBufferUsage", &GetTraceBufferUsage); + dict.SetMethod("enableHeapProfiling", &EnableHeapProfiling); } } // namespace diff --git a/spec/api-content-tracing-spec.ts b/spec/api-content-tracing-spec.ts index e404161422..a8357aa92b 100644 --- a/spec/api-content-tracing-spec.ts +++ b/spec/api-content-tracing-spec.ts @@ -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); diff --git a/spec/fixtures/api/content-tracing/index.html b/spec/fixtures/api/content-tracing/index.html new file mode 100644 index 0000000000..563d7faeab --- /dev/null +++ b/spec/fixtures/api/content-tracing/index.html @@ -0,0 +1,10 @@ + + + + + Hello World! + + +

Hello World!

+ + diff --git a/spec/fixtures/api/content-tracing/utility.js b/spec/fixtures/api/content-tracing/utility.js new file mode 100644 index 0000000000..0b91cc759b --- /dev/null +++ b/spec/fixtures/api/content-tracing/utility.js @@ -0,0 +1,3 @@ +setInterval(() => { + new Array(1000000).fill(0); +}, 100); diff --git a/spec/fixtures/apps/remote-control/main.js b/spec/fixtures/apps/remote-control/main.js index 8ea8d17def..bfb6e3b0a6 100644 --- a/spec/fixtures/apps/remote-control/main.js +++ b/spec/fixtures/apps/remote-control/main.js @@ -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());