You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUse...

160 lines
8.4 KiB
Kotlin

/* 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.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.HomeActivityTestRule
// BEFORE CHANGING EXPECTED_* VALUES, PLEASE READ THE TEST CLASS KDOC.
/**
* The number of times a StrictMode violation is suppressed during this start up scenario.
* Incrementing the expected value indicates a potential performance regression.
*
* One feature of StrictMode is to detect potential performance regressions and, in particular, to
* detect main thread IO. This includes network requests (which can block for multiple seconds),
* file read/writes (which generally block for tens to hundreds of milliseconds), and file stats
* (like most SharedPreferences accesses, which block for small amounts of time). Main thread IO
* should be replaced with a background operation that posts to the main thread when the IO request
* is complete.
*
* Say no to main thread IO! 🙅
*/
private const val EXPECTED_SUPPRESSION_COUNT = 19
/**
* The number of times we call the `runBlocking` coroutine method on the main thread during this
* start up scenario. Increment the expected values indicates a potential performance regression.
*
* runBlocking indicates that we're blocking the current thread waiting for the result of another
* coroutine. While the main thread is blocked, 1) we can't handle user input and the user may feel
* Firefox is slow and 2) we can't use the main thread to continue initialization that must occur on
* the main thread (like initializing UI), slowing down start up overall. Blocking calls should
* generally be replaced with a slow operation on a background thread launching onto the main thread
* when completed. However, in a very small number of cases, blocking may be impossible to avoid.
*/
private val EXPECTED_RUNBLOCKING_RANGE = 0..2 // CI has +1 counts compared to local runs: increment these together
/**
* The number of `ConstraintLayout`s we inflate that are children of a `RecyclerView` during this
* start up scenario. Incrementing the expected value indicates a potential performance regression.
* THIS IS AN EXPERIMENTAL METRIC and we are not yet confident reducing this count will mitigate
* start up regressions. If you do not find it useful or if it's too noisy, you can consider
* removing it.
*
* ConstraintLayout is expensive to inflate (though fast to measure/layout) so we want to avoid
* creating too many of them synchronously during start up. Generally, these should be inflated
* asynchronously or replaced with cheaper layouts (if they're not too expensive to measure/layout).
* If the view hierarchy uses Jetpack Compose, switching to that is also an option.
*/
private val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN =
4..6 // The messaging framework is not deterministic and could add to the count.
/**
* The number of layouts we inflate during this start up scenario. Incrementing the expected value
* indicates a potential performance regression. THIS IS AN EXPERIMENTAL METRIC and we are not yet
* confident reducing this count will mitigate start up regressions. If you do not find it useful or
* if it's too noisy, you can consider removing it.
*
* Each layout inflation is suspected of having overhead (e.g. accessing each layout resource from
* disk) so suspect inflating more layouts may slow down start up. Ideally, layouts would be merged
* such that there is one inflation that includes all of the views needed on start up.
*/
private val EXPECTED_NUMBER_OF_INFLATION =
13..14 // The messaging framework is not deterministic and could add a +1 to the count
private val failureMsgStrictMode = getErrorMessage("StrictMode suppression")
private val failureMsgRunBlocking = getErrorMessage("runBlockingIncrement")
private val failureMsgRecyclerViewConstraintLayoutChildren = getErrorMessage(
"ConstraintLayout being a common direct descendant of a RecyclerView",
16373 Count the # of inflations done on startup (#16778) * For #16373: Added performance Inflater to counter # of inflations This class is quite straight forward. The only thing that I have to point out is the onCreateView method. It usually calls its super if you don't override it. The problem with that is that the super.onCreateView actually uses android.view. as a prefix for the XML element it tries to inflate. So if we have an element that isn't part of that package, it'll crash. As I said in the code, a good example is ImageButton. Calling android.view.ImageButton will make the app crash. The method is implemented the same way that PhoneLayoutInflater does (Another example is the AsyncLayoutInflater) * For #16373: Added test for PerformanceInflater This test got quite awkward / complicated fast. I wanted to test the to make sure we don't break *any* of our layouts and to do so, I decided to just retrieve all our XML in our /res/layout folder. However, this gets quite a bit outside of a unit test scope. The point was to get every layouts and get their LayoutID through the resources using the testContext we have. It gets even weirder, since some of the XML tags have special implementation in android. One of them is the <fragment> tag. That tag actually is inflated by the OS using the Factory2 that the Activity.java implements. In order to get around the fragment issue, we just return a basic FrameLayout since the system LayoutInflater doesn't deal won't ever get a <fragment> tag to inflate. Another issue was the <merge> tag. In order to inflate those, you need 1) a root view and 2) attach your view to it. In order to be able to test those layouts file, I had to create an empty FrameLayout and use it as the root view for testing. Again, I know this is beyond the spirit of a unit test but if we use this inflater, I think it should make sure that no layouts are broken by it. * For #16373: Overrode getSystemService to return PerformanceInflater This allows PerformanceInflater to be called in every inflation to keep track of the number of inflations we do. * For #16373: Added UI test for # of inflations * For #16373: Lint fix * For #167373: Changed the LayoutInflater cloneInContext to take this instead of inflater The inflater parameter is set on the first call from the OS from the Window object. However, the activity itself sets multiple factories on the inflater during its creation (usually through AppCompatDelegateImpl.java). This means that, once we initially set the inflater with a null check, we pass an inflater that has no factory initially. However, since we keep a reference to it, when cloneInContext was called, it cloned the inflater with the original inflater which didn't have any factories set up. This meant that the app would crash on either browserFragment creation or any thing that required appCompat (such as ImageView and ImageButton). Now, passing itself with a cloneInContext means we keep all the factories initially set by the activity or the fragment. * For #16373: Fixed code issues for PR. No behavior change * For #16373: fixed some code nits
3 years ago
)
private val failureMsgNumberOfInflation = getErrorMessage("start up inflation")
/**
* A performance test that attempts to minimize start up performance regressions using heuristics
* rather than benchmarking. These heuristics measure occurrences of known performance anti-patterns
* and fails when the occurrence count changes. If the change indicates a regression, we should
* re-evaluate the PR to see if we can avoid the potential regression and, if not, change the
* expected value. If it indicates an improvement, we can change the expected value. The expected
* values can be updated without consulting the performance team.
*
* See `EXPECTED_*` above for explanations of the heuristics this test currently supports.
*
* The benefits of a heuristics-based performance test are that it is uses less CI time to get
* results so we can run it more often (e.g. for each PR) and it is less noisy than a benchmark.
* However, the downsides of this style of test is that if a heuristic value increases, it may not
* represent a real, significant performance regression.
*/
class StartupExcessiveResourceUseTest {
@get:Rule
val activityTestRule = HomeActivityTestRule(skipOnboarding = true)
private val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@Test
fun verifyRunBlockingAndStrictModeSuppresionCount() {
uiDevice.waitForIdle() // wait for async UI to load.
// This might cause intermittents: at an arbitrary point after start up (such as the visual
// completeness queue), we might run code on the main thread that suppresses StrictMode,
// causing this number to fluctuate depending on device speed. We'll deal with it if it occurs.
val actualSuppresionCount = activityTestRule.activity.components.strictMode.suppressionCount.get().toInt()
val actualRunBlocking = RunBlockingCounter.count.get()
assertEquals(failureMsgStrictMode, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount)
assertTrue(failureMsgRunBlocking + "actual: $actualRunBlocking", actualRunBlocking in EXPECTED_RUNBLOCKING_RANGE)
// This below asserts fail in Firebase with different values for
// "actualRecyclerViewConstraintLayoutChildren" or "actualNumberOfInflations"
// See https://github.com/mozilla-mobile/fenix/pull/26512 and https://github.com/mozilla-mobile/fenix/issues/25142
//
// val rootView = activityTestRule.activity.findViewById<LinearLayout>(R.id.rootContainer)
// val actualRecyclerViewConstraintLayoutChildren = countRecyclerViewConstraintLayoutChildren(rootView, null)
// assertTrue(
// failureMsgRecyclerViewConstraintLayoutChildren + "actual: $actualRecyclerViewConstraintLayoutChildren",
// actualRecyclerViewConstraintLayoutChildren in EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN,
// )
// val actualNumberOfInflations = InflationCounter.inflationCount.get()
// assertTrue(
// failureMsgNumberOfInflation + "actual: $actualNumberOfInflations",
// actualNumberOfInflations in EXPECTED_NUMBER_OF_INFLATION,
// )
}
}
private fun countRecyclerViewConstraintLayoutChildren(view: View, parent: View?): Int {
val viewValue = if (parent is RecyclerView && view is ConstraintLayout) {
1
} else {
0
}
return if (view !is ViewGroup) {
viewValue
} else {
viewValue + view.children.sumOf { countRecyclerViewConstraintLayoutChildren(it, view) }
}
}
private fun getErrorMessage(shortName: String) = """$shortName count does not match expected count.
This heuristic-based performance test is expected measure the number of occurrences of known
performance anti-patterns and fail when that count changes. Please read the class documentation
for more details about this test and an explanation of what the failed heuristic is expected to
measure. Please consult the performance team if you have questions.
"""