Performance test for MotionPredictor
The motion prediction is needed when we are trying to achieve the smallest possible end-to-end touch latency. The API is designed to be running directly inside the UI thread, while processing touch. Therefore, it's important for this class to be fast. To monitor regressions in the performance of this class, introduce the performance tests. Results: com.android.perftests.core (2 Tests) [1/2] android.input.MotionPredictorBenchmark#timeRecordAndPredict: PASSED (8.634s) perfetto_file_path: /sdcard/test_results/android.input.MotionPredictorBenchmark_timeRecordAndPredict/PerfettoListener/perfetto_android.input.MotionPredictorBenchmark_timeRecordAndPredict-1.perfetto-trace timeRecordAndPredict_mean (ns): 31594 timeRecordAndPredict_median (ns): 31285 timeRecordAndPredict_min (ns): 30983 timeRecordAndPredict_standardDeviation: 638 [2/2] android.input.MotionPredictorBenchmark#timeCreatePredictor: PASSED (6.973s) perfetto_file_path: /sdcard/test_results/android.input.MotionPredictorBenchmark_timeCreatePredictor/PerfettoListener/perfetto_android.input.MotionPredictorBenchmark_timeCreatePredictor-1.perfetto-trace timeCreatePredictor_mean (ns): 71687 timeCreatePredictor_median (ns): 68660 timeCreatePredictor_min (ns): 66266 timeCreatePredictor_standardDeviation: 6056 Bug: 167946763 Test: atest MotionPredictorBenchmark MotionPredictorTest Change-Id: I2415f796b2f99717ba915b3da2979d70302c235d
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 android.input
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.os.SystemProperties
|
||||
import android.perftests.utils.PerfStatusReporter
|
||||
import android.view.InputDevice
|
||||
import android.view.MotionEvent
|
||||
import android.view.MotionEvent.ACTION_DOWN
|
||||
import android.view.MotionEvent.ACTION_MOVE
|
||||
import android.view.MotionEvent.PointerCoords
|
||||
import android.view.MotionEvent.PointerProperties
|
||||
import android.view.MotionPredictor
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.`when`
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
private fun getStylusMotionEvent(
|
||||
eventTime: Duration,
|
||||
action: Int,
|
||||
x: Float,
|
||||
y: Float,
|
||||
): MotionEvent{
|
||||
val pointerCount = 1
|
||||
val properties = arrayOfNulls<MotionEvent.PointerProperties>(pointerCount)
|
||||
val coords = arrayOfNulls<MotionEvent.PointerCoords>(pointerCount)
|
||||
|
||||
for (i in 0 until pointerCount) {
|
||||
properties[i] = PointerProperties()
|
||||
properties[i]!!.id = i
|
||||
properties[i]!!.toolType = MotionEvent.TOOL_TYPE_STYLUS
|
||||
coords[i] = PointerCoords()
|
||||
coords[i]!!.x = x
|
||||
coords[i]!!.y = y
|
||||
}
|
||||
|
||||
return MotionEvent.obtain(/*downTime=*/0, eventTime.toMillis(), action, properties.size,
|
||||
properties, coords, /*metaState=*/0, /*buttonState=*/0,
|
||||
/*xPrecision=*/0f, /*yPrecision=*/0f, /*deviceId=*/0, /*edgeFlags=*/0,
|
||||
InputDevice.SOURCE_STYLUS, /*flags=*/0)
|
||||
}
|
||||
|
||||
private fun getPredictionContext(offset: Duration, enablePrediction: Boolean): Context {
|
||||
val context = mock(Context::class.java)
|
||||
val resources: Resources = mock(Resources::class.java)
|
||||
`when`(context.getResources()).thenReturn(resources)
|
||||
`when`(resources.getInteger(
|
||||
com.android.internal.R.integer.config_motionPredictionOffsetNanos)).thenReturn(
|
||||
offset.toNanos().toInt())
|
||||
`when`(resources.getBoolean(
|
||||
com.android.internal.R.bool.config_enableMotionPrediction)).thenReturn(enablePrediction)
|
||||
return context
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class MotionPredictorBenchmark {
|
||||
private val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
@get:Rule
|
||||
val perfStatusReporter = PerfStatusReporter()
|
||||
private val initialPropertyValue =
|
||||
SystemProperties.get("persist.input.enable_motion_prediction")
|
||||
|
||||
private var eventTime = Duration.ofMillis(1)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
instrumentation.uiAutomation.executeShellCommand(
|
||||
"setprop persist.input.enable_motion_prediction true")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
instrumentation.uiAutomation.executeShellCommand(
|
||||
"setprop persist.input.enable_motion_prediction $initialPropertyValue")
|
||||
}
|
||||
|
||||
/**
|
||||
* In a typical usage, app will send the event to the predictor and then call .predict to draw
|
||||
* a prediction. In a loop, we keep sending MOVE and then calling .predict to simulate this.
|
||||
*/
|
||||
@Test
|
||||
fun timeRecordAndPredict() {
|
||||
val offset = Duration.ofMillis(1)
|
||||
val predictor = MotionPredictor(getPredictionContext(offset, /*enablePrediction=*/true))
|
||||
// ACTION_DOWN t=0 x=0 y=0
|
||||
predictor.record(getStylusMotionEvent(eventTime, ACTION_DOWN, /*x=*/0f, /*y=*/0f))
|
||||
|
||||
val state = perfStatusReporter.getBenchmarkState()
|
||||
while (state.keepRunning()) {
|
||||
eventTime += Duration.ofMillis(1)
|
||||
|
||||
// Send MOVE event and then call .predict
|
||||
val moveEvent = getStylusMotionEvent(eventTime, ACTION_MOVE, /*x=*/1f, /*y=*/2f)
|
||||
predictor.record(moveEvent)
|
||||
val predictionTime = eventTime + Duration.ofMillis(2)
|
||||
val predicted = predictor.predict(predictionTime.toNanos())
|
||||
assertEquals(1, predicted.size)
|
||||
assertEquals((predictionTime + offset).toMillis(), predicted[0].eventTime)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The creation of the predictor should happen infrequently. However, we still want to be
|
||||
* mindful of the load times.
|
||||
*/
|
||||
@Test
|
||||
fun timeCreatePredictor() {
|
||||
val context = getPredictionContext(
|
||||
/*offset=*/Duration.ofMillis(1), /*enablePrediction=*/true)
|
||||
|
||||
val state = perfStatusReporter.getBenchmarkState()
|
||||
while (state.keepRunning()) {
|
||||
MotionPredictor(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
apct-tests/perftests/core/src/android/input/OWNERS
Normal file
3
apct-tests/perftests/core/src/android/input/OWNERS
Normal file
@@ -0,0 +1,3 @@
|
||||
include platform/frameworks/base:/INPUT_OWNERS
|
||||
|
||||
# Bug component: 136048
|
||||
@@ -31,15 +31,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.`when`
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.`when`
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
@@ -49,7 +48,6 @@ private fun getStylusMotionEvent(
|
||||
x: Float,
|
||||
y: Float,
|
||||
): MotionEvent{
|
||||
// One-time: send a DOWN event
|
||||
val pointerCount = 1
|
||||
val properties = arrayOfNulls<MotionEvent.PointerProperties>(pointerCount)
|
||||
val coords = arrayOfNulls<MotionEvent.PointerCoords>(pointerCount)
|
||||
|
||||
Reference in New Issue
Block a user