1 #!/usr/bin/env python 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """Basic pyauto performance tests. 7 8 For tests that need to be run for multiple iterations (e.g., so that average 9 and standard deviation values can be reported), the default number of iterations 10 run for each of these tests is specified by |_DEFAULT_NUM_ITERATIONS|. 11 That value can optionally be tweaked by setting an environment variable 12 'NUM_ITERATIONS' to a positive integer, representing the number of iterations 13 to run. An additional, initial iteration will also be run to "warm up" the 14 environment, and the result from that initial iteration will be ignored. 15 16 Some tests rely on repeatedly appending tabs to Chrome. Occasionally, these 17 automation calls time out, thereby affecting the timing measurements (see issue 18 crosbug.com/20503). To work around this, the tests discard timing measurements 19 that involve automation timeouts. The value |_DEFAULT_MAX_TIMEOUT_COUNT| 20 specifies the threshold number of timeouts that can be tolerated before the test 21 fails. To tweak this value, set environment variable 'MAX_TIMEOUT_COUNT' to the 22 desired threshold value. 23 """ 24 25 import BaseHTTPServer 26 import commands 27 import errno 28 import itertools 29 import logging 30 import math 31 import os 32 import posixpath 33 import re 34 import SimpleHTTPServer 35 import SocketServer 36 import signal 37 import subprocess 38 import sys 39 import tempfile 40 import threading 41 import time 42 import timeit 43 import urllib 44 import urllib2 45 import urlparse 46 47 import pyauto_functional # Must be imported before pyauto. 48 import pyauto 49 import simplejson # Must be imported after pyauto; located in third_party. 50 51 from netflix import NetflixTestHelper 52 import pyauto_utils 53 import test_utils 54 import webpagereplay 55 from youtube import YoutubeTestHelper 56 57 58 _CHROME_BASE_DIR = os.path.abspath(os.path.join( 59 os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, os.pardir)) 60 61 62 def FormatChromePath(posix_path, **kwargs): 63 """Convert a path relative to the Chromium root into an OS-specific path. 64 65 Args: 66 posix_path: a path string that may be a format(). 67 Example: 'src/third_party/{module_name}/__init__.py' 68 kwargs: args for the format replacement. 69 Example: {'module_name': 'pylib'} 70 71 Returns: 72 an absolute path in the current Chromium tree with formatting applied. 73 """ 74 formated_path = posix_path.format(**kwargs) 75 path_parts = formated_path.split('/') 76 return os.path.join(_CHROME_BASE_DIR, *path_parts) 77 78 79 def StandardDeviation(values): 80 """Returns the standard deviation of |values|.""" 81 avg = Mean(values) 82 if len(values) < 2 or not avg: 83 return 0.0 84 temp_vals = [math.pow(x - avg, 2) for x in values] 85 return math.sqrt(sum(temp_vals) / (len(temp_vals) - 1)) 86 87 88 def Mean(values): 89 """Returns the arithmetic mean of |values|.""" 90 if not values or None in values: 91 return None 92 return sum(values) / float(len(values)) 93 94 95 def GeometricMean(values): 96 """Returns the geometric mean of |values|.""" 97 if not values or None in values or [x for x in values if x < 0.0]: 98 return None 99 if 0.0 in values: 100 return 0.0 101 return math.exp(Mean([math.log(x) for x in values])) 102 103 104 class BasePerfTest(pyauto.PyUITest): 105 """Base class for performance tests.""" 106 107 _DEFAULT_NUM_ITERATIONS = 10 # Keep synced with desktopui_PyAutoPerfTests.py. 108 _DEFAULT_MAX_TIMEOUT_COUNT = 10 109 _PERF_OUTPUT_MARKER_PRE = '_PERF_PRE_' 110 _PERF_OUTPUT_MARKER_POST = '_PERF_POST_' 111 112 def setUp(self): 113 """Performs necessary setup work before running each test.""" 114 self._num_iterations = self._DEFAULT_NUM_ITERATIONS 115 if 'NUM_ITERATIONS' in os.environ: 116 self._num_iterations = int(os.environ['NUM_ITERATIONS']) 117 self._max_timeout_count = self._DEFAULT_MAX_TIMEOUT_COUNT 118 if 'MAX_TIMEOUT_COUNT' in os.environ: 119 self._max_timeout_count = int(os.environ['MAX_TIMEOUT_COUNT']) 120 self._timeout_count = 0 121 122 # For users who want to see local perf graphs for Chrome when running the 123 # tests on their own machines. 124 self._local_perf_dir = None 125 if 'LOCAL_PERF_DIR' in os.environ: 126 self._local_perf_dir = os.environ['LOCAL_PERF_DIR'] 127 if not os.path.exists(self._local_perf_dir): 128 self.fail('LOCAL_PERF_DIR environment variable specified as %s, ' 129 'but this directory does not exist.' % self._local_perf_dir) 130 # When outputting perf graph information on-the-fly for Chrome, this 131 # variable lets us know whether a perf measurement is for a new test 132 # execution, or the current test execution. 133 self._seen_graph_lines = {} 134 135 pyauto.PyUITest.setUp(self) 136 137 # Flush all buffers to disk and wait until system calms down. Must be done 138 # *after* calling pyauto.PyUITest.setUp, since that is where Chrome is 139 # killed and re-initialized for a new test. 140 # TODO(dennisjeffrey): Implement wait for idle CPU on Windows/Mac. 141 if self.IsLinux(): # IsLinux() also implies IsChromeOS(). 142 os.system('sync') 143 self._WaitForIdleCPU(60.0, 0.05) 144 145 def _IsPIDRunning(self, pid): 146 """Checks if a given process id is running. 147 148 Args: 149 pid: The process id of the process to check. 150 151 Returns: 152 True if the process is running. False if not. 153 """ 154 try: 155 # Note that this sends the signal 0, which should not interfere with the 156 # process. 157 os.kill(pid, 0) 158 except OSError, err: 159 if err.errno == errno.ESRCH: 160 return False 161 162 try: 163 with open('/proc/%s/status' % pid) as proc_file: 164 if 'zombie' in proc_file.read(): 165 return False 166 except IOError: 167 return False 168 return True 169 170 def _GetAllDescendentProcesses(self, pid): 171 pstree_out = subprocess.check_output(['pstree', '-p', '%s' % pid]) 172 children = re.findall('\((\d+)\)', pstree_out) 173 return [int(pid) for pid in children] 174 175 def _WaitForChromeExit(self, browser_info, timeout): 176 pid = browser_info['browser_pid'] 177 chrome_pids = self._GetAllDescendentProcesses(pid) 178 initial_time = time.time() 179 while time.time() - initial_time < timeout: 180 if any([self._IsPIDRunning(pid) for pid in chrome_pids]): 181 time.sleep(1) 182 else: 183 logging.info('_WaitForChromeExit() took: %s seconds', 184 time.time() - initial_time) 185 return 186 self.fail('_WaitForChromeExit() did not finish within %s seconds' % 187 timeout) 188 189 def tearDown(self): 190 if self._IsPGOMode(): 191 browser_info = self.GetBrowserInfo() 192 pid = browser_info['browser_pid'] 193 # session_manager kills chrome without waiting for it to cleanly exit. 194 # Until that behavior is changed, we stop it and wait for Chrome to exit 195 # cleanly before restarting it. See: 196 # crbug.com/264717 197 subprocess.call(['sudo', 'pkill', '-STOP', 'session_manager']) 198 os.kill(pid, signal.SIGINT) 199 self._WaitForChromeExit(browser_info, 120) 200 subprocess.call(['sudo', 'pkill', '-CONT', 'session_manager']) 201 202 pyauto.PyUITest.tearDown(self) 203 204 def _IsPGOMode(self): 205 return 'USE_PGO' in os.environ 206 207 def _WaitForIdleCPU(self, timeout, utilization): 208 """Waits for the CPU to become idle (< utilization). 209 210 Args: 211 timeout: The longest time in seconds to wait before throwing an error. 212 utilization: The CPU usage below which the system should be considered 213 idle (between 0 and 1.0 independent of cores/hyperthreads). 214 """ 215 time_passed = 0.0 216 fraction_non_idle_time = 1.0 217 logging.info('Starting to wait up to %fs for idle CPU...', timeout) 218 while fraction_non_idle_time >= utilization: 219 cpu_usage_start = self._GetCPUUsage() 220 time.sleep(2) 221 time_passed += 2.0 222 cpu_usage_end = self._GetCPUUsage() 223 fraction_non_idle_time = \ 224 self._GetFractionNonIdleCPUTime(cpu_usage_start, cpu_usage_end) 225 logging.info('Current CPU utilization = %f.', fraction_non_idle_time) 226 if time_passed > timeout: 227 self._LogProcessActivity() 228 message = ('CPU did not idle after %fs wait (utilization = %f).' % ( 229 time_passed, fraction_non_idle_time)) 230 231 # crosbug.com/37389 232 if self._IsPGOMode(): 233 logging.info(message) 234 logging.info('Still continuing because we are in PGO mode.') 235 return 236 237 self.fail(message) 238 logging.info('Wait for idle CPU took %fs (utilization = %f).', 239 time_passed, fraction_non_idle_time) 240 241 def _LogProcessActivity(self): 242 """Logs the output of top on Linux/Mac/CrOS. 243 244 TODO: use taskmgr or similar on Windows. 245 """ 246 if self.IsLinux() or self.IsMac(): # IsLinux() also implies IsChromeOS(). 247 logging.info('Logging current process activity using top.') 248 cmd = 'top -b -d1 -n1' 249 if self.IsMac(): 250 cmd = 'top -l1' 251 p = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, 252 stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) 253 output = p.stdout.read() 254 logging.info(output) 255 else: 256 logging.info('Process activity logging not implemented on this OS.') 257 258 def _AppendTab(self, url): 259 """Appends a tab and increments a counter if the automation call times out. 260 261 Args: 262 url: The string url to which the appended tab should be navigated. 263 """ 264 if not self.AppendTab(pyauto.GURL(url)): 265 self._timeout_count += 1 266 267 def _MeasureElapsedTime(self, python_command, num_invocations=1): 268 """Measures time (in msec) to execute a python command one or more times. 269 270 Args: 271 python_command: A callable. 272 num_invocations: An integer number of times to invoke the given command. 273 274 Returns: 275 The time required to execute the python command the specified number of 276 times, in milliseconds as a float. 277 """ 278 assert callable(python_command) 279 def RunCommand(): 280 for _ in range(num_invocations): 281 python_command() 282 timer = timeit.Timer(stmt=RunCommand) 283 return timer.timeit(number=1) * 1000 # Convert seconds to milliseconds. 284 285 def _OutputPerfForStandaloneGraphing(self, graph_name, description, value, 286 units, units_x, is_stacked): 287 """Outputs perf measurement data to a local folder to be graphed. 288 289 This function only applies to Chrome desktop, and assumes that environment 290 variable 'LOCAL_PERF_DIR' has been specified and refers to a valid directory 291 on the local machine. 292 293 Args: 294 graph_name: A string name for the graph associated with this performance 295 value. 296 description: A string description of the performance value. Should not 297 include spaces. 298 value: Either a single numeric value representing a performance 299 measurement, or else a list of (x, y) tuples representing one or more 300 long-running performance measurements, where 'x' is an x-axis value 301 (such as an iteration number) and 'y' is the corresponding performance 302 measurement. If a list of tuples is given, then the |units_x| 303 argument must also be specified. 304 units: A string representing the units of the performance measurement(s). 305 Should not include spaces. 306 units_x: A string representing the units of the x-axis values associated 307 with the performance measurements, such as 'iteration' if the x values 308 are iteration numbers. If this argument is specified, then the 309 |value| argument must be a list of (x, y) tuples. 310 is_stacked: True to draw a "stacked" graph. First-come values are 311 stacked at bottom by default. 312 """ 313 revision_num_file = os.path.join(self._local_perf_dir, 'last_revision.dat') 314 if os.path.exists(revision_num_file): 315 with open(revision_num_file) as f: 316 revision = int(f.read()) 317 else: 318 revision = 0 319 320 if not self._seen_graph_lines: 321 # We're about to output data for a new test run. 322 revision += 1 323 324 # Update graphs.dat. 325 existing_graphs = [] 326 graphs_file = os.path.join(self._local_perf_dir, 'graphs.dat') 327 if os.path.exists(graphs_file): 328 with open(graphs_file) as f: 329 existing_graphs = simplejson.loads(f.read()) 330 is_new_graph = True 331 for graph in existing_graphs: 332 if graph['name'] == graph_name: 333 is_new_graph = False 334 break 335 if is_new_graph: 336 new_graph = { 337 'name': graph_name, 338 'units': units, 339 'important': False, 340 } 341 if units_x: 342 new_graph['units_x'] = units_x 343 existing_graphs.append(new_graph) 344 with open(graphs_file, 'w') as f: 345 f.write(simplejson.dumps(existing_graphs)) 346 os.chmod(graphs_file, 0755) 347 348 # Update data file for this particular graph. 349 existing_lines = [] 350 data_file = os.path.join(self._local_perf_dir, graph_name + '-summary.dat') 351 if os.path.exists(data_file): 352 with open(data_file) as f: 353 existing_lines = f.readlines() 354 existing_lines = map( 355 simplejson.loads, map(lambda x: x.strip(), existing_lines)) 356 357 seen_key = graph_name 358 # We assume that the first line |existing_lines[0]| is the latest. 359 if units_x: 360 new_line = { 361 'rev': revision, 362 'traces': { description: [] } 363 } 364 if seen_key in self._seen_graph_lines: 365 # We've added points previously for this graph line in the current 366 # test execution, so retrieve the original set of points specified in 367 # the most recent revision in the data file. 368 new_line = existing_lines[0] 369 if not description in new_line['traces']: 370 new_line['traces'][description] = [] 371 for x_value, y_value in value: 372 new_line['traces'][description].append([str(x_value), str(y_value)]) 373 else: 374 new_line = { 375 'rev': revision, 376 'traces': { description: [str(value), str(0.0)] } 377 } 378 379 if is_stacked: 380 new_line['stack'] = True 381 if 'stack_order' not in new_line: 382 new_line['stack_order'] = [] 383 if description not in new_line['stack_order']: 384 new_line['stack_order'].append(description) 385 386 if seen_key in self._seen_graph_lines: 387 # Update results for the most recent revision. 388 existing_lines[0] = new_line 389 else: 390 # New results for a new revision. 391 existing_lines.insert(0, new_line) 392 self._seen_graph_lines[seen_key] = True 393 394 existing_lines = map(simplejson.dumps, existing_lines) 395 with open(data_file, 'w') as f: 396 f.write('\n'.join(existing_lines)) 397 os.chmod(data_file, 0755) 398 399 with open(revision_num_file, 'w') as f: 400 f.write(str(revision)) 401 402 def _OutputPerfGraphValue(self, description, value, units, 403 graph_name, units_x=None, is_stacked=False): 404 """Outputs a performance value to have it graphed on the performance bots. 405 406 The output format differs, depending on whether the current platform is 407 Chrome desktop or ChromeOS. 408 409 For ChromeOS, the performance bots have a 30-character limit on the length 410 of the key associated with a performance value. A key on ChromeOS is 411 considered to be of the form "units_description" (for example, 412 "milliseconds_NewTabPage"), and is created from the |units| and 413 |description| passed as input to this function. Any characters beyond the 414 length 30 limit are truncated before results are stored in the autotest 415 database. 416 417 Args: 418 description: A string description of the performance value. Should not 419 include spaces. 420 value: Either a numeric value representing a performance measurement, or 421 a list of values to be averaged. Lists may also contain (x, y) tuples 422 representing one or more performance measurements, where 'x' is an 423 x-axis value (such as an iteration number) and 'y' is the 424 corresponding performance measurement. If a list of tuples is given, 425 the |units_x| argument must also be specified. 426 units: A string representing the units of the performance measurement(s). 427 Should not include spaces. 428 graph_name: A string name for the graph associated with this performance 429 value. Only used on Chrome desktop. 430 units_x: A string representing the units of the x-axis values associated 431 with the performance measurements, such as 'iteration' if the x values 432 are iteration numbers. If this argument is specified, then the 433 |value| argument must be a list of (x, y) tuples. 434 is_stacked: True to draw a "stacked" graph. First-come values are 435 stacked at bottom by default. 436 """ 437 if (isinstance(value, list) and value[0] is not None and 438 isinstance(value[0], tuple)): 439 assert units_x 440 if units_x: 441 assert isinstance(value, list) 442 443 if self.IsChromeOS(): 444 # Autotest doesn't support result lists. 445 autotest_value = value 446 if (isinstance(value, list) and value[0] is not None and 447 not isinstance(value[0], tuple)): 448 autotest_value = Mean(value) 449 450 if units_x: 451 # TODO(dennisjeffrey): Support long-running performance measurements on 452 # ChromeOS in a way that can be graphed: crosbug.com/21881. 453 pyauto_utils.PrintPerfResult(graph_name, description, autotest_value, 454 units + ' ' + units_x) 455 else: 456 # Output short-running performance results in a format understood by 457 # autotest. 458 perf_key = '%s_%s' % (units, description) 459 if len(perf_key) > 30: 460 logging.warning('The description "%s" will be truncated to "%s" ' 461 '(length 30) when added to the autotest database.', 462 perf_key, perf_key[:30]) 463 print '\n%s(\'%s\', %f)%s' % (self._PERF_OUTPUT_MARKER_PRE, 464 perf_key, autotest_value, 465 self._PERF_OUTPUT_MARKER_POST) 466 467 # Also output results in the format recognized by buildbot, for cases 468 # in which these tests are run on chromeOS through buildbot. Since 469 # buildbot supports result lists, it's ok for |value| to be a list here. 470 pyauto_utils.PrintPerfResult(graph_name, description, value, units) 471 472 sys.stdout.flush() 473 else: 474 # TODO(dmikurube): Support stacked graphs in PrintPerfResult. 475 # See http://crbug.com/122119. 476 if units_x: 477 pyauto_utils.PrintPerfResult(graph_name, description, value, 478 units + ' ' + units_x) 479 else: 480 pyauto_utils.PrintPerfResult(graph_name, description, value, units) 481 482 if self._local_perf_dir: 483 self._OutputPerfForStandaloneGraphing( 484 graph_name, description, value, units, units_x, is_stacked) 485 486 def _OutputEventForStandaloneGraphing(self, description, event_list): 487 """Outputs event information to a local folder to be graphed. 488 489 See function _OutputEventGraphValue below for a description of an event. 490 491 This function only applies to Chrome Endure tests running on Chrome desktop, 492 and assumes that environment variable 'LOCAL_PERF_DIR' has been specified 493 and refers to a valid directory on the local machine. 494 495 Args: 496 description: A string description of the event. Should not include 497 spaces. 498 event_list: A list of (x, y) tuples representing one or more events 499 occurring during an endurance test, where 'x' is the time of the event 500 (in seconds since the start of the test), and 'y' is a dictionary 501 representing relevant data associated with that event (as key/value 502 pairs). 503 """ 504 revision_num_file = os.path.join(self._local_perf_dir, 'last_revision.dat') 505 if os.path.exists(revision_num_file): 506 with open(revision_num_file) as f: 507 revision = int(f.read()) 508 else: 509 revision = 0 510 511 if not self._seen_graph_lines: 512 # We're about to output data for a new test run. 513 revision += 1 514 515 existing_lines = [] 516 data_file = os.path.join(self._local_perf_dir, '_EVENT_-summary.dat') 517 if os.path.exists(data_file): 518 with open(data_file) as f: 519 existing_lines = f.readlines() 520 existing_lines = map(eval, map(lambda x: x.strip(), existing_lines)) 521 522 seen_event_type = description 523 value_list = [] 524 if seen_event_type in self._seen_graph_lines: 525 # We've added events previously for this event type in the current 526 # test execution, so retrieve the original set of values specified in 527 # the most recent revision in the data file. 528 value_list = existing_lines[0]['events'][description] 529 for event_time, event_data in event_list: 530 value_list.append([str(event_time), event_data]) 531 new_events = { 532 description: value_list 533 } 534 535 new_line = { 536 'rev': revision, 537 'events': new_events 538 } 539 540 if seen_event_type in self._seen_graph_lines: 541 # Update results for the most recent revision. 542 existing_lines[0] = new_line 543 else: 544 # New results for a new revision. 545 existing_lines.insert(0, new_line) 546 self._seen_graph_lines[seen_event_type] = True 547 548 existing_lines = map(str, existing_lines) 549 with open(data_file, 'w') as f: 550 f.write('\n'.join(existing_lines)) 551 os.chmod(data_file, 0755) 552 553 with open(revision_num_file, 'w') as f: 554 f.write(str(revision)) 555 556 def _OutputEventGraphValue(self, description, event_list): 557 """Outputs a set of events to have them graphed on the Chrome Endure bots. 558 559 An "event" can be anything recorded by a performance test that occurs at 560 particular times during a test execution. For example, a garbage collection 561 in the v8 heap can be considered an event. An event is distinguished from a 562 regular perf measurement in two ways: (1) an event is depicted differently 563 in the performance graphs than performance measurements; (2) an event can 564 be associated with zero or more data fields describing relevant information 565 associated with the event. For example, a garbage collection event will 566 occur at a particular time, and it may be associated with data such as 567 the number of collected bytes and/or the length of time it took to perform 568 the garbage collection. 569 570 This function only applies to Chrome Endure tests running on Chrome desktop. 571 572 Args: 573 description: A string description of the event. Should not include 574 spaces. 575 event_list: A list of (x, y) tuples representing one or more events 576 occurring during an endurance test, where 'x' is the time of the event 577 (in seconds since the start of the test), and 'y' is a dictionary 578 representing relevant data associated with that event (as key/value 579 pairs). 580 """ 581 pyauto_utils.PrintPerfResult('_EVENT_', description, event_list, '') 582 if self._local_perf_dir: 583 self._OutputEventForStandaloneGraphing(description, event_list) 584 585 def _PrintSummaryResults(self, description, values, units, graph_name): 586 """Logs summary measurement information. 587 588 This function computes and outputs the average and standard deviation of 589 the specified list of value measurements. It also invokes 590 _OutputPerfGraphValue() with the computed *average* value, to ensure the 591 average value can be plotted in a performance graph. 592 593 Args: 594 description: A string description for the specified results. 595 values: A list of numeric value measurements. 596 units: A string specifying the units for the specified measurements. 597 graph_name: A string name for the graph associated with this performance 598 value. Only used on Chrome desktop. 599 """ 600 logging.info('Overall results for: %s', description) 601 if values: 602 logging.info(' Average: %f %s', Mean(values), units) 603 logging.info(' Std dev: %f %s', StandardDeviation(values), units) 604 self._OutputPerfGraphValue(description, values, units, graph_name) 605 else: 606 logging.info('No results to report.') 607 608 def _RunNewTabTest(self, description, open_tab_command, graph_name, 609 num_tabs=1): 610 """Runs a perf test that involves opening new tab(s). 611 612 This helper function can be called from different tests to do perf testing 613 with different types of tabs. It is assumed that the |open_tab_command| 614 will open up a single tab. 615 616 Args: 617 description: A string description of the associated tab test. 618 open_tab_command: A callable that will open a single tab. 619 graph_name: A string name for the performance graph associated with this 620 test. Only used on Chrome desktop. 621 num_tabs: The number of tabs to open, i.e., the number of times to invoke 622 the |open_tab_command|. 623 """ 624 assert callable(open_tab_command) 625 626 timings = [] 627 for iteration in range(self._num_iterations + 1): 628 orig_timeout_count = self._timeout_count 629 elapsed_time = self._MeasureElapsedTime(open_tab_command, 630 num_invocations=num_tabs) 631 # Only count the timing measurement if no automation call timed out. 632 if self._timeout_count == orig_timeout_count: 633 # Ignore the first iteration. 634 if iteration: 635 timings.append(elapsed_time) 636 logging.info('Iteration %d of %d: %f milliseconds', iteration, 637 self._num_iterations, elapsed_time) 638 self.assertTrue(self._timeout_count <= self._max_timeout_count, 639 msg='Test exceeded automation timeout threshold.') 640 self.assertEqual(1 + num_tabs, self.GetTabCount(), 641 msg='Did not open %d new tab(s).' % num_tabs) 642 for _ in range(num_tabs): 643 self.CloseTab(tab_index=1) 644 645 self._PrintSummaryResults(description, timings, 'milliseconds', graph_name) 646 647 def _GetConfig(self): 648 """Load perf test configuration file. 649 650 Returns: 651 A dictionary that represents the config information. 652 """ 653 config_file = os.path.join(os.path.dirname(__file__), 'perf.cfg') 654 config = {'username': None, 655 'password': None, 656 'google_account_url': 'https://accounts.google.com/', 657 'gmail_url': 'https://www.gmail.com', 658 'plus_url': 'https://plus.google.com', 659 'docs_url': 'https://docs.google.com'} 660 if os.path.exists(config_file): 661 try: 662 new_config = pyauto.PyUITest.EvalDataFrom(config_file) 663 for key in new_config: 664 if new_config.get(key) is not None: 665 config[key] = new_config.get(key) 666 except SyntaxError, e: 667 logging.info('Could not read %s: %s', config_file, str(e)) 668 return config 669 670 def _LoginToGoogleAccount(self, account_key='test_google_account'): 671 """Logs in to a test Google account. 672 673 Login with user-defined credentials if they exist. 674 Else login with private test credentials if they exist. 675 Else fail. 676 677 Args: 678 account_key: The string key in private_tests_info.txt which is associated 679 with the test account login credentials to use. It will only 680 be used when fail to load user-defined credentials. 681 682 Raises: 683 RuntimeError: if could not get credential information. 684 """ 685 private_file = os.path.join(pyauto.PyUITest.DataDir(), 'pyauto_private', 686 'private_tests_info.txt') 687 config_file = os.path.join(os.path.dirname(__file__), 'perf.cfg') 688 config = self._GetConfig() 689 google_account_url = config.get('google_account_url') 690 username = config.get('username') 691 password = config.get('password') 692 if username and password: 693 logging.info( 694 'Using google account credential from %s', 695 os.path.join(os.path.dirname(__file__), 'perf.cfg')) 696 elif os.path.exists(private_file): 697 creds = self.GetPrivateInfo()[account_key] 698 username = creds['username'] 699 password = creds['password'] 700 logging.info( 701 'User-defined credentials not found,' + 702 ' using private test credentials instead.') 703 else: 704 message = 'No user-defined or private test ' \ 705 'credentials could be found. ' \ 706 'Please specify credential information in %s.' \ 707 % config_file 708 raise RuntimeError(message) 709 test_utils.GoogleAccountsLogin( 710 self, username, password, url=google_account_url) 711 self.NavigateToURL('about:blank') # Clear the existing tab. 712 713 def _GetCPUUsage(self): 714 """Returns machine's CPU usage. 715 716 This function uses /proc/stat to identify CPU usage, and therefore works 717 only on Linux/ChromeOS. 718 719 Returns: 720 A dictionary with 'user', 'nice', 'system' and 'idle' values. 721 Sample dictionary: 722 { 723 'user': 254544, 724 'nice': 9, 725 'system': 254768, 726 'idle': 2859878, 727 } 728 """ 729 try: 730 f = open('/proc/stat') 731 cpu_usage_str = f.readline().split() 732 f.close() 733 except IOError, e: 734 self.fail('Could not retrieve CPU usage: ' + str(e)) 735 return { 736 'user': int(cpu_usage_str[1]), 737 'nice': int(cpu_usage_str[2]), 738 'system': int(cpu_usage_str[3]), 739 'idle': int(cpu_usage_str[4]) 740 } 741 742 def _GetFractionNonIdleCPUTime(self, cpu_usage_start, cpu_usage_end): 743 """Computes the fraction of CPU time spent non-idling. 744 745 This function should be invoked using before/after values from calls to 746 _GetCPUUsage(). 747 """ 748 time_non_idling_end = (cpu_usage_end['user'] + cpu_usage_end['nice'] + 749 cpu_usage_end['system']) 750 time_non_idling_start = (cpu_usage_start['user'] + cpu_usage_start['nice'] + 751 cpu_usage_start['system']) 752 total_time_end = (cpu_usage_end['user'] + cpu_usage_end['nice'] + 753 cpu_usage_end['system'] + cpu_usage_end['idle']) 754 total_time_start = (cpu_usage_start['user'] + cpu_usage_start['nice'] + 755 cpu_usage_start['system'] + cpu_usage_start['idle']) 756 return ((float(time_non_idling_end) - time_non_idling_start) / 757 (total_time_end - total_time_start)) 758 759 def ExtraChromeFlags(self): 760 """Ensures Chrome is launched with custom flags. 761 762 Returns: 763 A list of extra flags to pass to Chrome when it is launched. 764 """ 765 flags = super(BasePerfTest, self).ExtraChromeFlags() 766 # Window size impacts a variety of perf tests, ensure consistency. 767 flags.append('--window-size=1024,768') 768 if self._IsPGOMode(): 769 flags = flags + ['--child-clean-exit', '--no-sandbox'] 770 return flags 771 772 773 class TabPerfTest(BasePerfTest): 774 """Tests that involve opening tabs.""" 775 776 def testNewTab(self): 777 """Measures time to open a new tab.""" 778 self._RunNewTabTest('NewTabPage', 779 lambda: self._AppendTab('chrome://newtab'), 'open_tab') 780 781 def testNewTabFlash(self): 782 """Measures time to open a new tab navigated to a flash page.""" 783 self.assertTrue( 784 os.path.exists(os.path.join(self.ContentDataDir(), 'plugin', 785 'flash.swf')), 786 msg='Missing required flash data file.') 787 url = self.GetFileURLForContentDataPath('plugin', 'flash.swf') 788 self._RunNewTabTest('NewTabFlashPage', lambda: self._AppendTab(url), 789 'open_tab') 790 791 def test20Tabs(self): 792 """Measures time to open 20 tabs.""" 793 self._RunNewTabTest('20TabsNewTabPage', 794 lambda: self._AppendTab('chrome://newtab'), 795 'open_20_tabs', num_tabs=20) 796 797 798 class BenchmarkPerfTest(BasePerfTest): 799 """Benchmark performance tests.""" 800 801 def testV8BenchmarkSuite(self): 802 """Measures score from v8 benchmark suite.""" 803 url = self.GetFileURLForDataPath('v8_benchmark_v6', 'run.html') 804 805 def _RunBenchmarkOnce(url): 806 """Runs the v8 benchmark suite once and returns the results in a dict.""" 807 self.assertTrue(self.AppendTab(pyauto.GURL(url)), 808 msg='Failed to append tab for v8 benchmark suite.') 809 js_done = """ 810 var val = document.getElementById("status").innerHTML; 811 window.domAutomationController.send(val); 812 """ 813 self.assertTrue( 814 self.WaitUntil( 815 lambda: 'Score:' in self.ExecuteJavascript(js_done, tab_index=1), 816 timeout=300, expect_retval=True, retry_sleep=1), 817 msg='Timed out when waiting for v8 benchmark score.') 818 819 js_get_results = """ 820 var result = {}; 821 result['final_score'] = document.getElementById("status").innerHTML; 822 result['all_results'] = document.getElementById("results").innerHTML; 823 window.domAutomationController.send(JSON.stringify(result)); 824 """ 825 results = eval(self.ExecuteJavascript(js_get_results, tab_index=1)) 826 score_pattern = '(\w+): (\d+)' 827 final_score = re.search(score_pattern, results['final_score']).group(2) 828 result_dict = {'final_score': int(final_score)} 829 for match in re.finditer(score_pattern, results['all_results']): 830 benchmark_name = match.group(1) 831 benchmark_score = match.group(2) 832 result_dict[benchmark_name] = int(benchmark_score) 833 self.CloseTab(tab_index=1) 834 return result_dict 835 836 timings = {} 837 for iteration in xrange(self._num_iterations + 1): 838 result_dict = _RunBenchmarkOnce(url) 839 # Ignore the first iteration. 840 if iteration: 841 for key, val in result_dict.items(): 842 timings.setdefault(key, []).append(val) 843 logging.info('Iteration %d of %d:\n%s', iteration, 844 self._num_iterations, self.pformat(result_dict)) 845 846 for key, val in timings.items(): 847 if key == 'final_score': 848 self._PrintSummaryResults('V8Benchmark', val, 'score', 849 'v8_benchmark_final') 850 else: 851 self._PrintSummaryResults('V8Benchmark-%s' % key, val, 'score', 852 'v8_benchmark_individual') 853 854 def testSunSpider(self): 855 """Runs the SunSpider javascript benchmark suite.""" 856 url = self.GetFileURLForDataPath('sunspider', 'sunspider-driver.html') 857 self.assertTrue(self.AppendTab(pyauto.GURL(url)), 858 msg='Failed to append tab for SunSpider benchmark suite.') 859 860 js_is_done = """ 861 var done = false; 862 if (document.getElementById("console")) 863 done = true; 864 window.domAutomationController.send(JSON.stringify(done)); 865 """ 866 self.assertTrue( 867 self.WaitUntil( 868 lambda: self.ExecuteJavascript(js_is_done, tab_index=1), 869 timeout=300, expect_retval='true', retry_sleep=1), 870 msg='Timed out when waiting for SunSpider benchmark score.') 871 872 js_get_results = """ 873 window.domAutomationController.send( 874 document.getElementById("console").innerHTML); 875 """ 876 # Append '<br>' to the result to simplify regular expression matching. 877 results = self.ExecuteJavascript(js_get_results, tab_index=1) + '<br>' 878 total = re.search('Total:\s*([\d.]+)ms', results).group(1) 879 logging.info('Total: %f ms', float(total)) 880 self._OutputPerfGraphValue('SunSpider-total', float(total), 'ms', 881 'sunspider_total') 882 883 for match_category in re.finditer('\s\s(\w+):\s*([\d.]+)ms.+?<br><br>', 884 results): 885 category_name = match_category.group(1) 886 category_result = match_category.group(2) 887 logging.info('Benchmark "%s": %f ms', category_name, 888 float(category_result)) 889 self._OutputPerfGraphValue('SunSpider-' + category_name, 890 float(category_result), 'ms', 891 'sunspider_individual') 892 893 for match_result in re.finditer('<br>\s\s\s\s([\w-]+):\s*([\d.]+)ms', 894 match_category.group(0)): 895 result_name = match_result.group(1) 896 result_value = match_result.group(2) 897 logging.info(' Result "%s-%s": %f ms', category_name, result_name, 898 float(result_value)) 899 self._OutputPerfGraphValue( 900 'SunSpider-%s-%s' % (category_name, result_name), 901 float(result_value), 'ms', 'sunspider_individual') 902 903 def testDromaeoSuite(self): 904 """Measures results from Dromaeo benchmark suite.""" 905 url = self.GetFileURLForDataPath('dromaeo', 'index.html') 906 self.assertTrue(self.AppendTab(pyauto.GURL(url + '?dromaeo')), 907 msg='Failed to append tab for Dromaeo benchmark suite.') 908 909 js_is_ready = """ 910 var val = document.getElementById('pause').value; 911 window.domAutomationController.send(val); 912 """ 913 self.assertTrue( 914 self.WaitUntil( 915 lambda: self.ExecuteJavascript(js_is_ready, tab_index=1), 916 timeout=30, expect_retval='Run', retry_sleep=1), 917 msg='Timed out when waiting for Dromaeo benchmark to load.') 918 919 js_run = """ 920 $('#pause').val('Run').click(); 921 window.domAutomationController.send('done'); 922 """ 923 self.ExecuteJavascript(js_run, tab_index=1) 924 925 js_is_done = """ 926 var val = document.getElementById('timebar').innerHTML; 927 window.domAutomationController.send(val); 928 """ 929 self.assertTrue( 930 self.WaitUntil( 931 lambda: 'Total' in self.ExecuteJavascript(js_is_done, tab_index=1), 932 timeout=900, expect_retval=True, retry_sleep=2), 933 msg='Timed out when waiting for Dromaeo benchmark to complete.') 934 935 js_get_results = """ 936 var result = {}; 937 result['total_result'] = $('#timebar strong').html(); 938 result['all_results'] = {}; 939 $('.result-item.done').each(function (i) { 940 var group_name = $(this).find('.test b').html().replace(':', ''); 941 var group_results = {}; 942 group_results['result'] = 943 $(this).find('span').html().replace('runs/s', '') 944 945 group_results['sub_groups'] = {} 946 $(this).find('li').each(function (i) { 947 var sub_name = $(this).find('b').html().replace(':', ''); 948 group_results['sub_groups'][sub_name] = 949 $(this).text().match(/: ([\d.]+)/)[1] 950 }); 951 result['all_results'][group_name] = group_results; 952 }); 953 window.domAutomationController.send(JSON.stringify(result)); 954 """ 955 results = eval(self.ExecuteJavascript(js_get_results, tab_index=1)) 956 total_result = results['total_result'] 957 logging.info('Total result: ' + total_result) 958 self._OutputPerfGraphValue('Dromaeo-total', float(total_result), 959 'runsPerSec', 'dromaeo_total') 960 961 for group_name, group in results['all_results'].iteritems(): 962 logging.info('Benchmark "%s": %s', group_name, group['result']) 963 self._OutputPerfGraphValue('Dromaeo-' + group_name.replace(' ', ''), 964 float(group['result']), 'runsPerSec', 965 'dromaeo_individual') 966 for benchmark_name, benchmark_score in group['sub_groups'].iteritems(): 967 logging.info(' Result "%s": %s', benchmark_name, benchmark_score) 968 969 def testSpaceport(self): 970 """Measures results from Spaceport benchmark suite.""" 971 # TODO(tonyg): Test is failing on bots. Diagnose and re-enable. 972 pass 973 974 # url = self.GetFileURLForDataPath('third_party', 'spaceport', 'index.html') 975 # self.assertTrue(self.AppendTab(pyauto.GURL(url + '?auto')), 976 # msg='Failed to append tab for Spaceport benchmark suite.') 977 # 978 # # The test reports results to console.log in the format "name: value". 979 # # Inject a bit of JS to intercept those. 980 # js_collect_console_log = """ 981 # window.__pyautoresult = {}; 982 # window.console.log = function(str) { 983 # if (!str) return; 984 # var key_val = str.split(': '); 985 # if (!key_val.length == 2) return; 986 # __pyautoresult[key_val[0]] = key_val[1]; 987 # }; 988 # window.domAutomationController.send('done'); 989 # """ 990 # self.ExecuteJavascript(js_collect_console_log, tab_index=1) 991 # 992 # def _IsDone(): 993 # expected_num_results = 30 # The number of tests in benchmark. 994 # results = eval(self.ExecuteJavascript(js_get_results, tab_index=1)) 995 # return expected_num_results == len(results) 996 # 997 # js_get_results = """ 998 # window.domAutomationController.send( 999 # JSON.stringify(window.__pyautoresult)); 1000 # """ 1001 # self.assertTrue( 1002 # self.WaitUntil(_IsDone, timeout=1200, expect_retval=True, 1003 # retry_sleep=5), 1004 # msg='Timed out when waiting for Spaceport benchmark to complete.') 1005 # results = eval(self.ExecuteJavascript(js_get_results, tab_index=1)) 1006 # 1007 # for key in results: 1008 # suite, test = key.split('.') 1009 # value = float(results[key]) 1010 # self._OutputPerfGraphValue(test, value, 'ObjectsAt30FPS', suite) 1011 # self._PrintSummaryResults('Overall', [float(x) for x in results.values()], 1012 # 'ObjectsAt30FPS', 'Overall') 1013 1014 1015 class LiveWebappLoadTest(BasePerfTest): 1016 """Tests that involve performance measurements of live webapps. 1017 1018 These tests connect to live webpages (e.g., Gmail, Calendar, Docs) and are 1019 therefore subject to network conditions. These tests are meant to generate 1020 "ball-park" numbers only (to see roughly how long things take to occur from a 1021 user's perspective), and are not expected to be precise. 1022 """ 1023 1024 def testNewTabGmail(self): 1025 """Measures time to open a tab to a logged-in Gmail account. 1026 1027 Timing starts right before the new tab is opened, and stops as soon as the 1028 webpage displays the substring 'Last account activity:'. 1029 """ 1030 EXPECTED_SUBSTRING = 'Last account activity:' 1031 1032 def _SubstringExistsOnPage(): 1033 js = """ 1034 var frame = document.getElementById("canvas_frame"); 1035 var divs = frame.contentDocument.getElementsByTagName("div"); 1036 for (var i = 0; i < divs.length; ++i) { 1037 if (divs[i].innerHTML.indexOf("%s") >= 0) 1038 window.domAutomationController.send("true"); 1039 } 1040 window.domAutomationController.send("false"); 1041 """ % EXPECTED_SUBSTRING 1042 return self.ExecuteJavascript(js, tab_index=1) 1043 1044 def _RunSingleGmailTabOpen(): 1045 self._AppendTab('http://www.gmail.com') 1046 self.assertTrue(self.WaitUntil(_SubstringExistsOnPage, timeout=120, 1047 expect_retval='true', retry_sleep=0.10), 1048 msg='Timed out waiting for expected Gmail string.') 1049 1050 self._LoginToGoogleAccount() 1051 self._RunNewTabTest('NewTabGmail', _RunSingleGmailTabOpen, 1052 'open_tab_live_webapp') 1053 1054 def testNewTabCalendar(self): 1055 """Measures time to open a tab to a logged-in Calendar account. 1056 1057 Timing starts right before the new tab is opened, and stops as soon as the 1058 webpage displays the calendar print button (title 'Print my calendar'). 1059 """ 1060 EXPECTED_SUBSTRING = 'Month' 1061 1062 def _DivTitleStartsWith(): 1063 js = """ 1064 var divs = document.getElementsByTagName("div"); 1065 for (var i = 0; i < divs.length; ++i) { 1066 if (divs[i].innerHTML == "%s") 1067 window.domAutomationController.send("true"); 1068 } 1069 window.domAutomationController.send("false"); 1070 """ % EXPECTED_SUBSTRING 1071 return self.ExecuteJavascript(js, tab_index=1) 1072 1073 def _RunSingleCalendarTabOpen(): 1074 self._AppendTab('http://calendar.google.com') 1075 self.assertTrue(self.WaitUntil(_DivTitleStartsWith, timeout=120, 1076 expect_retval='true', retry_sleep=0.10), 1077 msg='Timed out waiting for expected Calendar string.') 1078 1079 self._LoginToGoogleAccount() 1080 self._RunNewTabTest('NewTabCalendar', _RunSingleCalendarTabOpen, 1081 'open_tab_live_webapp') 1082 1083 def testNewTabDocs(self): 1084 """Measures time to open a tab to a logged-in Docs account. 1085 1086 Timing starts right before the new tab is opened, and stops as soon as the 1087 webpage displays the expected substring 'last modified' (case insensitive). 1088 """ 1089 EXPECTED_SUBSTRING = 'sort' 1090 1091 def _SubstringExistsOnPage(): 1092 js = """ 1093 var divs = document.getElementsByTagName("div"); 1094 for (var i = 0; i < divs.length; ++i) { 1095 if (divs[i].innerHTML.toLowerCase().indexOf("%s") >= 0) 1096 window.domAutomationController.send("true"); 1097 } 1098 window.domAutomationController.send("false"); 1099 """ % EXPECTED_SUBSTRING 1100 return self.ExecuteJavascript(js, tab_index=1) 1101 1102 def _RunSingleDocsTabOpen(): 1103 self._AppendTab('http://docs.google.com') 1104 self.assertTrue(self.WaitUntil(_SubstringExistsOnPage, timeout=120, 1105 expect_retval='true', retry_sleep=0.10), 1106 msg='Timed out waiting for expected Docs string.') 1107 1108 self._LoginToGoogleAccount() 1109 self._RunNewTabTest('NewTabDocs', _RunSingleDocsTabOpen, 1110 'open_tab_live_webapp') 1111 1112 1113 class NetflixPerfTest(BasePerfTest, NetflixTestHelper): 1114 """Test Netflix video performance.""" 1115 1116 def __init__(self, methodName='runTest', **kwargs): 1117 pyauto.PyUITest.__init__(self, methodName, **kwargs) 1118 NetflixTestHelper.__init__(self, self) 1119 1120 def tearDown(self): 1121 self.SignOut() 1122 pyauto.PyUITest.tearDown(self) 1123 1124 def testNetflixDroppedFrames(self): 1125 """Measures the Netflix video dropped frames/second. Runs for 60 secs.""" 1126 self.LoginAndStartPlaying() 1127 self.CheckNetflixPlaying(self.IS_PLAYING, 1128 'Player did not start playing the title.') 1129 # Ignore first 10 seconds of video playing so we get smooth videoplayback. 1130 time.sleep(10) 1131 init_dropped_frames = self._GetVideoDroppedFrames() 1132 dropped_frames = [] 1133 prev_dropped_frames = 0 1134 for iteration in xrange(60): 1135 # Ignoring initial dropped frames of first 10 seconds. 1136 total_dropped_frames = self._GetVideoDroppedFrames() - init_dropped_frames 1137 dropped_frames_last_sec = total_dropped_frames - prev_dropped_frames 1138 dropped_frames.append(dropped_frames_last_sec) 1139 logging.info('Iteration %d of %d: %f dropped frames in the last second', 1140 iteration + 1, 60, dropped_frames_last_sec) 1141 prev_dropped_frames = total_dropped_frames 1142 # Play the video for some time. 1143 time.sleep(1) 1144 self._PrintSummaryResults('NetflixDroppedFrames', dropped_frames, 'frames', 1145 'netflix_dropped_frames') 1146 1147 def testNetflixCPU(self): 1148 """Measures the Netflix video CPU usage. Runs for 60 seconds.""" 1149 self.LoginAndStartPlaying() 1150 self.CheckNetflixPlaying(self.IS_PLAYING, 1151 'Player did not start playing the title.') 1152 # Ignore first 10 seconds of video playing so we get smooth videoplayback. 1153 time.sleep(10) 1154 init_dropped_frames = self._GetVideoDroppedFrames() 1155 init_video_frames = self._GetVideoFrames() 1156 cpu_usage_start = self._GetCPUUsage() 1157 total_shown_frames = 0 1158 # Play the video for some time. 1159 time.sleep(60) 1160 total_video_frames = self._GetVideoFrames() - init_video_frames 1161 total_dropped_frames = self._GetVideoDroppedFrames() - init_dropped_frames 1162 cpu_usage_end = self._GetCPUUsage() 1163 fraction_non_idle_time = \ 1164 self._GetFractionNonIdleCPUTime(cpu_usage_start, cpu_usage_end) 1165 # Counting extrapolation for utilization to play the video. 1166 extrapolation_value = fraction_non_idle_time * \ 1167 (float(total_video_frames) + total_dropped_frames) / total_video_frames 1168 logging.info('Netflix CPU extrapolation: %f', extrapolation_value) 1169 self._OutputPerfGraphValue('NetflixCPUExtrapolation', extrapolation_value, 1170 'extrapolation', 'netflix_cpu_extrapolation') 1171 1172 1173 class YoutubePerfTest(BasePerfTest, YoutubeTestHelper): 1174 """Test Youtube video performance.""" 1175 1176 def __init__(self, methodName='runTest', **kwargs): 1177 pyauto.PyUITest.__init__(self, methodName, **kwargs) 1178 YoutubeTestHelper.__init__(self, self) 1179 1180 def _VerifyVideoTotalBytes(self): 1181 """Returns true if video total bytes information is available.""" 1182 return self.GetVideoTotalBytes() > 0 1183 1184 def _VerifyVideoLoadedBytes(self): 1185 """Returns true if video loaded bytes information is available.""" 1186 return self.GetVideoLoadedBytes() > 0 1187 1188 def StartVideoForPerformance(self, video_id='zuzaxlddWbk'): 1189 """Start the test video with all required buffering.""" 1190 self.PlayVideoAndAssert(video_id) 1191 self.ExecuteJavascript(""" 1192 ytplayer.setPlaybackQuality('hd720'); 1193 window.domAutomationController.send(''); 1194 """) 1195 self.AssertPlayerState(state=self.is_playing, 1196 msg='Player did not enter the playing state') 1197 self.assertTrue( 1198 self.WaitUntil(self._VerifyVideoTotalBytes, expect_retval=True), 1199 msg='Failed to get video total bytes information.') 1200 self.assertTrue( 1201 self.WaitUntil(self._VerifyVideoLoadedBytes, expect_retval=True), 1202 msg='Failed to get video loaded bytes information') 1203 loaded_video_bytes = self.GetVideoLoadedBytes() 1204 total_video_bytes = self.GetVideoTotalBytes() 1205 self.PauseVideo() 1206 logging.info('total_video_bytes: %f', total_video_bytes) 1207 # Wait for the video to finish loading. 1208 while total_video_bytes > loaded_video_bytes: 1209 loaded_video_bytes = self.GetVideoLoadedBytes() 1210 logging.info('loaded_video_bytes: %f', loaded_video_bytes) 1211 time.sleep(1) 1212 self.PlayVideo() 1213 # Ignore first 10 seconds of video playing so we get smooth videoplayback. 1214 time.sleep(10) 1215 1216 def testYoutubeDroppedFrames(self): 1217 """Measures the Youtube video dropped frames/second. Runs for 60 secs. 1218 1219 This test measures Youtube video dropped frames for three different types 1220 of videos like slow, normal and fast motion. 1221 """ 1222 youtube_video = {'Slow': 'VT1-sitWRtY', 1223 'Normal': '2tqK_3mKQUw', 1224 'Fast': '8ETDE0VGJY4', 1225 } 1226 for video_type in youtube_video: 1227 logging.info('Running %s video.', video_type) 1228 self.StartVideoForPerformance(youtube_video[video_type]) 1229 init_dropped_frames = self.GetVideoDroppedFrames() 1230 total_dropped_frames = 0 1231 dropped_fps = [] 1232 for iteration in xrange(60): 1233 frames = self.GetVideoDroppedFrames() - init_dropped_frames 1234 current_dropped_frames = frames - total_dropped_frames 1235 dropped_fps.append(current_dropped_frames) 1236 logging.info('Iteration %d of %d: %f dropped frames in the last ' 1237 'second', iteration + 1, 60, current_dropped_frames) 1238 total_dropped_frames = frames 1239 # Play the video for some time 1240 time.sleep(1) 1241 graph_description = 'YoutubeDroppedFrames' + video_type 1242 self._PrintSummaryResults(graph_description, dropped_fps, 'frames', 1243 'youtube_dropped_frames') 1244 1245 def testYoutubeCPU(self): 1246 """Measures the Youtube video CPU usage. Runs for 60 seconds. 1247 1248 Measures the Youtube video CPU usage (between 0 and 1), extrapolated to 1249 totalframes in the video by taking dropped frames into account. For smooth 1250 videoplayback this number should be < 0.5..1.0 on a hyperthreaded CPU. 1251 """ 1252 self.StartVideoForPerformance() 1253 init_dropped_frames = self.GetVideoDroppedFrames() 1254 logging.info('init_dropped_frames: %f', init_dropped_frames) 1255 cpu_usage_start = self._GetCPUUsage() 1256 total_shown_frames = 0 1257 for sec_num in xrange(60): 1258 # Play the video for some time. 1259 time.sleep(1) 1260 total_shown_frames = total_shown_frames + self.GetVideoFrames() 1261 logging.info('total_shown_frames: %f', total_shown_frames) 1262 total_dropped_frames = self.GetVideoDroppedFrames() - init_dropped_frames 1263 logging.info('total_dropped_frames: %f', total_dropped_frames) 1264 cpu_usage_end = self._GetCPUUsage() 1265 fraction_non_idle_time = self._GetFractionNonIdleCPUTime( 1266 cpu_usage_start, cpu_usage_end) 1267 logging.info('fraction_non_idle_time: %f', fraction_non_idle_time) 1268 total_frames = total_shown_frames + total_dropped_frames 1269 # Counting extrapolation for utilization to play the video. 1270 extrapolation_value = (fraction_non_idle_time * 1271 (float(total_frames) / total_shown_frames)) 1272 logging.info('Youtube CPU extrapolation: %f', extrapolation_value) 1273 # Video is still running so log some more detailed data. 1274 self._LogProcessActivity() 1275 self._OutputPerfGraphValue('YoutubeCPUExtrapolation', extrapolation_value, 1276 'extrapolation', 'youtube_cpu_extrapolation') 1277 1278 1279 class FlashVideoPerfTest(BasePerfTest): 1280 """General flash video performance tests.""" 1281 1282 def FlashVideo1080P(self): 1283 """Measures total dropped frames and average FPS for a 1080p flash video. 1284 1285 This is a temporary test to be run manually for now, needed to collect some 1286 performance statistics across different ChromeOS devices. 1287 """ 1288 # Open up the test webpage; it's assumed the test will start automatically. 1289 webpage_url = 'http://www/~arscott/fl/FlashVideoTests.html' 1290 self.assertTrue(self.AppendTab(pyauto.GURL(webpage_url)), 1291 msg='Failed to append tab for webpage.') 1292 1293 # Wait until the test is complete. 1294 js_is_done = """ 1295 window.domAutomationController.send(JSON.stringify(tests_done)); 1296 """ 1297 self.assertTrue( 1298 self.WaitUntil( 1299 lambda: self.ExecuteJavascript(js_is_done, tab_index=1) == 'true', 1300 timeout=300, expect_retval=True, retry_sleep=1), 1301 msg='Timed out when waiting for test result.') 1302 1303 # Retrieve and output the test results. 1304 js_results = """ 1305 window.domAutomationController.send(JSON.stringify(tests_results)); 1306 """ 1307 test_result = eval(self.ExecuteJavascript(js_results, tab_index=1)) 1308 test_result[0] = test_result[0].replace('true', 'True') 1309 test_result = eval(test_result[0]) # Webpage only does 1 test right now. 1310 1311 description = 'FlashVideo1080P' 1312 result = test_result['averageFPS'] 1313 logging.info('Result for %s: %f FPS (average)', description, result) 1314 self._OutputPerfGraphValue(description, result, 'FPS', 1315 'flash_video_1080p_fps') 1316 result = test_result['droppedFrames'] 1317 logging.info('Result for %s: %f dropped frames', description, result) 1318 self._OutputPerfGraphValue(description, result, 'DroppedFrames', 1319 'flash_video_1080p_dropped_frames') 1320 1321 1322 class WebGLTest(BasePerfTest): 1323 """Tests for WebGL performance.""" 1324 1325 def _RunWebGLTest(self, url, description, graph_name): 1326 """Measures FPS using a specified WebGL demo. 1327 1328 Args: 1329 url: The string URL that, once loaded, will run the WebGL demo (default 1330 WebGL demo settings are used, since this test does not modify any 1331 settings in the demo). 1332 description: A string description for this demo, used as a performance 1333 value description. Should not contain any spaces. 1334 graph_name: A string name for the performance graph associated with this 1335 test. Only used on Chrome desktop. 1336 """ 1337 self.assertTrue(self.AppendTab(pyauto.GURL(url)), 1338 msg='Failed to append tab for %s.' % description) 1339 1340 get_fps_js = """ 1341 var fps_field = document.getElementById("fps"); 1342 var result = -1; 1343 if (fps_field) 1344 result = fps_field.innerHTML; 1345 window.domAutomationController.send(JSON.stringify(result)); 1346 """ 1347 1348 # Wait until we start getting FPS values. 1349 self.assertTrue( 1350 self.WaitUntil( 1351 lambda: self.ExecuteJavascript(get_fps_js, tab_index=1) != '-1', 1352 timeout=300, retry_sleep=1), 1353 msg='Timed out when waiting for FPS values to be available.') 1354 1355 # Let the experiment run for 5 seconds before we start collecting perf 1356 # measurements. 1357 time.sleep(5) 1358 1359 # Collect the current FPS value each second for the next 30 seconds. The 1360 # final result of this test will be the average of these FPS values. 1361 fps_vals = [] 1362 for iteration in xrange(30): 1363 fps = self.ExecuteJavascript(get_fps_js, tab_index=1) 1364 fps = float(fps.replace('"', '')) 1365 fps_vals.append(fps) 1366 logging.info('Iteration %d of %d: %f FPS', iteration + 1, 30, fps) 1367 time.sleep(1) 1368 self._PrintSummaryResults(description, fps_vals, 'fps', graph_name) 1369 1370 def testWebGLAquarium(self): 1371 """Measures performance using the WebGL Aquarium demo.""" 1372 self._RunWebGLTest( 1373 self.GetFileURLForDataPath('pyauto_private', 'webgl', 'aquarium', 1374 'aquarium.html'), 1375 'WebGLAquarium', 'webgl_demo') 1376 1377 def testWebGLField(self): 1378 """Measures performance using the WebGL Field demo.""" 1379 self._RunWebGLTest( 1380 self.GetFileURLForDataPath('pyauto_private', 'webgl', 'field', 1381 'field.html'), 1382 'WebGLField', 'webgl_demo') 1383 1384 def testWebGLSpaceRocks(self): 1385 """Measures performance using the WebGL SpaceRocks demo.""" 1386 self._RunWebGLTest( 1387 self.GetFileURLForDataPath('pyauto_private', 'webgl', 'spacerocks', 1388 'spacerocks.html'), 1389 'WebGLSpaceRocks', 'webgl_demo') 1390 1391 1392 class GPUPerfTest(BasePerfTest): 1393 """Tests for GPU performance.""" 1394 1395 def setUp(self): 1396 """Performs necessary setup work before running each test in this class.""" 1397 self._gpu_info_dict = self.EvalDataFrom(os.path.join(self.DataDir(), 1398 'gpu', 'gpuperf.txt')) 1399 self._demo_name_url_dict = self._gpu_info_dict['demo_info'] 1400 pyauto.PyUITest.setUp(self) 1401 1402 def _MeasureFpsOverTime(self, tab_index=0): 1403 """Measures FPS using a specified demo. 1404 1405 This function assumes that the demo is already loaded in the specified tab 1406 index. 1407 1408 Args: 1409 tab_index: The tab index, default is 0. 1410 """ 1411 # Let the experiment run for 5 seconds before we start collecting FPS 1412 # values. 1413 time.sleep(5) 1414 1415 # Collect the current FPS value each second for the next 10 seconds. 1416 # Then return the average FPS value from among those collected. 1417 fps_vals = [] 1418 for iteration in xrange(10): 1419 fps = self.GetFPS(tab_index=tab_index) 1420 fps_vals.append(fps['fps']) 1421 time.sleep(1) 1422 return Mean(fps_vals) 1423 1424 def _GetStdAvgAndCompare(self, avg_fps, description, ref_dict): 1425 """Computes the average and compare set of values with reference data. 1426 1427 Args: 1428 avg_fps: Average fps value. 1429 description: A string description for this demo, used as a performance 1430 value description. 1431 ref_dict: Dictionary which contains reference data for this test case. 1432 1433 Returns: 1434 True, if the actual FPS value is within 10% of the reference FPS value, 1435 or False, otherwise. 1436 """ 1437 std_fps = 0 1438 status = True 1439 # Load reference data according to platform. 1440 platform_ref_dict = None 1441 if self.IsWin(): 1442 platform_ref_dict = ref_dict['win'] 1443 elif self.IsMac(): 1444 platform_ref_dict = ref_dict['mac'] 1445 elif self.IsLinux(): 1446 platform_ref_dict = ref_dict['linux'] 1447 else: 1448 self.assertFail(msg='This platform is unsupported.') 1449 std_fps = platform_ref_dict[description] 1450 # Compare reference data to average fps. 1451 # We allow the average FPS value to be within 10% of the reference 1452 # FPS value. 1453 if avg_fps < (0.9 * std_fps): 1454 logging.info('FPS difference exceeds threshold for: %s', description) 1455 logging.info(' Average: %f fps', avg_fps) 1456 logging.info('Reference Average: %f fps', std_fps) 1457 status = False 1458 else: 1459 logging.info('Average FPS is actually greater than 10 percent ' 1460 'more than the reference FPS for: %s', description) 1461 logging.info(' Average: %f fps', avg_fps) 1462 logging.info(' Reference Average: %f fps', std_fps) 1463 return status 1464 1465 def testLaunchDemosParallelInSeparateTabs(self): 1466 """Measures performance of demos in different tabs in same browser.""" 1467 # Launch all the demos parallel in separate tabs 1468 counter = 0 1469 all_demos_passed = True 1470 ref_dict = self._gpu_info_dict['separate_tab_ref_data'] 1471 # Iterate through dictionary and append all url to browser 1472 for url in self._demo_name_url_dict.iterkeys(): 1473 self.assertTrue( 1474 self.AppendTab(pyauto.GURL(self._demo_name_url_dict[url])), 1475 msg='Failed to append tab for %s.' % url) 1476 counter += 1 1477 # Assert number of tab count is equal to number of tabs appended. 1478 self.assertEqual(self.GetTabCount(), counter + 1) 1479 # Measures performance using different demos and compare it golden 1480 # reference. 1481 for url in self._demo_name_url_dict.iterkeys(): 1482 avg_fps = self._MeasureFpsOverTime(tab_index=counter) 1483 # Get the reference value of fps and compare the results 1484 if not self._GetStdAvgAndCompare(avg_fps, url, ref_dict): 1485 all_demos_passed = False 1486 counter -= 1 1487 self.assertTrue( 1488 all_demos_passed, 1489 msg='One or more demos failed to yield an acceptable FPS value') 1490 1491 def testLaunchDemosInSeparateBrowser(self): 1492 """Measures performance by launching each demo in a separate tab.""" 1493 # Launch demos in the browser 1494 ref_dict = self._gpu_info_dict['separate_browser_ref_data'] 1495 all_demos_passed = True 1496 for url in self._demo_name_url_dict.iterkeys(): 1497 self.NavigateToURL(self._demo_name_url_dict[url]) 1498 # Measures performance using different demos. 1499 avg_fps = self._MeasureFpsOverTime() 1500 self.RestartBrowser() 1501 # Get the standard value of fps and compare the rseults 1502 if not self._GetStdAvgAndCompare(avg_fps, url, ref_dict): 1503 all_demos_passed = False 1504 self.assertTrue( 1505 all_demos_passed, 1506 msg='One or more demos failed to yield an acceptable FPS value') 1507 1508 def testLaunchDemosBrowseForwardBackward(self): 1509 """Measures performance of various demos in browser going back and forth.""" 1510 ref_dict = self._gpu_info_dict['browse_back_forward_ref_data'] 1511 url_array = [] 1512 desc_array = [] 1513 all_demos_passed = True 1514 # Get URL/Description from dictionary and put in individual array 1515 for url in self._demo_name_url_dict.iterkeys(): 1516 url_array.append(self._demo_name_url_dict[url]) 1517 desc_array.append(url) 1518 for index in range(len(url_array) - 1): 1519 # Launch demo in the Browser 1520 if index == 0: 1521 self.NavigateToURL(url_array[index]) 1522 # Measures performance using the first demo. 1523 avg_fps = self._MeasureFpsOverTime() 1524 status1 = self._GetStdAvgAndCompare(avg_fps, desc_array[index], 1525 ref_dict) 1526 # Measures performance using the second demo. 1527 self.NavigateToURL(url_array[index + 1]) 1528 avg_fps = self._MeasureFpsOverTime() 1529 status2 = self._GetStdAvgAndCompare(avg_fps, desc_array[index + 1], 1530 ref_dict) 1531 # Go Back to previous demo 1532 self.TabGoBack() 1533 # Measures performance for first demo when moved back 1534 avg_fps = self._MeasureFpsOverTime() 1535 status3 = self._GetStdAvgAndCompare( 1536 avg_fps, desc_array[index] + '_backward', 1537 ref_dict) 1538 # Go Forward to previous demo 1539 self.TabGoForward() 1540 # Measures performance for second demo when moved forward 1541 avg_fps = self._MeasureFpsOverTime() 1542 status4 = self._GetStdAvgAndCompare( 1543 avg_fps, desc_array[index + 1] + '_forward', 1544 ref_dict) 1545 if not all([status1, status2, status3, status4]): 1546 all_demos_passed = False 1547 self.assertTrue( 1548 all_demos_passed, 1549 msg='One or more demos failed to yield an acceptable FPS value') 1550 1551 1552 class HTML5BenchmarkTest(BasePerfTest): 1553 """Tests for HTML5 performance.""" 1554 1555 def testHTML5Benchmark(self): 1556 """Measures performance using the benchmark at html5-benchmark.com.""" 1557 self.NavigateToURL('http://html5-benchmark.com') 1558 1559 start_benchmark_js = """ 1560 benchmark(); 1561 window.domAutomationController.send("done"); 1562 """ 1563 self.ExecuteJavascript(start_benchmark_js) 1564 1565 js_final_score = """ 1566 var score = "-1"; 1567 var elem = document.getElementById("score"); 1568 if (elem) 1569 score = elem.innerHTML; 1570 window.domAutomationController.send(score); 1571 """ 1572 # Wait for the benchmark to complete, which is assumed to be when the value 1573 # of the 'score' DOM element changes to something other than '87485'. 1574 self.assertTrue( 1575 self.WaitUntil( 1576 lambda: self.ExecuteJavascript(js_final_score) != '87485', 1577 timeout=900, retry_sleep=1), 1578 msg='Timed out when waiting for final score to be available.') 1579 1580 score = self.ExecuteJavascript(js_final_score) 1581 logging.info('HTML5 Benchmark final score: %f', float(score)) 1582 self._OutputPerfGraphValue('HTML5Benchmark', float(score), 'score', 1583 'html5_benchmark') 1584 1585 1586 class FileUploadDownloadTest(BasePerfTest): 1587 """Tests that involve measuring performance of upload and download.""" 1588 1589 def setUp(self): 1590 """Performs necessary setup work before running each test in this class.""" 1591 self._temp_dir = tempfile.mkdtemp() 1592 self._test_server = PerfTestServer(self._temp_dir) 1593 self._test_server_port = self._test_server.GetPort() 1594 self._test_server.Run() 1595 self.assertTrue(self.WaitUntil(self._IsTestServerRunning), 1596 msg='Failed to start local performance test server.') 1597 BasePerfTest.setUp(self) 1598 1599 def tearDown(self): 1600 """Performs necessary cleanup work after running each test in this class.""" 1601 BasePerfTest.tearDown(self) 1602 self._test_server.ShutDown() 1603 pyauto_utils.RemovePath(self._temp_dir) 1604 1605 def _IsTestServerRunning(self): 1606 """Determines whether the local test server is ready to accept connections. 1607 1608 Returns: 1609 True, if a connection can be made to the local performance test server, or 1610 False otherwise. 1611 """ 1612 conn = None 1613 try: 1614 conn = urllib2.urlopen('http://localhost:%d' % self._test_server_port) 1615 return True 1616 except IOError, e: 1617 return False 1618 finally: 1619 if conn: 1620 conn.close() 1621 1622 def testDownload100MBFile(self): 1623 """Measures the time to download a 100 MB file from a local server.""" 1624 CREATE_100MB_URL = ( 1625 'http://localhost:%d/create_file_of_size?filename=data&mb=100' % 1626 self._test_server_port) 1627 DOWNLOAD_100MB_URL = 'http://localhost:%d/data' % self._test_server_port 1628 DELETE_100MB_URL = ('http://localhost:%d/delete_file?filename=data' % 1629 self._test_server_port) 1630 1631 # Tell the local server to create a 100 MB file. 1632 self.NavigateToURL(CREATE_100MB_URL) 1633 1634 # Cleaning up downloaded files is done in the same way as in downloads.py. 1635 # We first identify all existing downloaded files, then remove only those 1636 # new downloaded files that appear during the course of this test. 1637 download_dir = self.GetDownloadDirectory().value() 1638 orig_downloads = [] 1639 if os.path.isdir(download_dir): 1640 orig_downloads = os.listdir(download_dir) 1641 1642 def _CleanupAdditionalFilesInDir(directory, orig_files): 1643 """Removes the additional files in the specified directory. 1644 1645 This function will remove all files from |directory| that are not 1646 specified in |orig_files|. 1647 1648 Args: 1649 directory: A string directory path. 1650 orig_files: A list of strings representing the original set of files in 1651 the specified directory. 1652 """ 1653 downloads_to_remove = [] 1654 if os.path.isdir(directory): 1655 downloads_to_remove = [os.path.join(directory, name) 1656 for name in os.listdir(directory) 1657 if name not in orig_files] 1658 for file_name in downloads_to_remove: 1659 pyauto_utils.RemovePath(file_name) 1660 1661 def _DownloadFile(url): 1662 self.DownloadAndWaitForStart(url) 1663 self.WaitForAllDownloadsToComplete(timeout=2 * 60 * 1000) # 2 minutes. 1664 1665 timings = [] 1666 for iteration in range(self._num_iterations + 1): 1667 elapsed_time = self._MeasureElapsedTime( 1668 lambda: _DownloadFile(DOWNLOAD_100MB_URL), num_invocations=1) 1669 # Ignore the first iteration. 1670 if iteration: 1671 timings.append(elapsed_time) 1672 logging.info('Iteration %d of %d: %f milliseconds', iteration, 1673 self._num_iterations, elapsed_time) 1674 self.SetDownloadShelfVisible(False) 1675 _CleanupAdditionalFilesInDir(download_dir, orig_downloads) 1676 1677 self._PrintSummaryResults('Download100MBFile', timings, 'milliseconds', 1678 'download_file') 1679 1680 # Tell the local server to delete the 100 MB file. 1681 self.NavigateToURL(DELETE_100MB_URL) 1682 1683 def testUpload50MBFile(self): 1684 """Measures the time to upload a 50 MB file to a local server.""" 1685 # TODO(dennisjeffrey): Replace the use of XMLHttpRequest in this test with 1686 # FileManager automation to select the upload file when crosbug.com/17903 1687 # is complete. 1688 START_UPLOAD_URL = ( 1689 'http://localhost:%d/start_upload?mb=50' % self._test_server_port) 1690 1691 EXPECTED_SUBSTRING = 'Upload complete' 1692 1693 def _IsUploadComplete(): 1694 js = """ 1695 result = ""; 1696 var div = document.getElementById("upload_result"); 1697 if (div) 1698 result = div.innerHTML; 1699 window.domAutomationController.send(result); 1700 """ 1701 return self.ExecuteJavascript(js).find(EXPECTED_SUBSTRING) >= 0 1702 1703 def _RunSingleUpload(): 1704 self.NavigateToURL(START_UPLOAD_URL) 1705 self.assertTrue( 1706 self.WaitUntil(_IsUploadComplete, timeout=120, expect_retval=True, 1707 retry_sleep=0.10), 1708 msg='Upload failed to complete before the timeout was hit.') 1709 1710 timings = [] 1711 for iteration in range(self._num_iterations + 1): 1712 elapsed_time = self._MeasureElapsedTime(_RunSingleUpload) 1713 # Ignore the first iteration. 1714 if iteration: 1715 timings.append(elapsed_time) 1716 logging.info('Iteration %d of %d: %f milliseconds', iteration, 1717 self._num_iterations, elapsed_time) 1718 1719 self._PrintSummaryResults('Upload50MBFile', timings, 'milliseconds', 1720 'upload_file') 1721 1722 1723 class ScrollResults(object): 1724 """Container for ScrollTest results.""" 1725 1726 def __init__(self, first_paint_seconds, results_list): 1727 assert len(results_list) == 2, 'Expecting initial and repeat results.' 1728 self._first_paint_time = 1000.0 * first_paint_seconds 1729 self._results_list = results_list 1730 1731 def GetFirstPaintTime(self): 1732 return self._first_paint_time 1733 1734 def GetFrameCount(self, index): 1735 results = self._results_list[index] 1736 return results.get('numFramesSentToScreen', results['numAnimationFrames']) 1737 1738 def GetFps(self, index): 1739 return (self.GetFrameCount(index) / 1740 self._results_list[index]['totalTimeInSeconds']) 1741 1742 def GetMeanFrameTime(self, index): 1743 return (self._results_list[index]['totalTimeInSeconds'] / 1744 self.GetFrameCount(index)) 1745 1746 def GetPercentBelow60Fps(self, index): 1747 return (float(self._results_list[index]['droppedFrameCount']) / 1748 self.GetFrameCount(index)) 1749 1750 1751 class BaseScrollTest(BasePerfTest): 1752 """Base class for tests measuring scrolling performance.""" 1753 1754 def setUp(self): 1755 """Performs necessary setup work before running each test.""" 1756 super(BaseScrollTest, self).setUp() 1757 scroll_file = os.path.join(self.DataDir(), 'scroll', 'scroll.js') 1758 with open(scroll_file) as f: 1759 self._scroll_text = f.read() 1760 1761 def ExtraChromeFlags(self): 1762 """Ensures Chrome is launched with custom flags. 1763 1764 Returns: 1765 A list of extra flags to pass to Chrome when it is launched. 1766 """ 1767 # Extra flag used by scroll performance tests. 1768 return (super(BaseScrollTest, self).ExtraChromeFlags() + 1769 ['--enable-gpu-benchmarking']) 1770 1771 def RunSingleInvocation(self, url, is_gmail_test=False): 1772 """Runs a single invocation of the scroll test. 1773 1774 Args: 1775 url: The string url for the webpage on which to run the scroll test. 1776 is_gmail_test: True iff the test is a GMail test. 1777 1778 Returns: 1779 Instance of ScrollResults. 1780 """ 1781 1782 self.assertTrue(self.AppendTab(pyauto.GURL(url)), 1783 msg='Failed to append tab for webpage.') 1784 1785 timeout = pyauto.PyUITest.ActionTimeoutChanger(self, 300 * 1000) # ms 1786 test_js = """%s; 1787 new __ScrollTest(function(results) { 1788 var stringify = JSON.stringify || JSON.encode; 1789 window.domAutomationController.send(stringify(results)); 1790 }, %s); 1791 """ % (self._scroll_text, 'true' if is_gmail_test else 'false') 1792 results = simplejson.loads(self.ExecuteJavascript(test_js, tab_index=1)) 1793 1794 first_paint_js = ('window.domAutomationController.send(' 1795 '(chrome.loadTimes().firstPaintTime - ' 1796 'chrome.loadTimes().requestTime).toString());') 1797 first_paint_time = float(self.ExecuteJavascript(first_paint_js, 1798 tab_index=1)) 1799 1800 self.CloseTab(tab_index=1) 1801 1802 return ScrollResults(first_paint_time, results) 1803 1804 def RunScrollTest(self, url, description, graph_name, is_gmail_test=False): 1805 """Runs a scroll performance test on the specified webpage. 1806 1807 Args: 1808 url: The string url for the webpage on which to run the scroll test. 1809 description: A string description for the particular test being run. 1810 graph_name: A string name for the performance graph associated with this 1811 test. Only used on Chrome desktop. 1812 is_gmail_test: True iff the test is a GMail test. 1813 """ 1814 results = [] 1815 for iteration in range(self._num_iterations + 1): 1816 result = self.RunSingleInvocation(url, is_gmail_test) 1817 # Ignore the first iteration. 1818 if iteration: 1819 fps = result.GetFps(1) 1820 assert fps, '%s did not scroll' % url 1821 logging.info('Iteration %d of %d: %f fps', iteration, 1822 self._num_iterations, fps) 1823 results.append(result) 1824 self._PrintSummaryResults( 1825 description, [r.GetFps(1) for r in results], 1826 'FPS', graph_name) 1827 1828 1829 class PopularSitesScrollTest(BaseScrollTest): 1830 """Measures scrolling performance on recorded versions of popular sites.""" 1831 1832 def ExtraChromeFlags(self): 1833 """Ensures Chrome is launched with custom flags. 1834 1835 Returns: 1836 A list of extra flags to pass to Chrome when it is launched. 1837 """ 1838 return super(PopularSitesScrollTest, 1839 self).ExtraChromeFlags() + PageCyclerReplay.CHROME_FLAGS 1840 1841 def _GetUrlList(self, test_name): 1842 """Returns list of recorded sites.""" 1843 sites_path = PageCyclerReplay.Path('page_sets', test_name=test_name) 1844 with open(sites_path) as f: 1845 sites_text = f.read() 1846 js = """ 1847 %s 1848 window.domAutomationController.send(JSON.stringify(pageSets)); 1849 """ % sites_text 1850 page_sets = eval(self.ExecuteJavascript(js)) 1851 return list(itertools.chain(*page_sets))[1:] # Skip first. 1852 1853 def _PrintScrollResults(self, results): 1854 self._PrintSummaryResults( 1855 'initial', [r.GetMeanFrameTime(0) for r in results], 1856 'ms', 'FrameTimes') 1857 self._PrintSummaryResults( 1858 'repeat', [r.GetMeanFrameTime(1) for r in results], 1859 'ms', 'FrameTimes') 1860 self._PrintSummaryResults( 1861 'initial', 1862 [r.GetPercentBelow60Fps(0) for r in results], 1863 'percent', 'PercentBelow60FPS') 1864 self._PrintSummaryResults( 1865 'repeat', 1866 [r.GetPercentBelow60Fps(1) for r in results], 1867 'percent', 'PercentBelow60FPS') 1868 self._PrintSummaryResults( 1869 'first_paint_time', [r.GetFirstPaintTime() for r in results], 1870 'ms', 'FirstPaintTime') 1871 1872 def test2012Q3(self): 1873 test_name = '2012Q3' 1874 urls = self._GetUrlList(test_name) 1875 results = [] 1876 with PageCyclerReplay.ReplayServer(test_name) as replay_server: 1877 if replay_server.is_record_mode: 1878 self._num_iterations = 1 1879 for iteration in range(self._num_iterations): 1880 for url in urls: 1881 result = self.RunSingleInvocation(url) 1882 fps = result.GetFps(0) 1883 assert fps, '%s did not scroll' % url 1884 logging.info('Iteration %d of %d: %f fps', iteration + 1, 1885 self._num_iterations, fps) 1886 results.append(result) 1887 self._PrintScrollResults(results) 1888 1889 1890 class ScrollTest(BaseScrollTest): 1891 """Tests to measure scrolling performance.""" 1892 1893 def ExtraChromeFlags(self): 1894 """Ensures Chrome is launched with custom flags. 1895 1896 Returns: 1897 A list of extra flags to pass to Chrome when it is launched. 1898 """ 1899 # Extra flag needed by scroll performance tests. 1900 return super(ScrollTest, self).ExtraChromeFlags() + ['--disable-gpu-vsync'] 1901 1902 def testBlankPageScroll(self): 1903 """Runs the scroll test on a blank page.""" 1904 self.RunScrollTest( 1905 self.GetFileURLForDataPath('scroll', 'blank.html'), 'ScrollBlankPage', 1906 'scroll_fps') 1907 1908 def testTextScroll(self): 1909 """Runs the scroll test on a text-filled page.""" 1910 self.RunScrollTest( 1911 self.GetFileURLForDataPath('scroll', 'text.html'), 'ScrollTextPage', 1912 'scroll_fps') 1913 1914 def testGooglePlusScroll(self): 1915 """Runs the scroll test on a Google Plus anonymized page.""" 1916 self.RunScrollTest( 1917 self.GetFileURLForDataPath('scroll', 'plus.html'), 1918 'ScrollGooglePlusPage', 'scroll_fps') 1919 1920 def testGmailScroll(self): 1921 """Runs the scroll test using the live Gmail site.""" 1922 self._LoginToGoogleAccount(account_key='test_google_account_gmail') 1923 self.RunScrollTest('http://www.gmail.com', 'ScrollGmail', 1924 'scroll_fps', True) 1925 1926 1927 class FlashTest(BasePerfTest): 1928 """Tests to measure flash performance.""" 1929 1930 def _RunFlashTestForAverageFPS(self, webpage_url, description, graph_name): 1931 """Runs a single flash test that measures an average FPS value. 1932 1933 Args: 1934 webpage_url: The string URL to a webpage that will run the test. 1935 description: A string description for this test. 1936 graph_name: A string name for the performance graph associated with this 1937 test. Only used on Chrome desktop. 1938 """ 1939 # Open up the test webpage; it's assumed the test will start automatically. 1940 self.assertTrue(self.AppendTab(pyauto.GURL(webpage_url)), 1941 msg='Failed to append tab for webpage.') 1942 1943 # Wait until the final result is computed, then retrieve and output it. 1944 js = """ 1945 window.domAutomationController.send( 1946 JSON.stringify(final_average_fps)); 1947 """ 1948 self.assertTrue( 1949 self.WaitUntil( 1950 lambda: self.ExecuteJavascript(js, tab_index=1) != '-1', 1951 timeout=300, expect_retval=True, retry_sleep=1), 1952 msg='Timed out when waiting for test result.') 1953 result = float(self.ExecuteJavascript(js, tab_index=1)) 1954 logging.info('Result for %s: %f FPS (average)', description, result) 1955 self._OutputPerfGraphValue(description, result, 'FPS', graph_name) 1956 1957 def testFlashGaming(self): 1958 """Runs a simple flash gaming benchmark test.""" 1959 webpage_url = self.GetHttpURLForDataPath('pyauto_private', 'flash', 1960 'FlashGamingTest2.html') 1961 self._RunFlashTestForAverageFPS(webpage_url, 'FlashGaming', 'flash_fps') 1962 1963 def testFlashText(self): 1964 """Runs a simple flash text benchmark test.""" 1965 webpage_url = self.GetHttpURLForDataPath('pyauto_private', 'flash', 1966 'FlashTextTest2.html') 1967 self._RunFlashTestForAverageFPS(webpage_url, 'FlashText', 'flash_fps') 1968 1969 def testScimarkGui(self): 1970 """Runs the ScimarkGui benchmark tests.""" 1971 webpage_url = self.GetHttpURLForDataPath('pyauto_private', 'flash', 1972 'scimarkGui.html') 1973 self.assertTrue(self.AppendTab(pyauto.GURL(webpage_url)), 1974 msg='Failed to append tab for webpage.') 1975 1976 js = 'window.domAutomationController.send(JSON.stringify(tests_done));' 1977 self.assertTrue( 1978 self.WaitUntil( 1979 lambda: self.ExecuteJavascript(js, tab_index=1), timeout=300, 1980 expect_retval='true', retry_sleep=1), 1981 msg='Timed out when waiting for tests to complete.') 1982 1983 js_result = """ 1984 var result = {}; 1985 for (var i = 0; i < tests_results.length; ++i) { 1986 var test_name = tests_results[i][0]; 1987 var mflops = tests_results[i][1]; 1988 var mem = tests_results[i][2]; 1989 result[test_name] = [mflops, mem] 1990 } 1991 window.domAutomationController.send(JSON.stringify(result)); 1992 """ 1993 result = eval(self.ExecuteJavascript(js_result, tab_index=1)) 1994 for benchmark in result: 1995 mflops = float(result[benchmark][0]) 1996 mem = float(result[benchmark][1]) 1997 if benchmark.endswith('_mflops'): 1998 benchmark = benchmark[:benchmark.find('_mflops')] 1999 logging.info('Results for ScimarkGui_%s:', benchmark) 2000 logging.info(' %f MFLOPS', mflops) 2001 logging.info(' %f MB', mem) 2002 self._OutputPerfGraphValue('ScimarkGui-%s-MFLOPS' % benchmark, mflops, 2003 'MFLOPS', 'scimark_gui_mflops') 2004 self._OutputPerfGraphValue('ScimarkGui-%s-Mem' % benchmark, mem, 'MB', 2005 'scimark_gui_mem') 2006 2007 2008 class LiveGamePerfTest(BasePerfTest): 2009 """Tests to measure performance of live gaming webapps.""" 2010 2011 def _RunLiveGamePerfTest(self, url, url_title_substring, 2012 description, graph_name): 2013 """Measures performance metrics for the specified live gaming webapp. 2014 2015 This function connects to the specified URL to launch the gaming webapp, 2016 waits for a period of time for the webapp to run, then collects some 2017 performance metrics about the running webapp. 2018 2019 Args: 2020 url: The string URL of the gaming webapp to analyze. 2021 url_title_substring: A string that is expected to be a substring of the 2022 webpage title for the specified gaming webapp. Used to verify that 2023 the webapp loads correctly. 2024 description: A string description for this game, used in the performance 2025 value description. Should not contain any spaces. 2026 graph_name: A string name for the performance graph associated with this 2027 test. Only used on Chrome desktop. 2028 """ 2029 self.NavigateToURL(url) 2030 loaded_tab_title = self.GetActiveTabTitle() 2031 self.assertTrue(url_title_substring in loaded_tab_title, 2032 msg='Loaded tab title missing "%s": "%s"' % 2033 (url_title_substring, loaded_tab_title)) 2034 cpu_usage_start = self._GetCPUUsage() 2035 2036 # Let the app run for 1 minute. 2037 time.sleep(60) 2038 2039 cpu_usage_end = self._GetCPUUsage() 2040 fraction_non_idle_time = self._GetFractionNonIdleCPUTime( 2041 cpu_usage_start, cpu_usage_end) 2042 2043 logging.info('Fraction of CPU time spent non-idle: %f', 2044 fraction_non_idle_time) 2045 self._OutputPerfGraphValue(description + 'CpuBusy', fraction_non_idle_time, 2046 'Fraction', graph_name + '_cpu_busy') 2047 v8_heap_stats = self.GetV8HeapStats() 2048 v8_heap_size = v8_heap_stats['v8_memory_used'] / (1024.0 * 1024.0) 2049 logging.info('Total v8 heap size: %f MB', v8_heap_size) 2050 self._OutputPerfGraphValue(description + 'V8HeapSize', v8_heap_size, 'MB', 2051 graph_name + '_v8_heap_size') 2052 2053 def testAngryBirds(self): 2054 """Measures performance for Angry Birds.""" 2055 self._RunLiveGamePerfTest('http://chrome.angrybirds.com', 'Angry Birds', 2056 'AngryBirds', 'angry_birds') 2057 2058 2059 class BasePageCyclerTest(BasePerfTest): 2060 """Page class for page cycler tests. 2061 2062 Derived classes must implement StartUrl(). 2063 2064 Environment Variables: 2065 PC_NO_AUTO: if set, avoids automatically loading pages. 2066 """ 2067 MAX_ITERATION_SECONDS = 60 2068 TRIM_PERCENT = 20 2069 DEFAULT_USE_AUTO = True 2070 2071 # Page Cycler lives in src/data/page_cycler rather than src/chrome/test/data 2072 DATA_PATH = os.path.abspath( 2073 os.path.join(BasePerfTest.DataDir(), os.pardir, os.pardir, 2074 os.pardir, 'data', 'page_cycler')) 2075 2076 def setUp(self): 2077 """Performs necessary setup work before running each test.""" 2078 super(BasePageCyclerTest, self).setUp() 2079 self.use_auto = 'PC_NO_AUTO' not in os.environ 2080 2081 @classmethod 2082 def DataPath(cls, subdir): 2083 return os.path.join(cls.DATA_PATH, subdir) 2084 2085 def ExtraChromeFlags(self): 2086 """Ensures Chrome is launched with custom flags. 2087 2088 Returns: 2089 A list of extra flags to pass to Chrome when it is launched. 2090 """ 2091 # Extra flags required to run these tests. 2092 # The first two are needed for the test. 2093 # The plugins argument is to prevent bad scores due to pop-ups from 2094 # running an old version of something (like Flash). 2095 return (super(BasePageCyclerTest, self).ExtraChromeFlags() + 2096 ['--js-flags="--expose_gc"', 2097 '--enable-file-cookies', 2098 '--allow-outdated-plugins']) 2099 2100 def WaitUntilStarted(self, start_url): 2101 """Check that the test navigates away from the start_url.""" 2102 js_is_started = """ 2103 var is_started = document.location.href !== "%s"; 2104 window.domAutomationController.send(JSON.stringify(is_started)); 2105 """ % start_url 2106 self.assertTrue( 2107 self.WaitUntil(lambda: self.ExecuteJavascript(js_is_started) == 'true', 2108 timeout=10), 2109 msg='Timed out when waiting to leave start page.') 2110 2111 def WaitUntilDone(self, url, iterations): 2112 """Check cookies for "__pc_done=1" to know the test is over.""" 2113 def IsDone(): 2114 cookies = self.GetCookie(pyauto.GURL(url)) # window 0, tab 0 2115 return '__pc_done=1' in cookies 2116 self.assertTrue( 2117 self.WaitUntil( 2118 IsDone, 2119 timeout=(self.MAX_ITERATION_SECONDS * iterations), 2120 retry_sleep=1), 2121 msg='Timed out waiting for page cycler test to complete.') 2122 2123 def CollectPagesAndTimes(self, url): 2124 """Collect the results from the cookies.""" 2125 pages, times = None, None 2126 cookies = self.GetCookie(pyauto.GURL(url)) # window 0, tab 0 2127 for cookie in cookies.split(';'): 2128 if '__pc_pages' in cookie: 2129 pages_str = cookie.split('=', 1)[1] 2130 pages = pages_str.split(',') 2131 elif '__pc_timings' in cookie: 2132 times_str = cookie.split('=', 1)[1] 2133 times = [float(t) for t in times_str.split(',')] 2134 self.assertTrue(pages and times, 2135 msg='Unable to find test results in cookies: %s' % cookies) 2136 return pages, times 2137 2138 def IteratePageTimes(self, pages, times, iterations): 2139 """Regroup the times by the page. 2140 2141 Args: 2142 pages: the list of pages 2143 times: e.g. [page1_iter1, page2_iter1, ..., page1_iter2, page2_iter2, ...] 2144 iterations: the number of times for each page 2145 Yields: 2146 (pageN, [pageN_iter1, pageN_iter2, ...]) 2147 """ 2148 num_pages = len(pages) 2149 num_times = len(times) 2150 expected_num_times = num_pages * iterations 2151 self.assertEqual( 2152 expected_num_times, num_times, 2153 msg=('num_times != num_pages * iterations: %s != %s * %s, times=%s' % 2154 (num_times, num_pages, iterations, times))) 2155 for i, page in enumerate(pages): 2156 yield page, list(itertools.islice(times, i, None, num_pages)) 2157 2158 def CheckPageTimes(self, pages, times, iterations): 2159 """Assert that all the times are greater than zero.""" 2160 failed_pages = [] 2161 for page, times in self.IteratePageTimes(pages, times, iterations): 2162 failed_times = [t for t in times if t <= 0.0] 2163 if failed_times: 2164 failed_pages.append((page, failed_times)) 2165 if failed_pages: 2166 self.fail('Pages with unexpected times: %s' % failed_pages) 2167 2168 def TrimTimes(self, times, percent): 2169 """Return a new list with |percent| number of times trimmed for each page. 2170 2171 Removes the largest and smallest values. 2172 """ 2173 iterations = len(times) 2174 times = sorted(times) 2175 num_to_trim = int(iterations * float(percent) / 100.0) 2176 logging.debug('Before trimming %d: %s' % (num_to_trim, times)) 2177 a = num_to_trim / 2 2178 b = iterations - (num_to_trim / 2 + num_to_trim % 2) 2179 trimmed_times = times[a:b] 2180 logging.debug('After trimming: %s', trimmed_times) 2181 return trimmed_times 2182 2183 def ComputeFinalResult(self, pages, times, iterations): 2184 """The final score that is calculated is a geometric mean of the 2185 arithmetic means of each page's load time, and we drop the 2186 upper/lower 20% of the times for each page so they don't skew the 2187 mean. The geometric mean is used for the final score because the 2188 time range for any given site may be very different, and we don't 2189 want slower sites to weight more heavily than others. 2190 """ 2191 self.CheckPageTimes(pages, times, iterations) 2192 page_means = [ 2193 Mean(self.TrimTimes(times, percent=self.TRIM_PERCENT)) 2194 for _, times in self.IteratePageTimes(pages, times, iterations)] 2195 return GeometricMean(page_means) 2196 2197 def StartUrl(self, test_name, iterations): 2198 """Return the URL to used to start the test. 2199 2200 Derived classes must implement this. 2201 """ 2202 raise NotImplemented 2203 2204 def RunPageCyclerTest(self, name, description): 2205 """Runs the specified PageCycler test. 2206 2207 Args: 2208 name: the page cycler test name (corresponds to a directory or test file) 2209 description: a string description for the test 2210 """ 2211 iterations = self._num_iterations 2212 start_url = self.StartUrl(name, iterations) 2213 self.NavigateToURL(start_url) 2214 if self.use_auto: 2215 self.WaitUntilStarted(start_url) 2216 self.WaitUntilDone(start_url, iterations) 2217 pages, times = self.CollectPagesAndTimes(start_url) 2218 final_result = self.ComputeFinalResult(pages, times, iterations) 2219 logging.info('%s page cycler final result: %f' % 2220 (description, final_result)) 2221 self._OutputPerfGraphValue(description + '_PageCycler', final_result, 2222 'milliseconds', graph_name='PageCycler') 2223 2224 2225 class PageCyclerTest(BasePageCyclerTest): 2226 """Tests to run various page cyclers. 2227 2228 Environment Variables: 2229 PC_NO_AUTO: if set, avoids automatically loading pages. 2230 """ 2231 2232 def _PreReadDataDir(self, subdir): 2233 """This recursively reads all of the files in a given url directory. 2234 2235 The intent is to get them into memory before they are used by the benchmark. 2236 2237 Args: 2238 subdir: a subdirectory of the page cycler data directory. 2239 """ 2240 def _PreReadDir(dirname, names): 2241 for rfile in names: 2242 with open(os.path.join(dirname, rfile)) as fp: 2243 fp.read() 2244 for root, dirs, files in os.walk(self.DataPath(subdir)): 2245 _PreReadDir(root, files) 2246 2247 def StartUrl(self, test_name, iterations): 2248 # Must invoke GetFileURLForPath before appending parameters to the URL, 2249 # otherwise those parameters will get quoted. 2250 start_url = self.GetFileURLForPath(self.DataPath(test_name), 'start.html') 2251 start_url += '?iterations=%d' % iterations 2252 if self.use_auto: 2253 start_url += '&auto=1' 2254 return start_url 2255 2256 def RunPageCyclerTest(self, dirname, description): 2257 """Runs the specified PageCycler test. 2258 2259 Args: 2260 dirname: directory containing the page cycler test 2261 description: a string description for the test 2262 """ 2263 self._PreReadDataDir('common') 2264 self._PreReadDataDir(dirname) 2265 super(PageCyclerTest, self).RunPageCyclerTest(dirname, description) 2266 2267 def testMoreJSFile(self): 2268 self.RunPageCyclerTest('morejs', 'MoreJSFile') 2269 2270 def testAlexaFile(self): 2271 self.RunPageCyclerTest('alexa_us', 'Alexa_usFile') 2272 2273 def testBloatFile(self): 2274 self.RunPageCyclerTest('bloat', 'BloatFile') 2275 2276 def testDHTMLFile(self): 2277 self.RunPageCyclerTest('dhtml', 'DhtmlFile') 2278 2279 def testIntl1File(self): 2280 self.RunPageCyclerTest('intl1', 'Intl1File') 2281 2282 def testIntl2File(self): 2283 self.RunPageCyclerTest('intl2', 'Intl2File') 2284 2285 def testMozFile(self): 2286 self.RunPageCyclerTest('moz', 'MozFile') 2287 2288 def testMoz2File(self): 2289 self.RunPageCyclerTest('moz2', 'Moz2File') 2290 2291 2292 class PageCyclerReplay(object): 2293 """Run page cycler tests with network simulation via Web Page Replay. 2294 2295 Web Page Replay is a proxy that can record and "replay" web pages with 2296 simulated network characteristics -- without having to edit the pages 2297 by hand. With WPR, tests can use "real" web content, and catch 2298 performance issues that may result from introducing network delays and 2299 bandwidth throttling. 2300 """ 2301 _PATHS = { 2302 'archive': 'src/data/page_cycler/webpagereplay/{test_name}.wpr', 2303 'page_sets': 'src/tools/page_cycler/webpagereplay/tests/{test_name}.js', 2304 'start_page': 'src/tools/page_cycler/webpagereplay/start.html', 2305 'extension': 'src/tools/page_cycler/webpagereplay/extension', 2306 } 2307 2308 WEBPAGEREPLAY_HOST = '127.0.0.1' 2309 WEBPAGEREPLAY_HTTP_PORT = 8080 2310 WEBPAGEREPLAY_HTTPS_PORT = 8413 2311 2312 CHROME_FLAGS = webpagereplay.GetChromeFlags( 2313 WEBPAGEREPLAY_HOST, 2314 WEBPAGEREPLAY_HTTP_PORT, 2315 WEBPAGEREPLAY_HTTPS_PORT) + [ 2316 '--log-level=0', 2317 '--disable-background-networking', 2318 '--enable-experimental-extension-apis', 2319 '--enable-logging', 2320 '--enable-benchmarking', 2321 '--enable-net-benchmarking', 2322 '--metrics-recording-only', 2323 '--activate-on-launch', 2324 '--no-first-run', 2325 '--no-proxy-server', 2326 ] 2327 2328 @classmethod 2329 def Path(cls, key, **kwargs): 2330 return FormatChromePath(cls._PATHS[key], **kwargs) 2331 2332 @classmethod 2333 def ReplayServer(cls, test_name, replay_options=None): 2334 archive_path = cls.Path('archive', test_name=test_name) 2335 return webpagereplay.ReplayServer(archive_path, 2336 cls.WEBPAGEREPLAY_HOST, 2337 cls.WEBPAGEREPLAY_HTTP_PORT, 2338 cls.WEBPAGEREPLAY_HTTPS_PORT, 2339 replay_options) 2340 2341 2342 class PageCyclerNetSimTest(BasePageCyclerTest): 2343 """Tests to run Web Page Replay backed page cycler tests.""" 2344 MAX_ITERATION_SECONDS = 180 2345 2346 def ExtraChromeFlags(self): 2347 """Ensures Chrome is launched with custom flags. 2348 2349 Returns: 2350 A list of extra flags to pass to Chrome when it is launched. 2351 """ 2352 flags = super(PageCyclerNetSimTest, self).ExtraChromeFlags() 2353 flags.append('--load-extension=%s' % PageCyclerReplay.Path('extension')) 2354 flags.extend(PageCyclerReplay.CHROME_FLAGS) 2355 return flags 2356 2357 def StartUrl(self, test_name, iterations): 2358 start_path = PageCyclerReplay.Path('start_page') 2359 start_url = 'file://%s?test=%s&iterations=%d' % ( 2360 start_path, test_name, iterations) 2361 if self.use_auto: 2362 start_url += '&auto=1' 2363 return start_url 2364 2365 def RunPageCyclerTest(self, test_name, description): 2366 """Runs the specified PageCycler test. 2367 2368 Args: 2369 test_name: name for archive (.wpr) and config (.js) files. 2370 description: a string description for the test 2371 """ 2372 replay_options = None 2373 with PageCyclerReplay.ReplayServer(test_name, replay_options) as server: 2374 if server.is_record_mode: 2375 self._num_iterations = 1 2376 super_self = super(PageCyclerNetSimTest, self) 2377 super_self.RunPageCyclerTest(test_name, description) 2378 2379 def test2012Q2(self): 2380 self.RunPageCyclerTest('2012Q2', '2012Q2') 2381 2382 2383 class MemoryTest(BasePerfTest): 2384 """Tests to measure memory consumption under different usage scenarios.""" 2385 2386 def ExtraChromeFlags(self): 2387 """Launches Chrome with custom flags. 2388 2389 Returns: 2390 A list of extra flags to pass to Chrome when it is launched. 2391 """ 2392 # Ensure Chrome assigns one renderer process to each tab. 2393 return super(MemoryTest, self).ExtraChromeFlags() + ['--process-per-tab'] 2394 2395 def _RecordMemoryStats(self, description, when, duration): 2396 """Outputs memory statistics to be graphed. 2397 2398 Args: 2399 description: A string description for the test. Should not contain 2400 spaces. For example, 'MemCtrl'. 2401 when: A string description of when the memory stats are being recorded 2402 during test execution (since memory stats may be recorded multiple 2403 times during a test execution at certain "interesting" times). Should 2404 not contain spaces. 2405 duration: The number of seconds to sample data before outputting the 2406 memory statistics. 2407 """ 2408 mem = self.GetMemoryStatsChromeOS(duration) 2409 measurement_types = [ 2410 ('gem_obj', 'GemObj'), 2411 ('gtt', 'GTT'), 2412 ('mem_free', 'MemFree'), 2413 ('mem_available', 'MemAvail'), 2414 ('mem_shared', 'MemShare'), 2415 ('mem_cached', 'MemCache'), 2416 ('mem_anon', 'MemAnon'), 2417 ('mem_file', 'MemFile'), 2418 ('mem_slab', 'MemSlab'), 2419 ('browser_priv', 'BrowPriv'), 2420 ('browser_shared', 'BrowShar'), 2421 ('gpu_priv', 'GpuPriv'), 2422 ('gpu_shared', 'GpuShar'), 2423 ('renderer_priv', 'RendPriv'), 2424 ('renderer_shared', 'RendShar'), 2425 ] 2426 for type_key, type_string in measurement_types: 2427 if type_key not in mem: 2428 continue 2429 self._OutputPerfGraphValue( 2430 '%s-Min%s-%s' % (description, type_string, when), 2431 mem[type_key]['min'], 'KB', '%s-%s' % (description, type_string)) 2432 self._OutputPerfGraphValue( 2433 '%s-Max%s-%s' % (description, type_string, when), 2434 mem[type_key]['max'], 'KB', '%s-%s' % (description, type_string)) 2435 self._OutputPerfGraphValue( 2436 '%s-End%s-%s' % (description, type_string, when), 2437 mem[type_key]['end'], 'KB', '%s-%s' % (description, type_string)) 2438 2439 def _RunTest(self, tabs, description, duration): 2440 """Runs a general memory test. 2441 2442 Args: 2443 tabs: A list of strings representing the URLs of the websites to open 2444 during this test. 2445 description: A string description for the test. Should not contain 2446 spaces. For example, 'MemCtrl'. 2447 duration: The number of seconds to sample data before outputting memory 2448 statistics. 2449 """ 2450 self._RecordMemoryStats(description, '0Tabs0', duration) 2451 2452 for iteration_num in xrange(2): 2453 for site in tabs: 2454 self.AppendTab(pyauto.GURL(site)) 2455 2456 self._RecordMemoryStats(description, 2457 '%dTabs%d' % (len(tabs), iteration_num + 1), 2458 duration) 2459 2460 for _ in xrange(len(tabs)): 2461 self.CloseTab(tab_index=1) 2462 2463 self._RecordMemoryStats(description, '0Tabs%d' % (iteration_num + 1), 2464 duration) 2465 2466 def testOpenCloseTabsControl(self): 2467 """Measures memory usage when opening/closing tabs to about:blank.""" 2468 tabs = ['about:blank'] * 10 2469 self._RunTest(tabs, 'MemCtrl', 15) 2470 2471 def testOpenCloseTabsLiveSites(self): 2472 """Measures memory usage when opening/closing tabs to live sites.""" 2473 tabs = [ 2474 'http://www.google.com/gmail', 2475 'http://www.google.com/calendar', 2476 'http://www.google.com/plus', 2477 'http://www.google.com/youtube', 2478 'http://www.nytimes.com', 2479 'http://www.cnn.com', 2480 'http://www.facebook.com/zuck', 2481 'http://www.techcrunch.com', 2482 'http://www.theverge.com', 2483 'http://www.yahoo.com', 2484 ] 2485 # Log in to a test Google account to make connections to the above Google 2486 # websites more interesting. 2487 self._LoginToGoogleAccount() 2488 self._RunTest(tabs, 'MemLive', 20) 2489 2490 2491 class PerfTestServerRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 2492 """Request handler for the local performance test server.""" 2493 2494 def _IgnoreHandler(self, unused_args): 2495 """A GET request handler that simply replies with status code 200. 2496 2497 Args: 2498 unused_args: A dictionary of arguments for the current GET request. 2499 The arguments are ignored. 2500 """ 2501 self.send_response(200) 2502 self.end_headers() 2503 2504 def _CreateFileOfSizeHandler(self, args): 2505 """A GET handler that creates a local file with the specified size. 2506 2507 Args: 2508 args: A dictionary of arguments for the current GET request. Must 2509 contain 'filename' and 'mb' keys that refer to the name of the file 2510 to create and its desired size, respectively. 2511 """ 2512 megabytes = None 2513 filename = None 2514 try: 2515 megabytes = int(args['mb'][0]) 2516 filename = args['filename'][0] 2517 except (ValueError, KeyError, IndexError), e: 2518 logging.exception('Server error creating file: %s', e) 2519 assert megabytes and filename 2520 with open(os.path.join(self.server.docroot, filename), 'wb') as f: 2521 f.write('X' * 1024 * 1024 * megabytes) 2522 self.send_response(200) 2523 self.end_headers() 2524 2525 def _DeleteFileHandler(self, args): 2526 """A GET handler that deletes the specified local file. 2527 2528 Args: 2529 args: A dictionary of arguments for the current GET request. Must 2530 contain a 'filename' key that refers to the name of the file to 2531 delete, relative to the server's document root. 2532 """ 2533 filename = None 2534 try: 2535 filename = args['filename'][0] 2536 except (KeyError, IndexError), e: 2537 logging.exception('Server error deleting file: %s', e) 2538 assert filename 2539 try: 2540 os.remove(os.path.join(self.server.docroot, filename)) 2541 except OSError, e: 2542 logging.warning('OS error removing file: %s', e) 2543 self.send_response(200) 2544 self.end_headers() 2545 2546 def _StartUploadHandler(self, args): 2547 """A GET handler to serve a page that uploads the given amount of data. 2548 2549 When the page loads, the specified amount of data is automatically 2550 uploaded to the same local server that is handling the current request. 2551 2552 Args: 2553 args: A dictionary of arguments for the current GET request. Must 2554 contain an 'mb' key that refers to the size of the data to upload. 2555 """ 2556 megabytes = None 2557 try: 2558 megabytes = int(args['mb'][0]) 2559 except (ValueError, KeyError, IndexError), e: 2560 logging.exception('Server error starting upload: %s', e) 2561 assert megabytes 2562 script = """ 2563 <html> 2564 <head> 2565 <script type='text/javascript'> 2566 function startUpload() { 2567 var megabytes = %s; 2568 var data = Array((1024 * 1024 * megabytes) + 1).join('X'); 2569 var boundary = '***BOUNDARY***'; 2570 var xhr = new XMLHttpRequest(); 2571 2572 xhr.open('POST', 'process_upload', true); 2573 xhr.setRequestHeader( 2574 'Content-Type', 2575 'multipart/form-data; boundary="' + boundary + '"'); 2576 xhr.setRequestHeader('Content-Length', data.length); 2577 xhr.onreadystatechange = function() { 2578 if (xhr.readyState == 4 && xhr.status == 200) { 2579 document.getElementById('upload_result').innerHTML = 2580 xhr.responseText; 2581 } 2582 }; 2583 var body = '--' + boundary + '\\r\\n'; 2584 body += 'Content-Disposition: form-data;' + 2585 'file_contents=' + data; 2586 xhr.send(body); 2587 } 2588 </script> 2589 </head> 2590 2591 <body onload="startUpload();"> 2592 <div id='upload_result'>Uploading...</div> 2593 </body> 2594 </html> 2595 """ % megabytes 2596 self.send_response(200) 2597 self.end_headers() 2598 self.wfile.write(script) 2599 2600 def _ProcessUploadHandler(self, form): 2601 """A POST handler that discards uploaded data and sends a response. 2602 2603 Args: 2604 form: A dictionary containing posted form data, as returned by 2605 urlparse.parse_qs(). 2606 """ 2607 upload_processed = False 2608 file_size = 0 2609 if 'file_contents' in form: 2610 file_size = len(form['file_contents'][0]) 2611 upload_processed = True 2612 self.send_response(200) 2613 self.end_headers() 2614 if upload_processed: 2615 self.wfile.write('Upload complete (%d bytes)' % file_size) 2616 else: 2617 self.wfile.write('No file contents uploaded') 2618 2619 GET_REQUEST_HANDLERS = { 2620 'create_file_of_size': _CreateFileOfSizeHandler, 2621 'delete_file': _DeleteFileHandler, 2622 'start_upload': _StartUploadHandler, 2623 'favicon.ico': _IgnoreHandler, 2624 } 2625 2626 POST_REQUEST_HANDLERS = { 2627 'process_upload': _ProcessUploadHandler, 2628 } 2629 2630 def translate_path(self, path): 2631 """Ensures files are served from the given document root. 2632 2633 Overridden from SimpleHTTPServer.SimpleHTTPRequestHandler. 2634 """ 2635 path = urlparse.urlparse(path)[2] 2636 path = posixpath.normpath(urllib.unquote(path)) 2637 words = path.split('/') 2638 words = filter(None, words) # Remove empty strings from |words|. 2639 path = self.server.docroot 2640 for word in words: 2641 _, word = os.path.splitdrive(word) 2642 _, word = os.path.split(word) 2643 if word in (os.curdir, os.pardir): 2644 continue 2645 path = os.path.join(path, word) 2646 return path 2647 2648 def do_GET(self): 2649 """Processes a GET request to the local server. 2650 2651 Overridden from SimpleHTTPServer.SimpleHTTPRequestHandler. 2652 """ 2653 split_url = urlparse.urlsplit(self.path) 2654 base_path = split_url[2] 2655 if base_path.startswith('/'): 2656 base_path = base_path[1:] 2657 args = urlparse.parse_qs(split_url[3]) 2658 if base_path in self.GET_REQUEST_HANDLERS: 2659 self.GET_REQUEST_HANDLERS[base_path](self, args) 2660 else: 2661 SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 2662 2663 def do_POST(self): 2664 """Processes a POST request to the local server. 2665 2666 Overridden from SimpleHTTPServer.SimpleHTTPRequestHandler. 2667 """ 2668 form = urlparse.parse_qs( 2669 self.rfile.read(int(self.headers.getheader('Content-Length')))) 2670 path = urlparse.urlparse(self.path)[2] 2671 if path.startswith('/'): 2672 path = path[1:] 2673 if path in self.POST_REQUEST_HANDLERS: 2674 self.POST_REQUEST_HANDLERS[path](self, form) 2675 else: 2676 self.send_response(200) 2677 self.send_header('Content-Type', 'text/plain') 2678 self.end_headers() 2679 self.wfile.write('No handler for POST request "%s".' % path) 2680 2681 2682 class ThreadedHTTPServer(SocketServer.ThreadingMixIn, 2683 BaseHTTPServer.HTTPServer): 2684 def __init__(self, server_address, handler_class): 2685 BaseHTTPServer.HTTPServer.__init__(self, server_address, handler_class) 2686 2687 2688 class PerfTestServer(object): 2689 """Local server for use by performance tests.""" 2690 2691 def __init__(self, docroot): 2692 """Initializes the performance test server. 2693 2694 Args: 2695 docroot: The directory from which to serve files. 2696 """ 2697 # The use of 0 means to start the server on an arbitrary available port. 2698 self._server = ThreadedHTTPServer(('', 0), 2699 PerfTestServerRequestHandler) 2700 self._server.docroot = docroot 2701 self._server_thread = threading.Thread(target=self._server.serve_forever) 2702 2703 def Run(self): 2704 """Starts the server thread.""" 2705 self._server_thread.start() 2706 2707 def ShutDown(self): 2708 """Shuts down the server.""" 2709 self._server.shutdown() 2710 self._server_thread.join() 2711 2712 def GetPort(self): 2713 """Identifies the port number to which the server is currently bound. 2714 2715 Returns: 2716 The numeric port number to which the server is currently bound. 2717 """ 2718 return self._server.server_address[1] 2719 2720 2721 if __name__ == '__main__': 2722 pyauto_functional.Main() 2723