Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report process init time for app start #3159

Merged
merged 9 commits into from
Feb 12, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add new threshold parameters to monitor config ([#3181](https://github.com/getsentry/sentry-java/pull/3181))
- Report process init time as a span for app start performance ([#3159](https://github.com/getsentry/sentry-java/pull/3159))

## 7.3.0

Expand Down
2 changes: 2 additions & 0 deletions sentry-android-core/api/sentry-android-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ public class io/sentry/android/core/performance/AppStartMetrics {
public fun getAppStartTimeSpanWithFallback (Lio/sentry/android/core/SentryAndroidOptions;)Lio/sentry/android/core/performance/TimeSpan;
public fun getAppStartType ()Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;
public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun getClassLoadedUptimeMs ()J
public fun getContentProviderOnCreateTimeSpans ()Ljava/util/List;
public static fun getInstance ()Lio/sentry/android/core/performance/AppStartMetrics;
public fun getSdkInitTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
Expand All @@ -445,6 +446,7 @@ public class io/sentry/android/core/performance/AppStartMetrics {
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 final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ final class PerformanceAndroidEventProcessor implements EventProcessor {
private static final String APP_METRICS_CONTENT_PROVIDER_OP = "contentprovider.load";
private static final String APP_METRICS_ACTIVITIES_OP = "activity.load";
private static final String APP_METRICS_APPLICATION_OP = "application.load";
private static final String APP_METRICS_PROCESS_INIT_OP = "process.load";
private static final long MAX_PROCESS_INIT_APP_START_DIFF_MS = 10000;

private boolean sentStartMeasurement = false;

Expand Down Expand Up @@ -77,13 +79,13 @@ public SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
if (!sentStartMeasurement && hasAppStartSpan(transaction)) {
final @NotNull TimeSpan appStartTimeSpan =
AppStartMetrics.getInstance().getAppStartTimeSpanWithFallback(options);
final long appStartUpInterval = appStartTimeSpan.getDurationMs();
final long appStartUpDurationMs = appStartTimeSpan.getDurationMs();

// if appStartUpInterval is 0, metrics are not ready to be sent
if (appStartUpInterval != 0) {
// if appStartUpDurationMs is 0, metrics are not ready to be sent
if (appStartUpDurationMs != 0) {
final MeasurementValue value =
new MeasurementValue(
(float) appStartUpInterval, MeasurementUnit.Duration.MILLISECOND.apiName());
(float) appStartUpDurationMs, MeasurementUnit.Duration.MILLISECOND.apiName());

final String appStartKey =
AppStartMetrics.getInstance().getAppStartType() == AppStartMetrics.AppStartType.COLD
Expand Down Expand Up @@ -155,6 +157,25 @@ private void attachColdAppStartSpans(
}
}

// 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) {
markushi marked this conversation as resolved.
Show resolved Hide resolved
final @NotNull TimeSpan processInitTimeSpan = new TimeSpan();
processInitTimeSpan.setStartedAt(appStartTimeSpan.getStartUptimeMs());
processInitTimeSpan.setStartUnixTimeMs(appStartTimeSpan.getStartTimestampMs());

processInitTimeSpan.setStoppedAt(classInitUptimeMs);
processInitTimeSpan.setDescription("Process Initialization");
markushi marked this conversation as resolved.
Show resolved Hide resolved

txn.getSpans()
.add(
timeSpanToSentrySpan(
processInitTimeSpan, parentSpanId, traceId, APP_METRICS_PROCESS_INIT_OP));
}

// Content Providers
final @NotNull List<TimeSpan> contentProviderOnCreates =
appStartMetrics.getContentProviderOnCreateTimeSpans();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public enum AppStartType {
WARM
}

private static long CLASS_LOADED_UPTIME_MS = SystemClock.uptimeMillis();

private static volatile @Nullable AppStartMetrics instance;

private @NotNull AppStartType appStartType = AppStartType.UNKNOWN;
Expand Down Expand Up @@ -121,6 +123,10 @@ public void addActivityLifecycleTimeSpans(final @NotNull ActivityLifecycleTimeSp
activityLifecycles.add(timeSpan);
}

public long getClassLoadedUptimeMs() {
return CLASS_LOADED_UPTIME_MS;
}

/**
* @return the app start time span if it was started and perf-2 is enabled, falls back to the sdk
* init time span otherwise
Expand Down Expand Up @@ -171,6 +177,12 @@ public void setAppStartSamplingDecision(
return appStartSamplingDecision;
}

@TestOnly
@ApiStatus.Internal
public void setClassLoadedUptimeMs(final long classLoadedUptimeMs) {
CLASS_LOADED_UPTIME_MS = classLoadedUptimeMs;
}

/**
* Called by instrumentation
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,13 @@ class PerformanceAndroidEventProcessorTest {
// then the app start metrics should be attached
tr = sut.process(tr, Hint())

assertTrue(
tr.spans.any {
"process.load" == it.op &&
appStartSpan.spanId == it.parentSpanId
}
)

assertTrue(
tr.spans.any {
"contentprovider.load" == it.op &&
Expand Down Expand Up @@ -300,7 +307,7 @@ class PerformanceAndroidEventProcessorTest {

@Test
fun `does not add app start metrics more than once`() {
// given some WARM app start metrics
// given some cold app start metrics
val appStartMetrics = AppStartMetrics.getInstance()
appStartMetrics.appStartType = AppStartType.COLD
appStartMetrics.appStartTimeSpan.setStartedAt(123)
Expand Down Expand Up @@ -351,6 +358,46 @@ class PerformanceAndroidEventProcessorTest {
)
}

@Test
fun `does not add process init span if it happened too early`() {
// given some cold app start metrics
// where class loaded happened way before app start
val appStartMetrics = AppStartMetrics.getInstance()
appStartMetrics.appStartType = AppStartType.COLD
appStartMetrics.appStartTimeSpan.setStartedAt(11001)
appStartMetrics.appStartTimeSpan.setStoppedAt(12000)
appStartMetrics.classLoadedUptimeMs = 1000

val sut = fixture.getSut(enablePerformanceV2 = true)
val context = TransactionContext("Activity", UI_LOAD_OP)
val tracer = SentryTracer(context, fixture.hub)
var tr = SentryTransaction(tracer)
val appStartSpan = SentrySpan(
0.0,
1.0,
tr.contexts.trace!!.traceId,
SpanId(),
null,
APP_START_COLD,
"App Start",
SpanStatus.OK,
null,
emptyMap(),
null
)
tr.spans.add(appStartSpan)

// when the processor attaches the app start spans
tr = sut.process(tr, Hint())

// process load should not be included
assertFalse(
tr.spans.any {
"process.load" == it.op
}
)
}

private fun setAppStart(options: SentryAndroidOptions, coldStart: Boolean = true) {
AppStartMetrics.getInstance().apply {
appStartType = when (coldStart) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.mockito.kotlin.mock
import org.robolectric.annotation.Config
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
Expand Down Expand Up @@ -100,4 +101,9 @@ class AppStartMetricsTest {
val sdkInitSpan = AppStartMetrics.getInstance().sdkInitTimeSpan
assertSame(sdkInitSpan, timeSpan)
}

@Test
fun `class load time is set`() {
assertNotEquals(0, AppStartMetrics.getInstance().classLoadedUptimeMs)
}
}
Loading