Skip to content
1 change: 1 addition & 0 deletions firebase-perf/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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();
}
}

Expand Down Expand Up @@ -614,7 +632,7 @@ Timer getOnResumeTime() {
}

@VisibleForTesting
void setIsStartFromBackground() {
isStartedFromBackground = true;
void setMainThreadRunnableTime(Timer timer) {
mainThreadRunnableTime = timer;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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(),
Expand Down