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

Commit ad8715bd authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add vibration latency to VibratorPerfTest" into main

parents 3840a0dc 0fe9157d
Loading
Loading
Loading
Loading
+449 −33
Original line number Diff line number Diff line
@@ -16,86 +16,502 @@

package android.os;

import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static com.google.common.truth.Truth.assertWithMessage;

import static org.junit.Assume.assumeTrue;

import static java.util.concurrent.TimeUnit.SECONDS;

import android.Manifest;
import android.content.Context;
import android.perftests.utils.ManualBenchmarkState;
import android.perftests.utils.PerfManualStatusReporter;
import android.perftests.utils.TraceMarkParser;
import android.util.Log;

import androidx.benchmark.BenchmarkState;
import androidx.benchmark.junit4.BenchmarkRule;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;

import com.android.compatibility.common.util.AdoptShellPermissionsRule;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

@LargeTest
public class VibratorPerfTest {
    private static final String TAG = "VibratorPerfTest";
    private static final long NANOS_PER_MS = 1000L * 1000;
    private static final long NANOS_PER_S = 1000 * NANOS_PER_MS;

    /**
     * Time to wait for the vibrator service to settle after vibration is started/ended/cancelled.
     *
     * <p>This should be used between iterations to make sure the vibrate method being tested is
     * hitting the vibrator while it's idle.
     */
    private static final long SERVICE_DELAY_MS = 100;

    private static final int ATRACE_BUFFER_SIZE = 1024;
    private static final String ATRACE_TAG = "vibrator";
    private static final String ATRACE_START =
            String.format("atrace --async_start -b %d -c %s", ATRACE_BUFFER_SIZE, ATRACE_TAG);
    private static final String ATRACE_STOP = "atrace --async_stop";
    private static final String ATRACE_DUMP = "atrace --async_dump";

    // Traces that includes the vibration duration and should be used to generate latency metrics.
    private static final Set<String> LATENCY_TRACES = Set.of("vibration", "HalVibrator.vibration");
    private static final String[] VIBRATION_TRACES = new String[]{
            // VibratorManagerService async trace for entire vibration
            "vibration",
            // HalVibrator async trace between on/off commands
            "HalVibrator.vibration",
            // HalVibrator methods
            "HalVibrator.onMillis",
            "HalVibrator.onPrebaked",
            "HalVibrator.onPrimitives",
            "HalVibrator.onPwleV2",
            "HalVibrator.setAmplitude",
            "HalVibrator.off",
            // VibratorManagerService methods
            "vibrate",
            "cancelVibrate",
    };

    private static final String LATENCY_METRIC_KEY_SUFFIX = "Latency";
    private static final String VIBRATOR_STATE_START_LATENCY_METRIC_KEY =
            "OnVibratorStateChangedListener.start" + LATENCY_METRIC_KEY_SUFFIX;
    private static final String VIBRATOR_STATE_STOP_LATENCY_METRIC_KEY =
            "OnVibratorStateChangedListener.stop" + LATENCY_METRIC_KEY_SUFFIX;

    @Rule
    public final PerfManualStatusReporter mStatusReporter = new PerfManualStatusReporter();

    @Rule
    public final BenchmarkRule mBenchmarkRule = new BenchmarkRule();
    public AdoptShellPermissionsRule mAdoptShellPermissionsRule = new AdoptShellPermissionsRule(
            getInstrumentation().getUiAutomation(), Manifest.permission.ACCESS_VIBRATOR_STATE);

    private TraceMarkParser mTraceMethods;

    private Vibrator mVibrator;
    private VibratorStateListener mStateListener;

    @Before
    public void setUp() {
        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
        Context context = getInstrumentation().getTargetContext();
        mVibrator = context.getSystemService(Vibrator.class);
        mStateListener = new VibratorStateListener();
        mVibrator.cancel();
        mVibrator.addVibratorStateListener(mStateListener);
    }

    @After
    public void cleanUp() {
        mVibrator.removeVibratorStateListener(mStateListener);
    }

    @Test
    public void testEffectClickSuperseded() {
        VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);

