Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;)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) {
tracers.endInitialDrawSpan(activity)
}
}

override fun onActivityCreated(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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 +51,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 +138,19 @@ public ActivityTracer addEvent(String eventName) {
return this;
}

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

void endInitialDrawSpan() {
if (initialDrawSpan != null) {
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 @@ -79,6 +79,10 @@ public ActivityTracer startActivityCreation(Activity activity) {
return getTracer(activity).startActivityCreation();
}

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

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,
() -> {
tracers.endInitialDrawSpan(activity);
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: () -> 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: () -> Unit,
val handler: Handler = Handler(Looper.getMainLooper()),
) : ViewTreeObserver.OnDrawListener {
var invoked = false

override fun onDraw() {
if (!invoked) {
invoked = true
drawDoneCallback()
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,14 @@ internal class ActivityTracerCacheTest {
assertSame(activityTracer, result)
Mockito.verify(activityTracer).startActivityCreation()
}

@Test
fun endInitialDrawSpan() {
Mockito.`when`(tracerCreator.apply(activity)).thenReturn(activityTracer)

val underTest = ActivityTracerCache(tracerCreator)

underTest.endInitialDrawSpan(activity)
Mockito.verify(activityTracer).endInitialDrawSpan()
}
}
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
Loading