feat: add process.takeHeapSnapshot() / webContents.takeHeapSnapshot() (#14456)

This commit is contained in:
Milan Burda 2018-09-18 20:00:31 +02:00 committed by Shelley Vohr
parent 1855144d26
commit e22142ef9c
17 changed files with 262 additions and 5 deletions

View file

@ -50,6 +50,7 @@
#include "atom/common/options_switches.h" #include "atom/common/options_switches.h"
#include "base/message_loop/message_loop.h" #include "base/message_loop/message_loop.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h" #include "base/threading/thread_task_runner_handle.h"
#include "base/values.h" #include "base/values.h"
#include "brightray/browser/inspectable_web_contents.h" #include "brightray/browser/inspectable_web_contents.h"
@ -1985,6 +1986,24 @@ void WebContents::GrantOriginAccess(const GURL& url) {
url::Origin::Create(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 // static
void WebContents::BuildPrototype(v8::Isolate* isolate, void WebContents::BuildPrototype(v8::Isolate* isolate,
v8::Local<v8::FunctionTemplate> prototype) { v8::Local<v8::FunctionTemplate> prototype) {
@ -2081,6 +2100,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate,
.SetMethod("getWebRTCIPHandlingPolicy", .SetMethod("getWebRTCIPHandlingPolicy",
&WebContents::GetWebRTCIPHandlingPolicy) &WebContents::GetWebRTCIPHandlingPolicy)
.SetMethod("_grantOriginAccess", &WebContents::GrantOriginAccess) .SetMethod("_grantOriginAccess", &WebContents::GrantOriginAccess)
.SetMethod("_takeHeapSnapshot", &WebContents::TakeHeapSnapshot)
.SetProperty("id", &WebContents::ID) .SetProperty("id", &WebContents::ID)
.SetProperty("session", &WebContents::Session) .SetProperty("session", &WebContents::Session)
.SetProperty("hostWebContents", &WebContents::HostWebContents) .SetProperty("hostWebContents", &WebContents::HostWebContents)

View file

@ -250,6 +250,9 @@ class WebContents : public mate::TrackableObject<WebContents>,
// the specified URL. // the specified URL.
void GrantOriginAccess(const GURL& url); void GrantOriginAccess(const GURL& url);
bool TakeHeapSnapshot(const base::FilePath& file_path,
const std::string& channel);
// Properties. // Properties.
int32_t ID() const; int32_t ID() const;
v8::Local<v8::Value> Session(v8::Isolate* isolate); v8::Local<v8::Value> Session(v8::Isolate* isolate);

View file

@ -10,6 +10,7 @@
#include "content/public/common/common_param_traits.h" #include "content/public/common/common_param_traits.h"
#include "content/public/common/referrer.h" #include "content/public/common/referrer.h"
#include "ipc/ipc_message_macros.h" #include "ipc/ipc_message_macros.h"
#include "ipc/ipc_platform_file.h"
#include "ui/gfx/geometry/rect_f.h" #include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/ipc/gfx_param_traits.h" #include "ui/gfx/ipc/gfx_param_traits.h"
#include "url/gurl.h" #include "url/gurl.h"
@ -76,3 +77,7 @@ IPC_SYNC_MESSAGE_ROUTED0_1(AtomFrameHostMsg_GetZoomLevel, double /* result */)
IPC_MESSAGE_ROUTED2(AtomFrameHostMsg_PDFSaveURLAs, IPC_MESSAGE_ROUTED2(AtomFrameHostMsg_PDFSaveURLAs,
GURL /* url */, GURL /* url */,
content::Referrer /* referrer */) content::Referrer /* referrer */)
IPC_MESSAGE_ROUTED2(AtomFrameMsg_TakeHeapSnapshot,
IPC::PlatformFileForTransit /* file_handle */,
std::string /* channel */)

View file

@ -11,12 +11,15 @@
#include "atom/common/api/locker.h" #include "atom/common/api/locker.h"
#include "atom/common/atom_version.h" #include "atom/common/atom_version.h"
#include "atom/common/chrome_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/native_mate_converters/string16_converter.h"
#include "atom/common/node_includes.h" #include "atom/common/node_includes.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/process/process_info.h" #include "base/process/process_info.h"
#include "base/process/process_metrics_iocounters.h" #include "base/process/process_metrics_iocounters.h"
#include "base/sys_info.h" #include "base/sys_info.h"
#include "base/threading/thread_restrictions.h"
#include "native_mate/dictionary.h" #include "native_mate/dictionary.h"
namespace atom { namespace atom {
@ -60,6 +63,7 @@ void AtomBindings::BindTo(v8::Isolate* isolate, v8::Local<v8::Object> process) {
dict.SetMethod("getCPUUsage", base::Bind(&AtomBindings::GetCPUUsage, dict.SetMethod("getCPUUsage", base::Bind(&AtomBindings::GetCPUUsage,
base::Unretained(metrics_.get()))); base::Unretained(metrics_.get())));
dict.SetMethod("getIOCounters", &GetIOCounters); dict.SetMethod("getIOCounters", &GetIOCounters);
dict.SetMethod("takeHeapSnapshot", &TakeHeapSnapshot);
#if defined(OS_POSIX) #if defined(OS_POSIX)
dict.SetMethod("setFdLimit", &base::SetFdLimit); dict.SetMethod("setFdLimit", &base::SetFdLimit);
#endif #endif
@ -238,4 +242,15 @@ v8::Local<v8::Value> AtomBindings::GetIOCounters(v8::Isolate* isolate) {
return dict.GetHandle(); 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 } // namespace atom

View file

@ -8,6 +8,7 @@
#include <list> #include <list>
#include <memory> #include <memory>
#include "base/files/file_path.h"
#include "base/macros.h" #include "base/macros.h"
#include "base/process/process_metrics.h" #include "base/process/process_metrics.h"
#include "base/strings/string16.h" #include "base/strings/string16.h"
@ -43,6 +44,8 @@ class AtomBindings {
static v8::Local<v8::Value> GetCPUUsage(base::ProcessMetrics* metrics, static v8::Local<v8::Value> GetCPUUsage(base::ProcessMetrics* metrics,
v8::Isolate* isolate); v8::Isolate* isolate);
static v8::Local<v8::Value> GetIOCounters(v8::Isolate* isolate); static v8::Local<v8::Value> GetIOCounters(v8::Isolate* isolate);
static bool TakeHeapSnapshot(v8::Isolate* isolate,
const base::FilePath& file_path);
private: private:
void ActivateUVLoop(v8::Isolate* isolate); void ActivateUVLoop(v8::Isolate* isolate);

View file

@ -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<v8::HeapSnapshot*>(snapshot)->Delete();
return stream.IsComplete();
}
} // namespace atom

View file

@ -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_

View file

@ -9,9 +9,11 @@
#include "atom/common/api/api_messages.h" #include "atom/common/api/api_messages.h"
#include "atom/common/api/event_emitter_caller.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/native_mate_converters/value_converter.h"
#include "atom/common/node_includes.h" #include "atom/common/node_includes.h"
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
#include "base/threading/thread_restrictions.h"
#include "base/trace_event/trace_event.h" #include "base/trace_event/trace_event.h"
#include "content/public/renderer/render_frame.h" #include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_view.h" #include "content/public/renderer/render_view.h"
@ -19,10 +21,10 @@
#include "native_mate/dictionary.h" #include "native_mate/dictionary.h"
#include "net/base/net_module.h" #include "net/base/net_module.h"
#include "net/grit/net_resources.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_document.h"
#include "third_party/blink/public/web/web_draggable_region.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/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_local_frame.h"
#include "third_party/blink/public/web/web_script_source.h" #include "third_party/blink/public/web/web_script_source.h"
#include "ui/base/resource/resource_bundle.h" #include "ui/base/resource/resource_bundle.h"
@ -161,6 +163,7 @@ bool AtomRenderFrameObserver::OnMessageReceived(const IPC::Message& message) {
bool handled = true; bool handled = true;
IPC_BEGIN_MESSAGE_MAP(AtomRenderFrameObserver, message) IPC_BEGIN_MESSAGE_MAP(AtomRenderFrameObserver, message)
IPC_MESSAGE_HANDLER(AtomFrameMsg_Message, OnBrowserMessage) IPC_MESSAGE_HANDLER(AtomFrameMsg_Message, OnBrowserMessage)
IPC_MESSAGE_HANDLER(AtomFrameMsg_TakeHeapSnapshot, OnTakeHeapSnapshot)
IPC_MESSAGE_UNHANDLED(handled = false) IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP() 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, void AtomRenderFrameObserver::EmitIPCEvent(blink::WebLocalFrame* frame,
const std::string& channel, const std::string& channel,
const base::ListValue& args, const base::ListValue& args,

View file

@ -10,6 +10,7 @@
#include "atom/renderer/renderer_client_base.h" #include "atom/renderer/renderer_client_base.h"
#include "base/strings/string16.h" #include "base/strings/string16.h"
#include "content/public/renderer/render_frame_observer.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" #include "third_party/blink/public/web/web_local_frame.h"
namespace base { namespace base {
@ -57,6 +58,8 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver {
const std::string& channel, const std::string& channel,
const base::ListValue& args, const base::ListValue& args,
int32_t sender_id); int32_t sender_id);
void OnTakeHeapSnapshot(IPC::PlatformFileForTransit file_handle,
const std::string& channel);
content::RenderFrame* render_frame_; content::RenderFrame* render_frame_;
RendererClientBase* renderer_client_; RendererClientBase* renderer_client_;

View file

@ -171,6 +171,14 @@ Returns `Object`:
Returns an object giving memory usage statistics about the entire system. Note Returns an object giving memory usage statistics about the entire system. Note
that all statistics are reported in Kilobytes. 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()` ### `process.hang()`
Causes the main thread of the current process hang. Causes the main thread of the current process hang.

View file

@ -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 be compared to the `frameProcessId` passed by frame specific navigation events
(e.g. `did-frame-navigate`) (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 ### Instance Properties
#### `contents.id` #### `contents.id`

View file

@ -486,6 +486,8 @@ filenames = {
"atom/common/draggable_region.cc", "atom/common/draggable_region.cc",
"atom/common/draggable_region.h", "atom/common/draggable_region.h",
"atom/common/google_api_key.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/key_weak_map.h",
"atom/common/keyboard_util.cc", "atom/common/keyboard_util.cc",
"atom/common/keyboard_util.h", "atom/common/keyboard_util.h",

View file

@ -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. // Translate the options of printToPDF.
WebContents.prototype.printToPDF = function (options, callback) { WebContents.prototype.printToPDF = function (options, callback) {
const printingSetting = Object.assign({}, defaultPrintingSetting) const printingSetting = Object.assign({}, defaultPrintingSetting)

6
package-lock.json generated
View file

@ -2841,9 +2841,9 @@
} }
}, },
"electron-typescript-definitions": { "electron-typescript-definitions": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/electron-typescript-definitions/-/electron-typescript-definitions-2.0.0.tgz", "resolved": "https://registry.npmjs.org/electron-typescript-definitions/-/electron-typescript-definitions-2.0.1.tgz",
"integrity": "sha512-uhbLoHoIWNafFqGEtdUMtkKimvxusU2GmdbgcXoT3CjD85B2vRyffbMxXYPpxx+o88z1xMP/lw2rQq2um/G6fw==", "integrity": "sha512-H1DD4g+Usrddyb5VK94Ofxn2gQUSUfj8gHRYcZKbkIe5CTWQ+Gl/kc/qRQ+QL+oH/8B8MHM6UJoxNfbcCrzIgQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "^7.0.18", "@types/node": "^7.0.18",

View file

@ -13,7 +13,7 @@
"dugite": "^1.45.0", "dugite": "^1.45.0",
"electabul": "~0.0.4", "electabul": "~0.0.4",
"electron-docs-linter": "^2.3.4", "electron-docs-linter": "^2.3.4",
"electron-typescript-definitions": "^2.0.0", "electron-typescript-definitions": "^2.0.1",
"eslint": "^5.6.0", "eslint": "^5.6.0",
"eslint-config-standard": "^12.0.0", "eslint-config-standard": "^12.0.0",
"eslint-plugin-mocha": "^5.2.0", "eslint-plugin-mocha": "^5.2.0",

View file

@ -1,3 +1,7 @@
const { remote } = require('electron')
const fs = require('fs')
const path = require('path')
const { expect } = require('chai') const { expect } = require('chai')
describe('process module', () => { describe('process module', () => {
@ -67,4 +71,32 @@ describe('process module', () => {
expect(heapStats.doesZapGarbage).to.be.a('boolean') 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()
})
})
}) })

View file

@ -1,6 +1,7 @@
'use strict' 'use strict'
const assert = require('assert') const assert = require('assert')
const fs = require('fs')
const http = require('http') const http = require('http')
const path = require('path') const path = require('path')
const { closeWindow } = require('./window-helpers') const { closeWindow } = require('./window-helpers')
@ -799,4 +800,53 @@ describe('webContents module', () => {
w.loadURL('about:blank') 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')
})
})
}) })