From 69294aea3722f8689566b468041af3d74df99a72 Mon Sep 17 00:00:00 2001 From: Milan Burda Date: Tue, 4 Sep 2018 23:46:10 +0200 Subject: [PATCH] feat: add process.takeHeapSnapshot() / webContents.takeHeapSnapshot() --- atom/browser/api/atom_api_web_contents.cc | 20 ++++++++ atom/browser/api/atom_api_web_contents.h | 3 ++ atom/common/api/api_messages.h | 5 ++ atom/common/api/atom_bindings.cc | 15 ++++++ atom/common/api/atom_bindings.h | 3 ++ atom/common/heap_snapshot.cc | 56 +++++++++++++++++++++ atom/common/heap_snapshot.h | 17 +++++++ atom/renderer/atom_render_frame_observer.cc | 21 +++++++- atom/renderer/atom_render_frame_observer.h | 3 ++ docs/api/process.md | 8 +++ docs/api/web-contents.md | 8 +++ filenames.gni | 2 + lib/browser/api/web-contents.js | 16 ++++++ package-lock.json | 6 +-- package.json | 2 +- spec/api-process-spec.js | 32 ++++++++++++ spec/api-web-contents-spec.js | 50 ++++++++++++++++++ 17 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 atom/common/heap_snapshot.cc create mode 100644 atom/common/heap_snapshot.h diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index d9cdc980d6156..fb1b3cb730d9f 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -50,6 +50,7 @@ #include "atom/common/options_switches.h" #include "base/message_loop/message_loop.h" #include "base/strings/utf_string_conversions.h" +#include "base/threading/thread_restrictions.h" #include "base/threading/thread_task_runner_handle.h" #include "base/values.h" #include "brightray/browser/inspectable_web_contents.h" @@ -1985,6 +1986,24 @@ void WebContents::GrantOriginAccess(const GURL& url) { url::Origin::Create(url)); } +bool WebContents::TakeHeapSnapshot(const base::FilePath& file_path, + const std::string& channel) { + base::ThreadRestrictions::ScopedAllowIO allow_io; + + base::File file(file_path, + base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); + if (!file.IsValid()) + return false; + + auto* frame_host = web_contents()->GetMainFrame(); + if (!frame_host) + return false; + + return frame_host->Send(new AtomFrameMsg_TakeHeapSnapshot( + frame_host->GetRoutingID(), + IPC::TakePlatformFileForTransit(std::move(file)), channel)); +} + // static void WebContents::BuildPrototype(v8::Isolate* isolate, v8::Local prototype) { @@ -2081,6 +2100,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate, .SetMethod("getWebRTCIPHandlingPolicy", &WebContents::GetWebRTCIPHandlingPolicy) .SetMethod("_grantOriginAccess", &WebContents::GrantOriginAccess) + .SetMethod("_takeHeapSnapshot", &WebContents::TakeHeapSnapshot) .SetProperty("id", &WebContents::ID) .SetProperty("session", &WebContents::Session) .SetProperty("hostWebContents", &WebContents::HostWebContents) diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index 294a82e761934..8c6d225e300e4 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -250,6 +250,9 @@ class WebContents : public mate::TrackableObject, // the specified URL. void GrantOriginAccess(const GURL& url); + bool TakeHeapSnapshot(const base::FilePath& file_path, + const std::string& channel); + // Properties. int32_t ID() const; v8::Local Session(v8::Isolate* isolate); diff --git a/atom/common/api/api_messages.h b/atom/common/api/api_messages.h index e7317736e0766..8062e1d7e9c37 100644 --- a/atom/common/api/api_messages.h +++ b/atom/common/api/api_messages.h @@ -10,6 +10,7 @@ #include "content/public/common/common_param_traits.h" #include "content/public/common/referrer.h" #include "ipc/ipc_message_macros.h" +#include "ipc/ipc_platform_file.h" #include "ui/gfx/geometry/rect_f.h" #include "ui/gfx/ipc/gfx_param_traits.h" #include "url/gurl.h" @@ -76,3 +77,7 @@ IPC_SYNC_MESSAGE_ROUTED0_1(AtomFrameHostMsg_GetZoomLevel, double /* result */) IPC_MESSAGE_ROUTED2(AtomFrameHostMsg_PDFSaveURLAs, GURL /* url */, content::Referrer /* referrer */) + +IPC_MESSAGE_ROUTED2(AtomFrameMsg_TakeHeapSnapshot, + IPC::PlatformFileForTransit /* file_handle */, + std::string /* channel */) diff --git a/atom/common/api/atom_bindings.cc b/atom/common/api/atom_bindings.cc index 32f05baabbb90..44107c197012e 100644 --- a/atom/common/api/atom_bindings.cc +++ b/atom/common/api/atom_bindings.cc @@ -11,12 +11,15 @@ #include "atom/common/api/locker.h" #include "atom/common/atom_version.h" #include "atom/common/chrome_version.h" +#include "atom/common/heap_snapshot.h" +#include "atom/common/native_mate_converters/file_path_converter.h" #include "atom/common/native_mate_converters/string16_converter.h" #include "atom/common/node_includes.h" #include "base/logging.h" #include "base/process/process_info.h" #include "base/process/process_metrics_iocounters.h" #include "base/sys_info.h" +#include "base/threading/thread_restrictions.h" #include "native_mate/dictionary.h" namespace atom { @@ -60,6 +63,7 @@ void AtomBindings::BindTo(v8::Isolate* isolate, v8::Local process) { dict.SetMethod("getCPUUsage", base::Bind(&AtomBindings::GetCPUUsage, base::Unretained(metrics_.get()))); dict.SetMethod("getIOCounters", &GetIOCounters); + dict.SetMethod("takeHeapSnapshot", &TakeHeapSnapshot); #if defined(OS_POSIX) dict.SetMethod("setFdLimit", &base::SetFdLimit); #endif @@ -238,4 +242,15 @@ v8::Local AtomBindings::GetIOCounters(v8::Isolate* isolate) { return dict.GetHandle(); } +// static +bool AtomBindings::TakeHeapSnapshot(v8::Isolate* isolate, + const base::FilePath& file_path) { + base::ThreadRestrictions::ScopedAllowIO allow_io; + + base::File file(file_path, + base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); + + return atom::TakeHeapSnapshot(isolate, &file); +} + } // namespace atom diff --git a/atom/common/api/atom_bindings.h b/atom/common/api/atom_bindings.h index 9ae736fe160b6..fd08c2dc1b6f9 100644 --- a/atom/common/api/atom_bindings.h +++ b/atom/common/api/atom_bindings.h @@ -8,6 +8,7 @@ #include #include +#include "base/files/file_path.h" #include "base/macros.h" #include "base/process/process_metrics.h" #include "base/strings/string16.h" @@ -43,6 +44,8 @@ class AtomBindings { static v8::Local GetCPUUsage(base::ProcessMetrics* metrics, v8::Isolate* isolate); static v8::Local GetIOCounters(v8::Isolate* isolate); + static bool TakeHeapSnapshot(v8::Isolate* isolate, + const base::FilePath& file_path); private: void ActivateUVLoop(v8::Isolate* isolate); diff --git a/atom/common/heap_snapshot.cc b/atom/common/heap_snapshot.cc new file mode 100644 index 0000000000000..bb36e98f598c7 --- /dev/null +++ b/atom/common/heap_snapshot.cc @@ -0,0 +1,56 @@ +// Copyright (c) 2018 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/common/heap_snapshot.h" + +#include "v8/include/v8-profiler.h" + +namespace { + +class HeapSnapshotOutputStream : public v8::OutputStream { + public: + explicit HeapSnapshotOutputStream(base::File* file) : file_(file) { + DCHECK(file_); + } + + bool IsComplete() const { return is_complete_; } + + // v8::OutputStream + int GetChunkSize() override { return 65536; } + void EndOfStream() override { is_complete_ = true; } + + v8::OutputStream::WriteResult WriteAsciiChunk(char* data, int size) override { + auto bytes_written = file_->WriteAtCurrentPos(data, size); + return bytes_written == size ? kContinue : kAbort; + } + + private: + base::File* file_ = nullptr; + bool is_complete_ = false; +}; + +} // namespace + +namespace atom { + +bool TakeHeapSnapshot(v8::Isolate* isolate, base::File* file) { + DCHECK(isolate); + DCHECK(file); + + if (!file->IsValid()) + return false; + + auto* snapshot = isolate->GetHeapProfiler()->TakeHeapSnapshot(); + if (!snapshot) + return false; + + HeapSnapshotOutputStream stream(file); + snapshot->Serialize(&stream, v8::HeapSnapshot::kJSON); + + const_cast(snapshot)->Delete(); + + return stream.IsComplete(); +} + +} // namespace atom diff --git a/atom/common/heap_snapshot.h b/atom/common/heap_snapshot.h new file mode 100644 index 0000000000000..8db7aeeb7c3f8 --- /dev/null +++ b/atom/common/heap_snapshot.h @@ -0,0 +1,17 @@ +// Copyright (c) 2018 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_COMMON_HEAP_SNAPSHOT_H_ +#define ATOM_COMMON_HEAP_SNAPSHOT_H_ + +#include "base/files/file.h" +#include "v8/include/v8.h" + +namespace atom { + +bool TakeHeapSnapshot(v8::Isolate* isolate, base::File* file); + +} // namespace atom + +#endif // ATOM_COMMON_HEAP_SNAPSHOT_H_ diff --git a/atom/renderer/atom_render_frame_observer.cc b/atom/renderer/atom_render_frame_observer.cc index 8a2162c1811ee..216ca49dcde27 100644 --- a/atom/renderer/atom_render_frame_observer.cc +++ b/atom/renderer/atom_render_frame_observer.cc @@ -9,9 +9,11 @@ #include "atom/common/api/api_messages.h" #include "atom/common/api/event_emitter_caller.h" +#include "atom/common/heap_snapshot.h" #include "atom/common/native_mate_converters/value_converter.h" #include "atom/common/node_includes.h" #include "base/strings/string_number_conversions.h" +#include "base/threading/thread_restrictions.h" #include "base/trace_event/trace_event.h" #include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_view.h" @@ -19,10 +21,10 @@ #include "native_mate/dictionary.h" #include "net/base/net_module.h" #include "net/grit/net_resources.h" +#include "third_party/blink/public/web/blink.h" #include "third_party/blink/public/web/web_document.h" #include "third_party/blink/public/web/web_draggable_region.h" #include "third_party/blink/public/web/web_element.h" -#include "third_party/blink/public/web/blink.h" #include "third_party/blink/public/web/web_local_frame.h" #include "third_party/blink/public/web/web_script_source.h" #include "ui/base/resource/resource_bundle.h" @@ -161,6 +163,7 @@ bool AtomRenderFrameObserver::OnMessageReceived(const IPC::Message& message) { bool handled = true; IPC_BEGIN_MESSAGE_MAP(AtomRenderFrameObserver, message) IPC_MESSAGE_HANDLER(AtomFrameMsg_Message, OnBrowserMessage) + IPC_MESSAGE_HANDLER(AtomFrameMsg_TakeHeapSnapshot, OnTakeHeapSnapshot) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() @@ -195,6 +198,22 @@ void AtomRenderFrameObserver::OnBrowserMessage(bool send_to_all, } } +void AtomRenderFrameObserver::OnTakeHeapSnapshot( + IPC::PlatformFileForTransit file_handle, + const std::string& channel) { + base::ThreadRestrictions::ScopedAllowIO allow_io; + + auto file = IPC::PlatformFileForTransitToFile(file_handle); + bool success = TakeHeapSnapshot(blink::MainThreadIsolate(), &file); + + base::ListValue args; + args.AppendString(channel); + args.AppendBoolean(success); + + render_frame_->Send(new AtomFrameHostMsg_Message( + render_frame_->GetRoutingID(), "ipc-message", args)); +} + void AtomRenderFrameObserver::EmitIPCEvent(blink::WebLocalFrame* frame, const std::string& channel, const base::ListValue& args, diff --git a/atom/renderer/atom_render_frame_observer.h b/atom/renderer/atom_render_frame_observer.h index d085c277b7500..7a1a102617dfa 100644 --- a/atom/renderer/atom_render_frame_observer.h +++ b/atom/renderer/atom_render_frame_observer.h @@ -10,6 +10,7 @@ #include "atom/renderer/renderer_client_base.h" #include "base/strings/string16.h" #include "content/public/renderer/render_frame_observer.h" +#include "ipc/ipc_platform_file.h" #include "third_party/blink/public/web/web_local_frame.h" namespace base { @@ -57,6 +58,8 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver { const std::string& channel, const base::ListValue& args, int32_t sender_id); + void OnTakeHeapSnapshot(IPC::PlatformFileForTransit file_handle, + const std::string& channel); content::RenderFrame* render_frame_; RendererClientBase* renderer_client_; diff --git a/docs/api/process.md b/docs/api/process.md index 691238a737a42..1f2a9fdc56d92 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -171,6 +171,14 @@ Returns `Object`: Returns an object giving memory usage statistics about the entire system. Note that all statistics are reported in Kilobytes. +### `process.takeHeapSnapshot(filePath)` + +* `filePath` String - Path to the output file. + +Returns `Boolean` - Indicates whether the snapshot has been created successfully. + +Takes a V8 heap snapshot and saves it to `filePath`. + ### `process.hang()` Causes the main thread of the current process hang. diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md index 9570ac904167b..f877613ff8a00 100644 --- a/docs/api/web-contents.md +++ b/docs/api/web-contents.md @@ -1497,6 +1497,14 @@ Returns `Integer` - The Chromium internal `pid` of the associated renderer. Can be compared to the `frameProcessId` passed by frame specific navigation events (e.g. `did-frame-navigate`) +#### `contents.takeHeapSnapshot(filePath)` + +* `filePath` String - Path to the output file. + +Returns `Promise` - Indicates whether the snapshot has been created successfully. + +Takes a V8 heap snapshot and saves it to `filePath`. + ### Instance Properties #### `contents.id` diff --git a/filenames.gni b/filenames.gni index 1b104ef6ad402..71f158c3d6295 100644 --- a/filenames.gni +++ b/filenames.gni @@ -486,6 +486,8 @@ filenames = { "atom/common/draggable_region.cc", "atom/common/draggable_region.h", "atom/common/google_api_key.h", + "atom/common/heap_snapshot.cc", + "atom/common/heap_snapshot.h", "atom/common/key_weak_map.h", "atom/common/keyboard_util.cc", "atom/common/keyboard_util.h", diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index f7a3394a57ef3..90e5fee91ddc6 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -160,6 +160,22 @@ WebContents.prototype.executeJavaScript = function (code, hasUserGesture, callba } } +WebContents.prototype.takeHeapSnapshot = function (filePath) { + return new Promise((resolve, reject) => { + const channel = `ELECTRON_TAKE_HEAP_SNAPSHOT_RESULT_${getNextId()}` + ipcMain.once(channel, (event, success) => { + if (success) { + resolve() + } else { + reject(new Error('takeHeapSnapshot failed')) + } + }) + if (!this._takeHeapSnapshot(filePath, channel)) { + ipcMain.emit(channel, false) + } + }) +} + // Translate the options of printToPDF. WebContents.prototype.printToPDF = function (options, callback) { const printingSetting = Object.assign({}, defaultPrintingSetting) diff --git a/package-lock.json b/package-lock.json index b19570198c304..c95be9f18de5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2841,9 +2841,9 @@ } }, "electron-typescript-definitions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/electron-typescript-definitions/-/electron-typescript-definitions-2.0.0.tgz", - "integrity": "sha512-uhbLoHoIWNafFqGEtdUMtkKimvxusU2GmdbgcXoT3CjD85B2vRyffbMxXYPpxx+o88z1xMP/lw2rQq2um/G6fw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/electron-typescript-definitions/-/electron-typescript-definitions-2.0.1.tgz", + "integrity": "sha512-H1DD4g+Usrddyb5VK94Ofxn2gQUSUfj8gHRYcZKbkIe5CTWQ+Gl/kc/qRQ+QL+oH/8B8MHM6UJoxNfbcCrzIgQ==", "dev": true, "requires": { "@types/node": "^7.0.18", diff --git a/package.json b/package.json index 4a166fe9859f9..1858d6e6164d4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dugite": "^1.45.0", "electabul": "~0.0.4", "electron-docs-linter": "^2.3.4", - "electron-typescript-definitions": "^2.0.0", + "electron-typescript-definitions": "^2.0.1", "eslint": "^5.6.0", "eslint-config-standard": "^12.0.0", "eslint-plugin-mocha": "^5.2.0", diff --git a/spec/api-process-spec.js b/spec/api-process-spec.js index 65f915ba5c543..42d84b115b421 100644 --- a/spec/api-process-spec.js +++ b/spec/api-process-spec.js @@ -1,3 +1,7 @@ +const { remote } = require('electron') +const fs = require('fs') +const path = require('path') + const { expect } = require('chai') describe('process module', () => { @@ -67,4 +71,32 @@ describe('process module', () => { expect(heapStats.doesZapGarbage).to.be.a('boolean') }) }) + + describe('process.takeHeapSnapshot()', () => { + it('returns true on success', () => { + const filePath = path.join(remote.app.getPath('temp'), 'test.heapsnapshot') + + const cleanup = () => { + try { + fs.unlinkSync(filePath) + } catch (e) { + // ignore error + } + } + + try { + const success = process.takeHeapSnapshot(filePath) + expect(success).to.be.true() + const stats = fs.statSync(filePath) + expect(stats.size).not.to.be.equal(0) + } finally { + cleanup() + } + }) + + it('returns false on failure', () => { + const success = process.takeHeapSnapshot('') + expect(success).to.be.false() + }) + }) }) diff --git a/spec/api-web-contents-spec.js b/spec/api-web-contents-spec.js index 2342bfaaa1423..cc397630fce33 100644 --- a/spec/api-web-contents-spec.js +++ b/spec/api-web-contents-spec.js @@ -1,6 +1,7 @@ 'use strict' const assert = require('assert') +const fs = require('fs') const http = require('http') const path = require('path') const { closeWindow } = require('./window-helpers') @@ -799,4 +800,53 @@ describe('webContents module', () => { w.loadURL('about:blank') }) }) + + describe('takeHeapSnapshot()', () => { + it('works with sandboxed renderers', async () => { + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true + } + }) + + w.loadURL('about:blank') + await emittedOnce(w.webContents, 'did-finish-load') + + const filePath = path.join(remote.app.getPath('temp'), 'test.heapsnapshot') + + const cleanup = () => { + try { + fs.unlinkSync(filePath) + } catch (e) { + // ignore error + } + } + + try { + await w.webContents.takeHeapSnapshot(filePath) + const stats = fs.statSync(filePath) + expect(stats.size).not.to.be.equal(0) + } finally { + cleanup() + } + }) + + it('fails with invalid file path', async () => { + w.destroy() + w = new BrowserWindow({ + show: false, + webPreferences: { + sandbox: true + } + }) + + w.loadURL('about:blank') + await emittedOnce(w.webContents, 'did-finish-load') + + const promise = w.webContents.takeHeapSnapshot('') + return expect(promise).to.be.eventually.rejectedWith(Error, 'takeHeapSnapshot failed') + }) + }) })