From f9b716286a899f7c5fe8cbd1dd58c354596cd202 Mon Sep 17 00:00:00 2001 From: Will Hawkins Date: Thu, 23 Jan 2020 03:24:48 -0500 Subject: [PATCH] For #8034: Create a post-visual completeness executor Create an object that will execute its enqued tasks when Fenix is visually complete. --- .../org/mozilla/fenix/FenixApplication.kt | 5 ++ .../java/org/mozilla/fenix/HomeActivity.kt | 30 ++++++- .../mozilla/fenix/components/Components.kt | 1 + .../fenix/components/PerformanceComponent.kt | 14 ++++ .../mozilla/fenix/perf/StartupTaskManager.kt | 56 +++++++++++++ .../PerformanceActivityLifecycleCallbacks.kt | 79 +++++++++++++++++++ .../fenix/MigratingFenixApplication.kt | 8 ++ 7 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt create mode 100644 app/src/main/java/org/mozilla/fenix/perf/StartupTaskManager.kt create mode 100644 app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index d82233f52..3cc261d96 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -37,6 +37,7 @@ import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.ext.settings import org.mozilla.fenix.session.NotificationSessionObserver +import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks import org.mozilla.fenix.session.VisibilityLifecycleCallback import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.Settings @@ -143,6 +144,10 @@ open class FenixApplication : LocaleAwareApplication() { // if ((System.currentTimeMillis() - settings().lastPlacesStorageMaintenance) > ONE_DAY_MILLIS) { // runStorageMaintenance() // } + + registerActivityLifecycleCallbacks( + PerformanceActivityLifecycleCallbacks(components.performance.visualCompletenessTaskManager) + ) } // See https://github.com/mozilla-mobile/fenix/issues/7227 for context. diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index d473a77ea..f6f954a61 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -15,13 +15,14 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PROTECTED import androidx.appcompat.app.ActionBar import androidx.appcompat.widget.Toolbar +import androidx.core.view.doOnPreDraw import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDestination import androidx.navigation.NavDirections import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI -import kotlinx.android.synthetic.main.activity_home.navigationToolbarStub +import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import mozilla.components.browser.search.SearchEngine @@ -69,6 +70,7 @@ import org.mozilla.fenix.settings.logins.SavedLoginsFragmentDirections import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.BrowsersCache +import org.mozilla.fenix.utils.StartupTaskManager @SuppressWarnings("TooManyFunctions", "LargeClass") open class HomeActivity : LocaleAwareAppCompatActivity() { @@ -77,6 +79,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { lateinit var themeManager: ThemeManager lateinit var browsingModeManager: BrowsingModeManager + private var isVisuallyComplete = false + + private var visualCompletenessTaskManager: StartupTaskManager? = null + private var sessionObserver: SessionManager.Observer? = null private val hotStartMonitor = HotStartPerformanceMonitor() @@ -107,6 +113,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent)) setContentView(R.layout.activity_home) + + // Must be after we set the content view + if (isVisuallyComplete) { + rootContainer.doOnPreDraw { + // This delay is temporary. We are delaying 5 seconds until the performance + // team can locate the real point of visual completeness. + it.postDelayed({ + visualCompletenessTaskManager!!.start() + }, delay) + } + } + Performance.instrumentColdStartupToHomescreenTime(this) externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) } @@ -390,6 +408,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { navHost.navController.navigate(action) } + /** + * The root container is null at this point, so let the HomeActivity know that + * we are visually complete. + */ + fun postVisualCompletenessQueue(visualCompletenessTaskManager: StartupTaskManager) { + isVisuallyComplete = true + this.visualCompletenessTaskManager = visualCompletenessTaskManager + } + companion object { const val OPEN_TO_BROWSER = "open_to_browser" const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load" @@ -397,5 +424,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { const val PRIVATE_BROWSING_MODE = "private_browsing_mode" const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open" const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open" + const val delay = 5000L } } diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index 78db4a40f..cc56661d0 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -78,4 +78,5 @@ class Components(private val context: Context) { val publicSuffixList by lazy { PublicSuffixList(context) } val clipboardHandler by lazy { ClipboardHandler(context) } val migrationStore by lazy { MigrationStore() } + val performance by lazy { PerformanceComponent() } } diff --git a/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt b/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt new file mode 100644 index 000000000..c1bc24003 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt @@ -0,0 +1,14 @@ +/* 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.components + +import org.mozilla.fenix.utils.StartupTaskManager + +/** + * Component group for all functionality related to performance. + */ +class PerformanceComponent { + val visualCompletenessTaskManager by lazy { StartupTaskManager() } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupTaskManager.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupTaskManager.kt new file mode 100644 index 000000000..271edfc67 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupTaskManager.kt @@ -0,0 +1,56 @@ +/* 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.utils + +import org.mozilla.gecko.util.ThreadUtils +import java.lang.IllegalStateException + +typealias StartupTask = () -> Unit + +/** + * A queue of tasks that are performed at specific points during Fenix startup. + * + * This queue contains a list of startup tasks. Each task in the queue will be started once Fenix + * is visually complete. + * + * This class is not thread safe and should only be called from the main thread. + */ +class StartupTaskManager { + private var tasks = mutableListOf() + private var hasStarted = false + private set + + /** + * Add a task to the queue. + * Each task will execute on the main thread. + * + * @param task: The task to add to the queue. + */ + @Synchronized + fun add(task: StartupTask) { + ThreadUtils.assertOnUiThread() + if (hasStarted) { + throw IllegalStateException("New tasks should not be added because queue already " + + "started, and these newly added tasks will not execute.") + } + + tasks.add(task) + } + + /** + * Start all tasks in the queue. When all the tasks have been started, + * clear the queue. + */ + fun start() { + ThreadUtils.assertOnUiThread() + hasStarted = true + + tasks.forEach { it.invoke() } + + // Anything captured by the lambda will remain captured if we hold on to these tasks, + // which takes up more memory than we need to. + tasks.clear() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt b/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt new file mode 100644 index 000000000..a60a9f65a --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt @@ -0,0 +1,79 @@ +/* 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.session + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.IntentReceiverActivity +import org.mozilla.fenix.browser.BrowserPerformanceTestActivity +import org.mozilla.fenix.settings.account.AuthIntentReceiverActivity +import org.mozilla.fenix.utils.StartupTaskManager +import org.mozilla.fenix.widget.VoiceSearchActivity + +/** + * These callbacks handle binding performance code to the activity lifecycle. + */ +@SuppressWarnings("EmptyFunctionBlock") +class PerformanceActivityLifecycleCallbacks( + private val visualCompletenessTaskManager: StartupTaskManager +) : Application.ActivityLifecycleCallbacks { + + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + ifNecessaryPostVisualCompleteness(activity) + } + + /** + * Returns true if it is a terminal activity, or false if it + * an activity used in transition. + */ + private fun isTransientActivity(activity: Activity): Boolean { + // These are the current list of non terminal activites. + // They have been whitelisted in case new activities are added to the application + // to ensure these new activities would not crash the application. + return isTransientActivityInMigrationVariant(activity) || + (activity is IntentReceiverActivity) || + (activity is VoiceSearchActivity) || + (activity is AuthIntentReceiverActivity) || + (activity is BrowserPerformanceTestActivity) + } + + /** + * This starts the StartupTaskManager, eitther delayed or right away, if the activity is a + * terminal activity. If not, do nothing. + */ + private fun ifNecessaryPostVisualCompleteness(activity: Activity) { + fun shouldStartVisualCompletenessQueueImmediately(): Boolean { + return !isTransientActivity(activity) + } + + if (activity is HomeActivity) { + // We should delay the visualCompletenessTaskManager when reaching the HomeActivity + // to ensure all tasks are delayed until after visual completeness + activity.postVisualCompletenessQueue(visualCompletenessTaskManager) + } else if (shouldStartVisualCompletenessQueueImmediately()) { + // If we do not go through the home activity, we have to start the tasks + // immediately to avoid spending time implementing it. + visualCompletenessTaskManager.start() + } + } + + override fun onActivityStarted(activity: Activity?) {} + override fun onActivityStopped(activity: Activity?) {} + override fun onActivityResumed(activity: Activity?) {} + override fun onActivityPaused(activity: Activity?) {} + override fun onActivitySaveInstanceState(activity: Activity?, bundle: Bundle?) {} + override fun onActivityDestroyed(activity: Activity?) {} + + companion object { + /** + * The source files are different from migration and non migration variants. + * We use this property to extend the [isTransientActivity] implementation for the + * migration variants. + */ + var isTransientActivityInMigrationVariant: (Activity) -> Boolean = { false } + } +} diff --git a/app/src/migration/java/org/mozilla/fenix/MigratingFenixApplication.kt b/app/src/migration/java/org/mozilla/fenix/MigratingFenixApplication.kt index 8ff88f9dd..84cc6d88d 100644 --- a/app/src/migration/java/org/mozilla/fenix/MigratingFenixApplication.kt +++ b/app/src/migration/java/org/mozilla/fenix/MigratingFenixApplication.kt @@ -7,11 +7,19 @@ package org.mozilla.fenix import android.content.Context import kotlinx.coroutines.runBlocking import mozilla.components.support.migration.FennecMigrator +import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks /** * An application class which knows how to migrate Fennec data. */ class MigratingFenixApplication : FenixApplication() { + + init { + PerformanceActivityLifecycleCallbacks.isTransientActivityInMigrationVariant = { + if (it is MigrationDecisionActivity) true else false + } + } + val migrator by lazy { FennecMigrator.Builder(this, this.components.analytics.crashReporter) .migrateOpenTabs(this.components.core.sessionManager)