diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index b4db8bb6bc4..4954be336b5 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased * [fixed] Fixed an ANR on app launch. [#4831] +* [fixed] Fixed app start traces on API 34+. [#5920] # 22.0.0 * [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java index 7574f989d92..ceac4b39335 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java @@ -75,6 +75,8 @@ public class AppStartTrace implements ActivityLifecycleCallbacks, LifecycleObser 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); + private static final long MAX_BACKGROUND_RUNNABLE_DELAY = TimeUnit.MILLISECONDS.toMicros(100); + // 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 @@ -111,6 +113,8 @@ public class AppStartTrace implements ActivityLifecycleCallbacks, LifecycleObser private final @Nullable Timer processStartTime; private final @Nullable Timer firebaseClassLoadTime; private Timer onCreateTime = null; + + private Timer mainThreadRunnableTime = null; private Timer onStartTime = null; private Timer onResumeTime = null; private Timer firstForegroundTime = null; @@ -319,8 +323,26 @@ private void recordOnDrawFrontOfQueue() { logExperimentTrace(this.experimentTtid); } + 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; + } + + // Set it to true if the runnable ran more than 100ms prior to onActivityCreated() + if (mainThreadRunnableTime.getDurationMicros() > MAX_BACKGROUND_RUNNABLE_DELAY) { + isStartedFromBackground = true; + } + + // Set this to null to prevent additional checks if `onActivityCreated()` is called again. + mainThreadRunnableTime = null; + } + @Override public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) { + resolveIsStartedFromBackground(); + if (isStartedFromBackground || onCreateTime != null // An activity already called onCreate() ) { return; @@ -560,8 +582,7 @@ public static boolean isScreenOn(Context appContext) { * 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 app is started from background, this runnable will - * be executed before any activity's onCreate() method. If app is started from foreground, - * activity's onCreate() method is executed before this runnable. + * be executed earlier than 100ms of any activity's onCreate() method. */ public static class StartFromBackgroundRunnable implements Runnable { private final AppStartTrace trace; @@ -572,10 +593,7 @@ public StartFromBackgroundRunnable(final AppStartTrace trace) { @Override public void run() { - // if no activity has ever been created. - if (trace.onCreateTime == null) { - trace.isStartedFromBackground = true; - } + trace.mainThreadRunnableTime = new Timer(); } } @@ -614,7 +632,7 @@ Timer getOnResumeTime() { } @VisibleForTesting - void setIsStartFromBackground() { - isStartedFromBackground = true; + void setMainThreadRunnableTime(Timer timer) { + mainThreadRunnableTime = timer; } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java index 36ae3d10116..25ad845231a 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java @@ -18,6 +18,7 @@ import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -238,11 +239,42 @@ public void testDelayedAppStart() { } @Test - public void testStartFromBackground() { + public void testStartFromBackground_within100ms() { FakeScheduledExecutorService fakeExecutorService = new FakeScheduledExecutorService(); + Timer fakeTimer = spy(new Timer(currentTime)); AppStartTrace trace = new AppStartTrace(transportManager, clock, configResolver, fakeExecutorService); - trace.setIsStartFromBackground(); + trace.registerActivityLifecycleCallbacks(appContext); + trace.setMainThreadRunnableTime(fakeTimer); + + when(fakeTimer.getDurationMicros()).thenReturn(99L); + trace.onActivityCreated(activity1, bundle); + Assert.assertNotNull(trace.getOnCreateTime()); + ++currentTime; + trace.onActivityStarted(activity1); + Assert.assertNotNull(trace.getOnStartTime()); + ++currentTime; + trace.onActivityResumed(activity1); + Assert.assertNotNull(trace.getOnResumeTime()); + fakeExecutorService.runAll(); + // There should be a trace sent since the delay between the main thread and onActivityCreated + // is limited. + verify(transportManager, times(1)) + .log( + traceArgumentCaptor.capture(), + ArgumentMatchers.nullable(ApplicationProcessState.class)); + } + + @Test + public void testStartFromBackground_moreThan100ms() { + FakeScheduledExecutorService fakeExecutorService = new FakeScheduledExecutorService(); + Timer fakeTimer = spy(new Timer(currentTime)); + AppStartTrace trace = + new AppStartTrace(transportManager, clock, configResolver, fakeExecutorService); + trace.registerActivityLifecycleCallbacks(appContext); + trace.setMainThreadRunnableTime(fakeTimer); + + when(fakeTimer.getDurationMicros()).thenReturn(TimeUnit.MILLISECONDS.toMicros(100) + 1); trace.onActivityCreated(activity1, bundle); Assert.assertNull(trace.getOnCreateTime()); ++currentTime; @@ -252,6 +284,7 @@ public void testStartFromBackground() { trace.onActivityResumed(activity1); Assert.assertNull(trace.getOnResumeTime()); // There should be no trace sent. + fakeExecutorService.runAll(); verify(transportManager, times(0)) .log( traceArgumentCaptor.capture(),