Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions common/api/common.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public final class io/opentelemetry/android/common/RumConstants {
public static final field OTEL_RUM_LOG_TAG Ljava/lang/String;
public static final field RUM_SDK_VERSION Lio/opentelemetry/api/common/AttributeKey;
public static final field SCREEN_NAME_KEY Lio/opentelemetry/api/common/AttributeKey;
public static final field SCREEN_VIEW_DEPTH_KEY Lio/opentelemetry/api/common/AttributeKey;
public static final field SCREEN_VIEW_NODES_KEY Lio/opentelemetry/api/common/AttributeKey;
public static final field START_TYPE_KEY Lio/opentelemetry/api/common/AttributeKey;
public static final field STORAGE_SPACE_FREE_KEY Lio/opentelemetry/api/common/AttributeKey;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ object RumConstants {
@JvmField
val BATTERY_PERCENT_KEY: AttributeKey<Double> = AttributeKey.doubleKey("battery.percent")

@JvmField
val SCREEN_VIEW_NODES_KEY: AttributeKey<Long> = AttributeKey.longKey("screen.view.nodes")

@JvmField
val SCREEN_VIEW_DEPTH_KEY: AttributeKey<Long> = AttributeKey.longKey("screen.view.depth")

const val APP_START_SPAN_NAME: String = "AppStart"

object Events {
Expand Down
14 changes: 14 additions & 0 deletions instrumentation/activity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ This instrumentation produces the following telemetry:
* `screen.name`: name of screen
* `last.screen.name`: name of screen, only when span contains the `activityPostResumed` event.

### First Draw

* Type: Span
* Name: `FirstDraw`
* Description: On activity PreCreated or Created (pre API 29) callback, a span will be created
to represent the time that the UI pipeline took to render that Activity's first frame. The span ends
at the first draw of the window
[DecorView](https://developer.android.com/reference/android/view/Window#getDecorView())
* Attributes:
* `activity.name`: name of activity
* `screen.name`: name of screen
* `screen.view.nodes`: a recursive count of all View nodes under the window DecorView. This gives a measure of the amount of work being done in the *layout-and-measure* stage of the UI pipeline. A high count correlates with longer FirstDraw. System-defined layers are included in this count.
* `screen.view.depth`: the depth of the deepest-nested View under the window DecorView. This gives a measure of your UI complexity; more work must be done for a deeply-nested View heirarchy. A high count correlates with longer FirstDraw. System-defined layers (usually 4-6) are included when calculating the depth.

## Installation

This instrumentation comes with the [android agent](../../android-agent) out of the box, so
Expand Down
1 change: 1 addition & 0 deletions instrumentation/activity/api/activity.api
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class io/opentelemetry/android/instrumentation/activity/ActivityTracer {
public class io/opentelemetry/android/instrumentation/activity/ActivityTracerCache {
public fun <init> (Lio/opentelemetry/api/trace/Tracer;Lio/opentelemetry/android/internal/services/visiblescreen/VisibleScreenTracker;Lio/opentelemetry/android/instrumentation/activity/startup/AppStartupTimer;Lio/opentelemetry/android/instrumentation/common/ScreenNameExtractor;)V
public fun addEvent (Landroid/app/Activity;Ljava/lang/String;)Lio/opentelemetry/android/instrumentation/activity/ActivityTracer;
public fun endInitialDrawSpan (Landroid/app/Activity;Landroid/view/View;)V
public fun initiateRestartSpanIfNecessary (Landroid/app/Activity;)Lio/opentelemetry/android/instrumentation/activity/ActivityTracer;
public fun startActivityCreation (Landroid/app/Activity;)Lio/opentelemetry/android/instrumentation/activity/ActivityTracer;
public fun startSpanIfNoneInProgress (Landroid/app/Activity;Ljava/lang/String;)Lio/opentelemetry/android/instrumentation/activity/ActivityTracer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package io.opentelemetry.android.instrumentation.activity

import android.app.Activity
import android.os.Bundle
import io.opentelemetry.android.instrumentation.activity.draw.FirstDrawListener
import io.opentelemetry.android.internal.services.visiblescreen.activities.DefaultingActivityLifecycleCallbacks

class ActivityCallbacks(
Expand All @@ -17,6 +18,9 @@ class ActivityCallbacks(
savedInstanceState: Bundle?,
) {
tracers.startActivityCreation(activity).addEvent("activityPreCreated")
FirstDrawListener.registerFirstDraw(activity) { view ->
tracers.endInitialDrawSpan(activity, view)
}
}

override fun onActivityCreated(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@
import static io.opentelemetry.android.common.RumConstants.START_TYPE_KEY;

import android.app.Activity;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.opentelemetry.android.common.RumConstants;
import io.opentelemetry.android.instrumentation.activity.startup.AppStartupTimer;
import io.opentelemetry.android.instrumentation.common.ActiveSpan;
import io.opentelemetry.android.instrumentation.common.ViewUtilsKt;
import io.opentelemetry.android.internal.services.visiblescreen.VisibleScreenTracker;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Context;
import java.util.concurrent.atomic.AtomicReference;
import kotlin.Pair;

public class ActivityTracer {
static final AttributeKey<String> ACTIVITY_NAME_KEY = AttributeKey.stringKey("activity.name");
Expand All @@ -31,6 +35,7 @@ public class ActivityTracer {
private final String screenName;
private final AppStartupTimer appStartupTimer;
private final ActiveSpan activeSpan;
@Nullable private Span initialDrawSpan;

private ActivityTracer(Builder builder) {
this.initialAppActivity = builder.initialAppActivity;
Expand All @@ -50,7 +55,9 @@ ActivityTracer startSpanIfNoneInProgress(String spanName) {
}

ActivityTracer startActivityCreation() {
activeSpan.startSpan(this::makeCreationSpan);
Span creationSpan = makeCreationSpan();
activeSpan.startSpan(() -> creationSpan);
startInitialDrawSpan(creationSpan);
return this;
}

Expand Down Expand Up @@ -135,6 +142,24 @@ public ActivityTracer addEvent(String eventName) {
return this;
}

void startInitialDrawSpan(Span parentSpan) {
if (initialDrawSpan == null) {
initialDrawSpan = createSpanWithParent("FirstDraw", parentSpan);
}
}

void endInitialDrawSpan(View view) {
if (initialDrawSpan != null) {
Pair<Integer, Integer> complexity = ViewUtilsKt.getComplexity(view);
initialDrawSpan.setAttribute(
RumConstants.SCREEN_VIEW_NODES_KEY, complexity.component1().longValue());
initialDrawSpan.setAttribute(
RumConstants.SCREEN_VIEW_DEPTH_KEY, complexity.component2().longValue());
initialDrawSpan.end();
initialDrawSpan = null;
}
}

static Builder builder(Activity activity) {
return new Builder(activity);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package io.opentelemetry.android.instrumentation.activity;

import android.app.Activity;
import android.view.View;
import androidx.annotation.VisibleForTesting;
import io.opentelemetry.android.instrumentation.activity.startup.AppStartupTimer;
import io.opentelemetry.android.instrumentation.common.ScreenNameExtractor;
Expand Down Expand Up @@ -79,6 +80,10 @@ public ActivityTracer startActivityCreation(Activity activity) {
return getTracer(activity).startActivityCreation();
}

public void endInitialDrawSpan(Activity activity, View view) {
getTracer(activity).endInitialDrawSpan(view);
}

private ActivityTracer getTracer(Activity activity) {
ActivityTracer activityTracer =
tracersByActivityClassName.get(activity.getClass().getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.opentelemetry.android.instrumentation.activity.draw.FirstDrawListener;
import io.opentelemetry.android.internal.services.visiblescreen.activities.DefaultingActivityLifecycleCallbacks;

public class Pre29ActivityCallbacks implements DefaultingActivityLifecycleCallbacks {
Expand All @@ -21,6 +22,12 @@ public Pre29ActivityCallbacks(ActivityTracerCache tracers) {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
tracers.startActivityCreation(activity).addEvent("activityCreated");
FirstDrawListener.INSTANCE.registerFirstDraw(
activity,
view -> {
tracers.endInitialDrawSpan(activity, view);
return null;
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.instrumentation.activity.draw

import android.app.Activity
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewTreeObserver

internal object FirstDrawListener {
fun registerFirstDraw(
activity: Activity,
drawDoneCallback: (View) -> Unit,
) {
val window = activity.window

// Wait until the decorView is created until we actually use it
window.onDecorViewReady {
val decorView = window.peekDecorView()
val versionsBeforeBugFix = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
val decorViewAttached = decorView.viewTreeObserver.isAlive && decorView.isAttachedToWindow

// Before API version 26, draw listeners were not merged back into the real view tree observer
// Workaround is to wait until the view is attached before registering draw listeners
// Source: https://android.googlesource.com/platform/frameworks/base/+/9f8ec54244a5e0343b9748db3329733f259604f3
if (versionsBeforeBugFix && !decorViewAttached) {
decorView.addOnAttachStateChangeListener(
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
decorView.viewTreeObserver.addOnDrawListener(NextDrawListener(decorView, drawDoneCallback))
decorView.removeOnAttachStateChangeListener(this)
}

override fun onViewDetachedFromWindow(v: View) = Unit
},
)
} else {
decorView.viewTreeObserver.addOnDrawListener(NextDrawListener(decorView, drawDoneCallback))
}
}
}

/**
* ViewTreeObserver.removeOnDrawListener() cannot be called from the onDraw() callback,
* so remove it in next draw.
*/
internal class NextDrawListener(
val view: View,
val drawDoneCallback: (View) -> Unit,
val handler: Handler = Handler(Looper.getMainLooper()),
) : ViewTreeObserver.OnDrawListener {
var invoked = false

override fun onDraw() {
if (!invoked) {
invoked = true
drawDoneCallback(view)
handler.post {
if (view.viewTreeObserver.isAlive) {
view.viewTreeObserver.removeOnDrawListener(this)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.android.instrumentation.activity.draw

import android.os.Build
import android.view.ActionMode
import android.view.SearchEvent
import android.view.Window
import androidx.annotation.RequiresApi

internal fun Window.onDecorViewReady(callback: () -> Unit) {
if (peekDecorView() == null) {
onContentChanged {
callback()
return@onContentChanged false
}
} else {
callback()
}
}

internal fun Window.onContentChanged(callbackInvocation: () -> Boolean) {
val currentCallback = callback
val callback =
if (currentCallback is WindowDelegateCallback) {
currentCallback
} else {
val newCallback = WindowDelegateCallback(currentCallback)
callback = newCallback
newCallback
}
callback.onContentChangedCallbacks += callbackInvocation
}

internal class WindowDelegateCallback(
private val delegate: Window.Callback,
) : Window.Callback by delegate {
val onContentChangedCallbacks = mutableListOf<() -> Boolean>()

override fun onContentChanged() {
onContentChangedCallbacks.removeAll { callback ->
!callback()
}
delegate.onContentChanged()
}

@RequiresApi(Build.VERSION_CODES.M)
override fun onSearchRequested(searchEvent: SearchEvent): Boolean = delegate.onSearchRequested(searchEvent)

@RequiresApi(Build.VERSION_CODES.M)
override fun onWindowStartingActionMode(
callback: ActionMode.Callback,
type: Int,
): ActionMode? = delegate.onWindowStartingActionMode(callback, type)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ package io.opentelemetry.android.instrumentation.activity
import android.app.Activity
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.opentelemetry.android.common.RumConstants
import io.opentelemetry.android.instrumentation.activity.draw.FirstDrawListener
import io.opentelemetry.android.instrumentation.activity.startup.AppStartupTimer
import io.opentelemetry.android.instrumentation.common.ScreenNameExtractor
import io.opentelemetry.android.internal.services.visiblescreen.VisibleScreenTracker
Expand Down Expand Up @@ -40,6 +42,9 @@ internal class ActivityCallbacksTest {
val extractor = mockk<ScreenNameExtractor>(relaxed = true)
every { extractor.extract(any<Activity>()) } returns "Activity"
tracers = ActivityTracerCache(tracer, visibleScreenTracker, startupTimer, extractor)

mockkObject(FirstDrawListener)
every { FirstDrawListener.registerFirstDraw(any(), any()) } returns Unit
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,15 @@ internal class ActivityTracerCacheTest {
assertSame(activityTracer, result)
Mockito.verify(activityTracer).startActivityCreation()
}

@Test
fun endInitialDrawSpan() {
val view = Mockito.mock(android.view.View::class.java)
Mockito.`when`(tracerCreator.apply(activity)).thenReturn(activityTracer)

val underTest = ActivityTracerCache(tracerCreator)

underTest.endInitialDrawSpan(activity, view)
Mockito.verify(activityTracer).endInitialDrawSpan(view)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ package io.opentelemetry.android.instrumentation.activity
import android.app.Activity
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.opentelemetry.android.common.RumConstants
import io.opentelemetry.android.instrumentation.activity.draw.FirstDrawListener
import io.opentelemetry.android.instrumentation.activity.startup.AppStartupTimer
import io.opentelemetry.android.instrumentation.common.ScreenNameExtractor
import io.opentelemetry.android.internal.services.visiblescreen.VisibleScreenTracker
Expand Down Expand Up @@ -39,6 +41,9 @@ internal class Pre29ActivityLifecycleCallbacksTest {
val extractor = mockk<ScreenNameExtractor>(relaxed = true)
every { extractor.extract(any<Activity>()) } returns "Activity"
tracers = ActivityTracerCache(tracer, visibleScreenTracker, appStartupTimer, extractor)

mockkObject(FirstDrawListener)
every { FirstDrawListener.registerFirstDraw(any(), any()) } returns Unit
}

@Test
Expand Down
Loading