generated from honeycombio/.github
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add render instrumentation (#20)
## Which problem is this PR solving? Adds manual instrumentation for rendering times of Composables in Jetpack Compose ## Short description of the changes heavily inspired by honeycombio/honeycomb-opentelemetry-swift#20 Multiple re-renders do get nested under a single "Created" Span, but otherwise the spans look identical to the iOS spans: ![image](https://github.com/user-attachments/assets/50460236-9649-4264-b457-b4be9d1eca62) ## How to verify that this has the expected result - [x] traces appear in honeycomb with the correct values (see screenshot above) - [x] smoke tests
- Loading branch information
1 parent
3b21688
commit 86307eb
Showing
8 changed files
with
307 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
core/src/main/java/io/honeycomb/opentelemetry/android/HoneycombInstrumentedComposable.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package io.honeycomb.opentelemetry.android | ||
|
||
import android.util.Log | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.SideEffect | ||
import androidx.compose.runtime.compositionLocalOf | ||
import io.opentelemetry.android.OpenTelemetryRum | ||
import java.time.Instant | ||
import kotlin.time.DurationUnit | ||
import kotlin.time.TimeSource.Monotonic.markNow | ||
|
||
private const val TAG = "HoneycombInstrumentedCo" // max 23 characters :sob: | ||
|
||
/** | ||
* Heavily inspired by https://github.com/theapache64/boil/blob/master/files/LogComposition.kt | ||
*/ | ||
@Composable | ||
fun HoneycombInstrumentedComposable( | ||
name: String, | ||
composable: @Composable (() -> Unit), | ||
) { | ||
if (LocalOpenTelemetryRum.current == null) { | ||
Log.w(TAG, "No LocalOpenTelemetryRum provided!") | ||
|
||
composable() | ||
return | ||
} | ||
|
||
val otelRum = LocalOpenTelemetryRum.current!!.openTelemetry | ||
val tracer = otelRum.tracerProvider.tracerBuilder("io.honeycomb.render-instrumentation").build() | ||
val span = | ||
tracer | ||
.spanBuilder("View Render") | ||
.setAttribute("view.name", name) | ||
.startSpan() | ||
|
||
span.makeCurrent().use { | ||
val bodySpan = | ||
tracer | ||
.spanBuilder("View Body") | ||
.setAttribute("view.name", name) | ||
.startSpan() | ||
|
||
bodySpan.makeCurrent().use { | ||
val start = markNow() | ||
composable() | ||
val endTime = Instant.now() | ||
|
||
val bodyDuration = start.elapsedNow() | ||
// bodyDuration is in seconds | ||
// calling duration.inWholeSeconds would lose precision | ||
span.setAttribute("view.renderDuration", bodyDuration.toDouble(DurationUnit.SECONDS)) | ||
|
||
SideEffect { | ||
bodySpan.end(endTime) | ||
val renderDuration = start.elapsedNow() | ||
span.setAttribute("view.totalDuration", renderDuration.toDouble(DurationUnit.SECONDS)) | ||
span.end() | ||
} | ||
} | ||
} | ||
} | ||
|
||
val LocalOpenTelemetryRum = compositionLocalOf<OpenTelemetryRum?> { null } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
...src/main/java/io/honeycomb/opentelemetry/android/example/ViewInstrumentationPlayground.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package io.honeycomb.opentelemetry.android.example | ||
|
||
import android.util.Log | ||
import androidx.compose.foundation.layout.Arrangement | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.Row | ||
import androidx.compose.foundation.layout.fillMaxWidth | ||
import androidx.compose.material3.Slider | ||
import androidx.compose.material3.Switch | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.mutableFloatStateOf | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.testTag | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import io.honeycomb.opentelemetry.android.HoneycombInstrumentedComposable | ||
import io.honeycomb.opentelemetry.android.example.ui.theme.HoneycombOpenTelemetryAndroidTheme | ||
import kotlin.time.Duration | ||
import kotlin.time.Duration.Companion.milliseconds | ||
import kotlin.time.DurationUnit | ||
|
||
private const val TAG = "ViewInstrumentation" | ||
|
||
@Composable | ||
private fun NestedExpensiveView(delay: Duration) { | ||
Row { | ||
HoneycombInstrumentedComposable("nested expensive text") { | ||
Text(text = timeConsumingCalculation(delay)) | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun DelayedSlider( | ||
delay: Long, | ||
onValueChange: (Duration) -> Unit, | ||
) { | ||
val (sliderDelay, setSliderDelay) = remember { mutableFloatStateOf(delay.toFloat()) } | ||
Slider( | ||
value = sliderDelay, | ||
onValueChange = setSliderDelay, | ||
onValueChangeFinished = { onValueChange(sliderDelay.toLong().milliseconds) }, | ||
valueRange = 0f..4000f, | ||
steps = 7, | ||
) | ||
} | ||
|
||
@Composable | ||
private fun ExpensiveView() { | ||
val (delay, setDelay) = remember { mutableStateOf(1000L.milliseconds) } | ||
|
||
HoneycombInstrumentedComposable("main view") { | ||
Column( | ||
verticalArrangement = Arrangement.Center, | ||
horizontalAlignment = Alignment.CenterHorizontally, | ||
modifier = Modifier.fillMaxWidth(), | ||
) { | ||
DelayedSlider(delay = delay.toLong(DurationUnit.MILLISECONDS), onValueChange = setDelay) | ||
|
||
HoneycombInstrumentedComposable("expensive text 1") { | ||
Text(text = timeConsumingCalculation(delay)) | ||
} | ||
|
||
HoneycombInstrumentedComposable("expensive text 2") { | ||
Text(text = timeConsumingCalculation(delay)) | ||
} | ||
|
||
HoneycombInstrumentedComposable("expensive text 3") { | ||
Text(text = timeConsumingCalculation(delay)) | ||
} | ||
|
||
HoneycombInstrumentedComposable("nested expensive view") { | ||
NestedExpensiveView(delay = delay) | ||
} | ||
|
||
HoneycombInstrumentedComposable("expensive text 4") { | ||
Text(text = timeConsumingCalculation(delay)) | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
internal fun ViewInstrumentationPlayground() { | ||
val (enabled, setEnabled) = remember { mutableStateOf(false) } | ||
|
||
Column( | ||
verticalArrangement = Arrangement.Center, | ||
horizontalAlignment = Alignment.CenterHorizontally, | ||
modifier = Modifier.fillMaxWidth(), | ||
) { | ||
Row( | ||
verticalAlignment = Alignment.CenterVertically, | ||
horizontalArrangement = Arrangement.SpaceBetween, | ||
modifier = Modifier.fillMaxWidth(), | ||
) { | ||
Text(text = "enable slow render") | ||
Switch( | ||
checked = enabled, | ||
onCheckedChange = setEnabled, | ||
modifier = Modifier.testTag("slow_render_switch"), | ||
) | ||
} | ||
if (enabled) { | ||
ExpensiveView() | ||
} | ||
} | ||
} | ||
|
||
private fun timeConsumingCalculation(delay: Duration): String { | ||
Log.d(TAG, "starting time consuming calculation") | ||
Thread.sleep(delay.toLong(DurationUnit.MILLISECONDS)) | ||
return "slow text: ${delay.toDouble(DurationUnit.SECONDS)} seconds" | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
fun ViewInstrumentationPlaygroundPreview() { | ||
HoneycombOpenTelemetryAndroidTheme { | ||
ViewInstrumentationPlayground() | ||
} | ||
} |
Oops, something went wrong.