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 17 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 @@ -9,6 +9,7 @@
- If set to `true`, performance is enabled, even if no `tracesSampleRate` or `tracesSampler` have been configured.
- If set to `false` performance is disabled, regardless of `tracesSampleRate` and `tracesSampler` options.
- Detect dependencies by listing MANIFEST.MF files at runtime ([#2538](https://github.com/getsentry/sentry-java/pull/2538))
- 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 @@ -64,7 +64,10 @@ class ActivityLifecycleIntegrationTest {

fun getSut(apiVersion: Int = 29, importance: Int = RunningAppProcessInfo.IMPORTANCE_FOREGROUND): ActivityLifecycleIntegration {
whenever(hub.options).thenReturn(options)
transaction = SentryTracer(context, hub, true, transactionFinishedCallback)
val transactionOptions = TransactionOptions().apply {
isWaitForChildren = true
}
transaction = SentryTracer(context, hub, transactionOptions, transactionFinishedCallback)
whenever(hub.startTransaction(any(), any<TransactionOptions>())).thenReturn(transaction)
whenever(buildInfo.sdkInfoVersion).thenReturn(apiVersion)

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;Lkotlin/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,96 @@
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,
null,
SpanOptions().apply {
isTrimStart = true
isTrimEnd = true
isIdle = true
}
)
)
}

private val localSentryRenderingParentSpan = compositionLocalOf {
ImmutableHolder(
getRootSpan()
?.startChild(
OP_PARENT_RENDER,
null,
SpanOptions().apply {
isTrimStart = true
isTrimEnd = true
isIdle = true
}
)
)
}

@ExperimentalComposeUiApi
@Composable
public fun SentryTraced(
tag: String,
modifier: Modifier = Modifier,
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) }

Box(
modifier = modifier
.testTag(tag)
markushi marked this conversation as resolved.
Show resolved Hide resolved
.drawWithContent {
val renderSpan = if (!firstRendered.item) {
parentRenderingSpan.item?.startChild(
OP_RENDER,
tag
)
} else {
null
}
drawContent()
firstRendered.item = true
renderSpan?.finish()
}
) {
content()
}
compositionSpan?.finish()
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalComposeUiApi::class)

package io.sentry.samples.android.compose

import android.os.Bundle
Expand All @@ -18,6 +20,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.TextFieldValue
Expand All @@ -28,7 +31,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import io.sentry.Sentry
import io.sentry.compose.SentryTraced
import io.sentry.compose.withSentryObservableEffect
import io.sentry.samples.android.GithubAPI
import kotlinx.coroutines.launch
Expand All @@ -43,48 +46,45 @@ class ComposeActivity : ComponentActivity() {
SampleNavigation(navController)
}
}

override fun onResume() {
super.onResume()
Sentry.getSpan()?.finish()
}
}

@Composable
fun Landing(
navigateGithub: () -> Unit,
navigateGithubWithArgs: () -> Unit
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Button(
onClick = {
navigateGithub()
},
modifier = Modifier
.testTag("button_nav_github")
.padding(top = 32.dp)
SentryTraced(tag = "buttons_page") {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
Text("Navigate to Github Page")
}
Button(
onClick = { navigateGithubWithArgs() },
modifier = Modifier
.testTag("button_nav_github_args")
.padding(top = 32.dp)
) {
Text("Navigate to Github Page With Args")
}
Button(
onClick = { throw RuntimeException("Crash from Compose") },
modifier = Modifier
.testTag("button_crash")
.padding(top = 32.dp)
) {
Text("Crash from Compose")
SentryTraced(tag = "button_nav_github") {
Button(
onClick = {
navigateGithub()
},
modifier = Modifier.padding(top = 32.dp)
) {
Text("Navigate to Github")
}
}
SentryTraced(tag = "button_nav_github_args") {
Button(
onClick = { navigateGithubWithArgs() },
modifier = Modifier.padding(top = 32.dp)
) {
Text("Navigate to Github Page With Args")
}
}
SentryTraced(tag = "button_crash") {
Button(
onClick = { throw RuntimeException("Crash from Compose") },
modifier = Modifier.padding(top = 32.dp)
) {
Text("Crash from Compose")
}
}
}
}
}
Expand All @@ -102,59 +102,67 @@ fun Github(
result = GithubAPI.service.listReposAsync(user.text, perPage).random().full_name
}

Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxSize()
) {
TextField(
value = user,
onValueChange = { newText ->
user = newText
}
)
Text("Random repo $result")
Button(
onClick = {
scope.launch {
result = GithubAPI.service.listReposAsync(user.text, perPage).random().full_name
}
},
SentryTraced("github-$user") {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.testTag("button_list_repos_async")
.padding(top = 32.dp)
.fillMaxSize()
) {
Text("Make Request")
TextField(
value = user,
onValueChange = { newText ->
user = newText
}
)
Text("Random repo $result")
Button(
onClick = {
scope.launch {
result =
GithubAPI.service.listReposAsync(user.text, perPage).random().full_name
}
},
modifier = Modifier
.testTag("button_list_repos_async")
.padding(top = 32.dp)
) {
Text("Make Request")
}
}
}
}

@Composable
fun SampleNavigation(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = Destination.Landing.route
) {
composable(Destination.Landing.route) {
Landing(
navigateGithub = { navController.navigate("github") },
navigateGithubWithArgs = { navController.navigate("github/spotify?per_page=10") }
)
}
composable(Destination.Github.route) {
Github()
}
composable(
Destination.GithubWithArgs.route,
arguments = listOf(
navArgument(Destination.USER_ARG) { type = NavType.StringType },
navArgument(Destination.PER_PAGE_ARG) { type = NavType.IntType; defaultValue = 10 }
)
SentryTraced(tag = "navhost") {
NavHost(
navController = navController,
startDestination = Destination.Landing.route
) {
Github(
it.arguments?.getString(Destination.USER_ARG) ?: "getsentry",
it.arguments?.getInt(Destination.PER_PAGE_ARG) ?: 10
)
composable(Destination.Landing.route) {
Landing(
navigateGithub = { navController.navigate("github") },
navigateGithubWithArgs = { navController.navigate("github/spotify?per_page=10") }
)
}
composable(Destination.Github.route) {
Github()
}
composable(
Destination.GithubWithArgs.route,
arguments = listOf(
navArgument(Destination.USER_ARG) { type = NavType.StringType },
navArgument(Destination.PER_PAGE_ARG) {
type = NavType.IntType; defaultValue = 10
}
)
) {
Github(
it.arguments?.getString(Destination.USER_ARG) ?: "getsentry",
it.arguments?.getInt(Destination.PER_PAGE_ARG) ?: 10
)
}
}
}
}
Expand Down
Loading