Skip to content

Commit

Permalink
Warm start detection (#3937)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stefanosiano authored Dec 23, 2024
1 parent ee833e6 commit 45a4343
Show file tree
Hide file tree
Showing 10 changed files with 580 additions and 427 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Fixes

- Fix warm start detection ([#3937](https://github.com/getsentry/sentry-java/pull/3937))

## 7.19.1

### Fixes
Expand Down
8 changes: 8 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityDestroyed (Landroid/app/Activity;)V
public fun onActivityPaused (Landroid/app/Activity;)V
public fun onActivityPostCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityPostResumed (Landroid/app/Activity;)V
public fun onActivityPostStarted (Landroid/app/Activity;)V
public fun onActivityPreCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityPrePaused (Landroid/app/Activity;)V
public fun onActivityPreStarted (Landroid/app/Activity;)V
public fun onActivityResumed (Landroid/app/Activity;)V
public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onActivityStarted (Landroid/app/Activity;)V
Expand Down Expand Up @@ -454,17 +458,21 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics;
public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun isAppLaunchedInForeground ()Z
public fun isColdStartValid ()Z
public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
public fun onAppStartSpansSent ()V
public static fun onApplicationCreate (Landroid/app/Application;)V
public static fun onApplicationPostCreate (Landroid/app/Application;)V
public static fun onContentProviderCreate (Landroid/content/ContentProvider;)V
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
public fun registerApplicationForegroundCheck (Landroid/app/Application;)V
public fun restartAppStart (J)V
public fun setAppLaunchedInForeground (Z)V
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
public fun setClassLoadedUptimeMs (J)V
public fun shouldSendStartMeasurements ()Z
}

public final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import androidx.annotation.NonNull;
import android.os.SystemClock;
import io.sentry.FullyDisplayedReporter;
import io.sentry.IHub;
import io.sentry.IScope;
Expand All @@ -29,6 +28,7 @@
import io.sentry.TransactionOptions;
import io.sentry.android.core.internal.util.ClassUtil;
import io.sentry.android.core.internal.util.FirstDrawDoneListener;
import io.sentry.android.core.performance.ActivityLifecycleTimeSpan;
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.protocol.MeasurementValue;
Expand Down Expand Up @@ -77,8 +77,10 @@ public final class ActivityLifecycleIntegration
private @Nullable ISpan appStartSpan;
private final @NotNull WeakHashMap<Activity, ISpan> ttidSpanMap = new WeakHashMap<>();
private final @NotNull WeakHashMap<Activity, ISpan> ttfdSpanMap = new WeakHashMap<>();
private final @NotNull WeakHashMap<Activity, ActivityLifecycleTimeSpan> activityLifecycleMap =
new WeakHashMap<>();
private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper());
private long lastPausedUptimeMillis = 0;
private @Nullable Future<?> ttfdAutoCloseFuture = null;

// WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
Expand Down Expand Up @@ -369,9 +371,32 @@ private void finishTransaction(
}
}

@Override
public void onActivityPreCreated(
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
// The very first activity start timestamp cannot be set to the class instantiation time, as it
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
// here.
if (firstActivityCreated) {
return;
}
lastPausedTime =
hub != null
? hub.getOptions().getDateProvider().now()
: AndroidDateUtils.getCurrentSentryDateTime();
lastPausedUptimeMillis = SystemClock.uptimeMillis();

final @NotNull ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan();
timeSpan.getOnCreate().setStartedAt(lastPausedUptimeMillis);
activityLifecycleMap.put(activity, timeSpan);
}

@Override
public synchronized void onActivityCreated(
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
if (!isAllActivityCallbacksAvailable) {
onActivityPreCreated(activity, savedInstanceState);
}
setColdStart(savedInstanceState);
if (hub != null && options != null && options.isEnableScreenTracking()) {
final @Nullable String activityClassName = ClassUtil.getClassName(activity);
Expand All @@ -387,8 +412,38 @@ public synchronized void onActivityCreated(
}
}

@Override
public void onActivityPostCreated(
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
if (appStartSpan == null) {
activityLifecycleMap.remove(activity);
return;
}

final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity);
if (timeSpan != null) {
timeSpan.getOnCreate().stop();
timeSpan.getOnCreate().setDescription(activity.getClass().getName() + ".onCreate");
}
}

@Override
public void onActivityPreStarted(final @NotNull Activity activity) {
if (appStartSpan == null) {
return;
}
final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity);
if (timeSpan != null) {
timeSpan.getOnStart().setStartedAt(SystemClock.uptimeMillis());
}
}

@Override
public synchronized void onActivityStarted(final @NotNull Activity activity) {
if (!isAllActivityCallbacksAvailable) {
onActivityPostCreated(activity, null);
onActivityPreStarted(activity);
}
if (performanceEnabled) {
// The docs on the screen rendering performance tracing
// (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition),
Expand All @@ -400,74 +455,75 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) {
}
}

@Override
public void onActivityPostStarted(final @NotNull Activity activity) {
final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.remove(activity);
if (appStartSpan == null) {
return;
}
if (timeSpan != null) {
timeSpan.getOnStart().stop();
timeSpan.getOnStart().setDescription(activity.getClass().getName() + ".onStart");
AppStartMetrics.getInstance().addActivityLifecycleTimeSpans(timeSpan);
}
}

