forked from firebase/firebase-android-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAppStartTrace.java
More file actions
665 lines (596 loc) · 26.2 KB
/
AppStartTrace.java
File metadata and controls
665 lines (596 loc) · 26.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
// Copyright 2020 Google LLC
//
// 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.
package com.google.firebase.perf.metrics;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Application;
import android.app.Application.ActivityLifecycleCallbacks;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Process;
import android.view.View;
import android.view.ViewTreeObserver;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.google.firebase.FirebaseApp;
import com.google.firebase.StartupTime;
import com.google.firebase.perf.config.ConfigResolver;
import com.google.firebase.perf.logging.AndroidLogger;
import com.google.firebase.perf.session.PerfSession;
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.FirstDrawDoneListener;
import com.google.firebase.perf.util.PreDrawListener;
import com.google.firebase.perf.util.Timer;
import com.google.firebase.perf.v1.ApplicationProcessState;
import com.google.firebase.perf.v1.TraceMetric;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* A class to capture the Android AppStart Trace information. The first time activity goes through
* onCreate()->onStart()->onResume() sequence is captured as app start timer and a TraceMetric log
* is sent to server.
*
* <p>The first time any activity (activityC) enters onCreate() method we record an onCreateTime.
* The first time any activity (activityS) enters onStart() method we record an onStartTime. The
* first time any activity (activityR) enters onResume() method we record an onResumeTime and this
* activity is recorded as AppStartActivity, this is end of AppStart trace..
*
* <p>In reality activityC, activityS and activityR do not need to be the same activity.
*
* @hide
*/
public class AppStartTrace implements ActivityLifecycleCallbacks, LifecycleObserver {
private static final @NonNull Timer PERF_CLASS_LOAD_TIME = new Clock().getTime();
private static final long MAX_LATENCY_BEFORE_UI_INIT = TimeUnit.MINUTES.toMicros(1);
// If the `mainThreadRunnableTime` was set within this duration, the assumption
// is that it was called immediately before `onActivityCreated` in foreground starts on API 34+.
//
// On API 34+, Android may drain posted main-thread runnables before processing the
// Activity-launch Binder transaction even during a genuine cold foreground start. The
// resulting gap between `StartFromBackgroundRunnable` firing and the first
// `onActivityCreated` is dominated by system scheduling, not by app work, and has been
// measured at ~316ms on a minimal repro app and ~204ms on a large production app on
// physical Pixel devices. The threshold must therefore comfortably exceed these
// real-world gaps; 1000ms provides ~5x headroom over the worst measured cold-start gap
// while still being two orders of magnitude below `MAX_LATENCY_BEFORE_UI_INIT` so that
// genuine warm starts (where the process was forked for background work seconds-to-
// minutes before any activity launch) remain correctly suppressed.
// See b/339891952 and https://github.com/firebase/firebase-android-sdk/issues/8103.
private static final long MAX_BACKGROUND_RUNNABLE_DELAY = TimeUnit.MILLISECONDS.toMicros(1000);
// Core pool size 0 allows threads to shut down if they're idle
private static final int CORE_POOL_SIZE = 0;
private static final int MAX_POOL_SIZE = 1; // Only need single thread
private static volatile AppStartTrace instance;
private static ExecutorService executorService;
private boolean isRegisteredForLifecycleCallbacks = false;
private final TransportManager transportManager;
private final Clock clock;
private final ConfigResolver configResolver;
private final TraceMetric.Builder experimentTtid;
private Context appContext;
/**
* The first time onCreate() of any activity is called, the activity is saved as launchActivity.
*/
private WeakReference<Activity> launchActivity;
/**
* The first time onResume() of any activity is called, the activity is saved as appStartActivity
*/
private WeakReference<Activity> appStartActivity;
/**
* If the time difference between app starts and creation of any Activity is larger than
* MAX_LATENCY_BEFORE_UI_INIT, set mTooLateToInitUI to true and we don't send AppStart Trace.
*/
private boolean isTooLateToInitUI = false;
// Critical timestamps during app-start, on the main-thread. IMPORTANT: these must all be captured
// or modified on the main thread. Without this invariant, we cannot guarantee that null means "it
// hasn't happened".
private final @Nullable Timer processStartTime;
private final @Nullable Timer firebaseClassLoadTime;
private Timer onCreateTime = null;
// TODO(b/339891952): Explore simplifying Timers in app start trace to use timestamps.
private Timer mainThreadRunnableTime = null;
private Timer onStartTime = null;
private Timer onResumeTime = null;
private Timer firstForegroundTime = null;
private @Nullable Timer firstBackgroundTime = null;
private Timer preDrawPostTime = null;
private Timer preDrawPostAtFrontOfQueueTime = null;
private Timer onDrawPostAtFrontOfQueueTime = null;
private PerfSession startSession;
private boolean isStartedFromBackground = false;
// TODO: remove after experiment
private int onDrawCount = 0;
private final DrawCounter onDrawCounterListener = new DrawCounter();
private boolean systemForegroundCheck = false;
/**
* Called from onCreate() method of an activity by instrumented byte code.
*
* @param activity Activity class name.
*/
@Keep
public static void setLauncherActivityOnCreateTime(String activity) {
// no-op, for backward compatibility with old version plugin.
}
/**
* Called from onStart() method of an activity by instrumented byte code.
*
* @param activity Activity class name.
*/
@Keep
public static void setLauncherActivityOnStartTime(String activity) {
// no-op, for backward compatibility with old version plugin.
}
/**
* Called from onResume() method of an activity by instrumented byte code.
*
* @param activity Activity class name.
*/
@Keep
public static void setLauncherActivityOnResumeTime(String activity) {
// no-op, for backward compatibility with old version plugin.
}
public static AppStartTrace getInstance() {
return instance != null ? instance : getInstance(TransportManager.getInstance(), new Clock());
}
// TODO(b/258263016): Migrate to go/firebase-android-executors
@SuppressLint("ThreadPoolCreation")
static AppStartTrace getInstance(TransportManager transportManager, Clock clock) {
if (instance == null) {
synchronized (AppStartTrace.class) {
if (instance == null) {
instance =
new AppStartTrace(
transportManager,
clock,
ConfigResolver.getInstance(),
new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
/* keepAliveTime= */ MAX_LATENCY_BEFORE_UI_INIT + 10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>()));
}
}
}
return instance;
}
@SuppressWarnings("FirebaseUseExplicitDependencies")
AppStartTrace(
@NonNull TransportManager transportManager,
@NonNull Clock clock,
@NonNull ConfigResolver configResolver,
@NonNull ExecutorService executorService) {
this.transportManager = transportManager;
this.clock = clock;
this.configResolver = configResolver;
this.executorService = executorService;
this.experimentTtid = TraceMetric.newBuilder().setName("_experiment_app_start_ttid");
// Set the timestamp for process-start (beginning of BIND_APPLICATION), if available
this.processStartTime =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
? Timer.ofElapsedRealtime(Process.getStartElapsedRealtime())
: null;
// Set the timestamp for Firebase's first class's class-loading (approx.), if available
StartupTime firebaseStart = FirebaseApp.getInstance().get(StartupTime.class);
this.firebaseClassLoadTime =
firebaseStart != null ? Timer.ofElapsedRealtime(firebaseStart.getElapsedRealtime()) : null;
}
/** Called from FirebasePerfEarly to register this callback. */
public synchronized void registerActivityLifecycleCallbacks(@NonNull Context context) {
// Make sure the callback is registered only once.
if (isRegisteredForLifecycleCallbacks) {
return;
}
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
Context appContext = context.getApplicationContext();
if (appContext instanceof Application) {
((Application) appContext).registerActivityLifecycleCallbacks(this);
systemForegroundCheck = systemForegroundCheck || isAnyAppProcessInForeground(appContext);
isRegisteredForLifecycleCallbacks = true;
this.appContext = appContext;
}
}
/** Unregister this callback after AppStart trace is logged. */
public synchronized void unregisterActivityLifecycleCallbacks() {
if (!isRegisteredForLifecycleCallbacks) {
return;
}
ProcessLifecycleOwner.get().getLifecycle().removeObserver(this);
((Application) appContext).unregisterActivityLifecycleCallbacks(this);
isRegisteredForLifecycleCallbacks = false;
}
/**
* Gets the timestamp that marks the beginning of app start, defined as the beginning of
* BIND_APPLICATION, when the forked process is about to start loading the app's resources and
* classes. Fallback to class-load time of a Firebase class for compatibility below API 24.
*
* @return {@link Timer} at the beginning of app start by Firebase-Performance definition.
*/
private @NonNull Timer getStartTimerCompat() {
// Preferred: Android system API provides BIND_APPLICATION time
if (processStartTime != null) {
return processStartTime;
}
// Fallback: static initializer time (during class-load) of a Firebase class
return getClassLoadTimeCompat();
}
/**
* Timestamp during class load. This timestamp is captured in a static initializer during class
* loading of a particular Firebase class, the order of which CANNOT be guaranteed to be prior to
* all other classes of an app, nor prior to resource-loading of an app. Thus this timestamp is
* NOT PREFERRED to be used as starting-point of app-start.
*
* @return {@link Timer} captured by static-initializer during class-loading of a Firebase class.
*/
private @NonNull Timer getClassLoadTimeCompat() {
// Prefered: static-initializer time of the 1st Firebase class during init
if (firebaseClassLoadTime != null) {
return firebaseClassLoadTime;
}
// Fallback: static-initializer time of the current class
return PERF_CLASS_LOAD_TIME;
}
private void recordPreDraw() {
if (preDrawPostTime != null) {
return;
}
this.preDrawPostTime = clock.getTime();
this.experimentTtid
.setClientStartTimeUs(getStartTimerCompat().getMicros())
.setDurationUs(getStartTimerCompat().getDurationMicros(this.preDrawPostTime));
logExperimentTrace(this.experimentTtid);
}
private void recordPreDrawFrontOfQueue() {
if (preDrawPostAtFrontOfQueueTime != null) {
return;
}
this.preDrawPostAtFrontOfQueueTime = clock.getTime();
this.experimentTtid.addSubtraces(
TraceMetric.newBuilder()
.setName("_experiment_preDrawFoQ")
.setClientStartTimeUs(getStartTimerCompat().getMicros())
.setDurationUs(
getStartTimerCompat().getDurationMicros(this.preDrawPostAtFrontOfQueueTime))
.build());
logExperimentTrace(this.experimentTtid);
}
private void recordOnDrawFrontOfQueue() {
if (onDrawPostAtFrontOfQueueTime != null) {
return;
}
this.onDrawPostAtFrontOfQueueTime = clock.getTime();
this.experimentTtid.addSubtraces(
TraceMetric.newBuilder()
.setName("_experiment_onDrawFoQ")
.setClientStartTimeUs(getStartTimerCompat().getMicros())
.setDurationUs(
getStartTimerCompat().getDurationMicros(this.onDrawPostAtFrontOfQueueTime))
.build());
if (processStartTime != null) {
this.experimentTtid.addSubtraces(
TraceMetric.newBuilder()
.setName("_experiment_procStart_to_classLoad")
.setClientStartTimeUs(getStartTimerCompat().getMicros())
.setDurationUs(getStartTimerCompat().getDurationMicros(getClassLoadTimeCompat()))
.build());
}
this.experimentTtid.putCustomAttributes(
"systemDeterminedForeground", systemForegroundCheck ? "true" : "false");
this.experimentTtid.putCounters("onDrawCount", onDrawCount);
this.experimentTtid.addPerfSessions(this.startSession.build());
logExperimentTrace(this.experimentTtid);
}
/**
* Sets the `isStartedFromBackground` flag to `true` if the `mainThreadRunnableTime` was set
* from the `StartFromBackgroundRunnable`.
* <p>
* If it's prior to API 34, it's always set to true if `mainThreadRunnableTime` was set.
* <p>
* If it's on or after API 34, and it was called less than `MAX_BACKGROUND_RUNNABLE_DELAY`
* before `onActivityCreated`, the
* assumption is that it was called immediately before the activity lifecycle callbacks in a
* foreground start.
* See b/339891952 and https://github.com/firebase/firebase-android-sdk/issues/8103.
*
* <p>TODO(b/339891952): Replace this timing-window heuristic with a causal signal.
* The current approach infers "was the process forked to launch an Activity?" from the
* ordering of a posted runnable vs the first onActivityCreated callback, which is
* inherently fragile (its calibration depends on Android scheduling behavior that has
* already shifted once on API 34+ and could shift again). Better candidates:
* <ul>
* <li>API 35+: use {@code ActivityManager.getHistoricalProcessStartReasons} /
* {@code ApplicationStartInfo}, which authoritatively reports the start reason
* ({@code START_REASON_LAUNCHER}, {@code START_REASON_SERVICE},
* {@code START_REASON_CONTENT_PROVIDER}, etc.). This removes the heuristic
* entirely on supported devices.
* <li>API 34: capture {@code ActivityManager.RunningAppProcessInfo.importanceReasonCode}
* and {@code .importance} once early in {@code FirebasePerfEarly} (before any of
* our own ContentProvider work mutates the cause), and combine with the timing
* window as a fallback for ambiguous cases.
* </ul>
* Adopting either of these would also let the API-34-vs-pre-34 branch below collapse.
*/
private void resolveIsStartedFromBackground() {
// If the mainThreadRunnableTime is null, either the runnable hasn't run, or this check has
// already been made.
if (mainThreadRunnableTime == null) {
return;
}
// If the `mainThreadRunnableTime` was set prior to API 34, it's always assumed that's it's
// a background start.
// Otherwise it's assumed to be a background start if the runnable was set more than
// `MAX_BACKGROUND_RUNNABLE_DELAY`
// before the first `onActivityCreated` call.
// TODO(b/339891952): Investigate removing the API check.
if ((Build.VERSION.SDK_INT < 34)
|| (mainThreadRunnableTime.getDurationMicros() > MAX_BACKGROUND_RUNNABLE_DELAY)) {
isStartedFromBackground = true;
}
// Set this to null to prevent additional checks.
mainThreadRunnableTime = null;
}
@Override
public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) {
resolveIsStartedFromBackground();
if (isStartedFromBackground || onCreateTime != null // An activity already called onCreate()
) {
return;
}
systemForegroundCheck = systemForegroundCheck || isAnyAppProcessInForeground(appContext);
launchActivity = new WeakReference<Activity>(activity);
onCreateTime = clock.getTime();
if (getStartTimerCompat().getDurationMicros(onCreateTime) > MAX_LATENCY_BEFORE_UI_INIT) {
isTooLateToInitUI = true;
}
}
@Override
public synchronized void onActivityStarted(Activity activity) {
if (isStartedFromBackground
|| onStartTime != null // An activity already called onStart()
|| isTooLateToInitUI) {
return;
}
onStartTime = clock.getTime();
}
@Override
public synchronized void onActivityResumed(Activity activity) {
if (isStartedFromBackground || isTooLateToInitUI) {
return;
}
// Shadow-launch experiment of new app start time
final boolean isExperimentTTIDEnabled = configResolver.getIsExperimentTTIDEnabled();
if (isExperimentTTIDEnabled) {
View rootView = activity.findViewById(android.R.id.content);
if (rootView != null) {
rootView.getViewTreeObserver().addOnDrawListener(onDrawCounterListener);
FirstDrawDoneListener.registerForNextDraw(rootView, this::recordOnDrawFrontOfQueue);
PreDrawListener.registerForNextDraw(
rootView, this::recordPreDraw, this::recordPreDrawFrontOfQueue);
}
}
if (onResumeTime != null) { // An activity already called onResume()
return;
}
appStartActivity = new WeakReference<Activity>(activity);
onResumeTime = clock.getTime();
this.startSession = SessionManager.getInstance().perfSession();
AndroidLogger.getInstance()
.debug(
"onResume(): "
+ activity.getClass().getName()
+ ": "
+ getClassLoadTimeCompat().getDurationMicros(onResumeTime)
+ " microseconds");
// Log the app start trace in a non-main thread.
executorService.execute(this::logAppStartTrace);
if (!isExperimentTTIDEnabled) {
// After AppStart trace is logged, we can unregister this callback.
unregisterActivityLifecycleCallbacks();
}
}
/** Helper for logging all experiments in one trace. */
private void logExperimentTrace(TraceMetric.Builder metric) {
if (this.preDrawPostTime == null
|| this.preDrawPostAtFrontOfQueueTime == null
|| this.onDrawPostAtFrontOfQueueTime == null) {
return;
}
executorService.execute(
() -> transportManager.log(metric.build(), ApplicationProcessState.FOREGROUND_BACKGROUND));
// After logging the experiment trace, we can unregister ourself from lifecycle listeners.
unregisterActivityLifecycleCallbacks();
}
private void logAppStartTrace() {
TraceMetric.Builder metric =
TraceMetric.newBuilder()
.setName(Constants.TraceNames.APP_START_TRACE_NAME.toString())
.setClientStartTimeUs(getClassLoadTimeCompat().getMicros())
.setDurationUs(getClassLoadTimeCompat().getDurationMicros(onResumeTime));
List<TraceMetric> subtraces = new ArrayList<>(/* initialCapacity= */ 3);
TraceMetric.Builder traceMetricBuilder =
TraceMetric.newBuilder()
.setName(Constants.TraceNames.ON_CREATE_TRACE_NAME.toString())
.setClientStartTimeUs(getClassLoadTimeCompat().getMicros())
.setDurationUs(getClassLoadTimeCompat().getDurationMicros(onCreateTime));
subtraces.add(traceMetricBuilder.build());
// OnStartTime is not captured in all situations, so checking for valid value before using it.
if (onStartTime != null) {
traceMetricBuilder = TraceMetric.newBuilder();
traceMetricBuilder
.setName(Constants.TraceNames.ON_START_TRACE_NAME.toString())
.setClientStartTimeUs(onCreateTime.getMicros())
.setDurationUs(onCreateTime.getDurationMicros(onStartTime));
subtraces.add(traceMetricBuilder.build());
traceMetricBuilder = TraceMetric.newBuilder();
traceMetricBuilder
.setName(Constants.TraceNames.ON_RESUME_TRACE_NAME.toString())
.setClientStartTimeUs(onStartTime.getMicros())
.setDurationUs(onStartTime.getDurationMicros(onResumeTime));
subtraces.add(traceMetricBuilder.build());
}
metric.addAllSubtraces(subtraces).addPerfSessions(this.startSession.build());
transportManager.log(metric.build(), ApplicationProcessState.FOREGROUND_BACKGROUND);
}
@Override
public void onActivityPaused(Activity activity) {
if (isStartedFromBackground
|| isTooLateToInitUI
|| !configResolver.getIsExperimentTTIDEnabled()) {
return;
}
View rootView = activity.findViewById(android.R.id.content);
if (rootView != null) {
rootView.getViewTreeObserver().removeOnDrawListener(onDrawCounterListener);
}
}
@Override
public void onActivityStopped(Activity activity) {}
/** App is entering foreground. Keep annotation is required so R8 does not remove this method. */
@Keep
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void onAppEnteredForeground() {
if (isStartedFromBackground || isTooLateToInitUI || firstForegroundTime != null) {
return;
}
// firstForeground is equivalent to the first Activity onStart. This marks the beginning of
// observable backgrounding. Prior to this point, backgrounding cannot be observed.
firstForegroundTime = clock.getTime();
this.experimentTtid.addSubtraces(
TraceMetric.newBuilder()
.setName("_experiment_firstForegrounding")
.setClientStartTimeUs(getStartTimerCompat().getMicros())
.setDurationUs(getStartTimerCompat().getDurationMicros(firstForegroundTime))
.build());
}
/** App is entering background. Keep annotation is required so R8 does not remove this method. */
@Keep
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
public void onAppEnteredBackground() {
if (isStartedFromBackground || isTooLateToInitUI || firstBackgroundTime != null) {
return;
}
firstBackgroundTime = clock.getTime();
// TODO: remove this subtrace after the experiment
this.experimentTtid.addSubtraces(
TraceMetric.newBuilder()
.setName("_experiment_firstBackgrounding")
.setClientStartTimeUs(getStartTimerCompat().getMicros())
.setDurationUs(getStartTimerCompat().getDurationMicros(firstBackgroundTime))
.build());
}
@Override
public void onActivityDestroyed(Activity activity) {}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
/**
* Returns whether any process corresponding to the package for the provided context is visible
* (in other words, whether the app is currently in the foreground).
*
* @param appContext The application's context.
*/
public static boolean isAnyAppProcessInForeground(Context appContext) {
// Do not call ProcessStats.getActivityManger, caching will break tests that indirectly depend
// on ProcessStats.
ActivityManager activityManager =
(ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE);
if (activityManager == null) {
return true;
}
List<ActivityManager.RunningAppProcessInfo> appProcesses =
activityManager.getRunningAppProcesses();
if (appProcesses != null) {
String appProcessName = appContext.getPackageName();
String allowedAppProcessNamePrefix = appProcessName + ":";
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (appProcess.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
continue;
}
if (appProcess.processName.equals(appProcessName)
|| appProcess.processName.startsWith(allowedAppProcessNamePrefix)) {
// Returns true if the process with `IMPORTANCE_FOREGROUND` matches current process.
return true;
}
}
}
return false;
}
/**
* We use StartFromBackgroundRunnable to detect if app is started from background or foreground.
* If app is started from background, we do not generate AppStart trace. This runnable is posted
* to main UI thread from FirebasePerfEarly. If `onActivityCreate` has never been called, we
* record the timestamp - which allows `onActivityCreate` to determine whether it was a background
* app start or not.
*/
public static class StartFromBackgroundRunnable implements Runnable {
private final AppStartTrace trace;
public StartFromBackgroundRunnable(final AppStartTrace trace) {
this.trace = trace;
}
@Override
public void run() {
// Only set the `mainThreadRunnableTime` if `onActivityCreate` has never been called.
if (trace.onCreateTime == null) {
trace.mainThreadRunnableTime = new Timer();
}
}
}
private final class DrawCounter implements ViewTreeObserver.OnDrawListener {
@Override
public void onDraw() {
onDrawCount++;
}
}
@VisibleForTesting
@Nullable
Activity getLaunchActivity() {
return launchActivity.get();
}
@VisibleForTesting
@Nullable
Activity getAppStartActivity() {
return appStartActivity.get();
}
@VisibleForTesting
Timer getOnCreateTime() {
return onCreateTime;
}
@VisibleForTesting
Timer getOnStartTime() {
return onStartTime;
}
@VisibleForTesting
Timer getOnResumeTime() {
return onResumeTime;
}
@VisibleForTesting
void setMainThreadRunnableTime(Timer timer) {
mainThreadRunnableTime = timer;
}
}