Skip to content

Fireperf: use the new class FrameMetricsRecorder in AppStateMonitor and FragmentStateMonitor #3683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
May 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
a5bab3d
Add tests for `snapshot()`
leotianlizhan Apr 20, 2022
207a203
Remove all null and use Optional instead
leotianlizhan Apr 22, 2022
994ee80
snapshot uses FrameMetricsCalculator, make snapshot private, make te…
leotianlizhan Apr 26, 2022
f3ec1a0
remove equals
leotianlizhan Apr 26, 2022
02fa988
use FrameMetricsRecorder in AppStateMonitor and FragmentStateMonitor.…
leotianlizhan Apr 22, 2022
5e745d1
undereference when activity is destroyed
leotianlizhan Apr 25, 2022
f0c5110
cbetter javadoc and comments
leotianlizhan Apr 26, 2022
ae95028
use new api startFragment
leotianlizhan Apr 27, 2022
29f1fd1
fix 1 test
leotianlizhan Apr 27, 2022
17cd156
remove test that is already covered in FrameMetricsRecorderTest
leotianlizhan Apr 27, 2022
b27349d
fix conflict
leotianlizhan Apr 27, 2022
85ed9a4
fix mistake when rebasing
leotianlizhan Apr 27, 2022
877d37b
Update tests for Fragment State Monitor tests.
visumickey Apr 28, 2022
2c7eea1
Update tests to be more relevant and accurate.
visumickey May 2, 2022
ed07861
Update the Google Java Format!
visumickey May 2, 2022
bd254d0
Make the mock fragment class package-private.
visumickey May 2, 2022
b405eec
Remove the redundant Assert include.
visumickey May 2, 2022
f693a23
Fix the failing unit tests.
visumickey May 3, 2022
59031eb
Fix the failing tests since onActivityCreated need not be called.
visumickey May 3, 2022
6246b4f
Revert the test change.
visumickey May 3, 2022
fa9616b
Start frame recording when the performance monitoring is enabled at r…
visumickey May 3, 2022
30439a0
Enable screen trace creation only when the app goes back to foregroun…
visumickey May 3, 2022
7946ac5
Add the missing static import.
visumickey May 3, 2022
63ad346
Fix the failing test by adding all the activity lifecycle events.
visumickey May 3, 2022
252490b
Fix the failing unit tests.
visumickey May 3, 2022
69aa698
Make the state of Fragment State Monitor and Metrics Recorder visible…
visumickey May 3, 2022
14ea1fc
Fix the failing fragment monitor tests.
visumickey May 3, 2022
857d9f2
Fix the failing tests.
visumickey May 3, 2022
f1e6fbf
Address some of the github comments.
visumickey May 3, 2022
fc74585
Address few more comments.
visumickey May 3, 2022
d4aba4f
Unify the mocks in a single group in unit tests.
visumickey May 3, 2022
c02d236
Move away from doReturn().when() and use when().thenReturn() as recom…
visumickey May 3, 2022
abc8dd3
Revert back one method conversion from Mockito.when to Mockito.doThen.
visumickey May 3, 2022
630843e
Start frame recording when the activity is not recorded already!
visumickey May 3, 2022
79cd01c
Move the FrameMetricsAggregator class reference to Frame Metrics Reco…
visumickey May 4, 2022
23d4663
Static import PerfFrameMetrics.
visumickey May 4, 2022
09a4fb1
Keep style gods happy!
visumickey May 4, 2022
1a62a8e
Address the last set of comments.
visumickey May 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.core.app.FrameMetricsAggregator;
import androidx.fragment.app.FragmentActivity;
import com.google.android.gms.common.util.VisibleForTesting;
import com.google.firebase.perf.config.ConfigResolver;
import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.metrics.FrameMetricsCalculator;
import com.google.firebase.perf.metrics.FrameMetricsCalculator.PerfFrameMetrics;
import com.google.firebase.perf.metrics.Trace;
import com.google.firebase.perf.session.SessionManager;
import com.google.firebase.perf.transport.TransportManager;
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Constants;
import com.google.firebase.perf.util.Constants.CounterNames;
import com.google.firebase.perf.util.Optional;
import com.google.firebase.perf.util.ScreenTraceUtil;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.v1.ApplicationProcessState;
Expand All @@ -49,13 +49,16 @@
public class AppStateMonitor implements ActivityLifecycleCallbacks {

private static final AndroidLogger logger = AndroidLogger.getInstance();
private static final String FRAME_METRICS_AGGREGATOR_CLASSNAME =
"androidx.core.app.FrameMetricsAggregator";

private static volatile AppStateMonitor instance;
private static boolean hasFrameMetricsAggregator = false;

private final WeakHashMap<Activity, Boolean> activityToResumedMap = new WeakHashMap<>();
private final WeakHashMap<Activity, FrameMetricsRecorder> activityToRecorderMap =
new WeakHashMap<>();

// Map for holding the fragment state monitor to remove receiving the fragment state callbacks
private final WeakHashMap<Activity, FragmentStateMonitor> activityToFragmentStateMonitorMap =
new WeakHashMap<>();
private final WeakHashMap<Activity, Trace> activityToScreenTraceMap = new WeakHashMap<>();
private final Map<String, Long> metricToCountMap = new HashMap<>();
private final Set<WeakReference<AppStateCallback>> appStateSubscribers = new HashSet<>();
Expand All @@ -67,8 +70,7 @@ public class AppStateMonitor implements ActivityLifecycleCallbacks {
private final TransportManager transportManager;
private final ConfigResolver configResolver;
private final Clock clock;

private FrameMetricsAggregator frameMetricsAggregator;
private final boolean screenPerformanceRecordingSupported;

private Timer resumeTime; // The time app comes to foreground
private Timer stopTime; // The time app goes to background
Expand All @@ -94,18 +96,19 @@ public static AppStateMonitor getInstance() {
transportManager,
clock,
ConfigResolver.getInstance(),
hasFrameMetricsAggregatorClass() ? new FrameMetricsAggregator() : null);
isScreenPerformanceRecordingSupported());
}

