Bug 1551992 - Compress profile data before sending to perf-html; r=mstange,julienw
authorJim Porter <jporter@mozilla.com>
Tue, 25 Jun 2019 09:54:49 +0000
changeset 480072 0ec0ee9c1e40c14df54d6d3eb611d56168afb4e4
parent 480071 5598fb78432945fa1cb0d00ea23baebeb4297bea
child 480073 93b6bc89bf32a6558a9988730c239bfeb4c59eb2
push id88480
push userjporter@mozilla.com
push dateTue, 25 Jun 2019 17:23:09 +0000
treeherderautoland@0ec0ee9c1e40 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmstange, julienw
bugs1551992
milestone69.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1551992 - Compress profile data before sending to perf-html; r=mstange,julienw Differential Revision: https://phabricator.services.mozilla.com/D33399
devtools/client/performance-new/popup/background.jsm
toolkit/components/extensions/parent/ext-geckoProfiler.js
toolkit/components/extensions/schemas/geckoProfiler.json
tools/profiler/gecko/nsIProfiler.idl
tools/profiler/gecko/nsProfiler.cpp
--- a/devtools/client/performance-new/popup/background.jsm
+++ b/devtools/client/performance-new/popup/background.jsm
@@ -86,22 +86,21 @@ async function captureProfile() {
   if (!state.isRunning) {
     // The profiler is not active, ignore this shortcut.
     return;
   }
   // Pause profiler before we collect the profile, so that we don't capture
   // more samples while the parent process waits for subprocess profiles.
   Services.profiler.PauseSampling();
 
-  const profile = await Services.profiler.getProfileDataAsArrayBuffer().catch(
-    e => {
-      console.error(e);
-      return {};
-    }
-  );
+  const profile = await Services.profiler.getProfileDataAsGzippedArrayBuffer()
+                                .catch(e => {
+                                  console.error(e);
+                                  return {};
+                                });
 
   receiveProfile(profile, getSymbols);
 
   Services.profiler.ResumeSampling();
 }
 
 /**
  * Not all features are supported on every version of Firefox. Get the list of checked
--- a/toolkit/components/extensions/parent/ext-geckoProfiler.js
+++ b/toolkit/components/extensions/parent/ext-geckoProfiler.js
@@ -117,16 +117,25 @@ this.geckoProfiler = class extends Exten
           if (!Services.profiler.IsActive()) {
             throw new ExtensionError("The profiler is stopped. " +
               "You need to start the profiler before you can capture a profile.");
           }
 
           return Services.profiler.getProfileDataAsArrayBuffer();
         },
 
+        async getProfileAsGzippedArrayBuffer() {
+          if (!Services.profiler.IsActive()) {
+            throw new ExtensionError("The profiler is stopped. " +
+              "You need to start the profiler before you can capture a profile.");
+          }
+
+          return Services.profiler.getProfileDataAsGzippedArrayBuffer();
+        },
+
         async getSymbols(debugName, breakpadId) {
           if (symbolCache.size === 0) {
             primeSymbolStore(Services.profiler.sharedLibraries);
           }
 
           const cachedLibInfo = symbolCache.get(`${debugName}/${breakpadId}`);
           if (!cachedLibInfo) {
             throw new Error(
--- a/toolkit/components/extensions/schemas/geckoProfiler.json
+++ b/toolkit/components/extensions/schemas/geckoProfiler.json
@@ -122,16 +122,23 @@
       {
         "name": "getProfileAsArrayBuffer",
         "type": "function",
         "description": "Gathers the profile data from the current profiling session. The returned promise resolves to an array buffer that contains a JSON string.",
         "async": true,
         "parameters": []
       },
       {
+        "name": "getProfileAsGzippedArrayBuffer",
+        "type": "function",
+        "description": "Gathers the profile data from the current profiling session. The returned promise resolves to an array buffer that contains a gzipped JSON string.",
+        "async": true,
+        "parameters": []
+      },
+      {
         "name": "getSymbols",
         "type": "function",
         "description": "Gets the debug symbols for a particular library.",
         "async": true,
         "parameters": [
           {
             "type": "string",
             "name": "debugName",
--- a/tools/profiler/gecko/nsIProfiler.idl
+++ b/tools/profiler/gecko/nsIProfiler.idl
@@ -57,16 +57,19 @@ interface nsIProfiler : nsISupports
   jsval getProfileData([optional] in double aSinceTime);
 
   [implicit_jscontext]
   Promise getProfileDataAsync([optional] in double aSinceTime);
 
   [implicit_jscontext]
   Promise getProfileDataAsArrayBuffer([optional] in double aSinceTime);
 
+  [implicit_jscontext]
+  Promise getProfileDataAsGzippedArrayBuffer([optional] in double aSinceTime);
+
   /**
    * Returns a promise that resolves once the file has been written.
    */
   [implicit_jscontext]
   Promise dumpProfileToFileAsync(in ACString aFilename,
                                  [optional] in double aSinceTime);
 
   boolean IsActive();
