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