Skip to content

Commit 45a4343

Browse files
authored
Warm start detection (#3937)
* ActivityLifecycleIntegration: - creates `onCreate` and `onStart` TimeSpans - set app start type to warm in AppStartMetrics when needed - saves SystemClock.uptimeMillis to set app start timestamp - sets start type to warm even when cold start was invalid (app was started in background, like via BroadcastReceiver) - restart app start in AppStartMetrics in perfv1, too * reverted TimeSpan.setStartUnixTimeMs to @testonly method * AppStartMetrics has now a method to restart appStartSpan and reset its uptime_ms * PerformanceAndroidEventProcessor now attaches activity start spans to warm starts, too * SentryPerformanceProvider doesn't create spans anymore * TimeSpan.setStartUnixTimeMs now shifts other timestamps accordingly
1 parent ee833e6 commit 45a4343

File tree

10 files changed

+580
-427
lines changed

10 files changed

+580
-427
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Fixes
6+
7+
- Fix warm start detection ([#3937](https://github.com/getsentry/sentry-java/pull/3937))
8+
39
## 7.19.1
410

511
### Fixes

sentry-android-core/api/sentry-android-core.api

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
2727
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
2828
public fun onActivityDestroyed (Landroid/app/Activity;)V
2929
public fun onActivityPaused (Landroid/app/Activity;)V
30+
public fun onActivityPostCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
3031
public fun onActivityPostResumed (Landroid/app/Activity;)V
32+
public fun onActivityPostStarted (Landroid/app/Activity;)V
33+
public fun onActivityPreCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
3134
public fun onActivityPrePaused (Landroid/app/Activity;)V
35+
public fun onActivityPreStarted (Landroid/app/Activity;)V
3236
public fun onActivityResumed (Landroid/app/Activity;)V
3337
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
3438
public fun onActivityStarted (Landroid/app/Activity;)V
@@ -454,17 +458,21 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
454458
public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics;
455459
public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
456460
public fun isAppLaunchedInForeground ()Z
461+
public fun isColdStartValid ()Z
457462
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
463+
public fun onAppStartSpansSent ()V
458464
public static fun onApplicationCreate (Landroid/app/Application;)V
459465
public static fun onApplicationPostCreate (Landroid/app/Application;)V
460466
public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V
461467
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
462468
public fun registerApplicationForegroundCheck (Landroid/app/Application;)V
469+
public fun restartAppStart (J)V
463470
public fun setAppLaunchedInForeground (Z)V
464471
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
465472
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
466473
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
467474
public fun setClassLoadedUptimeMs (J)V
475+
public fun shouldSendStartMeasurements ()Z
468476
}
469477

470478
public final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum {

sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java

Lines changed: 126 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
import android.os.Bundle;
1010
import android.os.Handler;
1111
import android.os.Looper;
12-
import android.view.View;
13-
import androidx.annotation.NonNull;
12+
import android.os.SystemClock;
1413
import io.sentry.FullyDisplayedReporter;
1514
import io.sentry.IHub;
1615
import io.sentry.IScope;
@@ -29,6 +28,7 @@
2928
import io.sentry.TransactionOptions;
3029
import io.sentry.android.core.internal.util.ClassUtil;
3130
import io.sentry.android.core.internal.util.FirstDrawDoneListener;
31+
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
3232
import io.sentry.android.core.performance.AppStartMetrics;
3333
import io.sentry.android.core.performance.TimeSpan;
3434
import io.sentry.protocol.MeasurementValue;
@@ -77,8 +77,10 @@ public final class ActivityLifecycleIntegration
7777
private @Nullable ISpan appStartSpan;
7878
private final @NotNull WeakHashMap<Activity, ISpan> ttidSpanMap = new WeakHashMap<>();
7979
private final @NotNull WeakHashMap<Activity, ISpan> ttfdSpanMap = new WeakHashMap<>();
80+
private final @NotNull WeakHashMap<Activity, ActivityLifecycleTimeSpan> activityLifecycleMap =
81+
new WeakHashMap<>();
8082
private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
81-
private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper());
83+
private long lastPausedUptimeMillis = 0;
8284
private @Nullable Future<?> ttfdAutoCloseFuture = null;
8385

8486
// WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
@@ -369,9 +371,32 @@ private void finishTransaction(
369371
}
370372
}
371373

374+
@Override
375+
public void onActivityPreCreated(
376+
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
377+
// The very first activity start timestamp cannot be set to the class instantiation time, as it
378+
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
379+
// here.
380+
if (firstActivityCreated) {
381+
return;
382+
}
383+
lastPausedTime =
384+
hub != null
385+
? hub.getOptions().getDateProvider().now()
386+
: AndroidDateUtils.getCurrentSentryDateTime();
387+
lastPausedUptimeMillis = SystemClock.uptimeMillis();
388+
389+
final @NotNull ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan();
390+
timeSpan.getOnCreate().setStartedAt(lastPausedUptimeMillis);
391+
activityLifecycleMap.put(activity, timeSpan);
392+
}
393+
372394
@Override
373395
public synchronized void onActivityCreated(
374396
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
397+
if (!isAllActivityCallbacksAvailable) {
398+
onActivityPreCreated(activity, savedInstanceState);
399+
}
375400
setColdStart(savedInstanceState);
376401
if (hub != null && options != null && options.isEnableScreenTracking()) {
377402
final @Nullable String activityClassName = ClassUtil.getClassName(activity);
@@ -387,8 +412,38 @@ public synchronized void onActivityCreated(
387412
}
388413
}
389414

415+
@Override
416+
public void onActivityPostCreated(
417+
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
418+
if (appStartSpan == null) {
419+
activityLifecycleMap.remove(activity);
420+
return;
421+
}
422+
423+
final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity);
424+
if (timeSpan != null) {
425+
timeSpan.getOnCreate().stop();
426+
timeSpan.getOnCreate().setDescription(activity.getClass().getName() + ".onCreate");
427+
}
428+
}
429+
430+
@Override
431+
public void onActivityPreStarted(final @NotNull Activity activity) {
432+
if (appStartSpan == null) {
433+
return;
434+
}
435+
final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity);
436+
if (timeSpan != null) {
437+
timeSpan.getOnStart().setStartedAt(SystemClock.uptimeMillis());
438+
}
439+
}
440+
390441
@Override
391442
public synchronized void onActivityStarted(final @NotNull Activity activity) {
443+
if (!isAllActivityCallbacksAvailable) {
444+
onActivityPostCreated(activity, null);
445+
onActivityPreStarted(activity);
446+
}
392447
if (performanceEnabled) {
393448
// The docs on the screen rendering performance tracing
394449
// (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition),
@@ -400,74 +455,75 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) {
400455
}
401456
}
402457

