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