Skip to content

Commit bc2a7f3

Browse files
authored
chore(compose): Update SentryTraced to emit a span only for initial composition (#6051)
Commit updates SentryTraced to: - emit a span only for initial composition, rather than for every composition of the wrapped composable; and - add an origin to ui.render spans, just like we already do for composition spans. It also cleans up use of ImmutableHolder and renames it to MutableRef.
1 parent cdb89fd commit bc2a7f3

2 files changed

Lines changed: 64 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
### Improvements
1111

12+
- 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))
1213
- Move ANR profiling out of experimental ([#6042](https://github.com/getsentry/sentry-java/pull/6042))
1314

1415
### Fixes

sentry-compose/src/androidMain/kotlin/io/sentry/compose/SentryComposeTracing.kt

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package io.sentry.compose
33
import androidx.compose.foundation.layout.Box
44
import androidx.compose.foundation.layout.BoxScope
55
import androidx.compose.runtime.Composable
6-
import androidx.compose.runtime.Immutable
76
import androidx.compose.runtime.SideEffect
87
import androidx.compose.runtime.compositionLocalOf
98
import androidx.compose.runtime.remember
@@ -25,45 +24,67 @@ private const val OP_RENDER = "ui.render"
2524
private const val OP_TRACE_ORIGIN = "auto.ui.jetpack_compose"
2625

2726
private 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

4342
private 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

Comments
 (0)