458+
@Override
459+
public void onActivityPostStarted(final @NotNull Activity activity) {
460+
final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.remove(activity);
461+
if (appStartSpan == null) {
462+
return;
463+
}
464+
if (timeSpan != null) {
465+
timeSpan.getOnStart().stop();
466+
timeSpan.getOnStart().setDescription(activity.getClass().getName() + ".onStart");
467+
AppStartMetrics.getInstance().addActivityLifecycleTimeSpans(timeSpan);
468+
}
469+
}
470+
403471
@Override
404472
public synchronized void onActivityResumed(final @NotNull Activity activity) {
473+
if (!isAllActivityCallbacksAvailable) {
474+
onActivityPostStarted(activity);
475+
}
405476
if (performanceEnabled) {
406-
407477
final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity);
408478
final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity);
409-
final View rootView = activity.findViewById(android.R.id.content);
410-
if (rootView != null) {
479+
if (activity.getWindow() != null) {
411480
FirstDrawDoneListener.registerForNextDraw(
412-
rootView, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
481+
activity, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
413482
} else {
414483
// Posting a task to the main thread's handler will make it executed after it finished
415484
// its current job. That is, right after the activity draws the layout.
416-
mainHandler.post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
485+
new Handler(Looper.getMainLooper()).post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
417486
}
418487
}
419488
}
420489

