diff --git a/CHANGELOG.md b/CHANGELOG.md index 3857217a98..e678b22319 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 diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 02725ba5df..f6760e1941 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -434,6 +434,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 @@ -446,6 +460,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; @@ -505,6 +520,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 0912051dd7..14141bf4bb 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,26 +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) { - if (appStartSpan == null) { - return; - } - final @Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap.get(activity); - if (timeSpan != null) { - timeSpan.getOnStart().setStartedAt(SystemClock.uptimeMillis()); + final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity); + if (helper != null) { + helper.setOnStartStartTimestamp( + options != null + ? options.getDateProvider().now() + : AndroidDateUtils.getCurrentSentryDateTime()); } } @@ -457,14 +451,11 @@ 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); + // Needed to handle hybrid SDKs + helper.saveSpanToAppStartMetrics(); } } @@ -523,7 +514,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 @@ -563,7 +557,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) { @@ -608,8 +602,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(); } @@ -672,8 +665,8 @@ WeakHashMap getActivitiesWithOngoingTransactions() { @TestOnly @NotNull - WeakHashMap getActivityLifecycleMap() { - return activityLifecycleMap; + 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 a3a15d7326..4f2448d8d9 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 242902b339..b75b1e35c0 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,40 @@ 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 @NotNull TimeSpan processInitTimeSpan = appStartMetrics.createProcessInitSpan(); + if (processInitTimeSpan.hasStarted() + && Math.abs(processInitTimeSpan.getDurationMs()) <= MAX_PROCESS_INIT_APP_START_DIFF_MS) { + 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 0aa946c255..8baf10ebc3 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; @@ -47,7 +40,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 +191,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); - } - } } } 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 0000000000..7fed5e0fdb --- /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 2e249d6dcc..ad6c402520 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,22 @@ public AppStartMetrics() { return appStartSpan; } + /** + * @return the app start span Uses Process.getStartUptimeMillis() as start timestamp, which + * 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", + 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 dac78920f8..eb63173972 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 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 be000b7517..e9021c6830 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 35e0f5257b..12a500966f 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 9f868d701b..d4469df071 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 0000000000..cb0602f016 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/ActivityLifecycleSpanHelperTest.kt @@ -0,0 +1,181 @@ +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 + + // 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) + 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 d8b9e727e2..86edd79b4f 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) + } }