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) {