Skip to content

Commit

Permalink
feat(perf): add custom screen rendering traces for android (#6588)
Browse files Browse the repository at this point in the history
  • Loading branch information
shamilovtim authored Jan 28, 2023
1 parent 6663dc2 commit 9f2498d
Show file tree
Hide file tree
Showing 9 changed files with 484 additions and 34 deletions.
20 changes: 20 additions & 0 deletions docs/perf/usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ async function customTrace() {
}
```

## Custom screen traces

Record a custom screen rendering trace (slow frames / frozen frames)

```jsx
import perf from '@react-native-firebase/perf';

async function screenTrace() {
// Define & start a screen trace
try {
const trace = await perf().startScrenTrace('FooScreen');
// Stop the trace
await trace.stop();
} catch (e) {
// rejects if iOS or (Android == 8 || Android == 8.1)
// or if hardware acceleration is off
}
}
```

## HTTP Request Tracing

Below illustrates you would measure the latency of a HTTP request.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package io.invertase.firebase.perf;

/**
* Copyright 2021 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import android.app.Activity;
import android.os.Build;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.WindowManager;

import androidx.core.app.FrameMetricsAggregator;

import com.google.firebase.perf.FirebasePerformance;
import com.google.firebase.perf.metrics.Trace;
import com.google.firebase.perf.util.Constants;

/**
* Utility class to capture Screen rendering information (Slow/Frozen frames) for the
* {@code Activity} passed to the constructor {@link io.invertase.firebase.perf.ScreenTrace#ScreenTrace(Activity, String)}.
* <p>
* Learn more at https://firebase.google.com/docs/perf-mon/screen-traces?platform=android.
* <p>
* A slow screen rendering often leads to a UI Jank which creates a bad user experience. Below are
* some tips and references to understand and fix common UI Jank issues:
* - https://developer.android.com/topic/performance/vitals/render.html#fixing_jank
* - https://youtu.be/CaMTIgxCSqU (Why 60fps?)
* - https://youtu.be/HXQhu6qfTVU (Rendering Performance)
* - https://youtu.be/1iaHxmfZGGc (Understanding VSYNC)
* - https://www.youtube.com/playlist?list=PLOU2XLYxmsIKEOXh5TwZEv89aofHzNCiu (Android Performance Patterns)
* <p>
* References:
* - Fireperf Source Code
*/
public class ScreenTrace {

private static final String TAG = "RNFirebasePerf";
private static final String FRAME_METRICS_AGGREGATOR_CLASSNAME =
"androidx.core.app.FrameMetricsAggregator";

private final Activity activity;
private final String traceName;

private final FrameMetricsAggregator frameMetricsAggregator;
private Trace perfScreenTrace;

/**
* Default constructor for this class.
*
* @param activity for which the screen traces should be recorded.
* @param tag used as an identifier for the name to be used to log screen rendering
* information (like "MyFancyScreen").
* @implNote It requires hardware acceleration to be on or it throws.
*/
public ScreenTrace(Activity activity, String tag) throws IllegalStateException {
this.activity = activity;

// We don't care about adding the activity name to the trace name
// because RN doesn't care about activities
this.traceName = tag;

boolean isScreenTraceSupported = checkScreenTraceSupport(activity);

if (!isScreenTraceSupported) {
throw new IllegalStateException("Device does not support screen traces. Hardware acceleration must be enabled and Android must not be 8.0 or 8.1.");
}

frameMetricsAggregator = new FrameMetricsAggregator();
}

// region Public APIs

/**
* Starts recording the frame metrics for the screen traces.
*/
public void recordScreenTrace() {
Log.d(TAG, "Recording screen trace " + traceName);

frameMetricsAggregator.add(activity);
perfScreenTrace = FirebasePerformance.startTrace(getScreenTraceName());
}

/**
* Stops recording screen traces and dispatches the trace capturing information on %age of
* Slow/Frozen frames.
*
* Inspired by fireperf source.
*/
public void sendScreenTrace() {
if (perfScreenTrace == null) return;

int totalFrames = 0;
int slowFrames = 0;
int frozenFrames = 0;

// Stops recording metrics for this Activity and returns the currently-collected metrics
SparseIntArray[] arr = frameMetricsAggregator.reset();

if (arr != null) {
SparseIntArray frameTimes = arr[FrameMetricsAggregator.TOTAL_INDEX];

if (frameTimes != null) {
for (int i = 0; i < frameTimes.size(); i++) {
int frameTime = frameTimes.keyAt(i);
int numFrames = frameTimes.valueAt(i);

totalFrames += numFrames;

if (frameTime > Constants.FROZEN_FRAME_TIME) {
// Frozen frames mean the app appear frozen. The recommended thresholds is 700ms
frozenFrames += numFrames;
}

if (frameTime > Constants.SLOW_FRAME_TIME) {
// Slow frames are anything above 16ms (i.e. 60 frames/second)
slowFrames += numFrames;
}
}
}
}

// Only incrementMetric if corresponding metric is non-zero.
if (totalFrames > 0) {
perfScreenTrace.putMetric(Constants.CounterNames.FRAMES_TOTAL.toString(), totalFrames);
}
if (slowFrames > 0) {
perfScreenTrace.putMetric(Constants.CounterNames.FRAMES_SLOW.toString(), slowFrames);
}
if (frozenFrames > 0) {
perfScreenTrace.putMetric(Constants.CounterNames.FRAMES_FROZEN.toString(), frozenFrames);
}

Log.d(TAG, new StringBuilder()
.append("sendScreenTrace ").append(traceName)
.append(", name: ").append(getScreenTraceName())
.append(", total_frames: ").append(totalFrames)
.append(", slow_frames: ").append(slowFrames)
.append(", frozen_frames: ").append(frozenFrames).toString());

// Stop and record trace
perfScreenTrace.stop();
}

// endregion

// region Helper Functions

private static boolean checkScreenTraceSupport(Activity activity) {
boolean isValidSDKVersion = checkSDKVersion();
boolean hasFrameMetricsAggregatorClass = checkFrameMetricsAggregatorClass();
boolean isActivityHardwareAccelerated = activity.getWindow() != null
&& ((activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED) != 0);


boolean supported = isValidSDKVersion && hasFrameMetricsAggregatorClass && isActivityHardwareAccelerated;

Log.d(TAG, new StringBuilder()
.append("isValidSDKVersion: ").append(isValidSDKVersion)
.append("isScreenTraceSupported(").append(activity).append("): ").append(supported)
.append(" [hasFrameMetricsAggregatorClass: ").append(hasFrameMetricsAggregatorClass)
.append(", isActivityHardwareAccelerated: ").append(isActivityHardwareAccelerated).append("]").toString());

return supported;
}

private static boolean checkSDKVersion() {
if (Build.VERSION.SDK_INT == 26 || Build.VERSION.SDK_INT == 27) {
return false;
}

return true;
}

/**
* Inspired by fireperf source.
*/
private static boolean checkFrameMetricsAggregatorClass() {
try {
Class<?> initializerClass = Class.forName(FRAME_METRICS_AGGREGATOR_CLASSNAME);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}

/**
* Inspired by fireperf source.
*/
private String getScreenTraceName() {
return Constants.SCREEN_TRACE_PREFIX + traceName;
}

// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*
*/

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.SparseArray;
Expand All @@ -33,6 +34,7 @@

public class UniversalFirebasePerfModule extends UniversalFirebaseModule {
private static SparseArray<Trace> traces = new SparseArray<>();
private static SparseArray<ScreenTrace> screenTraces = new SparseArray<>();
private static SparseArray<HttpMetric> httpMetrics = new SparseArray<>();

UniversalFirebasePerfModule(Context context, String serviceName) {
Expand All @@ -44,6 +46,7 @@ public void onTearDown() {
super.onTearDown();
traces.clear();
httpMetrics.clear();
screenTraces.clear();
}

@Override
Expand Down Expand Up @@ -100,6 +103,28 @@ Task<Void> stopTrace(int id, Bundle metrics, Bundle attributes) {
});
}

Task<Void> startScreenTrace(Activity activity, int id, String identifier) {
return Tasks.call(
() -> {
ScreenTrace screenTrace = new ScreenTrace(activity, identifier);
screenTrace.recordScreenTrace();
screenTraces.put(id, screenTrace);

return null;
});
}

Task<Void> stopScreenTrace(int id) {
return Tasks.call(
() -> {
ScreenTrace trace = screenTraces.get(id);
trace.sendScreenTrace();
screenTraces.remove(id);

return null;
});
}

Task<Void> startHttpMetric(int id, String url, String httpMethod) {
return Tasks.call(
() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*
*/

import android.app.Activity;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
Expand Down Expand Up @@ -85,6 +86,42 @@ public void stopTrace(int id, ReadableMap traceData, Promise promise) {
});
}

@ReactMethod
public void startScreenTrace(int id, String identifier, Promise promise) {
Activity currentActivity = getCurrentActivity();

// protect against NPEs
if (currentActivity == null) {
promise.resolve(null);
return;
}

module
.startScreenTrace(currentActivity, id, identifier)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
promise.resolve(task.getResult());
} else {
rejectPromiseWithExceptionMap(promise, task.getException());
}
});
}

@ReactMethod
public void stopScreenTrace(int id, Promise promise) {
module
.stopScreenTrace(id)
.addOnCompleteListener(
task -> {
if (task.isSuccessful()) {
promise.resolve(task.getResult());
} else {
rejectPromiseWithExceptionMap(promise, task.getException());
}
});
}

@ReactMethod
public void startHttpMetric(int id, String url, String httpMethod, Promise promise) {
module
Expand Down
13 changes: 13 additions & 0 deletions packages/perf/e2e/perf.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ describe('perf()', function () {
trace._identifier.should.equal('invertase');
trace._started.should.equal(true);
await trace.stop();
trace._stopped.should.equal(true);
});
});

describe('startScreenTrace()', function () {
it('resolves a started instance of a ScreenTrace', async function () {
if (device.getPlatform() === 'android') {
const screenTrace = await firebase.perf().startScreenTrace('FooScreen');
screenTrace.constructor.name.should.be.equal('ScreenTrace');
screenTrace._identifier.should.equal('FooScreen');
await screenTrace.stop();
screenTrace._stopped.should.equal(true);
}
});
});
});
Loading

1 comment on commit 9f2498d

@vercel
Copy link

@vercel vercel bot commented on 9f2498d Jan 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.