blob: bfc1d3a378710a2d707e4ce11553a801563dc065 [file] [log] [blame]
package com.android.launcher3.logging
import android.os.SystemClock
import android.util.Log
import android.util.SparseLongArray
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
import androidx.core.util.contains
import androidx.core.util.isEmpty
import com.android.launcher3.BuildConfig
import com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent
import com.android.launcher3.util.Preconditions
/** Logger for logging Launcher activity's startup latency. */
open class ColdRebootStartupLatencyLogger : StartupLatencyLogger {
companion object {
const val TAG = "ColdRebootStartupLatencyLogger"
const val UNSET_INT = -1
const val UNSET_LONG = -1L
}
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
val startTimeByEvent = SparseLongArray()
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
val endTimeByEvent = SparseLongArray()
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) var cardinality: Int = UNSET_INT
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
var workspaceLoadStartTime: Long = UNSET_LONG
// ColdRebootStartupLatencyLogger should only send launcher startup logs once in each launcher
// activity lifecycle. After launcher activity startup is completed, the logger should be torn
// down and reject all logging calls. This flag should be checked at all APIs to prevent logging
// invalid startup metrics (such as loading workspace in screen rotation).
var isTornDown = false
private var isInTest = false
/** Subclass can override this method to handle collected latency metrics. */
@MainThread
override fun log(): ColdRebootStartupLatencyLogger {
return this
}
@MainThread
override fun logWorkspaceLoadStartTime() =
logWorkspaceLoadStartTime(SystemClock.elapsedRealtime())
@VisibleForTesting
@MainThread
fun logWorkspaceLoadStartTime(startTimeMs: Long): ColdRebootStartupLatencyLogger {
Preconditions.assertUIThread()
if (isTornDown) {
return this
}
workspaceLoadStartTime = startTimeMs
return this
}
/**
* Log size of workspace. Larger number of workspace items (icons, folders, widgets) means
* longer latency to initialize workspace.
*/
@MainThread
override fun logCardinality(cardinality: Int): ColdRebootStartupLatencyLogger {
Preconditions.assertUIThread()
if (isTornDown) {
return this
}
this.cardinality = cardinality
return this
}
@MainThread
override fun logStart(event: LauncherLatencyEvent) =
logStart(event, SystemClock.elapsedRealtime())
@MainThread
override fun logStart(
event: LauncherLatencyEvent,
startTimeMs: Long
): ColdRebootStartupLatencyLogger {
// In unit test no looper is attached to current thread
Preconditions.assertUIThread()
if (isTornDown) {
return this
}
if (validateLoggingEventAtStart(event)) {
startTimeByEvent.put(event.id, startTimeMs)
}
return this
}
@MainThread
override fun logEnd(event: LauncherLatencyEvent) = logEnd(event, SystemClock.elapsedRealtime())
@MainThread
override fun logEnd(
event: LauncherLatencyEvent,
endTimeMs: Long
): ColdRebootStartupLatencyLogger {
// In unit test no looper is attached to current thread
Preconditions.assertUIThread()
if (isTornDown) {
return this
}
maybeLogStartOfWorkspaceLoadTime(event)
if (validateLoggingEventAtEnd(event)) {
endTimeByEvent.put(event.id, endTimeMs)
}
return this
}
@MainThread
override fun reset() {
// In unit test no looper is attached to current thread
Preconditions.assertUIThread()
startTimeByEvent.clear()
endTimeByEvent.clear()
cardinality = UNSET_INT
workspaceLoadStartTime = UNSET_LONG
isTornDown = true
}
@MainThread
private fun maybeLogStartOfWorkspaceLoadTime(event: LauncherLatencyEvent) {
if (workspaceLoadStartTime == UNSET_LONG) {
return
}
if (event == LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC) {
logStart(event, workspaceLoadStartTime)
workspaceLoadStartTime = UNSET_LONG
}
}
/** @return true if we can log start of [LauncherLatencyEvent] and vice versa. */
@MainThread
private fun validateLoggingEventAtStart(event: LauncherLatencyEvent): Boolean {
if (!BuildConfig.IS_STUDIO_BUILD && !isInTest) {
return true
}
if (startTimeByEvent.contains(event.id)) {
Log.e(TAG, "Cannot restart same ${event.name} event")
return false
} else if (
startTimeByEvent.isEmpty() &&
event != LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION
) {
Log.e(
TAG,
"The first log start event must be " +
"${LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.name}.",
)
return false
}
return true
}
/** @return true if we can log end of [LauncherLatencyEvent] and vice versa. */
@MainThread
private fun validateLoggingEventAtEnd(event: LauncherLatencyEvent): Boolean {
if (!BuildConfig.IS_STUDIO_BUILD && !isInTest) {
return true
}
if (!startTimeByEvent.contains(event.id)) {
Log.e(TAG, "Cannot end ${event.name} event before starting it")
return false
} else if (endTimeByEvent.contains(event.id)) {
Log.e(TAG, "Cannot end same ${event.name} event again")
return false
} else if (
event != LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION &&
endTimeByEvent.contains(
LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.id
)
) {
Log.e(
TAG,
"Cannot end ${event.name} event after ${LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION.name}",
)
return false
}
return true
}
@VisibleForTesting
fun setIsInTest() {
isInTest = true
}
}