Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 1752fb85 authored by Siarhei Vishniakou's avatar Siarhei Vishniakou
Browse files

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
parent 86413b57
Loading
Loading
Loading
Loading
+145 −0
Original line number Diff line number Diff line
/*
 * 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 −0
Original line number Diff line number Diff line
include platform/frameworks/base:/INPUT_OWNERS

# Bug component: 136048
+2 −4
Original line number Diff line number Diff line
@@ -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)