Skip to content

Commit c76d022

Browse files
Fireperf: encapsulate FrameMetricsAggregator in a new object called FrameMetricsRecorder (#3665)
* FrameMetricsRecorder and its tests, missing tests for snapshot() * Add tests for `snapshot()` * Remove all null and use Optional instead * fixing some review comments * javadoc, change nullable to any, and change Object to Fragment * log Exception * better javadoc to explain FrameMetricsRecorder must be dereferenced * snapshot uses FrameMetricsCalculator, make snapshot private, make tests follow unit test practices * remove equals * fix Julio's comments * fix Visu's comments * fix log level * clear fragmentSnapshotMap * add test for dangling fragment behavior
1 parent 51cc21f commit c76d022

File tree

3 files changed

+458
-0
lines changed

3 files changed

+458
-0
lines changed
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.perf.application;
16+
17+
import android.app.Activity;
18+
import android.util.SparseIntArray;
19+
import androidx.core.app.FrameMetricsAggregator;
20+
import androidx.fragment.app.Fragment;
21+
import com.google.android.gms.common.util.VisibleForTesting;
22+
import com.google.firebase.perf.logging.AndroidLogger;
23+
import com.google.firebase.perf.metrics.FrameMetricsCalculator;
24+
import com.google.firebase.perf.metrics.FrameMetricsCalculator.PerfFrameMetrics;
25+
import com.google.firebase.perf.util.Optional;
26+
import java.util.HashMap;
27+
import java.util.Map;
28+
29+
/**
30+
* Provides FrameMetrics data from an Activity's Window. Encapsulates FrameMetricsAggregator.
31+
*
32+
* <p>IMPORTANT: each recorder holds a reference to an Activity, so it is very important to
33+
* dereference each recorder at or before its Activity's onDestroy. Similar for Fragments.
34+
*
35+
* <p>Relationship of Fragment recording to Activity recording is like a stopwatch. The stopwatch
36+
* itself is for the Activity, Fragments traces are laps in the stopwatch. Stopwatch can only ever
37+
* be for the Activity by-definition because "frames" refer to a frame for a window, and
38+
* FrameMetrics can only come from an Activity's Window. Fragment traces are laps in the stopwatch,
39+
* because the frame metrics data is still for the Activity window, fragment traces are just
40+
* intervals in the Activity frames recording that has the name "fragment X" attached to them.
41+
*/
42+
public class FrameMetricsRecorder {
43+
private static final AndroidLogger logger = AndroidLogger.getInstance();
44+
45+
private final Activity activity;
46+
private final FrameMetricsAggregator frameMetricsAggregator;
47+
private final Map<Fragment, PerfFrameMetrics> fragmentSnapshotMap;
48+
49+
private boolean isRecording = false;
50+
51+
/**
52+
* Creates a recorder for a specific activity.
53+
*
54+
* @param activity the activity that the recorder is collecting data from.
55+
*/
56+
public FrameMetricsRecorder(Activity activity) {
57+
this(activity, new FrameMetricsAggregator(), new HashMap<>());
58+
}
59+
60+
@VisibleForTesting
61+
FrameMetricsRecorder(
62+
Activity activity,
63+
FrameMetricsAggregator frameMetricsAggregator,
64+
Map<Fragment, PerfFrameMetrics> subTraceMap) {
65+
this.activity = activity;
66+
this.frameMetricsAggregator = frameMetricsAggregator;
67+
this.fragmentSnapshotMap = subTraceMap;
68+
}
69+
70+
/** Starts recording FrameMetrics for the activity window. */
71+
public void start() {
72+
if (isRecording) {
73+
logger.debug(
74+
"FrameMetricsAggregator is already recording %s", activity.getClass().getSimpleName());
75+
return;
76+
}
77+
frameMetricsAggregator.add(activity);
78+
isRecording = true;
79+
}
80+
81+
/**
82+
* Stops recording FrameMetrics for the activity window.
83+
*
84+
* @return FrameMetrics accumulated during the current recording.
85+
*/
86+
public Optional<PerfFrameMetrics> stop() {
87+
if (!isRecording) {
88+
logger.debug("Cannot stop because no recording was started");
89+
return Optional.absent();
90+
}
91+
if (!fragmentSnapshotMap.isEmpty()) {
92+
// Instrumentation stops and still return activity data, but invalidate all dangling fragments
93+
logger.debug(
94+
"Sub-recordings are still ongoing! Sub-recordings should be stopped first before stopping Activity screen trace.");
95+
fragmentSnapshotMap.clear();
96+
}
97+
Optional<PerfFrameMetrics> data = this.snapshot();
98+
try {
99+
frameMetricsAggregator.remove(activity);
100+
} catch (IllegalArgumentException err) {
101+
logger.warn(
102+
"View not hardware accelerated. Unable to collect FrameMetrics. %s", err.toString());
103+
return Optional.absent();
104+
}
105+
frameMetricsAggregator.reset();
106+
isRecording = false;
107+
return data;
108+
}
109+
110+
/**
111+
* Starts a Fragment sub-recording in the current Activity recording. Fragments are sub-recordings
112+
* due to the way frame metrics work: frame metrics can only comes from an activity's window. An
113+
* analogy for sub-recording is a lap in a stopwatch.
114+
*
115+
* @param fragment a Fragment to associate this sub-trace with.
116+
*/
117+
public void startFragment(Fragment fragment) {
118+
if (!isRecording) {
119+
logger.debug("Cannot start sub-recording because FrameMetricsAggregator is not recording");
120+
return;
121+
}
122+
if (fragmentSnapshotMap.containsKey(fragment)) {
123+
logger.debug(
124+
"Cannot start sub-recording because one is already ongoing with the key %s",
125+
fragment.getClass().getSimpleName());
126+
return;
127+
}
128+
Optional<PerfFrameMetrics> snapshot = this.snapshot();
129+
if (!snapshot.isAvailable()) {
130+
logger.debug("startFragment(%s): snapshot() failed", fragment.getClass().getSimpleName());
131+
return;
132+
}
133+
fragmentSnapshotMap.put(fragment, snapshot.get());
134+
}
135+
136+
/**
137+
* Stops the sub-recording associated with the given Fragment. Fragments are sub-recordings due to
138+
* the way frame metrics work: frame metrics can only comes from an activity's window. An analogy
139+
* for sub-recording is a lap in a stopwatch.
140+
*
141+
* @param fragment the Fragment associated with the sub-recording to be stopped.
142+
* @return FrameMetrics accumulated during this sub-recording.
143+
*/
144+
public Optional<PerfFrameMetrics> stopFragment(Fragment fragment) {
145+
if (!isRecording) {
146+
logger.debug("Cannot stop sub-recording because FrameMetricsAggregator is not recording");
147+
return Optional.absent();
148+
}
149+
if (!fragmentSnapshotMap.containsKey(fragment)) {
150+
logger.debug(
151+
"Sub-recording associated with key %s was not started or does not exist",
152+
fragment.getClass().getSimpleName());
153+
return Optional.absent();
154+
}
155+
PerfFrameMetrics snapshotStart = fragmentSnapshotMap.remove(fragment);
156+
Optional<PerfFrameMetrics> snapshotEnd = this.snapshot();
157+
if (!snapshotEnd.isAvailable()) {
158+
logger.debug("stopFragment(%s): snapshot() failed", fragment.getClass().getSimpleName());
159+
return Optional.absent();
160+
}
161+
return Optional.of(snapshotEnd.get().deltaFrameMetricsFromSnapshot(snapshotStart));
162+
}
163+
164+
/**
165+
* Snapshots total frames, slow frames, and frozen frames from SparseIntArray[] recorded by {@link
166+
* FrameMetricsAggregator}.
167+
*
168+
* @return {@link PerfFrameMetrics} at the time of snapshot.
169+
*/
170+
private Optional<PerfFrameMetrics> snapshot() {
171+
if (!isRecording) {
172+
logger.debug("No recording has been started.");
173+
return Optional.absent();
174+
}
175+
SparseIntArray[] arr = this.frameMetricsAggregator.getMetrics();
176+
if (arr == null) {
177+
logger.debug("FrameMetricsAggregator.mMetrics is uninitialized.");
178+
return Optional.absent();
179+
}
180+
SparseIntArray frameTimes = arr[FrameMetricsAggregator.TOTAL_INDEX];
181+
if (frameTimes == null) {
182+
logger.debug("FrameMetricsAggregator.mMetrics[TOTAL_INDEX] is uninitialized.");
183+
return Optional.absent();
184+
}
185+
return Optional.of(FrameMetricsCalculator.calculateFrameMetrics(arr));
186+
}
187+
}

firebase-perf/src/main/java/com/google/firebase/perf/metrics/FrameMetricsCalculator.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ public int getSlowFrames() {
4949
public int getTotalFrames() {
5050
return totalFrames;
5151
}
52+
53+
/**
54+
* Subtracts frame-time counts of the argument object (that) from the current object (this).
55+
*
56+
* @param that the subtrahend PerfFrameMetrics object.
57+
* @return difference of this and the argument.
58+
*/
59+
public PerfFrameMetrics deltaFrameMetricsFromSnapshot(PerfFrameMetrics that) {
60+
int newTotalFrames = this.totalFrames - that.getTotalFrames();
61+
int newSlowFrames = this.slowFrames - that.getSlowFrames();
62+
int newFrozenFrames = this.frozenFrames - that.getFrozenFrames();
63+
return new PerfFrameMetrics(newTotalFrames, newSlowFrames, newFrozenFrames);
64+
}
5265
}
5366

5467
/**

0 commit comments

Comments
 (0)