Skip to content

Commit 2fed9e4

Browse files
Fireperf fragments: trace creation and adding custom attributes (#3575)
* implementation * test * gjf * ebugfix * copyright * add tests * long name test * fix test * change error to warn message
1 parent 6a42873 commit 2fed9e4

File tree

4 files changed

+241
-18
lines changed

4 files changed

+241
-18
lines changed

firebase-perf/src/main/java/com/google/firebase/perf/application/AppStateMonitor.java

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,22 @@ public static AppStateMonitor getInstance() {
9090
}
9191

9292
AppStateMonitor(TransportManager transportManager, Clock clock) {
93+
this(
94+
transportManager,
95+
clock,
96+
ConfigResolver.getInstance(),
97+
hasFrameMetricsAggregatorClass() ? new FrameMetricsAggregator() : null);
98+
}
99+
100+
AppStateMonitor(
101+
TransportManager transportManager,
102+
Clock clock,
103+
ConfigResolver configResolver,
104+
FrameMetricsAggregator frameMetricsAggregator) {
93105
this.transportManager = transportManager;
94106
this.clock = clock;
95-
configResolver = ConfigResolver.getInstance();
96-
hasFrameMetricsAggregator = hasFrameMetricsAggregatorClass();
97-
if (hasFrameMetricsAggregator) {
98-
frameMetricsAggregator = new FrameMetricsAggregator();
99-
}
107+
this.configResolver = configResolver;
108+
this.frameMetricsAggregator = frameMetricsAggregator;
100109
}
101110

102111
public synchronized void registerActivityLifecycleCallbacks(Context context) {
@@ -142,14 +151,13 @@ public void incrementTsnsCount(int value) {
142151

143152
@Override
144153
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
145-
if (isScreenTraceSupported(activity) && configResolver.isPerformanceMonitoringEnabled()) {
154+
if (isScreenTraceSupported() && configResolver.isPerformanceMonitoringEnabled()) {
146155
if (activity instanceof FragmentActivity) {
147156
FragmentActivity fragmentActivity = (FragmentActivity) activity;
148157
fragmentActivity
149158
.getSupportFragmentManager()
150159
.registerFragmentLifecycleCallbacks(
151-
new FragmentStateMonitor(
152-
fragmentActivity, clock, transportManager, this, frameMetricsAggregator),
160+
new FragmentStateMonitor(clock, transportManager, this, frameMetricsAggregator),
153161
true);
154162
}
155163
}
@@ -160,7 +168,7 @@ public void onActivityDestroyed(Activity activity) {}
160168

161169
@Override
162170
public synchronized void onActivityStarted(Activity activity) {
163-
if (isScreenTraceSupported(activity) && configResolver.isPerformanceMonitoringEnabled()) {
171+
if (isScreenTraceSupported() && configResolver.isPerformanceMonitoringEnabled()) {
164172
// Starts recording frame metrics for this activity.
165173
/**
166174
* TODO: Only add activities that are hardware acceleration enabled so that calling {@link
@@ -307,7 +315,7 @@ public void onActivityPaused(Activity activity) {}
307315
/** Stops screen trace right after onPause because of b/210055697 */
308316
@Override
309317
public void onActivityPostPaused(@NonNull Activity activity) {
310-
if (isScreenTraceSupported(activity)) {
318+
if (isScreenTraceSupported()) {
311319
sendScreenTrace(activity);
312320
}
313321
}
@@ -428,10 +436,9 @@ private void sendSessionLog(String name, Timer startTime, Timer endTime) {
428436
/**
429437
* Only send screen trace if FrameMetricsAggregator exists.
430438
*
431-
* @param activity The Activity for which we're monitoring the screen rendering performance.
432439
* @return true if supported, false if not.
433440
*/
434-
private boolean isScreenTraceSupported(Activity activity) {
441+
protected boolean isScreenTraceSupported() {
435442
return hasFrameMetricsAggregator;
436443
}
437444

@@ -440,7 +447,7 @@ private boolean isScreenTraceSupported(Activity activity) {
440447
* updated to 26.1.0 (b/69954793), there will be ClassNotFoundException. This method is to check
441448
* if FrameMetricsAggregator exists to avoid ClassNotFoundException.
442449
*/
443-
private boolean hasFrameMetricsAggregatorClass() {
450+
private static boolean hasFrameMetricsAggregatorClass() {
444451
try {
445452
Class<?> initializerClass = Class.forName(FRAME_METRICS_AGGREGATOR_CLASSNAME);
446453
return true;

firebase-perf/src/main/java/com/google/firebase/perf/application/FragmentStateMonitor.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,28 +17,28 @@
1717
import androidx.annotation.NonNull;
1818
import androidx.core.app.FrameMetricsAggregator;
1919
import androidx.fragment.app.Fragment;
20-
import androidx.fragment.app.FragmentActivity;
2120
import androidx.fragment.app.FragmentManager;
21+
import com.google.android.gms.common.util.VisibleForTesting;
2222
import com.google.firebase.perf.logging.AndroidLogger;
23+
import com.google.firebase.perf.metrics.Trace;
2324
import com.google.firebase.perf.transport.TransportManager;
2425
import com.google.firebase.perf.util.Clock;
2526
import com.google.firebase.perf.util.Constants;
27+
import java.util.WeakHashMap;
2628

2729
public class FragmentStateMonitor extends FragmentManager.FragmentLifecycleCallbacks {
2830
private static final AndroidLogger logger = AndroidLogger.getInstance();
29-
private final FragmentActivity activity;
31+
private final WeakHashMap<Fragment, Trace> fragmentToTraceMap = new WeakHashMap<>();
3032
private final Clock clock;
3133
private final TransportManager transportManager;
3234
private final AppStateMonitor appStateMonitor;
3335
private final FrameMetricsAggregator frameMetricsAggregator;
3436

3537
public FragmentStateMonitor(
36-
FragmentActivity activity,
3738
Clock clock,
3839
TransportManager transportManager,
3940
AppStateMonitor appStateMonitor,
4041
FrameMetricsAggregator fma) {
41-
this.activity = activity;
4242
this.clock = clock;
4343
this.transportManager = transportManager;
4444
this.appStateMonitor = appStateMonitor;
@@ -51,7 +51,7 @@ public FragmentStateMonitor(
5151
* @param fragment fragment object.
5252
* @return Fragment screen trace name.
5353
*/
54-
public static String getFragmentScreenTraceName(Fragment fragment) {
54+
public String getFragmentScreenTraceName(Fragment fragment) {
5555
return Constants.SCREEN_TRACE_PREFIX + fragment.getClass().getSimpleName();
5656
}
5757

@@ -60,12 +60,44 @@ public void onFragmentResumed(@NonNull FragmentManager fm, @NonNull Fragment f)
6060
super.onFragmentResumed(fm, f);
6161
// Start Fragment screen trace
6262
logger.debug("FragmentMonitor %s.onFragmentResumed", f.getClass().getSimpleName());
63+
Trace fragmentTrace =
64+
new Trace(getFragmentScreenTraceName(f), transportManager, clock, appStateMonitor);
65+
fragmentTrace.start();
66+
67+
if (f.getParentFragment() != null) {
68+
fragmentTrace.putAttribute(
69+
Constants.PARENT_FRAGMENT_ATTRIBUTE_KEY,
70+
f.getParentFragment().getClass().getSimpleName());
71+
}
72+
if (f.getActivity() != null) {
73+
fragmentTrace.putAttribute(
74+
Constants.ACTIVITY_ATTRIBUTE_KEY, f.getActivity().getClass().getSimpleName());
75+
}
76+
77+
fragmentToTraceMap.put(f, fragmentTrace);
6378
}
6479

6580
@Override
6681
public void onFragmentPaused(@NonNull FragmentManager fm, @NonNull Fragment f) {
6782
super.onFragmentPaused(fm, f);
6883
// Stop Fragment screen trace
6984
logger.debug("FragmentMonitor %s.onFragmentPaused ", f.getClass().getSimpleName());
85+
if (!fragmentToTraceMap.containsKey(f)) {
86+
logger.warn(
87+
"FragmentMonitor: missed a fragment trace from %s", f.getClass().getSimpleName());
88+
return;
89+
}
90+
91+
Trace fragmentTrace = fragmentToTraceMap.get(f);
92+
fragmentToTraceMap.remove(f);
93+
94+
// TODO: Add frame metrics
95+
96+
fragmentTrace.stop();
97+
}
98+
99+
@VisibleForTesting
100+
WeakHashMap<Fragment, Trace> getFragmentToTraceMap() {
101+
return fragmentToTraceMap;
70102
}
71103
}

firebase-perf/src/main/java/com/google/firebase/perf/util/Constants.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ public class Constants {
5252
/** Screen trace name is the prefix plus activity class name. */
5353
public static final String SCREEN_TRACE_PREFIX = "_st_";
5454

55+
/** Attribute key for the parent fragment of a fragment screen trace. */
56+
public static final String PARENT_FRAGMENT_ATTRIBUTE_KEY = "Parent_fragment";
57+
58+
/** Attribute key for the hosting activity of a fragment screen trace. */
59+
public static final String ACTIVITY_ATTRIBUTE_KEY = "Hosting_activity";
60+
5561
/** frames longer than 16 ms are slow frames */
5662
public static final int SLOW_FRAME_TIME = 16;
5763

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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 static org.mockito.ArgumentMatchers.any;
18+
import static org.mockito.ArgumentMatchers.nullable;
19+
import static org.mockito.Mockito.doAnswer;
20+
import static org.mockito.Mockito.doReturn;
21+
import static org.mockito.Mockito.spy;
22+
import static org.mockito.Mockito.times;
23+
import static org.mockito.Mockito.verify;
24+
import static org.mockito.MockitoAnnotations.initMocks;
25+
26+
import android.app.Activity;
27+
import androidx.appcompat.app.AppCompatActivity;
28+
import androidx.core.app.FrameMetricsAggregator;
29+
import androidx.fragment.app.Fragment;
30+
import androidx.fragment.app.FragmentManager;
31+
import com.google.firebase.perf.FirebasePerformanceTestBase;
32+
import com.google.firebase.perf.config.ConfigResolver;
33+
import com.google.firebase.perf.config.DeviceCacheManager;
34+
import com.google.firebase.perf.metrics.Trace;
35+
import com.google.firebase.perf.transport.TransportManager;
36+
import com.google.firebase.perf.util.Clock;
37+
import com.google.firebase.perf.util.Timer;
38+
import com.google.firebase.perf.v1.TraceMetric;
39+
import com.google.testing.timing.FakeDirectExecutorService;
40+
import java.util.WeakHashMap;
41+
import org.junit.Assert;
42+
import org.junit.Before;
43+
import org.junit.Test;
44+
import org.junit.runner.RunWith;
45+
import org.mockito.ArgumentCaptor;
46+
import org.mockito.Captor;
47+
import org.mockito.Mock;
48+
import org.mockito.stubbing.Answer;
49+
import org.robolectric.RobolectricTestRunner;
50+
51+
/** Unit tests for {@link com.google.firebase.perf.application.FragmentStateMonitor}. */
52+
@RunWith(RobolectricTestRunner.class)
53+
public class FragmentStateMonitorTest extends FirebasePerformanceTestBase {
54+
55+
@Mock private Clock clock;
56+
@Mock private Fragment mockFragment;
57+
@Mock private FragmentManager mockFragmentManager;
58+
@Mock private TransportManager mockTransportManager;
59+
@Mock private AppCompatActivity mockActivity;
60+
@Mock private AppCompatActivity mockActivityB;
61+
@Mock private AppStateMonitor appStateMonitor;
62+
@Mock private FrameMetricsAggregator fma;
63+
64+
@Captor private ArgumentCaptor<TraceMetric> argTraceMetric;
65+
66+
private long currentTime = 0;
67+
private static final String longFragmentName =
68+
"_st_NeverGonnaGiveYouUpNeverGonnaLetYouDownNeverGonnaRunAroundAndDesertYouNeverGonnaMakeYouCryNeverGonnaSayGoodbyeNeverGonnaTellALieAndHurtYou";
69+
70+
private Activity activity1;
71+
private ConfigResolver configResolver;
72+
73+
@Before
74+
public void setUp() {
75+
currentTime = 0;
76+
initMocks(this);
77+
doAnswer((Answer<Timer>) invocationOnMock -> new Timer(currentTime)).when(clock).getTime();
78+
79+
DeviceCacheManager.clearInstance();
80+
ConfigResolver.clearInstance();
81+
82+
ConfigResolver configResolver = ConfigResolver.getInstance();
83+
configResolver.setDeviceCacheManager(new DeviceCacheManager(new FakeDirectExecutorService()));
84+
ConfigResolver spyConfigResolver = spy(configResolver);
85+
doReturn(true).when(spyConfigResolver).isPerformanceMonitoringEnabled();
86+
this.configResolver = spyConfigResolver;
87+
}
88+
89+
/************ Trace Creation Tests ****************/
90+
91+
@Test
92+
public void lifecycleCallbacks_logFragmentScreenTrace() {
93+
FragmentStateMonitor monitor =
94+
new FragmentStateMonitor(clock, mockTransportManager, appStateMonitor, fma);
95+
monitor.onFragmentResumed(mockFragmentManager, mockFragment);
96+
verify(mockTransportManager, times(0)).log(any(TraceMetric.class), any());
97+
98+
monitor.onFragmentPaused(mockFragmentManager, mockFragment);
99+
verify(mockTransportManager, times(1)).log(any(TraceMetric.class), any());
100+
101+
monitor.onFragmentResumed(mockFragmentManager, mockFragment);
102+
verify(mockTransportManager, times(1)).log(any(TraceMetric.class), any());
103+
104+
monitor.onFragmentPaused(mockFragmentManager, mockFragment);
105+
verify(mockTransportManager, times(2)).log(any(TraceMetric.class), any());
106+
}
107+
108+
@Test
109+
public void lifecycleCallbacks_cleansUpMap_duringActivityTransitions() {
110+
// Simulate call order of activity + fragment lifecycle events
111+
AppStateMonitor appStateMonitor =
112+
spy(new AppStateMonitor(mockTransportManager, clock, configResolver, fma));
113+
FragmentStateMonitor fragmentMonitor =
114+
new FragmentStateMonitor(clock, mockTransportManager, appStateMonitor, fma);
115+
doReturn(true).when(appStateMonitor).isScreenTraceSupported();
116+
WeakHashMap<Fragment, Trace> map = fragmentMonitor.getFragmentToTraceMap();
117+
// Activity_A onCreate registers FragmentStateMonitor, then:
118+
appStateMonitor.onActivityStarted(mockActivity);
119+
Assert.assertEquals(0, map.size());
120+
appStateMonitor.onActivityResumed(mockActivity);
121+
fragmentMonitor.onFragmentResumed(mockFragmentManager, mockFragment);
122+
Assert.assertEquals(1, map.size());
123+
appStateMonitor.onActivityPaused(mockActivity);
124+
fragmentMonitor.onFragmentPaused(mockFragmentManager, mockFragment);
125+
Assert.assertEquals(0, map.size());
126+
appStateMonitor.onActivityPostPaused(mockActivity);
127+
// Activity_B onCreate registers FragmentStateMonitor, then:
128+
appStateMonitor.onActivityStarted(mockActivityB);
129+
appStateMonitor.onActivityResumed(mockActivityB);
130+
fragmentMonitor.onFragmentResumed(mockFragmentManager, mockFragment);
131+
appStateMonitor.onActivityStopped(mockActivity);
132+
Assert.assertEquals(1, map.size());
133+
}
134+
135+
@Test
136+
public void fragmentTraceCreation_dropsTrace_whenFragmentNameTooLong() {
137+
AppStateMonitor appStateMonitor =
138+
spy(new AppStateMonitor(mockTransportManager, clock, configResolver, fma));
139+
FragmentStateMonitor fragmentMonitor =
140+
spy(new FragmentStateMonitor(clock, mockTransportManager, appStateMonitor, fma));
141+
doReturn(true).when(appStateMonitor).isScreenTraceSupported();
142+
doReturn(longFragmentName)
143+
.when(fragmentMonitor)
144+
.getFragmentScreenTraceName(nullable(Fragment.class));
145+
146+
fragmentMonitor.onFragmentResumed(mockFragmentManager, mockFragment);
147+
verify(mockTransportManager, times(0)).log(any(TraceMetric.class), any());
148+
fragmentMonitor.onFragmentPaused(mockFragmentManager, mockFragment);
149+
verify(mockTransportManager, times(0)).log(any(TraceMetric.class), any());
150+
}
151+
152+
/************ FrameMetrics Collection Tests ****************/
153+
154+
@Test
155+
public void onFragmentPaused_processFrameMetrics_beforeReset() {
156+
// Simulate call order of activity + fragment lifecycle events
157+
AppStateMonitor appStateMonitor =
158+
spy(new AppStateMonitor(mockTransportManager, clock, configResolver, fma));
159+
FragmentStateMonitor fragmentMonitor =
160+
new FragmentStateMonitor(clock, mockTransportManager, appStateMonitor, fma);
161+
doReturn(true).when(appStateMonitor).isScreenTraceSupported();
162+
// Activity_A onCreate registers FragmentStateMonitor, then:
163+
appStateMonitor.onActivityStarted(mockActivity);
164+
appStateMonitor.onActivityResumed(mockActivity);
165+
fragmentMonitor.onFragmentResumed(mockFragmentManager, mockFragment);
166+
appStateMonitor.onActivityPaused(mockActivity);
167+
// reset() was not called at the time of fragments collecting its frame metrics
168+
verify(fma, times(0)).reset();
169+
verify(fma, times(0)).remove(nullable(Activity.class));
170+
fragmentMonitor.onFragmentPaused(mockFragmentManager, mockFragment);
171+
verify(fma, times(0)).reset();
172+
verify(fma, times(0)).remove(nullable(Activity.class));
173+
// reset() is only called after fragment is done collecting its metrics
174+
appStateMonitor.onActivityPostPaused(mockActivity);
175+
verify(fma, times(1)).reset();
176+
verify(fma, times(1)).remove(nullable(Activity.class));
177+
}
178+
}

0 commit comments

Comments
 (0)