1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.customtabs.test; 6 7 import android.annotation.SuppressLint; 8 import android.app.Activity; 9 import android.app.ActivityManager; 10 import android.content.ComponentName; 11 import android.content.Context; 12 import android.content.Intent; 13 import android.net.Uri; 14 import android.os.Build; 15 import android.os.Bundle; 16 import android.os.Debug; 17 import android.os.Handler; 18 import android.os.IBinder; 19 import android.os.Looper; 20 import android.os.SystemClock; 21 import android.util.Log; 22 import android.view.View; 23 import android.widget.Button; 24 import android.widget.CheckBox; 25 import android.widget.EditText; 26 import android.widget.RadioButton; 27 28 import androidx.browser.customtabs.CustomTabsCallback; 29 import androidx.browser.customtabs.CustomTabsClient; 30 import androidx.browser.customtabs.CustomTabsIntent; 31 import androidx.browser.customtabs.CustomTabsServiceConnection; 32 import androidx.browser.customtabs.CustomTabsSession; 33 import androidx.core.app.BundleCompat; 34 35 import java.lang.reflect.InvocationTargetException; 36 import java.lang.reflect.Method; 37 import java.util.ArrayList; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Random; 41 import java.util.Set; 42 43 /** Activity used to benchmark Custom Tabs PLT. 44 * 45 * This activity contains benchmark code for two modes: 46 * 1. Comparison between a basic use of Custom Tabs and a basic use of WebView. 47 * 2. Custom Tabs benchmarking under various scenarios. 48 * 49 * The two modes are not merged into one as the metrics we can extract in the two cases 50 * are constrained for the first one by what WebView provides. 51 */ 52 public class MainActivity extends Activity implements View.OnClickListener { 53 static final String TAG = "CUSTOMTABSBENCH"; 54 static final String TAGCSV = "CUSTOMTABSBENCHCSV"; 55 private static final String MEMORY_TAG = "CUSTOMTABSMEMORY"; 56 private static final String DEFAULT_URL = "https://www.android.com"; 57 private static final String DEFAULT_PACKAGE = "com.google.android.apps.chrome"; 58 private static final int NONE = -1; 59 // Common key between the benchmark modes. 60 private static final String URL_KEY = "url"; 61 private static final String PARALLEL_URL_KEY = "parallel_url"; 62 private static final String DEFAULT_REFERRER_URL = "https://www.google.com"; 63 // Keys for the WebView / Custom Tabs comparison. 64 static final String INTENT_SENT_EXTRA = "intent_sent_ms"; 65 private static final String USE_WEBVIEW_KEY = "use_webview"; 66 private static final String WARMUP_KEY = "warmup"; 67 68 // extraCommand related constants. 69 private static final String SET_PRERENDER_ON_CELLULAR = "setPrerenderOnCellularForSession"; 70 private static final String SET_SPECULATION_MODE = "setSpeculationModeForSession"; 71 private static final String SET_IGNORE_URL_FRAGMENTS_FOR_SESSION = 72 "setIgnoreUrlFragmentsForSession"; 73 74 private static final String ADD_VERIFIED_ORIGN = "addVerifiedOriginForSession"; 75 private static final String ENABLE_PARALLEL_REQUEST = "enableParallelRequestForSession"; 76 private static final String PARALLEL_REQUEST_REFERRER_KEY = 77 "android.support.customtabs.PARALLEL_REQUEST_REFERRER"; 78 private static final String PARALLEL_REQUEST_URL_KEY = 79 "android.support.customtabs.PARALLEL_REQUEST_URL"; 80 private static final int PARALLEL_REQUEST_MIN_DELAY_AFTER_WARMUP = 3000; 81 82 private static final int NO_SPECULATION = 0; 83 private static final int PRERENDER = 2; 84 private static final int HIDDEN_TAB = 3; 85 86 private final Handler mHandler = new Handler(Looper.getMainLooper()); 87 88 private EditText mUrlEditText; 89 private RadioButton mChromeRadioButton; 90 private RadioButton mWebViewRadioButton; 91 private CheckBox mWarmupCheckbox; 92 private CheckBox mParallelUrlCheckBox; 93 private EditText mParallelUrlEditText; 94 private long mIntentSentMs; 95 96 @Override onCreate(Bundle savedInstanceState)97 protected void onCreate(Bundle savedInstanceState) { 98 super.onCreate(savedInstanceState); 99 final Intent intent = getIntent(); 100 101 setUpUi(); 102 103 // Automated mode, 1s later to leave time for the app to settle. 104 if (intent.getStringExtra(URL_KEY) != null) { 105 mHandler.postDelayed(new Runnable() { 106 @Override 107 public void run() { 108 processArguments(intent); 109 } 110 }, 1000); 111 } 112 } 113 114 /** Displays the UI and registers the click listeners. */ setUpUi()115 private void setUpUi() { 116 setContentView(R.layout.main); 117 118 mUrlEditText = findViewById(R.id.url_text); 119 mChromeRadioButton = findViewById(R.id.radio_chrome); 120 mWebViewRadioButton = findViewById(R.id.radio_webview); 121 mWarmupCheckbox = findViewById(R.id.warmup_checkbox); 122 mParallelUrlCheckBox = findViewById(R.id.parallel_url_checkbox); 123 mParallelUrlEditText = findViewById(R.id.parallel_url_text); 124 125 Button goButton = findViewById(R.id.go_button); 126 127 mUrlEditText.setOnClickListener(this); 128 mChromeRadioButton.setOnClickListener(this); 129 mWebViewRadioButton.setOnClickListener(this); 130 mWarmupCheckbox.setOnClickListener(this); 131 mParallelUrlCheckBox.setOnClickListener(this); 132 mParallelUrlEditText.setOnClickListener(this); 133 goButton.setOnClickListener(this); 134 } 135 136 @Override onClick(View v)137 public void onClick(View v) { 138 int id = v.getId(); 139 140 boolean warmup = mWarmupCheckbox.isChecked(); 141 boolean useChrome = mChromeRadioButton.isChecked(); 142 boolean useWebView = mWebViewRadioButton.isChecked(); 143 String url = mUrlEditText.getText().toString(); 144 boolean willRequestParallelUrl = mParallelUrlCheckBox.isChecked(); 145 String parallelUrl = null; 146 if (willRequestParallelUrl) { 147 parallelUrl = mParallelUrlEditText.getText().toString(); 148 } 149 150 if (id == R.id.go_button) { 151 customTabsWebViewBenchmark(url, useChrome, useWebView, warmup, parallelUrl); 152 } 153 } 154 155 /** Routes to either of the benchmark modes. */ processArguments(Intent intent)156 private void processArguments(Intent intent) { 157 if (intent.hasExtra(USE_WEBVIEW_KEY)) { 158 startCustomTabsWebViewBenchmark(intent); 159 } else { 160 startCustomTabsBenchmark(intent); 161 } 162 } 163 164 /** Start the CustomTabs / WebView comparison benchmark. 165 * 166 * NOTE: Methods below are for the first benchmark mode. 167 */ startCustomTabsWebViewBenchmark(Intent intent)168 private void startCustomTabsWebViewBenchmark(Intent intent) { 169 Bundle extras = intent.getExtras(); 170 String url = extras.getString(URL_KEY); 171 String parallelUrl = extras.getString(PARALLEL_URL_KEY); 172 boolean useWebView = extras.getBoolean(USE_WEBVIEW_KEY); 173 boolean useChrome = !useWebView; 174 boolean warmup = extras.getBoolean(WARMUP_KEY); 175 customTabsWebViewBenchmark(url, useChrome, useWebView, warmup, parallelUrl); 176 } 177 178 /** Start the CustomTabs / WebView comparison benchmark. */ customTabsWebViewBenchmark( String url, boolean useChrome, boolean useWebView, boolean warmup, String parallelUrl)179 private void customTabsWebViewBenchmark( 180 String url, boolean useChrome, boolean useWebView, boolean warmup, String parallelUrl) { 181 if (useChrome) { 182 launchChrome(url, warmup, parallelUrl); 183 } else { 184 assert useWebView; 185 launchWebView(url); 186 } 187 } 188 launchWebView(String url)189 private void launchWebView(String url) { 190 Intent intent = new Intent(); 191 intent.setData(Uri.parse(url)); 192 intent.setClass(this, WebViewActivity.class); 193 intent.putExtra(INTENT_SENT_EXTRA, now()); 194 startActivity(intent); 195 } 196 launchChrome(String url, boolean warmup, String parallelUrl)197 private void launchChrome(String url, boolean warmup, String parallelUrl) { 198 CustomTabsServiceConnection connection = new CustomTabsServiceConnection() { 199 @Override 200 public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) { 201 launchChromeIntent(url, warmup, client, parallelUrl); 202 } 203 204 @Override 205 public void onServiceDisconnected(ComponentName name) {} 206 }; 207 CustomTabsClient.bindCustomTabsService(this, DEFAULT_PACKAGE, connection); 208 } 209 maybePrepareParallelUrlRequest(String parallelUrl, CustomTabsClient client, CustomTabsIntent intent, IBinder sessionBinder)210 private static void maybePrepareParallelUrlRequest(String parallelUrl, CustomTabsClient client, 211 CustomTabsIntent intent, IBinder sessionBinder) { 212 if (parallelUrl == null || parallelUrl.length() == 0) { 213 Log.w(TAG, "null or empty parallelUrl"); 214 return; 215 } 216 217 Uri parallelUri = Uri.parse(parallelUrl); 218 Bundle params = new Bundle(); 219 BundleCompat.putBinder(params, "session", sessionBinder); 220 221 Uri referrerUri = Uri.parse(DEFAULT_REFERRER_URL); 222 params.putParcelable("origin", referrerUri); 223 224 Bundle result = client.extraCommand(ADD_VERIFIED_ORIGN, params); 225 boolean ok = (result != null) && result.getBoolean(ADD_VERIFIED_ORIGN); 226 if (!ok) throw new RuntimeException("Cannot add verified origin"); 227 228 result = client.extraCommand(ENABLE_PARALLEL_REQUEST, params); 229 ok = (result != null) && result.getBoolean(ENABLE_PARALLEL_REQUEST); 230 if (!ok) throw new RuntimeException("Cannot enable Parallel Request"); 231 Log.w(TAG, "enabled Parallel Request"); 232 233 intent.intent.putExtra(PARALLEL_REQUEST_URL_KEY, parallelUri); 234 intent.intent.putExtra(PARALLEL_REQUEST_REFERRER_KEY, referrerUri); 235 } 236 launchChromeIntent( String url, boolean warmup, CustomTabsClient client, String parallelUrl)237 private void launchChromeIntent( 238 String url, boolean warmup, CustomTabsClient client, String parallelUrl) { 239 CustomTabsCallback callback = new CustomTabsCallback() { 240 private long mNavigationStartOffsetMs; 241 242 @Override 243 public void onNavigationEvent(int navigationEvent, Bundle extras) { 244 long offsetMs = now() - mIntentSentMs; 245 switch (navigationEvent) { 246 case CustomTabsCallback.NAVIGATION_STARTED: 247 mNavigationStartOffsetMs = offsetMs; 248 Log.w(TAG, "navigationStarted = " + offsetMs); 249 break; 250 case CustomTabsCallback.NAVIGATION_FINISHED: 251 Log.w(TAG, "navigationFinished = " + offsetMs); 252 Log.w(TAG, "CHROME," + mNavigationStartOffsetMs + "," + offsetMs); 253 break; 254 default: 255 break; 256 } 257 } 258 }; 259 CustomTabsSession session = client.newSession(callback); 260 final CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder(session).build(); 261 final Uri uri = Uri.parse(url); 262 263 IBinder sessionBinder = BundleCompat.getBinder( 264 customTabsIntent.intent.getExtras(), CustomTabsIntent.EXTRA_SESSION); 265 assert sessionBinder != null; 266 maybePrepareParallelUrlRequest(parallelUrl, client, customTabsIntent, sessionBinder); 267 268 if (warmup) { 269 client.warmup(0); 270 mHandler.postDelayed(new Runnable() { 271 @Override 272 public void run() { 273 mIntentSentMs = now(); 274 customTabsIntent.launchUrl(MainActivity.this, uri); 275 } 276 }, 3000); 277 } else { 278 mIntentSentMs = now(); 279 customTabsIntent.launchUrl(MainActivity.this, uri); 280 } 281 } 282 now()283 static long now() { 284 return System.currentTimeMillis(); 285 } 286 287 /** 288 * Holds the file and the range for pinning. Used only in the 'Pinning Benchmark' mode. 289 */ 290 private static class PinInfo { 291 public boolean pinningBenchmark; 292 public String fileName; 293 public int offset; 294 public int length; 295 PinInfo()296 public PinInfo() {} 297 PinInfo(boolean pinningBenchmark, String fileName, int offset, int length)298 public PinInfo(boolean pinningBenchmark, String fileName, int offset, int length) { 299 this.pinningBenchmark = pinningBenchmark; 300 this.fileName = fileName; 301 this.offset = offset; 302 this.length = length; 303 } 304 } 305 306 /** 307 * Holds immutable parameters of the benchmark that are not needed after launching an intent. 308 * 309 * There are a few parameters that need to be written to the CSV line, those better fit in the 310 * {@link CustomCallback}. 311 */ 312 private static class LaunchInfo { 313 public final String url; 314 public final String speculatedUrl; 315 public final String parallelUrl; 316 public final int timeoutSeconds; 317 LaunchInfo( String url, String speculatedUrl, String parallelUrl, int timeoutSeconds)318 public LaunchInfo( 319 String url, String speculatedUrl, String parallelUrl, int timeoutSeconds) { 320 this.url = url; 321 this.speculatedUrl = speculatedUrl; 322 this.parallelUrl = parallelUrl; 323 this.timeoutSeconds = timeoutSeconds; 324 } 325 } 326 327 /** Start the second benchmark mode. 328 * 329 * NOTE: Methods below are for the second mode. 330 */ startCustomTabsBenchmark(Intent intent)331 private void startCustomTabsBenchmark(Intent intent) { 332 String url = intent.getStringExtra(URL_KEY); 333 if (url == null) url = DEFAULT_URL; 334 String parallelUrl = intent.getStringExtra(PARALLEL_URL_KEY); 335 336 String speculatedUrl = intent.getStringExtra("speculated_url"); 337 if (speculatedUrl == null) speculatedUrl = url; 338 String packageName = intent.getStringExtra("package_name"); 339 if (packageName == null) packageName = DEFAULT_PACKAGE; 340 boolean warmup = intent.getBooleanExtra("warmup", false); 341 342 boolean skipLauncherActivity = intent.getBooleanExtra("skip_launcher_activity", false); 343 int delayToMayLaunchUrl = intent.getIntExtra("delay_to_may_launch_url", NONE); 344 int delayToLaunchUrl = intent.getIntExtra("delay_to_launch_url", NONE); 345 String speculationMode = intent.getStringExtra("speculation_mode"); 346 if (speculationMode == null) speculationMode = "prerender"; 347 int timeoutSeconds = intent.getIntExtra("timeout", NONE); 348 349 PinInfo pinInfo; 350 if (!intent.getBooleanExtra("pinning_benchmark", false)) { 351 pinInfo = new PinInfo(); 352 } else { 353 pinInfo = new PinInfo(true, intent.getStringExtra("pin_filename"), 354 intent.getIntExtra("pin_offset", NONE), intent.getIntExtra("pin_length", NONE)); 355 } 356 int extraBriefMemoryMb = intent.getIntExtra("extra_brief_memory_mb", 0); 357 358 if (parallelUrl != null && !parallelUrl.equals("") && !warmup) { 359 if (pinInfo.pinningBenchmark) { 360 String message = "Warming up while pinning is not interesting"; 361 Log.e(TAG, message); 362 throw new RuntimeException(message); 363 } 364 Log.w(TAG, "Parallel URL provided, forcing warmup"); 365 warmup = true; 366 delayToLaunchUrl = Math.max(delayToLaunchUrl, PARALLEL_REQUEST_MIN_DELAY_AFTER_WARMUP); 367 delayToMayLaunchUrl = 368 Math.max(delayToMayLaunchUrl, PARALLEL_REQUEST_MIN_DELAY_AFTER_WARMUP); 369 } 370 371 final CustomCallback cb = 372 new CustomCallback(packageName, warmup, skipLauncherActivity, speculationMode, 373 delayToMayLaunchUrl, delayToLaunchUrl, pinInfo, extraBriefMemoryMb); 374 launchCustomTabs(cb, new LaunchInfo(url, speculatedUrl, parallelUrl, timeoutSeconds)); 375 } 376 377 private final class CustomCallback extends CustomTabsCallback { 378 public final String packageName; 379 public final boolean warmup; 380 public final boolean skipLauncherActivity; 381 public final String speculationMode; 382 public final int delayToMayLaunchUrl; 383 public final int delayToLaunchUrl; 384 public boolean warmupCompleted; 385 public long intentSentMs = NONE; 386 public long pageLoadStartedMs = NONE; 387 public long pageLoadFinishedMs = NONE; 388 public long firstContentfulPaintMs = NONE; 389 public PinInfo pinInfo; 390 public long extraBriefMemoryMb; 391 CustomCallback(String packageName, boolean warmup, boolean skipLauncherActivity, String speculationMode, int delayToMayLaunchUrl, int delayToLaunchUrl, PinInfo pinInfo, long extraBriefMemoryMb)392 public CustomCallback(String packageName, boolean warmup, boolean skipLauncherActivity, 393 String speculationMode, int delayToMayLaunchUrl, int delayToLaunchUrl, 394 PinInfo pinInfo, long extraBriefMemoryMb) { 395 this.packageName = packageName; 396 this.warmup = warmup; 397 this.skipLauncherActivity = skipLauncherActivity; 398 this.speculationMode = speculationMode; 399 this.delayToMayLaunchUrl = delayToMayLaunchUrl; 400 this.delayToLaunchUrl = delayToLaunchUrl; 401 this.pinInfo = pinInfo; 402 this.extraBriefMemoryMb = extraBriefMemoryMb; 403 } 404 recordIntentHasBeenSent()405 public void recordIntentHasBeenSent() { 406 intentSentMs = SystemClock.uptimeMillis(); 407 } 408 409 @Override onNavigationEvent(int navigationEvent, Bundle extras)410 public void onNavigationEvent(int navigationEvent, Bundle extras) { 411 switch (navigationEvent) { 412 case CustomTabsCallback.NAVIGATION_STARTED: 413 pageLoadStartedMs = SystemClock.uptimeMillis(); 414 break; 415 case CustomTabsCallback.NAVIGATION_FINISHED: 416 pageLoadFinishedMs = SystemClock.uptimeMillis(); 417 break; 418 default: 419 break; 420 } 421 if (allSet()) logMetricsAndFinish(); 422 } 423 424 @Override extraCallback(String callbackName, Bundle args)425 public void extraCallback(String callbackName, Bundle args) { 426 if ("onWarmupCompleted".equals(callbackName)) { 427 warmupCompleted = true; 428 return; 429 } 430 431 if (!"NavigationMetrics".equals(callbackName)) { 432 Log.w(TAG, "Unknown extra callback skipped: " + callbackName); 433 return; 434 } 435 long firstPaintMs = args.getLong("firstContentfulPaint", NONE); 436 long navigationStartMs = args.getLong("navigationStart", NONE); 437 if (firstPaintMs == NONE || navigationStartMs == NONE) return; 438 // Can be reported several times, only record the first one. 439 if (firstContentfulPaintMs == NONE) { 440 firstContentfulPaintMs = navigationStartMs + firstPaintMs; 441 } 442 if (allSet()) logMetricsAndFinish(); 443 } 444 allSet()445 private boolean allSet() { 446 return intentSentMs != NONE && pageLoadStartedMs != NONE 447 && firstContentfulPaintMs != NONE && pageLoadFinishedMs != NONE; 448 } 449 450 /** Outputs the available metrics, and die. Unavalaible metrics are set to -1. */ logMetricsAndFinish()451 private void logMetricsAndFinish() { 452 String logLine = (warmup ? "1" : "0") + "," + (skipLauncherActivity ? "1" : "0") + "," 453 + speculationMode + "," + delayToMayLaunchUrl + "," + delayToLaunchUrl + "," 454 + intentSentMs + "," + pageLoadStartedMs + "," + pageLoadFinishedMs + "," 455 + firstContentfulPaintMs; 456 if (pinInfo.pinningBenchmark) { 457 logLine += ',' + extraBriefMemoryMb + ',' + pinInfo.length; 458 } 459 Log.w(TAGCSV, logLine); 460 logMemory(packageName, "AfterMetrics"); 461 MainActivity.this.finish(); 462 } 463 464 /** Same as {@link #logMetricsAndFinish()} with a set delay in ms. */ logMetricsAndFinishDelayed(int delayMs)465 public void logMetricsAndFinishDelayed(int delayMs) { 466 mHandler.postDelayed(new Runnable() { 467 @Override 468 public void run() { 469 logMetricsAndFinish(); 470 } 471 }, delayMs); 472 } 473 } 474 475 /** 476 * Sums all the memory usage of a package, and returns (PSS, Private Dirty). 477 * 478 * Only works for packages where a service is exported by each process, which is the case for 479 * Chrome. Also, doesn't work on O and above, as 480 * {@link ActivityManager#getRunningServices(int)}} is restricted. 481 * 482 * @param context Application context 483 * @param packageName the package to query 484 * @return {pss, privateDirty} in kB, or null. 485 */ getPackagePssAndPrivateDirty(Context context, String packageName)486 private static int[] getPackagePssAndPrivateDirty(Context context, String packageName) { 487 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) return null; 488 489 ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 490 List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(1000); 491 if (services == null) return null; 492 493 Set<Integer> pids = new HashSet<>(); 494 for (ActivityManager.RunningServiceInfo info : services) { 495 if (packageName.equals(info.service.getPackageName())) pids.add(info.pid); 496 } 497 498 int[] pidsArray = new int[pids.size()]; 499 int i = 0; 500 for (int pid : pids) pidsArray[i++] = pid; 501 Debug.MemoryInfo infos[] = am.getProcessMemoryInfo(pidsArray); 502 if (infos == null || infos.length == 0) return null; 503 504 int pss = 0; 505 int privateDirty = 0; 506 for (Debug.MemoryInfo info : infos) { 507 pss += info.getTotalPss(); 508 privateDirty += info.getTotalPrivateDirty(); 509 } 510 511 return new int[] {pss, privateDirty}; 512 } 513 logMemory(String packageName, String message)514 private void logMemory(String packageName, String message) { 515 int[] pssAndPrivateDirty = getPackagePssAndPrivateDirty( 516 getApplicationContext(), packageName); 517 if (pssAndPrivateDirty == null) return; 518 Log.w(MEMORY_TAG, message + "," + pssAndPrivateDirty[0] + "," + pssAndPrivateDirty[1]); 519 } 520 forceSpeculationMode( CustomTabsClient client, IBinder sessionBinder, String speculationMode)521 private static void forceSpeculationMode( 522 CustomTabsClient client, IBinder sessionBinder, String speculationMode) { 523 // The same bundle can be used for all calls, as the commands only look for their own 524 // arguments in it. 525 Bundle params = new Bundle(); 526 BundleCompat.putBinder(params, "session", sessionBinder); 527 params.putBoolean("ignoreFragments", true); 528 params.putBoolean("prerender", true); 529 530 int speculationModeValue; 531 switch (speculationMode) { 532 case "disabled": 533 speculationModeValue = NO_SPECULATION; 534 break; 535 case "prerender": 536 speculationModeValue = PRERENDER; 537 break; 538 case "hidden_tab": 539 speculationModeValue = HIDDEN_TAB; 540 break; 541 default: 542 throw new RuntimeException("Invalid speculation mode"); 543 } 544 params.putInt("speculationMode", speculationModeValue); 545 546 boolean ok = client.extraCommand(SET_PRERENDER_ON_CELLULAR, params) != null; 547 if (!ok) throw new RuntimeException("Cannot set cellular prerendering"); 548 ok = client.extraCommand(SET_IGNORE_URL_FRAGMENTS_FOR_SESSION, params) != null; 549 if (!ok) throw new RuntimeException("Cannot set ignoreFragments"); 550 ok = client.extraCommand(SET_SPECULATION_MODE, params) != null; 551 if (!ok) throw new RuntimeException("Cannot set the speculation mode"); 552 } 553 554 // Declare as public and volatile to prevent it from being optimized out. 555 private static volatile ArrayList<byte[]> sExtraArrays = new ArrayList<>(); 556 557 private static final int MAX_ALLOCATION_ALLOWED = 1 << 23; // 8 MiB 558 createRandomlyFilledArray(int size, Random random)559 private static byte[] createRandomlyFilledArray(int size, Random random) { 560 // Fill in small chunks to avoid allocating 2x the size. 561 byte[] array = new byte[size]; 562 final int chunkSize = 1 << 15; // 32 KiB 563 byte[] randomBytes = new byte[chunkSize]; 564 for (int i = 0; i < size / chunkSize; i++) { 565 random.nextBytes(randomBytes); 566 System.arraycopy(randomBytes /* src */, 0 /* srcPos */, array /* dest */, 567 i * chunkSize /* destPos */, chunkSize /* length */); 568 } 569 return array; 570 } 571 572 // In order for this method to work, the Android system image needs to be modified to export 573 // PinnerService and allow any app to call pinRangeFromFile(). Usually pinning is requested by 574 // Chrome (in LibraryPrefetcher), but the call is reimplemented here to avoid restarting 575 // Chrome unnecessarily. 576 @SuppressLint("WrongConstant") pinChrome(String fileName, int startOffset, int length)577 private boolean pinChrome(String fileName, int startOffset, int length) { 578 Context context = getApplicationContext(); 579 Object pinner = context.getSystemService("pinner"); 580 if (pinner == null) { 581 Log.w(TAG, "Cannot get PinnerService."); 582 return false; 583 } 584 585 try { 586 Method pinRangeFromFile = pinner.getClass().getMethod( 587 "pinRangeFromFile", String.class, int.class, int.class); 588 boolean ok = (Boolean) pinRangeFromFile.invoke(pinner, fileName, startOffset, length); 589 if (!ok) { 590 Log.e(TAG, "Not allowed to call the method, should not happen"); 591 return false; 592 } else { 593 Log.w(TAG, "Successfully pinned ordered code"); 594 } 595 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { 596 Log.w(TAG, "Error invoking the method. " + ex.getMessage()); 597 return false; 598 } 599 return true; 600 } 601 602 // In order for this method to work, the Android system image needs to be modified to export 603 // PinnerService and allow any app to call unpinChromeFiles(). 604 @SuppressLint("WrongConstant") unpinChrome()605 private boolean unpinChrome() { 606 Context context = getApplicationContext(); 607 Object pinner = context.getSystemService("pinner"); 608 if (pinner == null) { 609 Log.w(TAG, "Cannot get PinnerService for unpinning."); 610 return false; 611 } 612 try { 613 Method unpinChromeFiles = pinner.getClass().getMethod("unpinChromeFiles"); 614 boolean ok = (Boolean) unpinChromeFiles.invoke(pinner); 615 if (!ok) { 616 Log.e(TAG, "Could not make a reflection call to unpinChromeFiles()"); 617 return false; 618 } else { 619 Log.i(TAG, "Unpinned Chrome files"); 620 } 621 } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) { 622 Log.w(TAG, "Error invoking the method. " + ex.getMessage()); 623 return false; 624 } 625 return true; 626 } 627 consumeExtraMemoryBriefly(long amountMb)628 private void consumeExtraMemoryBriefly(long amountMb) { 629 // Allocate memory and fill with random data. Randomization is needed to avoid efficient 630 // compression of the data in ZRAM. 631 Log.i(TAG, "Consuming extra memory (MiB) = " + amountMb); 632 int bytesToUse = (int) (amountMb * (1 << 20)); 633 long beforeFill = SystemClock.uptimeMillis(); 634 Random random = new Random(); 635 do { 636 // Limit every allocation in size in case there is a per-allocation limit. 637 int size = bytesToUse < MAX_ALLOCATION_ALLOWED ? bytesToUse : MAX_ALLOCATION_ALLOWED; 638 bytesToUse -= MAX_ALLOCATION_ALLOWED; 639 sExtraArrays.add(createRandomlyFilledArray(size, random)); 640 } while (bytesToUse > 0); 641 long afterFill = SystemClock.uptimeMillis() - beforeFill; 642 Log.i(TAG, "Time to fill extra memory (ms) = " + afterFill); 643 644 // Allow a number of background apps to be killed. 645 int amountToWaitForBackgroundKilling = 3000; 646 syncSleepMs(amountToWaitForBackgroundKilling); 647 648 // Free up memory to give Chrome the room to start without killing even more background 649 // apps. 650 sExtraArrays.clear(); 651 System.gc(); 652 } 653 onCustomTabsServiceConnected( CustomTabsClient client, CustomCallback cb, LaunchInfo launchInfo)654 private void onCustomTabsServiceConnected( 655 CustomTabsClient client, CustomCallback cb, LaunchInfo launchInfo) { 656 logMemory(cb.packageName, "OnServiceConnected"); 657 658 final CustomTabsSession session = client.newSession(cb); 659 final CustomTabsIntent intent = (new CustomTabsIntent.Builder(session)).build(); 660 IBinder sessionBinder = 661 BundleCompat.getBinder(intent.intent.getExtras(), CustomTabsIntent.EXTRA_SESSION); 662 assert sessionBinder != null; 663 forceSpeculationMode(client, sessionBinder, cb.speculationMode); 664 665 final Runnable launchRunnable = () -> { 666 logMemory(cb.packageName, "BeforeLaunch"); 667 668 if (cb.warmupCompleted) { 669 maybePrepareParallelUrlRequest( 670 launchInfo.parallelUrl, client, intent, sessionBinder); 671 } else { 672 Log.e(TAG, "not warmed up yet!"); 673 } 674 675 intent.launchUrl(MainActivity.this, Uri.parse(launchInfo.url)); 676 cb.recordIntentHasBeenSent(); 677 if (launchInfo.timeoutSeconds != NONE) { 678 cb.logMetricsAndFinishDelayed(launchInfo.timeoutSeconds * 1000); 679 } 680 }; 681 682 if (cb.pinInfo.pinningBenchmark) { 683 mHandler.post(launchRunnable); // Already waited for the delay. 684 } else { 685 if (cb.warmup) client.warmup(0); 686 if (cb.delayToMayLaunchUrl != NONE) { 687 final Runnable mayLaunchRunnable = () -> { 688 logMemory(cb.packageName, "BeforeMayLaunchUrl"); 689 session.mayLaunchUrl(Uri.parse(launchInfo.speculatedUrl), null, null); 690 mHandler.postDelayed(launchRunnable, cb.delayToLaunchUrl); 691 }; 692 mHandler.postDelayed(mayLaunchRunnable, cb.delayToMayLaunchUrl); 693 } else { 694 mHandler.postDelayed(launchRunnable, cb.delayToLaunchUrl); 695 } 696 } 697 } 698 syncSleepMs(int delay)699 private static void syncSleepMs(int delay) { 700 try { 701 Thread.sleep(delay); 702 } catch (InterruptedException e) { 703 Log.w(TAG, "Interrupted: " + e); 704 } 705 } 706 continueWithServiceConnection( final CustomCallback cb, final LaunchInfo launchInfo)707 private void continueWithServiceConnection( 708 final CustomCallback cb, final LaunchInfo launchInfo) { 709 CustomTabsClient.bindCustomTabsService( 710 this, cb.packageName, new CustomTabsServiceConnection() { 711 @Override 712 public void onCustomTabsServiceConnected( 713 ComponentName name, final CustomTabsClient client) { 714 MainActivity.this.onCustomTabsServiceConnected(client, cb, launchInfo); 715 } 716 717 @Override 718 public void onServiceDisconnected(ComponentName name) {} 719 }); 720 } 721 launchCustomTabs(CustomCallback cb, LaunchInfo launchInfo)722 private void launchCustomTabs(CustomCallback cb, LaunchInfo launchInfo) { 723 final PinInfo pinInfo = cb.pinInfo; 724 if (!pinInfo.pinningBenchmark) { 725 continueWithServiceConnection(cb, launchInfo); 726 } else { 727 // Execute off the UI thread to allow slow operations like pinning or eating RAM for 728 // dinner. 729 new Thread(() -> { 730 if (pinInfo.length > 0) { 731 boolean ok = pinChrome(pinInfo.fileName, pinInfo.offset, pinInfo.length); 732 if (!ok) throw new RuntimeException("Failed to pin Chrome file."); 733 } else { 734 boolean ok = unpinChrome(); 735 if (!ok) throw new RuntimeException("Failed to unpin Chrome file."); 736 } 737 // Pinning is async, wait until hopefully it finishes. 738 syncSleepMs(3000); 739 if (cb.extraBriefMemoryMb != 0) { 740 consumeExtraMemoryBriefly(cb.extraBriefMemoryMb); 741 } 742 Log.i(TAG, "Waiting for " + cb.delayToLaunchUrl + "ms before launching URL"); 743 syncSleepMs(cb.delayToLaunchUrl); 744 continueWithServiceConnection(cb, launchInfo); 745 }).start(); 746 } 747 } 748 } 749