        long elapsedTimeNs = 0;
        ManualBenchmarkState state = mStatusReporter.getBenchmarkState();
        while (state.keepRunning(elapsedTimeNs)) {
            // Measure vibrate call right after initial call that will be superseded.
            mVibrator.vibrate(effect);
            elapsedTimeNs = measureVibrate(effect);
        }
    }

    @Test
    public void testEffectClick() {
        final BenchmarkState state = mBenchmarkRule.getState();
        while (state.keepRunning()) {
            mVibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
        // Unknown predefined click estimated duration, cannot add vibration latency metrics.
        benchmarkVibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK));
    }

    @Test
    public void testEffectClickTraces() throws InterruptedException {
        // Enable traces in separate test case, as they might affect performance.
        assumeTrue("Device without predefined click support",
                mVibrator.areAllEffectsSupported(VibrationEffect.EFFECT_CLICK)
                        == Vibrator.VIBRATION_EFFECT_SUPPORT_YES);

        // Unknown predefined click estimated duration, cannot add vibration latency metrics.
        VibrationEffect effect = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK);
        benchmarkVibrateWithTraces(effect, /* durationMs= */ -1);
    }

    @Test
    public void testOneShot() {
        final BenchmarkState state = mBenchmarkRule.getState();
        while (state.keepRunning()) {
            mVibrator.vibrate(VibrationEffect.createOneShot(SECONDS.toMillis(2),
                    VibrationEffect.DEFAULT_AMPLITUDE));
    public void testPrimitiveClickTraces() throws InterruptedException {
        // Enable traces in separate test case, as they might affect performance.
        assumeTrue("Device without primitive click support",
                mVibrator.areAllPrimitivesSupported(PRIMITIVE_CLICK));

        long durationMs = mVibrator.getPrimitiveDurations(PRIMITIVE_CLICK)[0];
        VibrationEffect effect =
                VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK).compose();
        benchmarkVibrateWithTraces(effect, durationMs);
    }

    @Test
    public void testOneShot() throws InterruptedException {
        long durationMs = 100;
        VibrationEffect effect =
                VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE);
        benchmarkVibrate(effect, durationMs);
    }

    @Test
    public void testOneShotTraces() throws InterruptedException {
        // Enable traces in separate test case, as they might affect performance.
        long durationMs = 100;
        VibrationEffect effect =
                VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE);
        benchmarkVibrateWithTraces(effect, durationMs);
    }

    @Test
    public void testWaveform() {
        final BenchmarkState state = mBenchmarkRule.getState();
        // Vibrator turns on/off multiple times, cannot add vibration latency metrics.
        long[] timings = new long[]{SECONDS.toMillis(1), SECONDS.toMillis(2), SECONDS.toMillis(1)};
        while (state.keepRunning()) {
            mVibrator.vibrate(VibrationEffect.createWaveform(timings, -1));
        benchmarkVibrate(VibrationEffect.createWaveform(timings, -1));
    }

    @Test
    public void testComposePrimitives() throws InterruptedException {
        int[] primitiveDurations = mVibrator.getPrimitiveDurations(PRIMITIVE_CLICK, PRIMITIVE_TICK);
        long durationMs = primitiveDurations[0] + 100 + primitiveDurations[1];
        VibrationEffect effect = VibrationEffect.startComposition()
                .addPrimitive(PRIMITIVE_CLICK)
                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 100)
                .compose();
        benchmarkVibrate(effect, durationMs,
                vib -> vib.areAllPrimitivesSupported(PRIMITIVE_CLICK, PRIMITIVE_TICK));
    }

    @Test
    public void testCompose() {
        final BenchmarkState state = mBenchmarkRule.getState();
        while (state.keepRunning()) {
            mVibrator.vibrate(
                    VibrationEffect.startComposition()
                            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
    public void testComposePrimitivesTraces() throws InterruptedException {
        // Enable traces in separate test case, as they might affect performance.
        assumeTrue("Device without primitives support",
                mVibrator.areAllPrimitivesSupported(PRIMITIVE_CLICK, PRIMITIVE_TICK));

        int[] primitiveDurations = mVibrator.getPrimitiveDurations(PRIMITIVE_CLICK, PRIMITIVE_TICK);
        long durationMs = primitiveDurations[0] + 100 + primitiveDurations[1];
        VibrationEffect effect = VibrationEffect.startComposition()
                .addPrimitive(PRIMITIVE_CLICK)
                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f, 100)
                            .compose());
                .compose();
        benchmarkVibrateWithTraces(effect, durationMs);
    }

    @Test
    public void testEnvelopeEffect() throws InterruptedException {
        long durationMs = 100;
        VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
                .addControlPoint(1, 1, 50)
                .addControlPoint(0, 0, 50)
                .build();
        benchmarkVibrate(effect, durationMs, Vibrator::areEnvelopeEffectsSupported);
    }

    @Test
    public void testEnvelopeEffectTraces() throws InterruptedException {
        // Enable traces in separate test case, as they might affect performance.
        assumeTrue("Device without envelope effect support",
                mVibrator.areEnvelopeEffectsSupported());

        long durationMs = 100;
        VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
                .addControlPoint(1, 1, 50)
                .addControlPoint(0, 0, 50)
                .build();
        benchmarkVibrateWithTraces(effect, durationMs);
    }

    @Test
    public void testAreEffectsSupported() {
        final BenchmarkState state = mBenchmarkRule.getState();
        int[] effects = new int[]{VibrationEffect.EFFECT_CLICK, VibrationEffect.EFFECT_TICK};
        while (state.keepRunning()) {

        long elapsedTimeNs = 0;
        ManualBenchmarkState state = mStatusReporter.getBenchmarkState();
        while (state.keepRunning(elapsedTimeNs)) {
            long startTimeNs = SystemClock.elapsedRealtimeNanos();
            mVibrator.areEffectsSupported(effects);
            elapsedTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
        }
    }

    @Test
    public void testArePrimitivesSupported() {
        final BenchmarkState state = mBenchmarkRule.getState();
        int[] primitives = new int[]{VibrationEffect.Composition.PRIMITIVE_CLICK,
        int[] primitives = new int[]{PRIMITIVE_CLICK,
                VibrationEffect.Composition.PRIMITIVE_TICK};
        while (state.keepRunning()) {

        long elapsedTimeNs = 0;
        ManualBenchmarkState state = mStatusReporter.getBenchmarkState();
        while (state.keepRunning(elapsedTimeNs)) {
            long startTimeNs = SystemClock.elapsedRealtimeNanos();
            mVibrator.arePrimitivesSupported(primitives);
            elapsedTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
        }
    }

    @Test
    public void testCancelIdle() {
        long elapsedTimeNs = 0;
        ManualBenchmarkState state = mStatusReporter.getBenchmarkState();
        while (state.keepRunning(elapsedTimeNs)) {
            long startTimeNs = SystemClock.elapsedRealtimeNanos();
            mVibrator.cancel();
            elapsedTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
        }
    }

    @Test
    public void testCancelVibrating() {
        VibrationEffect effect = VibrationEffect.createOneShot(SECONDS.toMillis(2),
                VibrationEffect.DEFAULT_AMPLITUDE);

        long elapsedTimeNs = 0;
        ManualBenchmarkState state = mStatusReporter.getBenchmarkState();

        while (state.keepRunning(elapsedTimeNs)) {
            // Wait until the vibration is taken by the service and starts before cancelling.
            mVibrator.vibrate(effect);
            SystemClock.sleep(SERVICE_DELAY_MS);

            long startTimeNs = SystemClock.elapsedRealtimeNanos();
            mVibrator.cancel();
            elapsedTimeNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;
        }
    }

    private void assertVibratorIdle() throws InterruptedException {
        assertWithMessage("Vibrator should be idle before test")
                .that(mStateListener.awaitIdle(5, SECONDS)).isTrue();
        mStateListener.resetCounters();
    }

    private void benchmarkVibrate(VibrationEffect effect, long durationMs)
            throws InterruptedException {
        benchmarkVibrate(effect, durationMs, unused -> true);
    }

    private void benchmarkVibrate(VibrationEffect effect, long durationMs,
            Predicate<Vibrator> isEffectSupported) throws InterruptedException {
        ManualBenchmarkState state = mStatusReporter.getBenchmarkState();
        if (!mVibrator.hasVibrator() || !isEffectSupported.test(mVibrator)) {
            // Device does not support effect, cannot listen to state changes.
            benchmarkVibrate(effect);
            return;
        }
        assertVibratorIdle();
        long elapsedTimeNs = 0;
        while (state.keepRunning(elapsedTimeNs)) {
            elapsedTimeNs = measureVibrateWithStateChangeLatency(state, effect, durationMs);
        }
    }

    private void benchmarkVibrate(VibrationEffect effect) {
        ManualBenchmarkState state = mStatusReporter.getBenchmarkState();
        long elapsedTimeNs = 0;
        while (state.keepRunning(elapsedTimeNs)) {
            elapsedTimeNs = measureVibrate(effect);
        }
    }

    private void benchmarkVibrateWithTraces(VibrationEffect effect, long durationMs)
            throws InterruptedException {
        assumeTrue("Device without vibrator", mVibrator.hasVibrator());
        assertVibratorIdle();
        ManualBenchmarkState state = mStatusReporter.getBenchmarkState();
        try {
            mTraceMethods = new TraceMarkParser(VIBRATION_TRACES);
            startAsyncAtrace();
            long elapsedTimeNs = 0;
            while (state.keepRunning(elapsedTimeNs)) {
                elapsedTimeNs = measureVibrateWithTraces(effect);
            }
        } finally {
            stopAsyncAtraceAndDumpTraces();
            addTracesToState(state, durationMs);
        }
    }

    /**
     * Measure {@link Vibrator#vibrate} method call latency then cancel vibration.
     *
     * <p>This will cancel the vibrator and apply a rate-limiting sleep to wait for the service to
     * become idle before next iteration.
     */
    private long measureVibrate(VibrationEffect effect) {
        long startTimeNs = SystemClock.elapsedRealtimeNanos();
        mVibrator.vibrate(effect);
        long latencyNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;

        // Rate-limiting, stop vibration and wait for service to become idle.
        mVibrator.cancel();
        SystemClock.sleep(SERVICE_DELAY_MS);

        return latencyNs;
    }

    /**
     * Measure {@link Vibrator#vibrate} method call latency then wait for vibration to finish.
     *
     * <p>This will wait for the vibration and apply a rate-limiting sleep to wait for the service
     * to become idle before next iteration.
     */
    private long measureVibrateWithTraces(VibrationEffect effect) throws InterruptedException {
        long startTimeNs = SystemClock.elapsedRealtimeNanos();
        mVibrator.vibrate(effect);
        long latencyNs = SystemClock.elapsedRealtimeNanos() - startTimeNs;

        mStateListener.awaitVibrating(5, SECONDS);
        mStateListener.awaitIdle(5, SECONDS);
        mStateListener.resetCounters();

        // Rate-limiting, wait for service to become idle after vibration ended.
        SystemClock.sleep(SERVICE_DELAY_MS);

        return latencyNs;
    }

    /**
     * Measure {@link Vibrator#vibrate} method call latency with added metrics for state change.
     *
     * <p>This will add vibration latency as extra results and apply a rate-limiting sleep to wait
     * for the service to become idle before next iteration.
     */
    private long measureVibrateWithStateChangeLatency(ManualBenchmarkState state,
            VibrationEffect effect, long durationMs) throws InterruptedException {
        long vibrateTimeNs = SystemClock.elapsedRealtimeNanos();
        mVibrator.vibrate(effect);
        long vibrateLatencyNs = SystemClock.elapsedRealtimeNanos() - vibrateTimeNs;

        long startTimeNs = mStateListener.awaitVibrating(5, SECONDS)
                ? SystemClock.elapsedRealtimeNanos() : -1;
        long stopTimeNs = mStateListener.awaitIdle(5, SECONDS)
                ? SystemClock.elapsedRealtimeNanos() : -1;
        mStateListener.resetCounters();

        // Rate-limiting, wait for service to clean-up previous vibration and become idle.
        SystemClock.sleep(SERVICE_DELAY_MS);

        if (startTimeNs < 0) {
            Log.w(TAG, "Vibrator state ON never received for " + effect);
            return vibrateLatencyNs;
        }
        if (stopTimeNs < 0) {
            Log.w(TAG, "Vibrator state OFF never received for " + effect);
            return vibrateLatencyNs;
        }

        long startLatencyNs = startTimeNs - vibrateTimeNs;
        long stopLatencyNs = stopTimeNs - startTimeNs - durationMs * NANOS_PER_MS;
        if (stopLatencyNs < 0) {
            Log.w(TAG, "Vibration stopped " + -stopLatencyNs + "ns early for " + effect);
            stopLatencyNs = 0;
        }
        state.addExtraResult(VIBRATOR_STATE_START_LATENCY_METRIC_KEY, startLatencyNs);
        state.addExtraResult(VIBRATOR_STATE_STOP_LATENCY_METRIC_KEY, stopLatencyNs);
        return vibrateLatencyNs;
    }

    private static void startAsyncAtrace() {
        getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_START);
        // Wait for command to take effect.
        SystemClock.sleep(SECONDS.toMillis(1));
    }

    private void stopAsyncAtraceAndDumpTraces() {
        getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_STOP);
        if (mTraceMethods == null) {
            Log.w(TAG, "No trace methods being tracked");
            return;
        }
        InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(
                getInstrumentation().getUiAutomation().executeShellCommand(ATRACE_DUMP));
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            String line;
            while ((line = reader.readLine()) != null) {
                mTraceMethods.visit(line);
            }
        } catch (IOException e) {
            Log.w(TAG, "Failed to read the result of stopped atrace", e);
        }
    }

    private void addTracesToState(ManualBenchmarkState state, long durationMs) {
        if (mTraceMethods == null) {
            Log.w(TAG, "No trace methods");
            return;
        }
        mTraceMethods.forAllSlices((key, slices) -> {
            if (slices.size() < 2) {
                Log.w(TAG, "No enough trace samples for: " + key);
                return;
            }
            for (TraceMarkParser.TraceMarkSlice slice : slices) {
                long valueNs = (long) (slice.getDurationInSeconds() * NANOS_PER_S);
                state.addExtraResult(key, valueNs);
                if (durationMs > 0 && LATENCY_TRACES.contains(key)) {
                    addVibrationLatencyMetricForTrace(state, key, valueNs, durationMs);
                }
            }
        });
        Log.i(TAG, String.valueOf(mTraceMethods));
    }

    private void addVibrationLatencyMetricForTrace(ManualBenchmarkState state, String key,
            long valueNs, long durationMs) {
        long latencyNs = valueNs - durationMs * NANOS_PER_MS;
        if (latencyNs < 0) {
            Log.w(TAG, "Vibration stopped " + -latencyNs + "ns early for trace " + key);
            latencyNs = 0;
        }
        state.addExtraResult(key + LATENCY_METRIC_KEY_SUFFIX, latencyNs);
    }

    /** {@link Vibrator.OnVibratorStateChangedListener} implementation for testing. */
    private static final class VibratorStateListener
            implements Vibrator.OnVibratorStateChangedListener {
        private CountDownLatch mStartCount = new CountDownLatch(1);
        private CountDownLatch mStopCount = new CountDownLatch(1);

        @Override
        public synchronized void onVibratorStateChanged(boolean isVibrating) {
            if (isVibrating) {
                mStartCount.countDown();
            } else {
                mStopCount.countDown();
            }
        }

        public boolean awaitIdle(long timeout, TimeUnit unit) throws InterruptedException {
            return mStopCount.await(timeout, unit);
        }

        public boolean awaitVibrating(long timeout, TimeUnit unit) throws InterruptedException {
            return mStartCount.await(timeout, unit);
        }

        public synchronized void resetCounters() {
            mStartCount = new CountDownLatch(1);
            mStopCount = new CountDownLatch(1);
        }
    }
}
+21 −13

