mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-09 19:10:42 +00:00
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.
This commit is contained in:
parent
4605ba9124
commit
9ed43b60b6
@ -61,6 +61,7 @@ import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
|
|||||||
import org.mozilla.fenix.library.history.HistoryFragmentDirections
|
import org.mozilla.fenix.library.history.HistoryFragmentDirections
|
||||||
import org.mozilla.fenix.perf.HotStartPerformanceMonitor
|
import org.mozilla.fenix.perf.HotStartPerformanceMonitor
|
||||||
import org.mozilla.fenix.perf.Performance
|
import org.mozilla.fenix.perf.Performance
|
||||||
|
import org.mozilla.fenix.perf.StartupTimeline
|
||||||
import org.mozilla.fenix.search.SearchFragmentDirections
|
import org.mozilla.fenix.search.SearchFragmentDirections
|
||||||
import org.mozilla.fenix.settings.DefaultBrowserSettingsFragmentDirections
|
import org.mozilla.fenix.settings.DefaultBrowserSettingsFragmentDirections
|
||||||
import org.mozilla.fenix.settings.SettingsFragmentDirections
|
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) }
|
externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }
|
||||||
|
|
||||||
Performance.processIntentIfPerformanceTest(intent, this)
|
Performance.processIntentIfPerformanceTest(intent, this)
|
||||||
@ -143,6 +142,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity() {
|
|||||||
supportActionBar?.hide()
|
supportActionBar?.hide()
|
||||||
|
|
||||||
lifecycle.addObserver(webExtensionPopupFeature)
|
lifecycle.addObserver(webExtensionPopupFeature)
|
||||||
|
StartupTimeline.onActivityCreateEndHome(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
|
@ -16,6 +16,7 @@ import org.mozilla.fenix.components.getType
|
|||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.settings
|
import org.mozilla.fenix.ext.settings
|
||||||
|
import org.mozilla.fenix.perf.StartupTimeline
|
||||||
import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor
|
import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,6 +36,8 @@ class IntentReceiverActivity : Activity() {
|
|||||||
intent.stripUnwantedFlags()
|
intent.stripUnwantedFlags()
|
||||||
processIntent(intent)
|
processIntent(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StartupTimeline.onActivityCreateEndIntentReceiver()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun processIntent(intent: Intent) {
|
suspend fun processIntent(intent: Intent) {
|
||||||
|
@ -10,6 +10,7 @@ import androidx.recyclerview.widget.DiffUtil
|
|||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import mozilla.components.feature.top.sites.TopSite
|
import mozilla.components.feature.top.sites.TopSite
|
||||||
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
|
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
|
||||||
|
import org.mozilla.fenix.perf.StartupTimeline
|
||||||
|
|
||||||
class TopSitesAdapter(
|
class TopSitesAdapter(
|
||||||
private val interactor: TopSiteInteractor
|
private val interactor: TopSiteInteractor
|
||||||
@ -21,6 +22,7 @@ class TopSitesAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: TopSiteItemViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: TopSiteItemViewHolder, position: Int) {
|
||||||
|
StartupTimeline.onTopSitesItemBound(holder)
|
||||||
holder.bind(getItem(position))
|
holder.bind(getItem(position))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,12 +8,9 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.os.BatteryManager
|
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 org.mozilla.fenix.onboarding.FenixOnboarding
|
||||||
import android.provider.Settings as AndroidSettings
|
|
||||||
import org.mozilla.fenix.utils.Settings
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
import android.provider.Settings as AndroidSettings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A collection of objects related to app performance.
|
* A collection of objects related to app performance.
|
||||||
@ -22,30 +19,6 @@ object Performance {
|
|||||||
const val TAG = "FenixPerf"
|
const val TAG = "FenixPerf"
|
||||||
private const val EXTRA_IS_PERFORMANCE_TEST = "performancetest"
|
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
|
* Processes intent for Performance testing to remove protection pop up ( but keeps the TP
|
||||||
* on) and removes the onboarding screen.
|
* on) and removes the onboarding screen.
|
||||||
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
47
app/src/main/java/org/mozilla/fenix/perf/StartupTimeline.kt
Normal file
47
app/src/main/java/org/mozilla/fenix/perf/StartupTimeline.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
Block a user