@VisibleForTesting
AppStateMonitor(
TransportManager transportManager,
Clock clock,
ConfigResolver configResolver,
FrameMetricsAggregator frameMetricsAggregator) {
boolean screenPerformanceRecordingSupported) {
this.transportManager = transportManager;
this.clock = clock;
this.configResolver = configResolver;
this.frameMetricsAggregator = frameMetricsAggregator;
this.screenPerformanceRecordingSupported = screenPerformanceRecordingSupported;
}

public synchronized void registerActivityLifecycleCallbacks(Context context) {
Expand Down Expand Up @@ -149,28 +152,64 @@ public void incrementTsnsCount(int value) {
tsnsCount.addAndGet(value);
}

@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
// Starts tracking the frame metrics for an activity.
private void startFrameMonitoring(Activity activity) {
if (isScreenTraceSupported() && configResolver.isPerformanceMonitoringEnabled()) {
FrameMetricsRecorder recorder = new FrameMetricsRecorder(activity);
activityToRecorderMap.put(activity, recorder);
if (activity instanceof FragmentActivity) {
FragmentStateMonitor fragmentStateMonitor =
new FragmentStateMonitor(clock, transportManager, this, recorder);
activityToFragmentStateMonitorMap.put(activity, fragmentStateMonitor);
FragmentActivity fragmentActivity = (FragmentActivity) activity;
fragmentActivity
.getSupportFragmentManager()
.registerFragmentLifecycleCallbacks(
new FragmentStateMonitor(clock, transportManager, this, frameMetricsAggregator),
true);
.registerFragmentLifecycleCallbacks(fragmentStateMonitor, true);
}
}
}

@Override
public void onActivityDestroyed(Activity activity) {}
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
startFrameMonitoring(activity);
}

@Override
public synchronized void onActivityStarted(Activity activity) {}
public void onActivityDestroyed(Activity activity) {
// Dereference FrameMetricsRecorder from the map because it holds an Activity reference
activityToRecorderMap.remove(activity);
// Dereference FragmentStateMonitor because it holds a FrameMetricsRecorder reference
if (activityToFragmentStateMonitorMap.containsKey(activity)) {
FragmentActivity fragmentActivity = (FragmentActivity) activity;
fragmentActivity
.getSupportFragmentManager()
.unregisterFragmentLifecycleCallbacks(activityToFragmentStateMonitorMap.remove(activity));
}
}

@Override
public synchronized void onActivityStarted(Activity activity) {
if (isScreenTraceSupported() && configResolver.isPerformanceMonitoringEnabled()) {
if (!activityToRecorderMap.containsKey(activity)) {
// If performance monitoring is disabled at start and enabled at runtime, start monitoring
// the activity as the app comes to foreground.
startFrameMonitoring(activity);
}
// Starts recording frame metrics for this activity.
activityToRecorderMap.get(activity).start();
// Start the Trace
Trace screenTrace = new Trace(getScreenTraceName(activity), transportManager, clock, this);
screenTrace.start();
activityToScreenTraceMap.put(activity, screenTrace);
}
}

