diff --git a/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt b/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt index 7df918cb1a..3575a7101b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt @@ -25,6 +25,7 @@ private const val EXPECTED_RUNBLOCKING_COUNT = 2 private const val EXPECTED_COMPONENT_INIT_COUNT = 42 private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12 private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4 +private const val EXPECTED_NUMBER_OF_INFLATION = 12 private val failureMsgStrictMode = getErrorMessage( shortName = "StrictMode suppression", @@ -54,6 +55,11 @@ private val failureMsgRecyclerViewConstraintLayoutChildren = getErrorMessage( ) + "Please note that we're not sure if this is a useful metric to assert: with your feedback, " + "we'll find out over time if it is or is not." +private val failureMsgNumberOfInflation = getErrorMessage( + shortName = "Number of inflation on start up doesn't match expected count", + implications = "The number of inflation can negatively impact start up time. Having more inflations" + + "will most likely mean we're adding extra work on the UI thread." +) /** * A performance test to limit the number of StrictMode suppressions and number of runBlocking used * on startup. @@ -90,6 +96,8 @@ class StartupExcessiveResourceUseTest { val actualViewHierarchyDepth = countAndLogViewHierarchyDepth(rootView, 1) val actualRecyclerViewConstraintLayoutChildren = countRecyclerViewConstraintLayoutChildren(rootView, null) + val actualNumberOfInflations = InflationCounter.inflationCount.get() + assertEquals(failureMsgStrictMode, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount) assertEquals(failureMsgRunBlocking, EXPECTED_RUNBLOCKING_COUNT, actualRunBlocking) assertEquals(failureMsgComponentInit, EXPECTED_COMPONENT_INIT_COUNT, actualComponentInitCount) @@ -99,6 +107,7 @@ class StartupExcessiveResourceUseTest { EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN, actualRecyclerViewConstraintLayoutChildren ) + assertEquals(failureMsgNumberOfInflation, EXPECTED_NUMBER_OF_INFLATION, actualNumberOfInflations) } } diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 5ca572000e..64cfb77401 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -14,6 +14,7 @@ import android.os.SystemClock import android.text.format.DateUtils import android.util.AttributeSet import android.view.KeyEvent +import android.view.LayoutInflater import android.view.View import android.view.ViewConfiguration import android.view.WindowManager @@ -89,6 +90,7 @@ import org.mozilla.fenix.library.bookmarks.DesktopFolders import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.perf.Performance +import org.mozilla.fenix.perf.PerformanceInflater import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.search.SearchDialogFragmentDirections import org.mozilla.fenix.session.PrivateNotificationService @@ -138,6 +140,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { WebExtensionPopupFeature(components.core.store, ::openPopup) } + private var inflater: LayoutInflater? = null + private val navHost by lazy { supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment } @@ -824,6 +828,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } } + override fun getSystemService(name: String): Any? { + if (LAYOUT_INFLATER_SERVICE == name) { + if (inflater == null) { + inflater = PerformanceInflater(LayoutInflater.from(baseContext), this) + } + return inflater + } + return super.getSystemService(name) + } + protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager { return DefaultBrowsingModeManager(initialMode, components.settings) { newMode -> themeManager.currentTheme = newMode diff --git a/app/src/main/java/org/mozilla/fenix/perf/PerformanceInflater.kt b/app/src/main/java/org/mozilla/fenix/perf/PerformanceInflater.kt new file mode 100644 index 0000000000..e34d7a8d01 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/PerformanceInflater.kt @@ -0,0 +1,76 @@ +/* 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.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import org.mozilla.fenix.ext.getAndIncrementNoOverflow +import java.lang.reflect.Modifier.PRIVATE +import java.util.concurrent.atomic.AtomicInteger + +private val classPrefixList = arrayOf( + "android.widget.", + "android.webkit.", + "android.app." +) +/** + * Counts the number of inflations fenix does. This class behaves only as an inflation counter since + * it takes the `inflater` that is given by the base system. This is done in order not to change + * the behavior of the app since all we want to do is count the inflations done. + * + */ +open class PerformanceInflater( + inflater: LayoutInflater, + context: Context +) : LayoutInflater( + inflater, + context +) { + + override fun cloneInContext(newContext: Context?): LayoutInflater { + return PerformanceInflater(this, newContext!!) + } + + override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View { + InflationCounter.inflationCount.getAndIncrementNoOverflow() + return super.inflate(resource, root, attachToRoot) + } + + /** + * This code was taken from the PhoneLayoutInflater.java located in the android source code + * (Similarly, AsyncLayoutInflater implements it the exact same way too which can be found in the + * Android Framework). This piece of code was taken from the other inflaters implemented by Android + * since we do not want to change the inflater behavior except to count the number of inflations + * that our app is doing for performance purposes. Looking at the `super.OnCreateView(name, attrs)`, + * it hardcodes the prefix as "android.view." this means that a xml element such as + * ImageButton will crash the app using android.view.ImageButton. This method only works with + * XML tag that contains no prefix. This means that views such as androidx.recyclerview... will not + * work with this method. + */ + @Suppress("EmptyCatchBlock") + @Throws(ClassNotFoundException::class) + override fun onCreateView(name: String?, attrs: AttributeSet?): View? { + for (prefix in classPrefixList) { + try { + val view = createView(name, prefix, attrs) + if (view != null) { + return view + } + } catch (e: ClassNotFoundException) { + // We want the super class to inflate if ever the view can't be inflated here + } + } + return super.onCreateView(name, attrs) + } +} + +@VisibleForTesting(otherwise = PRIVATE) +object InflationCounter { + val inflationCount = AtomicInteger(0) +} diff --git a/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt b/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt new file mode 100644 index 0000000000..59698c7831 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt @@ -0,0 +1,84 @@ +/* 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.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.FrameLayout +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import java.io.File + +@RunWith(FenixRobolectricTestRunner::class) +class PerformanceInflaterTest { + + private lateinit var perfInflater: MockInflater + + private val layoutsNotToTest = setOf( + "fragment_browser", + "fragment_add_on_internal_settings" + ) + + @Before + fun setup() { + InflationCounter.inflationCount.set(0) + + perfInflater = MockInflater(LayoutInflater.from(testContext), testContext) + } + + @Test + fun `WHEN we inflate a view,THEN the inflation counter should increase`() { + assertEquals(0, InflationCounter.inflationCount.get()) + perfInflater.inflate(R.layout.fragment_home, null, false) + assertEquals(1, InflationCounter.inflationCount.get()) + } + + @Test + fun `WHEN inflating one of our resource file, the inflater should not crash`() { + val fileList = File("./src/main/res/layout").listFiles() + for (file in fileList!!) { + val layoutName = file.name.split(".")[0] + val layoutId = testContext.resources.getIdentifier( + layoutName, + "layout", + testContext.packageName + ) + + assertNotEquals(-1, layoutId) + if (!layoutsNotToTest.contains(layoutName)) { + perfInflater.inflate(layoutId, FrameLayout(testContext), true) + } + } + } +} + +private class MockInflater( + inflater: LayoutInflater, + context: Context +) : PerformanceInflater( + inflater, + context +) { + + override fun onCreateView(name: String?, attrs: AttributeSet?): View? { + // We skip the fragment layout for the simple reason that it implements + // a whole different inflate which is implemented in the activity.LayoutFactory + // methods. To be able to properly test it here, we would have to copy the whole + // inflater file (or create an activity) and pass our layout through the onCreateView + // method of that activity. + if (name!!.contains("fragment")) { + return FrameLayout(testContext) + } + return super.onCreateView(name, attrs) + } +}