Bug 1666226 - Add GeckoView APIs for starting and stopping the Gecko profiler r=geckoview-reviewers,mstange,agi

***

Differential Revision: https://phabricator.services.mozilla.com/D133404
This commit is contained in:
mleclair 2022-03-23 20:05:07 +00:00
Родитель 8b2ec126dc
Коммит cb0a86aae4
9 изменённых файлов: 220 добавлений и 38 удалений

Просмотреть файл

@ -1772,6 +1772,8 @@ package org.mozilla.geckoview {
method public void addMarker(@NonNull String);
method @Nullable public Double getProfilerTime();
method public boolean isProfilerActive();
method public void startProfiler(@NonNull String[], @NonNull String[]);
method @NonNull public GeckoResult<byte[]> stopProfiler();
}
public abstract class RuntimeSettings implements Parcelable {

Просмотреть файл

@ -0,0 +1,40 @@
package org.mozilla.geckoview.test
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.runner.RunWith
import org.mozilla.geckoview.test.util.UiThreadUtils
import java.util.zip.GZIPInputStream
import org.hamcrest.Matchers.*
import org.json.JSONObject
import org.junit.Test
import java.io.BufferedReader
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
@RunWith(AndroidJUnit4::class)
class ProfilerControllerTest : BaseSessionTest() {
@Test
fun startAndStopProfiler(){
sessionRule.runtime.profilerController.startProfiler(arrayOf<String>(),arrayOf<String>())
val result = sessionRule.runtime.profilerController.stopProfiler()
val byteArray = sessionRule.waitForResult(result)
val head = (byteArray[0].toInt() and 0xff) or (byteArray[1].toInt() shl 8 and 0xff00)
assertThat("Header of byte array should be the same as the GZIP one",
head, equalTo(GZIPInputStream.GZIP_MAGIC))
val profileString = StringBuilder()
val gzipInputStream = GZIPInputStream(ByteArrayInputStream(byteArray))
val bufferedReader = BufferedReader(InputStreamReader(gzipInputStream))
var line = bufferedReader.readLine()
while(line!=null) {
profileString.append(line)
line = bufferedReader.readLine()
}
val json = JSONObject(profileString.toString())
assertThat("profile JSON object must not be empty",
json.length(), greaterThan(0))
}
}

Просмотреть файл

@ -30,6 +30,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.mozilla.gecko.annotation.WrapForJNI;
import org.mozilla.gecko.mozglue.JNIObject;
import org.mozilla.geckoview.GeckoResult;
/**
* Takes samples and adds markers for Java threads for the Gecko profiler.
@ -773,6 +774,22 @@ public class GeckoJavaSampler {
}
}
@WrapForJNI(dispatchTo = "gecko", stubName = "StartProfiler")
private static native void startProfilerNative(String[] aFilters, String[] aFeaturesArr);
@WrapForJNI(dispatchTo = "gecko", stubName = "StopProfiler")
private static native void stopProfilerNative(GeckoResult<byte[]> aResult);
public static void startProfiler(final String[] aFilters, final String[] aFeaturesArr) {
startProfilerNative(aFilters, aFeaturesArr);
}
public static GeckoResult<byte[]> stopProfiler() {
final GeckoResult<byte[]> result = new GeckoResult<byte[]>();
stopProfilerNative(result);
return result;
}
/** Returns the device brand and model as a string. */
@WrapForJNI
public static String getDeviceInformation() {

Просмотреть файл

@ -154,4 +154,29 @@ public class ProfilerController {
public void addMarker(@NonNull final String aMarkerName) {
addMarker(aMarkerName, null, null, null);
}
/**
* Start the Gecko profiler with the given settings. This is used by embedders which want to
* control the profiler from the embedding app. This allows them to provide an easier access point
* to profiling, as an alternative to the traditional way of using a desktop Firefox instance
* connected via USB + adb.
*
* @param aFilters The list of threads to profile, as an array of string of thread names filters.
* Each filter is used as a case-insensitive substring match against the actual thread names.
* @param aFeaturesArr The list of profiler features to enable for profiling, as a string array.
*/
public void startProfiler(
@NonNull final String[] aFilters, @NonNull final String[] aFeaturesArr) {
GeckoJavaSampler.startProfiler(aFilters, aFeaturesArr);
}
/**
* Stop the profiler and capture the recorded profile. This method is asynchronous.
*
* @return GeckoResult for the captured profile. The profile is returned as a byte[] buffer
* containing a gzip-compressed payload (with gzip header) of the profile JSON.
*/
public @NonNull GeckoResult<byte[]> stopProfiler() {
return GeckoJavaSampler.stopProfiler();
}
}

Просмотреть файл

@ -1144,4 +1144,4 @@ to allow adding gecko profiler markers.
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport(android.content.Context,android.os.Bundle,java.lang.String)
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: f4393a16a61b77f77a91788ac6e2bcf366a113b6
[api-version]: 24b60ce96bd68c81a55110bd1a3c57442dd8c65f

Просмотреть файл

@ -40,6 +40,7 @@
#include "ProfilerIOInterposeObserver.h"
#include "ProfilerParent.h"
#include "ProfilerRustBindings.h"
#include "mozilla/MozPromise.h"
#include "shared-libraries.h"
#include "VTuneProfiler.h"
@ -100,6 +101,7 @@
#include <type_traits>
#if defined(GP_OS_android)
# include "JavaExceptions.h"
# include "mozilla/java/GeckoJavaSamplerNatives.h"
# include "mozilla/jni/Refs.h"
#endif
@ -237,6 +239,59 @@ class GeckoJavaSampler
}
return profiler_time();
};
static void JavaStringArrayToCharArray(jni::ObjectArray::Param& aJavaArray,
Vector<const char*>& aCharArray,
JNIEnv* aJni) {
int arraySize = aJavaArray->Length();
for (int i = 0; i < arraySize; i++) {
jstring javaString =
(jstring)(aJni->GetObjectArrayElement(aJavaArray.Get(), i));
const char* filterString = aJni->GetStringUTFChars(javaString, 0);
// These strings are leaked. FIXME
MOZ_RELEASE_ASSERT(aCharArray.append(&filterString, 0));
}
}
static void StartProfiler(jni::ObjectArray::Param aFiltersArray,
jni::ObjectArray::Param aFeaturesArray) {
JNIEnv* jni = jni::GetEnvForThread();
Vector<const char*> filtersTemp;
Vector<const char*> featureStringArray;
JavaStringArrayToCharArray(aFiltersArray, filtersTemp, jni);
JavaStringArrayToCharArray(aFeaturesArray, featureStringArray, jni);
uint32_t features = 0;
features = ParseFeaturesFromStringArray(featureStringArray.begin(),
featureStringArray.length());
// 128 * 1024 * 1024 is the entries preset that is given in
// devtools/client/performance-new/popup/background.jsm.js
profiler_start(PowerOfTwo32(128 * 1024 * 1024), 5.0, features,
filtersTemp.begin(), filtersTemp.length(), 0, Nothing());
}
static void StopProfiler(jni::Object::Param aGeckoResult) {
auto result = java::GeckoResult::LocalRef(aGeckoResult);
profiler_pause();
nsCOMPtr<nsIProfiler> nsProfiler(
do_GetService("@mozilla.org/tools/profiler;1"));
nsProfiler->GetProfileDataAsGzippedArrayBufferAndroid(0)->Then(
GetMainThreadSerialEventTarget(), __func__,
[result](FallibleTArray<uint8_t> compressedProfile) {
result->Complete(jni::ByteArray::New(
reinterpret_cast<const int8_t*>(compressedProfile.Elements()),
compressedProfile.Length()));
},
[result](nsresult aRv) {
char errorString[9];
sprintf(errorString, "%08x", aRv);
result->CompleteExceptionally(
mozilla::java::sdk::IllegalStateException::New(errorString)
.Cast<jni::Throwable>());
});
}
};
#endif

