• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.inputmethod;
18 
19 import static android.perftests.utils.ManualBenchmarkState.StatsReport;
20 import static android.perftests.utils.PerfTestActivity.ID_EDITOR;
21 import static android.perftests.utils.TestUtils.getOnMainSync;
22 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
23 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
24 
25 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
26 
27 import static org.junit.Assert.assertTrue;
28 
29 import android.annotation.UiThread;
30 import android.app.Activity;
31 import android.content.ComponentName;
32 import android.content.Context;
33 import android.inputmethodservice.InputMethodService;
34 import android.os.Process;
35 import android.os.SystemClock;
36 import android.perftests.utils.ManualBenchmarkState;
37 import android.perftests.utils.ManualBenchmarkState.ManualBenchmarkTest;
38 import android.perftests.utils.PerfManualStatusReporter;
39 import android.perftests.utils.TraceMarkParser;
40 import android.perftests.utils.TraceMarkParser.TraceMarkSlice;
41 import android.provider.Settings;
42 import android.util.Log;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.view.WindowInsets;
46 import android.view.WindowInsetsAnimation;
47 import android.view.WindowInsetsController;
48 import android.view.inputmethod.InputMethodManager;
49 import android.widget.FrameLayout;
50 
51 import androidx.annotation.NonNull;
52 import androidx.annotation.Nullable;
53 import androidx.test.filters.LargeTest;
54 
55 import com.android.compatibility.common.util.PollingCheck;
56 
57 import junit.framework.Assert;
58 
59 import org.junit.Rule;
60 import org.junit.Test;
61 
62 import java.io.BufferedReader;
63 import java.io.IOException;
64 import java.io.InputStream;
65 import java.io.InputStreamReader;
66 import java.util.List;
67 import java.util.concurrent.CountDownLatch;
68 import java.util.concurrent.TimeUnit;
69 import java.util.concurrent.atomic.AtomicBoolean;
70 import java.util.concurrent.atomic.AtomicLong;
71 import java.util.concurrent.atomic.AtomicReference;
72 
73 /** Measure the performance of internal methods in Input Method framework by trace tag. */
74 @LargeTest
75 public class ImePerfTest extends ImePerfTestBase
76         implements ManualBenchmarkState.CustomizedIterationListener {
77     private static final String TAG = ImePerfTest.class.getSimpleName();
78     private static final long ANIMATION_NOT_STARTED = -1;
79 
80     @Rule
81     public final PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter();
82 
83     @Rule
84     public final PerfTestActivityRule mActivityRule = new PerfTestActivityRule();
85 
86     /**
87      * IMF common methods to log for show/hide in trace.
88      */
89     private String[] mCommonMethods = {
90             "IC.pendingAnim",
91             "IMMS.applyImeVisibility",
92             "applyPostLayoutPolicy",
93             "applyWindowSurfaceChanges",
94             "ISC.onPostLayout"
95     };
96 
97     /** IMF show methods to log in trace. */
98     private String[] mShowMethods = {
99             "IC.showRequestFromIme",
100             "IC.showRequestFromApi",
101             "IC.showRequestFromApiToImeReady",
102             "IC.pendingAnim",
103             "IMMS.applyImeVisibility",
104             "IMMS.showMySoftInput",
105             "IMMS.showSoftInput",
106             "IMS.showSoftInput",
107             "IMS.startInput",
108             "WMS.showImePostLayout",
109             "IMS.updateFullscreenMode",
110             "IMS.onComputeInsets",
111             "IMS.showWindow"
112     };
113 
114     /** IMF show methods to log in trace. */
115     private String[] mShowMethodsCold = {
116             "IMS.bindInput",
117             "IMS.initializeInternal",
118             "IMS.restartInput",
119             "IMS.onCreate",
120             "IMS.initSoftInputWindow",
121             "IMS.resetStateForNewConfiguration",
122             "IMMS.onServiceConnected",
123             "IMMS.sessionCreated",
124             "IMMS.startInputOrWindowGainedFocus"
125     };
126 
127     /** IMF hide lifecycle methods to log in trace. */
128     private String[] mHideMethods = {
129             "IC.hideRequestFromIme",
130             "IC.hideRequestFromApi",
131             "IMMS.hideMySoftInput",
132             "IMMS.hideSoftInput",
133             "IMS.hideSoftInput",
134             "WMS.hideIme"
135     };
136 
137     /**
138      * IMF methods to log in trace.
139      */
140     private TraceMarkParser mTraceMethods;
141 
142     private boolean mIsTraceStarted;
143 
144     /**
145      * Ime Session for {@link BaselineIme}.
146      */
147     private static class ImeSession implements AutoCloseable {
148 
149         private static final long TIMEOUT = 2000;
150         private final ComponentName mImeName;
151         private Context mContext = getInstrumentation().getContext();
152 
ImeSession(ComponentName ime)153         ImeSession(ComponentName ime) throws Exception {
154             mImeName = ime;
155             // using adb, enable and set Baseline IME.
156             executeShellCommand("ime reset");
157             executeShellCommand("ime enable " + ime.flattenToShortString());
158             executeShellCommand("ime set " + ime.flattenToShortString());
159             PollingCheck.check("Make sure that BaselineIme becomes available "
160                     + getCurrentInputMethodId(), TIMEOUT,
161                     () -> ime.equals(getCurrentInputMethodId()));
162         }
163 
164         @Override
close()165         public void close() throws Exception {
166             executeShellCommand("ime reset");
167             PollingCheck.check("Make sure that Baseline IME becomes unavailable", TIMEOUT, () ->
168                     mContext.getSystemService(InputMethodManager.class)
169                             .getEnabledInputMethodList()
170                             .stream()
171                             .noneMatch(info -> mImeName.equals(info.getComponent())));
172         }
173 
174         @Nullable
getCurrentInputMethodId()175         private ComponentName getCurrentInputMethodId() {
176             return ComponentName.unflattenFromString(
177                     Settings.Secure.getString(mContext.getContentResolver(),
178                             Settings.Secure.DEFAULT_INPUT_METHOD));
179         }
180     }
181 
182     /**
183      * A minimal baseline IME (that has a single static view) used to measure IMF latency.
184      */
185     public static class BaselineIme extends InputMethodService {
186 
187         public static final int HEIGHT_DP = 100;
188         private static int sPid;
189 
190         @Override
onCreateInputView()191         public View onCreateInputView() {
192             final ViewGroup view = new FrameLayout(this);
193             final View inner = new View(this);
194             final float density = getResources().getDisplayMetrics().density;
195             final int height = (int) (HEIGHT_DP * density);
196             view.setPadding(0, 0, 0, 0);
197             view.addView(inner, new FrameLayout.LayoutParams(MATCH_PARENT, height));
198             inner.setBackgroundColor(0xff01fe10); // green
199             sPid = Process.myPid();
200             return view;
201         }
202 
getPid()203         static int getPid() {
204             return sPid;
205         }
206 
getName(Context context)207         static ComponentName getName(Context context) {
208             return new ComponentName(context, BaselineIme.class);
209         }
210     }
211 
212     @Test
213     @ManualBenchmarkTest(
214             targetTestDurationNs = 10 * TIME_1_S_IN_NS,
215             statsReport = @StatsReport(
216                     flags = StatsReport.FLAG_ITERATION | StatsReport.FLAG_MEAN
217                             | StatsReport.FLAG_MIN | StatsReport.FLAG_MAX
218                             | StatsReport.FLAG_COEFFICIENT_VAR))
testShowImeWarm()219     public void testShowImeWarm() throws Throwable {
220         testShowOrHideImeWarm(true /* show */);
221     }
222 
223     @Test
224     @ManualBenchmarkTest(
225             targetTestDurationNs = 10 * TIME_1_S_IN_NS,
226             statsReport = @StatsReport(
227                     flags = StatsReport.FLAG_ITERATION | StatsReport.FLAG_MEAN
228                             | StatsReport.FLAG_MIN | StatsReport.FLAG_MAX
229                             | StatsReport.FLAG_COEFFICIENT_VAR))
testHideIme()230     public void testHideIme() throws Throwable {
231         testShowOrHideImeWarm(false /* show */);
232     }
233 
234     @Test
235     @ManualBenchmarkTest(
236             targetTestDurationNs = 10 * TIME_1_S_IN_NS,
237             statsReport = @StatsReport(
238                     flags = StatsReport.FLAG_ITERATION | StatsReport.FLAG_MEAN
239                             | StatsReport.FLAG_MIN | StatsReport.FLAG_MAX
240                             | StatsReport.FLAG_COEFFICIENT_VAR))
testShowImeCold()241     public void testShowImeCold() throws Throwable {
242         mTraceMethods = new TraceMarkParser(
243                 buildArray(mCommonMethods, mShowMethods, mShowMethodsCold));
244 
245         final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
246         state.setCustomizedIterations(getProfilingIterations(), this);
247         if (state.isWarmingUp()) {
248             // we don't need to warmup for cold start.
249             return;
250         }
251 
252         long measuredTimeNs = 0;
253         while (state.keepRunning(measuredTimeNs)) {
254             killBaselineIme();
255             try (ImeSession imeSession = new ImeSession(BaselineIme.getName(
256                     getInstrumentation().getContext()))) {
257                 final AtomicReference<CountDownLatch> latchStart = new AtomicReference<>();
258                 final Activity activity = getActivityWithFocus();
259 
260                 setImeListener(activity, latchStart, null /* latchEnd */);
261                 latchStart.set(new CountDownLatch(1));
262 
263                 if (!mIsTraceStarted) {
264                     startAsyncAtrace();
265                 }
266 
267                 final WindowInsetsController controller =
268                         activity.getWindow().getDecorView().getWindowInsetsController();
269                 AtomicLong startTime = new AtomicLong();
270                 activity.runOnUiThread(() -> {
271                     startTime.set(SystemClock.elapsedRealtimeNanos());
272                     controller.show(WindowInsets.Type.ime());
273                 });
274 
275                 measuredTimeNs = waitForAnimationStart(latchStart, startTime);
276                 mActivityRule.finishActivity();
277             }
278         }
279         stopAsyncAtrace();
280         addResultToState(state);
281     }
282 
killBaselineIme()283     private void killBaselineIme() {
284         assertTrue("PID of test and IME can't be same",
285                 Process.myPid() != BaselineIme.getPid());
286         Process.killProcess(BaselineIme.getPid());
287     }
288 
testShowOrHideImeWarm(final boolean show)289     private void testShowOrHideImeWarm(final boolean show) throws Throwable {
290         mTraceMethods = new TraceMarkParser(buildArray(
291                 mCommonMethods, show ? mShowMethods : mHideMethods));
292         final ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
293         state.setCustomizedIterations(getProfilingIterations(), this);
294         long measuredTimeNs = 0;
295         try (ImeSession imeSession = new ImeSession(BaselineIme.getName(
296                 getInstrumentation().getContext()))) {
297             final AtomicReference<CountDownLatch> latchStart = new AtomicReference<>();
298             final AtomicReference<CountDownLatch> latchEnd = new AtomicReference<>();
299             final Activity activity = getActivityWithFocus();
300 
301             // call IME show/hide
302             final WindowInsetsController controller =
303                     activity.getWindow().getDecorView().getWindowInsetsController();
304 
305             while (state.keepRunning(measuredTimeNs)) {
306                 setImeListener(activity, latchStart, latchEnd);
307                 // For measuring hide, lets show IME first.
308                 if (!show) {
309                     initLatch(latchStart, latchEnd);
310                     AtomicBoolean showCalled = new AtomicBoolean();
311                     getInstrumentation().runOnMainSync(() -> {
312                         if (!isImeVisible(activity)) {
313                             controller.show(WindowInsets.Type.ime());
314                             showCalled.set(true);
315                         }
316                     });
317                     if (showCalled.get()) {
318                         PollingCheck.check("IME show animation should finish ",
319                                 TIMEOUT_1_S_IN_MS * 3,
320                                 () -> latchStart.get().getCount() == 0
321                                         && latchEnd.get().getCount() == 0);
322                     }
323                 }
324                 if (!mIsTraceStarted && !state.isWarmingUp()) {
325                     startAsyncAtrace();
326                     mIsTraceStarted = true;
327                 }
328 
329                 AtomicLong startTime = new AtomicLong();
330                 AtomicBoolean unexpectedVisibility = new AtomicBoolean();
331                 initLatch(latchStart, latchEnd);
332                 getInstrumentation().runOnMainSync(() -> {
333                     boolean isVisible = isImeVisible(activity);
334                     startTime.set(SystemClock.elapsedRealtimeNanos());
335 
336                     if (show && !isVisible) {
337                         controller.show(WindowInsets.Type.ime());
338                     } else if (!show && isVisible) {
339                         controller.hide(WindowInsets.Type.ime());
340                     } else {
341                         // ignore this iteration as unexpected IME visibility was encountered.
342                         unexpectedVisibility.set(true);
343                     }
344                 });
345 
346                 if (!unexpectedVisibility.get()) {
347                     long timeElapsed = waitForAnimationStart(latchStart, startTime);
348                     if (timeElapsed != ANIMATION_NOT_STARTED) {
349                         measuredTimeNs = timeElapsed;
350                         // wait for animation to end or we may start two animations and timing
351                         // will not be measured accurately.
352                         waitForAnimationEnd(latchEnd);
353                     }
354                 }
355 
356                 // hide IME before next iteration.
357                 if (show) {
358                     initLatch(latchStart, latchEnd);
359                     activity.runOnUiThread(() -> controller.hide(WindowInsets.Type.ime()));
360                     try {
361                         latchEnd.get().await(TIMEOUT_1_S_IN_MS * 5, TimeUnit.MILLISECONDS);
362                         if (latchEnd.get().getCount() != 0
363                                 && getOnMainSync(() -> isImeVisible(activity))) {
364                             Assert.fail("IME hide animation should finish.");
365                         }
366                     } catch (InterruptedException e) {
367                     }
368                 }
369             }
370         } finally {
371             if (mIsTraceStarted) {
372                 stopAsyncAtrace();
373             }
374         }
375         mActivityRule.finishActivity();
376 
377         addResultToState(state);
378     }
379 
initLatch(AtomicReference<CountDownLatch> latchStart, AtomicReference<CountDownLatch> latchEnd)380     private void initLatch(AtomicReference<CountDownLatch> latchStart,
381             AtomicReference<CountDownLatch> latchEnd) {
382         latchStart.set(new CountDownLatch(1));
383         latchEnd.set(new CountDownLatch(1));
384     }
385 
386     @UiThread
isImeVisible(@onNull final Activity activity)387     private boolean isImeVisible(@NonNull final Activity activity) {
388         return activity.getWindow().getDecorView().getRootWindowInsets().isVisible(
389                 WindowInsets.Type.ime());
390     }
391 
waitForAnimationStart( AtomicReference<CountDownLatch> latchStart, AtomicLong startTime)392     private long waitForAnimationStart(
393             AtomicReference<CountDownLatch> latchStart, AtomicLong startTime) {
394         try {
395             latchStart.get().await(5, TimeUnit.SECONDS);
396             if (latchStart.get().getCount() != 0) {
397                 return ANIMATION_NOT_STARTED;
398             }
399         } catch (InterruptedException e) { }
400 
401         return SystemClock.elapsedRealtimeNanos() - startTime.get();
402     }
403 
waitForAnimationEnd(AtomicReference<CountDownLatch> latchEnd)404     private void waitForAnimationEnd(AtomicReference<CountDownLatch> latchEnd) {
405         try {
406             latchEnd.get().await(3, TimeUnit.SECONDS);
407         } catch (InterruptedException e) { }
408     }
409 
addResultToState(ManualBenchmarkState state)410     private void addResultToState(ManualBenchmarkState state) {
411         mTraceMethods.forAllSlices((key, slices) -> {
412             for (TraceMarkSlice slice : slices) {
413                 state.addExtraResult(key, (long) (slice.getDurationInSeconds() * NANOS_PER_S));
414             }
415         });
416         Log.i(TAG, String.valueOf(mTraceMethods));
417     }
418 
getActivityWithFocus()419     private Activity getActivityWithFocus() throws Exception {
420         final Activity activity = mActivityRule.launchActivity();
421         PollingCheck.check("Activity onResume()", TIMEOUT_1_S_IN_MS,
422                 () -> activity.isResumed());
423 
424         View editor = activity.findViewById(ID_EDITOR);
425         editor.requestFocus();
426 
427         // wait till editor is focused so we don't count activity/view latency.
428         PollingCheck.check("Editor is focused", TIMEOUT_1_S_IN_MS,
429                 () -> editor.isFocused());
430         getInstrumentation().waitForIdleSync();
431 
432         return activity;
433     }
434 
setImeListener(Activity activity, @NonNull AtomicReference<CountDownLatch> latchStart, @Nullable AtomicReference<CountDownLatch> latchEnd)435     private void setImeListener(Activity activity,
436             @NonNull AtomicReference<CountDownLatch> latchStart,
437             @Nullable AtomicReference<CountDownLatch> latchEnd) {
438         // set IME animation listener
439         activity.getWindow().getDecorView().setWindowInsetsAnimationCallback(
440                 new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
441                     @NonNull
442                     @Override
443                     public WindowInsetsAnimation.Bounds onStart(
444                             @NonNull WindowInsetsAnimation animation,
445                             @NonNull WindowInsetsAnimation.Bounds bounds) {
446                         latchStart.get().countDown();
447                         return super.onStart(animation, bounds);
448                     }
449 
450                     @NonNull
451                     @Override
452                     public WindowInsets onProgress(@NonNull WindowInsets insets,
453                             @NonNull List<WindowInsetsAnimation> runningAnimations) {
454                         return insets;
455                     }
456 
457                     @Override
458                     public void onEnd(@NonNull WindowInsetsAnimation animation) {
459                         super.onEnd(animation);
460                         if (latchEnd != null) {
461                             latchEnd.get().countDown();
462                         }
463                     }
464                 });
465     }
466 
startAsyncAtrace()467     private void startAsyncAtrace() {
468         mIsTraceStarted = true;
469         // IMF uses 'wm' component for trace in InputMethodService, InputMethodManagerService,
470         // WindowManagerService and 'view' for client window (InsetsController).
471         // TODO(b/167947940): Consider a separate input_method atrace
472         startAsyncAtrace("wm view");
473     }
474 
stopAsyncAtrace()475     private void stopAsyncAtrace() {
476         if (!mIsTraceStarted) {
477             return;
478         }
479         mIsTraceStarted = false;
480         final InputStream inputStream = stopAsyncAtraceWithStream();
481         try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
482             String line;
483             while ((line = reader.readLine()) != null) {
484                 mTraceMethods.visit(line);
485             }
486         } catch (IOException e) {
487             Log.w(TAG, "Failed to read the result of stopped atrace", e);
488         }
489     }
490 
491     @Override
onStart(int iteration)492     public void onStart(int iteration) {
493         // Do not capture trace when profiling because the result will be much slower.
494         stopAsyncAtrace();
495     }
496 
497     @Override
onFinished(int iteration)498     public void onFinished(int iteration) {
499         // do nothing.
500     }
501 }
502