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
diff --git a/apct-tests/perftests/inputmethod/Android.bp b/apct-tests/perftests/inputmethod/Android.bp
new file mode 100644
index 0000000..463ac9b
--- /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 0000000..1fb0b88
--- /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 0000000..1ec0cba
--- /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 0000000..8ba2087
--- /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 0000000..87cb1ad
--- /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 0000000..fc48fd5
--- /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 0000000..303c667
--- /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 0000000..1a861d7
--- /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 e934feb..f3bea17 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.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 @@
*/
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 @@
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) {