Skip to content

Commit

Permalink
Add capabilities to track jetpack compose composition/rendering time (#…
Browse files Browse the repository at this point in the history
…2507)

Co-authored-by: Roman Zavarnitsyn <rom4ek93@gmail.com>
Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
3 people committed Mar 16, 2023
1 parent e0df34f commit 550d8ec
Show file tree
Hide file tree
Showing 20 changed files with 975 additions and 286 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- You can enable it by adding `sentry.enable-tracing=true` to your `application.properties`
- The Spring Boot integration can now be configured to add the `SentryAppender` to specific loggers instead of the `ROOT` logger ([#2173](https://github.com/getsentry/sentry-java/pull/2173))
- You can specify the loggers using `"sentry.logging.loggers[0]=foo.bar` and `"sentry.logging.loggers[1]=baz` in your `application.properties`
- Add capabilities to track Jetpack Compose composition/rendering time ([#2507](https://github.com/getsentry/sentry-java/pull/2507))

### Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,10 @@ private void startTracing(final @NotNull Activity activity) {
final Boolean coldStart = AppStartState.getInstance().isColdStart();

final TransactionOptions transactionOptions = new TransactionOptions();

if (options.isEnableActivityLifecycleTracingAutoFinish()) {
transactionOptions.setIdleTimeout(options.getIdleTimeout());
transactionOptions.setTrimEnd(true);
}
transactionOptions.setWaitForChildren(true);
transactionOptions.setTransactionFinishedCallback(
(finishingTransaction) -> {
Expand Down Expand Up @@ -392,21 +395,11 @@ public synchronized void onActivityResumed(final @NotNull Activity activity) {
mainHandler.post(() -> onFirstFrameDrawn(ttidSpan));
}
addBreadcrumb(activity, "resumed");

// fallback call for API < 29 compatibility, otherwise it happens on onActivityPostResumed
if (!isAllActivityCallbacksAvailable && options != null) {
stopTracing(activity, options.isEnableActivityLifecycleTracingAutoFinish());
}
}

@Override
public synchronized void onActivityPostResumed(final @NotNull Activity activity) {
// only executed if API >= 29 otherwise it happens on onActivityResumed
if (isAllActivityCallbacksAvailable && options != null) {
// this should be called only when onResume has been executed already, which means
// the UI is responsive at this moment.
stopTracing(activity, options.isEnableActivityLifecycleTracingAutoFinish());
}
public void onActivityPostResumed(@NonNull Activity activity) {
// empty override, required to avoid a api-level breaking super.onActivityPostResumed() calls
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import io.sentry.FullyDisplayedReporter
import io.sentry.Hub
import io.sentry.ISentryExecutorService
import io.sentry.Scope
import io.sentry.Sentry
import io.sentry.SentryDate
import io.sentry.SentryLevel
import io.sentry.SentryNanotimeDate
Expand Down Expand Up @@ -71,9 +72,23 @@ class ActivityLifecycleIntegrationTest {
lateinit var transaction: SentryTracer
val buildInfo = mock<BuildInfoProvider>()

fun getSut(apiVersion: Int = 29, importance: Int = RunningAppProcessInfo.IMPORTANCE_FOREGROUND): ActivityLifecycleIntegration {
fun getSut(
apiVersion: Int = 29,
importance: Int = RunningAppProcessInfo.IMPORTANCE_FOREGROUND,
initializer: Sentry.OptionsConfiguration<SentryAndroidOptions>? = null
): ActivityLifecycleIntegration {
initializer?.configure(options)

whenever(hub.options).thenReturn(options)
transaction = SentryTracer(context, hub, true, transactionFinishedCallback)

// TODO: we should let the ActivityLifecycleIntegration create the proper transaction here
val transactionOptions = TransactionOptions().apply {
isWaitForChildren = true
if (options.isEnableActivityLifecycleTracingAutoFinish) {
idleTimeout = options.idleTimeout
}
}
transaction = SentryTracer(context, hub, transactionOptions, transactionFinishedCallback)
whenever(hub.startTransaction(any(), any<TransactionOptions>())).thenReturn(transaction)
whenever(buildInfo.sdkInfoVersion).thenReturn(apiVersion)

Expand Down Expand Up @@ -417,18 +432,32 @@ class ActivityLifecycleIntegrationTest {
}

@Test
fun `When tracing auto finish is enabled and ttid and ttfd spans are finished, it stops the transaction on onActivityPostResumed`() {
val sut = fixture.getSut()
fixture.options.tracesSampleRate = 1.0
fixture.options.isEnableTimeToFullDisplayTracing = true
sut.register(fixture.hub, fixture.options)

fun `When tracing auto finish is enabled and ttid and ttfd spans are finished, it schedules the transaction finish`() {
val activity = mock<Activity>()
val sut = fixture.getSut(initializer = {
it.tracesSampleRate = 1.0
it.isEnableTimeToFullDisplayTracing = true
it.idleTimeout = 200
})
sut.register(fixture.hub, fixture.options)
sut.onActivityCreated(activity, fixture.bundle)

sut.ttidSpanMap.values.first().finish()
sut.ttfdSpan?.finish()
sut.onActivityPostResumed(activity)

// then transaction should not be immediatelly finished
verify(fixture.hub, never())
.captureTransaction(
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull()
)

// but when idle timeout has passed
Thread.sleep(400)

// then the transaction should be finished
verify(fixture.hub).captureTransaction(
check {
assertEquals(SpanStatus.OK, it.status)
Expand Down Expand Up @@ -485,16 +514,16 @@ class ActivityLifecycleIntegrationTest {

@Test
fun `When tracing auto finish is disabled, do not finish transaction`() {
val sut = fixture.getSut()
fixture.options.tracesSampleRate = 1.0
fixture.options.isEnableActivityLifecycleTracingAutoFinish = false
val sut = fixture.getSut(initializer = {
it.tracesSampleRate = 1.0
it.isEnableActivityLifecycleTracingAutoFinish = false
})
sut.register(fixture.hub, fixture.options)

val activity = mock<Activity>()
sut.onActivityCreated(activity, fixture.bundle)
sut.onActivityPostResumed(activity)

verify(fixture.hub, never()).captureTransaction(any(), anyOrNull<TraceContext>(), anyOrNull(), anyOrNull())
verify(fixture.hub, never()).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull())
}

@Test
Expand Down Expand Up @@ -668,37 +697,37 @@ class ActivityLifecycleIntegrationTest {
sut.onActivityCreated(activity, mock())
sut.onActivityResumed(activity)

verify(fixture.hub, never()).captureTransaction(any(), any<TraceContext>(), anyOrNull(), anyOrNull())
verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull())
}

@Test
fun `start transaction on created if API less than 29`() {
fun `do not stop transaction on resumed if API less than 29 and ttid and ttfd are finished`() {
val sut = fixture.getSut(14)
fixture.options.tracesSampleRate = 1.0
fixture.options.isEnableTimeToFullDisplayTracing = true
sut.register(fixture.hub, fixture.options)

setAppStartTime()

val activity = mock<Activity>()
sut.onActivityCreated(activity, mock())
sut.ttidSpanMap.values.first().finish()
sut.ttfdSpan?.finish()
sut.onActivityResumed(activity)

verify(fixture.hub).startTransaction(any(), any<TransactionOptions>())
verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull())
}

@Test
fun `stop transaction on resumed if API 29 less than 29 and ttid and ttfd are finished`() {
fun `start transaction on created if API less than 29`() {
val sut = fixture.getSut(14)
fixture.options.tracesSampleRate = 1.0
fixture.options.isEnableTimeToFullDisplayTracing = true
sut.register(fixture.hub, fixture.options)

setAppStartTime()

val activity = mock<Activity>()
sut.onActivityCreated(activity, mock())
sut.ttidSpanMap.values.first().finish()
sut.ttfdSpan?.finish()
sut.onActivityResumed(activity)

verify(fixture.hub).captureTransaction(any(), anyOrNull<TraceContext>(), anyOrNull(), anyOrNull())
verify(fixture.hub).startTransaction(any(), any<TransactionOptions>())
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.sentry.uitest.android

import androidx.lifecycle.Lifecycle
import androidx.test.core.app.launchActivity
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.Sentry
import io.sentry.SentryLevel
import io.sentry.SentryOptions
import io.sentry.android.core.SentryAndroidOptions
import io.sentry.protocol.SentryTransaction
import org.junit.runner.RunWith
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
class AutomaticSpansTest : BaseUiTest() {

@Test
fun ttidTtfdSpans() {
val transactions = mutableListOf<SentryTransaction>()

initSentry(false) { options: SentryAndroidOptions ->
options.isDebug = true
options.setDiagnosticLevel(SentryLevel.DEBUG)
options.tracesSampleRate = 1.0
options.profilesSampleRate = 1.0
options.isEnableAutoActivityLifecycleTracing = true
options.beforeSendTransaction
options.isEnableTimeToFullDisplayTracing = true
options.beforeSendTransaction =
SentryOptions.BeforeSendTransactionCallback { transaction, _ ->
transactions.add(transaction)
transaction
}
}

val activity = launchActivity<ComposeActivity>()
activity.moveToState(Lifecycle.State.RESUMED)
activity.onActivity {
Sentry.reportFullyDisplayed()
}
activity.moveToState(Lifecycle.State.DESTROYED)

assertEquals(1, transactions.size)
assertTrue("TTID span missing") {
transactions.first().spans.any {
it.op == "ui.load.initial_display"
}
}
assertTrue("TTFD span missing") {
transactions.first().spans.any {
it.op == "ui.load.full_display"
}
}
}
}
4 changes: 4 additions & 0 deletions sentry-compose/api/android/sentry-compose.api
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ public final class io/sentry/compose/BuildConfig {
public fun <init> ()V
}

public final class io/sentry/compose/SentryComposeTracingKt {
public static final fun SentryTraced (Ljava/lang/String;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public final class io/sentry/compose/SentryNavigationIntegrationKt {
public static final fun withSentryObservableEffect (Landroidx/navigation/NavHostController;ZZLandroidx/compose/runtime/Composer;II)Landroidx/navigation/NavHostController;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package io.sentry.compose

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.platform.testTag
import io.sentry.ISpan
import io.sentry.Sentry
import io.sentry.SpanOptions

private const val OP_PARENT_COMPOSITION = "ui.compose.composition"
private const val OP_COMPOSE = "ui.compose"

private const val OP_PARENT_RENDER = "ui.compose.rendering"
private const val OP_RENDER = "ui.render"

@Immutable
private class ImmutableHolder<T>(var item: T)

private fun getRootSpan(): ISpan? {
var rootSpan: ISpan? = null
Sentry.configureScope {
rootSpan = it.transaction
}
return rootSpan
}

private val localSentryCompositionParentSpan = compositionLocalOf {
ImmutableHolder(
getRootSpan()
?.startChild(
OP_PARENT_COMPOSITION,
"Jetpack Compose Initial Composition",
SpanOptions().apply {
isTrimStart = true
isTrimEnd = true
isIdle = true
}
)
)
}

private val localSentryRenderingParentSpan = compositionLocalOf {
ImmutableHolder(
getRootSpan()
?.startChild(
OP_PARENT_RENDER,
"Jetpack Compose Initial Render",
SpanOptions().apply {
isTrimStart = true
isTrimEnd = true
isIdle = true
}
)
)
}

@ExperimentalComposeUiApi
@Composable
public fun SentryTraced(
tag: String,
modifier: Modifier = Modifier,
enableUserInteractionTracing: Boolean = true,
content: @Composable BoxScope.() -> Unit
) {
val parentCompositionSpan = localSentryCompositionParentSpan.current
val parentRenderingSpan = localSentryRenderingParentSpan.current
val compositionSpan = parentCompositionSpan.item?.startChild(OP_COMPOSE, tag)
val firstRendered = remember { ImmutableHolder(false) }

val baseModifier = if (enableUserInteractionTracing) modifier.testTag(tag) else modifier

Box(
modifier = baseModifier
.drawWithContent {
val renderSpan = if (!firstRendered.item) {
parentRenderingSpan.item?.startChild(
OP_RENDER,
tag
)
} else {
null
}
drawContent()
firstRendered.item = true
renderSpan?.finish()
}
) {
content()
}
compositionSpan?.finish()
}
Loading

0 comments on commit 550d8ec

Please sign in to comment.