diff options
author | Taran Singh <tarandeep@google.com> | 2020-11-03 11:46:37 -0800 |
---|---|---|
committer | Taran Singh <tarandeep@google.com> | 2020-11-11 18:05:26 +0000 |
commit | bf3370665517c6adddaec5e0146281ab0a93a632 (patch) | |
tree | d478b975ad48d65ad6ad9e290890ce2413960c97 | |
parent | a2a253995db2c584e6b153e7148a1b79b74e554d (diff) |
Measure baseline IMF latency (1/n)
Measure latency caused by IMF for IME operations like show / hide.
This CL uses apct tests for integration with Crystallball to measure
overall latency and breakdown of critical IMF methods.
In this CL we introduce a BaselineIme with minimal UI to measure
user-preceived delays in IME show/hide.
Refer to design doc in bug.
Bug: 167947940
Test: atest ImePerfTests and also refer to README.md
Change-Id: I8efff52fe25952d452aef7f059400c63d1a9fa4a
9 files changed, 913 insertions, 0 deletions
diff --git a/apct-tests/perftests/inputmethod/Android.bp b/apct-tests/perftests/inputmethod/Android.bp new file mode 100644 index 000000000000..463ac9b8b0c8 --- /dev/null +++ b/apct-tests/perftests/inputmethod/Android.bp @@ -0,0 +1,30 @@ +// Copyright (C) 2020 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. + +android_test { + name: "ImePerfTests", + srcs: ["src/**/*.java"], + static_libs: [ + "androidx.test.rules", + "androidx.annotation_annotation", + "apct-perftests-utils", + "collector-device-lib", + "compatibility-device-util-axt", + "platform-test-annotations", + ], + test_suites: ["device-tests"], + data: [":perfetto_artifacts"], + platform_apis: true, + certificate: "platform", +} diff --git a/apct-tests/perftests/inputmethod/AndroidManifest.xml b/apct-tests/perftests/inputmethod/AndroidManifest.xml new file mode 100644 index 000000000000..1fb0b880953b --- /dev/null +++ b/apct-tests/perftests/inputmethod/AndroidManifest.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.perftests.inputmethod"> + + <application> + <uses-library android:name="android.test.runner" /> + <activity android:name="android.perftests.utils.PerfTestActivity" + android:exported="true"> + <intent-filter> + <action android:name="com.android.perftests.core.PERFTEST" /> + </intent-filter> + </activity> + <service android:name="android.inputmethod.ImePerfTest$BaselineIme" + android:process=":BaselineIME" + android:label="Baseline IME" + android:permission="android.permission.BIND_INPUT_METHOD" + android:exported="true"> + <intent-filter> + <action android:name="android.view.InputMethod"/> + </intent-filter> + <meta-data android:name="android.view.im" + android:resource="@xml/simple_method"/> + </service> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.perftests.inputmethod"> + <meta-data android:name="listener" android:value="android.inputmethod.ImePerfRunPrecondition" /> + </instrumentation> +</manifest> diff --git a/apct-tests/perftests/inputmethod/AndroidTest.xml b/apct-tests/perftests/inputmethod/AndroidTest.xml new file mode 100644 index 000000000000..1ec0cbadb6bd --- /dev/null +++ b/apct-tests/perftests/inputmethod/AndroidTest.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> +<configuration description="Runs ImePerfTests metric instrumentation."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-metric-instrumentation" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="ImePerfTests.apk" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> + <option name="force-skip-system-props" value="true" /> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP" /> + <option name="run-command" value="cmd window dismiss-keyguard" /> + <option name="run-command" value="cmd package compile -m speed com.android.perftests.inputmethod" /> + </target_preparer> + + <!-- Needed for pushing the trace config file --> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="push-file" key="trace_config_detailed.textproto" value="/data/misc/perfetto-traces/trace_config.textproto" /> + <!--Install the content provider automatically when we push some file in sdcard folder.--> + <!--Needed to avoid the installation during the test suite.--> + <option name="push-file" key="trace_config_detailed.textproto" value="/sdcard/sample.textproto" /> + </target_preparer> + + <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> + <option name="isolated-storage" value="false" /> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.perftests.inputmethod" /> + <option name="hidden-api-checks" value="false"/> + + <!-- Listener related args for collecting the traces and waiting for the device to stabilize. --> + <option name="device-listeners" value="android.device.collectors.ProcLoadListener,android.device.collectors.PerfettoListener" /> + + <!-- Guarantee that user defined RunListeners will be running before any of the default listeners defined in this runner. --> + <option name="instrumentation-arg" key="newRunListenerMode" value="true" /> + + <!-- Kill background operations --> + <option name="instrumentation-arg" key="kill-bg" value="true" /> + + <!-- ProcLoadListener related arguments --> + <!-- Wait for device last minute threshold to reach 3 with 2 minute timeout before starting the test run --> + <option name="instrumentation-arg" key="procload-collector:per_run" value="true" /> + <option name="instrumentation-arg" key="proc-loadavg-threshold" value="3" /> + <option name="instrumentation-arg" key="proc-loadavg-timeout" value="120000" /> + <option name="instrumentation-arg" key="proc-loadavg-interval" value="10000" /> + + <!-- PerfettoListener related arguments --> + <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true" /> + <option name="instrumentation-arg" key="perfetto_config_file" value="trace_config.textproto" /> + </test> + + <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> + <option name="directory-keys" value="/data/local/tmp/ImePerfTests" /> + <!-- Needed for pulling the collected trace config on to the host --> + <option name="pull-pattern-keys" value="perfetto_file_path" /> + </metrics_collector> +</configuration> diff --git a/apct-tests/perftests/inputmethod/README.md b/apct-tests/perftests/inputmethod/README.md new file mode 100644 index 000000000000..8ba20879591b --- /dev/null +++ b/apct-tests/perftests/inputmethod/README.md @@ -0,0 +1,40 @@ +## IMF performance tests + +These tests are adaptation of Window Manager perf tests (apct-tests/perftests/windowmanager). + +### Precondition +To reduce the variance of the test, if `perf-setup` (platform_testing/scripts/perf-setup) +is available, it is better to use the following instructions to lock CPU and GPU frequencies. +``` +m perf-setup +PERF_SETUP_PATH=/data/local/tmp/perf-setup.sh +adb push $OUT/$PERF_SETUP_PATH $PERF_SETUP_PATH +adb shell chmod +x $PERF_SETUP_PATH +adb shell $PERF_SETUP_PATH +``` + +### Example to run +Use `atest` +``` +atest ImePerfTests:ImePerfTest -- \ + --module-arg ImePerfTests:instrumentation-arg:profiling-iterations:=20 + +``` +Note: `instrumentation-arg:kill-bg:=true` is already defined in the AndroidText.xml + +Use `am instrument` +``` +adb shell am instrument -w -r -e class android.inputmethod.ImePerfTest \ + -e listener android.inputmethod.ImePerfRunPrecondition \ + -e kill-bg true \ + com.android.perftests.inputmethod/androidx.test.runner.AndroidJUnitRunner +``` +* `kill-bg` is optional. + +Test arguments + - kill-bg + * boolean: Kill background process before running test. + - profiling-iterations + * int: Run the extra iterations with enabling method profiling. + - profiling-sampling + * int: The interval (0=trace each method, default is 10) of sample profiling in microseconds. diff --git a/apct-tests/perftests/inputmethod/res/xml/simple_method.xml b/apct-tests/perftests/inputmethod/res/xml/simple_method.xml new file mode 100644 index 000000000000..87cb1ad205a0 --- /dev/null +++ b/apct-tests/perftests/inputmethod/res/xml/simple_method.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<!-- Configuration info for an input method --> +<input-method xmlns:android="http://schemas.android.com/apk/res/android" /> diff --git a/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfRunPrecondition.java b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfRunPrecondition.java new file mode 100644 index 000000000000..fc48fd5271a8 --- /dev/null +++ b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfRunPrecondition.java @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2020 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.inputmethod; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.inputmethod.ImePerfTestBase.executeShellCommand; +import static android.inputmethod.ImePerfTestBase.runWithShellPermissionIdentity; + +import android.app.ActivityManager; +import android.app.ActivityManager.RunningAppProcessInfo; +import android.app.ActivityTaskManager; +import android.content.Context; +import android.inputmethod.ImePerfTestBase.SettingsSession; +import android.os.BatteryManager; +import android.os.Bundle; +import android.os.SystemClock; +import android.provider.Settings; +import android.util.Log; +import android.view.WindowManagerPolicyConstants; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.policy.PhoneWindow; + +import org.junit.runner.Description; +import org.junit.runner.Result; +import org.junit.runner.notification.RunListener; + +import java.util.List; + +/** Prepare the preconditions before running performance test. */ +public class ImePerfRunPrecondition extends RunListener { + private static final String TAG = ImePerfRunPrecondition.class.getSimpleName(); + + private static final String ARGUMENT_LOG_ONLY = "log"; + private static final String ARGUMENT_KILL_BACKGROUND = "kill-bg"; + private static final String ARGUMENT_PROFILING_ITERATIONS = "profiling-iterations"; + private static final String ARGUMENT_PROFILING_SAMPLING = "profiling-sampling"; + private static final String DEFAULT_PROFILING_ITERATIONS = "10"; + private static final String DEFAULT_PROFILING_SAMPLING_US = "10"; + private static final long KILL_BACKGROUND_WAIT_MS = 3000; + + /** The requested iterations to run with method profiling. */ + static int sProfilingIterations; + + /** The interval of sample profiling in microseconds. */ + static int sSamplingIntervalUs; + + private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext(); + private long mWaitPreconditionDoneMs = 500; + + private final SettingsSession<Integer> mStayOnWhilePluggedInSetting = new SettingsSession<>( + Settings.Global.getInt(mContext.getContentResolver(), + Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0), + value -> executeShellCommand(String.format("settings put global %s %d", + Settings.Global.STAY_ON_WHILE_PLUGGED_IN, value))); + + private final SettingsSession<Integer> mNavigationModeSetting = new SettingsSession<>( + mContext.getResources().getInteger( + com.android.internal.R.integer.config_navBarInteractionMode), + value -> { + final String navOverlay; + switch (value) { + case WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL: + default: + navOverlay = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY; + break; + } + executeShellCommand("cmd overlay enable-exclusive " + navOverlay); + }); + + /** It only executes once before all tests. */ + @Override + public void testRunStarted(Description description) { + final Bundle arguments = InstrumentationRegistry.getArguments(); + // If true, it only logs the method names without running. + final boolean skip = Boolean.parseBoolean(arguments.getString(ARGUMENT_LOG_ONLY, "false")); + Log.i(TAG, "arguments=" + arguments); + if (skip) { + return; + } + sProfilingIterations = Integer.parseInt( + arguments.getString(ARGUMENT_PROFILING_ITERATIONS, DEFAULT_PROFILING_ITERATIONS)); + sSamplingIntervalUs = Integer.parseInt( + arguments.getString(ARGUMENT_PROFILING_SAMPLING, DEFAULT_PROFILING_SAMPLING_US)); + + // Use same navigation mode (gesture navigation) across all devices and tests + // for consistency. + mNavigationModeSetting.set(WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL); + // Keep the device awake during testing. + mStayOnWhilePluggedInSetting.set(BatteryManager.BATTERY_PLUGGED_ANY); + + runWithShellPermissionIdentity(() -> { + final ActivityTaskManager atm = mContext.getSystemService(ActivityTaskManager.class); + atm.removeAllVisibleRecentTasks(); + atm.removeRootTasksWithActivityTypes(new int[] { ACTIVITY_TYPE_STANDARD, + ACTIVITY_TYPE_ASSISTANT, ACTIVITY_TYPE_RECENTS, ACTIVITY_TYPE_UNDEFINED }); + }); + PhoneWindow.sendCloseSystemWindows(mContext, "ImePerfTests"); + + if (Boolean.parseBoolean(arguments.getString(ARGUMENT_KILL_BACKGROUND))) { + runWithShellPermissionIdentity(this::killBackgroundProcesses); + mWaitPreconditionDoneMs = KILL_BACKGROUND_WAIT_MS; + } + // Wait a while for the precondition setup to complete. + SystemClock.sleep(mWaitPreconditionDoneMs); + } + + private void killBackgroundProcesses() { + Log.i(TAG, "Killing background processes..."); + final ActivityManager am = mContext.getSystemService(ActivityManager.class); + final List<RunningAppProcessInfo> processes = am.getRunningAppProcesses(); + if (processes == null) { + return; + } + for (RunningAppProcessInfo processInfo : processes) { + if (processInfo.importanceReasonCode == RunningAppProcessInfo.REASON_UNKNOWN + && processInfo.importance > RunningAppProcessInfo.IMPORTANCE_SERVICE) { + for (String pkg : processInfo.pkgList) { + am.forceStopPackage(pkg); + } + } + } + } + + /** It only executes once after all tests. */ + @Override + public void testRunFinished(Result result) { + mNavigationModeSetting.close(); + mStayOnWhilePluggedInSetting.close(); + } +} diff --git a/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java new file mode 100644 index 000000000000..303c667351d1 --- /dev/null +++ b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java @@ -0,0 +1,380 @@ +/* + * Copyright (C) 2020 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.inputmethod; + +import static android.perftests.utils.ManualBenchmarkState.StatsReport; +import static android.perftests.utils.PerfTestActivity.ID_EDITOR; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.inputmethodservice.InputMethodService; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.perftests.utils.ManualBenchmarkState; +import android.perftests.utils.ManualBenchmarkState.ManualBenchmarkTest; +import android.perftests.utils.PerfManualStatusReporter; +import android.perftests.utils.TraceMarkParser; +import android.perftests.utils.TraceMarkParser.TraceMarkSlice; +import android.provider.Settings; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; +import android.view.WindowInsetsAnimation; +import android.view.WindowInsetsController; +import android.view.inputmethod.InputMethodManager; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.filters.LargeTest; + +import com.android.compatibility.common.util.PollingCheck; + +import junit.framework.Assert; + +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.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +/** Measure the performance of internal methods in Input Method framework by trace tag. */ +@LargeTest +public class ImePerfTest extends ImePerfTestBase + implements ManualBenchmarkState.CustomizedIterationListener { + private static final String TAG = ImePerfTest.class.getSimpleName(); + + @Rule + public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter(); + + @Rule + public final PerfTestActivityRule mActivityRule = new PerfTestActivityRule(); + + /** + * IMF common methods to log for show/hide in trace. + */ + private String[] mCommonMethods = { + "IC.pendingAnim", + "IMMS.applyImeVisibility", + "applyPostLayoutPolicy", + "applyWindowSurfaceChanges", + "ISC.onPostLayout" + }; + + /** IMF show methods to log in trace. */ + private String[] mShowMethods = { + "IC.showRequestFromIme", + "IC.showRequestFromApi", + "IC.showRequestFromApiToImeReady", + "IC.pendingAnim", + "IMMS.applyImeVisibility", + "IMMS.showMySoftInput", + "IMMS.showSoftInput", + "IMS.showSoftInput", + "IMS.startInput", + "WMS.showImePostLayout" + }; + + /** IMF hide lifecycle methods to log in trace. */ + private String[] mHideMethods = { + "IC.hideRequestFromIme", + "IC.hideRequestFromApi", + "IMMS.hideMySoftInput", + "IMMS.hideSoftInput", + "IMS.hideSoftInput", + "WMS.hideIme" + }; + + /** + * IMF methods to log in trace. + */ + private TraceMarkParser mTraceMethods; + + private boolean mIsTraceStarted; + + /** + * Ime Session for {@link BaselineIme}. + */ + private static class ImeSession implements AutoCloseable { + + private static final long TIMEOUT = 2000; + private final ComponentName mImeName; + private Context mContext = getInstrumentation().getContext(); + + ImeSession(ComponentName ime) throws Exception { + mImeName = ime; + // using adb, enable and set Baseline IME. + executeShellCommand("ime reset"); + executeShellCommand("ime enable " + ime.flattenToShortString()); + executeShellCommand("ime set " + ime.flattenToShortString()); + PollingCheck.check("Make sure that BaselineIme becomes available " + + getCurrentInputMethodId(), TIMEOUT, + () -> ime.equals(getCurrentInputMethodId())); + } + + @Override + public void close() throws Exception { + executeShellCommand("ime reset"); + PollingCheck.check("Make sure that Baseline IME becomes unavailable", TIMEOUT, () -> + mContext.getSystemService(InputMethodManager.class) + .getEnabledInputMethodList() + .stream() + .noneMatch(info -> mImeName.equals(info.getComponent()))); + } + + @Nullable + private ComponentName getCurrentInputMethodId() { + return ComponentName.unflattenFromString( + Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.DEFAULT_INPUT_METHOD)); + } + } + + /** + * A minimal baseline IME (that has a single static view) used to measure IMF latency. + */ + public static class BaselineIme extends InputMethodService { + + public static final int HEIGHT_DP = 100; + + @Override + public View onCreateInputView() { + final ViewGroup view = new FrameLayout(this); + final View inner = new View(this); + final float density = getResources().getDisplayMetrics().density; + final int height = (int) (HEIGHT_DP * density); + view.setPadding(0, 0, 0, 0); + view.addView(inner, new FrameLayout.LayoutParams(MATCH_PARENT, height)); + inner.setBackgroundColor(0xff01fe10); // green + return view; + } + + static ComponentName getName(Context context) { + return new ComponentName(context, BaselineIme.class); + } + } + + @Test + @ManualBenchmarkTest( + targetTestDurationNs = 10 * TIME_1_S_IN_NS, + statsReport = @StatsReport( + flags = StatsReport.FLAG_ITERATION | StatsReport.FLAG_MEAN + | StatsReport.FLAG_MIN | StatsReport.FLAG_MAX + | StatsReport.FLAG_COEFFICIENT_VAR)) + public void testShowIme() throws Throwable { + testShowOrHideIme(true /* show */); + } + + @Test + @ManualBenchmarkTest( + targetTestDurationNs = 10 * TIME_1_S_IN_NS, + statsReport = @StatsReport( + flags = StatsReport.FLAG_ITERATION | StatsReport.FLAG_MEAN + | StatsReport.FLAG_MIN | StatsReport.FLAG_MAX + | StatsReport.FLAG_COEFFICIENT_VAR)) + public void testHideIme() throws Throwable { + testShowOrHideIme(false /* show */); + } + + private void testShowOrHideIme(final boolean show) throws Throwable { + mTraceMethods = new TraceMarkParser(buildArray( + mCommonMethods, show ? mShowMethods : mHideMethods)); + final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + state.setCustomizedIterations(getProfilingIterations(), this); + long measuredTimeNs = 0; + try (ImeSession imeSession = new ImeSession(BaselineIme.getName( + getInstrumentation().getContext()))) { + final AtomicReference<CountDownLatch> latchStart = new AtomicReference<>(); + final AtomicReference<CountDownLatch> latchEnd = new AtomicReference<>(); + final Activity activity = getActivityWithFocus(); + + // call IME show/hide + final WindowInsetsController controller = + activity.getWindow().getDecorView().getWindowInsetsController(); + + while (state.keepRunning(measuredTimeNs)) { + setImeListener(activity, latchStart, latchEnd); + latchStart.set(new CountDownLatch(show ? 1 : 2)); + latchEnd.set(new CountDownLatch(2)); + // For measuring hide, lets show IME first. + if (!show) { + activity.runOnUiThread(() -> { + controller.show(WindowInsets.Type.ime()); + }); + PollingCheck.check("IME show animation should finish ", TIMEOUT_1_S_IN_MS, + () -> latchStart.get().getCount() == 1 + && latchEnd.get().getCount() == 1); + } + if (!mIsTraceStarted && !state.isWarmingUp()) { + startAsyncAtrace(); + mIsTraceStarted = true; + } + + AtomicLong startTime = new AtomicLong(); + activity.runOnUiThread(() -> { + startTime.set(SystemClock.elapsedRealtimeNanos()); + if (show) { + controller.show(WindowInsets.Type.ime()); + } else { + controller.hide(WindowInsets.Type.ime()); + } + }); + + measuredTimeNs = waitForAnimationStart(latchStart, startTime); + + // hide IME before next iteration. + if (show) { + activity.runOnUiThread(() -> controller.hide(WindowInsets.Type.ime())); + try { + latchEnd.get().await(TIMEOUT_1_S_IN_MS * 5, TimeUnit.MILLISECONDS); + if (latchEnd.get().getCount() != 0) { + Assert.fail("IME hide animation should finish."); + } + } catch (InterruptedException e) { + } + } + } + } finally { + if (mIsTraceStarted) { + stopAsyncAtrace(); + } + } + mActivityRule.finishActivity(); + + addResultToState(state); + } + + private long waitForAnimationStart( + AtomicReference<CountDownLatch> latchStart, AtomicLong startTime) { + try { + latchStart.get().await(TIMEOUT_1_S_IN_MS * 5, TimeUnit.MILLISECONDS); + if (latchStart.get().getCount() != 0) { + Assert.fail("IME animation should start " + latchStart.get().getCount()); + } + } catch (InterruptedException e) { } + + return SystemClock.elapsedRealtimeNanos() - startTime.get(); + } + + private void addResultToState(ManualBenchmarkState state) { + mTraceMethods.forAllSlices((key, slices) -> { + for (TraceMarkSlice slice : slices) { + state.addExtraResult(key, (long) (slice.getDurationInSeconds() * NANOS_PER_S)); + } + }); + Log.i(TAG, String.valueOf(mTraceMethods)); + } + + private Activity getActivityWithFocus() throws Exception { + final Activity activity = mActivityRule.launchActivity(); + PollingCheck.check("Activity onResume()", TIMEOUT_1_S_IN_MS, + () -> activity.isResumed()); + + View editor = activity.findViewById(ID_EDITOR); + editor.requestFocus(); + + // wait till editor is focused so we don't count activity/view latency. + PollingCheck.check("Editor is focused", TIMEOUT_1_S_IN_MS, + () -> editor.isFocused()); + getInstrumentation().waitForIdleSync(); + + return activity; + } + + private void setImeListener(Activity activity, + @NonNull AtomicReference<CountDownLatch> latchStart, + @Nullable AtomicReference<CountDownLatch> latchEnd) { + // set IME animation listener + activity.getWindow().getDecorView().setWindowInsetsAnimationCallback( + new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { + @NonNull + @Override + public WindowInsetsAnimation.Bounds onStart( + @NonNull WindowInsetsAnimation animation, + @NonNull WindowInsetsAnimation.Bounds bounds) { + latchStart.get().countDown(); + return super.onStart(animation, bounds); + } + + @NonNull + @Override + public WindowInsets onProgress(@NonNull WindowInsets insets, + @NonNull List<WindowInsetsAnimation> runningAnimations) { + return insets; + } + + @Override + public void onEnd(@NonNull WindowInsetsAnimation animation) { + super.onEnd(animation); + if (latchEnd != null) { + latchEnd.get().countDown(); + } + } + }); + } + + private void startAsyncAtrace() throws IOException { + mIsTraceStarted = true; + // IMF uses 'wm' component for trace in InputMethodService, InputMethodManagerService, + // WindowManagerService and 'view' for client window (InsetsController). + // TODO(b/167947940): Consider a separate input_method atrace + UI_AUTOMATION.executeShellCommand("atrace -b 32768 --async_start wm view"); + // Avoid atrace isn't ready immediately. + SystemClock.sleep(TimeUnit.NANOSECONDS.toMillis(TIME_1_S_IN_NS)); + } + + private void stopAsyncAtrace() { + if (!mIsTraceStarted) { + return; + } + final ParcelFileDescriptor pfd = UI_AUTOMATION.executeShellCommand("atrace --async_stop"); + mIsTraceStarted = false; + final InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfd); + 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); + } + } + + @Override + public void onStart(int iteration) { + // Do not capture trace when profiling because the result will be much slower. + stopAsyncAtrace(); + } + + @Override + public void onFinished(int iteration) { + // do nothing. + } +} diff --git a/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTestBase.java b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTestBase.java new file mode 100644 index 000000000000..1a861d7ba87b --- /dev/null +++ b/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTestBase.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2020 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.inputmethod; + +import static android.perftests.utils.PerfTestActivity.INTENT_EXTRA_ADD_EDIT_TEXT; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import android.app.KeyguardManager; +import android.app.UiAutomation; +import android.content.Context; +import android.content.Intent; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.perftests.utils.PerfTestActivity; + + +import androidx.test.rule.ActivityTestRule; + +import org.junit.BeforeClass; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Objects; +import java.util.function.Consumer; + +public class ImePerfTestBase { + static final UiAutomation UI_AUTOMATION = getInstrumentation().getUiAutomation(); + static final long NANOS_PER_S = 1000L * 1000 * 1000; + static final long TIME_1_S_IN_NS = 1 * NANOS_PER_S; + static final long TIMEOUT_1_S_IN_MS = 1 * 1000L; + + @BeforeClass + public static void setUpOnce() { + final Context context = getInstrumentation().getContext(); + + if (!context.getSystemService(PowerManager.class).isInteractive() + || context.getSystemService(KeyguardManager.class).isKeyguardLocked()) { + executeShellCommand("input keyevent KEYCODE_WAKEUP"); + executeShellCommand("wm dismiss-keyguard"); + } + context.startActivity(new Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + /** + * Executes shell command with reading the output. It may also used to block until the current + * command is completed. + */ + static ByteArrayOutputStream executeShellCommand(String command) { + final ParcelFileDescriptor pfd = UI_AUTOMATION.executeShellCommand(command); + final byte[] buf = new byte[512]; + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int bytesRead; + try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) { + while ((bytesRead = fis.read(buf)) != -1) { + bytes.write(buf, 0, bytesRead); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return bytes; + } + + /** Returns how many iterations should run with method tracing. */ + static int getProfilingIterations() { + return ImePerfRunPrecondition.sProfilingIterations; + } + + static void runWithShellPermissionIdentity(Runnable runnable) { + UI_AUTOMATION.adoptShellPermissionIdentity(); + try { + runnable.run(); + } finally { + UI_AUTOMATION.dropShellPermissionIdentity(); + } + } + + static class SettingsSession<T> implements AutoCloseable { + private final Consumer<T> mSetter; + private final T mOriginalValue; + private boolean mChanged; + + SettingsSession(T originalValue, Consumer<T> setter) { + mOriginalValue = originalValue; + mSetter = setter; + } + + void set(T value) { + if (Objects.equals(value, mOriginalValue)) { + mChanged = false; + return; + } + mSetter.accept(value); + mChanged = true; + } + + @Override + public void close() { + if (mChanged) { + mSetter.accept(mOriginalValue); + } + } + } + + /** + * Provides an activity that keeps screen on and is able to wait for a stable lifecycle stage. + */ + static class PerfTestActivityRule extends ActivityTestRule<PerfTestActivity> { + private final Intent mStartIntent = + new Intent(getInstrumentation().getTargetContext(), PerfTestActivity.class); + + PerfTestActivityRule() { + this(false /* launchActivity */); + } + + PerfTestActivityRule(boolean launchActivity) { + super(PerfTestActivity.class, false /* initialTouchMode */, launchActivity); + } + + @Override + protected Intent getActivityIntent() { + return mStartIntent; + } + + @Override + public PerfTestActivity launchActivity(Intent intent) { + intent.putExtra(INTENT_EXTRA_ADD_EDIT_TEXT, true); + return super.launchActivity(intent); + } + + PerfTestActivity launchActivity() { + return launchActivity(mStartIntent); + } + } + + static String[] buildArray(String[]... arrays) { + int length = 0; + for (String[] array : arrays) { + length += array.length; + } + String[] newArray = new String[length]; + int offset = 0; + for (String[] array : arrays) { + System.arraycopy(array, 0, newArray, offset, array.length); + offset += array.length; + } + return newArray; + } +} diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java b/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java index e934feb01a84..f3bea17b2f0d 100644 --- a/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java +++ b/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java @@ -21,6 +21,8 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.view.WindowManager; +import android.widget.EditText; +import android.widget.LinearLayout; /** * A simple activity used for testing, e.g. performance of activity switching, or as a base @@ -28,6 +30,8 @@ import android.view.WindowManager; */ public class PerfTestActivity extends Activity { public static final String INTENT_EXTRA_KEEP_SCREEN_ON = "keep_screen_on"; + public static final String INTENT_EXTRA_ADD_EDIT_TEXT = "add_edit_text"; + public static final int ID_EDITOR = 3252356; @Override protected void onCreate(Bundle savedInstanceState) { @@ -35,6 +39,15 @@ public class PerfTestActivity extends Activity { if (getIntent().getBooleanExtra(INTENT_EXTRA_KEEP_SCREEN_ON, false)) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } + if (getIntent().getBooleanExtra(INTENT_EXTRA_ADD_EDIT_TEXT, false)) { + final LinearLayout layout = new LinearLayout(this); + layout.setOrientation(LinearLayout.VERTICAL); + + final EditText editText = new EditText(this); + editText.setId(ID_EDITOR); + layout.addView(editText); + setContentView(layout); + } } public static Intent createLaunchIntent(Context context) { |