1 /* 2 * Copyright (C) 2010 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 com.android.browser; 18 19 import android.app.Instrumentation; 20 import android.content.Intent; 21 import android.net.Uri; 22 import android.net.http.SslError; 23 import android.os.Environment; 24 import android.provider.Browser; 25 import android.test.ActivityInstrumentationTestCase2; 26 import android.text.TextUtils; 27 import android.util.Log; 28 import android.webkit.ClientCertRequestHandler; 29 import android.webkit.DownloadListener; 30 import android.webkit.HttpAuthHandler; 31 import android.webkit.JsPromptResult; 32 import android.webkit.JsResult; 33 import android.webkit.SslErrorHandler; 34 import android.webkit.WebView; 35 import android.webkit.WebViewClassic; 36 37 import java.io.BufferedReader; 38 import java.io.File; 39 import java.io.FileNotFoundException; 40 import java.io.FileReader; 41 import java.io.FileWriter; 42 import java.io.IOException; 43 import java.io.OutputStreamWriter; 44 import java.util.Iterator; 45 import java.util.LinkedList; 46 import java.util.List; 47 import java.util.concurrent.CountDownLatch; 48 import java.util.concurrent.TimeUnit; 49 50 /** 51 * 52 * Iterates over a list of URLs from a file and outputs the time to load each. 53 */ 54 public class PopularUrlsTest extends ActivityInstrumentationTestCase2<BrowserActivity> { 55 56 private final static String TAG = "PopularUrlsTest"; 57 private final static String newLine = System.getProperty("line.separator"); 58 private final static String sInputFile = "popular_urls.txt"; 59 private final static String sOutputFile = "test_output.txt"; 60 private final static String sStatusFile = "test_status.txt"; 61 private final static File sExternalStorage = Environment.getExternalStorageDirectory(); 62 63 private final static int PERF_LOOPCOUNT = 10; 64 private final static int STABILITY_LOOPCOUNT = 1; 65 private final static int PAGE_LOAD_TIMEOUT = 120000; // 2 minutes 66 67 private BrowserActivity mActivity = null; 68 private Controller mController = null; 69 private Instrumentation mInst = null; 70 private CountDownLatch mLatch = new CountDownLatch(1); 71 private RunStatus mStatus; 72 private boolean pageLoadFinishCalled, pageProgressFull; 73 74 public PopularUrlsTest() { 75 super(BrowserActivity.class); 76 } 77 78 @Override 79 protected void setUp() throws Exception { 80 super.setUp(); 81 82 Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("about:blank")); 83 i.putExtra(Controller.NO_CRASH_RECOVERY, true); 84 setActivityIntent(i); 85 mActivity = getActivity(); 86 mController = mActivity.getController(); 87 mInst = getInstrumentation(); 88 mInst.waitForIdleSync(); 89 90 mStatus = RunStatus.load(); 91 } 92 93 @Override 94 protected void tearDown() throws Exception { 95 if (mStatus != null) { 96 mStatus.cleanUp(); 97 } 98 99 super.tearDown(); 100 } 101 102 BufferedReader getInputStream() throws FileNotFoundException { 103 return getInputStream(sInputFile); 104 } 105 106 BufferedReader getInputStream(String inputFile) throws FileNotFoundException { 107 FileReader fileReader = new FileReader(new File(sExternalStorage, inputFile)); 108 BufferedReader bufferedReader = new BufferedReader(fileReader); 109 110 return bufferedReader; 111 } 112 113 OutputStreamWriter getOutputStream() throws IOException { 114 return getOutputStream(sOutputFile); 115 } 116 117 OutputStreamWriter getOutputStream(String outputFile) throws IOException { 118 return new FileWriter(new File(sExternalStorage, outputFile), mStatus.getIsRecovery()); 119 } 120 121 /** 122 * Gets the browser ready for testing by starting the application 123 * and wrapping the WebView's helper clients. 124 */ 125 void setUpBrowser() { 126 mInst.runOnMainSync(new Runnable() { 127 @Override 128 public void run() { 129 setupBrowserInternal(); 130 } 131 }); 132 } 133 134 void setupBrowserInternal() { 135 Tab tab = mController.getTabControl().getCurrentTab(); 136 WebView webView = tab.getWebView(); 137 138 webView.setWebChromeClient(new TestWebChromeClient( 139 WebViewClassic.fromWebView(webView).getWebChromeClient()) { 140 141 @Override 142 public void onProgressChanged(WebView view, int newProgress) { 143 super.onProgressChanged(view, newProgress); 144 if (newProgress >= 100) { 145 if (!pageProgressFull) { 146 // void duplicate calls 147 pageProgressFull = true; 148 if (pageLoadFinishCalled) { 149 //reset latch and move forward only if both indicators are true 150 resetLatch(); 151 } 152 } 153 } 154 } 155 156 /** 157 * Dismisses and logs Javascript alerts. 158 */ 159 @Override 160 public boolean onJsAlert(WebView view, String url, String message, 161 JsResult result) { 162 String logMsg = String.format("JS Alert '%s' received from %s", message, url); 163 Log.w(TAG, logMsg); 164 result.confirm(); 165 166 return true; 167 } 168 169 /** 170 * Confirms and logs Javascript alerts. 171 */ 172 @Override 173 public boolean onJsConfirm(WebView view, String url, String message, 174 JsResult result) { 175 String logMsg = String.format("JS Confirmation '%s' received from %s", 176 message, url); 177 Log.w(TAG, logMsg); 178 result.confirm(); 179 180 return true; 181 } 182 183 /** 184 * Confirms and logs Javascript alerts, providing the default value. 185 */ 186 @Override 187 public boolean onJsPrompt(WebView view, String url, String message, 188 String defaultValue, JsPromptResult result) { 189 String logMsg = String.format("JS Prompt '%s' received from %s; " + 190 "Giving default value '%s'", message, url, defaultValue); 191 Log.w(TAG, logMsg); 192 result.confirm(defaultValue); 193 194 return true; 195 } 196 197 /* 198 * Skip the unload confirmation 199 */ 200 @Override 201 public boolean onJsBeforeUnload( 202 WebView view, String url, String message, JsResult result) { 203 result.confirm(); 204 return true; 205 } 206 }); 207 208 webView.setWebViewClient(new TestWebViewClient( 209 WebViewClassic.fromWebView(webView).getWebViewClient()) { 210 211 /** 212 * Bypasses and logs errors. 213 */ 214 @Override 215 public void onReceivedError(WebView view, int errorCode, 216 String description, String failingUrl) { 217 String message = String.format("Error '%s' (%d) loading url: %s", 218 description, errorCode, failingUrl); 219 Log.w(TAG, message); 220 } 221 222 /** 223 * Ignores and logs SSL errors. 224 */ 225 @Override 226 public void onReceivedSslError(WebView view, SslErrorHandler handler, 227 SslError error) { 228 Log.w(TAG, "SSL error: " + error); 229 handler.proceed(); 230 } 231 232 /** 233 * Ignores and logs SSL client certificate requests. 234 */ 235 @Override 236 public void onReceivedClientCertRequest(WebView view, ClientCertRequestHandler handler, 237 String host_and_port) { 238 Log.w(TAG, "SSL client certificate request: " + host_and_port); 239 handler.cancel(); 240 } 241 242 /** 243 * Ignores http auth with dummy username and password 244 */ 245 @Override 246 public void onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, 247 String host, String realm) { 248 handler.proceed("user", "passwd"); 249 } 250 251 /* (non-Javadoc) 252 * @see com.android.browser.TestWebViewClient#onPageFinished(android.webkit.WebView, java.lang.String) 253 */ 254 @Override 255 public void onPageFinished(WebView view, String url) { 256 super.onPageFinished(view, url); 257 if (!pageLoadFinishCalled) { 258 pageLoadFinishCalled = true; 259 if (pageProgressFull) { 260 //reset latch and move forward only if both indicators are true 261 resetLatch(); 262 } 263 } 264 } 265 266 @Override 267 public boolean shouldOverrideUrlLoading(WebView view, String url) { 268 if (!(url.startsWith("http://") || url.startsWith("https://"))) { 269 Log.v(TAG, String.format("suppressing non-http url scheme: %s", url)); 270 return true; 271 } 272 return super.shouldOverrideUrlLoading(view, url); 273 } 274 }); 275 276 webView.setDownloadListener(new DownloadListener() { 277 278 @Override 279 public void onDownloadStart(String url, String userAgent, String contentDisposition, 280 String mimetype, long contentLength) { 281 Log.v(TAG, String.format("Download request ignored: %s", url)); 282 } 283 }); 284 } 285 286 void resetLatch() { 287 if (mLatch.getCount() != 1) { 288 Log.w(TAG, "Expecting latch to be 1, but it's not!"); 289 } else { 290 mLatch.countDown(); 291 } 292 } 293 294 void resetForNewPage() { 295 mLatch = new CountDownLatch(1); 296 pageLoadFinishCalled = false; 297 pageProgressFull = false; 298 } 299 300 void waitForLoad() throws InterruptedException { 301 boolean timedout = !mLatch.await(PAGE_LOAD_TIMEOUT, TimeUnit.MILLISECONDS); 302 if (timedout) { 303 Log.w(TAG, "page timeout. trying to stop."); 304 // try to stop page load 305 mInst.runOnMainSync(new Runnable(){ 306 public void run() { 307 mController.getTabControl().getCurrentTab().getWebView().stopLoading(); 308 } 309 }); 310 // try to wait for count down latch again 311 timedout = !mLatch.await(5000, TimeUnit.MILLISECONDS); 312 if (timedout) { 313 throw new RuntimeException("failed to stop timedout site, is browser pegged?"); 314 } 315 } 316 } 317 318 private static class RunStatus { 319 private File mFile; 320 private int iteration; 321 private int page; 322 private String url; 323 private boolean isRecovery; 324 private boolean allClear; 325 326 private RunStatus(File file) throws IOException { 327 mFile = file; 328 FileReader input = null; 329 BufferedReader reader = null; 330 isRecovery = false; 331 allClear = false; 332 iteration = 0; 333 page = 0; 334 try { 335 input = new FileReader(mFile); 336 isRecovery = true; 337 reader = new BufferedReader(input); 338 String line = reader.readLine(); 339 if (line == null) 340 return; 341 iteration = Integer.parseInt(line); 342 line = reader.readLine(); 343 if (line == null) 344 return; 345 page = Integer.parseInt(line); 346 } catch (FileNotFoundException ex) { 347 return; 348 } catch (NumberFormatException nfe) { 349 Log.wtf(TAG, "unexpected data in status file, will start from begining"); 350 return; 351 } finally { 352 try { 353 if (reader != null) { 354 reader.close(); 355 } 356 } finally { 357 if (input != null) { 358 input.close(); 359 } 360 } 361 } 362 } 363 364 public static RunStatus load() throws IOException { 365 return load(sStatusFile); 366 } 367 368 public static RunStatus load(String file) throws IOException { 369 return new RunStatus(new File(sExternalStorage, file)); 370 } 371 372 public void write() throws IOException { 373 FileWriter output = null; 374 if (mFile.exists()) { 375 mFile.delete(); 376 } 377 try { 378 output = new FileWriter(mFile); 379 output.write(iteration + newLine); 380 output.write(page + newLine); 381 output.write(url + newLine); 382 } finally { 383 if (output != null) { 384 output.close(); 385 } 386 } 387 } 388 389 public void cleanUp() { 390 // only perform cleanup when allClear flag is set 391 // i.e. when the test was not interrupted by a Java crash 392 if (mFile.exists() && allClear) { 393 mFile.delete(); 394 } 395 } 396 397 public void resetPage() { 398 page = 0; 399 } 400 401 public void incrementPage() { 402 ++page; 403 allClear = true; 404 } 405 406 public void incrementIteration() { 407 ++iteration; 408 } 409 410 public int getPage() { 411 return page; 412 } 413 414 public int getIteration() { 415 return iteration; 416 } 417 418 public boolean getIsRecovery() { 419 return isRecovery; 420 } 421 422 public void setUrl(String url) { 423 this.url = url; 424 allClear = false; 425 } 426 } 427 428 /** 429 * Loops over a list of URLs, points the browser to each one, and records the time elapsed. 430 * 431 * @param input the reader from which to get the URLs. 432 * @param writer the writer to which to output the results. 433 * @param clearCache determines whether the cache is cleared before loading each page 434 * @param loopCount the number of times to loop through the list of pages 435 * @throws IOException unable to read from input or write to writer. 436 * @throws InterruptedException the thread was interrupted waiting for the page to load. 437 */ 438 void loopUrls(BufferedReader input, OutputStreamWriter writer, 439 boolean clearCache, int loopCount) 440 throws IOException, InterruptedException { 441 Tab tab = mController.getTabControl().getCurrentTab(); 442 WebView webView = tab.getWebView(); 443 444 List<String> pages = new LinkedList<String>(); 445 446 String page; 447 while (null != (page = input.readLine())) { 448 if (!TextUtils.isEmpty(page)) { 449 pages.add(page); 450 } 451 } 452 453 Iterator<String> iterator = pages.iterator(); 454 for (int i = 0; i < mStatus.getPage(); ++i) { 455 iterator.next(); 456 } 457 458 if (mStatus.getIsRecovery()) { 459 Log.e(TAG, "Recovering after crash: " + iterator.next()); 460 mStatus.incrementPage(); 461 } 462 463 while (mStatus.getIteration() < loopCount) { 464 if (clearCache) { 465 clearCacheUiThread(webView, true); 466 } 467 while(iterator.hasNext()) { 468 page = iterator.next(); 469 mStatus.setUrl(page); 470 mStatus.write(); 471 Log.i(TAG, "start: " + page); 472 Uri uri = Uri.parse(page); 473 final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 474 intent.putExtra(Browser.EXTRA_APPLICATION_ID, 475 getInstrumentation().getTargetContext().getPackageName()); 476 477 long startTime = System.currentTimeMillis(); 478 resetForNewPage(); 479 mInst.runOnMainSync(new Runnable() { 480 481 public void run() { 482 mActivity.onNewIntent(intent); 483 } 484 485 }); 486 waitForLoad(); 487 long stopTime = System.currentTimeMillis(); 488 489 String url = getUrlUiThread(webView); 490 Log.i(TAG, "finish: " + url); 491 492 if (writer != null) { 493 writer.write(page + "|" + (stopTime - startTime) + newLine); 494 writer.flush(); 495 } 496 497 mStatus.incrementPage(); 498 } 499 mStatus.incrementIteration(); 500 mStatus.resetPage(); 501 iterator = pages.iterator(); 502 } 503 } 504 505 public void testLoadPerformance() throws IOException, InterruptedException { 506 setUpBrowser(); 507 508 OutputStreamWriter writer = getOutputStream(); 509 try { 510 BufferedReader bufferedReader = getInputStream(); 511 try { 512 loopUrls(bufferedReader, writer, true, PERF_LOOPCOUNT); 513 } finally { 514 if (bufferedReader != null) { 515 bufferedReader.close(); 516 } 517 } 518 } catch (FileNotFoundException fnfe) { 519 Log.e(TAG, fnfe.getMessage(), fnfe); 520 fail("Test environment not setup correctly"); 521 } finally { 522 if (writer != null) { 523 writer.close(); 524 } 525 } 526 } 527 528 public void testStability() throws IOException, InterruptedException { 529 setUpBrowser(); 530 531 BufferedReader bufferedReader = getInputStream(); 532 try { 533 loopUrls(bufferedReader, null, true, STABILITY_LOOPCOUNT); 534 } catch (FileNotFoundException fnfe) { 535 Log.e(TAG, fnfe.getMessage(), fnfe); 536 fail("Test environment not setup correctly"); 537 } finally { 538 if (bufferedReader != null) { 539 bufferedReader.close(); 540 } 541 } 542 } 543 544 private void clearCacheUiThread(final WebView webView, final boolean includeDiskFiles) { 545 Runnable runner = new Runnable() { 546 547 @Override 548 public void run() { 549 webView.clearCache(includeDiskFiles); 550 } 551 }; 552 getInstrumentation().runOnMainSync(runner); 553 } 554 555 private String getUrlUiThread(final WebView webView) { 556 WebViewUrlGetter urlGetter = new WebViewUrlGetter(webView); 557 getInstrumentation().runOnMainSync(urlGetter); 558 return urlGetter.getUrl(); 559 } 560 561 private class WebViewUrlGetter implements Runnable { 562 563 private WebView mWebView; 564 private String mUrl; 565 566 public WebViewUrlGetter(WebView webView) { 567 mWebView = webView; 568 } 569 570 @Override 571 public void run() { 572 mUrl = null; 573 mUrl = mWebView.getUrl(); 574 } 575 576 public String getUrl() { 577 if (mUrl != null) { 578 return mUrl; 579 } else 580 throw new IllegalStateException("url has not been fetched yet"); 581 } 582 } 583 } 584