Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@

### Improvements

- Emit a single `ui.compose` span per `SentryTraced` on initial composition instead of one on every recomposition, and set the origin on `ui.render` spans ([#6051](https://github.com/getsentry/sentry-java/pull/6051))
- Move ANR profiling out of experimental ([#6042](https://github.com/getsentry/sentry-java/pull/6042))

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
Expand All @@ -25,45 +24,67 @@ private const val OP_RENDER = "ui.render"
private const val OP_TRACE_ORIGIN = "auto.ui.jetpack_compose"

private val localSentryCompositionParentSpan = compositionLocalOf {
ImmutableHolder(
getRootSpan()
?.startChild(
OP_PARENT_COMPOSITION,
"Jetpack Compose Initial Composition",
SpanOptions().apply {
isTrimStart = true
isTrimEnd = true
isIdle = true
},
)
?.apply { spanContext.origin = OP_TRACE_ORIGIN }
)
getRootSpan()
// Create a single parent span to own composition spans emitted by all SentryTraced composables
// during the root's lifetime.
?.startChild(
OP_PARENT_COMPOSITION,
"Jetpack Compose Initial Composition",
SpanOptions().apply {
isTrimStart = true
isTrimEnd = true
isIdle = true
},
)
?.apply { spanContext.origin = OP_TRACE_ORIGIN }
}

private val localSentryRenderingParentSpan = compositionLocalOf {
ImmutableHolder(
getRootSpan()
?.startChild(
OP_PARENT_RENDER,
"Jetpack Compose Initial Render",
SpanOptions().apply {
isTrimStart = true
isTrimEnd = true
isIdle = true
},
)
?.apply { spanContext.origin = OP_TRACE_ORIGIN }
)
getRootSpan()
// Create a single parent span to own render spans emitted by all SentryTraced composables
// during the root's lifetime.
?.startChild(
OP_PARENT_RENDER,
"Jetpack Compose Initial Render",
SpanOptions().apply {
isTrimStart = true
isTrimEnd = true
isIdle = true
},
)
?.apply { spanContext.origin = OP_TRACE_ORIGIN }
}

@Immutable internal class ImmutableHolder<T>(var item: T)
Comment thread
0xadam-brown marked this conversation as resolved.
/**
* A substitute for Compose's `MutableState` that doesn't register itself with the snapshot system,
* so mutating [value] never triggers recomposition.
*/
private class MutableRef<T>(var value: T)

/**
* Creates spans for tracking the time required to compose the wrapped [content], and a span for its
* initial draw.
* Creates a single span for tracking the time required to compose the wrapped [content], and a span
* for its initial draw.
*
* Spans are approximate and include work performed by any composables [content] invokes. Abandoned
* recompositions are ignored.
*
* Spans live under a set of parents shared by all `SentryTraced` composables. Every `SentryTraced`
* contributes at most one `ui.compose` child and one `ui.render` child per parent lifetime:
* ```
* Root span
* │
* ├─ ui.compose.composition "Jetpack Compose Initial Composition"
* │ ├─ ui.compose "product_info"
* │ └─ ui.compose "add_to_cart_button"
* │
* └─ ui.compose.rendering "Jetpack Compose Initial Render"
* ├─ ui.render "product_info"
* └─ ui.render "add_to_cart_button"
* ```
*
* Here `ui.compose.composition` and `ui.compose.rendering` are the shared parents. A `SentryTraced`
* generates the "product_info" spans, and a separate `SentryTraced` generates the
* "add_to_cart_button" spans.
*/
@ExperimentalComposeUiApi
@Composable
Expand All @@ -73,27 +94,30 @@ public fun SentryTraced(
enableUserInteractionTracing: Boolean = true,
content: @Composable BoxScope.() -> Unit,
) {
val alreadyRendered = remember { ImmutableHolder(false) }
val baseModifier = if (enableUserInteractionTracing) modifier.sentryTag(tag) else modifier

val parentCompositionSpan = localSentryCompositionParentSpan.current.item
val parentRenderingSpan = localSentryRenderingParentSpan.current.item
val parentCompositionSpan = localSentryCompositionParentSpan.current
val parentRenderingSpan = localSentryRenderingParentSpan.current

val alreadyComposed = remember(parentCompositionSpan) { MutableRef(false) }
val alreadyRendered = remember(parentRenderingSpan) { MutableRef(false) }
Comment thread
0xadam-brown marked this conversation as resolved.
val dateProvider = Sentry.getCurrentScopes().options.dateProvider

// Only record spans if we have a parent for them.
val compositionStart = parentCompositionSpan?.let { dateProvider.now() }
val compositionStart =
if (!alreadyComposed.value) parentCompositionSpan?.let { dateProvider.now() } else null

Box(
modifier =
baseModifier.drawWithContent {
if (alreadyRendered.item || parentRenderingSpan == null) {
if (alreadyRendered.value || parentRenderingSpan == null) {
drawContent()
} else {
val renderStart = dateProvider.now()
drawContent()
val renderEnd = dateProvider.now()

alreadyRendered.item = true
alreadyRendered.value = true
recordRenderSpan(parentRenderingSpan, tag, renderStart, renderEnd)
}
},
Expand All @@ -112,6 +136,8 @@ public fun SentryTraced(
startTimestamp = compositionStart,
endTimestamp = compositionEnd,
)

alreadyComposed.value = true
}
}
}
Expand Down Expand Up @@ -141,6 +167,7 @@ private fun recordRenderSpan(
endTimestamp: SentryDate,
) {
parentSpan?.startChild(OP_RENDER, tag, startTimestamp)?.apply {
spanContext.origin = OP_TRACE_ORIGIN
finish(null, endTimestamp)
}
}
Loading