From 1752fb85f99a16278f7b3fb6fc408493c5246cca Mon Sep 17 00:00:00 2001 From: Siarhei Vishniakou Date: Thu, 15 Dec 2022 13:45:31 -0800 Subject: [PATCH] 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 --- .../android/input/MotionPredictorBenchmark.kt | 145 ++++++++++++++++++ .../perftests/core/src/android/input/OWNERS | 3 + .../android/test/input/MotionPredictorTest.kt | 6 +- 3 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt create mode 100644 apct-tests/perftests/core/src/android/input/OWNERS diff --git a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt new file mode 100644 index 0000000000000..3d447ac40b8c0 --- /dev/null +++ b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt @@ -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(pointerCount) + val coords = arrayOfNulls(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) + } + } +} diff --git a/apct-tests/perftests/core/src/android/input/OWNERS b/apct-tests/perftests/core/src/android/input/OWNERS new file mode 100644 index 0000000000000..95e3f0213f494 --- /dev/null +++ b/apct-tests/perftests/core/src/android/input/OWNERS @@ -0,0 +1,3 @@ +include platform/frameworks/base:/INPUT_OWNERS + +# Bug component: 136048 diff --git a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt index 46aad9f635caa..bc363b01ea4b1 100644 --- a/tests/Input/src/com/android/test/input/MotionPredictorTest.kt +++ b/tests/Input/src/com/android/test/input/MotionPredictorTest.kt @@ -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(pointerCount) val coords = arrayOfNulls(pointerCount)