@Override
public synchronized void onActivityResumed(final @NotNull Activity activity) {
if (!isAllActivityCallbacksAvailable) {
onActivityPostStarted(activity);
}
if (performanceEnabled) {

final @Nullable ISpan ttidSpan = ttidSpanMap.get(activity);
final @Nullable ISpan ttfdSpan = ttfdSpanMap.get(activity);
final View rootView = activity.findViewById(android.R.id.content);
if (rootView != null) {
if (activity.getWindow() != null) {
FirstDrawDoneListener.registerForNextDraw(
rootView, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
activity, () -> onFirstFrameDrawn(ttfdSpan, ttidSpan), buildInfoProvider);
} else {
// Posting a task to the main thread's handler will make it executed after it finished
// its current job. That is, right after the activity draws the layout.
mainHandler.post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
new Handler(Looper.getMainLooper()).post(() -> onFirstFrameDrawn(ttfdSpan, ttidSpan));
}
}
}

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

@Override
public void onActivityPrePaused(@NonNull Activity activity) {
public void onActivityPrePaused(@NotNull Activity activity) {
// only executed if API >= 29 otherwise it happens on onActivityPaused
if (isAllActivityCallbacksAvailable) {
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
// well
// this ensures any newly launched activity will not use the app start timestamp as txn start
firstActivityCreated = true;
if (hub == null) {
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
} else {
lastPausedTime = hub.getOptions().getDateProvider().now();
}
}
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
// well
// this ensures any newly launched activity will not use the app start timestamp as txn start
firstActivityCreated = true;
lastPausedTime =
hub != null
? hub.getOptions().getDateProvider().now()
: AndroidDateUtils.getCurrentSentryDateTime();
lastPausedUptimeMillis = SystemClock.uptimeMillis();
}

@Override
public synchronized void onActivityPaused(final @NotNull Activity activity) {
// only executed if API < 29 otherwise it happens on onActivityPrePaused
if (!isAllActivityCallbacksAvailable) {
// as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
// well
// this ensures any newly launched activity will not use the app start timestamp as txn start
firstActivityCreated = true;
if (hub == null) {
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
} else {
lastPausedTime = hub.getOptions().getDateProvider().now();
}
onActivityPrePaused(activity);
}
}

@Override
public synchronized void onActivityStopped(final @NotNull Activity activity) {
// no-op
}
public void onActivityStopped(final @NotNull Activity activity) {}

@Override
public synchronized void onActivitySaveInstanceState(
final @NotNull Activity activity, final @NotNull Bundle outState) {
// no-op
}
public void onActivitySaveInstanceState(
final @NotNull Activity activity, final @NotNull Bundle outState) {}

@Override
public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
activityLifecycleMap.remove(activity);
if (performanceEnabled) {

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

// clear it up, so we don't start again for the same activity if the activity is in the
// activity
// stack still.
// activity stack still.
// if the activity is opened again and not in memory, transactions will be created normally.
activitiesWithOngoingTransactions.remove(activity);

if (activitiesWithOngoingTransactions.isEmpty()) {
clear();
}
}

private void clear() {
firstActivityCreated = false;
lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
lastPausedUptimeMillis = 0;
activityLifecycleMap.clear();
}

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

@TestOnly
@NotNull
WeakHashMap<Activity, ActivityLifecycleTimeSpan> getActivityLifecycleMap() {
return activityLifecycleMap;
}

@TestOnly
void setFirstActivityCreated(boolean firstActivityCreated) {
this.firstActivityCreated = firstActivityCreated;
}

@TestOnly
@NotNull
ActivityFramesTracker getActivityFramesTracker() {
Expand All @@ -629,20 +706,17 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
}

private void setColdStart(final @Nullable Bundle savedInstanceState) {
// The very first activity start timestamp cannot be set to the class instantiation time, as it
// may happen before an activity is started (service, broadcast receiver, etc). So we set it
// here.
if (hub != null && lastPausedTime.nanoTimestamp() == 0) {
lastPausedTime = hub.getOptions().getDateProvider().now();
} else if (lastPausedTime.nanoTimestamp() == 0) {
lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime();
}
if (!firstActivityCreated) {
// if Activity has savedInstanceState then its a warm start
// https://developer.android.com/topic/performance/vitals/launch-time#warm
// SentryPerformanceProvider sets this already
// pre-performance-v2: back-fill with best guess
if (options != null && !options.isEnablePerformanceV2()) {
final @NotNull TimeSpan appStartSpan = AppStartMetrics.getInstance().getAppStartTimeSpan();
// If the app start span already started and stopped, it means the app restarted without
// killing the process, so we are in a warm start
// If the app has an invalid cold start, it means it was started in the background, like
// via BroadcastReceiver, so we consider it a warm start
if ((appStartSpan.hasStarted() && appStartSpan.hasStopped())
|| (!AppStartMetrics.getInstance().isColdStartValid())) {
AppStartMetrics.getInstance().restartAppStart(lastPausedUptimeMillis);
AppStartMetrics.getInstance().setAppStartType(AppStartMetrics.AppStartType.WARM);
} else {
AppStartMetrics.getInstance()
.setAppStartType(
savedInstanceState == null
Expand Down
Loading

0 comments on commit 45a4343

Please sign in to comment.