--- a/tools/profiler/gecko/nsProfiler.cpp
+++ b/tools/profiler/gecko/nsProfiler.cpp
@@ -25,16 +25,17 @@
 #include "nsILoadContext.h"
 #include "nsIObserverService.h"
 #include "nsIWebNavigation.h"
 #include "nsLocalFile.h"
 #include "nsMemory.h"
 #include "nsString.h"
 #include "nsThreadUtils.h"
 #include "shared-libraries.h"
+#include "zlib.h"
 
 #include <string>
 #include <sstream>
 
 using namespace mozilla;
 
 using dom::AutoJSAPI;
 using dom::Promise;
@@ -368,16 +369,110 @@ nsProfiler::GetProfileDataAsArrayBuffer(
           },
           [promise](nsresult aRv) { promise->MaybeReject(aRv); });
 
   promise.forget(aPromise);
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsProfiler::GetProfileDataAsGzippedArrayBuffer(double aSinceTime,
+                                               JSContext* aCx,
+                                               Promise** aPromise) {
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (!profiler_is_active()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  if (NS_WARN_IF(!aCx)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
+  if (NS_WARN_IF(!globalObject)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  ErrorResult result;
+  RefPtr<Promise> promise = Promise::Create(globalObject, result);
+  if (NS_WARN_IF(result.Failed())) {
+    return result.StealNSResult();
+  }
+
+  StartGathering(aSinceTime)
+      ->Then(
+          GetMainThreadSerialEventTarget(), __func__,
+          [promise](nsCString aResult) {
+            AutoJSAPI jsapi;
+            if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
+              // We're really hosed if we can't get a JS context for some
+              // reason.
+              promise->MaybeReject(NS_ERROR_DOM_UNKNOWN_ERR);
+              return;
+            }
+
+            // Compress a buffer via zlib (as with `compress()`), but emit a
+            // gzip header as well. Like `compress()`, this is limited to 4GB in
+            // size, but that shouldn't be an issue for our purposes.
+            uLongf outSize = compressBound(aResult.Length());
+            FallibleTArray<uint8_t> outBuff;
+            if (!outBuff.SetLength(outSize, fallible)) {
+              promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+              return;
+            }
+
+            int zerr;
+            z_stream stream;
+            stream.zalloc = nullptr;
+            stream.zfree = nullptr;
+            stream.opaque = nullptr;
+            stream.next_out = (Bytef*)outBuff.Elements();
+            stream.avail_out = outBuff.Length();
+            stream.next_in = (z_const Bytef*)aResult.Data();
+            stream.avail_in = aResult.Length();
+
+            // A windowBits of 31 is the default (15) plus 16 for emitting a
+            // gzip header; a memLevel of 8 is the default.
+            zerr = deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED,
+                                /* windowBits */ 31, /* memLevel */ 8,
+                                Z_DEFAULT_STRATEGY);
+            if (zerr != Z_OK) {
+              promise->MaybeReject(NS_ERROR_FAILURE);
+              return;
+            }
+
+            zerr = deflate(&stream, Z_FINISH);
+            outSize = stream.total_out;
+            deflateEnd(&stream);
+
+            if (zerr != Z_STREAM_END) {
+              promise->MaybeReject(NS_ERROR_FAILURE);
+              return;
+            }
+
+            outBuff.TruncateLength(outSize);
+
+            JSContext* cx = jsapi.cx();
+            JSObject* typedArray = dom::ArrayBuffer::Create(
+                cx, outBuff.Length(), outBuff.Elements());
+            if (typedArray) {
+              JS::RootedValue val(cx, JS::ObjectValue(*typedArray));
+              promise->MaybeResolve(val);
+            } else {
+              promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+            }
+          },
+          [promise](nsresult aRv) { promise->MaybeReject(aRv); });
+
+  promise.forget(aPromise);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsProfiler::DumpProfileToFileAsync(const nsACString& aFilename,
                                    double aSinceTime, JSContext* aCx,
                                    Promise** aPromise) {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (!profiler_is_active()) {
     return NS_ERROR_FAILURE;
   }