421490
@Override
422-
public void onActivityPostResumed(@NonNull Activity activity) {
491+
public void onActivityPostResumed(@NotNull Activity activity) {
423492
// empty override, required to avoid a api-level breaking super.onActivityPostResumed() calls
424493
}
425494

426495
@Override
427-
public void onActivityPrePaused(@NonNull Activity activity) {
496+
public void onActivityPrePaused(@NotNull Activity activity) {
428497
// only executed if API >= 29 otherwise it happens on onActivityPaused
429-
if (isAllActivityCallbacksAvailable) {
430-
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
431-
// well
432-
// this ensures any newly launched activity will not use the app start timestamp as txn start
433-
firstActivityCreated = true;
434-
if (hub == null) {
435-
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
436-
} else {
437-
lastPausedTime = hub.getOptions().getDateProvider().now();
438-
}
439-
}
498+
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
499+
// well
500+
// this ensures any newly launched activity will not use the app start timestamp as txn start
501+
firstActivityCreated = true;
502+
lastPausedTime =
503+
hub != null
504+
? hub.getOptions().getDateProvider().now()
505+
: AndroidDateUtils.getCurrentSentryDateTime();
506+
lastPausedUptimeMillis = SystemClock.uptimeMillis();
440507
}
441508

442509
@Override
443510
public synchronized void onActivityPaused(final @NotNull Activity activity) {
444511
// only executed if API < 29 otherwise it happens on onActivityPrePaused
445512
if (!isAllActivityCallbacksAvailable) {
446-
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
447-
// well
448-
// this ensures any newly launched activity will not use the app start timestamp as txn start
449-
firstActivityCreated = true;
450-
if (hub == null) {
451-
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
452-
} else {
453-
lastPausedTime = hub.getOptions().getDateProvider().now();
454-
}
513+
onActivityPrePaused(activity);
455514
}
456515
}
457516

458517
@Override
459-
public synchronized void onActivityStopped(final @NotNull Activity activity) {
460-
// no-op
461-
}
518+
public void onActivityStopped(final @NotNull Activity activity) {}
462519

463520
@Override
464-
public synchronized void onActivitySaveInstanceState(
465-
final @NotNull Activity activity, final @NotNull Bundle outState) {
466-
// no-op
467-
}
521+
public void onActivitySaveInstanceState(
522+
final @NotNull Activity activity, final @NotNull Bundle outState) {}
468523

469524
@Override
470525
public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
526+
activityLifecycleMap.remove(activity);
471527
if (performanceEnabled) {
472528

473529
// in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid
@@ -494,10 +550,20 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
494550
}
495551

496552
// clear it up, so we don't start again for the same activity if the activity is in the
497-
// activity
498-
// stack still.
553+
// activity stack still.
499554
// if the activity is opened again and not in memory, transactions will be created normally.
500555
activitiesWithOngoingTransactions.remove(activity);
556+
557+
if (activitiesWithOngoingTransactions.isEmpty()) {
558+
clear();
559+
}
560+
}
561+
562+
private void clear() {
563+
firstActivityCreated = false;
564+
lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
565+
lastPausedUptimeMillis = 0;
566+
activityLifecycleMap.clear();
501567
}
502568

503569
private void finishSpan(final @Nullable ISpan span) {
@@ -604,6 +670,17 @@ WeakHashMap<Activity, ITransaction> getActivitiesWithOngoingTransactions() {
604670
return activitiesWithOngoingTransactions;
605671
}
606672

673+
@TestOnly
674+
@NotNull
675+
WeakHashMap<Activity, ActivityLifecycleTimeSpan> getActivityLifecycleMap() {
676+
return activityLifecycleMap;
677+
}
678+
679+
@TestOnly
680+
void setFirstActivityCreated(boolean firstActivityCreated) {
681+
this.firstActivityCreated = firstActivityCreated;
682+
}
683+
607684
@TestOnly
608685
@NotNull
609686
ActivityFramesTracker getActivityFramesTracker() {
@@ -629,20 +706,17 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
629706
}
630707

631708
private void setColdStart(final @Nullable Bundle savedInstanceState) {
632-
// The very first activity start timestamp cannot be set to the class instantiation time, as it
633-
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
634-
// here.
635-
if (hub != null && lastPausedTime.nanoTimestamp() == 0) {
636-
lastPausedTime = hub.getOptions().getDateProvider().now();
637-
} else if (lastPausedTime.nanoTimestamp() == 0) {
638-
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
639-
}
640709
if (!firstActivityCreated) {
641-
// if Activity has savedInstanceState then its a warm start
642-
// https://developer.android.com/topic/performance/vitals/launch-time#warm
643-
// SentryPerformanceProvider sets this already
644-
// pre-performance-v2: back-fill with best guess
645-
if (options != null && !options.isEnablePerformanceV2()) {
710+
final @NotNull TimeSpan appStartSpan = AppStartMetrics.getInstance().getAppStartTimeSpan();
711+
// If the app start span already started and stopped, it means the app restarted without
712+
// killing the process, so we are in a warm start
713+
// If the app has an invalid cold start, it means it was started in the background, like
714+
// via BroadcastReceiver, so we consider it a warm start
715+
if ((appStartSpan.hasStarted() && appStartSpan.hasStopped())
716+
|| (!AppStartMetrics.getInstance().isColdStartValid())) {
717+
AppStartMetrics.getInstance().restartAppStart(lastPausedUptimeMillis);
718+
AppStartMetrics.getInstance().setAppStartType(AppStartMetrics.AppStartType.WARM);
719+
} else {
646720
AppStartMetrics.getInstance()
647721
.setAppStartType(
648722
savedInstanceState == null

0 commit comments

Comments
 (0)