Skip to content
Merged
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 @@ -180,9 +180,9 @@ object OpenTelemetryRumInitializer {
}

if (slowRenderingDetectionPollInterval != null) {
getInstrumentation<SlowRenderingInstrumentation>()?.setSlowRenderingDetectionPollInterval(
slowRenderingDetectionPollInterval,
)
val instrumentation = getInstrumentation<SlowRenderingInstrumentation>()
instrumentation?.setSlowRenderingDetectionPollInterval(slowRenderingDetectionPollInterval)
instrumentation?.enableDeprecatedZeroDurationSpan()
}
}

Expand Down
2 changes: 2 additions & 0 deletions instrumentation/slowrendering/api/slowrendering.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
public final class io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation : io/opentelemetry/android/instrumentation/AndroidInstrumentation {
public fun <init> ()V
public final fun enableDeprecatedZeroDurationSpan ()Lio/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation;
public final fun enableVerboseDebugLogging ()Lio/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation;
public fun getName ()Ljava/lang/String;
public fun install (Lio/opentelemetry/android/instrumentation/InstallationContext;)V
public final fun setSlowRenderingDetectionPollInterval (Ljava/time/Duration;)Lio/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation;
Expand Down
5 changes: 5 additions & 0 deletions instrumentation/slowrendering/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ android {
defaultConfig {
consumerProguardFiles("consumer-rules.pro")
}

testOptions {
unitTests.isReturnDefaultValues = true
}
}

dependencies {
Expand All @@ -24,5 +28,6 @@ dependencies {
implementation(libs.opentelemetry.semconv)
implementation(libs.opentelemetry.sdk)
implementation(libs.opentelemetry.instrumentation.api)
implementation(libs.opentelemetry.sdk.extension.incubator)
testImplementation(libs.robolectric)
}
11 changes: 11 additions & 0 deletions instrumentation/slowrendering/config/detekt/baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>EmptyFunctionBlock:JankReporterTest.kt$JankReporterTest.&lt;no name provided&gt;${ }</ID>
<ID>ForbiddenComment:EventJankReporter.kt$// TODO: Replace with semconv constants</ID>
<ID>MagicNumber:EventJankReporter.kt$EventJankReporter$1000.0</ID>
<ID>MagicNumber:SlowRenderingInstrumentation.kt$SlowRenderingInstrumentation$1000.0</ID>
<ID>TooGenericExceptionCaught:SlowRenderListener.kt$SlowRenderListener$e: Exception</ID>
</CurrentIssues>
</SmellBaseline>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.instrumentation.slowrendering

import android.util.Log
import io.opentelemetry.android.common.RumConstants
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder
import io.opentelemetry.api.logs.Logger

// TODO: Replace with semconv constants
internal val FRAME_COUNT: AttributeKey<Long> = AttributeKey.longKey("app.jank.frame_count")
internal val PERIOD: AttributeKey<Double> = AttributeKey.doubleKey("app.jank.period")
internal val THRESHOLD: AttributeKey<Double> = AttributeKey.doubleKey("app.jank.threshold")

internal class EventJankReporter(
private val eventLogger: Logger,
private val threshold: Double,
private val debugVerbose: Boolean = false,
) : JankReporter {
override fun reportSlow(
durationToCountHistogram: Map<Int, Int>,
periodSeconds: Double,
activityName: String,
) {
var frameCount: Long = 0
for (entry in durationToCountHistogram) {
val durationMillis = entry.key
if ((durationMillis / 1000.0) > threshold) {
val count = entry.value
if (debugVerbose) {
Log.d(
RumConstants.OTEL_RUM_LOG_TAG,
"* Slow render detected: $durationMillis ms. $count times",
)
}
frameCount += count
}
}

if (frameCount > 0) {
val eventBuilder = eventLogger.logRecordBuilder() as ExtendedLogRecordBuilder
val attributes =
Attributes
.builder()
.put(FRAME_COUNT, frameCount)
.put(PERIOD, periodSeconds)
.put(THRESHOLD, threshold)
.build()
eventBuilder
.setEventName("app.jank")
.setAllAttributes(attributes)
.emit()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.instrumentation.slowrendering

/**
* Responsible for sending telemetry. This is a temporary class that we can remove
* after the jank semconv becomes more stable.
*/
internal fun interface JankReporter {
fun reportSlow(
durationToCountHistogram: Map<Int, Int>,
periodSeconds: Double,
activityName: String,
)

/**
* Creates a combined JankReporter that will first report slow for this
* instance and then delegate to another JankReporter instance.
*/
fun combine(jankReporter: JankReporter): JankReporter {
require(jankReporter != this) { "cannot combine with self" }
val exec = this::reportSlow
return object : JankReporter {
override fun reportSlow(
durationToCountHistogram: Map<Int, Int>,
periodSeconds: Double,
activityName: String,
) {
exec(durationToCountHistogram, periodSeconds, activityName)
jankReporter.reportSlow(durationToCountHistogram, periodSeconds, activityName)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.instrumentation.slowrendering

import android.app.Activity
import android.os.Build
import android.view.FrameMetrics
import android.view.Window
import android.view.Window.OnFrameMetricsAvailableListener
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import java.util.concurrent.TimeUnit

private val NANOS_PER_MS = TimeUnit.MILLISECONDS.toNanos(1).toInt()

// rounding value adds half a millisecond, for rounding to nearest ms
private val NANOS_ROUNDING_VALUE: Int = NANOS_PER_MS / 2

@RequiresApi(api = Build.VERSION_CODES.N)
internal class PerActivityListener(
private val activity: Activity,
) : OnFrameMetricsAvailableListener {
private val lock = Any()

@GuardedBy("lock")
private var drawDurationHistogram: MutableMap<Int, Int> = HashMap<Int, Int>()

fun getActivityName(): String = activity.componentName.flattenToShortString()

override fun onFrameMetricsAvailable(
window: Window?,
frameMetrics: FrameMetrics,
dropCountSinceLastInvocation: Int,
) {
val firstDrawFrame = frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME)
if (firstDrawFrame == 1L) {
return
}

val drawDurationsNs = frameMetrics.getMetric(FrameMetrics.DRAW_DURATION)
// ignore values < 0; something must have gone wrong
if (drawDurationsNs >= 0) {
synchronized(lock) {
// calculation copied from FrameMetricsAggregator
val durationMs = ((drawDurationsNs + NANOS_ROUNDING_VALUE) / NANOS_PER_MS).toInt()
val oldValue: Int = drawDurationHistogram.getOrDefault(durationMs, 0)
drawDurationHistogram[durationMs] = (oldValue + 1)
}
}
}

fun resetMetrics(): Map<Int, Int> {
synchronized(lock) {
val metrics = HashMap(drawDurationHistogram)
drawDurationHistogram = HashMap()
return metrics
}
}
}
Loading