blob: c58d5fe0ad29e0443d578715e00e36ea50af9c0b [file] [log] [blame]
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.metrics.performance.test
import android.os.Build
import android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1
import android.os.Build.VERSION_CODES.JELLY_BEAN
import android.view.Choreographer
import androidx.annotation.RequiresApi
import androidx.metrics.performance.FrameData
import androidx.metrics.performance.FrameDataApi24
import androidx.metrics.performance.FrameDataApi31
import androidx.metrics.performance.JankStats
import androidx.metrics.performance.JankStats.OnFrameListener
import androidx.metrics.performance.PerformanceMetricsState
import androidx.metrics.performance.StateInfo
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.math.max
import org.hamcrest.Matchers
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class JankStatsTest {
private lateinit var jankStats: JankStats
private lateinit var metricsState: PerformanceMetricsState
private lateinit var delayedActivity: DelayedActivity
private lateinit var delayedView: DelayedView
private lateinit var latchedListener: LatchedListener
private var frameInit: FrameInitCompat
private val NUM_FRAMES = 10
/**
* On some older APIs and emulators, frames may occasionally take longer than predicted
* jank. We check against this MIN duration to avoid flaky tests.
*/
private val MIN_JANK_NS = 100000000
init {
if (Build.VERSION.SDK_INT >= 16) {
frameInit = FrameInit16(this)
} else {
frameInit = FrameInitCompat(this)
}
}
@Rule
@JvmField
var delayedActivityRule: ActivityScenarioRule<DelayedActivity> =
ActivityScenarioRule(DelayedActivity::class.java)
@Before
fun setup() {
val scenario = delayedActivityRule.scenario
scenario.onActivity { activity: DelayedActivity? ->
delayedActivity = activity!!
delayedView = delayedActivity.findViewById(R.id.delayedView)
latchedListener = LatchedListener()
latchedListener.latch = CountDownLatch(1)
jankStats = JankStats.createAndTrack(delayedActivity.window, latchedListener)
metricsState = PerformanceMetricsState.getHolderForHierarchy(delayedView).state!!
}
}
@Test
@UiThreadTest
fun testGetInstance() {
assert(PerformanceMetricsState.getHolderForHierarchy(delayedView).state == metricsState)
}
/**
* Test adding/removing listeners while inside the listener callback (on the same thread)
*/
@Test
fun testConcurrentListenerModifications() {
lateinit var jsSecond: JankStats
lateinit var jsThird: JankStats
// We add two more JankStats object with another listener which will add/remove
// in the callback. One more listener will not necessarily trigger the issue, because if
// that listener is at the end of the internal list of listeners, then the iterator will
// not try to find another one after it has been removed. So we make the list large enough
// that we will be removing a listener in the middle.
val secondListener = OnFrameListener {
jsSecond.isTrackingEnabled = !jsSecond.isTrackingEnabled
jsThird.isTrackingEnabled = !jsThird.isTrackingEnabled
}
delayedActivityRule.scenario.onActivity {
jsSecond = JankStats.createAndTrack(delayedActivity.window, secondListener)
jsThird = JankStats.createAndTrack(delayedActivity.window, secondListener)
}
runDelayTest(frameDelay = 0, NUM_FRAMES, latchedListener)
}
@Test
fun testEnable() {
assertTrue(jankStats.isTrackingEnabled)
jankStats.isTrackingEnabled = false
assertFalse(jankStats.isTrackingEnabled)
jankStats.isTrackingEnabled = true
assertTrue(jankStats.isTrackingEnabled)
}
@Test
fun testEquality() {
val states1 = listOf(StateInfo("1", "a"))
val states2 = listOf(StateInfo("1", "a"), StateInfo("2", "b"))
val frameDataBase = FrameData(0, 0, true, states1)
val frameDataBaseCopy = FrameData(0, 0, true, states1)
val frameDataBaseA = FrameData(0, 0, true, states2)
val frameDataBaseB = FrameData(0, 0, false, states1)
val frameDataBaseC = FrameData(0, 1, true, states1)
val frameDataBaseD = FrameData(1, 0, true, states1)
val frameData24 = FrameDataApi24(0, 0, 0, true, states1)
val frameData24Copy = FrameDataApi24(0, 0, 0, true, states1)
val frameData24A = FrameDataApi24(0, 0, 1, true, states1)
val frameData31 = FrameDataApi31(0, 0, 0, 0, 0, true, states1)
val frameData31Copy = FrameDataApi31(0, 0, 0, 0, 0, true, states1)
val frameData31A = FrameDataApi31(0, 0, 0, 1, 0, true, states1)
assertEquals(frameDataBase, frameDataBase)
assertEquals(frameDataBase, frameDataBaseCopy)
assertEquals(frameData24, frameData24)
assertEquals(frameData24, frameData24Copy)
assertEquals(frameData31, frameData31)
assertEquals(frameData31, frameData31Copy)
assertNotEquals(frameDataBase, frameDataBaseA)
assertNotEquals(frameDataBase, frameDataBaseB)
assertNotEquals(frameDataBase, frameDataBaseC)
assertNotEquals(frameDataBase, frameDataBaseD)
assertNotEquals(frameDataBase, frameData24)
assertNotEquals(frameData24, frameDataBase)
assertNotEquals(frameDataBase, frameData31)
assertNotEquals(frameData31, frameDataBase)
assertNotEquals(frameData24, frameData31)
assertNotEquals(frameData31, frameData24)
assertNotEquals(frameData24, frameData24A)
assertNotEquals(frameData31, frameData31A)
}
@SdkSuppress(minSdkVersion = JELLY_BEAN)
@Test
@Ignore("b/272347202")
fun testNoJank() {
val frameDelay = 0
frameInit.initFramePipeline()
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
assertEquals("numJankFrames should equal 0", 0, latchedListener.numJankFrames)
latchedListener.reset()
jankStats.jankHeuristicMultiplier = 0f
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
// FrameMetrics sometimes drops a frame, so the total number of
// jankData items might be less than NUM_FRAMES
assertEquals(
"jank frames != NUMFRAMES",
NUM_FRAMES, latchedListener.numJankFrames
)
assertTrue(
"With heuristicMultiplier 0, should be at least ${NUM_FRAMES - 1} " +
"frames with jank data, not ${latchedListener.numJankFrames}",
latchedListener.numJankFrames >= (NUM_FRAMES - 1)
)
}
@SdkSuppress(minSdkVersion = JELLY_BEAN)
@Test
fun testMultipleListeners() {
var secondListenerLatch = CountDownLatch(0)
val frameDelay = 0
frameInit.initFramePipeline()
var numSecondListenerCalls = 0
val secondListenerStates = mutableListOf<StateInfo>()
val secondListener = OnFrameListener { volatileFrameData ->
secondListenerStates.addAll(volatileFrameData.states)
numSecondListenerCalls++
if (numSecondListenerCalls >= NUM_FRAMES) {
secondListenerLatch.countDown()
}
}
lateinit var jankStats2: JankStats
val scenario = delayedActivityRule.scenario
scenario.onActivity { _ ->
jankStats2 = JankStats.createAndTrack(delayedActivity.window, secondListener)
}
val testState = StateInfo("Testing State", "sampleState")
metricsState.putSingleFrameState(testState.key, testState.value)
// in case earlier frames arrive before our test begins
secondListenerStates.clear()
secondListenerLatch = CountDownLatch(1)
latchedListener.reset()
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
secondListenerLatch.await(frameDelay * NUM_FRAMES + 1000L, TimeUnit.MILLISECONDS)
val jankData: FrameData = latchedListener.jankData[0]
assertTrue("No calls to second listener", numSecondListenerCalls > 0)
assertEquals(listOf(testState), jankData.states)
assertEquals(listOf(testState), secondListenerStates)
jankStats2.isTrackingEnabled = false
numSecondListenerCalls = 0
latchedListener.reset()
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
assertEquals(0, numSecondListenerCalls)
assertTrue("Removal of second listener should not have removed first",
latchedListener.jankData.size > 0)
// Now make sure that extra listeners can be added concurrently from other threads
latchedListener.reset()
val listenerPostingThread = Thread()
var numNewListeners = 0
lateinit var poster: Runnable
poster = Runnable {
JankStats.createAndTrack(
delayedActivity.window,
secondListener
)
++numNewListeners
if (numNewListeners < 100) {
delayedView.postDelayed(poster, 10)
}
}
scenario.onActivity { _ ->
listenerPostingThread.run {
poster.run()
}
}
listenerPostingThread.start()
// add listeners concurrently - no asserts here, just testing whether we
// avoid any concurrency issues with adding and using multiple listeners
runDelayTest(frameDelay, NUM_FRAMES * 100, latchedListener)
}
@SdkSuppress(minSdkVersion = JELLY_BEAN)
@Test
fun testRegularJank() {
val frameDelay = 100
frameInit.initFramePipeline()
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
// FrameMetrics sometimes drops a frame, so the total number of
// jankData items might be less than NUM_FRAMES
assertTrue(
"There should be at least ${NUM_FRAMES - 1} frames with jank data, " +
"not ${latchedListener.jankData.size}",
latchedListener.jankData.size >= (NUM_FRAMES - 1)
)
latchedListener.reset()
jankStats.jankHeuristicMultiplier = 20f
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
assertEquals(
"multiplier 20, extremeMs 0: numJankFrames should equal 0",
0, latchedListener.numJankFrames
)
}
@SdkSuppress(minSdkVersion = JELLY_BEAN)
@Test
@Ignore("b/272347202")
fun testFrameStates() {
val frameDelay = 0
frameInit.initFramePipeline()
val state0 = StateInfo("Testing State 0", "sampleStateA")
val state1 = StateInfo("Testing State 1", "sampleStateB")
val state2 = StateInfo("Testing State 2", "sampleStateC")
metricsState.putState(state0.key, state0.value)
metricsState.putState(state1.key, state1.value)
metricsState.putSingleFrameState(state2.key, state2.value)
runDelayTest(frameDelay, NUM_FRAMES, latchedListener)
assertEquals(
"frameDelay 100: There should be $NUM_FRAMES frames with jank data", NUM_FRAMES,
latchedListener.jankData.size
)
var item0: FrameData = latchedListener.jankData[0]
assertEquals("There should be 3 states at frame 0", 3,
item0.states.size)
for (state in item0.states) {
// Test that every state is in the states set above
assertThat(state, Matchers.isIn(listOf(state0, state1, state2)))
}
// Test that all states set above are in the states for the first frame
assertThat(state0, Matchers.isIn(item0.states))
assertThat(state1, Matchers.isIn(item0.states))
assertThat(state2, Matchers.isIn(item0.states))
// Now test the rest of the frames, which should not include singleFrameState state2
for (i in 1 until NUM_FRAMES) {
val item = latchedListener.jankData[i]
assertEquals("There should be 2 states at frame $i", 2,
item.states.size)
for (state in item.states) {
assertThat(
state,
Matchers.either(Matchers.`is`(state0)).or(Matchers.`is`(state1))
)
}
}
// reset and clear states
latchedListener.reset()
metricsState.removeState(state0.key)
metricsState.removeState(state1.key)
runDelayTest(frameDelay, 1, latchedListener)
item0 = latchedListener.jankData[0]
assertEquals(
"States should be empty after being cleared",
0,
item0.states.size
)
latchedListener.reset()
val state3 = Pair("Testing State 3", "sampleStateD")
val state4 = Pair("Testing State 4", "sampleStateE")
metricsState.putState(state3.first, state3.second)
metricsState.putState(state4.first, state4.second)
runDelayTest(frameDelay, 1, latchedListener)
item0 = latchedListener.jankData[0]
assertEquals(2, item0.states.size)
latchedListener.reset()
// Test removal of state3 and replacement of state4
metricsState.removeState(state3.first)
metricsState.putState(state4.first, "sampleStateF")
runDelayTest(frameDelay, 1, latchedListener)
item0 = latchedListener.jankData[0]
assertEquals(1, item0.states.size)
assertEquals(state4.first, item0.states[0].key)
assertEquals("sampleStateF", item0.states[0].value)
latchedListener.reset()
}
/**
* Data structure to hold per-frame state data to be injected during the test
*/
data class FrameStateInputData(
val addSFStates: List<Pair<String, String>> = emptyList(),
val addStates: List<Pair<String, String>> = emptyList(),
val removeStates: List<String> = emptyList()
)
/**
* Utility function (embedded in a class because it uses version-specific APIs) which
* is used by tests which require the frame pipeline to be empty when they
* start. When the activity first starts, there are usually a couple of frames drawn.
* Depending on when those frames are drawn relative to when the JankStats object and
* OnFrameListener are set up, there can be old frame data still being set to JankStats
* after the test has started, which causes problems with a test not getting the result
* that it should. The workaround is to force these initial frames to draw before the test
* begins, so that any data used by the test will only land on frames after the test begins
* instead of these old activity-creation frames.
*/
open class FrameInitCompat(val jankStatsTest: JankStatsTest) {
open fun initFramePipeline() {}
}
@RequiresApi(16)
class FrameInit16(jankStatsTest: JankStatsTest) : FrameInitCompat(jankStatsTest) {
override fun initFramePipeline() {
val latch = CountDownLatch(10)
var numFrames = 10
val callback: Choreographer.FrameCallback = object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
--numFrames
latch.countDown()
if (numFrames > 0) {
Choreographer.getInstance().postFrameCallback(this)
}
}
}
jankStatsTest.delayedActivityRule.getScenario().onActivity {
Choreographer.getInstance().postFrameCallback(callback)
}
latch.await(5, TimeUnit.SECONDS)
jankStatsTest.latchedListener.reset()
}
}
/**
* JankStats doesn't do anything pre API 16. But it would be nice to not crash running
* code that calls JankStats functionality on that version. This test just calls basic APIs
* to make sure they don't crash.
*/
@SdkSuppress(maxSdkVersion = ICE_CREAM_SANDWICH_MR1)
@Test
fun testPreAPI16() {
delayedActivityRule.getScenario().onActivity {
val state0 = StateInfo("Testing State 0", "sampleStateA")
val state1 = StateInfo("Testing State 1", "sampleStateB")
metricsState.putState(state0.key, state0.value)
metricsState.putSingleFrameState(state1.key, state1.value)
}
runDelayTest(0, NUM_FRAMES, latchedListener)
}
@SdkSuppress(minSdkVersion = JELLY_BEAN)
@Test
@Ignore("b/272347202")
fun testComplexFrameStateData() {
frameInit.initFramePipeline()
// perFrameStateData is a structure for testing which holds information about the
// states that should be added or removed on every frame. This functionality is
// handled inside DelayedView. //-Comments above each item indicate what the resulting
// state should be in that frame, which is checked in the asserts below
// TODO: make immutable, copy to mutable list for delayedView
var perFrameStateData = mutableListOf(
// 0: A:0
JankStatsTest.FrameStateInputData(
addStates = listOf("stateNameA" to "0"),
),
// 1: A:0
JankStatsTest.FrameStateInputData(),
// 2: A:1
JankStatsTest.FrameStateInputData(
addStates = listOf("stateNameA" to "1"),
),
// 3: A:2
JankStatsTest.FrameStateInputData(
addStates = listOf("stateNameA" to "2"),
),
// 4: A:2
JankStatsTest.FrameStateInputData(
removeStates = listOf("stateNameA"),
),
// 5: [nothing]
JankStatsTest.FrameStateInputData(),
// 6: A:0, B:10
JankStatsTest.FrameStateInputData(
addStates = listOf("stateNameA" to "0", "stateNameB" to "10"),
),
// 7: A:0, B:10, C:100
JankStatsTest.FrameStateInputData(
addSFStates = listOf("stateNameC" to "100"),
),
// 8: A:0, B:10
JankStatsTest.FrameStateInputData(),
// 9: A:0, B:10
JankStatsTest.FrameStateInputData(
removeStates = listOf("stateNameA", "stateNameB"),
),
// 10: empty
JankStatsTest.FrameStateInputData(),
// 11: A:1
JankStatsTest.FrameStateInputData(
addStates = listOf("stateNameA" to "0", "stateNameA" to "1"),
),
)
// testData will hold input (above) plus expected results
val expectedResults = listOf(
mapOf("stateNameA" to "0"),
mapOf("stateNameA" to "0"),
mapOf("stateNameA" to "1"),
mapOf("stateNameA" to "2"),
mapOf("stateNameA" to "2"),
emptyMap(),
mapOf("stateNameA" to "0", "stateNameB" to "10"),
mapOf("stateNameA" to "0", "stateNameB" to "10", "stateNameC" to "100"),
mapOf("stateNameA" to "0", "stateNameB" to "10"),
mapOf("stateNameA" to "0", "stateNameB" to "10"),
emptyMap(),
mapOf("stateNameA" to "1"),
)
runDelayTest(frameDelay = 0, numFrames = perFrameStateData.size,
latchedListener, perFrameStateData)
assertEquals("There should be ${expectedResults.size} frames of data",
expectedResults.size, latchedListener.jankData.size)
for (i in 0 until expectedResults.size) {
val testResultStates = latchedListener.jankData[i].states
val expectedResult = expectedResults[i]
assertEquals("There should be ${expectedResult.size} states",
expectedResult.size, testResultStates.size)
for (state in testResultStates) {
assertEquals("State value not correct",
state.value, expectedResult.get(state.key))
}
}
}
private fun runDelayTest(
frameDelay: Int,
numFrames: Int,
listener: LatchedListener,
perFrameStateData: List<FrameStateInputData>? = null
) {
val latch = CountDownLatch(1)
listener.latch = latch
listener.minFrames = numFrames
delayedActivityRule.getScenario().onActivity {
if (perFrameStateData != null) delayedView.perFrameStateData = perFrameStateData
delayedActivity.repetitions = 0
delayedActivity.maxReps = numFrames
delayedActivity.delayMs = frameDelay.toLong()
delayedActivity.invalidate()
listener.numFrames = 0
}
try {
latch.await(max(frameDelay, 1) * numFrames + 1000L, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
assert(false)
}
}
inner class LatchedListener : JankStats.OnFrameListener {
var numJankFrames = 0
var jankData = mutableListOf<FrameData>()
var latch: CountDownLatch? = null
var minFrames = 0
var numFrames = 0
fun reset() {
jankData.clear()
numJankFrames = 0
numFrames = 0
}
override fun onFrame(volatileFrameData: FrameData) {
if (latch == null) {
throw Exception("latch not set in LatchedListener")
} else {
if (volatileFrameData.isJank && volatileFrameData.frameDurationUiNanos >
(MIN_JANK_NS * jankStats.jankHeuristicMultiplier)) {
this.numJankFrames++
}
this.jankData.add(volatileFrameData.copy())
numFrames++
if (numFrames >= minFrames) {
latch!!.countDown()
}
}
}
}
}