Skip to content
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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }
Copy link
Contributor Author

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 the OpenTelemetryRum object as a parameter but I didn't want to pass it through the many layers of nested Composables in the ViewInstrumentationPlayground, and I thought it was easier for HoneycombInstrumentedComposable to read from a context-like thing instead of accepting an OpenTelemetryRum object.

I'm open to push this back to a parameter if we think that's a better idea, though.

Copy link
Collaborator

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.

Copy link
Contributor Author

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:

image

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:

// MainActivity.kt

setContent {
  HoneycombLocalProvider( /* honeycomb config */ ) {
    // body content
  }
}

and

// HoneycombLocalProvider.kt

val LocalOpenTelemetryRum = compositionLocalOf<OpenTelemetryRum?> { null }

@Composable
fun HoneycombLocalProvder( /* config */ ) {
  val options = HoneycombOptions.builder(this)
    // config
    .build()

  otelRum = Honeycomb.configure(this, options)

  CompositionLocalProvider(LocalOpenTelemetryRum provides otelRum) {
    // child content
  }
}


/**
* An activity with various UI elements that cause telemetry to be emitted.
*/
Expand All @@ -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),
)
}
}
}
}
Expand All @@ -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 =
Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

}
}
}
Expand Down
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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()
}
}
Loading