summaryrefslogtreecommitdiff
path: root/apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java')
-rw-r--r--apct-tests/perftests/inputmethod/src/android/inputmethod/ImePerfTest.java380
1 files changed, 380 insertions, 0 deletions
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.
+ }
+}