Skip to content

Commit e7047cb

Browse files
committed
fix(android): Improve app start type detection with main thread timing
1 parent d15471f commit e7047cb

File tree

2 files changed

+140
-3
lines changed

2 files changed

+140
-3
lines changed

sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public enum AppStartType {
5757

5858
private @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
5959
private boolean appLaunchedInForeground;
60+
private volatile long firstPostUptimeMillis = -1;
6061

6162
private final @NotNull TimeSpan appStartSpan;
6263
private final @NotNull TimeSpan sdkInitTimeSpan;
@@ -234,6 +235,7 @@ public void clear() {
234235
shouldSendStartMeasurements = true;
235236
firstDrawDone.set(false);
236237
activeActivitiesCounter.set(0);
238+
firstPostUptimeMillis = -1;
237239
}
238240

239241
public @Nullable ITransactionProfiler getAppStartProfiler() {
@@ -316,7 +318,15 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
316318
// (possibly others) the first task posted on the main thread is called before the
317319
// Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
318320
// callback is called before the application one.
319-
new Handler(Looper.getMainLooper()).post(() -> checkCreateTimeOnMain());
321+
new Handler(Looper.getMainLooper())
322+
.post(
323+
new Runnable() {
324+
@Override
325+
public void run() {
326+
firstPostUptimeMillis = SystemClock.uptimeMillis();
327+
checkCreateTimeOnMain();
328+
}
329+
});
320330
}
321331

322332
private void checkCreateTimeOnMain() {
@@ -348,7 +358,7 @@ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle saved
348358
if (activeActivitiesCounter.incrementAndGet() == 1 && !firstDrawDone.get()) {
349359
final long nowUptimeMs = SystemClock.uptimeMillis();
350360

351-
// If the app (process) was launched more than 1 minute ago, it's likely wrong
361+
// If the app (process) was launched more than 1 minute ago, consider it a warm start
352362
final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan.getStartUptimeMs();
353363
if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit.MINUTES.toMillis(1)) {
354364
appStartType = AppStartType.WARM;
@@ -360,8 +370,12 @@ public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle saved
360370
CLASS_LOADED_UPTIME_MS = nowUptimeMs;
361371
contentProviderOnCreates.clear();
362372
applicationOnCreate.reset();
373+
} else if (savedInstanceState != null) {
374+
appStartType = AppStartType.WARM;
375+
} else if (firstPostUptimeMillis > 0 && nowUptimeMs > firstPostUptimeMillis) {
376+
appStartType = AppStartType.WARM;
363377
} else {
364-
appStartType = savedInstanceState == null ? AppStartType.COLD : AppStartType.WARM;
378+
appStartType = AppStartType.COLD;
365379
}
366380
}
367381
appLaunchedInForeground = true;

sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,4 +537,127 @@ class AppStartMetricsTest {
537537

538538
assertEquals(secondActivity, CurrentActivityHolder.getInstance().activity)
539539
}
540+
541+
@Test
542+
fun `firstPostUptimeMillis is properly cleared`() {
543+
val metrics = AppStartMetrics.getInstance()
544+
metrics.registerLifecycleCallbacks(mock<Application>())
545+
Shadows.shadowOf(Looper.getMainLooper()).idle()
546+
547+
val reflectionField = AppStartMetrics::class.java.getDeclaredField("firstPostUptimeMillis")
548+
reflectionField.isAccessible = true
549+
val firstPostValue = reflectionField.getLong(metrics)
550+
assertTrue(firstPostValue > 0)
551+
552+
metrics.clear()
553+
554+
val clearedValue = reflectionField.getLong(metrics)
555+
assertEquals(-1, clearedValue)
556+
}
557+
558+
@Test
559+
fun `firstPostUptimeMillis is set when registerLifecycleCallbacks is called`() {
560+
val metrics = AppStartMetrics.getInstance()
561+
val beforeRegister = SystemClock.uptimeMillis()
562+
563+
metrics.registerLifecycleCallbacks(mock<Application>())
564+
Shadows.shadowOf(Looper.getMainLooper()).idle()
565+
566+
val afterIdle = SystemClock.uptimeMillis()
567+
568+
val reflectionField = AppStartMetrics::class.java.getDeclaredField("firstPostUptimeMillis")
569+
reflectionField.isAccessible = true
570+
val firstPostValue = reflectionField.getLong(metrics)
571+
572+
assertTrue(firstPostValue >= beforeRegister)
573+
assertTrue(firstPostValue <= afterIdle)
574+
}
575+
576+
@Test
577+
fun `Sets app launch type to WARM when activity created after firstPost`() {
578+
val metrics = AppStartMetrics.getInstance()
579+
assertEquals(AppStartMetrics.AppStartType.UNKNOWN, metrics.appStartType)
580+
581+
metrics.registerLifecycleCallbacks(mock<Application>())
582+
Shadows.shadowOf(Looper.getMainLooper()).idle()
583+
584+
SystemClock.setCurrentTimeMillis(SystemClock.uptimeMillis() + 100)
585+
metrics.onActivityCreated(mock<Activity>(), null)
586+
587+
assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
588+
}
589+
590+
@Test
591+
fun `Sets app launch type to COLD when activity created before firstPost executes`() {
592+
val metrics = AppStartMetrics.getInstance()
593+
assertEquals(AppStartMetrics.AppStartType.UNKNOWN, metrics.appStartType)
594+
595+
metrics.registerLifecycleCallbacks(mock<Application>())
596+
metrics.onActivityCreated(mock<Activity>(), null)
597+
598+
assertEquals(AppStartMetrics.AppStartType.COLD, metrics.appStartType)
599+
600+
Shadows.shadowOf(Looper.getMainLooper()).idle()
601+
602+
assertEquals(AppStartMetrics.AppStartType.COLD, metrics.appStartType)
603+
}
604+
605+
@Test
606+
fun `Sets app launch type to COLD when activity created at same time as firstPost`() {
607+
val metrics = AppStartMetrics.getInstance()
608+
609+
val now = SystemClock.uptimeMillis()
610+
val reflectionField = AppStartMetrics::class.java.getDeclaredField("firstPostUptimeMillis")
611+
reflectionField.isAccessible = true
612+
reflectionField.setLong(metrics, now)
613+
614+
SystemClock.setCurrentTimeMillis(now)
615+
metrics.onActivityCreated(mock<Activity>(), null)
616+
617+
assertEquals(AppStartMetrics.AppStartType.COLD, metrics.appStartType)
618+
}
619+
620+
@Test
621+
fun `savedInstanceState check takes precedence over firstPost timing`() {
622+
val metrics = AppStartMetrics.getInstance()
623+
624+
metrics.registerLifecycleCallbacks(mock<Application>())
625+
Shadows.shadowOf(Looper.getMainLooper()).idle()
626+
627+
SystemClock.setCurrentTimeMillis(SystemClock.uptimeMillis() + 100)
628+
metrics.onActivityCreated(mock<Activity>(), mock<Bundle>())
629+
630+
assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
631+
}
632+
633+
@Test
634+
fun `timeout check takes precedence over firstPost timing`() {
635+
val metrics = AppStartMetrics.getInstance()
636+
637+
metrics.registerLifecycleCallbacks(mock<Application>())
638+
Shadows.shadowOf(Looper.getMainLooper()).idle()
639+
640+
val futureTime = SystemClock.uptimeMillis() + TimeUnit.MINUTES.toMillis(2)
641+
SystemClock.setCurrentTimeMillis(futureTime)
642+
metrics.onActivityCreated(mock<Activity>(), null)
643+
644+
assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
645+
assertTrue(metrics.appStartTimeSpan.hasStarted())
646+
assertEquals(futureTime, metrics.appStartTimeSpan.startUptimeMs)
647+
}
648+
649+
@Test
650+
fun `firstPost timing does not affect subsequent activity creations`() {
651+
val metrics = AppStartMetrics.getInstance()
652+
653+
metrics.registerLifecycleCallbacks(mock<Application>())
654+
Shadows.shadowOf(Looper.getMainLooper()).idle()
655+
656+
SystemClock.setCurrentTimeMillis(SystemClock.uptimeMillis() + 100)
657+
metrics.onActivityCreated(mock<Activity>(), null)
658+
assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
659+
660+
metrics.onActivityCreated(mock<Activity>(), mock<Bundle>())
661+
assertEquals(AppStartMetrics.AppStartType.WARM, metrics.appStartType)
662+
}
540663
}

0 commit comments

Comments
 (0)