@@ -3,7 +3,6 @@ package io.sentry.compose
33import androidx.compose.foundation.layout.Box
44import androidx.compose.foundation.layout.BoxScope
55import androidx.compose.runtime.Composable
6- import androidx.compose.runtime.Immutable
76import androidx.compose.runtime.SideEffect
87import androidx.compose.runtime.compositionLocalOf
98import androidx.compose.runtime.remember
@@ -25,45 +24,67 @@ private const val OP_RENDER = "ui.render"
2524private const val OP_TRACE_ORIGIN = " auto.ui.jetpack_compose"
2625
2726private val localSentryCompositionParentSpan = compositionLocalOf {
28- ImmutableHolder (
29- getRootSpan()
30- ?.startChild(
31- OP_PARENT_COMPOSITION ,
32- " Jetpack Compose Initial Composition " ,
33- SpanOptions (). apply {
34- isTrimStart = true
35- isTrimEnd = true
36- isIdle = true
37- },
38- )
39- ?. apply { spanContext.origin = OP_TRACE_ORIGIN }
40- )
27+ getRootSpan()
28+ // Create a single parent span to own composition spans emitted by all SentryTraced composables
29+ // during the root's lifetime.
30+ ?.startChild(
31+ OP_PARENT_COMPOSITION ,
32+ " Jetpack Compose Initial Composition " ,
33+ SpanOptions (). apply {
34+ isTrimStart = true
35+ isTrimEnd = true
36+ isIdle = true
37+ },
38+ )
39+ ?. apply { spanContext.origin = OP_TRACE_ORIGIN }
4140}
4241
4342private val localSentryRenderingParentSpan = compositionLocalOf {
44- ImmutableHolder (
45- getRootSpan()
46- ?.startChild(
47- OP_PARENT_RENDER ,
48- " Jetpack Compose Initial Render " ,
49- SpanOptions (). apply {
50- isTrimStart = true
51- isTrimEnd = true
52- isIdle = true
53- },
54- )
55- ?. apply { spanContext.origin = OP_TRACE_ORIGIN }
56- )
43+ getRootSpan()
44+ // Create a single parent span to own render spans emitted by all SentryTraced composables
45+ // during the root's lifetime.
46+ ?.startChild(
47+ OP_PARENT_RENDER ,
48+ " Jetpack Compose Initial Render " ,
49+ SpanOptions (). apply {
50+ isTrimStart = true
51+ isTrimEnd = true
52+ isIdle = true
53+ },
54+ )
55+ ?. apply { spanContext.origin = OP_TRACE_ORIGIN }
5756}
5857
59- @Immutable internal class ImmutableHolder <T >(var item : T )
58+ /* *
59+ * A substitute for Compose's `MutableState` that doesn't register itself with the snapshot system,
60+ * so mutating [value] never triggers recomposition.
61+ */
62+ private class MutableRef <T >(var value : T )
6063
6164/* *
62- * Creates spans for tracking the time required to compose the wrapped [content], and a span for its
63- * initial draw.
65+ * Creates a single span for tracking the time required to compose the wrapped [content], and a span
66+ * for its initial draw.
6467 *
6568 * Spans are approximate and include work performed by any composables [content] invokes. Abandoned
6669 * recompositions are ignored.
70+ *
71+ * Spans live under a set of parents shared by all `SentryTraced` composables. Every `SentryTraced`
72+ * contributes at most one `ui.compose` child and one `ui.render` child per parent lifetime:
73+ * ```
74+ * Root span
75+ * │
76+ * ├─ ui.compose.composition "Jetpack Compose Initial Composition"
77+ * │ ├─ ui.compose "product_info"
78+ * │ └─ ui.compose "add_to_cart_button"
79+ * │
80+ * └─ ui.compose.rendering "Jetpack Compose Initial Render"
81+ * ├─ ui.render "product_info"
82+ * └─ ui.render "add_to_cart_button"
83+ * ```
84+ *
85+ * Here `ui.compose.composition` and `ui.compose.rendering` are the shared parents. A `SentryTraced`
86+ * generates the "product_info" spans, and a separate `SentryTraced` generates the
87+ * "add_to_cart_button" spans.
6788 */
6889@ExperimentalComposeUiApi
6990@Composable
@@ -73,27 +94,30 @@ public fun SentryTraced(
7394 enableUserInteractionTracing : Boolean = true,
7495 content : @Composable BoxScope .() -> Unit ,
7596) {
76- val alreadyRendered = remember { ImmutableHolder (false ) }
7797 val baseModifier = if (enableUserInteractionTracing) modifier.sentryTag(tag) else modifier
7898
79- val parentCompositionSpan = localSentryCompositionParentSpan.current.item
80- val parentRenderingSpan = localSentryRenderingParentSpan.current.item
99+ val parentCompositionSpan = localSentryCompositionParentSpan.current
100+ val parentRenderingSpan = localSentryRenderingParentSpan.current
101+
102+ val alreadyComposed = remember(parentCompositionSpan) { MutableRef (false ) }
103+ val alreadyRendered = remember(parentRenderingSpan) { MutableRef (false ) }
81104 val dateProvider = Sentry .getCurrentScopes().options.dateProvider
82105
83106 // Only record spans if we have a parent for them.
84- val compositionStart = parentCompositionSpan?.let { dateProvider.now() }
107+ val compositionStart =
108+ if (! alreadyComposed.value) parentCompositionSpan?.let { dateProvider.now() } else null
85109
86110 Box (
87111 modifier =
88112 baseModifier.drawWithContent {
89- if (alreadyRendered.item || parentRenderingSpan == null ) {
113+ if (alreadyRendered.value || parentRenderingSpan == null ) {
90114 drawContent()
91115 } else {
92116 val renderStart = dateProvider.now()
93117 drawContent()
94118 val renderEnd = dateProvider.now()
95119
96- alreadyRendered.item = true
120+ alreadyRendered.value = true
97121 recordRenderSpan(parentRenderingSpan, tag, renderStart, renderEnd)
98122 }
99123 },
@@ -112,6 +136,8 @@ public fun SentryTraced(
112136 startTimestamp = compositionStart,
113137 endTimestamp = compositionEnd,
114138 )
139+
140+ alreadyComposed.value = true
115141 }
116142 }
117143}
@@ -141,6 +167,7 @@ private fun recordRenderSpan(
141167 endTimestamp : SentryDate ,
142168) {
143169 parentSpan?.startChild(OP_RENDER , tag, startTimestamp)?.apply {
170+ spanContext.origin = OP_TRACE_ORIGIN
144171 finish(null , endTimestamp)
145172 }
146173}
0 commit comments