Skip to content

Commit

Permalink
Add comopnent to create an activity open trace based on a sequence of…
Browse files Browse the repository at this point in the history
… events
  • Loading branch information
bidetofevil committed Sep 13, 2024
1 parent aa495a3 commit 7e1c048
Show file tree
Hide file tree
Showing 4 changed files with 657 additions and 0 deletions.
21 changes: 21 additions & 0 deletions embrace-android-api/api/embrace-android-api.api
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,27 @@ public abstract interface class io/embrace/android/embracesdk/internal/api/UserA
public abstract fun setUsername (Ljava/lang/String;)V
}

public abstract interface class io/embrace/android/embracesdk/internal/capture/activity/OpenEvents {
public abstract fun create (ILjava/lang/String;J)V
public abstract fun createEnd (IJ)V
public abstract fun hibernate (ILjava/lang/String;J)V
public abstract fun render (ILjava/lang/String;J)V
public abstract fun renderEnd (IJ)V
public abstract fun resetTrace (ILjava/lang/String;J)V
public abstract fun resume (ILjava/lang/String;J)V
public abstract fun resumeEnd (IJ)V
public abstract fun start (ILjava/lang/String;J)V
public abstract fun startEnd (IJ)V
}

public final class io/embrace/android/embracesdk/internal/capture/activity/OpenEvents$OpenType : java/lang/Enum {
public static final field COLD Lio/embrace/android/embracesdk/internal/capture/activity/OpenEvents$OpenType;
public static final field HOT Lio/embrace/android/embracesdk/internal/capture/activity/OpenEvents$OpenType;
public final fun getTypeName ()Ljava/lang/String;
public static fun valueOf (Ljava/lang/String;)Lio/embrace/android/embracesdk/internal/capture/activity/OpenEvents$OpenType;
public static fun values ()[Lio/embrace/android/embracesdk/internal/capture/activity/OpenEvents$OpenType;
}

public final class io/embrace/android/embracesdk/internal/network/http/NetworkCaptureData {
public fun <init> (Ljava/util/Map;Ljava/lang/String;[BLjava/util/Map;[BLjava/lang/String;)V
public synthetic fun <init> (Ljava/util/Map;Ljava/lang/String;[BLjava/util/Map;[BLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.embrace.android.embracesdk.internal.capture.activity

/**
* The relevant stages in the lifecycle of Activities pertaining to observing the performance of their loading
*/
public interface OpenEvents {

/**
* When a previously in-progress Activity Open trace should be abandoned, and that the component managing
* the trace recording should prepare itself to start tracing the opening of a new Activity instance.
*/
public fun resetTrace(instanceId: Int, activityName: String, timestampMs: Long)

/**
* When the app is no longer in a state where it is trying to open up a new Activity
*/
public fun hibernate(instanceId: Int, activityName: String, timestampMs: Long)

/**
* When the given Activity is entering the CREATE stage of its lifecycle.
*/
public fun create(instanceId: Int, activityName: String, timestampMs: Long)

/**
* When the given Activity has exited the CREATE stage of its lifecycle.
*/
public fun createEnd(instanceId: Int, timestampMs: Long)

/**
* When the given Activity is entering the START stage of its lifecycle.
*/
public fun start(instanceId: Int, activityName: String, timestampMs: Long)

/**
* When the given Activity has exited the START stage of its lifecycle.
*/
public fun startEnd(instanceId: Int, timestampMs: Long)

/**
* When the given Activity is entering the RESUME stage of its lifecycle.
*/
public fun resume(instanceId: Int, activityName: String, timestampMs: Long)

/**
* When the given Activity has exited the RESUME stage of its lifecycle.
*/
public fun resumeEnd(instanceId: Int, timestampMs: Long)

/**
* When the given Activity's first UI frame starts to be rendered.
*/
public fun render(instanceId: Int, activityName: String, timestampMs: Long)

/**
* When the given Activity's first UI frame has been displayed.
*/
public fun renderEnd(instanceId: Int, timestampMs: Long)

public enum class OpenType(public val typeName: String) {
COLD("cold"), HOT("hot")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
package io.embrace.android.embracesdk.internal.capture.activity

import android.app.Application.ActivityLifecycleCallbacks
import android.os.Build
import io.embrace.android.embracesdk.internal.arch.schema.EmbType
import io.embrace.android.embracesdk.internal.spans.PersistableEmbraceSpan
import io.embrace.android.embracesdk.internal.spans.SpanService
import io.embrace.android.embracesdk.internal.utils.VersionChecker
import io.embrace.android.embracesdk.spans.ErrorCode
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReference

/**
* Observes Activity lifecycle and rendering events to create traces that model the workflow for showing an Activity on screen.
* Depending on the version of Android and the state of the app, the start, end, and intermediate stages of the workflow will use
* timestamps from different events, which affects to preciseness of the measurement.
*
* Trace Start:
*
* - On Android 10+, when [ActivityLifecycleCallbacks.onActivityPostPaused] is fired, denoting that the previous activity has completed
* its [ActivityLifecycleCallbacks.onActivityPaused] callbacks and a new Activity is ready to be created.
* - Android 9 and lower, when [ActivityLifecycleCallbacks.onActivityPaused] is fired, denoting that the previous activity is in the
* process of exiting. This will possibly result in some cleanup work of exiting the previous activity being included in the duration
* of the next trace that is logged.
*
* Trace End:
*
* - Android 10+, when the Activity's first UI frame finishes rendering and is delivered to the screen
* - Android 9 and lower.... TODO
*/
public class OpenTraceEmitter(
private val spanService: SpanService,
private val versionChecker: VersionChecker,
) : OpenEvents {

private val activeTraces: MutableMap<Int, ActivityOpenTrace> = ConcurrentHashMap()
private val traceIds: MutableMap<String, Int> = ConcurrentHashMap()
private val traceZygoteHolder: AtomicReference<OpenTraceZygote> = AtomicReference(INITIAL)

override fun resetTrace(instanceId: Int, activityName: String, timestampMs: Long) {
traceIds[activityName]?.let { existingTraceId ->
if (instanceId != existingTraceId) {
endTrace(instanceId = existingTraceId, timestampMs = timestampMs, errorCode = ErrorCode.USER_ABANDON)
}
}
traceZygoteHolder.set(
OpenTraceZygote(
lastActivityName = activityName,
lastActivityInstanceId = instanceId,
lastActivityPausedTimeMs = timestampMs
)
)
}

override fun hibernate(instanceId: Int, activityName: String, timestampMs: Long) {
if (traceZygoteHolder.get().lastActivityInstanceId == instanceId) {
traceZygoteHolder.set(BACKGROUNDED)
}
}

override fun create(instanceId: Int, activityName: String, timestampMs: Long) {
startTrace(
openType = OpenEvents.OpenType.COLD,
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
)
startChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
lifecycleEvent = LifecycleEvent.CREATE
)
}

override fun createEnd(instanceId: Int, timestampMs: Long) {
endChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
lifecycleEvent = LifecycleEvent.CREATE
)
}

override fun start(instanceId: Int, activityName: String, timestampMs: Long) {
startTrace(
openType = OpenEvents.OpenType.HOT,
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
)
startChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
lifecycleEvent = LifecycleEvent.START
)
}

override fun startEnd(instanceId: Int, timestampMs: Long) {
endChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
lifecycleEvent = LifecycleEvent.START
)
}

override fun resume(instanceId: Int, activityName: String, timestampMs: Long) {
if (!hasRenderEvent()) {
endTrace(
instanceId = instanceId,
timestampMs = timestampMs,
)
} else {
startChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
lifecycleEvent = LifecycleEvent.RESUME
)
}
traceZygoteHolder.set(READY)
}

override fun resumeEnd(instanceId: Int, timestampMs: Long) {
endChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
lifecycleEvent = LifecycleEvent.RESUME
)
}

override fun render(instanceId: Int, activityName: String, timestampMs: Long) {
startChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
lifecycleEvent = LifecycleEvent.RENDER
)
}

override fun renderEnd(instanceId: Int, timestampMs: Long) {
endChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
lifecycleEvent = LifecycleEvent.RENDER
)
endTrace(
instanceId = instanceId,
timestampMs = timestampMs,
)
}

private fun startTrace(
openType: OpenEvents.OpenType,
instanceId: Int,
activityName: String,
timestampMs: Long
) {
if (traceZygoteHolder.get() == INITIAL) {
return
}

if (!activeTraces.containsKey(instanceId)) {
val zygote = traceZygoteHolder.getAndSet(READY)
val startTimeMs = if (zygote.lastActivityPausedTimeMs != -1L) {
zygote.lastActivityPausedTimeMs
} else {
timestampMs
}

spanService.startSpan(
name = traceName(activityName, openType),
type = EmbType.Performance.ActivityOpen,
startTimeMs = startTimeMs,
)?.let { root ->
if (zygote.lastActivityInstanceId != -1) {
root.addSystemAttribute("last_activity", zygote.lastActivityName)
}
activeTraces[instanceId] = ActivityOpenTrace(root = root, activityName = activityName)
}
}
}

private fun endTrace(instanceId: Int, timestampMs: Long, errorCode: ErrorCode? = null) {
activeTraces[instanceId]?.let { trace ->
with(trace) {
children.values.filter { it.isRecording }.forEach { span ->
span.stop(endTimeMs = timestampMs, errorCode = errorCode)
}
root.stop(endTimeMs = timestampMs, errorCode = errorCode)
}
activeTraces.remove(instanceId)
}
}

private fun startChildSpan(instanceId: Int, timestampMs: Long, lifecycleEvent: LifecycleEvent) {
val trace = activeTraces[instanceId]
if (trace != null && !trace.children.containsKey(lifecycleEvent)) {
spanService.startSpan(
name = lifecycleEvent.spanName(trace.activityName),
parent = trace.root,
startTimeMs = timestampMs,
)?.let { newSpan ->
val newChildren = trace.children.plus(lifecycleEvent to newSpan)
activeTraces[instanceId] = trace.copy(
children = newChildren
)
}
}
}

private fun endChildSpan(instanceId: Int, timestampMs: Long, lifecycleEvent: LifecycleEvent) {
activeTraces[instanceId]?.let { trace ->
trace.children[lifecycleEvent]?.stop(timestampMs)
}
}

private fun hasRenderEvent(): Boolean = versionChecker.isAtLeast(Build.VERSION_CODES.Q)

private fun traceName(
activityName: String,
openType: OpenEvents.OpenType
): String = "$activityName-${openType.typeName}-open"

public enum class LifecycleEvent(private val typeName: String) {
CREATE("create"),
START("start"),
RESUME("resume"),
RENDER("render");

public fun spanName(activityName: String): String = "$activityName-$typeName"
}

private data class ActivityOpenTrace(
val activityName: String,
val root: PersistableEmbraceSpan,
val children: Map<LifecycleEvent, PersistableEmbraceSpan> = ConcurrentHashMap(),
)

private data class OpenTraceZygote(
val lastActivityName: String,
val lastActivityInstanceId: Int,
val lastActivityPausedTimeMs: Long,
)

private companion object {
const val INVALID_INSTANCE: Int = -1
const val INVALID_TIME: Long = -1L

val INITIAL = OpenTraceZygote(
lastActivityName = "NEW_APP_LAUNCH",
lastActivityInstanceId = INVALID_INSTANCE,
lastActivityPausedTimeMs = INVALID_TIME
)

val READY = OpenTraceZygote(
lastActivityName = "READY",
lastActivityInstanceId = INVALID_INSTANCE,
lastActivityPausedTimeMs = INVALID_TIME
)

val BACKGROUNDED = OpenTraceZygote(
lastActivityName = "BACKGROUNDED",
lastActivityInstanceId = INVALID_INSTANCE,
lastActivityPausedTimeMs = INVALID_TIME
)
}
}
Loading

0 comments on commit 7e1c048

Please sign in to comment.