-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add comopnent to create an activity open trace based on a sequence of…
… events
- Loading branch information
1 parent
c1e9a6b
commit c426aab
Showing
4 changed files
with
657 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
...api/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/activity/OpenEvents.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} |
264 changes: 264 additions & 0 deletions
264
...c/main/kotlin/io/embrace/android/embracesdk/internal/capture/activity/OpenTraceEmitter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
Check warning on line 43 in embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/activity/OpenTraceEmitter.kt Codecov / codecov/patchembrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/activity/OpenTraceEmitter.kt#L43
|
||
} | ||
} | ||
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) | ||
Check warning on line 184 in embrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/activity/OpenTraceEmitter.kt Codecov / codecov/patchembrace-android-features/src/main/kotlin/io/embrace/android/embracesdk/internal/capture/activity/OpenTraceEmitter.kt#L184
|
||
} | ||
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 | ||
) | ||
} | ||
} |
Oops, something went wrong.