File changed.

Preview size limit exceeded, changes collapsed.

+1 −3
Original line number Diff line number Diff line
@@ -123,8 +123,6 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
    private static final String EXTERNAL_VIBRATOR_SERVICE = "external_vibrator_service";
    private static final String VIBRATOR_CONTROL_SERVICE =
            "android.frameworks.vibrator.IVibratorControlService/default";
    // To enable these logs, run:
    // 'adb shell setprop persist.log.tag.VibratorManagerService DEBUG && adb reboot'
    private static final boolean DEBUG = VibratorDebugUtils.isDebuggable(TAG);
    private static final VibrationAttributes DEFAULT_ATTRIBUTES =
            new VibrationAttributes.Builder().build();
@@ -1907,7 +1905,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub {
                        }
                        finishAppOpModeLocked(mCurrentSession.getCallerInfo());
                        clearCurrentSessionLocked();
                        Trace.asyncTraceEnd(Trace.TRACE_TAG_VIBRATOR, "vibration", 0);
                        Trace.asyncTraceEnd(TRACE_TAG_VIBRATOR, "vibration", 0);
                        // Start next vibration if it's waiting for the thread.
                        maybeStartNextSessionLocked();
                    } else if (mCurrentSession instanceof VendorVibrationSession session) {
+21 −14

File changed.

Preview size limit exceeded, changes collapsed.

+3 −3
Original line number Diff line number Diff line
@@ -138,9 +138,9 @@ class VintfHalVibratorManager {
        }

        @Override
        public void init(@NonNull Callbacks cb, @NonNull HalVibrator.Callbacks vibratorCb) {
        public void init(@NonNull Callbacks cb, @NonNull HalVibrator.Callbacks vibratorCallbacks) {
            mCallbacks = new CallbacksWrapper(cb);
            mNativeHandler.init(mCallbacks, vibratorCb);
            mNativeHandler.init(mCallbacks, vibratorCallbacks);

            // Load vibrator hardware info. The vibrator ids and manager capabilities are loaded
            // once and assumed unchanged for the lifecycle of this service. Each vibrator can still
@@ -155,7 +155,7 @@ class VintfHalVibratorManager {
            mVibratorIds = vibratorIds.orElseGet(() -> new int[0]);
            for (int id : mVibratorIds) {
                HalVibrator vibrator = mVibratorFactory.apply(id);
                vibrator.init(vibratorCb);
                vibrator.init(vibratorCallbacks);
                mVibrators.put(id, vibrator);
            }

Loading