Skip to content

Commit

Permalink
Add option to manually end UI load trace (#1776)
Browse files Browse the repository at this point in the history
## Goal

Add in the ability at the `UiLoadTraceEmitter` level to designate a UI Load trace to be ended manually. This has not been hooked up with the API yet so it cannot be triggered externally yet.
  • Loading branch information
bidetofevil authored Dec 20, 2024
2 parents eac60be + a0bde6d commit 9e471a8
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ interface UiLoadEventListener {
* When the given UI instance is starting to be created.
*
* For an Activity, it means it has entered the CREATED state of its lifecycle.
*
* Set [manualEnd] to true to signal that the load of this UI instance will be ended manually by calling [complete]
*/
fun create(instanceId: Int, activityName: String, timestampMs: Long)
fun create(instanceId: Int, activityName: String, timestampMs: Long, manualEnd: Boolean)

/**
* When the given UI instance has been fully created and is ready to be displayed on screen.
Expand All @@ -24,8 +26,10 @@ interface UiLoadEventListener {
* When the given UI instance is starting to be displayed on screen
*
* For an Activity, it means it is about to enter the STARTED state of its lifecycle.
*
* Set [manualEnd] to true to signal that the load of this UI instance will be ended manually by calling [complete]
*/
fun start(instanceId: Int, activityName: String, timestampMs: Long)
fun start(instanceId: Int, activityName: String, timestampMs: Long, manualEnd: Boolean)

/**
* When the given UI instance is displayed on screen and its views are ready to be rendered
Expand Down Expand Up @@ -62,6 +66,12 @@ interface UiLoadEventListener {
*/
fun renderEnd(instanceId: Int, timestampMs: Long)

/**
* When the app manually signals that the load of the given UI instance is complete. This will only be respected
* if the load is expected to be ended manually.
*/
fun complete(instanceId: Int, timestampMs: Long)

/**
* When we no longer wish to observe the loading of the given UI instance. This may be called during its load
* or after it has loaded. Calls to this for a given instance should be idempotent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ private class UiLoadEventEmitter(
uiLoadEventListener.create(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs()
timestampMs = nowMs(),
manualEnd = false,
)
}

Expand All @@ -163,7 +164,8 @@ private class UiLoadEventEmitter(
uiLoadEventListener.start(
instanceId = traceInstanceId(activity),
activityName = activity.localClassName,
timestampMs = nowMs()
timestampMs = nowMs(),
manualEnd = false,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ class UiLoadTraceEmitter(
private val traceZygoteHolder: AtomicReference<UiLoadTraceZygote> = AtomicReference(INITIAL)
private var currentTracedInstanceId: Int? = null

override fun create(instanceId: Int, activityName: String, timestampMs: Long) {
override fun create(instanceId: Int, activityName: String, timestampMs: Long, manualEnd: Boolean) {
startTrace(
uiLoadType = UiLoadType.COLD,
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
timestampMs = timestampMs,
manualEnd = manualEnd
)
startChildSpan(
instanceId = instanceId,
Expand All @@ -76,12 +77,13 @@ class UiLoadTraceEmitter(
)
}

override fun start(instanceId: Int, activityName: String, timestampMs: Long) {
override fun start(instanceId: Int, activityName: String, timestampMs: Long, manualEnd: Boolean) {
startTrace(
uiLoadType = UiLoadType.HOT,
instanceId = instanceId,
activityName = activityName,
timestampMs = timestampMs
timestampMs = timestampMs,
manualEnd = manualEnd
)
startChildSpan(
instanceId = instanceId,
Expand All @@ -99,12 +101,12 @@ class UiLoadTraceEmitter(
}

override fun resume(instanceId: Int, activityName: String, timestampMs: Long) {
if (!hasRenderEvent()) {
if (traceCompleteTrigger(instanceId) == TraceCompleteTrigger.RESUME) {
endTrace(
instanceId = instanceId,
timestampMs = timestampMs,
)
} else {
} else if (hasRenderEvent()) {
startChildSpan(
instanceId = instanceId,
timestampMs = timestampMs,
Expand Down Expand Up @@ -136,16 +138,32 @@ class UiLoadTraceEmitter(
timestampMs = timestampMs,
lifecycleStage = LifecycleStage.RENDER
)
endTrace(
instanceId = instanceId,
timestampMs = timestampMs,
)

if (traceCompleteTrigger(instanceId) == TraceCompleteTrigger.RENDER) {
endTrace(
instanceId = instanceId,
timestampMs = timestampMs,
)
}
}

override fun complete(instanceId: Int, timestampMs: Long) {
if (traceCompleteTrigger(instanceId) == TraceCompleteTrigger.MANUAL) {
endTrace(
instanceId = instanceId,
timestampMs = timestampMs,
)
}
}

override fun abandon(instanceId: Int, activityName: String, timestampMs: Long) {
currentTracedInstanceId?.let { currentlyTracedInstanceId ->
if (instanceId != currentlyTracedInstanceId) {
endTrace(instanceId = currentlyTracedInstanceId, timestampMs = timestampMs, errorCode = ErrorCode.USER_ABANDON)
endTrace(
instanceId = currentlyTracedInstanceId,
timestampMs = timestampMs,
errorCode = ErrorCode.USER_ABANDON
)
}
}
traceZygoteHolder.set(
Expand All @@ -167,7 +185,8 @@ class UiLoadTraceEmitter(
uiLoadType: UiLoadType,
instanceId: Int,
activityName: String,
timestampMs: Long
timestampMs: Long,
manualEnd: Boolean,
) {
if (traceZygoteHolder.get() == INITIAL) {
return
Expand All @@ -189,11 +208,25 @@ class UiLoadTraceEmitter(
if (zygote.lastActivityInstanceId != -1) {
root.addSystemAttribute("last_activity", zygote.lastActivityName)
}
activeTraces[instanceId] = UiLoadTrace(root = root, activityName = activityName)
activeTraces[instanceId] = UiLoadTrace(
root = root,
traceCompleteTrigger = determineEndEvent(manualEnd),
activityName = activityName
)
}
}
}

private fun determineEndEvent(manualEnd: Boolean): TraceCompleteTrigger {
return if (manualEnd) {
TraceCompleteTrigger.MANUAL
} else if (hasRenderEvent()) {
TraceCompleteTrigger.RENDER
} else {
TraceCompleteTrigger.RESUME
}
}

private fun endTrace(instanceId: Int, timestampMs: Long, errorCode: ErrorCode? = null) {
activeTraces[instanceId]?.let { trace ->
with(trace) {
Expand Down Expand Up @@ -228,19 +261,29 @@ class UiLoadTraceEmitter(
}
}

private fun traceCompleteTrigger(instanceId: Int): TraceCompleteTrigger? = activeTraces[instanceId]?.traceCompleteTrigger

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

private fun traceName(
activityName: String,
uiLoadType: UiLoadType
uiLoadType: UiLoadType,
): String = "$activityName-${uiLoadType.typeName}-time-to-initial-display"

/**
* Metadata for the trace recorded for a particular instance of UI Load
*/
private data class UiLoadTrace(
val activityName: String,
val traceCompleteTrigger: TraceCompleteTrigger,
val root: PersistableEmbraceSpan,
val children: Map<LifecycleStage, PersistableEmbraceSpan> = ConcurrentHashMap(),
)

/**
* Metadata for the conditions of the app prior to the start of a UI Load trace. This is used to determine
* when the newly created trace started.
*/
private data class UiLoadTraceZygote(
val lastActivityName: String,
val lastActivityInstanceId: Int,
Expand All @@ -251,18 +294,38 @@ class UiLoadTraceEmitter(
const val INVALID_INSTANCE: Int = -1
const val INVALID_TIME: Long = -1L

/**
* The trigger to end a particular UI Load trace.
*/
enum class TraceCompleteTrigger {
RESUME,
RENDER,
MANUAL,
}

/**
* Presents the state of the app before the first before any UI is displayed. No UI Load traces should be logged
* when the app is in this state. Any piece of UI loaded should contribute to the app launch trace.
*/
val INITIAL = UiLoadTraceZygote(
lastActivityName = "NEW_APP_LAUNCH",
lastActivityInstanceId = INVALID_INSTANCE,
lastActivityPausedTimeMs = INVALID_TIME
)

/**
* The app is in the foreground and ready to log UI Load traces for any configured components.
*/
val READY = UiLoadTraceZygote(
lastActivityName = "READY",
lastActivityInstanceId = INVALID_INSTANCE,
lastActivityPausedTimeMs = INVALID_TIME
)

/**
* The app is backgrounded, so the next trace would be recording the foregrounding of the app, so the trace
* start time should take that into account.
*/
val BACKGROUNDED = UiLoadTraceZygote(
lastActivityName = "BACKGROUNDED",
lastActivityInstanceId = INVALID_INSTANCE,
Expand Down
Loading

0 comments on commit 9e471a8

Please sign in to comment.