From e4ca6558729b4a6734b1e189eadddd1d1628e89e 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() --- atom/browser/api/atom_api_web_contents.cc | 24 +++++++ atom/browser/api/atom_api_web_contents.h | 3 + atom/common/api/api_messages.h | 5 ++ atom/common/api/atom_bindings.cc | 64 +++++++++++++++++++ atom/common/api/atom_bindings.h | 4 ++ .../common/api/heap_snapshot_output_stream.cc | 50 +++++++++++++++ atom/common/api/heap_snapshot_output_stream.h | 34 ++++++++++ .../atom_sandboxed_renderer_client.cc | 4 +- docs/api/process.md | 4 ++ filenames.gni | 2 + lib/browser/api/web-contents.js | 14 ++++ lib/renderer/heap-snapshot.js | 16 +++++ lib/renderer/init.js | 1 + lib/sandboxed_renderer/init.js | 2 + 14 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 atom/common/api/heap_snapshot_output_stream.cc create mode 100644 atom/common/api/heap_snapshot_output_stream.h create mode 100644 lib/renderer/heap-snapshot.js diff --git a/atom/browser/api/atom_api_web_contents.cc b/atom/browser/api/atom_api_web_contents.cc index 87411ec7f73cd..a5742817f6f5a 100644 --- a/atom/browser/api/atom_api_web_contents.cc +++ b/atom/browser/api/atom_api_web_contents.cc @@ -28,6 +28,8 @@ #include "atom/browser/lib/bluetooth_chooser.h" #include "atom/browser/native_window.h" #include "atom/browser/net/atom_network_delegate.h" +#include "atom/common/api/heap_snapshot_output_stream.h" +#include "base/threading/thread_restrictions.h" #if defined(ENABLE_OSR) #include "atom/browser/osr/osr_output_device.h" #include "atom/browser/osr/osr_render_widget_host_view.h" @@ -307,6 +309,10 @@ struct WebContents::FrameDispatchHelper { IPC::Message* message) { api_web_contents->OnRendererMessageSync(rfh, channel, args, message); } + + void OnCreateHeapSnapshotFile(IPC::Message* message) { + api_web_contents->OnCreateHeapSnapshotFile(rfh, message); + } }; WebContents::WebContents(v8::Isolate* isolate, @@ -1044,6 +1050,9 @@ bool WebContents::OnMessageReceived(const IPC::Message& message, IPC_MESSAGE_HANDLER(AtomAutofillFrameHostMsg_ShowPopup, ShowAutofillPopup) IPC_MESSAGE_HANDLER(AtomAutofillFrameHostMsg_HidePopup, HideAutofillPopup) #endif + IPC_MESSAGE_FORWARD_DELAY_REPLY( + AtomFrameHostMsg_CreateHeapSnapshotFile, &helper, + FrameDispatchHelper::OnCreateHeapSnapshotFile) IPC_MESSAGE_UNHANDLED(handled = false) IPC_END_MESSAGE_MAP() @@ -2116,6 +2125,21 @@ void WebContents::OnRendererMessageTo(content::RenderFrameHost* frame_host, } } +void WebContents::OnCreateHeapSnapshotFile(content::RenderFrameHost* frame_host, + IPC::Message* message) { + base::ScopedAllowBlockingForTesting allow_blocking; + + auto file_path = HeapSnapshotOutputStream::GetFilePath(); + + base::File file(file_path, + base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); + + AtomFrameHostMsg_CreateHeapSnapshotFile::WriteReplyParams( + message, file_path, IPC::TakePlatformFileForTransit(std::move(file))); + + frame_host->Send(message); +} + // static mate::Handle WebContents::CreateFrom( v8::Isolate* isolate, diff --git a/atom/browser/api/atom_api_web_contents.h b/atom/browser/api/atom_api_web_contents.h index 0dbb808856840..d81045610d20d 100644 --- a/atom/browser/api/atom_api_web_contents.h +++ b/atom/browser/api/atom_api_web_contents.h @@ -433,6 +433,9 @@ class WebContents : public mate::TrackableObject, const std::string& channel, const base::ListValue& args); + void OnCreateHeapSnapshotFile(content::RenderFrameHost* frame_host, + IPC::Message* message); + // Called when received a synchronous message from renderer to // set temporary zoom level. void OnSetTemporaryZoomLevel(content::RenderFrameHost* frame_host, diff --git a/atom/common/api/api_messages.h b/atom/common/api/api_messages.h index e7317736e0766..afc3aa94fe64d 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_SYNC_MESSAGE_ROUTED0_2(AtomFrameHostMsg_CreateHeapSnapshotFile, + base::FilePath /* file_path */, + IPC::PlatformFileForTransit /* file_handle */) diff --git a/atom/common/api/atom_bindings.cc b/atom/common/api/atom_bindings.cc index 32f05baabbb90..734490cd9a519 100644 --- a/atom/common/api/atom_bindings.cc +++ b/atom/common/api/atom_bindings.cc @@ -8,16 +8,22 @@ #include #include +#include "atom/common/api/api_messages.h" +#include "atom/common/api/heap_snapshot_output_stream.h" #include "atom/common/api/locker.h" #include "atom/common/atom_version.h" #include "atom/common/chrome_version.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 "content/public/renderer/render_frame.h" #include "native_mate/dictionary.h" +#include "third_party/blink/public/web/web_local_frame.h" +#include "v8/include/v8-profiler.h" namespace atom { @@ -35,6 +41,14 @@ void FatalErrorCallback(const char* location, const char* message) { AtomBindings::Crash(); } +content::RenderFrame* GetCurrentRenderFrame() { + auto* frame = blink::WebLocalFrame::FrameForCurrentContext(); + if (!frame) + return nullptr; + + return content::RenderFrame::FromWebFrame(frame); +} + } // namespace AtomBindings::AtomBindings(uv_loop_t* loop) { @@ -60,6 +74,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 +253,53 @@ v8::Local AtomBindings::GetIOCounters(v8::Isolate* isolate) { return dict.GetHandle(); } +// static +base::FilePath AtomBindings::TakeHeapSnapshot(v8::Isolate* isolate, + mate::Arguments* args) { + base::ScopedAllowBlockingForTesting allow_blocking; + + auto file_path = HeapSnapshotOutputStream::GetFilePath(); + + base::File file(file_path, + base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); + + if (!file.IsValid()) { + auto* render_frame = GetCurrentRenderFrame(); + if (!render_frame) { + args->ThrowError("takeHeapSnapshot failed"); + return base::FilePath(); + } + + IPC::PlatformFileForTransit file_handle; + auto* message = new AtomFrameHostMsg_CreateHeapSnapshotFile( + render_frame->GetRoutingID(), &file_path, &file_handle); + + if (!render_frame->Send(message)) { + args->ThrowError("takeHeapSnapshot failed"); + return base::FilePath(); + } + + file = IPC::PlatformFileForTransitToFile(file_handle); + } + + if (!file.IsValid()) { + args->ThrowError("takeHeapSnapshot failed - cannot create file"); + return base::FilePath(); + } + + auto* snap = isolate->GetHeapProfiler()->TakeHeapSnapshot(); + + HeapSnapshotOutputStream stream(&file); + snap->Serialize(&stream, v8::HeapSnapshot::kJSON); + + const_cast(snap)->Delete(); + + if (!stream.IsComplete()) { + args->ThrowError("takeHeapSnapshot failed - snapshot incomplete"); + return base::FilePath(); + } + + return file_path; +} + } // namespace atom diff --git a/atom/common/api/atom_bindings.h b/atom/common/api/atom_bindings.h index 592ca08ba3833..1d1e720fd2dd4 100644 --- a/atom/common/api/atom_bindings.h +++ b/atom/common/api/atom_bindings.h @@ -6,7 +6,9 @@ #define ATOM_COMMON_API_ATOM_BINDINGS_H_ #include +#include +#include "base/files/file_path.h" #include "base/macros.h" #include "base/process/process_metrics.h" #include "base/strings/string16.h" @@ -42,6 +44,8 @@ class AtomBindings { static v8::Local GetCPUUsage(base::ProcessMetrics* metrics, v8::Isolate* isolate); static v8::Local GetIOCounters(v8::Isolate* isolate); + static base::FilePath TakeHeapSnapshot(v8::Isolate* isolate, + mate::Arguments* args); private: void ActivateUVLoop(v8::Isolate* isolate); diff --git a/atom/common/api/heap_snapshot_output_stream.cc b/atom/common/api/heap_snapshot_output_stream.cc new file mode 100644 index 0000000000000..787c955f324c5 --- /dev/null +++ b/atom/common/api/heap_snapshot_output_stream.cc @@ -0,0 +1,50 @@ +// 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/api/heap_snapshot_output_stream.h" + +#include "base/path_service.h" +#include "base/strings/stringprintf.h" +#include "brightray/browser/brightray_paths.h" +#include "uv.h" // NOLINT(build/include) + +namespace atom { + +HeapSnapshotOutputStream::HeapSnapshotOutputStream(base::File* file) + : file_(file) {} + +bool HeapSnapshotOutputStream::IsComplete() const { + return is_complete_; +} + +int HeapSnapshotOutputStream::GetChunkSize() { + return 65536; +} + +void HeapSnapshotOutputStream::EndOfStream() { + is_complete_ = true; +} + +v8::OutputStream::WriteResult HeapSnapshotOutputStream::WriteAsciiChunk( + char* data, + int size) { + auto bytes_written = file_->WriteAtCurrentPos(data, size); + return bytes_written == size ? kContinue : kAbort; +} + +// static +base::FilePath HeapSnapshotOutputStream::GetFilePath() { + base::FilePath user_data; + if (!PathService::Get(brightray::DIR_USER_DATA, &user_data)) + return base::FilePath(); + + auto now = uv_hrtime(); + auto sec = static_cast(now / 1000000); + auto usec = static_cast(now % 1000000); + auto filename = base::StringPrintf("heapdump-%u-%u.heapsnapshot", sec, usec); + + return user_data.AppendASCII(filename.c_str()); +} + +} // namespace atom diff --git a/atom/common/api/heap_snapshot_output_stream.h b/atom/common/api/heap_snapshot_output_stream.h new file mode 100644 index 0000000000000..d19fbd5e867dd --- /dev/null +++ b/atom/common/api/heap_snapshot_output_stream.h @@ -0,0 +1,34 @@ +// 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_API_HEAP_SNAPSHOT_OUTPUT_STREAM_H_ +#define ATOM_COMMON_API_HEAP_SNAPSHOT_OUTPUT_STREAM_H_ + +#include "base/files/file.h" +#include "base/files/file_path.h" +#include "v8/include/v8-profiler.h" + +namespace atom { + +class HeapSnapshotOutputStream : public v8::OutputStream { + public: + explicit HeapSnapshotOutputStream(base::File* file); + + bool IsComplete() const; + + // v8::OutputStream + int GetChunkSize() override; + void EndOfStream() override; + v8::OutputStream::WriteResult WriteAsciiChunk(char* data, int size) override; + + static base::FilePath GetFilePath(); + + private: + base::File* file_ = nullptr; + bool is_complete_ = false; +}; + +} // namespace atom + +#endif // ATOM_COMMON_API_HEAP_SNAPSHOT_OUTPUT_STREAM_H_ diff --git a/atom/renderer/atom_sandboxed_renderer_client.cc b/atom/renderer/atom_sandboxed_renderer_client.cc index 397f96e60d28f..71aac4699caa4 100644 --- a/atom/renderer/atom_sandboxed_renderer_client.cc +++ b/atom/renderer/atom_sandboxed_renderer_client.cc @@ -6,6 +6,7 @@ #include "atom/common/api/api_messages.h" #include "atom/common/api/atom_bindings.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/value_converter.h" #include "atom/common/node_bindings.h" @@ -20,8 +21,8 @@ #include "chrome/renderer/printing/print_web_view_helper.h" #include "content/public/renderer/render_frame.h" #include "native_mate/dictionary.h" -#include "third_party/blink/public/web/web_document.h" #include "third_party/blink/public/web/blink.h" +#include "third_party/blink/public/web/web_document.h" #include "atom/common/node_includes.h" #include "atom_natives.h" // NOLINT: This file is generated with js2c @@ -157,6 +158,7 @@ void AtomSandboxedRendererClient::InitializeBindings( b.SetMethod("getCPUUsage", base::Bind(&AtomBindings::GetCPUUsage, base::Unretained(metrics_.get()))); b.SetMethod("getIOCounters", &AtomBindings::GetIOCounters); + b.SetMethod("takeHeapSnapshot", &AtomBindings::TakeHeapSnapshot); // Pass in CLI flags needed to setup the renderer base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); diff --git a/docs/api/process.md b/docs/api/process.md index 691238a737a42..8092a4fca1eb0 100644 --- a/docs/api/process.md +++ b/docs/api/process.md @@ -171,6 +171,10 @@ Returns `Object`: Returns an object giving memory usage statistics about the entire system. Note that all statistics are reported in Kilobytes. +### `process.takeHeapSnapshot()` + +Returns `String` - The file path if the heap snapshot has been created successfully. + ### `process.hang()` Causes the main thread of the current process hang. diff --git a/filenames.gni b/filenames.gni index 29fc65b928959..71c7c657aa72e 100644 --- a/filenames.gni +++ b/filenames.gni @@ -445,6 +445,8 @@ filenames = { "atom/common/api/event_emitter_caller.cc", "atom/common/api/event_emitter_caller.h", "atom/common/api/features.cc", + "atom/common/api/heap_snapshot_output_stream.cc", + "atom/common/api/heap_snapshot_output_stream.h", "atom/common/api/locker.cc", "atom/common/api/locker.h", "atom/common/api/object_life_monitor.cc", diff --git a/lib/browser/api/web-contents.js b/lib/browser/api/web-contents.js index 11080fc7de17b..675a1d1b5c772 100644 --- a/lib/browser/api/web-contents.js +++ b/lib/browser/api/web-contents.js @@ -160,6 +160,20 @@ WebContents.prototype.executeJavaScript = function (code, hasUserGesture, callba } } +WebContents.prototype.takeHeapSnapshot = function (...args) { + return new Promise((resolve, reject) => { + const requestId = getNextId() + this.send('ELECTRON_RENDERER_TAKE_HEAP_SNAPSHOT', requestId, args) + ipcMain.once(`ELECTRON_BROWSER_TAKE_HEAP_SNAPSHOT_RESULT_${requestId}`, (event, error, result) => { + if (error) { + reject(errorUtils.deserialize(error)) + } else { + resolve(result) + } + }) + }) +} + // Translate the options of printToPDF. WebContents.prototype.printToPDF = function (options, callback) { const printingSetting = Object.assign({}, defaultPrintingSetting) diff --git a/lib/renderer/heap-snapshot.js b/lib/renderer/heap-snapshot.js new file mode 100644 index 0000000000000..d804ce73eafcf --- /dev/null +++ b/lib/renderer/heap-snapshot.js @@ -0,0 +1,16 @@ +const {ipcRenderer} = require('electron') +const errorUtils = require('../common/error-utils') + +module.exports = (process) => { + ipcRenderer.on('ELECTRON_RENDERER_TAKE_HEAP_SNAPSHOT', (event, requestId, args) => { + new Promise(resolve => + process.takeHeapSnapshot(...args) + ).then(result => { + return [null, result] + }, error => { + return [errorUtils.serialize(error)] + }).then(responseArgs => { + event.sender.send(`ELECTRON_BROWSER_TAKE_HEAP_SNAPSHOT_RESULT_${requestId}`, ...responseArgs) + }) + }) +} diff --git a/lib/renderer/init.js b/lib/renderer/init.js index 7ea87363cbe9c..7bc90befe9f6f 100644 --- a/lib/renderer/init.js +++ b/lib/renderer/init.js @@ -40,6 +40,7 @@ const { } = require('./security-warnings') require('./web-frame-init')() +require('./heap-snapshot')(process) // Process command line arguments. let nodeIntegration = 'false' diff --git a/lib/sandboxed_renderer/init.js b/lib/sandboxed_renderer/init.js index 9569ec49c9153..cde352da3961a 100644 --- a/lib/sandboxed_renderer/init.js +++ b/lib/sandboxed_renderer/init.js @@ -42,6 +42,7 @@ const { } = electron.ipcRenderer.sendSync('ELECTRON_BROWSER_SANDBOX_LOAD') require('../renderer/web-frame-init')() +require('../renderer/heap-snapshot')(binding) // Pass different process object to the preload script(which should not have // access to things like `process.atomBinding`). @@ -52,6 +53,7 @@ preloadProcess.getHeapStatistics = () => binding.getHeapStatistics() preloadProcess.getSystemMemoryInfo = () => binding.getSystemMemoryInfo() preloadProcess.getCPUUsage = () => binding.getCPUUsage() preloadProcess.getIOCounters = () => binding.getIOCounters() +preloadProcess.takeHeapSnapshot = () => binding.takeHeapSnapshot() preloadProcess.argv = process.argv = binding.getArgv() preloadProcess.execPath = process.execPath = binding.getExecPath() preloadProcess.pid = process.pid = binding.getPid()