| /* |
| * 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.perftests.utils.TestUtils.getOnMainSync; |
| 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 static org.junit.Assert.assertTrue; |
| |
| import android.annotation.UiThread; |
| import android.app.Activity; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.inputmethodservice.InputMethodService; |
| import android.os.Process; |
| 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.AtomicBoolean; |
| 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(); |
| private static final long ANIMATION_NOT_STARTED = -1; |
| |
| @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", |
| "IMS.updateFullscreenMode", |
| "IMS.onComputeInsets", |
| "IMS.showWindow" |
| }; |
| |
| /** IMF show methods to log in trace. */ |
| private String[] mShowMethodsCold = { |
| "IMS.bindInput", |
| "IMS.initializeInternal", |
| "IMS.restartInput", |
| "IMS.onCreate", |
| "IMS.initSoftInputWindow", |
| "IMS.resetStateForNewConfiguration", |
| "IMMS.onServiceConnected", |
| "IMMS.sessionCreated", |
| "IMMS.startInputOrWindowGainedFocus" |
| }; |
| |
| /** 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; |
| private static int sPid; |
| |
| @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 |
| sPid = Process.myPid(); |
| return view; |
| } |
| |
| static int getPid() { |
| return sPid; |
| } |
| |
| 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 testShowImeWarm() throws Throwable { |
| testShowOrHideImeWarm(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 { |
| testShowOrHideImeWarm(false /* 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 testShowImeCold() throws Throwable { |
| mTraceMethods = new TraceMarkParser( |
| buildArray(mCommonMethods, mShowMethods, mShowMethodsCold)); |
| |
| final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState(); |
| state.setCustomizedIterations(getProfilingIterations(), this); |
| if (state.isWarmingUp()) { |
| // we don't need to warmup for cold start. |
| return; |
| } |
| |
| long measuredTimeNs = 0; |
| while (state.keepRunning(measuredTimeNs)) { |
| killBaselineIme(); |
| try (ImeSession imeSession = new ImeSession(BaselineIme.getName( |
| getInstrumentation().getContext()))) { |
| final AtomicReference<CountDownLatch> latchStart = new AtomicReference<>(); |
| final Activity activity = getActivityWithFocus(); |
| |
| setImeListener(activity, latchStart, null /* latchEnd */); |
| latchStart.set(new CountDownLatch(1)); |
| |
| if (!mIsTraceStarted) { |
| startAsyncAtrace(); |
| } |
| |
| final WindowInsetsController controller = |
| activity.getWindow().getDecorView().getWindowInsetsController(); |
| AtomicLong startTime = new AtomicLong(); |
| activity.runOnUiThread(() -> { |
| startTime.set(SystemClock.elapsedRealtimeNanos()); |
| controller.show(WindowInsets.Type.ime()); |
| }); |
| |
| measuredTimeNs = waitForAnimationStart(latchStart, startTime); |
| mActivityRule.finishActivity(); |
| } |
| } |
| stopAsyncAtrace(); |
| addResultToState(state); |
| } |
| |
| private void killBaselineIme() { |
| assertTrue("PID of test and IME can't be same", |
| Process.myPid() != BaselineIme.getPid()); |
| Process.killProcess(BaselineIme.getPid()); |
| } |
| |
| private void testShowOrHideImeWarm(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); |
| // For measuring hide, lets show IME first. |
| if (!show) { |
| initLatch(latchStart, latchEnd); |
| AtomicBoolean showCalled = new AtomicBoolean(); |
| getInstrumentation().runOnMainSync(() -> { |
| if (!isImeVisible(activity)) { |
| controller.show(WindowInsets.Type.ime()); |
| showCalled.set(true); |
| } |
| }); |
| if (showCalled.get()) { |
| PollingCheck.check("IME show animation should finish ", |
| TIMEOUT_1_S_IN_MS * 3, |
| () -> latchStart.get().getCount() == 0 |
| && latchEnd.get().getCount() == 0); |
| } |
| } |
| if (!mIsTraceStarted && !state.isWarmingUp()) { |
| startAsyncAtrace(); |
| mIsTraceStarted = true; |
| } |
| |
| AtomicLong startTime = new AtomicLong(); |
| AtomicBoolean unexpectedVisibility = new AtomicBoolean(); |
| initLatch(latchStart, latchEnd); |
| getInstrumentation().runOnMainSync(() -> { |
| boolean isVisible = isImeVisible(activity); |
| startTime.set(SystemClock.elapsedRealtimeNanos()); |
| |
| if (show && !isVisible) { |
| controller.show(WindowInsets.Type.ime()); |
| } else if (!show && isVisible) { |
| controller.hide(WindowInsets.Type.ime()); |
| } else { |
| // ignore this iteration as unexpected IME visibility was encountered. |
| unexpectedVisibility.set(true); |
| } |
| }); |
| |
| if (!unexpectedVisibility.get()) { |
| long timeElapsed = waitForAnimationStart(latchStart, startTime); |
| if (timeElapsed != ANIMATION_NOT_STARTED) { |
| measuredTimeNs = timeElapsed; |
| // wait for animation to end or we may start two animations and timing |
| // will not be measured accurately. |
| waitForAnimationEnd(latchEnd); |
| } |
| } |
| |
| // hide IME before next iteration. |
| if (show) { |
| initLatch(latchStart, latchEnd); |
| activity.runOnUiThread(() -> controller.hide(WindowInsets.Type.ime())); |
| try { |
| latchEnd.get().await(TIMEOUT_1_S_IN_MS * 5, TimeUnit.MILLISECONDS); |
| if (latchEnd.get().getCount() != 0 |
| && getOnMainSync(() -> isImeVisible(activity))) { |
| Assert.fail("IME hide animation should finish."); |
| } |
| } catch (InterruptedException e) { |
| } |
| } |
| } |
| } finally { |
| if (mIsTraceStarted) { |
| stopAsyncAtrace(); |
| } |
| } |
| mActivityRule.finishActivity(); |
| |
| addResultToState(state); |
| } |
| |
| private void initLatch(AtomicReference<CountDownLatch> latchStart, |
| AtomicReference<CountDownLatch> latchEnd) { |
| latchStart.set(new CountDownLatch(1)); |
| latchEnd.set(new CountDownLatch(1)); |
| } |
| |
| @UiThread |
| private boolean isImeVisible(@NonNull final Activity activity) { |
| return activity.getWindow().getDecorView().getRootWindowInsets().isVisible( |
| WindowInsets.Type.ime()); |
| } |
| |
| private long waitForAnimationStart( |
| AtomicReference<CountDownLatch> latchStart, AtomicLong startTime) { |
| try { |
| latchStart.get().await(5, TimeUnit.SECONDS); |
| if (latchStart.get().getCount() != 0) { |
| return ANIMATION_NOT_STARTED; |
| } |
| } catch (InterruptedException e) { } |
| |
| return SystemClock.elapsedRealtimeNanos() - startTime.get(); |
| } |
| |
| private void waitForAnimationEnd(AtomicReference<CountDownLatch> latchEnd) { |
| try { |
| latchEnd.get().await(3, TimeUnit.SECONDS); |
| } catch (InterruptedException e) { } |
| } |
| |
| 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() { |
| 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 |
| startAsyncAtrace("wm view"); |
| } |
| |
| private void stopAsyncAtrace() { |
| if (!mIsTraceStarted) { |
| return; |
| } |
| mIsTraceStarted = false; |
| final InputStream inputStream = stopAsyncAtraceWithStream(); |
| 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. |
| } |
| } |