From 7869654a83cae10c35239fabca2a825fa46f24c3 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 18 Aug 2025 10:55:47 -0700 Subject: [PATCH 01/18] initial refactor of slow renders, extract PerActivityListener and add JankReporter interface --- .../slowrendering/JankReporter.kt | 9 ++ .../slowrendering/PerActivityListener.kt | 54 +++++++++ .../slowrendering/SlowRenderListener.java | 113 ++---------------- .../SlowRenderingInstrumentation.kt | 13 +- .../slowrendering/SpanBasedJankReporter.kt | 52 ++++++++ .../slowrendering/SlowRenderListenerTest.java | 27 +++-- .../SpanBasedJankReporterTest.kt | 20 ++++ 7 files changed, 165 insertions(+), 123 deletions(-) create mode 100644 instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt create mode 100644 instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt create mode 100644 instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt create mode 100644 instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt new file mode 100644 index 000000000..4b03cec9d --- /dev/null +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt @@ -0,0 +1,9 @@ +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 interface JankReporter { + fun reportSlow(listener: PerActivityListener) +} \ No newline at end of file diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt new file mode 100644 index 000000000..2ab3bef87 --- /dev/null +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt @@ -0,0 +1,54 @@ +package io.opentelemetry.android.instrumentation.slowrendering + +import android.app.Activity +import android.os.Build +import android.util.SparseIntArray +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 = SparseIntArray() + + 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 = drawDurationHistogram.get(durationMs) + drawDurationHistogram.put(durationMs, (oldValue + 1)) + } + } + } + + fun resetMetrics(): SparseIntArray { + synchronized(lock) { + val metrics = drawDurationHistogram + drawDurationHistogram = SparseIntArray() + return metrics + } + } +} diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java index ac0fbe283..f6765f136 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java @@ -5,9 +5,6 @@ package io.opentelemetry.android.instrumentation.slowrendering; -import static android.view.FrameMetrics.DRAW_DURATION; -import static android.view.FrameMetrics.FIRST_DRAW_FRAME; - import android.app.Activity; import android.os.Build; import android.os.Handler; @@ -15,9 +12,7 @@ import android.os.Looper; import android.util.Log; import android.util.SparseIntArray; -import android.view.FrameMetrics; -import android.view.Window; -import androidx.annotation.GuardedBy; + import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import io.opentelemetry.android.common.RumConstants; @@ -39,14 +34,10 @@ class SlowRenderListener implements DefaultingActivityLifecycleCallbacks { static final int SLOW_THRESHOLD_MS = 16; static final int FROZEN_THRESHOLD_MS = 700; - private static final int NANOS_PER_MS = (int) TimeUnit.MILLISECONDS.toNanos(1); - // rounding value adds half a millisecond, for rounding to nearest ms - private static final int NANOS_ROUNDING_VALUE = NANOS_PER_MS / 2; - private static final HandlerThread frameMetricsThread = new HandlerThread("FrameMetricsCollector"); - private final Tracer tracer; + private final JankReporter jankReporter; private final ScheduledExecutorService executorService; private final Handler frameMetricsHandler; private final Duration pollInterval; @@ -54,9 +45,9 @@ class SlowRenderListener implements DefaultingActivityLifecycleCallbacks { private final ConcurrentMap activities = new ConcurrentHashMap<>(); - SlowRenderListener(Tracer tracer, Duration pollInterval) { + SlowRenderListener(JankReporter jankReporter, Duration pollInterval) { this( - tracer, + jankReporter, Executors.newScheduledThreadPool(1), new Handler(startFrameMetricsLoop()), pollInterval); @@ -64,11 +55,11 @@ class SlowRenderListener implements DefaultingActivityLifecycleCallbacks { // Exists for testing SlowRenderListener( - Tracer tracer, + JankReporter jankReporter, ScheduledExecutorService executorService, Handler frameMetricsHandler, Duration pollInterval) { - this.tracer = tracer; + this.jankReporter = jankReporter; this.executorService = executorService; this.frameMetricsHandler = frameMetricsHandler; this.pollInterval = pollInterval; @@ -123,103 +114,15 @@ public void onActivityPaused(@NonNull Activity activity) { PerActivityListener listener = activities.remove(activity); if (listener != null) { activity.getWindow().removeOnFrameMetricsAvailableListener(listener); - reportSlow(listener); - } - } - - @RequiresApi(api = Build.VERSION_CODES.N) - static class PerActivityListener implements Window.OnFrameMetricsAvailableListener { - - private final Activity activity; - private final Object lock = new Object(); - - @GuardedBy("lock") - private SparseIntArray drawDurationHistogram = new SparseIntArray(); - - PerActivityListener(Activity activity) { - this.activity = activity; - } - - @Override - public void onFrameMetricsAvailable( - Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) { - - long firstDrawFrame = frameMetrics.getMetric(FIRST_DRAW_FRAME); - if (firstDrawFrame == 1) { - return; - } - - long drawDurationsNs = frameMetrics.getMetric(DRAW_DURATION); - // ignore values < 0; something must have gone wrong - if (drawDurationsNs >= 0) { - synchronized (lock) { - // calculation copied from FrameMetricsAggregator - int durationMs = - (int) ((drawDurationsNs + NANOS_ROUNDING_VALUE) / NANOS_PER_MS); - int oldValue = drawDurationHistogram.get(durationMs); - drawDurationHistogram.put(durationMs, (oldValue + 1)); - } - } - } - - SparseIntArray resetMetrics() { - synchronized (lock) { - SparseIntArray metrics = drawDurationHistogram; - drawDurationHistogram = new SparseIntArray(); - return metrics; - } - } - - public String getActivityName() { - return activity.getComponentName().flattenToShortString(); + jankReporter.reportSlow(listener); } } private void reportSlowRenders() { try { - activities.forEach((activity, listener) -> reportSlow(listener)); + activities.forEach((activity, listener) -> jankReporter.reportSlow(listener)); } catch (Exception e) { Log.w(RumConstants.OTEL_RUM_LOG_TAG, "Exception while processing frame metrics", e); } } - - private void reportSlow(PerActivityListener listener) { - int slowCount = 0; - int frozenCount = 0; - SparseIntArray durationToCountHistogram = listener.resetMetrics(); - for (int i = 0; i < durationToCountHistogram.size(); i++) { - int duration = durationToCountHistogram.keyAt(i); - int count = durationToCountHistogram.get(duration); - if (duration > FROZEN_THRESHOLD_MS) { - Log.d( - RumConstants.OTEL_RUM_LOG_TAG, - "* FROZEN RENDER DETECTED: " + duration + " ms." + count + " times"); - frozenCount += count; - } else if (duration > SLOW_THRESHOLD_MS) { - Log.d( - RumConstants.OTEL_RUM_LOG_TAG, - "* Slow render detected: " + duration + " ms. " + count + " times"); - slowCount += count; - } - } - - Instant now = Instant.now(); - if (slowCount > 0) { - makeSpan("slowRenders", listener.getActivityName(), slowCount, now); - } - if (frozenCount > 0) { - makeSpan("frozenRenders", listener.getActivityName(), frozenCount, now); - } - } - - private void makeSpan(String spanName, String activityName, int slowCount, Instant now) { - // TODO: Use an event rather than a zero-duration span - Span span = - tracer.spanBuilder(spanName) - .setAttribute("count", slowCount) - .setAttribute("activity.name", activityName) - .setStartTimestamp(now) - .startSpan(); - span.end(now); - } } diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt index 6794186a7..67b51b232 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt @@ -61,14 +61,15 @@ class SlowRenderingInstrumentation : AndroidInstrumentation { return } - detector = - SlowRenderListener( - ctx.openTelemetry.getTracer("io.opentelemetry.slow-rendering"), - slowRenderingDetectionPollInterval, - ) + + // TODO: Let the type of reporter be configurable + val tracer = ctx.openTelemetry.getTracer("io.opentelemetry.slow-rendering") + val jankReporter = SpanBasedJankReporter(tracer) + + detector = SlowRenderListener(jankReporter, slowRenderingDetectionPollInterval) ctx.application.registerActivityLifecycleCallbacks(detector) - detector?.start() + detector!!.start() } override fun uninstall(ctx: InstallationContext) { diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt new file mode 100644 index 000000000..a417214df --- /dev/null +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt @@ -0,0 +1,52 @@ +package io.opentelemetry.android.instrumentation.slowrendering + +import android.util.Log +import io.opentelemetry.android.common.RumConstants +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.Tracer +import java.time.Instant + +internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter { + + override fun reportSlow(listener: PerActivityListener) { + var slowCount = 0 + var frozenCount = 0 + val durationToCountHistogram = listener.resetMetrics() + for (i in 0 until durationToCountHistogram.size()) { + val duration = durationToCountHistogram.keyAt(i) + val count = durationToCountHistogram.get(duration) + if (duration > SlowRenderListener.FROZEN_THRESHOLD_MS) { + Log.d( + RumConstants.OTEL_RUM_LOG_TAG, + "* FROZEN RENDER DETECTED: $duration ms.$count times" + ) + frozenCount += count + } else if (duration > SlowRenderListener.SLOW_THRESHOLD_MS) { + Log.d( + RumConstants.OTEL_RUM_LOG_TAG, + "* Slow render detected: $duration ms. $count times" + ) + slowCount += count + } + } + + val now = Instant.now(); + if (slowCount > 0) { + makeSpan("slowRenders", listener.getActivityName(), slowCount, now); + } + if (frozenCount > 0) { + makeSpan("frozenRenders", listener.getActivityName(), frozenCount, now); + } + } + + private fun makeSpan(spanName: String, activityName: String, slowCount: Int, now: Instant) { + // TODO: Use an event rather than a zero-duration span + val span: Span = + tracer.spanBuilder(spanName) + .setAttribute("count", slowCount.toLong()) + .setAttribute("activity.name", activityName) + .setStartTimestamp(now) + .startSpan() + span.end(now) + } +} \ No newline at end of file diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java index ec1605fd1..4f0a136b7 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java @@ -64,14 +64,13 @@ public class SlowRenderListenerTest { Activity activity; @Mock FrameMetrics frameMetrics; - Tracer tracer; + @Mock JankReporter jankReporter; ScheduledExecutorService executorService; - @Captor ArgumentCaptor activityListenerCaptor; + @Captor ArgumentCaptor activityListenerCaptor; @Before public void setup() { - tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); executorService = Executors.newSingleThreadScheduledExecutor(); ComponentName componentName = new ComponentName("io.otel", "Komponent"); when(activity.getComponentName()).thenReturn(componentName); @@ -80,7 +79,7 @@ public void setup() { @Test public void add() { SlowRenderListener testInstance = - new SlowRenderListener(tracer, executorService, frameMetricsHandler, Duration.ZERO); + new SlowRenderListener(jankReporter, executorService, frameMetricsHandler, Duration.ZERO); testInstance.onActivityResumed(activity); @@ -93,7 +92,7 @@ public void add() { @Test public void removeBeforeAddOk() { SlowRenderListener testInstance = - new SlowRenderListener(tracer, executorService, frameMetricsHandler, Duration.ZERO); + new SlowRenderListener(jankReporter, executorService, frameMetricsHandler, Duration.ZERO); testInstance.onActivityPaused(activity); @@ -104,7 +103,7 @@ public void removeBeforeAddOk() { @Test public void addAndRemove() { SlowRenderListener testInstance = - new SlowRenderListener(tracer, executorService, frameMetricsHandler, Duration.ZERO); + new SlowRenderListener(jankReporter, executorService, frameMetricsHandler, Duration.ZERO); testInstance.onActivityResumed(activity); testInstance.onActivityPaused(activity); @@ -120,14 +119,16 @@ public void addAndRemove() { @Test public void removeWithMetrics() { + Tracer tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); + jankReporter = new SpanBasedJankReporter(tracer); SlowRenderListener testInstance = - new SlowRenderListener(tracer, executorService, frameMetricsHandler, Duration.ZERO); + new SlowRenderListener(jankReporter, executorService, frameMetricsHandler, Duration.ZERO); testInstance.onActivityResumed(activity); verify(activity.getWindow()) .addOnFrameMetricsAvailableListener(activityListenerCaptor.capture(), any()); - SlowRenderListener.PerActivityListener listener = activityListenerCaptor.getValue(); + PerActivityListener listener = activityListenerCaptor.getValue(); for (long duration : makeSomeDurations()) { when(frameMetrics.getMetric(DRAW_DURATION)).thenReturn(duration); listener.onFrameMetricsAvailable(null, frameMetrics, 0); @@ -152,14 +153,16 @@ public void start() { .when(exec) .scheduleWithFixedDelay(any(), eq(1001L), eq(1001L), eq(TimeUnit.MILLISECONDS)); + Tracer tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); + jankReporter = new SpanBasedJankReporter(tracer); SlowRenderListener testInstance = - new SlowRenderListener(tracer, exec, frameMetricsHandler, Duration.ofMillis(1001)); + new SlowRenderListener(jankReporter, exec, frameMetricsHandler, Duration.ofMillis(1001)); testInstance.onActivityResumed(activity); verify(activity.getWindow()) .addOnFrameMetricsAvailableListener(activityListenerCaptor.capture(), any()); - SlowRenderListener.PerActivityListener listener = activityListenerCaptor.getValue(); + PerActivityListener listener = activityListenerCaptor.getValue(); for (long duration : makeSomeDurations()) { when(frameMetrics.getMetric(DRAW_DURATION)).thenReturn(duration); listener.onFrameMetricsAvailable(null, frameMetrics, 0); @@ -173,8 +176,8 @@ public void start() { @Test public void activityListenerSkipsFirstFrame() { - SlowRenderListener.PerActivityListener listener = - new SlowRenderListener.PerActivityListener(null); + PerActivityListener listener = + new PerActivityListener(activity); when(frameMetrics.getMetric(FIRST_DRAW_FRAME)).thenReturn(1L); listener.onFrameMetricsAvailable(null, frameMetrics, 99); verify(frameMetrics, never()).getMetric(DRAW_DURATION); diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt new file mode 100644 index 000000000..88c4c1751 --- /dev/null +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt @@ -0,0 +1,20 @@ +package io.opentelemetry.android.instrumentation.slowrendering + +import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule +import org.junit.Rule +import org.junit.jupiter.api.BeforeEach + +class SpanBasedJankReporterTest { + + private lateinit var tracer: Tracer + + @Rule + var otelTesting: OpenTelemetryRule = OpenTelemetryRule.create() + + @BeforeEach + fun setup(){ + tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); + } + +} \ No newline at end of file From 510c5f8588ef4b18a5580a0e732a023b85ac6b97 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 18 Aug 2025 11:24:49 -0700 Subject: [PATCH 02/18] add test --- .../slowrendering/SlowRenderListener.java | 4 -- .../slowrendering/SpanBasedJankReporter.kt | 7 +- .../SpanBasedJankReporterTest.kt | 65 ++++++++++++++++++- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java index f6765f136..a779b5a8f 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java @@ -30,10 +30,6 @@ @RequiresApi(api = Build.VERSION_CODES.N) class SlowRenderListener implements DefaultingActivityLifecycleCallbacks { - - static final int SLOW_THRESHOLD_MS = 16; - static final int FROZEN_THRESHOLD_MS = 700; - private static final HandlerThread frameMetricsThread = new HandlerThread("FrameMetricsCollector"); diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt index a417214df..e7075ce11 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt @@ -6,6 +6,9 @@ import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.Tracer import java.time.Instant +private const val SLOW_THRESHOLD_MS = 16 +private const val FROZEN_THRESHOLD_MS = 700 + internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter { override fun reportSlow(listener: PerActivityListener) { @@ -15,13 +18,13 @@ internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter for (i in 0 until durationToCountHistogram.size()) { val duration = durationToCountHistogram.keyAt(i) val count = durationToCountHistogram.get(duration) - if (duration > SlowRenderListener.FROZEN_THRESHOLD_MS) { + if (duration > FROZEN_THRESHOLD_MS) { Log.d( RumConstants.OTEL_RUM_LOG_TAG, "* FROZEN RENDER DETECTED: $duration ms.$count times" ) frozenCount += count - } else if (duration > SlowRenderListener.SLOW_THRESHOLD_MS) { + } else if (duration > SLOW_THRESHOLD_MS) { Log.d( RumConstants.OTEL_RUM_LOG_TAG, "* Slow render detected: $duration ms. $count times" diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt index 88c4c1751..cde17dc48 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt @@ -1,9 +1,23 @@ package io.opentelemetry.android.instrumentation.slowrendering +import android.util.Log +import android.util.SparseIntArray +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.trace.Tracer +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule +import io.opentelemetry.sdk.trace.data.SpanData +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.ThrowingConsumer import org.junit.Rule import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +private val COUNT_KEY = AttributeKey.longKey("count") + class SpanBasedJankReporterTest { @@ -14,7 +28,56 @@ class SpanBasedJankReporterTest { @BeforeEach fun setup(){ - tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); + tracer = otelTesting.openTelemetry.getTracer("testTracer"); } + @Test + fun `spans are generated`() { + val jankReporter = SpanBasedJankReporter(tracer) + val perActivityListener: PerActivityListener = mockk() + val histogramData: SparseIntArray = mockk() + every { histogramData.size() } returns 2 + val key1 = 17 + val key2 = 701 + every { histogramData.keyAt(0) } returns key1 + every { histogramData.keyAt(1) } returns key2 + every { histogramData.get(key1) } returns 3 + every { histogramData.get(key2) } returns 1 + every { perActivityListener.resetMetrics() } returns histogramData + every { perActivityListener.getActivityName() } returns "io.otel/Komponent" + mockkStatic(Log::class) + every { Log.d(any(), any())} returns 0 + + jankReporter.reportSlow(perActivityListener) + + assertSpanContent(otelTesting.spans) + } + + private fun assertSpanContent(spans: MutableList?) { + assertThat(spans) + .hasSize(2) + .satisfiesExactly( + ThrowingConsumer { span: SpanData? -> + OpenTelemetryAssertions.assertThat(span) + .hasName("slowRenders") + .endsAt(span!!.getStartEpochNanos()) + .hasAttribute(COUNT_KEY, 3L) + .hasAttribute( + AttributeKey.stringKey("activity.name"), + "io.otel/Komponent" + ) + }, + ThrowingConsumer { span: SpanData? -> + OpenTelemetryAssertions.assertThat(span) + .hasName("frozenRenders") + .endsAt(span!!.getStartEpochNanos()) + .hasAttribute(COUNT_KEY, 1L) + .hasAttribute( + AttributeKey.stringKey("activity.name"), + "io.otel/Komponent" + ) + }) + } + + } \ No newline at end of file From 06669d82d95de5138f446836b19ed91167c669c9 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 18 Aug 2025 11:26:36 -0700 Subject: [PATCH 03/18] add test --- .../SpanBasedJankReporterTest.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt index cde17dc48..99bedc726 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt @@ -53,6 +53,29 @@ class SpanBasedJankReporterTest { assertSpanContent(otelTesting.spans) } + @Test + fun `no spans created when no slow frames`() { + val jankReporter = SpanBasedJankReporter(tracer) + val perActivityListener: PerActivityListener = mockk() + val histogramData: SparseIntArray = mockk() + every { histogramData.size() } returns 2 + val key1 = 3 + val key2 = 8 + every { histogramData.keyAt(0) } returns key1 + every { histogramData.keyAt(1) } returns key2 + every { histogramData.get(key1) } returns 3 + every { histogramData.get(key2) } returns 1 + every { perActivityListener.resetMetrics() } returns histogramData + every { perActivityListener.getActivityName() } returns "io.otel/Komponent" + mockkStatic(Log::class) + every { Log.d(any(), any())} returns 0 + + jankReporter.reportSlow(perActivityListener) + + assertThat(otelTesting.spans.size).isZero + } + + private fun assertSpanContent(spans: MutableList?) { assertThat(spans) .hasSize(2) From 8d21dc7471765ac5eae384567e2e6b824dc23abf Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 18 Aug 2025 11:49:22 -0700 Subject: [PATCH 04/18] Rename .java to .kt --- .../{SlowRenderListener.java => SlowRenderListener.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/{SlowRenderListener.java => SlowRenderListener.kt} (100%) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt similarity index 100% rename from instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.java rename to instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt From 15945bcb8cf1d4b517ff31fda449be1ea244a4e2 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Mon, 18 Aug 2025 11:49:22 -0700 Subject: [PATCH 05/18] convert to kotlin --- .../slowrendering/JankReporter.kt | 15 +- .../slowrendering/SlowRenderListener.kt | 168 ++++++++---------- .../slowrendering/SpanBasedJankReporter.kt | 6 +- 3 files changed, 91 insertions(+), 98 deletions(-) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt index 4b03cec9d..dc0a1a0ce 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt @@ -4,6 +4,19 @@ 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 interface JankReporter { +internal fun interface JankReporter { fun reportSlow(listener: PerActivityListener) + + /** + * Creates a combined JankReporter that will first report slow for this + * instance and then delegate to another JankReporter instance. + */ + fun combine(jankReporter: JankReporter): JankReporter { + return object: JankReporter { + override fun reportSlow(listener: PerActivityListener) { + reportSlow(listener) + jankReporter.reportSlow(listener) + } + } + } } \ No newline at end of file diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt index a779b5a8f..90f10bbaa 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt @@ -2,123 +2,103 @@ * Copyright The OpenTelemetry Authors * SPDX-License-Identifier: Apache-2.0 */ +package io.opentelemetry.android.instrumentation.slowrendering -package io.opentelemetry.android.instrumentation.slowrendering; - -import android.app.Activity; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Looper; -import android.util.Log; -import android.util.SparseIntArray; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import io.opentelemetry.android.common.RumConstants; -import io.opentelemetry.android.internal.services.visiblescreen.activities.DefaultingActivityLifecycleCallbacks; -import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.Tracer; -import java.time.Duration; -import java.time.Instant; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import android.app.Activity +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper +import android.util.Log +import androidx.annotation.RequiresApi +import io.opentelemetry.android.common.RumConstants +import io.opentelemetry.android.internal.services.visiblescreen.activities.DefaultingActivityLifecycleCallbacks +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentMap +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService +import java.util.concurrent.TimeUnit @RequiresApi(api = Build.VERSION_CODES.N) -class SlowRenderListener implements DefaultingActivityLifecycleCallbacks { - private static final HandlerThread frameMetricsThread = - new HandlerThread("FrameMetricsCollector"); - - private final JankReporter jankReporter; - private final ScheduledExecutorService executorService; - private final Handler frameMetricsHandler; - private final Duration pollInterval; - - private final ConcurrentMap activities = - new ConcurrentHashMap<>(); - - SlowRenderListener(JankReporter jankReporter, Duration pollInterval) { - this( - jankReporter, - Executors.newScheduledThreadPool(1), - new Handler(startFrameMetricsLoop()), - pollInterval); - } - - // Exists for testing - SlowRenderListener( - JankReporter jankReporter, - ScheduledExecutorService executorService, - Handler frameMetricsHandler, - Duration pollInterval) { - this.jankReporter = jankReporter; - this.executorService = executorService; - this.frameMetricsHandler = frameMetricsHandler; - this.pollInterval = pollInterval; - } +internal class SlowRenderListener // Exists for testing + ( + private val jankReporter: JankReporter, + private val executorService: ScheduledExecutorService, + private val frameMetricsHandler: Handler, + private val pollInterval: Duration +) : DefaultingActivityLifecycleCallbacks { + private val activities: ConcurrentMap = ConcurrentHashMap() - private static Looper startFrameMetricsLoop() { - // just a precaution: this is supposed to be called only once, and the thread should always - // be not started here - if (!frameMetricsThread.isAlive()) { - frameMetricsThread.start(); - } - return frameMetricsThread.getLooper(); - } + constructor(jankReporter: JankReporter, pollInterval: Duration) : this( + jankReporter, + Executors.newScheduledThreadPool(1), + Handler(startFrameMetricsLoop()), + pollInterval + ) // the returned future is very unlikely to fail - @SuppressWarnings("FutureReturnValueIgnored") - void start() { + fun start() { executorService.scheduleWithFixedDelay( - this::reportSlowRenders, - pollInterval.toMillis(), - pollInterval.toMillis(), - TimeUnit.MILLISECONDS); + { this.reportSlowRenders() }, + pollInterval.toMillis(), + pollInterval.toMillis(), + TimeUnit.MILLISECONDS + ) } - public void shutdown() { - executorService.shutdownNow(); - for (Map.Entry entry : activities.entrySet()) { - Activity activity = entry.getKey(); - PerActivityListener listener = entry.getValue(); - activity.getWindow().removeOnFrameMetricsAvailableListener(listener); + fun shutdown() { + executorService.shutdownNow() + for (entry in activities.entries) { + val activity: Activity = entry.key!! + val listener = entry.value + activity.window.removeOnFrameMetricsAvailableListener(listener) } - activities.clear(); + activities.clear() } - @Override - public void onActivityResumed(@NonNull Activity activity) { - if (executorService.isShutdown()) { - return; + override fun onActivityResumed(activity: Activity) { + if (executorService.isShutdown) { + return } - PerActivityListener listener = new PerActivityListener(activity); - PerActivityListener existing = activities.putIfAbsent(activity, listener); + val listener = PerActivityListener(activity) + val existing = activities.putIfAbsent(activity, listener) if (existing == null) { - activity.getWindow().addOnFrameMetricsAvailableListener(listener, frameMetricsHandler); + activity.window.addOnFrameMetricsAvailableListener(listener, frameMetricsHandler) } } - @Override - public void onActivityPaused(@NonNull Activity activity) { - if (executorService.isShutdown()) { - return; + override fun onActivityPaused(activity: Activity) { + if (executorService.isShutdown) { + return } - PerActivityListener listener = activities.remove(activity); + val listener = activities.remove(activity) if (listener != null) { - activity.getWindow().removeOnFrameMetricsAvailableListener(listener); - jankReporter.reportSlow(listener); + activity.window.removeOnFrameMetricsAvailableListener(listener) + jankReporter.reportSlow(listener) } } - private void reportSlowRenders() { + private fun reportSlowRenders() { try { - activities.forEach((activity, listener) -> jankReporter.reportSlow(listener)); - } catch (Exception e) { - Log.w(RumConstants.OTEL_RUM_LOG_TAG, "Exception while processing frame metrics", e); + activities.forEach { (_: Activity?, listener: PerActivityListener) -> + jankReporter.reportSlow(listener) + } + } catch (e: Exception) { + Log.w(RumConstants.OTEL_RUM_LOG_TAG, "Exception while processing frame metrics", e) + } + } + + companion object { + private val frameMetricsThread = HandlerThread("FrameMetricsCollector") + + private fun startFrameMetricsLoop(): Looper { + // just a precaution: this is supposed to be called only once, and the thread should always + // be not started here + if (!frameMetricsThread.isAlive) { + frameMetricsThread.start() + } + return frameMetricsThread.looper } } } diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt index e7075ce11..7ecc0f85a 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt @@ -33,12 +33,12 @@ internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter } } - val now = Instant.now(); + val now = Instant.now() if (slowCount > 0) { - makeSpan("slowRenders", listener.getActivityName(), slowCount, now); + makeSpan("slowRenders", listener.getActivityName(), slowCount, now) } if (frozenCount > 0) { - makeSpan("frozenRenders", listener.getActivityName(), frozenCount, now); + makeSpan("frozenRenders", listener.getActivityName(), frozenCount, now) } } From 87e2b89905bf713862c5c4458917122c2753e31e Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Tue, 19 Aug 2025 11:47:38 -0700 Subject: [PATCH 06/18] change signature to reuse same histogram, because reset clears it atomically. --- .../instrumentation/slowrendering/JankReporter.kt | 10 ++++++---- .../slowrendering/SlowRenderListener.kt | 6 ++++-- .../slowrendering/SpanBasedJankReporter.kt | 9 ++++----- .../slowrendering/SpanBasedJankReporterTest.kt | 11 ++--------- 4 files changed, 16 insertions(+), 20 deletions(-) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt index dc0a1a0ce..05ae12603 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt @@ -1,11 +1,13 @@ package io.opentelemetry.android.instrumentation.slowrendering +import android.util.SparseIntArray + /** * 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(listener: PerActivityListener) + fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) /** * Creates a combined JankReporter that will first report slow for this @@ -13,9 +15,9 @@ internal fun interface JankReporter { */ fun combine(jankReporter: JankReporter): JankReporter { return object: JankReporter { - override fun reportSlow(listener: PerActivityListener) { - reportSlow(listener) - jankReporter.reportSlow(listener) + override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { + reportSlow(durationToCountHistogram, periodSeconds, activityName) + jankReporter.reportSlow(durationToCountHistogram, periodSeconds, activityName) } } } diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt index 90f10bbaa..ecb4d8b5e 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt @@ -75,14 +75,16 @@ internal class SlowRenderListener // Exists for testing val listener = activities.remove(activity) if (listener != null) { activity.window.removeOnFrameMetricsAvailableListener(listener) - jankReporter.reportSlow(listener) + val durationToCountHistogram = listener.resetMetrics() + jankReporter.reportSlow(durationToCountHistogram, pollInterval.toSeconds().toDouble(), listener.getActivityName()) } } private fun reportSlowRenders() { try { activities.forEach { (_: Activity?, listener: PerActivityListener) -> - jankReporter.reportSlow(listener) + val durationToCountHistogram = listener.resetMetrics() + jankReporter.reportSlow(durationToCountHistogram, pollInterval.toSeconds().toDouble(), listener.getActivityName()) } } catch (e: Exception) { Log.w(RumConstants.OTEL_RUM_LOG_TAG, "Exception while processing frame metrics", e) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt index 7ecc0f85a..d9c3761a8 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt @@ -1,6 +1,7 @@ package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log +import android.util.SparseIntArray import io.opentelemetry.android.common.RumConstants import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.Tracer @@ -11,10 +12,9 @@ private const val FROZEN_THRESHOLD_MS = 700 internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter { - override fun reportSlow(listener: PerActivityListener) { + override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { var slowCount = 0 var frozenCount = 0 - val durationToCountHistogram = listener.resetMetrics() for (i in 0 until durationToCountHistogram.size()) { val duration = durationToCountHistogram.keyAt(i) val count = durationToCountHistogram.get(duration) @@ -35,15 +35,14 @@ internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter val now = Instant.now() if (slowCount > 0) { - makeSpan("slowRenders", listener.getActivityName(), slowCount, now) + makeSpan("slowRenders", activityName, slowCount, now) } if (frozenCount > 0) { - makeSpan("frozenRenders", listener.getActivityName(), frozenCount, now) + makeSpan("frozenRenders", activityName, frozenCount, now) } } private fun makeSpan(spanName: String, activityName: String, slowCount: Int, now: Instant) { - // TODO: Use an event rather than a zero-duration span val span: Span = tracer.spanBuilder(spanName) .setAttribute("count", slowCount.toLong()) diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt index 99bedc726..c9d656fce 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt @@ -34,7 +34,6 @@ class SpanBasedJankReporterTest { @Test fun `spans are generated`() { val jankReporter = SpanBasedJankReporter(tracer) - val perActivityListener: PerActivityListener = mockk() val histogramData: SparseIntArray = mockk() every { histogramData.size() } returns 2 val key1 = 17 @@ -43,12 +42,10 @@ class SpanBasedJankReporterTest { every { histogramData.keyAt(1) } returns key2 every { histogramData.get(key1) } returns 3 every { histogramData.get(key2) } returns 1 - every { perActivityListener.resetMetrics() } returns histogramData - every { perActivityListener.getActivityName() } returns "io.otel/Komponent" mockkStatic(Log::class) every { Log.d(any(), any())} returns 0 - jankReporter.reportSlow(perActivityListener) + jankReporter.reportSlow(histogramData, 0.1, "io.otel/Komponent") assertSpanContent(otelTesting.spans) } @@ -56,7 +53,6 @@ class SpanBasedJankReporterTest { @Test fun `no spans created when no slow frames`() { val jankReporter = SpanBasedJankReporter(tracer) - val perActivityListener: PerActivityListener = mockk() val histogramData: SparseIntArray = mockk() every { histogramData.size() } returns 2 val key1 = 3 @@ -65,17 +61,14 @@ class SpanBasedJankReporterTest { every { histogramData.keyAt(1) } returns key2 every { histogramData.get(key1) } returns 3 every { histogramData.get(key2) } returns 1 - every { perActivityListener.resetMetrics() } returns histogramData - every { perActivityListener.getActivityName() } returns "io.otel/Komponent" mockkStatic(Log::class) every { Log.d(any(), any())} returns 0 - jankReporter.reportSlow(perActivityListener) + jankReporter.reportSlow(histogramData, 0.1, "") assertThat(otelTesting.spans.size).isZero } - private fun assertSpanContent(spans: MutableList?) { assertThat(spans) .hasSize(2) From 171f980febda4d870239e7ca1feb73b97f61537c Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Tue, 19 Aug 2025 11:47:48 -0700 Subject: [PATCH 07/18] begin EventsJankReporter --- .../slowrendering/build.gradle.kts | 1 + .../slowrendering/EventsJankReporter.kt | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt diff --git a/instrumentation/slowrendering/build.gradle.kts b/instrumentation/slowrendering/build.gradle.kts index 040428e6a..54046309e 100644 --- a/instrumentation/slowrendering/build.gradle.kts +++ b/instrumentation/slowrendering/build.gradle.kts @@ -24,5 +24,6 @@ dependencies { implementation(libs.opentelemetry.semconv) implementation(libs.opentelemetry.sdk) implementation(libs.opentelemetry.instrumentation.api) + implementation(libs.opentelemetry.sdk.extension.incubator) testImplementation(libs.robolectric) } diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt new file mode 100644 index 000000000..a00d34cd6 --- /dev/null +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt @@ -0,0 +1,50 @@ +package io.opentelemetry.android.instrumentation.slowrendering + +import android.util.Log +import android.util.SparseIntArray +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 +import java.util.concurrent.TimeUnit + +//TODO: Replace with semconv constants +val FRAME_COUNT: AttributeKey = AttributeKey.longKey("app.jank.frame_count") +val PERIOD: AttributeKey = AttributeKey.doubleKey("app.jank.period") +val THRESHOLD: AttributeKey = AttributeKey.doubleKey("app.jank.threshold") + +internal class EventsJankReporter( + private val eventLogger: Logger, + private val threshold: Double, + private val period: Double +) : JankReporter { + + override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { + var frameCount: Long = 0 + for (i in 0 until durationToCountHistogram.size()) { + val durationMillis = durationToCountHistogram.keyAt(i) + if (TimeUnit.MILLISECONDS.toSeconds(durationMillis.toLong()) > threshold) { + val count = durationToCountHistogram.get(durationMillis) + 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, period) + .put(THRESHOLD, threshold) + .build() + eventBuilder + .setEventName("app.jank") + .setAllAttributes(attributes) + .emit() + } + } +} \ No newline at end of file From 752793bfb7c9fbd4481cf200cba7cb6cfc08dffc Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Tue, 19 Aug 2025 16:28:44 -0700 Subject: [PATCH 08/18] add tests --- .../slowrendering/build.gradle.kts | 5 ++ .../slowrendering/EventsJankReporter.kt | 7 +-- .../slowrendering/JankReporter.kt | 6 +- .../slowrendering/EventsJankReporterTest.kt | 42 +++++++++++++ .../slowrendering/JankReporterTest.kt | 61 +++++++++++++++++++ 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt create mode 100644 instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt diff --git a/instrumentation/slowrendering/build.gradle.kts b/instrumentation/slowrendering/build.gradle.kts index 54046309e..aee5e829f 100644 --- a/instrumentation/slowrendering/build.gradle.kts +++ b/instrumentation/slowrendering/build.gradle.kts @@ -11,6 +11,10 @@ android { defaultConfig { consumerProguardFiles("consumer-rules.pro") } + + testOptions { + unitTests.isReturnDefaultValues = true + } } dependencies { @@ -27,3 +31,4 @@ dependencies { implementation(libs.opentelemetry.sdk.extension.incubator) testImplementation(libs.robolectric) } + diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt index a00d34cd6..6123acb4d 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt @@ -16,15 +16,14 @@ val THRESHOLD: AttributeKey = AttributeKey.doubleKey("app.jank.threshold internal class EventsJankReporter( private val eventLogger: Logger, - private val threshold: Double, - private val period: Double + private val threshold: Double ) : JankReporter { override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { var frameCount: Long = 0 for (i in 0 until durationToCountHistogram.size()) { val durationMillis = durationToCountHistogram.keyAt(i) - if (TimeUnit.MILLISECONDS.toSeconds(durationMillis.toLong()) > threshold) { + if ((durationMillis/1000.0) > threshold) { val count = durationToCountHistogram.get(durationMillis) Log.d( RumConstants.OTEL_RUM_LOG_TAG, @@ -38,7 +37,7 @@ internal class EventsJankReporter( val eventBuilder = eventLogger.logRecordBuilder() as ExtendedLogRecordBuilder val attributes = Attributes.builder() .put(FRAME_COUNT, frameCount) - .put(PERIOD, period) + .put(PERIOD, periodSeconds) .put(THRESHOLD, threshold) .build() eventBuilder diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt index 05ae12603..443a52c5c 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt @@ -14,9 +14,13 @@ internal fun interface JankReporter { * instance and then delegate to another JankReporter instance. */ fun combine(jankReporter: JankReporter): JankReporter { + if(jankReporter == this){ + throw IllegalArgumentException("cannot combine with self") + } + val exec = this::reportSlow return object: JankReporter { override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { - reportSlow(durationToCountHistogram, periodSeconds, activityName) + exec(durationToCountHistogram, periodSeconds, activityName) jankReporter.reportSlow(durationToCountHistogram, periodSeconds, activityName) } } diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt new file mode 100644 index 000000000..1d250384d --- /dev/null +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt @@ -0,0 +1,42 @@ +package io.opentelemetry.android.instrumentation.slowrendering + +import android.util.Log +import android.util.SparseIntArray +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule +import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule +import org.junit.jupiter.api.Test + +class EventsJankReporterTest { + @Rule + var otelTesting: OpenTelemetryRule = OpenTelemetryRule.create() + + @Test + fun `event is generated`() { + val eventLogger = otelTesting.openTelemetry.logsBridge.get("JANK!") + val jankReporter = EventsJankReporter(eventLogger, 0.600) + val histogramData: SparseIntArray = mockk() + every { histogramData.size() } returns 2 + val key1 = 17 + val key2 = 701 + every { histogramData.keyAt(0) } returns key1 + every { histogramData.keyAt(1) } returns key2 + every { histogramData.get(key1) } returns 3 + every { histogramData.get(key2) } returns 1 + mockkStatic(Log::class) + every { Log.d(any(), any())} returns 0 + + jankReporter.reportSlow(histogramData, 10.5, "io.otel/Komponent") + + assertThat(otelTesting.logRecords.size).isEqualTo(1) + val log = otelTesting.logRecords.get(0) + assertThat(log.eventName).isEqualTo("app.jank") + assertThat(log.attributes.get(FRAME_COUNT)).isEqualTo(1) + assertThat(log.attributes.get(PERIOD)).isEqualTo(10.5) + assertThat(log.attributes.get(THRESHOLD)).isEqualTo(0.6) + } + +} \ No newline at end of file diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt new file mode 100644 index 000000000..0c54f0d2b --- /dev/null +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt @@ -0,0 +1,61 @@ +package io.opentelemetry.android.instrumentation.slowrendering + +import android.util.SparseIntArray +import io.mockk.every +import io.mockk.mockk +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.Test + +class JankReporterTest { + + @Test + fun combine(){ + val state = StringBuilder("") + val inner = object: JankReporter { + override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { + state.append(".inner.") + .append(durationToCountHistogram) + .append(".") + .append(periodSeconds) + .append(".") + .append(activityName) + } + } + val outer = object: JankReporter { + override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { + state.append(".outer.") + .append(durationToCountHistogram) + .append(".") + .append(periodSeconds) + .append(".") + .append(activityName) + } + } + + val both = inner.combine(outer) + + val histogram = SparseIntArray() + val histogramData: SparseIntArray = mockk() + every { histogramData.size() } returns 1 + every { histogramData.toString() } returns "x" + val key1 = 99 + every { histogramData.keyAt(0) } returns key1 + every { histogramData.get(key1) } returns 37 + both.reportSlow(histogram, 6.9, "four.something") + val expected = ".inner.null.6.9.four.something.outer.null.6.9.four.something" + assertThat(state.toString()).isEqualTo(expected) + } + + @Test + fun `combine with self fails`(){ + val state = StringBuilder("") + val reporter = object: JankReporter { + override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { + } + } + assertThatExceptionOfType(IllegalArgumentException::class.java) + .isThrownBy { reporter.combine(reporter) } + .withMessage("cannot combine with self") + } +} \ No newline at end of file From 0be43732a9c104f91124e95a20fdfde3fbf81582 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 20 Aug 2025 14:49:47 -0700 Subject: [PATCH 09/18] spotless --- .../slowrendering/build.gradle.kts | 1 - .../slowrendering/EventsJankReporter.kt | 35 +++++---- .../slowrendering/JankReporter.kt | 23 ++++-- .../slowrendering/PerActivityListener.kt | 15 +++- .../slowrendering/SlowRenderListener.kt | 10 +-- .../SlowRenderingInstrumentation.kt | 1 - .../slowrendering/SpanBasedJankReporter.kt | 32 ++++++--- .../slowrendering/EventsJankReporterTest.kt | 10 ++- .../slowrendering/JankReporterTest.kt | 71 ++++++++++++------- .../slowrendering/SlowRenderListenerTest.java | 18 +++-- .../SpanBasedJankReporterTest.kt | 32 +++++---- 11 files changed, 163 insertions(+), 85 deletions(-) diff --git a/instrumentation/slowrendering/build.gradle.kts b/instrumentation/slowrendering/build.gradle.kts index aee5e829f..e38370aaa 100644 --- a/instrumentation/slowrendering/build.gradle.kts +++ b/instrumentation/slowrendering/build.gradle.kts @@ -31,4 +31,3 @@ dependencies { implementation(libs.opentelemetry.sdk.extension.incubator) testImplementation(libs.robolectric) } - diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt index 6123acb4d..124a7e8a2 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log @@ -7,27 +12,29 @@ 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 -import java.util.concurrent.TimeUnit -//TODO: Replace with semconv constants +// TODO: Replace with semconv constants val FRAME_COUNT: AttributeKey = AttributeKey.longKey("app.jank.frame_count") val PERIOD: AttributeKey = AttributeKey.doubleKey("app.jank.period") val THRESHOLD: AttributeKey = AttributeKey.doubleKey("app.jank.threshold") internal class EventsJankReporter( private val eventLogger: Logger, - private val threshold: Double + private val threshold: Double, ) : JankReporter { - - override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { + override fun reportSlow( + durationToCountHistogram: SparseIntArray, + periodSeconds: Double, + activityName: String, + ) { var frameCount: Long = 0 for (i in 0 until durationToCountHistogram.size()) { val durationMillis = durationToCountHistogram.keyAt(i) - if ((durationMillis/1000.0) > threshold) { + if ((durationMillis / 1000.0) > threshold) { val count = durationToCountHistogram.get(durationMillis) Log.d( RumConstants.OTEL_RUM_LOG_TAG, - "* Slow render detected: $durationMillis ms. $count times" + "* Slow render detected: $durationMillis ms. $count times", ) frameCount += count } @@ -35,15 +42,17 @@ internal class EventsJankReporter( if (frameCount > 0) { val eventBuilder = eventLogger.logRecordBuilder() as ExtendedLogRecordBuilder - val attributes = Attributes.builder() - .put(FRAME_COUNT, frameCount) - .put(PERIOD, periodSeconds) - .put(THRESHOLD, threshold) - .build() + val attributes = + Attributes + .builder() + .put(FRAME_COUNT, frameCount) + .put(PERIOD, periodSeconds) + .put(THRESHOLD, threshold) + .build() eventBuilder .setEventName("app.jank") .setAllAttributes(attributes) .emit() } } -} \ No newline at end of file +} diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt index 443a52c5c..1052b08b6 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.android.instrumentation.slowrendering import android.util.SparseIntArray @@ -7,22 +12,30 @@ import android.util.SparseIntArray * after the jank semconv becomes more stable. */ internal fun interface JankReporter { - fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) + fun reportSlow( + durationToCountHistogram: SparseIntArray, + 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 { - if(jankReporter == this){ + if (jankReporter == this) { throw IllegalArgumentException("cannot combine with self") } val exec = this::reportSlow - return object: JankReporter { - override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { + return object : JankReporter { + override fun reportSlow( + durationToCountHistogram: SparseIntArray, + periodSeconds: Double, + activityName: String, + ) { exec(durationToCountHistogram, periodSeconds, activityName) jankReporter.reportSlow(durationToCountHistogram, periodSeconds, activityName) } } } -} \ No newline at end of file +} diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt index 2ab3bef87..5810030e1 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.android.instrumentation.slowrendering import android.app.Activity @@ -11,12 +16,14 @@ 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 { +internal class PerActivityListener( + private val activity: Activity, +) : OnFrameMetricsAvailableListener { private val lock = Any() @GuardedBy("lock") @@ -25,7 +32,9 @@ internal class PerActivityListener(private val activity: Activity) : fun getActivityName(): String = activity.componentName.flattenToShortString() override fun onFrameMetricsAvailable( - window: Window?, frameMetrics: FrameMetrics, dropCountSinceLastInvocation: Int + window: Window?, + frameMetrics: FrameMetrics, + dropCountSinceLastInvocation: Int, ) { val firstDrawFrame = frameMetrics.getMetric(FrameMetrics.FIRST_DRAW_FRAME) if (firstDrawFrame == 1L) { diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt index ecb4d8b5e..64f187270 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListener.kt @@ -2,6 +2,7 @@ * Copyright The OpenTelemetry Authors * SPDX-License-Identifier: Apache-2.0 */ + package io.opentelemetry.android.instrumentation.slowrendering import android.app.Activity @@ -21,12 +22,11 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit @RequiresApi(api = Build.VERSION_CODES.N) -internal class SlowRenderListener // Exists for testing - ( +internal class SlowRenderListener( private val jankReporter: JankReporter, private val executorService: ScheduledExecutorService, private val frameMetricsHandler: Handler, - private val pollInterval: Duration + private val pollInterval: Duration, ) : DefaultingActivityLifecycleCallbacks { private val activities: ConcurrentMap = ConcurrentHashMap() @@ -34,7 +34,7 @@ internal class SlowRenderListener // Exists for testing jankReporter, Executors.newScheduledThreadPool(1), Handler(startFrameMetricsLoop()), - pollInterval + pollInterval, ) // the returned future is very unlikely to fail @@ -43,7 +43,7 @@ internal class SlowRenderListener // Exists for testing { this.reportSlowRenders() }, pollInterval.toMillis(), pollInterval.toMillis(), - TimeUnit.MILLISECONDS + TimeUnit.MILLISECONDS, ) } diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt index 67b51b232..caadb22de 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt @@ -61,7 +61,6 @@ class SlowRenderingInstrumentation : AndroidInstrumentation { return } - // TODO: Let the type of reporter be configurable val tracer = ctx.openTelemetry.getTracer("io.opentelemetry.slow-rendering") val jankReporter = SpanBasedJankReporter(tracer) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt index d9c3761a8..d329afb7e 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log @@ -10,9 +15,14 @@ import java.time.Instant private const val SLOW_THRESHOLD_MS = 16 private const val FROZEN_THRESHOLD_MS = 700 -internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter { - - override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { +internal class SpanBasedJankReporter( + private val tracer: Tracer, +) : JankReporter { + override fun reportSlow( + durationToCountHistogram: SparseIntArray, + periodSeconds: Double, + activityName: String, + ) { var slowCount = 0 var frozenCount = 0 for (i in 0 until durationToCountHistogram.size()) { @@ -21,13 +31,13 @@ internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter if (duration > FROZEN_THRESHOLD_MS) { Log.d( RumConstants.OTEL_RUM_LOG_TAG, - "* FROZEN RENDER DETECTED: $duration ms.$count times" + "* FROZEN RENDER DETECTED: $duration ms.$count times", ) frozenCount += count } else if (duration > SLOW_THRESHOLD_MS) { Log.d( RumConstants.OTEL_RUM_LOG_TAG, - "* Slow render detected: $duration ms. $count times" + "* Slow render detected: $duration ms. $count times", ) slowCount += count } @@ -42,13 +52,19 @@ internal class SpanBasedJankReporter(private val tracer: Tracer) : JankReporter } } - private fun makeSpan(spanName: String, activityName: String, slowCount: Int, now: Instant) { + private fun makeSpan( + spanName: String, + activityName: String, + slowCount: Int, + now: Instant, + ) { val span: Span = - tracer.spanBuilder(spanName) + tracer + .spanBuilder(spanName) .setAttribute("count", slowCount.toLong()) .setAttribute("activity.name", activityName) .setStartTimestamp(now) .startSpan() span.end(now) } -} \ No newline at end of file +} diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt index 1d250384d..6b451dc33 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log @@ -27,7 +32,7 @@ class EventsJankReporterTest { every { histogramData.get(key1) } returns 3 every { histogramData.get(key2) } returns 1 mockkStatic(Log::class) - every { Log.d(any(), any())} returns 0 + every { Log.d(any(), any()) } returns 0 jankReporter.reportSlow(histogramData, 10.5, "io.otel/Komponent") @@ -38,5 +43,4 @@ class EventsJankReporterTest { assertThat(log.attributes.get(PERIOD)).isEqualTo(10.5) assertThat(log.attributes.get(THRESHOLD)).isEqualTo(0.6) } - -} \ No newline at end of file +} diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt index 0c54f0d2b..395ff0706 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.android.instrumentation.slowrendering import android.util.SparseIntArray @@ -8,30 +13,41 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test class JankReporterTest { - @Test - fun combine(){ + fun combine() { val state = StringBuilder("") - val inner = object: JankReporter { - override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { - state.append(".inner.") - .append(durationToCountHistogram) - .append(".") - .append(periodSeconds) - .append(".") - .append(activityName) + val inner = + object : JankReporter { + override fun reportSlow( + durationToCountHistogram: SparseIntArray, + periodSeconds: Double, + activityName: String, + ) { + state + .append(".inner.") + .append(durationToCountHistogram) + .append(".") + .append(periodSeconds) + .append(".") + .append(activityName) + } } - } - val outer = object: JankReporter { - override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { - state.append(".outer.") - .append(durationToCountHistogram) - .append(".") - .append(periodSeconds) - .append(".") - .append(activityName) + val outer = + object : JankReporter { + override fun reportSlow( + durationToCountHistogram: SparseIntArray, + periodSeconds: Double, + activityName: String, + ) { + state + .append(".outer.") + .append(durationToCountHistogram) + .append(".") + .append(periodSeconds) + .append(".") + .append(activityName) + } } - } val both = inner.combine(outer) @@ -48,14 +64,19 @@ class JankReporterTest { } @Test - fun `combine with self fails`(){ + fun `combine with self fails`() { val state = StringBuilder("") - val reporter = object: JankReporter { - override fun reportSlow(durationToCountHistogram: SparseIntArray, periodSeconds: Double, activityName: String) { + val reporter = + object : JankReporter { + override fun reportSlow( + durationToCountHistogram: SparseIntArray, + periodSeconds: Double, + activityName: String, + ) { + } } - } assertThatExceptionOfType(IllegalArgumentException::class.java) .isThrownBy { reporter.combine(reporter) } .withMessage("cannot combine with self") } -} \ No newline at end of file +} diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java index 4f0a136b7..9d63b6ac8 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderListenerTest.java @@ -79,7 +79,8 @@ public void setup() { @Test public void add() { SlowRenderListener testInstance = - new SlowRenderListener(jankReporter, executorService, frameMetricsHandler, Duration.ZERO); + new SlowRenderListener( + jankReporter, executorService, frameMetricsHandler, Duration.ZERO); testInstance.onActivityResumed(activity); @@ -92,7 +93,8 @@ public void add() { @Test public void removeBeforeAddOk() { SlowRenderListener testInstance = - new SlowRenderListener(jankReporter, executorService, frameMetricsHandler, Duration.ZERO); + new SlowRenderListener( + jankReporter, executorService, frameMetricsHandler, Duration.ZERO); testInstance.onActivityPaused(activity); @@ -103,7 +105,8 @@ public void removeBeforeAddOk() { @Test public void addAndRemove() { SlowRenderListener testInstance = - new SlowRenderListener(jankReporter, executorService, frameMetricsHandler, Duration.ZERO); + new SlowRenderListener( + jankReporter, executorService, frameMetricsHandler, Duration.ZERO); testInstance.onActivityResumed(activity); testInstance.onActivityPaused(activity); @@ -122,7 +125,8 @@ public void removeWithMetrics() { Tracer tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); jankReporter = new SpanBasedJankReporter(tracer); SlowRenderListener testInstance = - new SlowRenderListener(jankReporter, executorService, frameMetricsHandler, Duration.ZERO); + new SlowRenderListener( + jankReporter, executorService, frameMetricsHandler, Duration.ZERO); testInstance.onActivityResumed(activity); @@ -156,7 +160,8 @@ public void start() { Tracer tracer = otelTesting.getOpenTelemetry().getTracer("testTracer"); jankReporter = new SpanBasedJankReporter(tracer); SlowRenderListener testInstance = - new SlowRenderListener(jankReporter, exec, frameMetricsHandler, Duration.ofMillis(1001)); + new SlowRenderListener( + jankReporter, exec, frameMetricsHandler, Duration.ofMillis(1001)); testInstance.onActivityResumed(activity); @@ -176,8 +181,7 @@ public void start() { @Test public void activityListenerSkipsFirstFrame() { - PerActivityListener listener = - new PerActivityListener(activity); + PerActivityListener listener = new PerActivityListener(activity); when(frameMetrics.getMetric(FIRST_DRAW_FRAME)).thenReturn(1L); listener.onFrameMetricsAvailable(null, frameMetrics, 99); verify(frameMetrics, never()).getMetric(DRAW_DURATION); diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt index c9d656fce..acb1b57f8 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt @@ -1,3 +1,8 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log @@ -18,17 +23,15 @@ import org.junit.jupiter.api.Test private val COUNT_KEY = AttributeKey.longKey("count") - class SpanBasedJankReporterTest { - private lateinit var tracer: Tracer @Rule var otelTesting: OpenTelemetryRule = OpenTelemetryRule.create() @BeforeEach - fun setup(){ - tracer = otelTesting.openTelemetry.getTracer("testTracer"); + fun setup() { + tracer = otelTesting.openTelemetry.getTracer("testTracer") } @Test @@ -43,7 +46,7 @@ class SpanBasedJankReporterTest { every { histogramData.get(key1) } returns 3 every { histogramData.get(key2) } returns 1 mockkStatic(Log::class) - every { Log.d(any(), any())} returns 0 + every { Log.d(any(), any()) } returns 0 jankReporter.reportSlow(histogramData, 0.1, "io.otel/Komponent") @@ -62,7 +65,7 @@ class SpanBasedJankReporterTest { every { histogramData.get(key1) } returns 3 every { histogramData.get(key2) } returns 1 mockkStatic(Log::class) - every { Log.d(any(), any())} returns 0 + every { Log.d(any(), any()) } returns 0 jankReporter.reportSlow(histogramData, 0.1, "") @@ -74,26 +77,27 @@ class SpanBasedJankReporterTest { .hasSize(2) .satisfiesExactly( ThrowingConsumer { span: SpanData? -> - OpenTelemetryAssertions.assertThat(span) + OpenTelemetryAssertions + .assertThat(span) .hasName("slowRenders") .endsAt(span!!.getStartEpochNanos()) .hasAttribute(COUNT_KEY, 3L) .hasAttribute( AttributeKey.stringKey("activity.name"), - "io.otel/Komponent" + "io.otel/Komponent", ) }, ThrowingConsumer { span: SpanData? -> - OpenTelemetryAssertions.assertThat(span) + OpenTelemetryAssertions + .assertThat(span) .hasName("frozenRenders") .endsAt(span!!.getStartEpochNanos()) .hasAttribute(COUNT_KEY, 1L) .hasAttribute( AttributeKey.stringKey("activity.name"), - "io.otel/Komponent" + "io.otel/Komponent", ) - }) + }, + ) } - - -} \ No newline at end of file +} From 0657babdf2cb908a9422c23fde90387d594625f8 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 20 Aug 2025 15:03:43 -0700 Subject: [PATCH 10/18] wire up jank events and allow enabling legacy/deprecated zero duration span. --- .../agent/OpenTelemetryRumInitializer.kt | 6 +++--- .../SlowRenderingInstrumentation.kt | 20 ++++++++++++++++--- .../slowrendering/SpanBasedJankReporter.kt | 4 ++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt index 079062f3c..50fdd1fc5 100644 --- a/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt +++ b/android-agent/src/main/kotlin/io/opentelemetry/android/agent/OpenTelemetryRumInitializer.kt @@ -180,9 +180,9 @@ object OpenTelemetryRumInitializer { } if (slowRenderingDetectionPollInterval != null) { - getInstrumentation()?.setSlowRenderingDetectionPollInterval( - slowRenderingDetectionPollInterval, - ) + val instrumentation = getInstrumentation() + instrumentation?.setSlowRenderingDetectionPollInterval(slowRenderingDetectionPollInterval) + instrumentation?.enableDeprecatedZeroDurationSpan() } } diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt index caadb22de..5a2aefaa4 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt @@ -18,6 +18,7 @@ import java.time.Duration */ @AutoService(AndroidInstrumentation::class) class SlowRenderingInstrumentation : AndroidInstrumentation { + internal var useDeprecatedSpan: Boolean = false internal var slowRenderingDetectionPollInterval: Duration = Duration.ofSeconds(1) @Volatile @@ -45,6 +46,14 @@ class SlowRenderingInstrumentation : AndroidInstrumentation { return this } + /** + * Reports jank by using a zero-duration span. + */ + @Deprecated("Use the default event to report jank") + fun enableDeprecatedZeroDurationSpan() { + useDeprecatedSpan = true + } + override fun install(ctx: InstallationContext) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { Log.w( @@ -61,9 +70,14 @@ class SlowRenderingInstrumentation : AndroidInstrumentation { return } - // TODO: Let the type of reporter be configurable - val tracer = ctx.openTelemetry.getTracer("io.opentelemetry.slow-rendering") - val jankReporter = SpanBasedJankReporter(tracer) + val logger = ctx.openTelemetry.logsBridge.get("app.jank") + var jankReporter: JankReporter = EventsJankReporter(logger, SLOW_THRESHOLD_MS / 1000.0) + jankReporter = jankReporter.combine(EventsJankReporter(logger, FROZEN_THRESHOLD_MS / 1000.0)) + + if (useDeprecatedSpan) { + val tracer = ctx.openTelemetry.getTracer("io.opentelemetry.slow-rendering") + jankReporter = jankReporter.combine(SpanBasedJankReporter(tracer)) + } detector = SlowRenderListener(jankReporter, slowRenderingDetectionPollInterval) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt index d329afb7e..a3da2fa10 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt @@ -12,8 +12,8 @@ import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.Tracer import java.time.Instant -private const val SLOW_THRESHOLD_MS = 16 -private const val FROZEN_THRESHOLD_MS = 700 +internal const val SLOW_THRESHOLD_MS = 16 +internal const val FROZEN_THRESHOLD_MS = 700 internal class SpanBasedJankReporter( private val tracer: Tracer, From bf3977d159968653c2e1dc85b22a5d7a7a957f19 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 20 Aug 2025 15:28:16 -0700 Subject: [PATCH 11/18] update detekt baseline --- .../slowrendering/config/detekt/baseline.xml | 11 +++++++++++ .../instrumentation/slowrendering/JankReporter.kt | 4 +--- .../instrumentation/slowrendering/JankReporterTest.kt | 1 - 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 instrumentation/slowrendering/config/detekt/baseline.xml diff --git a/instrumentation/slowrendering/config/detekt/baseline.xml b/instrumentation/slowrendering/config/detekt/baseline.xml new file mode 100644 index 000000000..f3cae5b1f --- /dev/null +++ b/instrumentation/slowrendering/config/detekt/baseline.xml @@ -0,0 +1,11 @@ + + + + + EmptyFunctionBlock:JankReporterTest.kt$JankReporterTest.<no name provided>${ } + ForbiddenComment:EventsJankReporter.kt$// TODO: Replace with semconv constants + MagicNumber:EventsJankReporter.kt$EventsJankReporter$1000.0 + MagicNumber:SlowRenderingInstrumentation.kt$SlowRenderingInstrumentation$1000.0 + TooGenericExceptionCaught:SlowRenderListener.kt$SlowRenderListener$e: Exception + + diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt index 1052b08b6..59350e2a6 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt @@ -23,9 +23,7 @@ internal fun interface JankReporter { * instance and then delegate to another JankReporter instance. */ fun combine(jankReporter: JankReporter): JankReporter { - if (jankReporter == this) { - throw IllegalArgumentException("cannot combine with self") - } + require(jankReporter != this) { "cannot combine with self" } val exec = this::reportSlow return object : JankReporter { override fun reportSlow( diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt index 395ff0706..dca569eec 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt @@ -65,7 +65,6 @@ class JankReporterTest { @Test fun `combine with self fails`() { - val state = StringBuilder("") val reporter = object : JankReporter { override fun reportSlow( From dd6219e79966074d6b2eb6413a3fad14f5221399 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 20 Aug 2025 15:42:06 -0700 Subject: [PATCH 12/18] fix api --- instrumentation/slowrendering/api/slowrendering.api | 1 + .../instrumentation/slowrendering/EventsJankReporter.kt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/instrumentation/slowrendering/api/slowrendering.api b/instrumentation/slowrendering/api/slowrendering.api index 5555326c5..67709ccfe 100644 --- a/instrumentation/slowrendering/api/slowrendering.api +++ b/instrumentation/slowrendering/api/slowrendering.api @@ -1,5 +1,6 @@ public final class io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation : io/opentelemetry/android/instrumentation/AndroidInstrumentation { public fun ()V + public final fun enableDeprecatedZeroDurationSpan ()V 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; diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt index 124a7e8a2..7bdf74a38 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt @@ -14,9 +14,9 @@ import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder import io.opentelemetry.api.logs.Logger // TODO: Replace with semconv constants -val FRAME_COUNT: AttributeKey = AttributeKey.longKey("app.jank.frame_count") -val PERIOD: AttributeKey = AttributeKey.doubleKey("app.jank.period") -val THRESHOLD: AttributeKey = AttributeKey.doubleKey("app.jank.threshold") +internal val FRAME_COUNT: AttributeKey = AttributeKey.longKey("app.jank.frame_count") +internal val PERIOD: AttributeKey = AttributeKey.doubleKey("app.jank.period") +internal val THRESHOLD: AttributeKey = AttributeKey.doubleKey("app.jank.threshold") internal class EventsJankReporter( private val eventLogger: Logger, From 961512be7e62ed339a5cb38549f72b9877254665 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Thu, 21 Aug 2025 13:24:39 -0700 Subject: [PATCH 13/18] fix test --- .../SlowRenderingInstrumentation.kt | 3 +- .../SlowRenderingInstrumentationTest.kt | 29 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt index 5a2aefaa4..da614004c 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt @@ -50,8 +50,9 @@ class SlowRenderingInstrumentation : AndroidInstrumentation { * Reports jank by using a zero-duration span. */ @Deprecated("Use the default event to report jank") - fun enableDeprecatedZeroDurationSpan() { + fun enableDeprecatedZeroDurationSpan(): SlowRenderingInstrumentation { useDeprecatedSpan = true + return this } override fun install(ctx: InstallationContext) { diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentationTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentationTest.kt index 973b3880a..4dda30238 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentationTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentationTest.kt @@ -8,13 +8,17 @@ package io.opentelemetry.android.instrumentation.slowrendering import android.app.Application import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.Called +import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.every +import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import io.mockk.slot import io.mockk.verify import io.opentelemetry.android.instrumentation.InstallationContext +import io.opentelemetry.api.logs.Logger +import io.opentelemetry.api.logs.LoggerProvider import io.opentelemetry.sdk.OpenTelemetrySdk import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -26,14 +30,23 @@ import java.time.Duration @RunWith(AndroidJUnit4::class) class SlowRenderingInstrumentationTest { private lateinit var slowRenderingInstrumentation: SlowRenderingInstrumentation + + @MockK private lateinit var application: Application + + @MockK private lateinit var openTelemetry: OpenTelemetrySdk + @MockK + private lateinit var logger: Logger + @Before fun setUp() { - application = mockk() - openTelemetry = mockk() + MockKAnnotations.init(this) + val logsBridge: LoggerProvider = mockk() slowRenderingInstrumentation = SlowRenderingInstrumentation() + every { openTelemetry.logsBridge } returns logsBridge + every { logsBridge.get("app.jank") } returns logger } @Test @@ -84,6 +97,18 @@ class SlowRenderingInstrumentationTest { val ctx = InstallationContext(application, openTelemetry, mockk()) slowRenderingInstrumentation.install(ctx) + verify { application.registerActivityLifecycleCallbacks(capture(capturedListener)) } + } + + @Config(sdk = [24, 25]) + @Test + fun `can use legacy span`() { + val capturedListener = slot() + every { openTelemetry.getTracer(any()) }.returns(mockk()) + every { application.registerActivityLifecycleCallbacks(any()) } just Runs + val ctx = InstallationContext(application, openTelemetry, mockk()) + slowRenderingInstrumentation.enableDeprecatedZeroDurationSpan().install(ctx) + verify { openTelemetry.getTracer("io.opentelemetry.slow-rendering") } verify { application.registerActivityLifecycleCallbacks(capture(capturedListener)) } } From 32a2d8f31ef347306932f8570ea93789bb40d10b Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Thu, 21 Aug 2025 13:34:47 -0700 Subject: [PATCH 14/18] update api --- instrumentation/slowrendering/api/slowrendering.api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instrumentation/slowrendering/api/slowrendering.api b/instrumentation/slowrendering/api/slowrendering.api index 67709ccfe..a9851ba89 100644 --- a/instrumentation/slowrendering/api/slowrendering.api +++ b/instrumentation/slowrendering/api/slowrendering.api @@ -1,6 +1,6 @@ public final class io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation : io/opentelemetry/android/instrumentation/AndroidInstrumentation { public fun ()V - public final fun enableDeprecatedZeroDurationSpan ()V + public final fun enableDeprecatedZeroDurationSpan ()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; From e1e95cc54b7f7dd35a7869390a19943c082a2484 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Tue, 26 Aug 2025 15:35:04 -0700 Subject: [PATCH 15/18] change to singular to match SpanBasedJankReporter --- .../{EventsJankReporter.kt => EventJankReporter.kt} | 2 +- .../slowrendering/SlowRenderingInstrumentation.kt | 4 ++-- .../instrumentation/slowrendering/EventsJankReporterTest.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/{EventsJankReporter.kt => EventJankReporter.kt} (98%) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt similarity index 98% rename from instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt rename to instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt index 7bdf74a38..cebd28996 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt @@ -18,7 +18,7 @@ internal val FRAME_COUNT: AttributeKey = AttributeKey.longKey("app.jank.fr internal val PERIOD: AttributeKey = AttributeKey.doubleKey("app.jank.period") internal val THRESHOLD: AttributeKey = AttributeKey.doubleKey("app.jank.threshold") -internal class EventsJankReporter( +internal class EventJankReporter( private val eventLogger: Logger, private val threshold: Double, ) : JankReporter { diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt index da614004c..bcc187b5a 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt @@ -72,8 +72,8 @@ class SlowRenderingInstrumentation : AndroidInstrumentation { } val logger = ctx.openTelemetry.logsBridge.get("app.jank") - var jankReporter: JankReporter = EventsJankReporter(logger, SLOW_THRESHOLD_MS / 1000.0) - jankReporter = jankReporter.combine(EventsJankReporter(logger, FROZEN_THRESHOLD_MS / 1000.0)) + var jankReporter: JankReporter = EventJankReporter(logger, SLOW_THRESHOLD_MS / 1000.0) + jankReporter = jankReporter.combine(EventJankReporter(logger, FROZEN_THRESHOLD_MS / 1000.0)) if (useDeprecatedSpan) { val tracer = ctx.openTelemetry.getTracer("io.opentelemetry.slow-rendering") diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt index 6b451dc33..79ec406f1 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt @@ -22,7 +22,7 @@ class EventsJankReporterTest { @Test fun `event is generated`() { val eventLogger = otelTesting.openTelemetry.logsBridge.get("JANK!") - val jankReporter = EventsJankReporter(eventLogger, 0.600) + val jankReporter = EventJankReporter(eventLogger, 0.600) val histogramData: SparseIntArray = mockk() every { histogramData.size() } returns 2 val key1 = 17 From e07eb662fbae512d8a66389eef557324462cc1e7 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 27 Aug 2025 09:44:45 -0700 Subject: [PATCH 16/18] use map instead of SparseIntArray --- .../slowrendering/config/detekt/baseline.xml | 4 ++-- .../slowrendering/EventJankReporter.kt | 9 ++++--- .../slowrendering/JankReporter.kt | 6 ++--- .../slowrendering/PerActivityListener.kt | 13 +++++----- .../slowrendering/SpanBasedJankReporter.kt | 9 ++++--- ...porterTest.kt => EventJankReporterTest.kt} | 16 ++++--------- .../slowrendering/JankReporterTest.kt | 20 +++++----------- .../SpanBasedJankReporterTest.kt | 24 +++++-------------- 8 files changed, 35 insertions(+), 66 deletions(-) rename instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/{EventsJankReporterTest.kt => EventJankReporterTest.kt} (72%) diff --git a/instrumentation/slowrendering/config/detekt/baseline.xml b/instrumentation/slowrendering/config/detekt/baseline.xml index f3cae5b1f..93334d400 100644 --- a/instrumentation/slowrendering/config/detekt/baseline.xml +++ b/instrumentation/slowrendering/config/detekt/baseline.xml @@ -3,8 +3,8 @@ EmptyFunctionBlock:JankReporterTest.kt$JankReporterTest.<no name provided>${ } - ForbiddenComment:EventsJankReporter.kt$// TODO: Replace with semconv constants - MagicNumber:EventsJankReporter.kt$EventsJankReporter$1000.0 + ForbiddenComment:EventJankReporter.kt$// TODO: Replace with semconv constants + MagicNumber:EventJankReporter.kt$EventJankReporter$1000.0 MagicNumber:SlowRenderingInstrumentation.kt$SlowRenderingInstrumentation$1000.0 TooGenericExceptionCaught:SlowRenderListener.kt$SlowRenderListener$e: Exception diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt index cebd28996..31ebad0ec 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt @@ -6,7 +6,6 @@ package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log -import android.util.SparseIntArray import io.opentelemetry.android.common.RumConstants import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes @@ -23,15 +22,15 @@ internal class EventJankReporter( private val threshold: Double, ) : JankReporter { override fun reportSlow( - durationToCountHistogram: SparseIntArray, + durationToCountHistogram: Map, periodSeconds: Double, activityName: String, ) { var frameCount: Long = 0 - for (i in 0 until durationToCountHistogram.size()) { - val durationMillis = durationToCountHistogram.keyAt(i) + for (entry in durationToCountHistogram) { + val durationMillis = entry.key if ((durationMillis / 1000.0) > threshold) { - val count = durationToCountHistogram.get(durationMillis) + val count = entry.value Log.d( RumConstants.OTEL_RUM_LOG_TAG, "* Slow render detected: $durationMillis ms. $count times", diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt index 59350e2a6..33fb24881 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporter.kt @@ -5,15 +5,13 @@ package io.opentelemetry.android.instrumentation.slowrendering -import android.util.SparseIntArray - /** * 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: SparseIntArray, + durationToCountHistogram: Map, periodSeconds: Double, activityName: String, ) @@ -27,7 +25,7 @@ internal fun interface JankReporter { val exec = this::reportSlow return object : JankReporter { override fun reportSlow( - durationToCountHistogram: SparseIntArray, + durationToCountHistogram: Map, periodSeconds: Double, activityName: String, ) { diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt index 5810030e1..04ec4ff5c 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/PerActivityListener.kt @@ -7,7 +7,6 @@ package io.opentelemetry.android.instrumentation.slowrendering import android.app.Activity import android.os.Build -import android.util.SparseIntArray import android.view.FrameMetrics import android.view.Window import android.view.Window.OnFrameMetricsAvailableListener @@ -27,7 +26,7 @@ internal class PerActivityListener( private val lock = Any() @GuardedBy("lock") - private var drawDurationHistogram = SparseIntArray() + private var drawDurationHistogram: MutableMap = HashMap() fun getActivityName(): String = activity.componentName.flattenToShortString() @@ -47,16 +46,16 @@ internal class PerActivityListener( synchronized(lock) { // calculation copied from FrameMetricsAggregator val durationMs = ((drawDurationsNs + NANOS_ROUNDING_VALUE) / NANOS_PER_MS).toInt() - val oldValue = drawDurationHistogram.get(durationMs) - drawDurationHistogram.put(durationMs, (oldValue + 1)) + val oldValue: Int = drawDurationHistogram.getOrDefault(durationMs, 0) + drawDurationHistogram[durationMs] = (oldValue + 1) } } } - fun resetMetrics(): SparseIntArray { + fun resetMetrics(): Map { synchronized(lock) { - val metrics = drawDurationHistogram - drawDurationHistogram = SparseIntArray() + val metrics = HashMap(drawDurationHistogram) + drawDurationHistogram = HashMap() return metrics } } diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt index a3da2fa10..0712ab068 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporter.kt @@ -6,7 +6,6 @@ package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log -import android.util.SparseIntArray import io.opentelemetry.android.common.RumConstants import io.opentelemetry.api.trace.Span import io.opentelemetry.api.trace.Tracer @@ -19,15 +18,15 @@ internal class SpanBasedJankReporter( private val tracer: Tracer, ) : JankReporter { override fun reportSlow( - durationToCountHistogram: SparseIntArray, + durationToCountHistogram: Map, periodSeconds: Double, activityName: String, ) { var slowCount = 0 var frozenCount = 0 - for (i in 0 until durationToCountHistogram.size()) { - val duration = durationToCountHistogram.keyAt(i) - val count = durationToCountHistogram.get(duration) + for (entry in durationToCountHistogram) { + val duration = entry.key + val count = entry.value if (duration > FROZEN_THRESHOLD_MS) { Log.d( RumConstants.OTEL_RUM_LOG_TAG, diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporterTest.kt similarity index 72% rename from instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt rename to instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporterTest.kt index 79ec406f1..5e0f25355 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventsJankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporterTest.kt @@ -6,16 +6,14 @@ package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log -import android.util.SparseIntArray import io.mockk.every -import io.mockk.mockk import io.mockk.mockkStatic import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule import org.assertj.core.api.Assertions.assertThat import org.junit.Rule import org.junit.jupiter.api.Test -class EventsJankReporterTest { +class EventJankReporterTest { @Rule var otelTesting: OpenTelemetryRule = OpenTelemetryRule.create() @@ -23,14 +21,10 @@ class EventsJankReporterTest { fun `event is generated`() { val eventLogger = otelTesting.openTelemetry.logsBridge.get("JANK!") val jankReporter = EventJankReporter(eventLogger, 0.600) - val histogramData: SparseIntArray = mockk() - every { histogramData.size() } returns 2 - val key1 = 17 - val key2 = 701 - every { histogramData.keyAt(0) } returns key1 - every { histogramData.keyAt(1) } returns key2 - every { histogramData.get(key1) } returns 3 - every { histogramData.get(key2) } returns 1 + val histogramData = HashMap() + histogramData[17] = 3 + histogramData[701] = 1 + mockkStatic(Log::class) every { Log.d(any(), any()) } returns 0 diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt index dca569eec..f613de5b5 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/JankReporterTest.kt @@ -5,9 +5,6 @@ package io.opentelemetry.android.instrumentation.slowrendering -import android.util.SparseIntArray -import io.mockk.every -import io.mockk.mockk import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test @@ -19,7 +16,7 @@ class JankReporterTest { val inner = object : JankReporter { override fun reportSlow( - durationToCountHistogram: SparseIntArray, + durationToCountHistogram: Map, periodSeconds: Double, activityName: String, ) { @@ -35,7 +32,7 @@ class JankReporterTest { val outer = object : JankReporter { override fun reportSlow( - durationToCountHistogram: SparseIntArray, + durationToCountHistogram: Map, periodSeconds: Double, activityName: String, ) { @@ -51,15 +48,10 @@ class JankReporterTest { val both = inner.combine(outer) - val histogram = SparseIntArray() - val histogramData: SparseIntArray = mockk() - every { histogramData.size() } returns 1 - every { histogramData.toString() } returns "x" - val key1 = 99 - every { histogramData.keyAt(0) } returns key1 - every { histogramData.get(key1) } returns 37 + val histogram = HashMap() + histogram[99] = 37 both.reportSlow(histogram, 6.9, "four.something") - val expected = ".inner.null.6.9.four.something.outer.null.6.9.four.something" + val expected = ".inner.{99=37}.6.9.four.something.outer.{99=37}.6.9.four.something" assertThat(state.toString()).isEqualTo(expected) } @@ -68,7 +60,7 @@ class JankReporterTest { val reporter = object : JankReporter { override fun reportSlow( - durationToCountHistogram: SparseIntArray, + durationToCountHistogram: Map, periodSeconds: Double, activityName: String, ) { diff --git a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt index acb1b57f8..94336b07b 100644 --- a/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt +++ b/instrumentation/slowrendering/src/test/java/io/opentelemetry/android/instrumentation/slowrendering/SpanBasedJankReporterTest.kt @@ -6,9 +6,7 @@ package io.opentelemetry.android.instrumentation.slowrendering import android.util.Log -import android.util.SparseIntArray import io.mockk.every -import io.mockk.mockk import io.mockk.mockkStatic import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.trace.Tracer @@ -37,14 +35,9 @@ class SpanBasedJankReporterTest { @Test fun `spans are generated`() { val jankReporter = SpanBasedJankReporter(tracer) - val histogramData: SparseIntArray = mockk() - every { histogramData.size() } returns 2 - val key1 = 17 - val key2 = 701 - every { histogramData.keyAt(0) } returns key1 - every { histogramData.keyAt(1) } returns key2 - every { histogramData.get(key1) } returns 3 - every { histogramData.get(key2) } returns 1 + val histogramData = HashMap() + histogramData[17] = 3 + histogramData[701] = 1 mockkStatic(Log::class) every { Log.d(any(), any()) } returns 0 @@ -56,14 +49,9 @@ class SpanBasedJankReporterTest { @Test fun `no spans created when no slow frames`() { val jankReporter = SpanBasedJankReporter(tracer) - val histogramData: SparseIntArray = mockk() - every { histogramData.size() } returns 2 - val key1 = 3 - val key2 = 8 - every { histogramData.keyAt(0) } returns key1 - every { histogramData.keyAt(1) } returns key2 - every { histogramData.get(key1) } returns 3 - every { histogramData.get(key2) } returns 1 + val histogramData = HashMap() + histogramData[3] = 3 + histogramData[8] = 1 mockkStatic(Log::class) every { Log.d(any(), any()) } returns 0 From 6fa166f15c2149f70be1888468c2322941ddd255 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 27 Aug 2025 09:52:45 -0700 Subject: [PATCH 17/18] only log if verbose debug is enabled. --- .../slowrendering/EventJankReporter.kt | 11 +++++++---- .../slowrendering/SlowRenderingInstrumentation.kt | 13 +++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt index 31ebad0ec..bc9497ace 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/EventJankReporter.kt @@ -20,6 +20,7 @@ internal val THRESHOLD: AttributeKey = AttributeKey.doubleKey("app.jank. internal class EventJankReporter( private val eventLogger: Logger, private val threshold: Double, + private val debugVerbose: Boolean = false, ) : JankReporter { override fun reportSlow( durationToCountHistogram: Map, @@ -31,10 +32,12 @@ internal class EventJankReporter( val durationMillis = entry.key if ((durationMillis / 1000.0) > threshold) { val count = entry.value - Log.d( - RumConstants.OTEL_RUM_LOG_TAG, - "* Slow render detected: $durationMillis ms. $count times", - ) + if (debugVerbose) { + Log.d( + RumConstants.OTEL_RUM_LOG_TAG, + "* Slow render detected: $durationMillis ms. $count times", + ) + } frameCount += count } } diff --git a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt index bcc187b5a..573d37d17 100644 --- a/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt +++ b/instrumentation/slowrendering/src/main/java/io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation.kt @@ -19,6 +19,7 @@ import java.time.Duration @AutoService(AndroidInstrumentation::class) class SlowRenderingInstrumentation : AndroidInstrumentation { internal var useDeprecatedSpan: Boolean = false + internal var debugVerbose: Boolean = false internal var slowRenderingDetectionPollInterval: Duration = Duration.ofSeconds(1) @Volatile @@ -46,6 +47,14 @@ class SlowRenderingInstrumentation : AndroidInstrumentation { return this } + /** + * Call this to enable verbose debug logging when slow renders are detected. + */ + fun enableVerboseDebugLogging(): SlowRenderingInstrumentation { + debugVerbose = true + return this + } + /** * Reports jank by using a zero-duration span. */ @@ -72,8 +81,8 @@ class SlowRenderingInstrumentation : AndroidInstrumentation { } val logger = ctx.openTelemetry.logsBridge.get("app.jank") - var jankReporter: JankReporter = EventJankReporter(logger, SLOW_THRESHOLD_MS / 1000.0) - jankReporter = jankReporter.combine(EventJankReporter(logger, FROZEN_THRESHOLD_MS / 1000.0)) + var jankReporter: JankReporter = EventJankReporter(logger, SLOW_THRESHOLD_MS / 1000.0, debugVerbose) + jankReporter = jankReporter.combine(EventJankReporter(logger, FROZEN_THRESHOLD_MS / 1000.0, debugVerbose)) if (useDeprecatedSpan) { val tracer = ctx.openTelemetry.getTracer("io.opentelemetry.slow-rendering") From 5826afc435801c0512b6f99c88186e24f6d4c649 Mon Sep 17 00:00:00 2001 From: Jason Plumb Date: Wed, 27 Aug 2025 10:26:22 -0700 Subject: [PATCH 18/18] add new api --- instrumentation/slowrendering/api/slowrendering.api | 1 + 1 file changed, 1 insertion(+) diff --git a/instrumentation/slowrendering/api/slowrendering.api b/instrumentation/slowrendering/api/slowrendering.api index a9851ba89..f398c6c60 100644 --- a/instrumentation/slowrendering/api/slowrendering.api +++ b/instrumentation/slowrendering/api/slowrendering.api @@ -1,6 +1,7 @@ public final class io/opentelemetry/android/instrumentation/slowrendering/SlowRenderingInstrumentation : io/opentelemetry/android/instrumentation/AndroidInstrumentation { public fun ()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;