-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add render instrumentation #20
base: main
Are you sure you want to change the base?
Changes from all commits
da90c01
c9f8a6f
e74b747
2e11d22
85b8acb
b4a5da4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,18 +14,22 @@ import androidx.compose.material.icons.Icons | |
import androidx.compose.material.icons.filled.Home | ||
import androidx.compose.material.icons.filled.Language | ||
import androidx.compose.material.icons.filled.Palette | ||
import androidx.compose.material.icons.filled.Straighten | ||
import androidx.compose.material.icons.outlined.Home | ||
import androidx.compose.material.icons.outlined.Language | ||
import androidx.compose.material.icons.outlined.Palette | ||
import androidx.compose.material.icons.outlined.Straighten | ||
import androidx.compose.material3.ExperimentalMaterial3Api | ||
import androidx.compose.material3.Icon | ||
import androidx.compose.material3.NavigationBar | ||
import androidx.compose.material3.NavigationBarItem | ||
import androidx.compose.material3.Scaffold | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.CompositionLocalProvider | ||
import androidx.compose.runtime.MutableState | ||
import androidx.compose.runtime.State | ||
import androidx.compose.runtime.compositionLocalOf | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Alignment | ||
|
@@ -47,8 +51,11 @@ enum class PlaygroundTab( | |
CORE("Core", Icons.Outlined.Home, Icons.Filled.Home), | ||
UI("UI", Icons.Outlined.Palette, Icons.Filled.Palette), | ||
NETWORK("Network", Icons.Outlined.Language, Icons.Filled.Language), | ||
VIEW_INSTRUMENTATION("Render", Icons.Outlined.Straighten, Icons.Filled.Straighten), | ||
} | ||
|
||
val LocalOtelComposition = compositionLocalOf<OpenTelemetryRum?> { null } | ||
|
||
/** | ||
* An activity with various UI elements that cause telemetry to be emitted. | ||
*/ | ||
|
@@ -63,16 +70,18 @@ class MainActivity : ComponentActivity() { | |
setContent { | ||
val currentTab = remember { mutableStateOf(PlaygroundTab.CORE) } | ||
|
||
HoneycombOpenTelemetryAndroidTheme { | ||
Scaffold( | ||
modifier = Modifier.fillMaxSize(), | ||
bottomBar = { NavBar(currentTab) }, | ||
) { innerPadding -> | ||
Playground( | ||
otelRum, | ||
currentTab, | ||
modifier = Modifier.padding(innerPadding), | ||
) | ||
CompositionLocalProvider(LocalOtelComposition provides otelRum) { | ||
HoneycombOpenTelemetryAndroidTheme { | ||
Scaffold( | ||
modifier = Modifier.fillMaxSize(), | ||
bottomBar = { NavBar(currentTab) }, | ||
) { innerPadding -> | ||
Playground( | ||
otelRum, | ||
currentTab, | ||
modifier = Modifier.padding(innerPadding), | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -89,7 +98,10 @@ fun Playground( | |
Column( | ||
verticalArrangement = Arrangement.Center, | ||
horizontalAlignment = Alignment.CenterHorizontally, | ||
modifier = modifier.fillMaxSize().padding(20.dp), | ||
modifier = | ||
modifier | ||
.fillMaxSize() | ||
.padding(20.dp), | ||
) { | ||
Text( | ||
text = | ||
|
@@ -103,12 +115,15 @@ fun Playground( | |
PlaygroundTab.CORE -> { | ||
CorePlayground(otel) | ||
} | ||
PlaygroundTab.UI -> { | ||
UIPlayground() | ||
} | ||
PlaygroundTab.NETWORK -> { | ||
NetworkPlayground() | ||
} | ||
PlaygroundTab.VIEW_INSTRUMENTATION -> { | ||
ViewInstrumentationPlayground() | ||
} | ||
PlaygroundTab.UI -> { | ||
UIPlayground() | ||
} | ||
Comment on lines
+124
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was just to reorder the tabs to match the iOS smoke test app |
||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
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.SideEffect | ||
import androidx.compose.runtime.mutableFloatStateOf | ||
import androidx.compose.runtime.mutableLongStateOf | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import io.honeycomb.opentelemetry.android.example.ui.theme.HoneycombOpenTelemetryAndroidTheme | ||
import java.math.BigDecimal | ||
import java.math.RoundingMode | ||
import java.time.Instant | ||
import kotlin.time.TimeSource.Monotonic.markNow | ||
|
||
private const val TAG = "ViewInstrumentation" | ||
|
||
/** | ||
* Heavily inspired by https://github.com/theapache64/boil/blob/master/files/LogComposition.kt | ||
*/ | ||
@Composable | ||
@Suppress("NOTHING_TO_INLINE") | ||
private inline fun HoneycombInstrumentedComposable( | ||
name: String, | ||
composable: @Composable (() -> Unit), | ||
) { | ||
val tracer = LocalOtelComposition.current!!.openTelemetry.tracerProvider.tracerBuilder("ViewInstrumentationPlayground").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.inWholeMicroseconds / 1_000_000.toDouble()) | ||
|
||
SideEffect { | ||
bodySpan.end(endTime) | ||
val renderDuration = start.elapsedNow() | ||
span.setAttribute("view.totalDuration", renderDuration.inWholeMicroseconds / 1_000_000.toDouble()) | ||
span.end() | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun NestedExpensiveView(delayMs: Long) { | ||
Row { | ||
HoneycombInstrumentedComposable("nested expensive text") { | ||
Text(text = timeConsumingCalculation(delayMs)) | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun DelayedSlider( | ||
delay: Long, | ||
onValueChange: (Long) -> Unit, | ||
) { | ||
val (sliderDelay, setSliderDelay) = remember { mutableFloatStateOf(delay.toFloat()) } | ||
Slider( | ||
value = sliderDelay, | ||
onValueChange = setSliderDelay, | ||
onValueChangeFinished = { onValueChange(sliderDelay.toLong()) }, | ||
valueRange = 0f..4000f, | ||
steps = 7, | ||
) | ||
} | ||
|
||
@Composable | ||
private fun ExpensiveView() { | ||
val (delay, setDelay) = remember { mutableLongStateOf(1000L) } | ||
|
||
HoneycombInstrumentedComposable("main view") { | ||
Column( | ||
verticalArrangement = Arrangement.Center, | ||
horizontalAlignment = Alignment.CenterHorizontally, | ||
modifier = Modifier.fillMaxWidth(), | ||
) { | ||
DelayedSlider(delay = delay, 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(delayMs = 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) | ||
} | ||
if (enabled) { | ||
ExpensiveView() | ||
} | ||
} | ||
} | ||
Comment on lines
+130
to
+151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This structure mirrors the same playground view in iOS: https://github.com/honeycombio/honeycomb-opentelemetry-swift/pull/20/files#diff-3ed6499a52b381eba3646ace172cde5bfa2464d33360b412f51b80b35d120c2bR85-R100 |
||
|
||
private fun timeConsumingCalculation(delayMs: Long): String { | ||
Log.d(TAG, "starting time consuming calculation") | ||
Thread.sleep(delayMs) | ||
return "slow text: ${BigDecimal.valueOf(delayMs / 1000.toDouble()).setScale(2, RoundingMode.HALF_UP)} seconds" | ||
} | ||
|
||
@Preview(showBackground = true) | ||
@Composable | ||
fun ViewInstrumentationPlaygroundPreview() { | ||
HoneycombOpenTelemetryAndroidTheme { | ||
ViewInstrumentationPlayground() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw that the
CorePlayground
accepts theOpenTelemetryRum
object as a parameter but I didn't want to pass it through the many layers of nested Composables in theViewInstrumentationPlayground
, and I thought it was easier forHoneycombInstrumentedComposable
to read from a context-like thing instead of accepting anOpenTelemetryRum
object.I'm open to push this back to a parameter if we think that's a better idea, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this makes a lot of sense, and fits the criteria for when to use it as described on https://developer.android.com/develop/ui/compose/compositionlocal#intro
But this would effectively become part of our public API at that point, right? Our developers would have to use this line?
CompositionLocalProvider(LocalOtelComposition provides otelRum) {
If so, we should think carefully about how to name
LocalOtelComposition
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's true. IntelliJ reports that the convention here is that the key starts with the
Local
prefix:The CompositionLocalProvider pattern reminds me of React Context. In that ecosystem, it's common for libraries to export their own Providers and consumers use that as an entry point. ex:
and