From 55f0201408454350292c71aea603d5580e6455c7 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 28 Nov 2024 16:44:21 +0100 Subject: [PATCH 01/15] ActivityLifecycleIntegration: - creates `onCreate` and `onStart` TimeSpans - set app start type to warm in AppStartMetrics when needed 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 --- .../core/ActivityLifecycleIntegration.java | 165 +++++++++++++----- .../PerformanceAndroidEventProcessor.java | 132 +++++++------- .../core/SentryPerformanceProvider.java | 94 +--------- .../core/performance/AppStartMetrics.java | 32 +++- .../android/core/performance/TimeSpan.java | 6 +- 5 files changed, 223 insertions(+), 206 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 14b7ec98fb2..809161a946e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -9,8 +9,8 @@ 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.DateUtils; import io.sentry.FullyDisplayedReporter; import io.sentry.IHub; import io.sentry.IScope; @@ -29,6 +29,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; @@ -77,8 +78,9 @@ public final class ActivityLifecycleIntegration private @Nullable ISpan appStartSpan; private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>(); private final @NotNull WeakHashMap ttfdSpanMap = new WeakHashMap<>(); + private final @NotNull WeakHashMap activityLifecycleMap = + new WeakHashMap<>(); private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0); - private final @NotNull Handler mainHandler = new Handler(Looper.getMainLooper()); private @Nullable Future ttfdAutoCloseFuture = null; // WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the @@ -369,9 +371,33 @@ 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(); + + final @NotNull ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan(); + timeSpan + .getOnCreate() + .setStartUnixTimeMs((long) DateUtils.nanosToMillis(lastPausedTime.nanoTimestamp())); + 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); @@ -387,8 +413,39 @@ 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) { + final long now = SystemClock.uptimeMillis(); + if (appStartSpan == null) { + return; + } + final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity); + if (timeSpan != null) { + timeSpan.getOnStart().setStartedAt(now); + } + } + @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), @@ -400,42 +457,54 @@ 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; + if (hub == null) { + lastPausedTime = AndroidDateUtils.getCurrentSentryDateTime(); + } else { + lastPausedTime = hub.getOptions().getDateProvider().now(); } } @@ -443,31 +512,20 @@ public void onActivityPrePaused(@NonNull Activity activity) { 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 @@ -494,10 +552,19 @@ 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); + activityLifecycleMap.clear(); } private void finishSpan(final @Nullable ISpan span) { @@ -629,14 +696,6 @@ WeakHashMap 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 @@ -648,6 +707,20 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { savedInstanceState == null ? AppStartMetrics.AppStartType.COLD : AppStartMetrics.AppStartType.WARM); + } else { + TimeSpan appStartSpan = AppStartMetrics.getInstance().getAppStartTimeSpan(); + // If the app start span already started and stopped, it means we are in a warm start + if (appStartSpan.hasStarted() && appStartSpan.hasStopped()) { + AppStartMetrics.getInstance() + .restartAppStart((long) DateUtils.nanosToMillis(lastPausedTime.nanoTimestamp())); + AppStartMetrics.getInstance().setAppStartType(AppStartMetrics.AppStartType.WARM); + } else { + AppStartMetrics.getInstance() + .setAppStartType( + savedInstanceState == null + ? AppStartMetrics.AppStartType.COLD + : AppStartMetrics.AppStartType.WARM); + } } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 00ba9122e7f..242902b339c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -78,12 +78,13 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { return transaction; } + final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); // the app start measurement is only sent once and only if the transaction has // the app.start span, which is automatically created by the SDK. if (hasAppStartSpan(transaction)) { - if (!sentStartMeasurement) { + if (appStartMetrics.shouldSendStartMeasurements()) { final @NotNull TimeSpan appStartTimeSpan = - AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options); + appStartMetrics.getAppStartTimeSpanWithFallback(options); final long appStartUpDurationMs = appStartTimeSpan.getDurationMs(); // if appStartUpDurationMs is 0, metrics are not ready to be sent @@ -93,14 +94,14 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { (float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName()); final String appStartKey = - AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD + appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD ? MeasurementValue.KEY_APP_START_COLD : MeasurementValue.KEY_APP_START_WARM; transaction.getMeasurements().put(appStartKey, value); - attachColdAppStartSpans(AppStartMetrics.getInstance(), transaction); - sentStartMeasurement = true; + attachAppStartSpans(appStartMetrics, transaction); + appStartMetrics.onAppStartSpansSent(); } } @@ -110,9 +111,7 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { transaction.getContexts().setApp(appContext); } final String appStartType = - AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD - ? "cold" - : "warm"; + appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD ? "cold" : "warm"; appContext.setStartType(appStartType); } @@ -217,11 +216,11 @@ private boolean hasAppStartSpan(final @NotNull SentryTransaction txn) { || context.getOperation().equals(APP_START_WARM)); } - private void attachColdAppStartSpans( + private void attachAppStartSpans( final @NotNull AppStartMetrics appStartMetrics, final @NotNull SentryTransaction txn) { - // data will be filled only for cold app starts - if (appStartMetrics.getAppStartType() != AppStartMetrics.AppStartType.COLD) { + // data will be filled only for cold and warm app starts + if (appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.UNKNOWN) { return; } @@ -235,76 +234,79 @@ private void attachColdAppStartSpans( @Nullable SpanId parentSpanId = null; final @NotNull List spans = txn.getSpans(); for (final @NotNull SentrySpan span : spans) { - if (span.getOp().contentEquals(APP_START_COLD)) { + if (span.getOp().contentEquals(APP_START_COLD) + || span.getOp().contentEquals(APP_START_WARM)) { parentSpanId = span.getSpanId(); break; } } - // Process init - final long classInitUptimeMs = appStartMetrics.getClassLoadedUptimeMs(); - final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan(); - if (appStartTimeSpan.hasStarted() - && Math.abs(classInitUptimeMs - appStartTimeSpan.getStartUptimeMs()) - <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { - final @NotNull TimeSpan processInitTimeSpan = new TimeSpan(); - processInitTimeSpan.setStartedAt(appStartTimeSpan.getStartUptimeMs()); - processInitTimeSpan.setStartUnixTimeMs(appStartTimeSpan.getStartTimestampMs()); - - processInitTimeSpan.setStoppedAt(classInitUptimeMs); - processInitTimeSpan.setDescription("Process Initialization"); - - txn.getSpans() - .add( - timeSpanToSentrySpan( - processInitTimeSpan, parentSpanId, traceId, APP_METRICS_PROCESS_INIT_OP)); - } + // We include process init, content providers and application.onCreate spans only on cold start + if (appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD) { + // Process init + final long classInitUptimeMs = appStartMetrics.getClassLoadedUptimeMs(); + final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan(); + if (appStartTimeSpan.hasStarted() + && Math.abs(classInitUptimeMs - appStartTimeSpan.getStartUptimeMs()) + <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { + final @NotNull TimeSpan processInitTimeSpan = new TimeSpan(); + processInitTimeSpan.setStartedAt(appStartTimeSpan.getStartUptimeMs()); + processInitTimeSpan.setStartUnixTimeMs(appStartTimeSpan.getStartTimestampMs()); + + processInitTimeSpan.setStoppedAt(classInitUptimeMs); + processInitTimeSpan.setDescription("Process Initialization"); - // Content Providers - final @NotNull List contentProviderOnCreates = - appStartMetrics.getContentProviderOnCreateTimeSpans(); - if (!contentProviderOnCreates.isEmpty()) { - for (final @NotNull TimeSpan contentProvider : contentProviderOnCreates) { txn.getSpans() .add( timeSpanToSentrySpan( - contentProvider, parentSpanId, traceId, APP_METRICS_CONTENT_PROVIDER_OP)); + processInitTimeSpan, parentSpanId, traceId, APP_METRICS_PROCESS_INIT_OP)); } - } - // Application.onCreate - final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan(); - if (appOnCreate.hasStopped()) { - txn.getSpans() - .add( - timeSpanToSentrySpan(appOnCreate, parentSpanId, traceId, APP_METRICS_APPLICATION_OP)); + // Content Providers + final @NotNull List contentProviderOnCreates = + appStartMetrics.getContentProviderOnCreateTimeSpans(); + if (!contentProviderOnCreates.isEmpty()) { + for (final @NotNull TimeSpan contentProvider : contentProviderOnCreates) { + txn.getSpans() + .add( + timeSpanToSentrySpan( + contentProvider, parentSpanId, traceId, APP_METRICS_CONTENT_PROVIDER_OP)); + } + } + + // Application.onCreate + final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan(); + if (appOnCreate.hasStopped()) { + txn.getSpans() + .add( + timeSpanToSentrySpan( + appOnCreate, parentSpanId, traceId, APP_METRICS_APPLICATION_OP)); + } } // Activities final @NotNull List activityLifecycleTimeSpans = appStartMetrics.getActivityLifecycleTimeSpans(); - if (!activityLifecycleTimeSpans.isEmpty()) { - for (ActivityLifecycleTimeSpan activityTimeSpan : activityLifecycleTimeSpans) { - if (activityTimeSpan.getOnCreate().hasStarted() - && activityTimeSpan.getOnCreate().hasStopped()) { - txn.getSpans() - .add( - timeSpanToSentrySpan( - activityTimeSpan.getOnCreate(), - parentSpanId, - traceId, - APP_METRICS_ACTIVITIES_OP)); - } - if (activityTimeSpan.getOnStart().hasStarted() - && activityTimeSpan.getOnStart().hasStopped()) { - txn.getSpans() - .add( - timeSpanToSentrySpan( - activityTimeSpan.getOnStart(), - parentSpanId, - traceId, - APP_METRICS_ACTIVITIES_OP)); - } + for (ActivityLifecycleTimeSpan activityTimeSpan : activityLifecycleTimeSpans) { + if (activityTimeSpan.getOnCreate().hasStarted() + && activityTimeSpan.getOnCreate().hasStopped()) { + txn.getSpans() + .add( + timeSpanToSentrySpan( + activityTimeSpan.getOnCreate(), + parentSpanId, + traceId, + APP_METRICS_ACTIVITIES_OP)); + } + if (activityTimeSpan.getOnStart().hasStarted() + && activityTimeSpan.getOnStart().hasStopped()) { + txn.getSpans() + .add( + timeSpanToSentrySpan( + activityTimeSpan.getOnStart(), + parentSpanId, + traceId, + APP_METRICS_ACTIVITIES_OP)); } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 971ead378ff..c5778a31135 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -9,14 +9,14 @@ import android.content.pm.ProviderInfo; import android.net.Uri; import android.os.Build; -import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.os.Process; import android.os.SystemClock; import androidx.annotation.NonNull; import io.sentry.ILogger; import io.sentry.ITransactionProfiler; import io.sentry.JsonSerializer; -import io.sentry.NoOpLogger; import io.sentry.SentryAppStartProfilingOptions; import io.sentry.SentryExecutorService; import io.sentry.SentryLevel; @@ -25,7 +25,6 @@ import io.sentry.android.core.internal.util.FirstDrawDoneListener; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.ActivityLifecycleCallbacksAdapter; -import io.sentry.android.core.performance.ActivityLifecycleTimeSpan; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; import java.io.BufferedReader; @@ -34,7 +33,6 @@ import java.io.FileNotFoundException; import java.io.InputStreamReader; import java.io.Reader; -import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -206,101 +204,23 @@ private void onAppLaunched( activityCallback = new ActivityLifecycleCallbacksAdapter() { - final WeakHashMap activityLifecycleMap = - new WeakHashMap<>(); - - @Override - public void onActivityPreCreated( - @NonNull Activity activity, @Nullable Bundle savedInstanceState) { - final long now = SystemClock.uptimeMillis(); - if (appStartMetrics.getAppStartTimeSpan().hasStopped()) { - return; - } - - final ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan(); - timeSpan.getOnCreate().setStartedAt(now); - activityLifecycleMap.put(activity, timeSpan); - } - - @Override - public void onActivityCreated( - @NonNull Activity activity, @Nullable Bundle savedInstanceState) { - if (appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.UNKNOWN) { - appStartMetrics.setAppStartType( - savedInstanceState == null - ? AppStartMetrics.AppStartType.COLD - : AppStartMetrics.AppStartType.WARM); - } - } - - @Override - public void onActivityPostCreated( - @NonNull Activity activity, @Nullable Bundle savedInstanceState) { - if (appStartMetrics.getAppStartTimeSpan().hasStopped()) { - 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(@NonNull Activity activity) { - final long now = SystemClock.uptimeMillis(); - if (appStartMetrics.getAppStartTimeSpan().hasStopped()) { - return; - } - final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity); - if (timeSpan != null) { - timeSpan.getOnStart().setStartedAt(now); - } - } - @Override public void onActivityStarted(@NonNull Activity activity) { if (firstDrawDone.get()) { return; } - FirstDrawDoneListener.registerForNextDraw( - activity, - () -> { - if (firstDrawDone.compareAndSet(false, true)) { - onAppStartDone(); - } - }, - // as the SDK isn't initialized yet, we don't have access to SentryOptions - new BuildInfoProvider(NoOpLogger.getInstance())); - } - - @Override - public void onActivityPostStarted(@NonNull Activity activity) { - final @Nullable ActivityLifecycleTimeSpan timeSpan = - activityLifecycleMap.remove(activity); - if (appStartMetrics.getAppStartTimeSpan().hasStopped()) { - return; - } - if (timeSpan != null) { - timeSpan.getOnStart().stop(); - timeSpan.getOnStart().setDescription(activity.getClass().getName() + ".onStart"); - - appStartMetrics.addActivityLifecycleTimeSpans(timeSpan); + if (activity.getWindow() != null) { + FirstDrawDoneListener.registerForNextDraw( + activity, () -> onAppStartDone(), buildInfoProvider); + } else { + new Handler(Looper.getMainLooper()).post(() -> onAppStartDone()); } } - - @Override - public void onActivityDestroyed(@NonNull Activity activity) { - // safety net for activities which were created but never stopped - activityLifecycleMap.remove(activity); - } }; app.registerActivityLifecycleCallbacks(activityCallback); } - @TestOnly synchronized void onAppStartDone() { final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); appStartMetrics.getSdkInitTimeSpan().stop(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 461ee5eed65..231f62ad62c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -57,6 +57,7 @@ public enum AppStartType { private @Nullable SentryDate onCreateTime = null; private boolean appLaunchTooLong = false; private boolean isCallbackRegistered = false; + private boolean shouldSendStartMeasurements = true; public static @NotNull AppStartMetrics getInstance() { @@ -126,21 +127,39 @@ public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) { * @return A sorted list of all onCreate calls */ public @NotNull List getContentProviderOnCreateTimeSpans() { - final List measurements = new ArrayList<>(contentProviderOnCreates.values()); - Collections.sort(measurements); - return measurements; + final List spans = new ArrayList<>(contentProviderOnCreates.values()); + Collections.sort(spans); + return spans; } public @NotNull List getActivityLifecycleTimeSpans() { - final List measurements = new ArrayList<>(activityLifecycles); - Collections.sort(measurements); - return measurements; + final List spans = new ArrayList<>(activityLifecycles); + Collections.sort(spans); + return spans; } public void addActivityLifecycleTimeSpans(final @NotNull ActivityLifecycleTimeSpan timeSpan) { activityLifecycles.add(timeSpan); } + public void onAppStartSpansSent() { + shouldSendStartMeasurements = false; + contentProviderOnCreates.clear(); + activityLifecycles.clear(); + } + + public boolean shouldSendStartMeasurements() { + return shouldSendStartMeasurements; + } + + public void restartAppStart(final long timestampMs) { + shouldSendStartMeasurements = true; + appStartSpan.reset(); + appStartSpan.start(); + appStartSpan.setStartUnixTimeMs(timestampMs); + CLASS_LOADED_UPTIME_MS = appStartSpan.getStartUptimeMs(); + } + public long getClassLoadedUptimeMs() { return CLASS_LOADED_UPTIME_MS; } @@ -188,6 +207,7 @@ public void clear() { appLaunchedInForeground = false; onCreateTime = null; isCallbackRegistered = false; + shouldSendStartMeasurements = true; } public @Nullable ITransactionProfiler getAppStartProfiler() { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java index dac78920f83..7c912b72dc5 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java @@ -8,7 +8,6 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; /** * A measurement for time critical components on a macro (ms) level. Based on {@link @@ -148,9 +147,12 @@ public long getDurationMs() { } } - @TestOnly public void setStartUnixTimeMs(long startUnixTimeMs) { this.startUnixTimeMs = startUnixTimeMs; + + final long shiftMs = System.currentTimeMillis() - startUnixTimeMs; + this.startUptimeMs = SystemClock.uptimeMillis() - shiftMs; + startSystemNanos = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(shiftMs); } public @Nullable String getDescription() { From 5a8a74516ab116faa9f51cabcb3387034b7b2d74 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 29 Nov 2024 11:56:51 +0100 Subject: [PATCH 02/15] ActivityLifecycleIntegration now saves SystemClock.uptimeMillis to set app start timestamp reverted TimeSpan.setStartUnixTimeMs to @TestOnly method --- .../api/sentry-android-core.api | 7 +++++++ .../core/ActivityLifecycleIntegration.java | 21 +++++++++---------- .../core/performance/AppStartMetrics.java | 4 ++-- .../android/core/performance/TimeSpan.java | 6 ++---- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index f525e056f6c..a5c07bb83ce 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -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 @@ -445,16 +449,19 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun isAppLaunchedInForeground ()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 { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 809161a946e..212a1f8a91a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -10,7 +10,6 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; -import io.sentry.DateUtils; import io.sentry.FullyDisplayedReporter; import io.sentry.IHub; import io.sentry.IScope; @@ -81,6 +80,7 @@ public final class ActivityLifecycleIntegration private final @NotNull WeakHashMap activityLifecycleMap = new WeakHashMap<>(); private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0); + private long lastPausedUptimeMillis = 0; private @Nullable Future ttfdAutoCloseFuture = null; // WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the @@ -384,11 +384,10 @@ public void onActivityPreCreated( hub != null ? hub.getOptions().getDateProvider().now() : AndroidDateUtils.getCurrentSentryDateTime(); + lastPausedUptimeMillis = SystemClock.uptimeMillis(); final @NotNull ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan(); - timeSpan - .getOnCreate() - .setStartUnixTimeMs((long) DateUtils.nanosToMillis(lastPausedTime.nanoTimestamp())); + timeSpan.getOnCreate().setStartedAt(lastPausedUptimeMillis); activityLifecycleMap.put(activity, timeSpan); } @@ -501,11 +500,11 @@ public void onActivityPrePaused(@NotNull Activity activity) { // 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(); - } + lastPausedTime = + hub != null + ? hub.getOptions().getDateProvider().now() + : AndroidDateUtils.getCurrentSentryDateTime(); + lastPausedUptimeMillis = SystemClock.uptimeMillis(); } @Override @@ -564,6 +563,7 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) { private void clear() { firstActivityCreated = false; lastPausedTime = new SentryNanotimeDate(new Date(0), 0); + lastPausedUptimeMillis = 0; activityLifecycleMap.clear(); } @@ -711,8 +711,7 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { TimeSpan appStartSpan = AppStartMetrics.getInstance().getAppStartTimeSpan(); // If the app start span already started and stopped, it means we are in a warm start if (appStartSpan.hasStarted() && appStartSpan.hasStopped()) { - AppStartMetrics.getInstance() - .restartAppStart((long) DateUtils.nanosToMillis(lastPausedTime.nanoTimestamp())); + AppStartMetrics.getInstance().restartAppStart(lastPausedUptimeMillis); AppStartMetrics.getInstance().setAppStartType(AppStartMetrics.AppStartType.WARM); } else { AppStartMetrics.getInstance() diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 231f62ad62c..a5b9a23fbe6 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -152,11 +152,11 @@ public boolean shouldSendStartMeasurements() { return shouldSendStartMeasurements; } - public void restartAppStart(final long timestampMs) { + public void restartAppStart(final long uptimeMillis) { shouldSendStartMeasurements = true; appStartSpan.reset(); appStartSpan.start(); - appStartSpan.setStartUnixTimeMs(timestampMs); + appStartSpan.setStartedAt(uptimeMillis); CLASS_LOADED_UPTIME_MS = appStartSpan.getStartUptimeMs(); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java index 7c912b72dc5..dac78920f83 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; /** * A measurement for time critical components on a macro (ms) level. Based on {@link @@ -147,12 +148,9 @@ public long getDurationMs() { } } + @TestOnly public void setStartUnixTimeMs(long startUnixTimeMs) { this.startUnixTimeMs = startUnixTimeMs; - - final long shiftMs = System.currentTimeMillis() - startUnixTimeMs; - this.startUptimeMs = SystemClock.uptimeMillis() - shiftMs; - startSystemNanos = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(shiftMs); } public @Nullable String getDescription() { From bb0b52afa0de13a46b24868d7627306a09460a4f Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 29 Nov 2024 16:05:18 +0100 Subject: [PATCH 03/15] ActivityLifecycleIntegration now sets start type to warm even when cold start was invalid (app was started in background, like via BroadcastReceiver) --- .../api/sentry-android-core.api | 1 + .../core/ActivityLifecycleIntegration.java | 10 ++++++--- .../core/performance/AppStartMetrics.java | 22 ++++++++++--------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index a5c07bb83ce..b05ed1bb085 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -448,6 +448,7 @@ 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 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 212a1f8a91a..7e2a4ca90f8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -708,9 +708,13 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) { ? AppStartMetrics.AppStartType.COLD : AppStartMetrics.AppStartType.WARM); } else { - TimeSpan appStartSpan = AppStartMetrics.getInstance().getAppStartTimeSpan(); - // If the app start span already started and stopped, it means we are in a warm start - if (appStartSpan.hasStarted() && appStartSpan.hasStopped()) { + 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 { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index a5b9a23fbe6..2e249d6dccf 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -116,6 +116,10 @@ public boolean isAppLaunchedInForeground() { return appLaunchedInForeground; } + public boolean isColdStartValid() { + return appLaunchedInForeground && !appLaunchTooLong; + } + @VisibleForTesting public void setAppLaunchedInForeground(final boolean appLaunchedInForeground) { this.appLaunchedInForeground = appLaunchedInForeground; @@ -154,6 +158,8 @@ public boolean shouldSendStartMeasurements() { public void restartAppStart(final long uptimeMillis) { shouldSendStartMeasurements = true; + appLaunchTooLong = false; + appLaunchedInForeground = true; appStartSpan.reset(); appStartSpan.start(); appStartSpan.setStartedAt(uptimeMillis); @@ -170,24 +176,20 @@ public long getClassLoadedUptimeMs() { */ public @NotNull TimeSpan getAppStartTimeSpanWithFallback( final @NotNull SentryAndroidOptions options) { + // If the app launch took too long or it was launched in the background we return an empty span + if (!isColdStartValid()) { + return new TimeSpan(); + } if (options.isEnablePerformanceV2()) { // Only started when sdk version is >= N final @NotNull TimeSpan appStartSpan = getAppStartTimeSpan(); if (appStartSpan.hasStarted()) { - return validateAppStartSpan(appStartSpan); + return appStartSpan; } } // fallback: use sdk init time span, as it will always have a start time set - return validateAppStartSpan(getSdkInitTimeSpan()); - } - - private @NotNull TimeSpan validateAppStartSpan(final @NotNull TimeSpan appStartSpan) { - // If the app launch took too long or it was launched in the background we return an empty span - if (appLaunchTooLong || !appLaunchedInForeground) { - return new TimeSpan(); - } - return appStartSpan; + return getSdkInitTimeSpan(); } @TestOnly From 8acddb644c503330ed0198bacc6caf40c201cdbf Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 2 Dec 2024 16:34:55 +0100 Subject: [PATCH 04/15] ActivityLifecycleIntegration now restart app start in AppStartMetrics in perfv1, too updated tests --- .../core/ActivityLifecycleIntegration.java | 42 +- .../core/SentryPerformanceProvider.java | 6 - .../core/ActivityLifecycleIntegrationTest.kt | 377 +++++++++++------- .../PerformanceAndroidEventProcessorTest.kt | 59 ++- .../core/SentryPerformanceProviderTest.kt | 44 -- .../core/performance/AppStartMetricsTest.kt | 46 +++ 6 files changed, 348 insertions(+), 226 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 7e2a4ca90f8..2e1972abc0a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -671,6 +671,16 @@ WeakHashMap getActivitiesWithOngoingTransactions() { return activitiesWithOngoingTransactions; } + @TestOnly + @NotNull WeakHashMap getActivityLifecycleMap() { + return activityLifecycleMap; + } + + @TestOnly + void setFirstActivityCreated(boolean firstActivityCreated) { + this.firstActivityCreated = firstActivityCreated; + } + @TestOnly @NotNull ActivityFramesTracker getActivityFramesTracker() { @@ -697,33 +707,21 @@ WeakHashMap getTtfdSpanMap() { private void setColdStart(final @Nullable Bundle savedInstanceState) { 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 ? AppStartMetrics.AppStartType.COLD : AppStartMetrics.AppStartType.WARM); - } else { - 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 - ? AppStartMetrics.AppStartType.COLD - : AppStartMetrics.AppStartType.WARM); - } } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index c5778a31135..0aa946c2553 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -232,10 +232,4 @@ synchronized void onAppStartDone() { } } } - - @TestOnly - @Nullable - Application.ActivityLifecycleCallbacks getActivityCallback() { - return activityCallback; - } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index addeb948194..56e8d88551e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -338,15 +338,27 @@ class ActivityLifecycleIntegrationTest { sut.onActivityPostResumed(activity) verify(fixture.hub, never()).captureTransaction( - check { - assertEquals(SpanStatus.OK, it.status) - }, + any(), anyOrNull(), anyOrNull(), anyOrNull() ) } + @Test + fun `When tracing auto finish is disabled, do not finish transaction`() { + val sut = fixture.getSut(initializer = { + it.tracesSampleRate = 1.0 + it.isEnableActivityLifecycleTracingAutoFinish = false + }) + sut.register(fixture.hub, fixture.options) + val activity = mock() + sut.onActivityCreated(activity, fixture.bundle) + // We don't schedule the transaction to finish + assertFalse(fixture.transaction.isFinishing()) + assertFalse(fixture.transaction.isFinished) + } + @Test fun `When tracing has status, do not overwrite it`() { val sut = fixture.getSut() @@ -371,20 +383,6 @@ class ActivityLifecycleIntegrationTest { ) } - @Test - fun `When tracing auto finish is disabled, do not finish transaction`() { - val sut = fixture.getSut(initializer = { - it.tracesSampleRate = 1.0 - it.isEnableActivityLifecycleTracingAutoFinish = false - }) - sut.register(fixture.hub, fixture.options) - val activity = mock() - sut.onActivityCreated(activity, fixture.bundle) - sut.onActivityPostResumed(activity) - - verify(fixture.hub, never()).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) - } - @Test fun `When tracing is disabled, do not finish transaction`() { val sut = fixture.getSut() @@ -467,7 +465,7 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When Activity is destroyed, sets ttidSpan status to deadline_exceeded and finish it`() { + fun `When Activity is destroyed, finish ttidSpan with deadline_exceeded and remove from map`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -476,31 +474,17 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) + assertNotNull(sut.ttidSpanMap[activity]) sut.onActivityDestroyed(activity) val span = fixture.transaction.children.first { it.operation == ActivityLifecycleIntegration.TTID_OP } assertEquals(SpanStatus.DEADLINE_EXCEEDED, span.status) assertTrue(span.isFinished) - } - - @Test - fun `When Activity is destroyed, sets ttidSpan to null`() { - val sut = fixture.getSut() - fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) - - setAppStartTime() - - val activity = mock() - sut.onActivityCreated(activity, fixture.bundle) - assertNotNull(sut.ttidSpanMap[activity]) - - sut.onActivityDestroyed(activity) assertNull(sut.ttidSpanMap[activity]) } @Test - fun `When Activity is destroyed, sets ttfdSpan status to deadline_exceeded and finish it`() { + fun `When Activity is destroyed, finish ttfdSpan with deadline_exceeded and remove from map`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true @@ -510,27 +494,12 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) + assertNotNull(sut.ttfdSpanMap[activity]) sut.onActivityDestroyed(activity) val span = fixture.transaction.children.first { it.operation == ActivityLifecycleIntegration.TTFD_OP } assertEquals(SpanStatus.DEADLINE_EXCEEDED, span.status) assertTrue(span.isFinished) - } - - @Test - fun `When Activity is destroyed, sets ttfdSpan to null`() { - val sut = fixture.getSut() - fixture.options.tracesSampleRate = 1.0 - fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) - - setAppStartTime() - - val activity = mock() - sut.onActivityCreated(activity, fixture.bundle) - assertNotNull(sut.ttfdSpanMap[activity]) - - sut.onActivityDestroyed(activity) assertNull(sut.ttfdSpanMap[activity]) } @@ -547,7 +516,7 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `do not stop transaction on resumed if API 29`() { + fun `do not stop transaction on resumed`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -560,31 +529,11 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `do not stop transaction on resumed if API less than 29 and ttid and ttfd are finished`() { - val sut = fixture.getSut(Build.VERSION_CODES.P) - fixture.options.tracesSampleRate = 1.0 - fixture.options.isEnableTimeToFullDisplayTracing = true - sut.register(fixture.hub, fixture.options) - - val activity = mock() - sut.onActivityCreated(activity, mock()) - sut.ttidSpanMap.values.first().finish() - sut.ttfdSpanMap.values.first().finish() - sut.onActivityResumed(activity) - - verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) - } - - @Test - fun `start transaction on created if API less than 29`() { - val sut = fixture.getSut(Build.VERSION_CODES.P) + fun `start transaction on created`() { + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) - - setAppStartTime() - - val activity = mock() - sut.onActivityCreated(activity, mock()) + sut.onActivityCreated(mock(), mock()) verify(fixture.hub).startTransaction(any(), any()) } @@ -594,6 +543,7 @@ class ActivityLifecycleIntegrationTest { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 fixture.options.isEnableTimeToFullDisplayTracing = true + fixture.options.idleTimeout = 0 sut.register(fixture.hub, fixture.options) val activity = mock() @@ -602,6 +552,7 @@ class ActivityLifecycleIntegrationTest { sut.ttidSpanMap.values.first().finish() sut.onActivityResumed(activity) sut.onActivityPostResumed(activity) + runFirstDraw(fixture.createView()) assertNotNull(ttfd) assertFalse(ttfd.isFinished) @@ -682,15 +633,18 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When firstActivityCreated is true, start transaction with given appStartTime`() { + fun `When firstActivityCreated is false, start transaction with given appStartTime`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) + fixture.options.dateProvider = SentryDateProvider { date } val activity = mock() + sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) // call only once @@ -703,15 +657,17 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When firstActivityCreated is true and app start sampling decision is set, start transaction with isAppStart true`() { + fun `When firstActivityCreated is false and app start sampling decision is set, start transaction with isAppStart true`() { AppStartMetrics.getInstance().appStartSamplingDecision = mock() val sut = fixture.getSut { it.tracesSampleRate = 1.0 } sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) val activity = mock() + sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) verify(fixture.hub).startTransaction( @@ -724,9 +680,10 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When firstActivityCreated is true and app start sampling decision is not set, start transaction with isAppStart false`() { + fun `When firstActivityCreated is false and app start sampling decision is not set, start transaction with isAppStart false`() { val sut = fixture.getSut { it.tracesSampleRate = 1.0 } sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) val date2 = SentryNanotimeDate(Date(2), 2) @@ -735,6 +692,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() // The activity onCreate date will be ignored fixture.options.dateProvider = SentryDateProvider { date2 } + sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) verify(fixture.hub).startTransaction( @@ -748,65 +706,71 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When firstActivityCreated is false and app start sampling decision is set, start transaction with isAppStart false`() { + fun `When firstActivityCreated is true and app start sampling decision is set, start transaction with isAppStart false`() { AppStartMetrics.getInstance().appStartSamplingDecision = mock() val sut = fixture.getSut { it.tracesSampleRate = 1.0 } sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(true) + val date = SentryNanotimeDate(Date(1), 0) + setAppStartTime(date) val activity = mock() + sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) verify(fixture.hub).startTransaction(any(), check { assertFalse(it.isAppStartTransaction) }) } @Test - fun `When firstActivityCreated is true, do not create app start span if not foregroundImportance`() { - val sut = fixture.getSut(importance = RunningAppProcessInfo.IMPORTANCE_BACKGROUND) + fun `When firstActivityCreated is false and no app start time is set, default to onActivityPreCreated time`() { + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(false) // usually set by SentryPerformanceProvider val date = SentryNanotimeDate(Date(1), 0) - setAppStartTime(date) - AppStartMetrics.getInstance().sdkInitTimeSpan.setStoppedAt(2) + val date2 = SentryNanotimeDate(Date(2), 2) val activity = mock() + // Activity onCreate date will be used + fixture.options.dateProvider = SentryDateProvider { date2 } + sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) - // call only once verify(fixture.hub).startTransaction( any(), - check { assertNotEquals(date, it.startTimestamp) } + check { + assertEquals(date2.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) + assertNotEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) + } ) } @Test - fun `When firstActivityCreated is true and no app start time is set, default to onActivityCreated time`() { - val sut = fixture.getSut() + fun `When not foregroundImportance, do not create app start span`() { + val sut = fixture.getSut(importance = RunningAppProcessInfo.IMPORTANCE_BACKGROUND) fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) // usually set by SentryPerformanceProvider val date = SentryNanotimeDate(Date(1), 0) - val date2 = SentryNanotimeDate(Date(2), 2) + setAppStartTime(date) val activity = mock() - // Activity onCreate date will be used - fixture.options.dateProvider = SentryDateProvider { date2 } + sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) + // call only once verify(fixture.hub).startTransaction( any(), - check { - assertEquals(date2.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) - assertNotEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) - } + check { assertNotEquals(date.nanoTimestamp(), it.startTimestamp!!.nanoTimestamp()) } ) } @Test fun `Create and finish app start span immediately in case SDK init is deferred`() { - val sut = fixture.getSut(importance = RunningAppProcessInfo.IMPORTANCE_FOREGROUND) + val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -822,12 +786,10 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, fixture.bundle) - val appStartSpanCount = fixture.transaction.children.count { - it.spanContext.operation.startsWith("app.start.warm") && - it.startDate.nanoTimestamp() == startDate.nanoTimestamp() && - it.finishDate!!.nanoTimestamp() == endDate!!.nanoTimestamp() - } - assertEquals(1, appStartSpanCount) + val appStartSpan = fixture.transaction.children.first { it.operation.startsWith("app.start.warm") } + assertEquals(startDate.nanoTimestamp(), appStartSpan.startDate.nanoTimestamp()) + assertEquals(endDate!!.nanoTimestamp(), appStartSpan.finishDate!!.nanoTimestamp()) + assertTrue(appStartSpan.isFinished) } @Test @@ -921,10 +883,11 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `When firstActivityCreated is true, start app start warm span with given appStartTime`() { + fun `When firstActivityCreated is false and bundle is not null, start app start warm span with given appStartTime`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) @@ -934,48 +897,16 @@ class ActivityLifecycleIntegrationTest { val span = fixture.transaction.children.first() assertEquals(span.operation, "app.start.warm") - assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) - } - - @Test - fun `When firstActivityCreated is true, start app start cold span with given appStartTime`() { - val sut = fixture.getSut() - fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) - - val date = SentryNanotimeDate(Date(1), 0) - setAppStartTime(date) - - val activity = mock() - sut.onActivityCreated(activity, null) - - val span = fixture.transaction.children.first() - assertEquals(span.operation, "app.start.cold") - assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) - } - - @Test - fun `When firstActivityCreated is true, start app start span with Warm description`() { - val sut = fixture.getSut() - fixture.options.tracesSampleRate = 1.0 - sut.register(fixture.hub, fixture.options) - - val date = SentryNanotimeDate(Date(1), 0) - setAppStartTime(date) - - val activity = mock() - sut.onActivityCreated(activity, fixture.bundle) - - val span = fixture.transaction.children.first() assertEquals(span.description, "Warm Start") assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test - fun `When firstActivityCreated is true, start app start span with Cold description`() { + fun `When firstActivityCreated is false and bundle is not null, start app start cold span with given appStartTime`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) @@ -984,15 +915,17 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, null) val span = fixture.transaction.children.first() + assertEquals(span.operation, "app.start.cold") assertEquals(span.description, "Cold Start") assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test - fun `When firstActivityCreated is true and app started more than 1 minute ago, app start spans are dropped`() { + fun `When firstActivityCreated is false and app started more than 1 minute ago, start app with Warm start`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) val duration = TimeUnit.MINUTES.toMillis(1) + 2 @@ -1003,18 +936,19 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, null) - val appStartSpan = fixture.transaction.children.firstOrNull { - it.description == "Cold Start" - } - assertNull(appStartSpan) + val span = fixture.transaction.children.first() + assertEquals(span.operation, "app.start.warm") + assertEquals(span.description, "Warm Start") + assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test - fun `When firstActivityCreated is true and app started in background, app start spans are dropped`() { + fun `When firstActivityCreated is false and app started in background, start app with Warm start`() { val sut = fixture.getSut() AppStartMetrics.getInstance().isAppLaunchedInForeground = false fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(false) val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) @@ -1022,17 +956,18 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityCreated(activity, null) - val appStartSpan = fixture.transaction.children.firstOrNull { - it.description == "Cold Start" - } - assertNull(appStartSpan) + val span = fixture.transaction.children.first() + assertEquals(span.operation, "app.start.warm") + assertEquals(span.description, "Warm Start") + assertEquals(span.startDate.nanoTimestamp(), date.nanoTimestamp()) } @Test - fun `When firstActivityCreated is false, start transaction but not with given appStartTime`() { + fun `When firstActivityCreated is true, start transaction but not with given appStartTime`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(true) val date = SentryNanotimeDate(Date(1), 0) setAppStartTime() @@ -1041,11 +976,6 @@ class ActivityLifecycleIntegrationTest { // First invocation: we expect to start a transaction with the appStartTime sut.onActivityCreated(activity, fixture.bundle) sut.onActivityPostResumed(activity) - assertEquals(date.nanoTimestamp(), fixture.transaction.startDate.nanoTimestamp()) - - val newActivity = mock() - // Second invocation: we expect to start a transaction with a different start timestamp - sut.onActivityCreated(newActivity, fixture.bundle) assertNotEquals(date.nanoTimestamp(), fixture.transaction.startDate.nanoTimestamp()) } @@ -1493,6 +1423,151 @@ class ActivityLifecycleIntegrationTest { assertEquals(now.nanoTimestamp(), fixture.transaction.startDate.nanoTimestamp()) } + @Test + fun `On activity preCreated onCreate span is created`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + sut.register(fixture.hub, fixture.options) + + val date = SentryNanotimeDate(Date(1), 0) + setAppStartTime(date) + + assertTrue(sut.activityLifecycleMap.isEmpty()) + + val activity = mock() + // Activity onCreate date will be used + sut.onActivityPreCreated(activity, fixture.bundle) + //sut.onActivityCreated(activity, fixture.bundle) + + assertFalse(sut.activityLifecycleMap.isEmpty()) + assertTrue(sut.activityLifecycleMap.values.first().onCreate.hasStarted()) + assertFalse(sut.activityLifecycleMap.values.first().onCreate.hasStopped()) + } + + @Test + fun `Creates activity lifecycle spans`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + val appStartDate = SentryNanotimeDate(Date(1), 0) + val startDate = SentryNanotimeDate(Date(2), 0) + val appStartMetrics = AppStartMetrics.getInstance() + val activity = mock() + fixture.options.dateProvider = SentryDateProvider { startDate } + setAppStartTime(appStartDate) + + sut.register(fixture.hub, fixture.options) + assertTrue(sut.activityLifecycleMap.isEmpty()) + + sut.onActivityPreCreated(activity, null) + + assertFalse(sut.activityLifecycleMap.isEmpty()) + val activityLifecycleSpan = sut.activityLifecycleMap.values.first() + assertTrue(activityLifecycleSpan.onCreate.hasStarted()) + assertEquals(startDate.nanoTimestamp(), sut.getProperty("lastPausedTime").nanoTimestamp()) + + sut.onActivityCreated(activity, null) + assertNotNull(sut.appStartSpan) + + sut.onActivityPostCreated(activity, null) + assertTrue(activityLifecycleSpan.onCreate.hasStopped()) + + sut.onActivityPreStarted(activity) + assertTrue(activityLifecycleSpan.onStart.hasStarted()) + + sut.onActivityStarted(activity) + assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + + sut.onActivityPostStarted(activity) + assertTrue(activityLifecycleSpan.onStart.hasStopped()) + assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + } + + @Test + fun `Creates activity lifecycle spans on API lower than 29`() { + val sut = fixture.getSut(apiVersion = Build.VERSION_CODES.P) + fixture.options.tracesSampleRate = 1.0 + val appStartDate = SentryNanotimeDate(Date(1), 0) + val startDate = SentryNanotimeDate(Date(2), 0) + val appStartMetrics = AppStartMetrics.getInstance() + val activity = mock() + fixture.options.dateProvider = SentryDateProvider { startDate } + setAppStartTime(appStartDate) + + sut.register(fixture.hub, fixture.options) + assertTrue(sut.activityLifecycleMap.isEmpty()) + + sut.onActivityCreated(activity, null) + + assertFalse(sut.activityLifecycleMap.isEmpty()) + val activityLifecycleSpan = sut.activityLifecycleMap.values.first() + assertTrue(activityLifecycleSpan.onCreate.hasStarted()) + assertEquals(startDate.nanoTimestamp(), sut.getProperty("lastPausedTime").nanoTimestamp()) + assertNotNull(sut.appStartSpan) + + sut.onActivityStarted(activity) + assertTrue(activityLifecycleSpan.onCreate.hasStopped()) + assertTrue(activityLifecycleSpan.onStart.hasStarted()) + assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + + sut.onActivityResumed(activity) + assertTrue(activityLifecycleSpan.onStart.hasStopped()) + assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + } + + @Test + fun `Does not add activity lifecycle spans when firstActivityCreated is true`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + val appStartDate = SentryNanotimeDate(Date(1), 0) + val startDate = SentryNanotimeDate(Date(2), 0) + val appStartMetrics = AppStartMetrics.getInstance() + val activity = mock() + fixture.options.dateProvider = SentryDateProvider { startDate } + setAppStartTime(appStartDate) + sut.register(fixture.hub, fixture.options) + sut.setFirstActivityCreated(true) + + sut.onActivityPreCreated(activity, null) + sut.onActivityCreated(activity, null) + sut.onActivityPostCreated(activity, null) + sut.onActivityPreStarted(activity) + sut.onActivityStarted(activity) + sut.onActivityPostStarted(activity) + assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + } + + @Test + fun `When firstActivityCreated is false and app start span has stopped, restart app start to current date`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + val appStartDate = SentryNanotimeDate(Date(1), 0) + val appStartMetrics = AppStartMetrics.getInstance() + val activity = mock() + setAppStartTime(appStartDate) + // Let's pretend app start started and finished + appStartMetrics.appStartTimeSpan.stop() + sut.register(fixture.hub, fixture.options) + + assertEquals(0, sut.getProperty("lastPausedUptimeMillis")) + + // An Activity (the first) is created after app start has finished + sut.onActivityPreCreated(activity, null) + // lastPausedUptimeMillis is set to current SystemClock.uptimeMillis() + val lastUptimeMillis = sut.getProperty("lastPausedUptimeMillis") + assertNotEquals(0, lastUptimeMillis) + + sut.onActivityCreated(activity, null) + // AppStartMetrics app start time is set to Activity preCreated timestamp + assertEquals(lastUptimeMillis, appStartMetrics.appStartTimeSpan.startUptimeMs) + // AppStart type is considered warm + assertEquals(AppStartType.WARM, appStartMetrics.appStartType) + + // Activity appStart span timestamp is the same of AppStartMetrics.appStart timestamp + assertEquals(sut.appStartSpan!!.startDate.nanoTimestamp(), appStartMetrics.getAppStartTimeSpanWithFallback(fixture.options).startTimestamp!!.nanoTimestamp()) + } + + private fun SentryTracer.isFinishing() = getProperty("finishStatus").getProperty("isFinishing") + private fun runFirstDraw(view: View) { // Removes OnDrawListener in the next OnGlobalLayout after onDraw view.viewTreeObserver.dispatchOnDraw() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 23ab5a3bc83..35e0f5257bf 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -13,6 +13,7 @@ import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision import io.sentry.TransactionContext import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD +import io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM import io.sentry.android.core.ActivityLifecycleIntegration.UI_LOAD_OP import io.sentry.android.core.performance.ActivityLifecycleTimeSpan import io.sentry.android.core.performance.AppStartMetrics @@ -59,13 +60,13 @@ class PerformanceAndroidEventProcessorTest { private val fixture = Fixture() - private fun createAppStartSpan(traceId: SentryId) = SentrySpan( + private fun createAppStartSpan(traceId: SentryId, coldStart: Boolean = true) = SentrySpan( 0.0, 1.0, traceId, SpanId(), null, - APP_START_COLD, + if (coldStart) APP_START_COLD else APP_START_WARM, "App Start", SpanStatus.OK, null, @@ -292,6 +293,56 @@ class PerformanceAndroidEventProcessorTest { ) } + @Test + fun `adds app start metrics to app warm start txn`() { + // given some app start metrics + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.appStartType = AppStartType.WARM + appStartMetrics.appStartTimeSpan.setStartedAt(123) + appStartMetrics.appStartTimeSpan.setStoppedAt(456) + + val contentProvider = mock() + AppStartMetrics.onContentProviderCreate(contentProvider) + AppStartMetrics.onContentProviderPostCreate(contentProvider) + + appStartMetrics.applicationOnCreateTimeSpan.apply { + setStartedAt(10) + setStoppedAt(42) + } + + val activityTimeSpan = ActivityLifecycleTimeSpan() + activityTimeSpan.onCreate.description = "MainActivity.onCreate" + activityTimeSpan.onStart.description = "MainActivity.onStart" + + activityTimeSpan.onCreate.setStartedAt(200) + activityTimeSpan.onStart.setStartedAt(220) + activityTimeSpan.onStart.setStoppedAt(240) + activityTimeSpan.onCreate.setStoppedAt(260) + appStartMetrics.addActivityLifecycleTimeSpans(activityTimeSpan) + + // when an activity transaction is created + val sut = fixture.getSut(enablePerformanceV2 = true) + val context = TransactionContext("Activity", UI_LOAD_OP) + val tracer = SentryTracer(context, fixture.hub) + var tr = SentryTransaction(tracer) + + // and it contains an app.start.warm span + val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId, false) + tr.spans.add(appStartSpan) + + // then the app start metrics should be attached + tr = sut.process(tr, Hint()) + + // process init, content provider and application span should not be attached + assertFalse(tr.spans.any { "process.load" == it.op }) + assertFalse(tr.spans.any { "contentprovider.load" == it.op }) + assertFalse(tr.spans.any { "application.load" == it.op }) + + // activity spans should be attached + assertTrue(tr.spans.any { "activity.load" == it.op && "MainActivity.onCreate" == it.description }) + assertTrue(tr.spans.any { "activity.load" == it.op && "MainActivity.onStart" == it.description }) + } + @Test fun `when app launched from background, app start spans are dropped`() { // given some app start metrics @@ -444,8 +495,10 @@ class PerformanceAndroidEventProcessorTest { val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId) tr.spans.add(appStartSpan) - // then the app start metrics should not be attached + assertTrue(appStartMetrics.shouldSendStartMeasurements()) + // then the app start metrics should be attached tr = sut.process(tr, Hint()) + assertFalse(appStartMetrics.shouldSendStartMeasurements()) assertTrue( tr.spans.any { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index ff6a299bed2..bd112acc31e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -104,50 +104,6 @@ class SentryPerformanceProviderTest { assertTrue(AppStartMetrics.getInstance().appStartTimeSpan.hasStarted()) } - @Test - fun `provider sets cold start based on first activity`() { - val provider = fixture.getSut() - - // up until this point app start is not known - assertEquals(AppStartType.UNKNOWN, AppStartMetrics.getInstance().appStartType) - - // when there's no saved state - provider.activityCallback!!.onActivityCreated(mock(), null) - // then app start should be cold - assertEquals(AppStartType.COLD, AppStartMetrics.getInstance().appStartType) - } - - @Test - fun `provider sets warm start based on first activity`() { - val provider = fixture.getSut() - - // up until this point app start is not known - assertEquals(AppStartType.UNKNOWN, AppStartMetrics.getInstance().appStartType) - - // when there's a saved state - provider.activityCallback!!.onActivityCreated(mock(), Bundle()) - - // then app start should be warm - assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) - } - - @Test - fun `provider keeps startup state even if multiple activities are launched`() { - val provider = fixture.getSut() - - // when there's a saved state - provider.activityCallback!!.onActivityCreated(mock(), Bundle()) - - // then app start should be warm - assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) - - // when another activity is launched cold - provider.activityCallback!!.onActivityCreated(mock(), null) - - // then app start should remain warm - assertEquals(AppStartType.WARM, AppStartMetrics.getInstance().appStartType) - } - @Test fun `provider sets both appstart and sdk init start + end times`() { val provider = fixture.getSut() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index eb0e85dc28e..d8b9e727e20 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -276,4 +276,50 @@ class AppStartMetricsTest { Shadows.shadowOf(Looper.getMainLooper()).idle() assertTrue(AppStartMetrics.getInstance().isAppLaunchedInForeground) } + + @Test + fun `isColdStartValid is false if app was launched in background`() { + AppStartMetrics.getInstance().isAppLaunchedInForeground = false + assertFalse(AppStartMetrics.getInstance().isColdStartValid) + } + + @Test + fun `isColdStartValid is false if app launched in more than 1 minute`() { + val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan + appStartTimeSpan.start() + appStartTimeSpan.stop() + appStartTimeSpan.setStartedAt(1) + appStartTimeSpan.setStoppedAt(TimeUnit.MINUTES.toMillis(1) + 2) + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) + assertFalse(AppStartMetrics.getInstance().isColdStartValid) + } + + @Test + fun `onAppStartSpansSent set measurement flag and clear internal lists`() { + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.addActivityLifecycleTimeSpans(mock()) + appStartMetrics.contentProviderOnCreateTimeSpans.add(mock()) + assertTrue(appStartMetrics.shouldSendStartMeasurements()) + appStartMetrics.onAppStartSpansSent() + assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + assertTrue(appStartMetrics.contentProviderOnCreateTimeSpans.isEmpty()) + assertFalse(appStartMetrics.shouldSendStartMeasurements()) + } + + @Test + fun `restartAppStart set measurement flag and clear internal lists`() { + val appStartMetrics = AppStartMetrics.getInstance() + appStartMetrics.onAppStartSpansSent() + appStartMetrics.isAppLaunchedInForeground = false + assertFalse(appStartMetrics.shouldSendStartMeasurements()) + assertFalse(appStartMetrics.isColdStartValid) + + appStartMetrics.restartAppStart(10) + + assertTrue(appStartMetrics.shouldSendStartMeasurements()) + assertTrue(appStartMetrics.isColdStartValid) + assertTrue(appStartMetrics.appStartTimeSpan.hasStarted()) + assertTrue(appStartMetrics.appStartTimeSpan.hasNotStopped()) + assertEquals(10, appStartMetrics.appStartTimeSpan.startUptimeMs) + } } From ed06f9f6c7a49e46890bb7bdcd0e2b46b38e0764 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 2 Dec 2024 16:38:52 +0100 Subject: [PATCH 05/15] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 837f4c220e4..3e3bb198146 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixes - Fix testTag not working for Jetpack Compose user interaction tracking ([#3878](https://github.com/getsentry/sentry-java/pull/3878)) +- Fix warm start detection ([#3937](https://github.com/getsentry/sentry-java/pull/3937)) ## 7.18.0 From d64c2437cc37ff3abaf98b0b1a453a002d3fa3b4 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 2 Dec 2024 15:42:40 +0000 Subject: [PATCH 06/15] Format code --- .../io/sentry/android/core/ActivityLifecycleIntegration.java | 3 ++- .../io/sentry/android/core/ActivityLifecycleIntegrationTest.kt | 2 +- .../io/sentry/android/core/SentryPerformanceProviderTest.kt | 3 --- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 2e1972abc0a..a8023ad291c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -672,7 +672,8 @@ WeakHashMap getActivitiesWithOngoingTransactions() { } @TestOnly - @NotNull WeakHashMap getActivityLifecycleMap() { + @NotNull + WeakHashMap getActivityLifecycleMap() { return activityLifecycleMap; } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 56e8d88551e..be000b7517c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -1437,7 +1437,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() // Activity onCreate date will be used sut.onActivityPreCreated(activity, fixture.bundle) - //sut.onActivityCreated(activity, fixture.bundle) + // sut.onActivityCreated(activity, fixture.bundle) assertFalse(sut.activityLifecycleMap.isEmpty()) assertTrue(sut.activityLifecycleMap.values.first().onCreate.hasStarted()) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index bd112acc31e..9f868d701b4 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -3,7 +3,6 @@ package io.sentry.android.core import android.app.Application import android.content.pm.ProviderInfo import android.os.Build -import android.os.Bundle import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.ILogger import io.sentry.JsonSerializer @@ -12,7 +11,6 @@ import io.sentry.SentryAppStartProfilingOptions import io.sentry.SentryLevel import io.sentry.SentryOptions import io.sentry.android.core.performance.AppStartMetrics -import io.sentry.android.core.performance.AppStartMetrics.AppStartType import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq @@ -28,7 +26,6 @@ import java.nio.file.Files import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNotNull From 0a51b769ff8c987b78de1bffca4730f0bb59bce1 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 2 Dec 2024 17:10:58 +0100 Subject: [PATCH 07/15] removed Activity callback from SentryPerformanceProvider --- .../core/ActivityLifecycleIntegration.java | 3 +- .../core/SentryPerformanceProvider.java | 33 ------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 2e1972abc0a..0629becbeea 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -609,8 +609,7 @@ private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable I final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan(); final @NotNull TimeSpan sdkInitTimeSpan = appStartMetrics.getSdkInitTimeSpan(); - // in case the SentryPerformanceProvider is disabled it does not set the app start end times, - // and we need to set the end time manually here + // and we need to set the end time of the app start here, after the first frame is drawn. if (appStartTimeSpan.hasStarted() && appStartTimeSpan.hasNotStopped()) { appStartTimeSpan.stop(); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 0aa946c2553..5bd93584293 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -47,7 +47,6 @@ public final class SentryPerformanceProvider extends EmptySecureContentProvider private static final long sdkInitMillis = SystemClock.uptimeMillis(); private @Nullable Application app; - private @Nullable Application.ActivityLifecycleCallbacks activityCallback; private final @NotNull ILogger logger; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -199,37 +198,5 @@ private void onAppLaunched( final @NotNull TimeSpan appStartTimespan = appStartMetrics.getAppStartTimeSpan(); appStartTimespan.setStartedAt(Process.getStartUptimeMillis()); appStartMetrics.registerApplicationForegroundCheck(app); - - final AtomicBoolean firstDrawDone = new AtomicBoolean(false); - - activityCallback = - new ActivityLifecycleCallbacksAdapter() { - @Override - public void onActivityStarted(@NonNull Activity activity) { - if (firstDrawDone.get()) { - return; - } - if (activity.getWindow() != null) { - FirstDrawDoneListener.registerForNextDraw( - activity, () -> onAppStartDone(), buildInfoProvider); - } else { - new Handler(Looper.getMainLooper()).post(() -> onAppStartDone()); - } - } - }; - - app.registerActivityLifecycleCallbacks(activityCallback); - } - - synchronized void onAppStartDone() { - final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); - appStartMetrics.getSdkInitTimeSpan().stop(); - appStartMetrics.getAppStartTimeSpan().stop(); - - if (app != null) { - if (activityCallback != null) { - app.unregisterActivityLifecycleCallbacks(activityCallback); - } - } } } From 616c9f43ee13ac82485315536b320dc801df0861 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 2 Dec 2024 17:13:07 +0100 Subject: [PATCH 08/15] updated changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9392138042b..6745e03b9d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,16 @@ # Changelog +## Unreleased + +### Fixes + +- Fix warm start detection ([#3937](https://github.com/getsentry/sentry-java/pull/3937)) + ## 7.18.1 ### Fixes - Fix testTag not working for Jetpack Compose user interaction tracking ([#3878](https://github.com/getsentry/sentry-java/pull/3878)) -- Fix warm start detection ([#3937](https://github.com/getsentry/sentry-java/pull/3937)) ## 7.18.0 From 140fa4f41d9d258e3923f58504fd2fbf52a6f4e6 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 3 Dec 2024 17:57:34 +0100 Subject: [PATCH 09/15] moved activity lifecycle spans logic into a separate class ActivityLifecycleSpanHelper moved processInitSpan creation to AppStartMetrics ActivityLifecycleIntegration now create regular spans, and adds TimeSpans to AppStartMetrics to handle hybrid SDKs PerformanceAndroidEventProcessor does not add activity lifecycle spans to the transaction, as they are added by ActivityLifecycleIntegration directly --- .../api/sentry-android-core.api | 16 ++ .../core/ActivityLifecycleIntegration.java | 61 ++++---- .../android/core/InternalSentrySdk.java | 9 +- .../PerformanceAndroidEventProcessor.java | 93 ++++-------- .../core/SentryPerformanceProvider.java | 7 - .../ActivityLifecycleSpanHelper.java | 138 ++++++++++++++++++ .../core/performance/AppStartMetrics.java | 14 ++ .../android/core/performance/TimeSpan.java | 17 ++- 8 files changed, 235 insertions(+), 120 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelper.java diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index b05ed1bb085..3df89647b10 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -424,6 +424,20 @@ public class io/sentry/android/core/performance/ActivityLifecycleCallbacksAdapte public fun onActivityStopped (Landroid/app/Activity;)V } +public class io/sentry/android/core/performance/ActivityLifecycleSpanHelper { + public fun (Ljava/lang/String;)V + public fun clear ()V + public fun createAndStopOnCreateSpan (Lio/sentry/ISpan;)V + public fun createAndStopOnStartSpan (Lio/sentry/ISpan;)V + public fun getOnCreateSpan ()Lio/sentry/ISpan; + public fun getOnCreateStartTimestamp ()Lio/sentry/SentryDate; + public fun getOnStartSpan ()Lio/sentry/ISpan; + public fun getOnStartStartTimestamp ()Lio/sentry/SentryDate; + public fun saveSpanToAppStartMetrics ()V + public fun setOnCreateStartTimestamp (Lio/sentry/SentryDate;)V + public fun setOnStartStartTimestamp (Lio/sentry/SentryDate;)V +} + public class io/sentry/android/core/performance/ActivityLifecycleTimeSpan : java/lang/Comparable { public fun ()V public fun compareTo (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)I @@ -436,6 +450,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public fun ()V public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V public fun clear ()V + public fun createProcessInitSpan ()Lio/sentry/android/core/performance/TimeSpan; public fun getActivityLifecycleTimeSpans ()Ljava/util/List; public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler; public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision; @@ -495,6 +510,7 @@ public class io/sentry/android/core/performance/TimeSpan : java/lang/Comparable public fun setStartUnixTimeMs (J)V public fun setStartedAt (J)V public fun setStoppedAt (J)V + public fun setup (Ljava/lang/String;JJJ)V public fun start ()V public fun stop ()V } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java index 0629becbeea..326a2999978 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java @@ -28,7 +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.ActivityLifecycleSpanHelper; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; import io.sentry.protocol.MeasurementValue; @@ -77,7 +77,7 @@ public final class ActivityLifecycleIntegration private @Nullable ISpan appStartSpan; private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>(); private final @NotNull WeakHashMap ttfdSpanMap = new WeakHashMap<>(); - private final @NotNull WeakHashMap activityLifecycleMap = + private final @NotNull WeakHashMap activitySpanHelpers = new WeakHashMap<>(); private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0); private long lastPausedUptimeMillis = 0; @@ -374,6 +374,9 @@ private void finishTransaction( @Override public void onActivityPreCreated( final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) { + final ActivityLifecycleSpanHelper helper = + new ActivityLifecycleSpanHelper(activity.getClass().getName()); + activitySpanHelpers.put(activity, helper); // 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. @@ -385,10 +388,7 @@ public void onActivityPreCreated( ? hub.getOptions().getDateProvider().now() : AndroidDateUtils.getCurrentSentryDateTime(); lastPausedUptimeMillis = SystemClock.uptimeMillis(); - - final @NotNull ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan(); - timeSpan.getOnCreate().setStartedAt(lastPausedUptimeMillis); - activityLifecycleMap.put(activity, timeSpan); + helper.setOnCreateStartTimestamp(lastPausedTime); } @Override @@ -415,27 +415,20 @@ 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"); + final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity); + if (helper != null) { + helper.createAndStopOnCreateSpan(appStartSpan); } } @Override public void onActivityPreStarted(final @NotNull Activity activity) { - final long now = SystemClock.uptimeMillis(); - if (appStartSpan == null) { - return; - } - final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity); - if (timeSpan != null) { - timeSpan.getOnStart().setStartedAt(now); + final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity); + if (helper != null) { + helper.setOnStartStartTimestamp( + options != null + ? options.getDateProvider().now() + : AndroidDateUtils.getCurrentSentryDateTime()); } } @@ -458,14 +451,10 @@ 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); + final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity); + if (helper != null) { + helper.createAndStopOnStartSpan(appStartSpan); + helper.saveSpanToAppStartMetrics(); } } @@ -524,7 +513,10 @@ public void onActivitySaveInstanceState( @Override public synchronized void onActivityDestroyed(final @NotNull Activity activity) { - activityLifecycleMap.remove(activity); + final ActivityLifecycleSpanHelper helper = activitySpanHelpers.remove(activity); + if (helper != null) { + helper.clear(); + } if (performanceEnabled) { // in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid @@ -564,7 +556,7 @@ private void clear() { firstActivityCreated = false; lastPausedTime = new SentryNanotimeDate(new Date(0), 0); lastPausedUptimeMillis = 0; - activityLifecycleMap.clear(); + activitySpanHelpers.clear(); } private void finishSpan(final @Nullable ISpan span) { @@ -671,8 +663,9 @@ WeakHashMap getActivitiesWithOngoingTransactions() { } @TestOnly - @NotNull WeakHashMap getActivityLifecycleMap() { - return activityLifecycleMap; + @NotNull + WeakHashMap getActivitySpanHelpers() { + return activitySpanHelpers; } @TestOnly diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java index a3a15d7326c..4f2448d8d9d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/InternalSentrySdk.java @@ -213,14 +213,7 @@ public static Map getAppStartMeasurement() { final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance(); final @NotNull List> spans = new ArrayList<>(); - final @NotNull TimeSpan processInitNativeSpan = new TimeSpan(); - processInitNativeSpan.setStartedAt(metrics.getAppStartTimeSpan().getStartUptimeMs()); - processInitNativeSpan.setStartUnixTimeMs( - metrics.getAppStartTimeSpan().getStartTimestampMs()); // This has to go after setStartedAt - processInitNativeSpan.setStoppedAt(metrics.getClassLoadedUptimeMs()); - processInitNativeSpan.setDescription("Process Initialization"); - - addTimeSpanToSerializedSpans(processInitNativeSpan, spans); + addTimeSpanToSerializedSpans(metrics.createProcessInitSpan(), spans); addTimeSpanToSerializedSpans(metrics.getApplicationOnCreateTimeSpan(), spans); for (final TimeSpan span : metrics.getContentProviderOnCreateTimeSpans()) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 242902b339c..7438a2136df 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -13,7 +13,6 @@ import io.sentry.SpanDataConvention; import io.sentry.SpanId; import io.sentry.SpanStatus; -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.App; @@ -219,8 +218,8 @@ private boolean hasAppStartSpan(final @NotNull SentryTransaction txn) { private void attachAppStartSpans( final @NotNull AppStartMetrics appStartMetrics, final @NotNull SentryTransaction txn) { - // data will be filled only for cold and warm app starts - if (appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.UNKNOWN) { + // We include process init, content providers and application.onCreate spans only on cold start + if (appStartMetrics.getAppStartType() != AppStartMetrics.AppStartType.COLD) { return; } @@ -234,80 +233,44 @@ private void attachAppStartSpans( @Nullable SpanId parentSpanId = null; final @NotNull List spans = txn.getSpans(); for (final @NotNull SentrySpan span : spans) { - if (span.getOp().contentEquals(APP_START_COLD) - || span.getOp().contentEquals(APP_START_WARM)) { + if (span.getOp().contentEquals(APP_START_COLD)) { parentSpanId = span.getSpanId(); break; } } - // We include process init, content providers and application.onCreate spans only on cold start - if (appStartMetrics.getAppStartType() == AppStartMetrics.AppStartType.COLD) { - // Process init - final long classInitUptimeMs = appStartMetrics.getClassLoadedUptimeMs(); - final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan(); - if (appStartTimeSpan.hasStarted() - && Math.abs(classInitUptimeMs - appStartTimeSpan.getStartUptimeMs()) - <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { - final @NotNull TimeSpan processInitTimeSpan = new TimeSpan(); - processInitTimeSpan.setStartedAt(appStartTimeSpan.getStartUptimeMs()); - processInitTimeSpan.setStartUnixTimeMs(appStartTimeSpan.getStartTimestampMs()); - - processInitTimeSpan.setStoppedAt(classInitUptimeMs); - processInitTimeSpan.setDescription("Process Initialization"); - - txn.getSpans() - .add( - timeSpanToSentrySpan( - processInitTimeSpan, parentSpanId, traceId, APP_METRICS_PROCESS_INIT_OP)); - } - - // Content Providers - final @NotNull List contentProviderOnCreates = - appStartMetrics.getContentProviderOnCreateTimeSpans(); - if (!contentProviderOnCreates.isEmpty()) { - for (final @NotNull TimeSpan contentProvider : contentProviderOnCreates) { - txn.getSpans() - .add( - timeSpanToSentrySpan( - contentProvider, parentSpanId, traceId, APP_METRICS_CONTENT_PROVIDER_OP)); - } - } + // Process init + final long classInitUptimeMs = appStartMetrics.getClassLoadedUptimeMs(); + final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan(); + if (appStartTimeSpan.hasStarted() + && Math.abs(classInitUptimeMs - appStartTimeSpan.getStartUptimeMs()) + <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { + final @NotNull TimeSpan processInitTimeSpan = appStartMetrics.createProcessInitSpan(); + + txn.getSpans() + .add( + timeSpanToSentrySpan( + processInitTimeSpan, parentSpanId, traceId, APP_METRICS_PROCESS_INIT_OP)); + } - // Application.onCreate - final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan(); - if (appOnCreate.hasStopped()) { + // Content Providers + final @NotNull List contentProviderOnCreates = + appStartMetrics.getContentProviderOnCreateTimeSpans(); + if (!contentProviderOnCreates.isEmpty()) { + for (final @NotNull TimeSpan contentProvider : contentProviderOnCreates) { txn.getSpans() .add( timeSpanToSentrySpan( - appOnCreate, parentSpanId, traceId, APP_METRICS_APPLICATION_OP)); + contentProvider, parentSpanId, traceId, APP_METRICS_CONTENT_PROVIDER_OP)); } } - // Activities - final @NotNull List activityLifecycleTimeSpans = - appStartMetrics.getActivityLifecycleTimeSpans(); - for (ActivityLifecycleTimeSpan activityTimeSpan : activityLifecycleTimeSpans) { - if (activityTimeSpan.getOnCreate().hasStarted() - && activityTimeSpan.getOnCreate().hasStopped()) { - txn.getSpans() - .add( - timeSpanToSentrySpan( - activityTimeSpan.getOnCreate(), - parentSpanId, - traceId, - APP_METRICS_ACTIVITIES_OP)); - } - if (activityTimeSpan.getOnStart().hasStarted() - && activityTimeSpan.getOnStart().hasStopped()) { - txn.getSpans() - .add( - timeSpanToSentrySpan( - activityTimeSpan.getOnStart(), - parentSpanId, - traceId, - APP_METRICS_ACTIVITIES_OP)); - } + // Application.onCreate + final @NotNull TimeSpan appOnCreate = appStartMetrics.getApplicationOnCreateTimeSpan(); + if (appOnCreate.hasStopped()) { + txn.getSpans() + .add( + timeSpanToSentrySpan(appOnCreate, parentSpanId, traceId, APP_METRICS_APPLICATION_OP)); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 5bd93584293..8baf10ebc32 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -3,17 +3,13 @@ import static io.sentry.Sentry.APP_START_PROFILING_CONFIG_FILE_NAME; import android.annotation.SuppressLint; -import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.pm.ProviderInfo; import android.net.Uri; import android.os.Build; -import android.os.Handler; -import android.os.Looper; import android.os.Process; import android.os.SystemClock; -import androidx.annotation.NonNull; import io.sentry.ILogger; import io.sentry.ITransactionProfiler; import io.sentry.JsonSerializer; @@ -22,9 +18,7 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.TracesSamplingDecision; -import io.sentry.android.core.internal.util.FirstDrawDoneListener; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; -import io.sentry.android.core.performance.ActivityLifecycleCallbacksAdapter; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; import java.io.BufferedReader; @@ -33,7 +27,6 @@ import java.io.FileNotFoundException; import java.io.InputStreamReader; import java.io.Reader; -import java.util.concurrent.atomic.AtomicBoolean; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelper.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelper.java new file mode 100644 index 00000000000..7fed5e0fdbe --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelper.java @@ -0,0 +1,138 @@ +package io.sentry.android.core.performance; + +import android.os.Looper; +import android.os.SystemClock; +import io.sentry.ISpan; +import io.sentry.Instrumenter; +import io.sentry.SentryDate; +import io.sentry.SpanDataConvention; +import io.sentry.SpanStatus; +import io.sentry.android.core.AndroidDateUtils; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public class ActivityLifecycleSpanHelper { + private static final String APP_METRICS_ACTIVITIES_OP = "activity.load"; + + private final @NotNull String activityName; + + private @Nullable SentryDate onCreateStartTimestamp = null; + private @Nullable SentryDate onStartStartTimestamp = null; + private @Nullable ISpan onCreateSpan = null; + private @Nullable ISpan onStartSpan = null; + + public ActivityLifecycleSpanHelper(final @NotNull String activityName) { + this.activityName = activityName; + } + + public void setOnCreateStartTimestamp(final @NotNull SentryDate onCreateStartTimestamp) { + this.onCreateStartTimestamp = onCreateStartTimestamp; + } + + public void setOnStartStartTimestamp(final @NotNull SentryDate onStartStartTimestamp) { + this.onStartStartTimestamp = onStartStartTimestamp; + } + + public void createAndStopOnCreateSpan(final @Nullable ISpan appStartSpan) { + if (onCreateStartTimestamp != null && appStartSpan != null) { + onCreateSpan = + createLifecycleSpan(appStartSpan, activityName + ".onCreate", onCreateStartTimestamp); + onCreateSpan.finish(); + } + } + + public void createAndStopOnStartSpan(final @Nullable ISpan appStartSpan) { + if (onStartStartTimestamp != null && appStartSpan != null) { + onStartSpan = + createLifecycleSpan(appStartSpan, activityName + ".onStart", onStartStartTimestamp); + onStartSpan.finish(); + } + } + + public @Nullable ISpan getOnCreateSpan() { + return onCreateSpan; + } + + public @Nullable ISpan getOnStartSpan() { + return onStartSpan; + } + + public @Nullable SentryDate getOnCreateStartTimestamp() { + return onCreateStartTimestamp; + } + + public @Nullable SentryDate getOnStartStartTimestamp() { + return onStartStartTimestamp; + } + + public void saveSpanToAppStartMetrics() { + if (onCreateSpan == null || onStartSpan == null) { + return; + } + final @Nullable SentryDate onCreateFinishDate = onCreateSpan.getFinishDate(); + final @Nullable SentryDate onStartFinishDate = onStartSpan.getFinishDate(); + if (onCreateFinishDate == null || onStartFinishDate == null) { + return; + } + final long now = SystemClock.uptimeMillis(); + final @NotNull SentryDate nowDate = AndroidDateUtils.getCurrentSentryDateTime(); + final long onCreateShiftMs = + TimeUnit.NANOSECONDS.toMillis(nowDate.diff(onCreateSpan.getStartDate())); + final long onCreateStopShiftMs = + TimeUnit.NANOSECONDS.toMillis(nowDate.diff(onCreateFinishDate)); + final long onStartShiftMs = + TimeUnit.NANOSECONDS.toMillis(nowDate.diff(onStartSpan.getStartDate())); + final long onStartStopShiftMs = TimeUnit.NANOSECONDS.toMillis(nowDate.diff(onStartFinishDate)); + + ActivityLifecycleTimeSpan activityLifecycleTimeSpan = new ActivityLifecycleTimeSpan(); + activityLifecycleTimeSpan + .getOnCreate() + .setup( + onCreateSpan.getDescription(), + TimeUnit.NANOSECONDS.toMillis(onCreateSpan.getStartDate().nanoTimestamp()), + now - onCreateShiftMs, + now - onCreateStopShiftMs); + activityLifecycleTimeSpan + .getOnStart() + .setup( + onStartSpan.getDescription(), + TimeUnit.NANOSECONDS.toMillis(onStartSpan.getStartDate().nanoTimestamp()), + now - onStartShiftMs, + now - onStartStopShiftMs); + AppStartMetrics.getInstance().addActivityLifecycleTimeSpans(activityLifecycleTimeSpan); + } + + private @NotNull ISpan createLifecycleSpan( + final @NotNull ISpan appStartSpan, + final @NotNull String description, + final @NotNull SentryDate startTimestamp) { + final @NotNull ISpan span = + appStartSpan.startChild( + APP_METRICS_ACTIVITIES_OP, description, startTimestamp, Instrumenter.SENTRY); + setDefaultStartSpanData(span); + return span; + } + + public void clear() { + // in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid + // memory leak + if (onCreateSpan != null && !onCreateSpan.isFinished()) { + onCreateSpan.finish(SpanStatus.CANCELLED); + } + onCreateSpan = null; + if (onStartSpan != null && !onStartSpan.isFinished()) { + onStartSpan.finish(SpanStatus.CANCELLED); + } + onStartSpan = null; + } + + private void setDefaultStartSpanData(final @NotNull ISpan span) { + span.setData(SpanDataConvention.THREAD_ID, Looper.getMainLooper().getThread().getId()); + span.setData(SpanDataConvention.THREAD_NAME, "main"); + span.setData(SpanDataConvention.CONTRIBUTES_TTID, true); + span.setData(SpanDataConvention.CONTRIBUTES_TTFD, true); + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 2e249d6dccf..70dd683a412 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -89,6 +89,20 @@ public AppStartMetrics() { return appStartSpan; } + /** + * @return the app start span Uses Process.getStartUptimeMillis() as start timestamp, which + * requires API level 24+ + */ + public @NotNull TimeSpan createProcessInitSpan() { + final @NotNull TimeSpan processInitSpan = new TimeSpan(); + processInitSpan.setup( + "Process Initialization", + appStartSpan.getStartTimestampMs(), + appStartSpan.getStartUptimeMs(), + CLASS_LOADED_UPTIME_MS); + return processInitSpan; + } + /** * @return the SDK init time span, as measured pre-performance-v2 Uses ContentProvider/Sdk init * time as start timestamp diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java index dac78920f83..eb631739728 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/TimeSpan.java @@ -4,7 +4,6 @@ import io.sentry.DateUtils; import io.sentry.SentryDate; import io.sentry.SentryLongDate; -import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,17 +20,25 @@ public class TimeSpan implements Comparable { private @Nullable String description; - - private long startSystemNanos; private long startUnixTimeMs; private long startUptimeMs; private long stopUptimeMs; + public void setup( + final @Nullable String description, + final long startUnixTimeMs, + final long startUptimeMs, + final long stopUptimeMs) { + this.description = description; + this.startUnixTimeMs = startUnixTimeMs; + this.startUptimeMs = startUptimeMs; + this.stopUptimeMs = stopUptimeMs; + } + /** Start the time span */ public void start() { startUptimeMs = SystemClock.uptimeMillis(); startUnixTimeMs = System.currentTimeMillis(); - startSystemNanos = System.nanoTime(); } /** @@ -43,7 +50,6 @@ public void setStartedAt(final long uptimeMs) { final long shiftMs = SystemClock.uptimeMillis() - startUptimeMs; startUnixTimeMs = System.currentTimeMillis() - shiftMs; - startSystemNanos = System.nanoTime() - TimeUnit.MILLISECONDS.toNanos(shiftMs); } /** Stops the time span */ @@ -166,7 +172,6 @@ public void reset() { startUptimeMs = 0; stopUptimeMs = 0; startUnixTimeMs = 0; - startSystemNanos = 0; } @Override From 7eb96801c5348aabc5788b455cc55c539cacfdf5 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 5 Dec 2024 13:07:11 +0100 Subject: [PATCH 10/15] add tests --- .../core/ActivityLifecycleIntegrationTest.kt | 84 ++++++-- .../PerformanceAndroidEventProcessorTest.kt | 17 +- .../core/SentryPerformanceProviderTest.kt | 23 --- .../ActivityLifecycleSpanHelperTest.kt | 182 ++++++++++++++++++ .../core/performance/AppStartMetricsTest.kt | 24 ++- 5 files changed, 269 insertions(+), 61 deletions(-) create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index be000b7517c..e9021c68302 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -784,6 +784,7 @@ class ActivityLifecycleIntegrationTest { val endDate = appStartMetrics.sdkInitTimeSpan.projectedStopTimestamp val activity = mock() + sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) val appStartSpan = fixture.transaction.children.first { it.operation.startsWith("app.start.warm") } @@ -893,6 +894,7 @@ class ActivityLifecycleIntegrationTest { setAppStartTime(date) val activity = mock() + sut.onActivityPreCreated(activity, fixture.bundle) sut.onActivityCreated(activity, fixture.bundle) val span = fixture.transaction.children.first() @@ -934,6 +936,7 @@ class ActivityLifecycleIntegrationTest { setAppStartTime(date, stopDate) val activity = mock() + sut.onActivityPreCreated(activity, null) sut.onActivityCreated(activity, null) val span = fixture.transaction.children.first() @@ -954,6 +957,7 @@ class ActivityLifecycleIntegrationTest { setAppStartTime(date) val activity = mock() + sut.onActivityPreCreated(activity, null) sut.onActivityCreated(activity, null) val span = fixture.transaction.children.first() @@ -1424,7 +1428,7 @@ class ActivityLifecycleIntegrationTest { } @Test - fun `On activity preCreated onCreate span is created`() { + fun `On activity preCreated onCreate span is started`() { val sut = fixture.getSut() fixture.options.tracesSampleRate = 1.0 sut.register(fixture.hub, fixture.options) @@ -1432,16 +1436,14 @@ class ActivityLifecycleIntegrationTest { val date = SentryNanotimeDate(Date(1), 0) setAppStartTime(date) - assertTrue(sut.activityLifecycleMap.isEmpty()) + assertTrue(sut.activitySpanHelpers.isEmpty()) val activity = mock() - // Activity onCreate date will be used + // Activity onPreCreate date will be used sut.onActivityPreCreated(activity, fixture.bundle) - // sut.onActivityCreated(activity, fixture.bundle) - assertFalse(sut.activityLifecycleMap.isEmpty()) - assertTrue(sut.activityLifecycleMap.values.first().onCreate.hasStarted()) - assertFalse(sut.activityLifecycleMap.values.first().onCreate.hasStopped()) + assertFalse(sut.activitySpanHelpers.isEmpty()) + assertNotNull(sut.activitySpanHelpers[activity]!!.onCreateStartTimestamp) } @Test @@ -1456,29 +1458,50 @@ class ActivityLifecycleIntegrationTest { setAppStartTime(appStartDate) sut.register(fixture.hub, fixture.options) - assertTrue(sut.activityLifecycleMap.isEmpty()) + assertTrue(sut.activitySpanHelpers.isEmpty()) sut.onActivityPreCreated(activity, null) - assertFalse(sut.activityLifecycleMap.isEmpty()) - val activityLifecycleSpan = sut.activityLifecycleMap.values.first() - assertTrue(activityLifecycleSpan.onCreate.hasStarted()) + assertFalse(sut.activitySpanHelpers.isEmpty()) + val helper = sut.activitySpanHelpers.values.first() + assertNotNull(helper.onCreateStartTimestamp) assertEquals(startDate.nanoTimestamp(), sut.getProperty("lastPausedTime").nanoTimestamp()) sut.onActivityCreated(activity, null) assertNotNull(sut.appStartSpan) sut.onActivityPostCreated(activity, null) - assertTrue(activityLifecycleSpan.onCreate.hasStopped()) + assertTrue(helper.onCreateSpan!!.isFinished) sut.onActivityPreStarted(activity) - assertTrue(activityLifecycleSpan.onStart.hasStarted()) + assertNotNull(helper.onStartStartTimestamp) sut.onActivityStarted(activity) assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) sut.onActivityPostStarted(activity) - assertTrue(activityLifecycleSpan.onStart.hasStopped()) + assertTrue(helper.onStartSpan!!.isFinished) + assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + } + + @Test + fun `Save activity lifecycle spans in AppStartMetrics onPostSarted`() { + val sut = fixture.getSut() + fixture.options.tracesSampleRate = 1.0 + val appStartMetrics = AppStartMetrics.getInstance() + val activity = mock() + setAppStartTime() + + sut.register(fixture.hub, fixture.options) + assertTrue(sut.activitySpanHelpers.isEmpty()) + + sut.onActivityPreCreated(activity, null) + sut.onActivityCreated(activity, null) + sut.onActivityPostCreated(activity, null) + sut.onActivityPreStarted(activity) + sut.onActivityStarted(activity) + assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + sut.onActivityPostStarted(activity) assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) } @@ -1494,23 +1517,41 @@ class ActivityLifecycleIntegrationTest { setAppStartTime(appStartDate) sut.register(fixture.hub, fixture.options) - assertTrue(sut.activityLifecycleMap.isEmpty()) + assertTrue(sut.activitySpanHelpers.isEmpty()) sut.onActivityCreated(activity, null) - assertFalse(sut.activityLifecycleMap.isEmpty()) - val activityLifecycleSpan = sut.activityLifecycleMap.values.first() - assertTrue(activityLifecycleSpan.onCreate.hasStarted()) + assertFalse(sut.activitySpanHelpers.isEmpty()) + val helper = sut.activitySpanHelpers.values.first() + assertNotNull(helper.onCreateStartTimestamp) assertEquals(startDate.nanoTimestamp(), sut.getProperty("lastPausedTime").nanoTimestamp()) assertNotNull(sut.appStartSpan) sut.onActivityStarted(activity) - assertTrue(activityLifecycleSpan.onCreate.hasStopped()) - assertTrue(activityLifecycleSpan.onStart.hasStarted()) + assertTrue(helper.onCreateSpan!!.isFinished) + assertNotNull(helper.onStartStartTimestamp) assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) sut.onActivityResumed(activity) - assertTrue(activityLifecycleSpan.onStart.hasStopped()) + assertTrue(helper.onStartSpan!!.isFinished) + assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + } + + @Test + fun `Save activity lifecycle spans in AppStartMetrics onResumed on API lower than 29`() { + val sut = fixture.getSut(apiVersion = Build.VERSION_CODES.P) + fixture.options.tracesSampleRate = 1.0 + val appStartMetrics = AppStartMetrics.getInstance() + val activity = mock() + setAppStartTime() + + sut.register(fixture.hub, fixture.options) + assertTrue(sut.activitySpanHelpers.isEmpty()) + + sut.onActivityCreated(activity, null) + sut.onActivityStarted(activity) + assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + sut.onActivityResumed(activity) assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) } @@ -1556,6 +1597,7 @@ class ActivityLifecycleIntegrationTest { val lastUptimeMillis = sut.getProperty("lastPausedUptimeMillis") assertNotEquals(0, lastUptimeMillis) + sut.onActivityPreCreated(activity, null) sut.onActivityCreated(activity, null) // AppStartMetrics app start time is set to Activity preCreated timestamp assertEquals(lastUptimeMillis, appStartMetrics.appStartTimeSpan.startUptimeMs) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index 35e0f5257bf..12a500966f6 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -280,21 +280,10 @@ class PerformanceAndroidEventProcessorTest { "application.load" == it.op } ) - - assertTrue( - tr.spans.any { - "activity.load" == it.op && "MainActivity.onCreate" == it.description - } - ) - assertTrue( - tr.spans.any { - "activity.load" == it.op && "MainActivity.onStart" == it.description - } - ) } @Test - fun `adds app start metrics to app warm start txn`() { + fun `does not add app start metrics to app warm start txn`() { // given some app start metrics val appStartMetrics = AppStartMetrics.getInstance() appStartMetrics.appStartType = AppStartType.WARM @@ -337,10 +326,6 @@ class PerformanceAndroidEventProcessorTest { assertFalse(tr.spans.any { "process.load" == it.op }) assertFalse(tr.spans.any { "contentprovider.load" == it.op }) assertFalse(tr.spans.any { "application.load" == it.op }) - - // activity spans should be attached - assertTrue(tr.spans.any { "activity.load" == it.op && "MainActivity.onCreate" == it.description }) - assertTrue(tr.spans.any { "activity.load" == it.op && "MainActivity.onStart" == it.description }) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index 9f868d701b4..d4469df0715 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -101,29 +101,6 @@ class SentryPerformanceProviderTest { assertTrue(AppStartMetrics.getInstance().appStartTimeSpan.hasStarted()) } - @Test - fun `provider sets both appstart and sdk init start + end times`() { - val provider = fixture.getSut() - provider.onAppStartDone() - - val metrics = AppStartMetrics.getInstance() - assertTrue(metrics.appStartTimeSpan.hasStarted()) - assertTrue(metrics.appStartTimeSpan.hasStopped()) - - assertTrue(metrics.sdkInitTimeSpan.hasStarted()) - assertTrue(metrics.sdkInitTimeSpan.hasStopped()) - } - - @Test - fun `provider properly registers and unregisters ActivityLifecycleCallbacks`() { - val provider = fixture.getSut() - - // It register once for the provider itself and once for the appStartMetrics - verify(fixture.mockContext, times(2)).registerActivityLifecycleCallbacks(any()) - provider.onAppStartDone() - verify(fixture.mockContext).unregisterActivityLifecycleCallbacks(any()) - } - //region app start profiling @Test fun `when config file does not exists, nothing happens`() { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt new file mode 100644 index 00000000000..ed2ac1860a3 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt @@ -0,0 +1,182 @@ +package io.sentry.android.core.performance + +import android.os.Looper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.IHub +import io.sentry.ISpan +import io.sentry.SentryNanotimeDate +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.Span +import io.sentry.SpanDataConvention +import io.sentry.SpanOptions +import io.sentry.TracesSamplingDecision +import io.sentry.TransactionContext +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import java.util.Date +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class ActivityLifecycleSpanHelperTest { + private class Fixture { + val appStartSpan: ISpan + val hub = mock() + val options = SentryOptions() + val date = SentryNanotimeDate(Date(1), 1000000) + val endDate = SentryNanotimeDate(Date(3), 3000000) + + init { + whenever(hub.options).thenReturn(options) + appStartSpan = Span( + TransactionContext("name", "op", TracesSamplingDecision(true)), + SentryTracer(TransactionContext("name", "op", TracesSamplingDecision(true)), hub), + hub, + null, + SpanOptions() + ) + } + fun getSut(activityName: String = "ActivityName"): ActivityLifecycleSpanHelper { + return ActivityLifecycleSpanHelper(activityName) + } + } + private val fixture = Fixture() + + + @Test + fun `createAndStopOnCreateSpan creates and finishes onCreate span`() { + val helper = fixture.getSut() + val date = SentryNanotimeDate(Date(1), 1) + helper.setOnCreateStartTimestamp(date) + helper.createAndStopOnCreateSpan(fixture.appStartSpan) + + val onCreateSpan = helper.onCreateSpan + assertNotNull(onCreateSpan) + assertTrue(onCreateSpan.isFinished) + + assertEquals("activity.load", onCreateSpan.operation) + assertEquals("ActivityName.onCreate", onCreateSpan.description) + assertEquals(date.nanoTimestamp(), onCreateSpan.startDate.nanoTimestamp()) + assertEquals(date.nanoTimestamp(), onCreateSpan.startDate.nanoTimestamp()) + + assertEquals(Looper.getMainLooper().thread.id, onCreateSpan.getData(SpanDataConvention.THREAD_ID)) + assertEquals("main", onCreateSpan.getData(SpanDataConvention.THREAD_NAME)) + assertEquals(true, onCreateSpan.getData(SpanDataConvention.CONTRIBUTES_TTID)) + assertEquals(true, onCreateSpan.getData(SpanDataConvention.CONTRIBUTES_TTFD)) + } + + @Test + fun `createAndStopOnCreateSpan does nothing if no onCreate start timestamp is available`() { + val helper = fixture.getSut() + helper.createAndStopOnCreateSpan(fixture.appStartSpan) + assertNull(helper.onCreateSpan) + } + + @Test + fun `createAndStopOnCreateSpan does nothing if passed appStartSpan is null`() { + val helper = fixture.getSut() + helper.setOnCreateStartTimestamp(SentryNanotimeDate()) + helper.createAndStopOnCreateSpan(null) + assertNull(helper.onCreateSpan) + } + + @Test + fun `createAndStopOnStartSpan creates and finishes onStart span`() { + val helper = fixture.getSut() + val date = SentryNanotimeDate(Date(1), 1) + helper.setOnStartStartTimestamp(date) + helper.createAndStopOnStartSpan(fixture.appStartSpan) + + val onStartSpan = helper.onStartSpan + assertNotNull(onStartSpan) + assertTrue(onStartSpan.isFinished) + + assertEquals("activity.load", onStartSpan.operation) + assertEquals("ActivityName.onStart", onStartSpan.description) + assertEquals(date.nanoTimestamp(), onStartSpan.startDate.nanoTimestamp()) + assertEquals(date.nanoTimestamp(), onStartSpan.startDate.nanoTimestamp()) + + assertEquals(Looper.getMainLooper().thread.id, onStartSpan.getData(SpanDataConvention.THREAD_ID)) + assertEquals("main", onStartSpan.getData(SpanDataConvention.THREAD_NAME)) + assertEquals(true, onStartSpan.getData(SpanDataConvention.CONTRIBUTES_TTID)) + assertEquals(true, onStartSpan.getData(SpanDataConvention.CONTRIBUTES_TTFD)) + } + + @Test + fun `createAndStopOnStartSpan does nothing if no onStart start timestamp is available`() { + val helper = fixture.getSut() + helper.createAndStopOnStartSpan(fixture.appStartSpan) + assertNull(helper.onStartSpan) + } + + @Test + fun `createAndStopOnStartSpan does nothing if passed appStartSpan is null`() { + val helper = fixture.getSut() + helper.setOnStartStartTimestamp(SentryNanotimeDate()) + helper.createAndStopOnStartSpan(null) + assertNull(helper.onStartSpan) + } + + @Test + fun `saveSpanToAppStartMetrics does nothing if onCreate span is null`() { + val helper = fixture.getSut() + helper.setOnCreateStartTimestamp(fixture.date) + helper.setOnStartStartTimestamp(fixture.date) + helper.createAndStopOnStartSpan(fixture.appStartSpan) + assertNull(helper.onCreateSpan) + assertNotNull(helper.onStartSpan) + } + + @Test + fun `saveSpanToAppStartMetrics does nothing if onStart span is null`() { + val helper = fixture.getSut() + helper.setOnCreateStartTimestamp(fixture.date) + helper.createAndStopOnCreateSpan(fixture.appStartSpan) + helper.setOnStartStartTimestamp(fixture.date) + assertNotNull(helper.onCreateSpan) + assertNull(helper.onStartSpan) + } + + @Test + fun `saveSpanToAppStartMetrics saves spans to AppStartMetrics`() { + val helper = fixture.getSut() + helper.setOnCreateStartTimestamp(fixture.date) + helper.createAndStopOnCreateSpan(fixture.appStartSpan) + helper.onCreateSpan!!.updateEndDate(fixture.endDate) + helper.setOnStartStartTimestamp(fixture.date) + helper.createAndStopOnStartSpan(fixture.appStartSpan) + helper.onStartSpan!!.updateEndDate(fixture.endDate) + assertNotNull(helper.onCreateSpan) + assertNotNull(helper.onStartSpan) + + val appStartMetrics = AppStartMetrics.getInstance() + assertTrue(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + + // Save spans to AppStartMetrics + helper.saveSpanToAppStartMetrics() + assertFalse(appStartMetrics.activityLifecycleTimeSpans.isEmpty()) + val onCreate = appStartMetrics.activityLifecycleTimeSpans.first().onCreate + val onStart = appStartMetrics.activityLifecycleTimeSpans.first().onStart + + assertNotNull(onCreate) + assertEquals(helper.onCreateSpan!!.startDate.nanoTimestamp(), onCreate.startTimestamp!!.nanoTimestamp()) + val spanOnCreateDurationNanos = helper.onCreateSpan!!.finishDate!!.diff(helper.onCreateSpan!!.startDate) + assertEquals(onCreate.durationMs, TimeUnit.NANOSECONDS.toMillis(spanOnCreateDurationNanos)) + assertEquals(onCreate.description, helper.onCreateSpan!!.description) + + assertNotNull(onStart) + assertEquals(helper.onStartSpan!!.startDate.nanoTimestamp(), onStart.startTimestamp!!.nanoTimestamp()) + val spanOnStartDurationNanos = helper.onStartSpan!!.finishDate!!.diff(helper.onStartSpan!!.startDate) + assertEquals(onStart.durationMs, TimeUnit.NANOSECONDS.toMillis(spanOnStartDurationNanos)) + assertEquals(onStart.description, helper.onStartSpan!!.description) + } + + +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index d8b9e727e20..86edd79b4f6 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -5,7 +5,9 @@ import android.content.ContentProvider import android.os.Build import android.os.Looper import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.DateUtils import io.sentry.ITransactionProfiler +import io.sentry.SentryNanotimeDate import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.SentryShadowProcess import org.junit.Before @@ -18,6 +20,7 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.robolectric.Shadows import org.robolectric.annotation.Config +import java.util.Date import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals @@ -56,7 +59,7 @@ class AppStartMetricsTest { metrics.addActivityLifecycleTimeSpans(ActivityLifecycleTimeSpan()) AppStartMetrics.onApplicationCreate(mock()) AppStartMetrics.onContentProviderCreate(mock()) - metrics.setAppStartProfiler(mock()) + metrics.appStartProfiler = mock() metrics.appStartSamplingDecision = mock() metrics.clear() @@ -322,4 +325,23 @@ class AppStartMetricsTest { assertTrue(appStartMetrics.appStartTimeSpan.hasNotStopped()) assertEquals(10, appStartMetrics.appStartTimeSpan.startUptimeMs) } + + @Test + fun `createProcessInitSpan creates a span`() { + val appStartMetrics = AppStartMetrics.getInstance() + val startDate = SentryNanotimeDate(Date(1), 1000000) + appStartMetrics.classLoadedUptimeMs = 10 + val startMillis = DateUtils.nanosToMillis(startDate.nanoTimestamp().toDouble()).toLong() + appStartMetrics.appStartTimeSpan.setStartedAt(1) + appStartMetrics.appStartTimeSpan.setStartUnixTimeMs(startMillis) + val span = appStartMetrics.createProcessInitSpan() + + assertEquals("Process Initialization", span.description) + // Start timestampMs is taken by appStartSpan + assertEquals(startMillis, span.startTimestampMs) + // Start uptime is taken by appStartSpan and stop uptime is class loaded uptime: 10 - 1 + assertEquals(9, span.durationMs) + // Class loaded uptimeMs is 10 ms, and process init span should finish at the same ms + assertEquals(10, span.projectedStopTimestampMs) + } } From 5d30553cfbeb752afb61401b66ef3fa4ad6bf302 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 5 Dec 2024 12:12:54 +0000 Subject: [PATCH 11/15] Format code --- .../core/performance/ActivityLifecycleSpanHelperTest.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt index ed2ac1860a3..799ee543a9e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt @@ -49,7 +49,6 @@ class ActivityLifecycleSpanHelperTest { } private val fixture = Fixture() - @Test fun `createAndStopOnCreateSpan creates and finishes onCreate span`() { val helper = fixture.getSut() @@ -177,6 +176,4 @@ class ActivityLifecycleSpanHelperTest { assertEquals(onStart.durationMs, TimeUnit.NANOSECONDS.toMillis(spanOnStartDurationNanos)) assertEquals(onStart.description, helper.onStartSpan!!.description) } - - } From d0764bdb0e935d077782ac0e5f22e1e7ddcebb42 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 5 Dec 2024 13:19:36 +0100 Subject: [PATCH 12/15] add tests --- .../android/core/performance/ActivityLifecycleSpanHelperTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt index 799ee543a9e..cb0602f016b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt @@ -164,12 +164,14 @@ class ActivityLifecycleSpanHelperTest { val onCreate = appStartMetrics.activityLifecycleTimeSpans.first().onCreate val onStart = appStartMetrics.activityLifecycleTimeSpans.first().onStart + // Check onCreate TimeSpan has same values as helper.onCreateSpan assertNotNull(onCreate) assertEquals(helper.onCreateSpan!!.startDate.nanoTimestamp(), onCreate.startTimestamp!!.nanoTimestamp()) val spanOnCreateDurationNanos = helper.onCreateSpan!!.finishDate!!.diff(helper.onCreateSpan!!.startDate) assertEquals(onCreate.durationMs, TimeUnit.NANOSECONDS.toMillis(spanOnCreateDurationNanos)) assertEquals(onCreate.description, helper.onCreateSpan!!.description) + // Check onStart TimeSpan has same values as helper.onStartSpan assertNotNull(onStart) assertEquals(helper.onStartSpan!!.startDate.nanoTimestamp(), onStart.startTimestamp!!.nanoTimestamp()) val spanOnStartDurationNanos = helper.onStartSpan!!.finishDate!!.diff(helper.onStartSpan!!.startDate) From 8ebf2cdbea91fd7e26de93526ea09f47001b0f5d Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 2 Jan 2025 15:56:42 +0100 Subject: [PATCH 13/15] merged main --- .../PerformanceAndroidEventProcessorTest.kt | 50 ------------------- 1 file changed, 50 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt index f31ad36b5f3..12a500966f6 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/PerformanceAndroidEventProcessorTest.kt @@ -328,56 +328,6 @@ class PerformanceAndroidEventProcessorTest { assertFalse(tr.spans.any { "application.load" == it.op }) } - @Test - fun `adds app start metrics to app warm start txn`() { - // given some app start metrics - val appStartMetrics = AppStartMetrics.getInstance() - appStartMetrics.appStartType = AppStartType.WARM - appStartMetrics.appStartTimeSpan.setStartedAt(123) - appStartMetrics.appStartTimeSpan.setStoppedAt(456) - - val contentProvider = mock() - AppStartMetrics.onContentProviderCreate(contentProvider) - AppStartMetrics.onContentProviderPostCreate(contentProvider) - - appStartMetrics.applicationOnCreateTimeSpan.apply { - setStartedAt(10) - setStoppedAt(42) - } - - val activityTimeSpan = ActivityLifecycleTimeSpan() - activityTimeSpan.onCreate.description = "MainActivity.onCreate" - activityTimeSpan.onStart.description = "MainActivity.onStart" - - activityTimeSpan.onCreate.setStartedAt(200) - activityTimeSpan.onStart.setStartedAt(220) - activityTimeSpan.onStart.setStoppedAt(240) - activityTimeSpan.onCreate.setStoppedAt(260) - appStartMetrics.addActivityLifecycleTimeSpans(activityTimeSpan) - - // when an activity transaction is created - val sut = fixture.getSut(enablePerformanceV2 = true) - val context = TransactionContext("Activity", UI_LOAD_OP) - val tracer = SentryTracer(context, fixture.hub) - var tr = SentryTransaction(tracer) - - // and it contains an app.start.warm span - val appStartSpan = createAppStartSpan(tr.contexts.trace!!.traceId, false) - tr.spans.add(appStartSpan) - - // then the app start metrics should be attached - tr = sut.process(tr, Hint()) - - // process init, content provider and application span should not be attached - assertFalse(tr.spans.any { "process.load" == it.op }) - assertFalse(tr.spans.any { "contentprovider.load" == it.op }) - assertFalse(tr.spans.any { "application.load" == it.op }) - - // activity spans should be attached - assertTrue(tr.spans.any { "activity.load" == it.op && "MainActivity.onCreate" == it.description }) - assertTrue(tr.spans.any { "activity.load" == it.op && "MainActivity.onStart" == it.description }) - } - @Test fun `when app launched from background, app start spans are dropped`() { // given some app start metrics From a35c8fa77526ce5243f12b63e2d6cbc229efcb7c Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 3 Jan 2025 11:46:43 +0100 Subject: [PATCH 14/15] small cleanup on processInitTimeSpan creation --- CHANGELOG.md | 7 ++++++- .../android/core/PerformanceAndroidEventProcessor.java | 10 +++------- .../android/core/performance/AppStartMetrics.java | 2 ++ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b70d48abb..e678b22319a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Internal + +- Warm starts cleanup ([#3954](https://github.com/getsentry/sentry-java/pull/3954)) + ## 7.20.0 ### Features @@ -36,7 +42,6 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE - Session Replay: Allow overriding `SdkVersion` for replay events ([#4014](https://github.com/getsentry/sentry-java/pull/4014)) - Session Replay: Send replay options as tags ([#4015](https://github.com/getsentry/sentry-java/pull/4015)) -- Warm starts cleanup ([#3954](https://github.com/getsentry/sentry-java/pull/3954)) ### Breaking changes diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index 7438a2136df..cf5b0db4abb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -240,13 +240,9 @@ private void attachAppStartSpans( } // Process init - final long classInitUptimeMs = appStartMetrics.getClassLoadedUptimeMs(); - final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan(); - if (appStartTimeSpan.hasStarted() - && Math.abs(classInitUptimeMs - appStartTimeSpan.getStartUptimeMs()) - <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { - final @NotNull TimeSpan processInitTimeSpan = appStartMetrics.createProcessInitSpan(); - + final @NotNull TimeSpan processInitTimeSpan = appStartMetrics.createProcessInitSpan(); + if (processInitTimeSpan.hasStarted() + && processInitTimeSpan.getDurationMs() <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { txn.getSpans() .add( timeSpanToSentrySpan( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 70dd683a412..ad6c4025201 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -94,6 +94,8 @@ public AppStartMetrics() { * requires API level 24+ */ public @NotNull TimeSpan createProcessInitSpan() { + // AppStartSpan and CLASS_LOADED_UPTIME_MS can be modified at any time. + // So, we cannot cache the processInitSpan, but we need to create it when needed. final @NotNull TimeSpan processInitSpan = new TimeSpan(); processInitSpan.setup( "Process Initialization", From d1323263bbdfcdaac263d70e9838b3667cb827d1 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 3 Jan 2025 12:07:11 +0100 Subject: [PATCH 15/15] updated process init span duration check for negative values --- .../sentry/android/core/PerformanceAndroidEventProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java index cf5b0db4abb..b75b1e35c06 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/PerformanceAndroidEventProcessor.java @@ -242,7 +242,7 @@ private void attachAppStartSpans( // Process init final @NotNull TimeSpan processInitTimeSpan = appStartMetrics.createProcessInitSpan(); if (processInitTimeSpan.hasStarted() - && processInitTimeSpan.getDurationMs() <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { + && Math.abs(processInitTimeSpan.getDurationMs()) <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { txn.getSpans() .add( timeSpanToSentrySpan(