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

Add capabilities to track jetpack compose composition/rendering time #2507

Merged
merged 29 commits into from
Mar 16, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c720257
Add initial poc for tracking compose rendering time
markushi Jan 26, 2023
777da18
Merge branch 'main' into feat/compose-tracing
markushi Jan 27, 2023
b28318b
Add SpanOptions to auto-remove, trim and finish spans
markushi Feb 1, 2023
cd8e3e4
Improve Compose tracing, only first composition/rendering is tracked
markushi Feb 1, 2023
aa9f166
Merge branch 'main' into feat/compose-tracing
markushi Feb 1, 2023
81f1ba5
Fix span trimming logic
markushi Feb 1, 2023
c43fcd4
Format code
getsentry-bot Feb 1, 2023
a105676
Add tests, remove optional span
markushi Feb 3, 2023
50ed172
Merge branch 'feat/compose-tracing' of github.com:getsentry/sentry-ja…
markushi Feb 3, 2023
a2718a8
Merge branch 'main' into feat/compose-tracing
markushi Feb 3, 2023
17500bd
Update changelog
markushi Feb 3, 2023
78c7345
Apply suggestions from code review
markushi Feb 9, 2023
b8689b2
Merge branch 'main' into feat/compose-tracing
markushi Feb 9, 2023
49f1310
Update based on PR comments
markushi Feb 16, 2023
bde92b7
Merge branch 'main' into feat/compose-tracing
markushi Feb 21, 2023
4098d79
Let TransactionOptions inherit from SpanOptions
markushi Feb 21, 2023
5929c5a
Fix Changelog
markushi Feb 21, 2023
b87364e
Added enableUserInteractionTracing to SentryTraced
markushi Feb 27, 2023
be159b4
Add more tests for span trimming
markushi Feb 27, 2023
ef3702f
Fix use proper status for finishing idle spans
markushi Feb 27, 2023
845b7a6
Merge branch 'main' into feat/compose-tracing
markushi Feb 27, 2023
babea52
Fix merge conflict
markushi Feb 27, 2023
83e5a6f
Change visibility of span start date setter
markushi Feb 27, 2023
72c276e
Merge branch 'main' into feat/compose-tracing
markushi Mar 9, 2023
672fb22
Make activity transactions idle
markushi Mar 10, 2023
8750da7
Fix span trim start/end logic
markushi Mar 14, 2023
f3463db
Add e2e UI test to ensure ttid/ttfd spans are created
markushi Mar 14, 2023
ac5f687
Merge branch 'main' into feat/compose-tracing
markushi Mar 15, 2023
e8d857d
Merge branch 'main' into feat/compose-tracing
markushi Mar 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

- Improve versatility of exception resolver component for Spring with more flexible API for consumers. ([#2577](https://github.com/getsentry/sentry-java/pull/2577))
- 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()) {
markushi marked this conversation as resolved.
Show resolved Hide resolved
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
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

markushi marked this conversation as resolved.
Show resolved Hide resolved
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