Skip to content

Commit ff209d4

Browse files
[Fireperf][AASA] Additional experiments for _experiment_as_ttid (#4283)
* predraw and uptimeMillis * fix send event * fix dangling variable and naming * copyright * fix test * onStop * test * better readability * comments * reduce code * reset
1 parent 5204dd4 commit ff209d4

File tree

6 files changed

+193
-44
lines changed

6 files changed

+193
-44
lines changed

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

Lines changed: 94 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import com.google.firebase.perf.util.Clock;
3636
import com.google.firebase.perf.util.Constants;
3737
import com.google.firebase.perf.util.FirstDrawDoneListener;
38+
import com.google.firebase.perf.util.PreDrawListener;
3839
import com.google.firebase.perf.util.Timer;
3940
import com.google.firebase.perf.v1.ApplicationProcessState;
4041
import com.google.firebase.perf.v1.TraceMetric;
@@ -75,6 +76,7 @@ public class AppStartTrace implements ActivityLifecycleCallbacks {
7576
private final TransportManager transportManager;
7677
private final Clock clock;
7778
private final ConfigResolver configResolver;
79+
private final TraceMetric.Builder experimentTtid;
7880
private Context appContext;
7981
/**
8082
* The first time onCreate() of any activity is called, the activity is saved as launchActivity.
@@ -96,6 +98,7 @@ public class AppStartTrace implements ActivityLifecycleCallbacks {
9698
private Timer onStartTime = null;
9799
private Timer onResumeTime = null;
98100
private Timer firstDrawDone = null;
101+
private Timer preDraw = null;
99102

100103
private PerfSession startSession;
101104
private boolean isStartedFromBackground = false;
@@ -163,6 +166,7 @@ static AppStartTrace getInstance(TransportManager transportManager, Clock clock)
163166
this.clock = clock;
164167
this.configResolver = configResolver;
165168
this.executorService = executorService;
169+
this.experimentTtid = TraceMetric.newBuilder().setName("_experiment_app_start_ttid");
166170
}
167171

168172
/** Called from FirebasePerfProvider to register this callback. */
@@ -196,7 +200,8 @@ public synchronized void unregisterActivityLifecycleCallbacks() {
196200
*/
197201
private static Timer getStartTimer() {
198202
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
199-
return Timer.ofElapsedRealtime(Process.getStartElapsedRealtime());
203+
return Timer.ofElapsedRealtime(
204+
Process.getStartElapsedRealtime(), Process.getStartUptimeMillis());
200205
}
201206
return FirebasePerfProvider.getAppStartTime();
202207
}
@@ -205,16 +210,73 @@ private void recordFirstDrawDone() {
205210
if (firstDrawDone != null) {
206211
return;
207212
}
213+
Timer start = getStartTimer();
208214
this.firstDrawDone = clock.getTime();
209-
executorService.execute(
210-
() -> this.logColdStart(getStartTimer(), this.firstDrawDone, this.startSession));
215+
this.experimentTtid
216+
.setClientStartTimeUs(start.getMicros())
217+
.setDurationUs(start.getDurationMicros(this.firstDrawDone));
211218

212-
if (isRegisteredForLifecycleCallbacks) {
213-
// After AppStart trace is queued to be logged, we can unregister this callback.
214-
unregisterActivityLifecycleCallbacks();
219+
TraceMetric.Builder subtrace =
220+
TraceMetric.newBuilder()
221+
.setName("_experiment_classLoadTime")
222+
.setClientStartTimeUs(FirebasePerfProvider.getAppStartTime().getMicros())
223+
.setDurationUs(
224+
FirebasePerfProvider.getAppStartTime().getDurationMicros(this.firstDrawDone));
225+
this.experimentTtid.addSubtraces(subtrace.build());
226+
227+
subtrace = TraceMetric.newBuilder();
228+
subtrace
229+
.setName("_experiment_uptimeMillis")
230+
.setClientStartTimeUs(start.getMicros())
231+
.setDurationUs(start.getDurationUptimeMicros(this.firstDrawDone));
232+
this.experimentTtid.addSubtraces(subtrace.build());
233+
234+
this.experimentTtid.addPerfSessions(this.startSession.build());
235+
236+
if (isExperimentTraceDone()) {
237+
executorService.execute(() -> this.logExperimentTtid(this.experimentTtid));
238+
239+
if (isRegisteredForLifecycleCallbacks) {
240+
// After AppStart trace is queued to be logged, we can unregister this callback.
241+
unregisterActivityLifecycleCallbacks();
242+
}
215243
}
216244
}
217245

246+
private void recordFirstDrawDonePreDraw() {
247+
if (preDraw != null) {
248+
return;
249+
}
250+
Timer start = getStartTimer();
251+
this.preDraw = clock.getTime();
252+
TraceMetric.Builder subtrace =
253+
TraceMetric.newBuilder()
254+
.setName("_experiment_preDraw")
255+
.setClientStartTimeUs(start.getMicros())
256+
.setDurationUs(start.getDurationMicros(this.preDraw));
257+
this.experimentTtid.addSubtraces(subtrace.build());
258+
259+
subtrace = TraceMetric.newBuilder();
260+
subtrace
261+
.setName("_experiment_preDraw_uptimeMillis")
262+
.setClientStartTimeUs(start.getMicros())
263+
.setDurationUs(start.getDurationUptimeMicros(this.preDraw));
264+
this.experimentTtid.addSubtraces(subtrace.build());
265+
266+
if (isExperimentTraceDone()) {
267+
executorService.execute(() -> this.logExperimentTtid(this.experimentTtid));
268+
269+
if (isRegisteredForLifecycleCallbacks) {
270+
// After AppStart trace is queued to be logged, we can unregister this callback.
271+
unregisterActivityLifecycleCallbacks();
272+
}
273+
}
274+
}
275+
276+
private boolean isExperimentTraceDone() {
277+
return this.preDraw != null && this.firstDrawDone != null;
278+
}
279+
218280
@Override
219281
public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) {
220282
if (isStartedFromBackground || onCreateTime != null // An activity already called onCreate()
@@ -252,6 +314,7 @@ public synchronized void onActivityResumed(Activity activity) {
252314
if (isExperimentTTIDEnabled) {
253315
View rootView = activity.findViewById(android.R.id.content);
254316
FirstDrawDoneListener.registerForNextDraw(rootView, this::recordFirstDrawDone);
317+
PreDrawListener.registerForNextDraw(rootView, this::recordFirstDrawDonePreDraw);
255318
}
256319

257320
if (onResumeTime != null) { // An activity already called onResume()
@@ -280,21 +343,7 @@ public synchronized void onActivityResumed(Activity activity) {
280343
}
281344
}
282345

283-
private void logColdStart(Timer start, Timer end, PerfSession session) {
284-
TraceMetric.Builder metric =
285-
TraceMetric.newBuilder()
286-
.setName("_experiment_app_start_ttid")
287-
.setClientStartTimeUs(start.getMicros())
288-
.setDurationUs(start.getDurationMicros(end));
289-
290-
TraceMetric.Builder subtrace =
291-
TraceMetric.newBuilder()
292-
.setName("_experiment_classLoadTime")
293-
.setClientStartTimeUs(FirebasePerfProvider.getAppStartTime().getMicros())
294-
.setDurationUs(FirebasePerfProvider.getAppStartTime().getDurationMicros(end));
295-
296-
metric.addSubtraces(subtrace).addPerfSessions(this.startSession.build());
297-
346+
private void logExperimentTtid(TraceMetric.Builder metric) {
298347
transportManager.log(metric.build(), ApplicationProcessState.FOREGROUND_BACKGROUND);
299348
}
300349

@@ -333,10 +382,32 @@ private void logAppStartTrace() {
333382
}
334383

335384
@Override
336-
public void onActivityPaused(Activity activity) {}
385+
public void onActivityPaused(Activity activity) {
386+
if (isExperimentTraceDone()) {
387+
return;
388+
}
389+
Timer onPauseTime = clock.getTime();
390+
TraceMetric.Builder subtrace =
391+
TraceMetric.newBuilder()
392+
.setName("_experiment_onPause")
393+
.setClientStartTimeUs(onPauseTime.getMicros())
394+
.setDurationUs(getStartTimer().getDurationMicros(onPauseTime));
395+
this.experimentTtid.addSubtraces(subtrace.build());
396+
}
337397

338398
@Override
339-
public synchronized void onActivityStopped(Activity activity) {}
399+
public void onActivityStopped(Activity activity) {
400+
if (isExperimentTraceDone()) {
401+
return;
402+
}
403+
Timer onStopTime = clock.getTime();
404+
TraceMetric.Builder subtrace =
405+
TraceMetric.newBuilder()
406+
.setName("_experiment_onStop")
407+
.setClientStartTimeUs(onStopTime.getMicros())
408+
.setDurationUs(getStartTimer().getDurationMicros(onStopTime));
409+
this.experimentTtid.addSubtraces(subtrace.build());
410+
}
340411

341412
@Override
342413
public void onActivityDestroyed(Activity activity) {}

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

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,12 @@
1818
import android.os.Looper;
1919
import android.view.View;
2020
import android.view.ViewTreeObserver;
21-
import androidx.annotation.RequiresApi;
2221
import java.util.concurrent.atomic.AtomicReference;
2322

2423
/**
2524
* OnDrawListener that unregisters itself and invokes callback when the next draw is done. This API
26-
* 16+ implementation is an approximation of the initial display time. {@link
27-
* android.view.Choreographer#postFrameCallback} is an Android API that provides a simpler and more
28-
* accurate initial display time, but it was bugged before API 30, hence we use this backported
29-
* implementation.
25+
* 16+ implementation is an approximation of the initial-display-time defined by Android Vitals.
3026
*/
31-
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
3227
public class FirstDrawDoneListener implements ViewTreeObserver.OnDrawListener {
3328
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
3429
private final AtomicReference<View> viewReference;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
//
6+
// You may obtain a copy of the License at
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+
package com.google.firebase.perf.util;
15+
16+
import android.os.Handler;
17+
import android.os.Looper;
18+
import android.view.View;
19+
import android.view.ViewTreeObserver;
20+
import java.util.concurrent.atomic.AtomicReference;
21+
22+
/**
23+
* OnPreDraw listener that unregisters itself and post a callback to the main thread during
24+
* OnPreDraw. This is an approximation of the initial-display time defined by Android Vitals.
25+
*/
26+
public class PreDrawListener implements ViewTreeObserver.OnPreDrawListener {
27+
private final Handler mainThreadHandler = new Handler(Looper.getMainLooper());
28+
private final AtomicReference<View> viewReference;
29+
private final Runnable callback;
30+
31+
/** Registers a post-draw callback for the next draw of a view. */
32+
public static void registerForNextDraw(View view, Runnable drawDoneCallback) {
33+
final PreDrawListener listener = new PreDrawListener(view, drawDoneCallback);
34+
view.getViewTreeObserver().addOnPreDrawListener(listener);
35+
}
36+
37+
private PreDrawListener(View view, Runnable callback) {
38+
this.viewReference = new AtomicReference<>(view);
39+
this.callback = callback;
40+
}
41+
42+
@Override
43+
public boolean onPreDraw() {
44+
// Set viewReference to null so any onPreDraw past the first is a no-op
45+
View view = viewReference.getAndSet(null);
46+
if (view == null) {
47+
return true;
48+
}
49+
view.getViewTreeObserver().removeOnPreDrawListener(this);
50+
mainThreadHandler.post(callback);
51+
return true;
52+
}
53+
}

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,23 @@ public class Timer implements Parcelable {
4343
* compute duration between 2 timestamps in the same timebase. It is NOT wall-clock time.
4444
*/
4545
private long elapsedRealtimeMicros;
46+
/**
47+
* Monotonic time measured in the {@link SystemClock#uptimeMillis()} timebase. Only used to
48+
* compute duration between 2 timestamps in the same timebase. It is NOT wall-clock time.
49+
*/
50+
private long uptimeMicros;
4651

4752
/**
4853
* Returns a new Timer object as if it was stamped at the given elapsedRealtime. Uses current
4954
* wall-clock as a reference to extrapolate the wall-clock at the given elapsedRealtime.
5055
*
5156
* @param elapsedRealtimeMillis timestamp in the {@link SystemClock#elapsedRealtime()} timebase
5257
*/
53-
public static Timer ofElapsedRealtime(final long elapsedRealtimeMillis) {
58+
public static Timer ofElapsedRealtime(final long elapsedRealtimeMillis, final long uptimeMillis) {
59+
long uptimeMicros = MILLISECONDS.toMicros(uptimeMillis);
5460
long elapsedRealtimeMicros = MILLISECONDS.toMicros(elapsedRealtimeMillis);
5561
long wallClockMicros = wallClockMicros() + (elapsedRealtimeMicros - elapsedRealtimeMicros());
56-
return new Timer(wallClockMicros, elapsedRealtimeMicros);
62+
return new Timer(wallClockMicros, elapsedRealtimeMicros, uptimeMicros);
5763
}
5864

5965
/**
@@ -77,10 +83,14 @@ private static long elapsedRealtimeMicros() {
7783
return MILLISECONDS.toMicros(SystemClock.elapsedRealtime());
7884
}
7985

86+
private static long uptimeMicros() {
87+
return MILLISECONDS.toMicros(SystemClock.uptimeMillis());
88+
}
89+
8090
// TODO: make all constructors private, use public static factory methods, per Effective Java
8191
/** Construct Timer object using System clock. */
8292
public Timer() {
83-
this(wallClockMicros(), elapsedRealtimeMicros());
93+
this(wallClockMicros(), elapsedRealtimeMicros(), uptimeMicros());
8494
}
8595

8696
/**
@@ -91,9 +101,10 @@ public Timer() {
91101
* SystemClock#elapsedRealtime()} timebase
92102
*/
93103
@VisibleForTesting
94-
Timer(long epochMicros, long elapsedRealtimeMicros) {
104+
Timer(long epochMicros, long elapsedRealtimeMicros, long uptimeMicros) {
95105
this.wallClockMicros = epochMicros;
96106
this.elapsedRealtimeMicros = elapsedRealtimeMicros;
107+
this.uptimeMicros = uptimeMicros;
97108
}
98109

99110
/**
@@ -104,18 +115,19 @@ public Timer() {
104115
*/
105116
@VisibleForTesting
106117
public Timer(long testTime) {
107-
this(testTime, testTime);
118+
this(testTime, testTime, testTime);
108119
}
109120

110121
private Timer(Parcel in) {
111-
this(in.readLong(), in.readLong());
122+
this(in.readLong(), in.readLong(), in.readLong());
112123
}
113124

114125
/** resets the start time */
115126
public void reset() {
116127
// TODO: consider removing this method and make Timer immutable thus fully thread-safe
117128
wallClockMicros = wallClockMicros();
118129
elapsedRealtimeMicros = elapsedRealtimeMicros();
130+
uptimeMicros = uptimeMicros();
119131
}
120132

121133
/** Return wall-clock time in microseconds. */
@@ -144,6 +156,16 @@ public long getDurationMicros(@NonNull final Timer end) {
144156
return end.elapsedRealtimeMicros - this.elapsedRealtimeMicros;
145157
}
146158

159+
/**
160+
* Calculate duration in microseconds using uptime. The start time is this Timer object.
161+
*
162+
* @param end end Timer object
163+
* @return duration in microseconds.
164+
*/
165+
public long getDurationUptimeMicros(@NonNull final Timer end) {
166+
return end.uptimeMicros - this.uptimeMicros;
167+
}
168+
147169
/**
148170
* Calculates the current wall clock off the existing wall clock time. The reason this is better
149171
* instead of just doing System.getCurrentTimeMillis is that the device time could've changed
@@ -166,6 +188,7 @@ public long getCurrentTimestampMicros() {
166188
public void writeToParcel(Parcel out, int flags) {
167189
out.writeLong(wallClockMicros);
168190
out.writeLong(elapsedRealtimeMicros);
191+
out.writeLong(uptimeMicros);
169192
}
170193

171194
/**

firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,19 @@ public void timeToInitialDisplay_isLogged() {
298298
trace.onActivityCreated(activity1, bundle);
299299
trace.onActivityStarted(activity1);
300300
trace.onActivityResumed(activity1);
301+
// Experiment: simulate backgrounding before draw
302+
trace.onActivityPaused(activity1);
303+
trace.onActivityStopped(activity1);
304+
trace.onActivityStarted(activity1);
305+
trace.onActivityResumed(activity1);
301306
fakeExecutorService.runAll();
302307
verify(transportManager, times(1))
303308
.log(isA(TraceMetric.class), isA(ApplicationProcessState.class));
304309

305310
// Simulate draw and manually stepping time forward
306311
ShadowSystemClock.advanceBy(Duration.ofMillis(1000));
307312
long drawTime = TimeUnit.NANOSECONDS.toMicros(SystemClock.elapsedRealtimeNanos());
313+
testView.getViewTreeObserver().dispatchOnPreDraw();
308314
testView.getViewTreeObserver().dispatchOnDraw();
309315
shadowOf(Looper.getMainLooper()).idle();
310316
fakeExecutorService.runNext();
@@ -317,5 +323,6 @@ public void timeToInitialDisplay_isLogged() {
317323
assertThat(ttid.getName()).isEqualTo("_experiment_app_start_ttid");
318324
assertThat(ttid.getDurationUs()).isNotEqualTo(resumeTime - appStartTime);
319325
assertThat(ttid.getDurationUs()).isEqualTo(drawTime - appStartTime);
326+
assertThat(ttid.getSubtracesCount()).isEqualTo(6);
320327
}
321328
}

0 commit comments

Comments
 (0)