@@ -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,68 @@ 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+ // We create a single parent span to be shared among *all* composition spans emitted by all
29+ // SentryTraced composables.
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+ // We create a single parent span to be shared among *all* render spans emitted by all
45+ // SentryTraced composables.
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 span for tracking the time required to compose the wrapped [content], and a span for
66+ * its initial draw.
6467 *
6568 * Spans are approximate and include work performed by any composables [content] invokes. Abandoned
6669 * recompositions are ignored.
70+ *
71+ * Generated spans live under a set of parents shared by all `SentryTraced` composables. Every
72+ * `SentryTraced` contributes one `ui.compose` child and one `ui.render` child:
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. The
86+ * "product_info" spans are generated by a `SentryTraced` that wraps a product info composable, and
87+ * the "add_to_cart_button" spans are generated by a separate `SentryTraced` that wraps an
88+ * add-to-cart button.
6789 */
6890@ExperimentalComposeUiApi
6991@Composable
@@ -73,11 +95,12 @@ public fun SentryTraced(
7395 enableUserInteractionTracing : Boolean = true,
7496 content : @Composable BoxScope .() -> Unit ,
7597) {
76- val alreadyRendered = remember { ImmutableHolder (false ) }
98+ val alreadyComposed = remember { MutableRef (false ) }
99+ val alreadyRendered = remember { MutableRef (false ) }
77100 val baseModifier = if (enableUserInteractionTracing) modifier.sentryTag(tag) else modifier
78101
79- val parentCompositionSpan = localSentryCompositionParentSpan.current.item
80- val parentRenderingSpan = localSentryRenderingParentSpan.current.item
102+ val parentCompositionSpan = localSentryCompositionParentSpan.current
103+ val parentRenderingSpan = localSentryRenderingParentSpan.current
81104 val dateProvider = Sentry .getCurrentScopes().options.dateProvider
82105
83106 // Only record spans if we have a parent for them.
@@ -86,14 +109,14 @@ public fun SentryTraced(
86109 Box (
87110 modifier =
88111 baseModifier.drawWithContent {
89- if (alreadyRendered.item || parentRenderingSpan == null ) {
112+ if (alreadyRendered.value || parentRenderingSpan == null ) {
90113 drawContent()
91114 } else {
92115 val renderStart = dateProvider.now()
93116 drawContent()
94117 val renderEnd = dateProvider.now()
95118
96- alreadyRendered.item = true
119+ alreadyRendered.value = true
97120 recordRenderSpan(parentRenderingSpan, tag, renderStart, renderEnd)
98121 }
99122 },
@@ -102,7 +125,7 @@ public fun SentryTraced(
102125 content()
103126 }
104127
105- if (compositionStart != null ) {
128+ if (! alreadyComposed.value && compositionStart != null ) {
106129 val compositionEnd = dateProvider.now()
107130
108131 SideEffect {
@@ -112,6 +135,8 @@ public fun SentryTraced(
112135 startTimestamp = compositionStart,
113136 endTimestamp = compositionEnd,
114137 )
138+
139+ alreadyComposed.value = true
115140 }
116141 }
117142}
@@ -141,6 +166,7 @@ private fun recordRenderSpan(
141166 endTimestamp : SentryDate ,
142167) {
143168 parentSpan?.startChild(OP_RENDER , tag, startTimestamp)?.apply {
169+ spanContext.origin = OP_TRACE_ORIGIN
144170 finish(null , endTimestamp)
145171 }
146172}
0 commit comments