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

Conversation

MustafaHaddara
Copy link
Contributor

@MustafaHaddara MustafaHaddara commented Dec 11, 2024

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

How to verify that this has the expected result

  • traces appear in honeycomb with the correct values (see screenshot above)
  • smoke tests

@MustafaHaddara MustafaHaddara force-pushed the mh/render-instrumentation branch from 3c4751d to 2e11d22 Compare December 12, 2024 17:09
}

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
  }
}

Comment on lines +124 to +126
PlaygroundTab.UI -> {
UIPlayground()
}
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

Comment on lines 23 to 42
@Composable
private fun HoneycombInstrumentedComposable(
name: String,
composable: @Composable (() -> Unit),
) {
val tracer = LocalOtelComposition.current!!.openTelemetry.tracerProvider.tracerBuilder("ViewInstrumentationPlayground").build()
val span = tracer.spanBuilder("Render").setAttribute("view.name", name).startSpan()
span.makeCurrent()

val duration =
measureTime {
composable()
}

// renderDuration is in seconds
// calling duration.inWholeSeconds would lose precision
span.setAttribute("view.renderDuration", duration.inWholeMicroseconds / 1_000_000.toDouble())

span.end()
}
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 lives here for now while I'm prototyping this approach/interface

Once we are happy with the general API, I can move it in the core library.

Comment on lines +95 to +116
@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()
}
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants