Skip to content

Commit

Permalink
Feat: Measure app start time (#1487)
Browse files Browse the repository at this point in the history
  • Loading branch information
marandaneto authored Jun 11, 2021
1 parent 97c6a66 commit 4af15fa
Show file tree
Hide file tree
Showing 30 changed files with 1,045 additions and 62 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

* Feat: Measure app start time (#1487)

## 5.0.1

* Fix: Sources and Javadoc artifacts were mixed up (#1515)
Expand Down
11 changes: 11 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ public final class io/sentry/android/core/SentryInitProvider : android/content/C
public fun update (Landroid/net/Uri;Landroid/content/ContentValues;Ljava/lang/String;[Ljava/lang/String;)I
}

public final class io/sentry/android/core/SentryPerformanceProvider : android/content/ContentProvider {
public fun <init> ()V
public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V
public fun delete (Landroid/net/Uri;Ljava/lang/String;[Ljava/lang/String;)I
public fun getType (Landroid/net/Uri;)Ljava/lang/String;
public fun insert (Landroid/net/Uri;Landroid/content/ContentValues;)Landroid/net/Uri;
public fun onCreate ()Z
public fun query (Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;
public fun update (Landroid/net/Uri;Landroid/content/ContentValues;Ljava/lang/String;[Ljava/lang/String;)I
}

public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : io/sentry/Integration, java/io/Closeable {
public fun <init> (Landroid/content/Context;)V
public fun <init> (Landroid/content/Context;Ljava/util/List;)V
Expand Down
6 changes: 6 additions & 0 deletions sentry-android-core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,11 @@
android:name=".SentryInitProvider"
android:authorities="${applicationId}.SentryInitProvider"
android:exported="false"/>

<provider
android:name=".SentryPerformanceProvider"
android:authorities="${applicationId}.SentryPerformanceProvider"
android:initOrder="200"
android:exported="false"/>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import androidx.annotation.Nullable;
import io.sentry.Breadcrumb;
import io.sentry.IHub;
import io.sentry.ISpan;
import io.sentry.ITransaction;
import io.sentry.Integration;
import io.sentry.Scope;
Expand All @@ -17,6 +18,7 @@
import io.sentry.util.Objects;
import java.io.Closeable;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.WeakHashMap;
import org.jetbrains.annotations.NotNull;
Expand All @@ -26,6 +28,10 @@
public final class ActivityLifecycleIntegration
implements Integration, Closeable, Application.ActivityLifecycleCallbacks {

private static final String UI_LOAD_OP = "ui.load";
static final String APP_START_WARM = "app.start.warm";
static final String APP_START_COLD = "app.start.cold";

private final @NotNull Application application;
private @Nullable IHub hub;
private @Nullable SentryAndroidOptions options;
Expand All @@ -34,6 +40,11 @@ public final class ActivityLifecycleIntegration

private boolean isAllActivityCallbacksAvailable;

private boolean firstActivityCreated = false;
private boolean firstActivityResumed = false;

private @Nullable ISpan appStartSpan;

// WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
// main-thread
private final @NotNull WeakHashMap<Activity, ITransaction> activitiesWithOngoingTransactions =
Expand Down Expand Up @@ -116,8 +127,21 @@ private void startTracing(final @NonNull Activity activity) {
stopPreviousTransactions();

// we can only bind to the scope if there's no running transaction
final ITransaction transaction =
hub.startTransaction(getActivityName(activity), "navigation");
ITransaction transaction;
final String activityName = getActivityName(activity);

final Date appStartTime = AppStartState.getInstance().getAppStartTime();

// in case appStartTime isn't available, we don't create a span for it.
if (firstActivityCreated || appStartTime == null) {
transaction = hub.startTransaction(activityName, UI_LOAD_OP);
} else {
// start transaction with app start timestamp
transaction = hub.startTransaction(activityName, UI_LOAD_OP, appStartTime);
// start specific span for app start

appStartSpan = transaction.startChild(getAppStartOp(), getAppStartDesc(), appStartTime);
}

// lets bind to the scope so other integrations can pick it up
hub.configureScope(
Expand Down Expand Up @@ -176,6 +200,8 @@ public synchronized void onActivityPreCreated(

// only executed if API >= 29 otherwise it happens on onActivityCreated
if (isAllActivityCallbacksAvailable) {
setColdStart(savedInstanceState);

// if activity has global fields being init. and
// they are slow, this won't count the whole fields/ctor initialization time, but only
// when onCreate is actually called.
Expand All @@ -186,12 +212,17 @@ public synchronized void onActivityPreCreated(
@Override
public synchronized void onActivityCreated(
final @NonNull Activity activity, final @Nullable Bundle savedInstanceState) {
if (!isAllActivityCallbacksAvailable) {
setColdStart(savedInstanceState);
}

addBreadcrumb(activity, "created");

// fallback call for API < 29 compatibility, otherwise it happens on onActivityPreCreated
if (!isAllActivityCallbacksAvailable) {
startTracing(activity);
}
firstActivityCreated = true;
}

@Override
Expand All @@ -201,6 +232,17 @@ public synchronized void onActivityStarted(final @NonNull Activity activity) {

@Override
public synchronized void onActivityResumed(final @NonNull Activity activity) {
if (!firstActivityResumed && performanceEnabled) {
// sets App start as finished when the very first activity calls onResume
AppStartState.getInstance().setAppStartEnd();

// finishes app start span
if (appStartSpan != null) {
appStartSpan.finish();
}
firstActivityResumed = true;
}

addBreadcrumb(activity, "resumed");

// fallback call for API < 29 compatibility, otherwise it happens on onActivityPostResumed
Expand Down Expand Up @@ -256,4 +298,28 @@ public synchronized void onActivityDestroyed(final @NonNull Activity activity) {
WeakHashMap<Activity, ITransaction> getActivitiesWithOngoingTransactions() {
return activitiesWithOngoingTransactions;
}

private void setColdStart(final @Nullable Bundle savedInstanceState) {
if (!firstActivityCreated && performanceEnabled) {
// if Activity has savedInstanceState then its a warm start
// https://developer.android.com/topic/performance/vitals/launch-time#warm
AppStartState.getInstance().setColdStart(savedInstanceState == null);
}
}

private @NotNull String getAppStartDesc() {
if (AppStartState.getInstance().isColdStart()) {
return "Cold Start";
} else {
return "Warm Start";
}
}

private @NotNull String getAppStartOp() {
if (AppStartState.getInstance().isColdStart()) {
return APP_START_COLD;
} else {
return APP_START_WARM;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ static void init(
readDefaultOptionValues(options, context);

options.addEventProcessor(new DefaultAndroidEventProcessor(context, logger, buildInfoProvider));
options.addEventProcessor(new PerformanceAndroidEventProcessor(options));

options.setTransportGate(new AndroidTransportGate(context, options.getLogger()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.sentry.android.core;

import android.os.SystemClock;
import java.util.Date;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

/** AppStartState holds the state of the App Start metric and appStartTime */
final class AppStartState {

private static @NotNull AppStartState instance = new AppStartState();

private @Nullable Long appStartMillis;

private @Nullable Long appStartEndMillis;

/** The type of App start coldStart=true -> Cold start, coldStart=false -> Warm start */
private boolean coldStart;

/** appStart as a Date used in the App's Context */
private @Nullable Date appStartTime;

private AppStartState() {}

static @NotNull AppStartState getInstance() {
return instance;
}

@TestOnly
void resetInstance() {
instance = new AppStartState();
}

synchronized void setAppStartEnd() {
setAppStartEnd(SystemClock.uptimeMillis());
}

@TestOnly
void setAppStartEnd(final long appStartEndMillis) {
this.appStartEndMillis = appStartEndMillis;
}

@Nullable
synchronized Long getAppStartInterval() {
if (appStartMillis == null || appStartEndMillis == null) {
return null;
}
return appStartEndMillis - appStartMillis;
}

boolean isColdStart() {
return coldStart;
}

synchronized void setColdStart(final boolean coldStart) {
this.coldStart = coldStart;
}

@Nullable
Date getAppStartTime() {
return appStartTime;
}

synchronized void setAppStartTime(final long appStartMillis, final @NotNull Date appStartTime) {
// method is synchronized because the SDK may by init. on a background thread.
if (this.appStartTime != null && this.appStartMillis != null) {
return;
}
this.appStartTime = appStartTime;
this.appStartMillis = appStartMillis;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ final class DefaultAndroidEventProcessor implements EventProcessor {
@TestOnly static final String EMULATOR = "emulator";
@TestOnly static final String SIDE_LOADED = "sideLoaded";

// it could also be a parameter and get from Sentry.init(...)
private static final @Nullable Date appStartTime = DateUtils.getCurrentDateTime();

@TestOnly final Context context;

@TestOnly final Future<Map<String, Object>> contextData;
Expand Down Expand Up @@ -299,7 +296,7 @@ private void mergeDebugImages(final @NotNull SentryEvent event) {

private void setAppExtras(final @NotNull App app) {
app.setAppName(getApplicationName());
app.setAppStartTime(appStartTime);
app.setAppStartTime(AppStartState.getInstance().getAppStartTime());
}

@SuppressWarnings("deprecation")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.sentry.android.core;

import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_COLD;
import static io.sentry.android.core.ActivityLifecycleIntegration.APP_START_WARM;

import io.sentry.EventProcessor;
import io.sentry.protocol.MeasurementValue;
import io.sentry.protocol.SentrySpan;
import io.sentry.protocol.SentryTransaction;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/** Event Processor responsible for adding Android metrics to transactions */
final class PerformanceAndroidEventProcessor implements EventProcessor {

private final boolean tracingEnabled;

private boolean sentStartMeasurement = false;

PerformanceAndroidEventProcessor(final @NotNull SentryAndroidOptions options) {
tracingEnabled = options.isTracingEnabled();
}

@Override
public synchronized @NotNull SentryTransaction process(
@NotNull SentryTransaction transaction, @Nullable Object hint) {
// 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 (!sentStartMeasurement && tracingEnabled && hasAppStartSpan(transaction.getSpans())) {
final Long appStartUpInterval = AppStartState.getInstance().getAppStartInterval();
// if appStartUpInterval is null, metrics are not ready to be sent
if (appStartUpInterval != null) {
final MeasurementValue value = new MeasurementValue((float) appStartUpInterval);

final String appStartKey =
AppStartState.getInstance().isColdStart() ? "app_start_cold" : "app_start_warm";

transaction.getMeasurements().put(appStartKey, value);
sentStartMeasurement = true;
}
}

return transaction;
}

private boolean hasAppStartSpan(final @NotNull List<SentrySpan> spans) {
for (final SentrySpan span : spans) {
if (span.getOp().contentEquals(APP_START_COLD)
|| span.getOp().contentEquals(APP_START_WARM)) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package io.sentry.android.core;

import android.content.Context;
import android.os.SystemClock;
import io.sentry.DateUtils;
import io.sentry.ILogger;
import io.sentry.OptionsContainer;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
import java.lang.reflect.InvocationTargetException;
import java.util.Date;
import org.jetbrains.annotations.NotNull;

/** Sentry initialization class */
public final class SentryAndroid {

// static to rely on Class load init.
private static final @NotNull Date appStartTime = DateUtils.getCurrentDateTime();
// SystemClock.uptimeMillis() isn't affected by phone provider or clock changes.
private static final long appStart = SystemClock.uptimeMillis();

private SentryAndroid() {}

/**
Expand Down Expand Up @@ -51,10 +59,14 @@ public static void init(
* @param logger your custom logger that implements ILogger
* @param configuration Sentry.OptionsConfiguration configuration handler
*/
public static void init(
public static synchronized void init(
@NotNull final Context context,
@NotNull ILogger logger,
@NotNull Sentry.OptionsConfiguration<SentryAndroidOptions> configuration) {
// if SentryPerformanceProvider was disabled or removed, we set the App Start when
// the SDK is called.
AppStartState.getInstance().setAppStartTime(appStart, appStartTime);

try {
Sentry.init(
OptionsContainer.create(SentryAndroidOptions.class),
Expand Down
Loading

0 comments on commit 4af15fa

Please sign in to comment.