1
0
Fork 0

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.
master
Michael Comella 2020-02-20 15:15:24 -08:00 committed by Michael Comella
parent 4605ba9124
commit 9ed43b60b6
8 changed files with 241 additions and 30 deletions

View File

@ -61,6 +61,7 @@ import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.perf.HotStartPerformanceMonitor
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchFragmentDirections
import org.mozilla.fenix.settings.DefaultBrowserSettingsFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
@ -125,8 +126,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
}
}
Performance.instrumentColdStartupToHomescreenTime(this)
externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }
Performance.processIntentIfPerformanceTest(intent, this)
@ -143,6 +142,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
supportActionBar?.hide()
lifecycle.addObserver(webExtensionPopupFeature)
StartupTimeline.onActivityCreateEndHome(this)
}
@CallSuper

View File

@ -16,6 +16,7 @@ import org.mozilla.fenix.components.getType
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor
/**
@ -35,6 +36,8 @@ class IntentReceiverActivity : Activity() {
intent.stripUnwantedFlags()
processIntent(intent)
}
StartupTimeline.onActivityCreateEndIntentReceiver()
}
suspend fun processIntent(intent: Intent) {

View File

@ -10,6 +10,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
import org.mozilla.fenix.perf.StartupTimeline
class TopSitesAdapter(
private val interactor: TopSiteInteractor
@ -21,6 +22,7 @@ class TopSitesAdapter(
}
override fun onBindViewHolder(holder: TopSiteItemViewHolder, position: Int) {
StartupTimeline.onTopSitesItemBound(holder)
holder.bind(getItem(position))
}

View File

@ -8,12 +8,9 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import androidx.core.view.doOnPreDraw
import kotlinx.android.synthetic.main.activity_home.*
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.onboarding.FenixOnboarding
import android.provider.Settings as AndroidSettings
import org.mozilla.fenix.utils.Settings
import android.provider.Settings as AndroidSettings
/**
* A collection of objects related to app performance.
@ -22,30 +19,6 @@ object Performance {
const val TAG = "FenixPerf"
private const val EXTRA_IS_PERFORMANCE_TEST = "performancetest"
/**
* Instruments cold startup time for use with our internal measuring system, FNPRMS. This may
* also appear in Google Play Vitals dashboards.
*
* This will need to be rewritten if any parts of the UI are changed to be displayed
* asynchronously.
*
* In the current implementation, we only intend to instrument cold startup to the homescreen.
* To save implementation time, we ignore the fact that the RecyclerView draws twice if the user
* has tabs, collections, etc. open: the "No tabs" placeholder and a tab list. This
* instrumentation will only capture the "No tabs" draw.
*/
fun instrumentColdStartupToHomescreenTime(activity: HomeActivity) {
// 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, it should be comparable
//
// Unfortunately, this is tightly coupled to the root view of HomeActivity's view hierarchy
activity.rootContainer.doOnPreDraw {
activity.reportFullyDrawn()
}
}
/**
* Processes intent for Performance testing to remove protection pop up ( but keeps the TP
* on) and removes the onboarding screen.

View File

@ -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() }
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}
}