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:
Siarhei Vishniakou
2022-12-15 13:45:31 -08:00
parent 86413b57e1
commit 1752fb85f9
3 changed files with 150 additions and 4 deletions

View File

@@ -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)
}
}
}

View File

@@ -0,0 +1,3 @@
include platform/frameworks/base:/INPUT_OWNERS
# Bug component: 136048

View File

@@ -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)