@Override
public synchronized void onActivityStopped(Activity activity) {
if (isScreenTraceSupported()) {
sendScreenTrace(activity);
}

// Last activity has its onActivityStopped called, the app goes to background.
if (activityToResumedMap.containsKey(activity)) {
activityToResumedMap.remove(activity);
Expand Down Expand Up @@ -211,20 +250,6 @@ public synchronized void onActivityResumed(Activity activity) {
// current activity was paused then resumed without onStop, for example by an AlertDialog
activityToResumedMap.put(activity, true);
}

// Screen trace is after session update so the sessionId is not added twice to the Trace
if (isScreenTraceSupported() && configResolver.isPerformanceMonitoringEnabled()) {
// Starts recording frame metrics for this activity.
/**
* TODO: Only add activities that are hardware acceleration enabled so that calling {@link
* FrameMetricsAggregator#remove(Activity)} will not throw exceptions.
*/
frameMetricsAggregator.add(activity);
// Start the Trace
Trace screenTrace = new Trace(getScreenTraceName(activity), transportManager, clock, this);
screenTrace.start();
activityToScreenTraceMap.put(activity, screenTrace);
}
}

/** Returns if this is the cold start of the app. */
Expand Down Expand Up @@ -315,54 +340,24 @@ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
@Override
public void onActivityPaused(Activity activity) {}

/** Stops screen trace right after onPause because of b/210055697 */
@Override
public void onActivityPostPaused(@NonNull Activity activity) {
if (isScreenTraceSupported()) {
sendScreenTrace(activity);
}
}

/**
* Send screen trace. If hardware acceleration is not enabled, all frame metrics will be zero and
* the trace will not be sent.
* Sends the screen trace for the provided activity.
*
* @param activity activity object.
*/
private void sendScreenTrace(Activity activity) {
if (!activityToScreenTraceMap.containsKey(activity)) {
return;
}
Trace screenTrace = activityToScreenTraceMap.get(activity);
if (screenTrace == null) {
return;
}
activityToScreenTraceMap.remove(activity);

int totalFrames = 0;
int slowFrames = 0;
int frozenFrames = 0;
/**
* Resets the metrics data and returns the currently-collected metrics. Note that {@link
* FrameMetricsAggregator#reset()} will not stop recording for the activity. The reason of using
* {@link FrameMetricsAggregator#reset()} is that {@link
* FrameMetricsAggregator#remove(Activity)} will throw exceptions for hardware acceleration
* disabled activities.
*/
try {
frameMetricsAggregator.remove(activity);
} catch (IllegalArgumentException ignored) {
logger.debug("View not hardware accelerated. Unable to collect screen trace.");
}
FrameMetricsCalculator.PerfFrameMetrics perfFrameMetrics =
FrameMetricsCalculator.calculateFrameMetrics(frameMetricsAggregator.reset());
if (perfFrameMetrics.getTotalFrames() == 0
&& perfFrameMetrics.getSlowFrames() == 0
&& perfFrameMetrics.getFrozenFrames() == 0) {
// All metrics are zero, no need to send screen trace.
Optional<PerfFrameMetrics> perfFrameMetrics = activityToRecorderMap.get(activity).stop();
if (!perfFrameMetrics.isAvailable()) {
logger.warn("Failed to record frame data for %s.", activity.getClass().getSimpleName());
return;
}
ScreenTraceUtil.addFrameCounters(screenTrace, perfFrameMetrics);
ScreenTraceUtil.addFrameCounters(screenTrace, perfFrameMetrics.get());
// Stop and record trace
screenTrace.stop();
}
Expand Down Expand Up @@ -407,23 +402,16 @@ private void sendSessionLog(String name, Timer startTime, Timer endTime) {
* @return true if supported, false if not.
*/
protected boolean isScreenTraceSupported() {
return hasFrameMetricsAggregator;
return screenPerformanceRecordingSupported;
}

/**
* FrameMetricsAggregator first appears in Android Support Library 26.1.0. Before GMSCore SDK is
* updated to 26.1.0 (b/69954793), there will be ClassNotFoundException. This method is to check
* if FrameMetricsAggregator exists to avoid ClassNotFoundException.
*/
private static boolean hasFrameMetricsAggregatorClass() {
try {
Class<?> initializerClass = Class.forName(FRAME_METRICS_AGGREGATOR_CLASSNAME);
hasFrameMetricsAggregator = true;
return true;
} catch (ClassNotFoundException e) {
hasFrameMetricsAggregator = false;
return false;
}
private static boolean isScreenPerformanceRecordingSupported() {
return FrameMetricsRecorder.isFrameMetricsRecordingSupported();
}

/** An interface to be implemented by subscribers which needs to receive app state update. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,36 @@
package com.google.firebase.perf.application;

import androidx.annotation.NonNull;
import androidx.core.app.FrameMetricsAggregator;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.gms.common.util.VisibleForTesting;
import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.metrics.FrameMetricsCalculator;
import com.google.firebase.perf.metrics.FrameMetricsCalculator.PerfFrameMetrics;
import com.google.firebase.perf.metrics.Trace;
import com.google.firebase.perf.transport.TransportManager;
import com.google.firebase.perf.util.Clock;
import com.google.firebase.perf.util.Constants;
import com.google.firebase.perf.util.Optional;
import com.google.firebase.perf.util.ScreenTraceUtil;
import java.util.WeakHashMap;

public class FragmentStateMonitor extends FragmentManager.FragmentLifecycleCallbacks {
private static final AndroidLogger logger = AndroidLogger.getInstance();
private final WeakHashMap<Fragment, Trace> fragmentToTraceMap = new WeakHashMap<>();
private final WeakHashMap<Fragment, PerfFrameMetrics> fragmentToMetricsMap = new WeakHashMap<>();
private final Clock clock;
private final TransportManager transportManager;
private final AppStateMonitor appStateMonitor;
private final FrameMetricsAggregator frameMetricsAggregator;
private final FrameMetricsRecorder activityFramesRecorder;

public FragmentStateMonitor(
Clock clock,
TransportManager transportManager,
AppStateMonitor appStateMonitor,
FrameMetricsAggregator fma) {
FrameMetricsRecorder recorder) {
this.clock = clock;
this.transportManager = transportManager;
this.appStateMonitor = appStateMonitor;
this.frameMetricsAggregator = fma;
this.activityFramesRecorder = recorder;
}

/**
Expand Down Expand Up @@ -78,10 +76,7 @@ public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f)
Constants.ACTIVITY_ATTRIBUTE_KEY, f.getActivity().getClass().getSimpleName());
}
fragmentToTraceMap.put(f, fragmentTrace);

PerfFrameMetrics perfFrameMetrics =
FrameMetricsCalculator.calculateFrameMetrics(this.frameMetricsAggregator.getMetrics());
fragmentToMetricsMap.put(f, perfFrameMetrics);
activityFramesRecorder.startFragment(f);
}

@Override
Expand All @@ -96,33 +91,18 @@ public void onFragmentPaused(@NonNull FragmentManager fm, @NonNull Fragment f) {

Trace fragmentTrace = fragmentToTraceMap.get(f);
fragmentToTraceMap.remove(f);
PerfFrameMetrics prePerfFrameMetrics = fragmentToMetricsMap.get(f);
fragmentToMetricsMap.remove(f);

PerfFrameMetrics curPerfFrameMetrics =
FrameMetricsCalculator.calculateFrameMetrics(this.frameMetricsAggregator.getMetrics());

int totalFrames = curPerfFrameMetrics.getTotalFrames() - prePerfFrameMetrics.getTotalFrames();
int slowFrames = curPerfFrameMetrics.getSlowFrames() - prePerfFrameMetrics.getSlowFrames();
int frozenFrames =
curPerfFrameMetrics.getFrozenFrames() - prePerfFrameMetrics.getFrozenFrames();

if (totalFrames == 0 && slowFrames == 0 && frozenFrames == 0) {
// All metrics are zero, no need to send screen trace.
Optional<PerfFrameMetrics> frameMetricsData = activityFramesRecorder.stopFragment(f);
if (!frameMetricsData.isAvailable()) {
logger.warn("onFragmentPaused: recorder failed to trace %s", f.getClass().getSimpleName());
return;
}
ScreenTraceUtil.addFrameCounters(
fragmentTrace, new PerfFrameMetrics(totalFrames, slowFrames, frozenFrames));
ScreenTraceUtil.addFrameCounters(fragmentTrace, frameMetricsData.get());
fragmentTrace.stop();
}

@VisibleForTesting
WeakHashMap<Fragment, Trace> getFragmentToTraceMap() {
return fragmentToTraceMap;
}

@VisibleForTesting
WeakHashMap<Fragment, PerfFrameMetrics> getFragmentToMetricsMap() {
return fragmentToMetricsMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,24 @@
*/
public class FrameMetricsRecorder {
private static final AndroidLogger logger = AndroidLogger.getInstance();
private static final String FRAME_METRICS_AGGREGATOR_CLASSNAME =
"androidx.core.app.FrameMetricsAggregator";

private final Activity activity;
private final FrameMetricsAggregator frameMetricsAggregator;
private final Map<Fragment, PerfFrameMetrics> fragmentSnapshotMap;

private boolean isRecording = false;

static boolean isFrameMetricsRecordingSupported() {
try {
Class<?> initializerClass = Class.forName(FRAME_METRICS_AGGREGATOR_CLASSNAME);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}

/**
* Creates a recorder for a specific activity.
*
Expand Down
Loading