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 "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<v8::FunctionTemplate> 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)

View file

@ -250,6 +250,9 @@ class WebContents : public mate::TrackableObject<WebContents>,
// 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<v8::Value> Session(v8::Isolate* isolate);

View file

@ -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 */)

View file

@ -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<v8::Object> 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<v8::Value> 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

View file

@ -8,6 +8,7 @@
#include <list>
#include <memory>
#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<v8::Value> GetCPUUsage(base::ProcessMetrics* metrics,
v8::Isolate* isolate);
static v8::Local<v8::Value> GetIOCounters(v8::Isolate* isolate);
static bool TakeHeapSnapshot(v8::Isolate* isolate,
const base::FilePath& file_path);
private:
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/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,

View file

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

View file

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

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
(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`

View file

@ -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",

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

6
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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()
})
})
})

View file

@ -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')
})
})
})