For #7781: instrument visual completeness for top sites.
Eyeballing my output in *Debug builds on my P2, this adds approximately 115ms or slightly less from first frame drawn to visually complete time.fennec/beta
parent
4605ba9124
commit
9ed43b60b6
@ -0,0 +1,69 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.perf
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.View
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import kotlinx.android.synthetic.main.activity_home.*
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSiteItemViewHolder
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.APP_LINK
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.HOMESCREEN
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupState
|
||||
|
||||
/**
|
||||
* Instruments the Android framework method [Activity.reportFullyDrawn], which prints time to visual
|
||||
* completeness to logcat.
|
||||
*
|
||||
* At the time of writing (2020-02-26), this functionality is tightly coupled to FNPRMS, our internal
|
||||
* startup measurement system. However, these values may also appear in the Google Play Vitals
|
||||
* dashboards.
|
||||
*/
|
||||
class StartupReportFullyDrawn {
|
||||
|
||||
// Ideally we'd incorporate this state into the StartupState but we're short on implementation time.
|
||||
private var isInstrumented = false
|
||||
|
||||
/**
|
||||
* Instruments "visually complete" cold startup time for app link for use with FNPRMS.
|
||||
*/
|
||||
fun onActivityCreateEndHome(state: StartupState, activity: HomeActivity) {
|
||||
if (!isInstrumented &&
|
||||
state is StartupState.Cold && state.destination == APP_LINK) {
|
||||
// Instrumenting the first frame drawn should be good enough for app link for now.
|
||||
isInstrumented = true
|
||||
attachReportFullyDrawn(activity, activity.rootContainer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instruments "visually complete" cold startup time to homescreen for use with FNPRMS.
|
||||
*
|
||||
* For FNPRMS, we define "visually complete" to be when top sites is loaded with placeholders;
|
||||
* the animation to display top sites will occur after this point, as will the asynchronous
|
||||
* loading of the actual top sites icons. Our focus for visually complete is usability.
|
||||
* There are no tabs available in our FNPRMS tests so they are ignored for this instrumentation.
|
||||
*/
|
||||
fun onTopSitesItemBound(state: StartupState, holder: TopSiteItemViewHolder) {
|
||||
if (!isInstrumented &&
|
||||
state is StartupState.Cold && state.destination == HOMESCREEN) {
|
||||
isInstrumented = true
|
||||
|
||||
// Ideally we wouldn't cast to HomeActivity but we want to save implementation time.
|
||||
val view = holder.itemView
|
||||
attachReportFullyDrawn(view.context as HomeActivity, view)
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachReportFullyDrawn(activity: HomeActivity, view: View) {
|
||||
// For greater accuracy, we could add an onDrawListener instead of a preDrawListener but:
|
||||
// - single use onDrawListeners are not built-in and it's non-trivial to write one
|
||||
// - the difference in timing is minimal (< 7ms on Pixel 2)
|
||||
// - if we compare against another app using a preDrawListener, as we are with Fennec, it
|
||||
// should be comparable
|
||||
view.doOnPreDraw { activity.reportFullyDrawn() }
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.perf
|
||||
|
||||
import androidx.annotation.UiThread
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSiteItemViewHolder
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupActivity
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupState
|
||||
|
||||
/**
|
||||
* A collection of functionality to instrument, measure, and understand startup performance. The
|
||||
* responsibilities of this class are to update the internal [StartupState] based on the methods
|
||||
* called and to delegate calls to its dependencies, which handle other functionality related to
|
||||
* understanding startup.
|
||||
*
|
||||
* This class, and its dependencies, may need to be modified for any changes in startup.
|
||||
*
|
||||
* This class is not thread safe and should only be called from the main thread.
|
||||
*/
|
||||
@UiThread
|
||||
object StartupTimeline {
|
||||
|
||||
private var state: StartupState = StartupState.Cold(StartupDestination.UNKNOWN)
|
||||
private val reportFullyDrawn = StartupReportFullyDrawn()
|
||||
|
||||
fun onActivityCreateEndIntentReceiver() {
|
||||
advanceState(StartupActivity.INTENT_RECEIVER)
|
||||
}
|
||||
|
||||
fun onActivityCreateEndHome(activity: HomeActivity) {
|
||||
advanceState(StartupActivity.HOME)
|
||||
reportFullyDrawn.onActivityCreateEndHome(state, activity)
|
||||
}
|
||||
|
||||
fun onTopSitesItemBound(holder: TopSiteItemViewHolder) {
|
||||
// no advanceState associated with this method.
|
||||
reportFullyDrawn.onTopSitesItemBound(state, holder)
|
||||
}
|
||||
|
||||
private fun advanceState(startingActivity: StartupActivity) {
|
||||
state = StartupTimelineStateMachine.getNextState(state, startingActivity)
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.perf
|
||||
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.APP_LINK
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.HOMESCREEN
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.UNKNOWN
|
||||
|
||||
/**
|
||||
* A state machine representing application startup for use with [StartupTimeline]. Android
|
||||
* application startup is complex so it's helpful to make all of our expected states explicit, e.g.
|
||||
* with a state machine, which helps check our assumptions. Unfortunately, because this state machine
|
||||
* is not used by the framework to determine possible startup scenarios, this is duplicating the
|
||||
* startup logic and is thus extremely fragile (especially because most devs won't know about this
|
||||
* class when they change the startup flow!). We may be able to mitigate this with assertions.
|
||||
*
|
||||
* To devs changing this class: by design as a state machine, this class should never hold any state
|
||||
* and should be 100% unit tested to validate assumptions.
|
||||
*/
|
||||
object StartupTimelineStateMachine {
|
||||
|
||||
/**
|
||||
* The states the application passes through during startup. We define these states to help us
|
||||
* better understand Android startup. Note that these states are not 100% correlated to the
|
||||
* cold/warm/hot states Google Play Vitals uses.
|
||||
*
|
||||
* TODO: link to extensive documentation on cold/warm/hot states when completed.
|
||||
*/
|
||||
sealed class StartupState {
|
||||
/** The state when the application is starting up but is not in memory. */
|
||||
data class Cold(val destination: StartupDestination) : StartupState()
|
||||
}
|
||||
|
||||
/**
|
||||
* The final screen the user will see during startup.
|
||||
*/
|
||||
enum class StartupDestination {
|
||||
HOMESCREEN,
|
||||
APP_LINK,
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of Activities supported by the app.
|
||||
*/
|
||||
enum class StartupActivity {
|
||||
HOME,
|
||||
INTENT_RECEIVER,
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the current state and any arguments, returns the next state of the state machine.
|
||||
*/
|
||||
fun getNextState(currentState: StartupState, startingActivity: StartupActivity): StartupState {
|
||||
return when (currentState) {
|
||||
is StartupState.Cold -> nextStateIsCold(currentState, startingActivity)
|
||||
}
|
||||
}
|
||||
|
||||
private fun nextStateIsCold(currentState: StartupState.Cold, startingActivity: StartupActivity): StartupState {
|
||||
return when (currentState.destination) {
|
||||
UNKNOWN -> when (startingActivity) {
|
||||
StartupActivity.HOME -> StartupState.Cold(HOMESCREEN)
|
||||
StartupActivity.INTENT_RECEIVER -> StartupState.Cold(APP_LINK)
|
||||
}
|
||||
|
||||
// We haven't defined the state machine after these states yet so we return the current state.
|
||||
else -> currentState
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.perf
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupActivity
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.APP_LINK
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.HOMESCREEN
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupDestination.UNKNOWN
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.StartupState.Cold
|
||||
import org.mozilla.fenix.perf.StartupTimelineStateMachine.getNextState
|
||||
|
||||
class StartupTimelineStateMachineTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN state cold-unknown WHEN home activity is first shown THEN we are in cold-homescreen state`() {
|
||||
val actual = getNextState(Cold(UNKNOWN), StartupActivity.HOME)
|
||||
assertEquals(Cold(HOMESCREEN), actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN state cold-unknown WHEN intent receiver activity is first shown THEN we are in cold-app-link state`() {
|
||||
val actual = getNextState(Cold(UNKNOWN), StartupActivity.INTENT_RECEIVER)
|
||||
assertEquals(Cold(APP_LINK), actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN state cold + known destination WHEN any activity is passed in THEN we remain in the same state`() {
|
||||
val knownDestinations = StartupDestination.values().filter { it != UNKNOWN }
|
||||
val allActivities = StartupActivity.values()
|
||||
|
||||
knownDestinations.forEach { destination ->
|
||||
val initial = Cold(destination)
|
||||
allActivities.forEach { activity ->
|
||||
val actual = getNextState(initial, activity)
|
||||
assertEquals("$destination $activity", initial, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue