Skip to content

Commit e11e900

Browse files
authored
Make a change to prevent false positive background app starts. (#7274)
Added a change in `AppStartTrace` to prevent false positives of a background app start on API 34+ devices. See #5920 Verified locally: ``` 2025-08-20 10:34:51.283 18373-18373 AppMonDemo com.tdeshpande.appmontester D mainThreadRunnable.run(): activityCreated = false 2025-08-20 10:34:51.292 18373-18373 AppMonDemo com.tdeshpande.appmontester D onActivityPreCreated: mainThreadRunnableRun = true 2025-08-20 10:34:51.294 18373-18373 AppMonDemo com.tdeshpande.appmontester D onActivityCreated: mainThreadRunnableRun = true 2025-08-20 10:34:51.314 18373-18373 AppMonDemo com.tdeshpande.appmontester D onActivityStarted: mainThreadRunnableRun = true 2025-08-20 10:34:51.318 18373-18373 FirebasePerformance com.tdeshpande.appmontester I Firebase Performance Monitoring is successfully initialized! In a minute, visit the Firebase console to view your data: 2025-08-20 10:34:51.319 18373-18373 FirebasePerformance com.tdeshpande.appmontester D onResume(): com.tdeshpande.appmontester.MainActivity: 74362 microseconds 2025-08-20 10:34:51.320 18373-18373 AppMonDemo com.tdeshpande.appmontester D onActivityResumed: mainThreadRunnableRun = true 2025-08-20 10:34:51.323 18373-18431 FirebasePerformance com.tdeshpande.appmontester I Logging trace metric: _as (duration: 74.362ms). In a minute, visit the Firebase console to view your data: ```
1 parent c85227e commit e11e900

File tree

3 files changed

+62
-10
lines changed

3 files changed

+62
-10
lines changed

firebase-perf/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22
* [fixed] Fixed an ANR on app launch. [#4831]
3+
* [fixed] Fixed app start traces on API 34+. [#5920]
34

45
# 22.0.0
56
* [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher.

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

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public class AppStartTrace implements ActivityLifecycleCallbacks, LifecycleObser
7575
private static final @NonNull Timer PERF_CLASS_LOAD_TIME = new Clock().getTime();
7676
private static final long MAX_LATENCY_BEFORE_UI_INIT = TimeUnit.MINUTES.toMicros(1);
7777

78+
private static final long MAX_BACKGROUND_RUNNABLE_DELAY = TimeUnit.MILLISECONDS.toMicros(100);
79+
7880
// Core pool size 0 allows threads to shut down if they're idle
7981
private static final int CORE_POOL_SIZE = 0;
8082
private static final int MAX_POOL_SIZE = 1; // Only need single thread
@@ -111,6 +113,8 @@ public class AppStartTrace implements ActivityLifecycleCallbacks, LifecycleObser
111113
private final @Nullable Timer processStartTime;
112114
private final @Nullable Timer firebaseClassLoadTime;
113115
private Timer onCreateTime = null;
116+
117+
private Timer mainThreadRunnableTime = null;
114118
private Timer onStartTime = null;
115119
private Timer onResumeTime = null;
116120
private Timer firstForegroundTime = null;
@@ -319,8 +323,26 @@ private void recordOnDrawFrontOfQueue() {
319323
logExperimentTrace(this.experimentTtid);
320324
}
321325

326+
private void resolveIsStartedFromBackground() {
327+
// If the mainThreadRunnableTime is null, either the runnable hasn't run, or this check has
328+
// already been made.
329+
if (mainThreadRunnableTime == null) {
330+
return;
331+
}
332+
333+
// Set it to true if the runnable ran more than 100ms prior to onActivityCreated()
334+
if (mainThreadRunnableTime.getDurationMicros() > MAX_BACKGROUND_RUNNABLE_DELAY) {
335+
isStartedFromBackground = true;
336+
}
337+
338+
// Set this to null to prevent additional checks if `onActivityCreated()` is called again.
339+
mainThreadRunnableTime = null;
340+
}
341+
322342
@Override
323343
public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) {
344+
resolveIsStartedFromBackground();
345+
324346
if (isStartedFromBackground || onCreateTime != null // An activity already called onCreate()
325347
) {
326348
return;
@@ -560,8 +582,7 @@ public static boolean isScreenOn(Context appContext) {
560582
* We use StartFromBackgroundRunnable to detect if app is started from background or foreground.
561583
* If app is started from background, we do not generate AppStart trace. This runnable is posted
562584
* to main UI thread from FirebasePerfEarly. If app is started from background, this runnable will
563-
* be executed before any activity's onCreate() method. If app is started from foreground,
564-
* activity's onCreate() method is executed before this runnable.
585+
* be executed earlier than 100ms of any activity's onCreate() method.
565586
*/
566587
public static class StartFromBackgroundRunnable implements Runnable {
567588
private final AppStartTrace trace;
@@ -572,10 +593,7 @@ public StartFromBackgroundRunnable(final AppStartTrace trace) {
572593

573594
@Override
574595
public void run() {
575-
// if no activity has ever been created.
576-
if (trace.onCreateTime == null) {
577-
trace.isStartedFromBackground = true;
578-
}
596+
trace.mainThreadRunnableTime = new Timer();
579597
}
580598
}
581599

@@ -614,7 +632,7 @@ Timer getOnResumeTime() {
614632
}
615633

616634
@VisibleForTesting
617-
void setIsStartFromBackground() {
618-
isStartedFromBackground = true;
635+
void setMainThreadRunnableTime(Timer timer) {
636+
mainThreadRunnableTime = timer;
619637
}
620638
}

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static org.mockito.ArgumentMatchers.isA;
1919
import static org.mockito.Mockito.doAnswer;
2020
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.spy;
2122
import static org.mockito.Mockito.times;
2223
import static org.mockito.Mockito.verify;
2324
import static org.mockito.Mockito.when;
@@ -238,11 +239,42 @@ public void testDelayedAppStart() {
238239
}
239240

240241
@Test
241-
public void testStartFromBackground() {
242+
public void testStartFromBackground_within100ms() {
242243
FakeScheduledExecutorService fakeExecutorService = new FakeScheduledExecutorService();
244+
Timer fakeTimer = spy(new Timer(currentTime));
243245
AppStartTrace trace =
244246
new AppStartTrace(transportManager, clock, configResolver, fakeExecutorService);
245-
trace.setIsStartFromBackground();
247+
trace.registerActivityLifecycleCallbacks(appContext);
248+
trace.setMainThreadRunnableTime(fakeTimer);
249+
250+
when(fakeTimer.getDurationMicros()).thenReturn(99L);
251+
trace.onActivityCreated(activity1, bundle);
252+
Assert.assertNotNull(trace.getOnCreateTime());
253+
++currentTime;
254+
trace.onActivityStarted(activity1);
255+
Assert.assertNotNull(trace.getOnStartTime());
256+
++currentTime;
257+
trace.onActivityResumed(activity1);
258+
Assert.assertNotNull(trace.getOnResumeTime());
259+
fakeExecutorService.runAll();
260+
// There should be a trace sent since the delay between the main thread and onActivityCreated
261+
// is limited.
262+
verify(transportManager, times(1))
263+
.log(
264+
traceArgumentCaptor.capture(),
265+
ArgumentMatchers.nullable(ApplicationProcessState.class));
266+
}
267+
268+
@Test
269+
public void testStartFromBackground_moreThan100ms() {
270+
FakeScheduledExecutorService fakeExecutorService = new FakeScheduledExecutorService();
271+
Timer fakeTimer = spy(new Timer(currentTime));
272+
AppStartTrace trace =
273+
new AppStartTrace(transportManager, clock, configResolver, fakeExecutorService);
274+
trace.registerActivityLifecycleCallbacks(appContext);
275+
trace.setMainThreadRunnableTime(fakeTimer);
276+
277+
when(fakeTimer.getDurationMicros()).thenReturn(TimeUnit.MILLISECONDS.toMicros(100) + 1);
246278
trace.onActivityCreated(activity1, bundle);
247279
Assert.assertNull(trace.getOnCreateTime());
248280
++currentTime;
@@ -252,6 +284,7 @@ public void testStartFromBackground() {
252284
trace.onActivityResumed(activity1);
253285
Assert.assertNull(trace.getOnResumeTime());
254286
// There should be no trace sent.
287+
fakeExecutorService.runAll();
255288
verify(transportManager, times(0))
256289
.log(
257290
traceArgumentCaptor.capture(),

0 commit comments

Comments
 (0)