Просмотреть файл

@ -9,6 +9,7 @@
#include "mozilla/Maybe.h"
#include "nsTArrayForwardDeclare.h"
#include "nsStringFwd.h"
#include "mozilla/MozPromise.h"
%}
[ref] native nsCString(const nsCString);
@ -190,4 +191,8 @@ interface nsIProfiler : nsISupports
* Dump the collected profile to a file.
*/
void dumpProfileToFile(in string aFilename);
%{C++
virtual RefPtr<mozilla::MozPromise<FallibleTArray<uint8_t>, nsresult, true>> GetProfileDataAsGzippedArrayBufferAndroid(double aSinceTime) = 0;
%}
};

Просмотреть файл

@ -463,6 +463,47 @@ nsProfiler::GetProfileDataAsArrayBuffer(double aSinceTime, JSContext* aCx,
return NS_OK;
}
nsresult CompressString(const nsCString& aString,
FallibleTArray<uint8_t>& aOutBuff) {
// 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(aString.Length());
if (!aOutBuff.SetLength(outSize, fallible)) {
return NS_ERROR_OUT_OF_MEMORY;
}
int zerr;
z_stream stream;
stream.zalloc = nullptr;
stream.zfree = nullptr;
stream.opaque = nullptr;
stream.next_out = (Bytef*)aOutBuff.Elements();
stream.avail_out = aOutBuff.Length();
stream.next_in = (z_const Bytef*)aString.Data();
stream.avail_in = aString.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) {
return NS_ERROR_FAILURE;
}
zerr = deflate(&stream, Z_FINISH);
outSize = stream.total_out;
deflateEnd(&stream);
if (zerr != Z_STREAM_END) {
return NS_ERROR_FAILURE;
}
aOutBuff.TruncateLength(outSize);
return NS_OK;
}
NS_IMETHODIMP
nsProfiler::GetProfileDataAsGzippedArrayBuffer(double aSinceTime,
JSContext* aCx,
@ -500,47 +541,14 @@ nsProfiler::GetProfileDataAsGzippedArrayBuffer(double aSinceTime,
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);
nsresult result = CompressString(aResult, outBuff);
if (result != NS_OK) {
promise->MaybeReject(result);
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());
@ -986,6 +994,31 @@ void nsProfiler::GatheredOOPProfile(base::ProcessId aChildPid,
RestartGatheringTimer();
}
RefPtr<nsProfiler::GatheringPromiseAndroid>
nsProfiler::GetProfileDataAsGzippedArrayBufferAndroid(double aSinceTime) {
MOZ_ASSERT(NS_IsMainThread());
if (!profiler_is_active()) {
return GatheringPromiseAndroid::CreateAndReject(NS_ERROR_FAILURE, __func__);
}
return StartGathering(aSinceTime)
->Then(
GetMainThreadSerialEventTarget(), __func__,
[](const nsCString& profileResult) {
FallibleTArray<uint8_t> outBuff;
nsresult result = CompressString(profileResult, outBuff);
if (result != NS_OK) {
return GatheringPromiseAndroid::CreateAndReject(result, __func__);
}
return GatheringPromiseAndroid::CreateAndResolve(std::move(outBuff),
__func__);
},
[](nsresult aRv) {
return GatheringPromiseAndroid::CreateAndReject(aRv, __func__);
});
}
RefPtr<nsProfiler::GatheringPromise> nsProfiler::StartGathering(
double aSinceTime) {
MOZ_RELEASE_ASSERT(NS_IsMainThread());

Просмотреть файл

@ -38,6 +38,8 @@ class nsProfiler final : public nsIProfiler {
private:
~nsProfiler();
typedef mozilla::MozPromise<FallibleTArray<uint8_t>, nsresult, true>
GatheringPromiseAndroid;
typedef mozilla::MozPromise<nsCString, nsresult, false> GatheringPromise;
typedef mozilla::MozPromise<mozilla::SymbolTable, nsresult, true>
SymbolTablePromise;
@ -53,6 +55,9 @@ class nsProfiler final : public nsIProfiler {
RefPtr<SymbolTablePromise> GetSymbolTableMozPromise(
const nsACString& aDebugPath, const nsACString& aBreakpadID);
RefPtr<nsProfiler::GatheringPromiseAndroid>
GetProfileDataAsGzippedArrayBufferAndroid(double aSinceTime) override;
struct ExitProfile {
nsCString mJSON;
uint64_t mBufferPositionAtGatherTime;