Browse Source

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

Milan Burda 6 years ago
parent
commit
e22142ef9c

+ 20 - 0
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<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)

+ 3 - 0
atom/browser/api/atom_api_web_contents.h

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

+ 5 - 0
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 */)

+ 15 - 0
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<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

+ 3 - 0
atom/common/api/atom_bindings.h

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

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

+ 17 - 0
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_

+ 20 - 1
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,

+ 3 - 0
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_;

+ 8 - 0
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.

+ 8 - 0
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`

+ 2 - 0
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",

+ 16 - 0
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)

+ 3 - 3
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",

+ 1 - 1
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",

+ 32 - 0
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()
+    })
+  })
 })

+ 50 - 0
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')
+    })
+  })
 })