diff --git a/devtools/client/performance-new/popup/background.jsm b/devtools/client/performance-new/popup/background.jsm index 0c50c8164208..e8cb34ce0eb9 100644 --- a/devtools/client/performance-new/popup/background.jsm +++ b/devtools/client/performance-new/popup/background.jsm @@ -91,12 +91,11 @@ async function captureProfile() { // 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); diff --git a/toolkit/components/extensions/parent/ext-geckoProfiler.js b/toolkit/components/extensions/parent/ext-geckoProfiler.js index e01cdaacab26..3d3b4c2efa31 100644 --- a/toolkit/components/extensions/parent/ext-geckoProfiler.js +++ b/toolkit/components/extensions/parent/ext-geckoProfiler.js @@ -122,6 +122,15 @@ this.geckoProfiler = class extends ExtensionAPI { 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); diff --git a/toolkit/components/extensions/schemas/geckoProfiler.json b/toolkit/components/extensions/schemas/geckoProfiler.json index 9f2702a70d9d..cb7a40f337f5 100644 --- a/toolkit/components/extensions/schemas/geckoProfiler.json +++ b/toolkit/components/extensions/schemas/geckoProfiler.json @@ -126,6 +126,13 @@ "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", diff --git a/tools/profiler/gecko/nsIProfiler.idl b/tools/profiler/gecko/nsIProfiler.idl index 341c5e47c87d..9f8b62bf9799 100644 --- a/tools/profiler/gecko/nsIProfiler.idl +++ b/tools/profiler/gecko/nsIProfiler.idl @@ -62,6 +62,9 @@ interface nsIProfiler : nsISupports [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. */ diff --git a/tools/profiler/gecko/nsProfiler.cpp b/tools/profiler/gecko/nsProfiler.cpp index f9d9c540525b..f55b1ed58178 100644 --- a/tools/profiler/gecko/nsProfiler.cpp +++ b/tools/profiler/gecko/nsProfiler.cpp @@ -30,6 +30,7 @@ #include "nsString.h" #include "nsThreadUtils.h" #include "shared-libraries.h" +#include "zlib.h" #include #include @@ -372,6 +373,100 @@ nsProfiler::GetProfileDataAsArrayBuffer(double aSinceTime, JSContext* aCx, 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::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 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,