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