• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""Performance tests for Chrome Endure (long-running perf tests on Chrome).
7
8This module accepts the following environment variable inputs:
9  TEST_LENGTH: The number of seconds in which to run each test.
10  PERF_STATS_INTERVAL: The number of seconds to wait in-between each sampling
11      of performance/memory statistics.
12
13The following variables are related to the Deep Memory Profiler.
14  DEEP_MEMORY_PROFILE: Enable the Deep Memory Profiler if it's set to 'True'.
15  DEEP_MEMORY_PROFILE_SAVE: Don't clean up dump files if it's set to 'True'.
16  DEEP_MEMORY_PROFILE_UPLOAD: Upload dumped files if the variable has a Google
17      Storage bucket like gs://chromium-endure/.  The 'gsutil' script in $PATH
18      is used by default, or set a variable 'GSUTIL' to specify a path to the
19      'gsutil' script.  A variable 'REVISION' (or 'BUILDBOT_GOT_REVISION') is
20      used as a subdirectory in the destination if it is set.
21  GSUTIL: A path to the 'gsutil' script.  Not mandatory.
22  REVISION: A string that represents the revision or some build configuration.
23      Not mandatory.
24  BUILDBOT_GOT_REVISION: Similar to 'REVISION', but checked only if 'REVISION'
25      is not specified.  Not mandatory.
26
27  ENDURE_NO_WPR: Run tests without Web Page Replay if it's set.
28  WPR_RECORD: Run tests in record mode. If you want to make a fresh
29              archive, make sure to delete the old one, otherwise
30              it will append to the old one.
31  WPR_ARCHIVE_PATH: an alternative archive file to use.
32"""
33
34from datetime import datetime
35import json
36import logging
37import os
38import re
39import subprocess
40import tempfile
41import time
42
43import perf
44import pyauto_functional  # Must be imported before pyauto.
45import pyauto
46import pyauto_errors
47import pyauto_utils
48import remote_inspector_client
49import selenium.common.exceptions
50from selenium.webdriver.support.ui import WebDriverWait
51import webpagereplay
52
53
54class NotSupportedEnvironmentError(RuntimeError):
55  """Represent an error raised since the environment (OS) is not supported."""
56  pass
57
58
59class DeepMemoryProfiler(object):
60  """Controls Deep Memory Profiler (dmprof) for endurance tests."""
61  DEEP_MEMORY_PROFILE = False
62  DEEP_MEMORY_PROFILE_SAVE = False
63  DEEP_MEMORY_PROFILE_UPLOAD = ''
64
65  _WORKDIR_PATTERN = re.compile('endure\.[0-9]+\.[0-9]+\.[A-Za-z0-9]+')
66  _SAVED_WORKDIRS = 8
67
68  _DMPROF_DIR_PATH = os.path.abspath(os.path.join(
69      os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
70      'tools', 'deep_memory_profiler'))
71  _DMPROF_SCRIPT_PATH = os.path.join(_DMPROF_DIR_PATH, 'dmprof')
72  _POLICIES = ['l0', 'l1', 'l2', 't0']
73
74  def __init__(self):
75    self._enabled = self.GetEnvironmentVariable(
76        'DEEP_MEMORY_PROFILE', bool, self.DEEP_MEMORY_PROFILE)
77    self._save = self.GetEnvironmentVariable(
78        'DEEP_MEMORY_PROFILE_SAVE', bool, self.DEEP_MEMORY_PROFILE_SAVE)
79    self._upload = self.GetEnvironmentVariable(
80        'DEEP_MEMORY_PROFILE_UPLOAD', str, self.DEEP_MEMORY_PROFILE_UPLOAD)
81    if self._upload and not self._upload.endswith('/'):
82      self._upload += '/'
83
84    self._revision = ''
85    self._gsutil = ''
86    self._json_file = None
87    self._last_json_filename = ''
88    self._proc = None
89    self._last_time = {}
90    for policy in self._POLICIES:
91      self._last_time[policy] = -1.0
92
93  def __nonzero__(self):
94    return self._enabled
95
96  @staticmethod
97  def GetEnvironmentVariable(env_name, converter, default):
98    """Returns a converted environment variable for Deep Memory Profiler.
99
100    Args:
101      env_name: A string name of an environment variable.
102      converter: A function taking a string to convert an environment variable.
103      default: A value used if the environment variable is not specified.
104
105    Returns:
106      A value converted from the environment variable with 'converter'.
107    """
108    return converter(os.environ.get(env_name, default))
109
110  def SetUp(self, is_linux, revision, gsutil):
111    """Sets up Deep Memory Profiler settings for a Chrome process.
112
113    It sets environment variables and makes a working directory.
114    """
115    if not self._enabled:
116      return
117
118    if not is_linux:
119      raise NotSupportedEnvironmentError(
120          'Deep Memory Profiler is not supported in this environment (OS).')
121
122    self._revision = revision
123    self._gsutil = gsutil
124
125    # Remove old dumped files with keeping latest _SAVED_WORKDIRS workdirs.
126    # It keeps the latest workdirs not to miss data by failure in uploading
127    # and other operations.  Dumped files are no longer available if they are
128    # removed.  Re-execution doesn't generate the same files.
129    tempdir = tempfile.gettempdir()
130    saved_workdirs = 0
131    for filename in sorted(os.listdir(tempdir), reverse=True):
132      if self._WORKDIR_PATTERN.match(filename):
133        saved_workdirs += 1
134        if saved_workdirs > self._SAVED_WORKDIRS:
135          fullpath = os.path.abspath(os.path.join(tempdir, filename))
136          logging.info('Removing an old workdir: %s' % fullpath)
137          pyauto_utils.RemovePath(fullpath)
138
139    dir_prefix = 'endure.%s.' % datetime.today().strftime('%Y%m%d.%H%M%S')
140    self._workdir = tempfile.mkdtemp(prefix=dir_prefix, dir=tempdir)
141    os.environ['HEAPPROFILE'] = os.path.join(self._workdir, 'endure')
142    os.environ['HEAP_PROFILE_MMAP'] = '1'
143    os.environ['DEEP_HEAP_PROFILE'] = '1'
144
145  def TearDown(self):
146    """Tear down Deep Memory Profiler settings for the Chrome process.
147
148    It removes the environment variables and the temporary directory.
149    Call it after Chrome finishes.  Chrome may dump last files at the end.
150    """
151    if not self._enabled:
152      return
153
154    del os.environ['DEEP_HEAP_PROFILE']
155    del os.environ['HEAP_PROFILE_MMAP']
156    del os.environ['HEAPPROFILE']
157    if not self._save and self._workdir:
158      pyauto_utils.RemovePath(self._workdir)
159
160  def LogFirstMessage(self):
161    """Logs first messages."""
162    if not self._enabled:
163      return
164
165    logging.info('Running with the Deep Memory Profiler.')
166    if self._save:
167      logging.info('  Dumped files won\'t be cleaned.')
168    else:
169      logging.info('  Dumped files will be cleaned up after every test.')
170
171  def StartProfiler(self, proc_info, is_last, webapp_name, test_description):
172    """Starts Deep Memory Profiler in background."""
173    if not self._enabled:
174      return
175
176    logging.info('  Profiling with the Deep Memory Profiler...')
177
178    # Wait for a running dmprof process for last _GetPerformanceStat call to
179    # cover last dump files.
180    if is_last:
181      logging.info('    Waiting for the last dmprof.')
182      self._WaitForDeepMemoryProfiler()
183
184    if self._proc and self._proc.poll() is None:
185      logging.info('    Last dmprof is still running.')
186    else:
187      if self._json_file:
188        self._last_json_filename = self._json_file.name
189        self._json_file.close()
190        self._json_file = None
191      first_dump = ''
192      last_dump = ''
193      for filename in sorted(os.listdir(self._workdir)):
194        if re.match('^endure.%05d.\d+.heap$' % proc_info['tab_pid'],
195                    filename):
196          logging.info('    Profiled dump file: %s' % filename)
197          last_dump = filename
198          if not first_dump:
199            first_dump = filename
200      if first_dump:
201        logging.info('    First dump file: %s' % first_dump)
202        matched = re.match('^endure.\d+.(\d+).heap$', last_dump)
203        last_sequence_id = matched.group(1)
204        self._json_file = open(
205            os.path.join(self._workdir,
206                         'endure.%05d.%s.json' % (proc_info['tab_pid'],
207                                                  last_sequence_id)), 'w+')
208        self._proc = subprocess.Popen(
209            '%s json %s' % (self._DMPROF_SCRIPT_PATH,
210                            os.path.join(self._workdir, first_dump)),
211            shell=True, stdout=self._json_file)
212        if is_last:
213          # Wait only when it is the last profiling.  dmprof may take long time.
214          self._WaitForDeepMemoryProfiler()
215
216          # Upload the dumped files.
217          if first_dump and self._upload and self._gsutil:
218            if self._revision:
219              destination_path = '%s%s/' % (self._upload, self._revision)
220            else:
221              destination_path = self._upload
222            destination_path += '%s-%s-%s.zip' % (
223                webapp_name,
224                test_description,
225                os.path.basename(self._workdir))
226            gsutil_command = '%s upload --gsutil %s %s %s' % (
227                self._DMPROF_SCRIPT_PATH,
228                self._gsutil,
229                os.path.join(self._workdir, first_dump),
230                destination_path)
231            logging.info('Uploading: %s' % gsutil_command)
232            try:
233              returncode = subprocess.call(gsutil_command, shell=True)
234              logging.info('  Return code: %d' % returncode)
235            except OSError, e:
236              logging.error('  Error while uploading: %s', e)
237          else:
238            logging.info('Note that the dumped files are not uploaded.')
239      else:
240        logging.info('    No dump files.')
241
242  def ParseResultAndOutputPerfGraphValues(
243      self, webapp_name, test_description, output_perf_graph_value):
244    """Parses Deep Memory Profiler result, and outputs perf graph values."""
245    if not self._enabled:
246      return
247
248    results = {}
249    for policy in self._POLICIES:
250      if self._last_json_filename:
251        json_data = {}
252        with open(self._last_json_filename) as json_f:
253          json_data = json.load(json_f)
254        if json_data['version'] == 'JSON_DEEP_1':
255          results[policy] = json_data['snapshots']
256        elif json_data['version'] == 'JSON_DEEP_2':
257          results[policy] = json_data['policies'][policy]['snapshots']
258    for policy, result in results.iteritems():
259      if result and result[-1]['second'] > self._last_time[policy]:
260        started = False
261        for legend in json_data['policies'][policy]['legends']:
262          if legend == 'FROM_HERE_FOR_TOTAL':
263            started = True
264          elif legend == 'UNTIL_HERE_FOR_TOTAL':
265            break
266          elif started:
267            output_perf_graph_value(
268                legend.encode('utf-8'), [
269                    (int(round(snapshot['second'])), snapshot[legend] / 1024)
270                    for snapshot in result
271                    if snapshot['second'] > self._last_time[policy]],
272                'KB',
273                graph_name='%s%s-%s-DMP' % (
274                    webapp_name, test_description, policy),
275                units_x='seconds', is_stacked=True)
276        self._last_time[policy] = result[-1]['second']
277
278  def _WaitForDeepMemoryProfiler(self):
279    """Waits for the Deep Memory Profiler to finish if running."""
280    if not self._enabled or not self._proc:
281      return
282
283    self._proc.wait()
284    self._proc = None
285    if self._json_file:
286      self._last_json_filename = self._json_file.name
287      self._json_file.close()
288      self._json_file = None
289
290
291class ChromeEndureBaseTest(perf.BasePerfTest):
292  """Implements common functionality for all Chrome Endure tests.
293
294  All Chrome Endure test classes should inherit from this class.
295  """
296
297  _DEFAULT_TEST_LENGTH_SEC = 60 * 60 * 6  # Tests run for 6 hours.
298  _GET_PERF_STATS_INTERVAL = 60 * 5  # Measure perf stats every 5 minutes.
299  # TODO(dennisjeffrey): Do we still need to tolerate errors?
300  _ERROR_COUNT_THRESHOLD = 50  # Number of errors to tolerate.
301  _REVISION = ''
302  _GSUTIL = 'gsutil'
303
304  def setUp(self):
305    # The Web Page Replay environment variables must be parsed before
306    # perf.BasePerfTest.setUp()
307    self._ParseReplayEnv()
308    # The environment variables for the Deep Memory Profiler must be set
309    # before perf.BasePerfTest.setUp() to inherit them to Chrome.
310    self._dmprof = DeepMemoryProfiler()
311    self._revision = str(os.environ.get('REVISION', self._REVISION))
312    if not self._revision:
313      self._revision = str(os.environ.get('BUILDBOT_GOT_REVISION',
314                                          self._REVISION))
315    self._gsutil = str(os.environ.get('GSUTIL', self._GSUTIL))
316    if self._dmprof:
317      self._dmprof.SetUp(self.IsLinux(), self._revision, self._gsutil)
318
319    perf.BasePerfTest.setUp(self)
320
321    self._test_length_sec = int(
322        os.environ.get('TEST_LENGTH', self._DEFAULT_TEST_LENGTH_SEC))
323    self._get_perf_stats_interval = int(
324        os.environ.get('PERF_STATS_INTERVAL', self._GET_PERF_STATS_INTERVAL))
325
326    logging.info('Running test for %d seconds.', self._test_length_sec)
327    logging.info('Gathering perf stats every %d seconds.',
328                 self._get_perf_stats_interval)
329
330    if self._dmprof:
331      self._dmprof.LogFirstMessage()
332
333    # Set up a remote inspector client associated with tab 0.
334    logging.info('Setting up connection to remote inspector...')
335    self._remote_inspector_client = (
336        remote_inspector_client.RemoteInspectorClient())
337    logging.info('Connection to remote inspector set up successfully.')
338
339    self._test_start_time = 0
340    self._num_errors = 0
341    self._events_to_output = []
342    self._StartReplayServerIfNecessary()
343
344  def tearDown(self):
345    logging.info('Terminating connection to remote inspector...')
346    self._remote_inspector_client.Stop()
347    logging.info('Connection to remote inspector terminated.')
348
349    # Must be done at end of this function except for post-cleaning after
350    # Chrome finishes.
351    perf.BasePerfTest.tearDown(self)
352
353    # Must be done after perf.BasePerfTest.tearDown()
354    self._StopReplayServerIfNecessary()
355    if self._dmprof:
356      self._dmprof.TearDown()
357
358  def _GetArchiveName(self):
359    """Return the Web Page Replay archive name that corresponds to a test.
360
361    Override this function to return the name of an archive that
362    corresponds to the test, e.g "ChromeEndureGmailTest.wpr".
363
364    Returns:
365      None, by default no archive name is provided.
366    """
367    return None
368
369  def _ParseReplayEnv(self):
370    """Parse Web Page Replay related envrionment variables."""
371    if 'ENDURE_NO_WPR' in os.environ:
372      self._use_wpr = False
373      logging.info('Skipping Web Page Replay since ENDURE_NO_WPR is set.')
374    else:
375      self._archive_path = None
376      if 'WPR_ARCHIVE_PATH' in os.environ:
377        self._archive_path = os.environ.get('WPR_ARCHIVE_PATH')
378      else:
379        if self._GetArchiveName():
380          self._archive_path = ChromeEndureReplay.Path(
381              'archive', archive_name=self._GetArchiveName())
382      self._is_record_mode = 'WPR_RECORD' in os.environ
383      if self._is_record_mode:
384        if self._archive_path:
385          self._use_wpr = True
386        else:
387          self._use_wpr = False
388          logging.info('Fail to record since a valid archive path can not ' +
389                       'be generated. Did you implement ' +
390                       '_GetArchiveName() in your test?')
391      else:
392        if self._archive_path and os.path.exists(self._archive_path):
393          self._use_wpr = True
394        else:
395          self._use_wpr = False
396          logging.info(
397              'Skipping Web Page Replay since archive file %sdoes not exist.',
398              self._archive_path + ' ' if self._archive_path else '')
399
400  def ExtraChromeFlags(self):
401    """Ensures Chrome is launched with custom flags.
402
403    Returns:
404      A list of extra flags to pass to Chrome when it is launched.
405    """
406    # The same with setUp, but need to fetch the environment variable since
407    # ExtraChromeFlags is called before setUp.
408    deep_memory_profile = DeepMemoryProfiler.GetEnvironmentVariable(
409        'DEEP_MEMORY_PROFILE', bool, DeepMemoryProfiler.DEEP_MEMORY_PROFILE)
410
411    # Ensure Chrome enables remote debugging on port 9222.  This is required to
412    # interact with Chrome's remote inspector.
413    # Also, enable the memory benchmarking V8 extension for heap dumps.
414    extra_flags = ['--remote-debugging-port=9222',
415                   '--enable-memory-benchmarking']
416    if deep_memory_profile:
417      extra_flags.append('--no-sandbox')
418    if self._use_wpr:
419      extra_flags.extend(ChromeEndureReplay.CHROME_FLAGS)
420    return perf.BasePerfTest.ExtraChromeFlags(self) + extra_flags
421
422  def _OnTimelineEvent(self, event_info):
423    """Invoked by the Remote Inspector Client when a timeline event occurs.
424
425    Args:
426      event_info: A dictionary containing raw information associated with a
427         timeline event received from Chrome's remote inspector.  Refer to
428         chrome/src/third_party/WebKit/Source/WebCore/inspector/Inspector.json
429         for the format of this dictionary.
430    """
431    elapsed_time = int(round(time.time() - self._test_start_time))
432
433    if event_info['type'] == 'GCEvent':
434      self._events_to_output.append({
435        'type': 'GarbageCollection',
436        'time': elapsed_time,
437        'data':
438            {'collected_bytes': event_info['data']['usedHeapSizeDelta']},
439      })
440
441  def _RunEndureTest(self, webapp_name, tab_title_substring, test_description,
442                     do_scenario, frame_xpath=''):
443    """The main test harness function to run a general Chrome Endure test.
444
445    After a test has performed any setup work and has navigated to the proper
446    starting webpage, this function should be invoked to run the endurance test.
447
448    Args:
449      webapp_name: A string name for the webapp being tested.  Should not
450          include spaces.  For example, 'Gmail', 'Docs', or 'Plus'.
451      tab_title_substring: A unique substring contained within the title of
452          the tab to use, for identifying the appropriate tab.
453      test_description: A string description of what the test does, used for
454          outputting results to be graphed.  Should not contain spaces.  For
455          example, 'ComposeDiscard' for Gmail.
456      do_scenario: A callable to be invoked that implements the scenario to be
457          performed by this test.  The callable is invoked iteratively for the
458          duration of the test.
459      frame_xpath: The string xpath of the frame in which to inject javascript
460          to clear chromedriver's cache (a temporary workaround until the
461          WebDriver team changes how they handle their DOM node cache).
462    """
463    self._num_errors = 0
464    self._test_start_time = time.time()
465    last_perf_stats_time = time.time()
466    if self._dmprof:
467      self.HeapProfilerDump('renderer', 'Chrome Endure (first)')
468    self._GetPerformanceStats(
469        webapp_name, test_description, tab_title_substring)
470    self._iteration_num = 0  # Available to |do_scenario| if needed.
471
472    self._remote_inspector_client.StartTimelineEventMonitoring(
473        self._OnTimelineEvent)
474
475    while time.time() - self._test_start_time < self._test_length_sec:
476      self._iteration_num += 1
477
478      if self._num_errors >= self._ERROR_COUNT_THRESHOLD:
479        logging.error('Error count threshold (%d) reached. Terminating test '
480                      'early.' % self._ERROR_COUNT_THRESHOLD)
481        break
482
483      if time.time() - last_perf_stats_time >= self._get_perf_stats_interval:
484        last_perf_stats_time = time.time()
485        if self._dmprof:
486          self.HeapProfilerDump('renderer', 'Chrome Endure')
487        self._GetPerformanceStats(
488            webapp_name, test_description, tab_title_substring)
489
490      if self._iteration_num % 10 == 0:
491        remaining_time = self._test_length_sec - (time.time() -
492                                                  self._test_start_time)
493        logging.info('Chrome interaction #%d. Time remaining in test: %d sec.' %
494                     (self._iteration_num, remaining_time))
495
496      do_scenario()
497      # Clear ChromeDriver's DOM node cache so its growth doesn't affect the
498      # results of Chrome Endure.
499      # TODO(dennisjeffrey): Once the WebDriver team implements changes to
500      # handle their DOM node cache differently, we need to revisit this.  It
501      # may no longer be necessary at that point to forcefully delete the cache.
502      # Additionally, the Javascript below relies on an internal property of
503      # WebDriver that may change at any time.  This is only a temporary
504      # workaround to stabilize the Chrome Endure test results.
505      js = """
506        (function() {
507          delete document.$wdc_;
508          window.domAutomationController.send('done');
509        })();
510      """
511      try:
512        self.ExecuteJavascript(js, frame_xpath=frame_xpath)
513      except pyauto_errors.AutomationCommandTimeout:
514        self._num_errors += 1
515        logging.warning('Logging an automation timeout: delete chromedriver '
516                        'cache.')
517
518    self._remote_inspector_client.StopTimelineEventMonitoring()
519
520    if self._dmprof:
521      self.HeapProfilerDump('renderer', 'Chrome Endure (last)')
522    self._GetPerformanceStats(
523        webapp_name, test_description, tab_title_substring, is_last=True)
524
525  def _GetProcessInfo(self, tab_title_substring):
526    """Gets process info associated with an open browser/tab.
527
528    Args:
529      tab_title_substring: A unique substring contained within the title of
530          the tab to use; needed for locating the tab info.
531
532    Returns:
533      A dictionary containing information about the browser and specified tab
534      process:
535      {
536        'browser_private_mem': integer,  # Private memory associated with the
537                                         # browser process, in KB.
538        'tab_private_mem': integer,  # Private memory associated with the tab
539                                     # process, in KB.
540        'tab_pid': integer,  # Process ID of the tab process.
541      }
542    """
543    browser_process_name = (
544        self.GetBrowserInfo()['properties']['BrowserProcessExecutableName'])
545    info = self.GetProcessInfo()
546
547    # Get the information associated with the browser process.
548    browser_proc_info = []
549    for browser_info in info['browsers']:
550      if browser_info['process_name'] == browser_process_name:
551        for proc_info in browser_info['processes']:
552          if proc_info['child_process_type'] == 'Browser':
553            browser_proc_info.append(proc_info)
554    self.assertEqual(len(browser_proc_info), 1,
555                     msg='Expected to find 1 Chrome browser process, but found '
556                         '%d instead.\nCurrent process info:\n%s.' % (
557                         len(browser_proc_info), self.pformat(info)))
558
559    # Get the process information associated with the specified tab.
560    tab_proc_info = []
561    for browser_info in info['browsers']:
562      for proc_info in browser_info['processes']:
563        if (proc_info['child_process_type'] == 'Tab' and
564            [x for x in proc_info['titles'] if tab_title_substring in x]):
565          tab_proc_info.append(proc_info)
566    self.assertEqual(len(tab_proc_info), 1,
567                     msg='Expected to find 1 %s tab process, but found %d '
568                         'instead.\nCurrent process info:\n%s.' % (
569                         tab_title_substring, len(tab_proc_info),
570                         self.pformat(info)))
571
572    browser_proc_info = browser_proc_info[0]
573    tab_proc_info = tab_proc_info[0]
574    return {
575      'browser_private_mem': browser_proc_info['working_set_mem']['priv'],
576      'tab_private_mem': tab_proc_info['working_set_mem']['priv'],
577      'tab_pid': tab_proc_info['pid'],
578    }
579
580  def _GetPerformanceStats(self, webapp_name, test_description,
581                           tab_title_substring, is_last=False):
582    """Gets performance statistics and outputs the results.
583
584    Args:
585      webapp_name: A string name for the webapp being tested.  Should not
586          include spaces.  For example, 'Gmail', 'Docs', or 'Plus'.
587      test_description: A string description of what the test does, used for
588          outputting results to be graphed.  Should not contain spaces.  For
589          example, 'ComposeDiscard' for Gmail.
590      tab_title_substring: A unique substring contained within the title of
591          the tab to use, for identifying the appropriate tab.
592      is_last: A boolean value which should be True if it's the last call of
593          _GetPerformanceStats.  The default is False.
594    """
595    logging.info('Gathering performance stats...')
596    elapsed_time = int(round(time.time() - self._test_start_time))
597
598    memory_counts = self._remote_inspector_client.GetMemoryObjectCounts()
599    proc_info = self._GetProcessInfo(tab_title_substring)
600
601    if self._dmprof:
602      self._dmprof.StartProfiler(
603          proc_info, is_last, webapp_name, test_description)
604
605    # DOM node count.
606    dom_node_count = memory_counts['DOMNodeCount']
607    self._OutputPerfGraphValue(
608        'TotalDOMNodeCount', [(elapsed_time, dom_node_count)], 'nodes',
609        graph_name='%s%s-Nodes-DOM' % (webapp_name, test_description),
610        units_x='seconds')
611
612    # Event listener count.
613    event_listener_count = memory_counts['EventListenerCount']
614    self._OutputPerfGraphValue(
615        'EventListenerCount', [(elapsed_time, event_listener_count)],
616        'listeners',
617        graph_name='%s%s-EventListeners' % (webapp_name, test_description),
618        units_x='seconds')
619
620    # Browser process private memory.
621    self._OutputPerfGraphValue(
622        'BrowserPrivateMemory',
623        [(elapsed_time, proc_info['browser_private_mem'])], 'KB',
624        graph_name='%s%s-BrowserMem-Private' % (webapp_name, test_description),
625        units_x='seconds')
626
627    # Tab process private memory.
628    self._OutputPerfGraphValue(
629        'TabPrivateMemory',
630        [(elapsed_time, proc_info['tab_private_mem'])], 'KB',
631        graph_name='%s%s-TabMem-Private' % (webapp_name, test_description),
632        units_x='seconds')
633
634    # V8 memory used.
635    v8_info = self.GetV8HeapStats()  # First window, first tab.
636    v8_mem_used = v8_info['v8_memory_used'] / 1024.0  # Convert to KB.
637    self._OutputPerfGraphValue(
638        'V8MemoryUsed', [(elapsed_time, v8_mem_used)], 'KB',
639        graph_name='%s%s-V8MemUsed' % (webapp_name, test_description),
640        units_x='seconds')
641
642    # V8 memory allocated.
643    v8_mem_allocated = v8_info['v8_memory_allocated'] / 1024.0  # Convert to KB.
644    self._OutputPerfGraphValue(
645        'V8MemoryAllocated', [(elapsed_time, v8_mem_allocated)], 'KB',
646        graph_name='%s%s-V8MemAllocated' % (webapp_name, test_description),
647        units_x='seconds')
648
649    if self._dmprof:
650      self._dmprof.ParseResultAndOutputPerfGraphValues(
651          webapp_name, test_description, self._OutputPerfGraphValue)
652
653    logging.info('  Total DOM node count: %d nodes' % dom_node_count)
654    logging.info('  Event listener count: %d listeners' % event_listener_count)
655    logging.info('  Browser process private memory: %d KB' %
656                 proc_info['browser_private_mem'])
657    logging.info('  Tab process private memory: %d KB' %
658                 proc_info['tab_private_mem'])
659    logging.info('  V8 memory used: %f KB' % v8_mem_used)
660    logging.info('  V8 memory allocated: %f KB' % v8_mem_allocated)
661
662    # Output any new timeline events that have occurred.
663    if self._events_to_output:
664      logging.info('Logging timeline events...')
665      event_type_to_value_list = {}
666      for event_info in self._events_to_output:
667        if not event_info['type'] in event_type_to_value_list:
668          event_type_to_value_list[event_info['type']] = []
669        event_type_to_value_list[event_info['type']].append(
670            (event_info['time'], event_info['data']))
671      for event_type, value_list in event_type_to_value_list.iteritems():
672        self._OutputEventGraphValue(event_type, value_list)
673      self._events_to_output = []
674    else:
675      logging.info('No new timeline events to log.')
676
677  def _GetElement(self, find_by, value):
678    """Gets a WebDriver element object from the webpage DOM.
679
680    Args:
681      find_by: A callable that queries WebDriver for an element from the DOM.
682      value: A string value that can be passed to the |find_by| callable.
683
684    Returns:
685      The identified WebDriver element object, if found in the DOM, or
686      None, otherwise.
687    """
688    try:
689      return find_by(value)
690    except selenium.common.exceptions.NoSuchElementException:
691      return None
692
693  def _ClickElementByXpath(self, driver, xpath):
694    """Given the xpath for a DOM element, clicks on it using WebDriver.
695
696    Args:
697      driver: A WebDriver object, as returned by self.NewWebDriver().
698      xpath: The string xpath associated with the DOM element to click.
699
700    Returns:
701      True, if the DOM element was found and clicked successfully, or
702      False, otherwise.
703    """
704    try:
705      self.WaitForDomNode(xpath)
706    except (pyauto_errors.JSONInterfaceError,
707            pyauto_errors.JavascriptRuntimeError) as e:
708      logging.exception('PyAuto exception: %s' % e)
709      return False
710
711    try:
712      element = self._GetElement(driver.find_element_by_xpath, xpath)
713      element.click()
714    except (selenium.common.exceptions.StaleElementReferenceException,
715            selenium.common.exceptions.TimeoutException) as e:
716      logging.exception('WebDriver exception: %s' % e)
717      return False
718
719    return True
720
721  def _StartReplayServerIfNecessary(self):
722    """Start replay server if necessary."""
723    if self._use_wpr:
724      mode = 'record' if self._is_record_mode else 'replay'
725      self._wpr_server = ChromeEndureReplay.ReplayServer(self._archive_path)
726      self._wpr_server.StartServer()
727      logging.info('Web Page Replay server has started in %s mode.', mode)
728
729  def _StopReplayServerIfNecessary(self):
730    """Stop the Web Page Replay server if necessary.
731
732    This method has to be called AFTER all network connections which go
733    through Web Page Replay server have shut down. Otherwise the
734    Web Page Replay server will hang to wait for them. A good
735    place is to call it at the end of the teardown process.
736    """
737    if self._use_wpr:
738      self._wpr_server.StopServer()
739      logging.info('The Web Page Replay server stopped.')
740
741
742class ChromeEndureControlTest(ChromeEndureBaseTest):
743  """Control tests for Chrome Endure."""
744
745  _WEBAPP_NAME = 'Control'
746  _TAB_TITLE_SUBSTRING = 'Chrome Endure Control Test'
747
748  def testControlAttachDetachDOMTree(self):
749    """Continually attach and detach a DOM tree from a basic document."""
750    test_description = 'AttachDetachDOMTree'
751    url = self.GetHttpURLForDataPath('chrome_endure', 'endurance_control.html')
752    self.NavigateToURL(url)
753    loaded_tab_title = self.GetActiveTabTitle()
754    self.assertTrue(self._TAB_TITLE_SUBSTRING in loaded_tab_title,
755                    msg='Loaded tab title does not contain "%s": "%s"' %
756                        (self._TAB_TITLE_SUBSTRING, loaded_tab_title))
757
758    def scenario():
759      # Just sleep.  Javascript in the webpage itself does the work.
760      time.sleep(5)
761
762    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
763                        test_description, scenario)
764
765  def testControlAttachDetachDOMTreeWebDriver(self):
766    """Use WebDriver to attach and detach a DOM tree from a basic document."""
767    test_description = 'AttachDetachDOMTreeWebDriver'
768    url = self.GetHttpURLForDataPath('chrome_endure',
769                                     'endurance_control_webdriver.html')
770    self.NavigateToURL(url)
771    loaded_tab_title = self.GetActiveTabTitle()
772    self.assertTrue(self._TAB_TITLE_SUBSTRING in loaded_tab_title,
773                    msg='Loaded tab title does not contain "%s": "%s"' %
774                        (self._TAB_TITLE_SUBSTRING, loaded_tab_title))
775
776    driver = self.NewWebDriver()
777
778    def scenario(driver):
779      # Click the "attach" button to attach a large DOM tree (with event
780      # listeners) to the document, wait half a second, click "detach" to detach
781      # the DOM tree from the document, wait half a second.
782      self._ClickElementByXpath(driver, 'id("attach")')
783      time.sleep(0.5)
784      self._ClickElementByXpath(driver, 'id("detach")')
785      time.sleep(0.5)
786
787    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
788                        test_description, lambda: scenario(driver))
789
790
791# TODO(dennisjeffrey): Make new WPR recordings of the Gmail tests so that we
792# can remove the special handling for when self._use_wpr is True.
793class ChromeEndureGmailTest(ChromeEndureBaseTest):
794  """Long-running performance tests for Chrome using Gmail."""
795
796  _WEBAPP_NAME = 'Gmail'
797  _TAB_TITLE_SUBSTRING = 'Gmail'
798  _FRAME_XPATH = 'id("canvas_frame")'
799
800  def setUp(self):
801    ChromeEndureBaseTest.setUp(self)
802
803    self._FRAME_XPATH = self._FRAME_XPATH if self._use_wpr else ''
804
805    # Log into a test Google account and open up Gmail.
806    self._LoginToGoogleAccount(account_key='test_google_account_gmail')
807    self.NavigateToURL(self._GetConfig().get('gmail_url'))
808    self.assertTrue(
809        self.WaitUntil(lambda: self._TAB_TITLE_SUBSTRING in
810                       self.GetActiveTabTitle(),
811                       timeout=60, expect_retval=True, retry_sleep=1),
812        msg='Timed out waiting for Gmail to load. Tab title is: %s' %
813        self.GetActiveTabTitle())
814
815    self._driver = self.NewWebDriver()
816    # Any call to wait.until() will raise an exception if the timeout is hit.
817    # TODO(dennisjeffrey): Remove the need for webdriver's wait using the new
818    # DOM mutation observer mechanism.
819    self._wait = WebDriverWait(self._driver, timeout=60)
820
821
822    if self._use_wpr:
823      # Wait until Gmail's 'canvas_frame' loads and the 'Inbox' link is present.
824      # TODO(dennisjeffrey): Check with the Gmail team to see if there's a
825      # better way to tell when the webpage is ready for user interaction.
826      self._wait.until(
827          self._SwitchToCanvasFrame)  # Raises exception if the timeout is hit.
828
829    # Wait for the inbox to appear.
830    self.WaitForDomNode('//a[starts-with(@title, "Inbox")]',
831                        frame_xpath=self._FRAME_XPATH)
832
833    # Test whether latency dom element is available.
834    try:
835      self._GetLatencyDomElement(5000)
836      self._has_latency = True
837    except pyauto_errors.JSONInterfaceError:
838      logging.info('Skip recording latency as latency ' +
839                   'dom element is not available.')
840      self._has_latency = False
841
842  def _GetArchiveName(self):
843    """Return Web Page Replay archive name."""
844    return 'ChromeEndureGmailTest.wpr'
845
846  def _SwitchToCanvasFrame(self, driver):
847    """Switch the WebDriver to Gmail's 'canvas_frame', if it's available.
848
849    Args:
850      driver: A selenium.webdriver.remote.webdriver.WebDriver object.
851
852    Returns:
853      True, if the switch to Gmail's 'canvas_frame' is successful, or
854      False if not.
855    """
856    try:
857      driver.switch_to_frame('canvas_frame')
858      return True
859    except selenium.common.exceptions.NoSuchFrameException:
860      return False
861
862  def _GetLatencyDomElement(self, timeout=-1):
863    """Returns a reference to the latency info element in the Gmail DOM.
864
865    Args:
866      timeout: The maximum amount of time (in milliseconds) to wait for
867               the latency dom element to appear, defaults to the
868               default automation timeout.
869    Returns:
870      A latency dom element.
871    """
872    latency_xpath = (
873        '//span[starts-with(text(), "Why was the last action slow?")]')
874    self.WaitForDomNode(latency_xpath, timeout=timeout,
875                        frame_xpath=self._FRAME_XPATH)
876    return self._GetElement(self._driver.find_element_by_xpath, latency_xpath)
877
878  def _WaitUntilDomElementRemoved(self, dom_element):
879    """Waits until the given element is no longer attached to the DOM.
880
881    Args:
882      dom_element: A selenium.webdriver.remote.WebElement object.
883    """
884    def _IsElementStale():
885      try:
886        dom_element.tag_name
887      except selenium.common.exceptions.StaleElementReferenceException:
888        return True
889      return False
890
891    self.WaitUntil(_IsElementStale, timeout=60, expect_retval=True)
892
893  def _ClickElementAndRecordLatency(self, element, test_description,
894                                    action_description):
895    """Clicks a DOM element and records the latency associated with that action.
896
897    To account for scenario warm-up time, latency values during the first
898    minute of test execution are not recorded.
899
900    Args:
901      element: A selenium.webdriver.remote.WebElement object to click.
902      test_description: A string description of what the test does, used for
903          outputting results to be graphed.  Should not contain spaces.  For
904          example, 'ComposeDiscard' for Gmail.
905      action_description: A string description of what action is being
906          performed.  Should not contain spaces.  For example, 'Compose'.
907    """
908    if not self._has_latency:
909      element.click()
910      return
911    latency_dom_element = self._GetLatencyDomElement()
912    element.click()
913    # Wait for the old latency value to be removed, before getting the new one.
914    self._WaitUntilDomElementRemoved(latency_dom_element)
915
916    latency_dom_element = self._GetLatencyDomElement()
917    match = re.search(r'\[(\d+) ms\]', latency_dom_element.text)
918    if match:
919      latency = int(match.group(1))
920      elapsed_time = int(round(time.time() - self._test_start_time))
921      if elapsed_time > 60:  # Ignore the first minute of latency measurements.
922        self._OutputPerfGraphValue(
923            '%sLatency' % action_description, [(elapsed_time, latency)], 'msec',
924            graph_name='%s%s-%sLatency' % (self._WEBAPP_NAME, test_description,
925                                           action_description),
926            units_x='seconds')
927    else:
928      logging.warning('Could not identify latency value.')
929
930  def testGmailComposeDiscard(self):
931    """Continuously composes/discards an e-mail before sending.
932
933    This test continually composes/discards an e-mail using Gmail, and
934    periodically gathers performance stats that may reveal memory bloat.
935    """
936    test_description = 'ComposeDiscard'
937
938    def scenario_wpr():
939      # Click the "Compose" button, enter some text into the "To" field, enter
940      # some text into the "Subject" field, then click the "Discard" button to
941      # discard the message.
942      compose_xpath = '//div[text()="COMPOSE"]'
943      self.WaitForDomNode(compose_xpath, frame_xpath=self._FRAME_XPATH)
944      compose_button = self._GetElement(self._driver.find_element_by_xpath,
945                                        compose_xpath)
946      self._ClickElementAndRecordLatency(
947          compose_button, test_description, 'Compose')
948
949      to_xpath = '//textarea[@name="to"]'
950      self.WaitForDomNode(to_xpath, frame_xpath=self._FRAME_XPATH)
951      to_field = self._GetElement(self._driver.find_element_by_xpath, to_xpath)
952      to_field.send_keys('nobody@nowhere.com')
953
954      subject_xpath = '//input[@name="subject"]'
955      self.WaitForDomNode(subject_xpath, frame_xpath=self._FRAME_XPATH)
956      subject_field = self._GetElement(self._driver.find_element_by_xpath,
957                                       subject_xpath)
958      subject_field.send_keys('This message is about to be discarded')
959
960      discard_xpath = '//div[text()="Discard"]'
961      self.WaitForDomNode(discard_xpath, frame_xpath=self._FRAME_XPATH)
962      discard_button = self._GetElement(self._driver.find_element_by_xpath,
963                                        discard_xpath)
964      discard_button.click()
965
966      # Wait for the message to be discarded, assumed to be true after the
967      # "To" field is removed from the webpage DOM.
968      self._wait.until(lambda _: not self._GetElement(
969                           self._driver.find_element_by_name, 'to'))
970
971    def scenario_live():
972      compose_xpath = '//div[text()="COMPOSE"]'
973      self.WaitForDomNode(compose_xpath, frame_xpath=self._FRAME_XPATH)
974      compose_button = self._GetElement(self._driver.find_element_by_xpath,
975                                        compose_xpath)
976      self._ClickElementAndRecordLatency(
977          compose_button, test_description, 'Compose')
978
979      to_xpath = '//textarea[@name="to"]'
980      self.WaitForDomNode(to_xpath, frame_xpath=self._FRAME_XPATH)
981      to_field = self._GetElement(self._driver.find_element_by_xpath, to_xpath)
982      to_field.send_keys('nobody@nowhere.com')
983
984      subject_xpath = '//input[@name="subjectbox"]'
985      self.WaitForDomNode(subject_xpath, frame_xpath=self._FRAME_XPATH)
986      subject_field = self._GetElement(self._driver.find_element_by_xpath,
987                                       subject_xpath)
988      subject_field.send_keys('This message is about to be discarded')
989
990      discard_xpath = '//div[@aria-label="Discard draft"]'
991      self.WaitForDomNode(discard_xpath, frame_xpath=self._FRAME_XPATH)
992      discard_button = self._GetElement(self._driver.find_element_by_xpath,
993                                        discard_xpath)
994      discard_button.click()
995
996      # Wait for the message to be discarded, assumed to be true after the
997      # "To" element is removed from the webpage DOM.
998      self._wait.until(lambda _: not self._GetElement(
999                           self._driver.find_element_by_name, 'to'))
1000
1001    scenario = scenario_wpr if self._use_wpr else scenario_live
1002    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
1003                        test_description, scenario,
1004                        frame_xpath=self._FRAME_XPATH)
1005
1006  def testGmailAlternateThreadlistConversation(self):
1007    """Alternates between threadlist view and conversation view.
1008
1009    This test continually clicks between the threadlist (Inbox) and the
1010    conversation view (e-mail message view), and periodically gathers
1011    performance stats that may reveal memory bloat.
1012    """
1013    test_description = 'ThreadConversation'
1014
1015    def scenario():
1016      # Click an e-mail to see the conversation view, wait 1 second, click the
1017      # "Inbox" link to see the threadlist, wait 1 second.
1018
1019      # Find the first thread (e-mail) identified by a "span" tag that contains
1020      # an "email" attribute.  Then click it and wait for the conversation view
1021      # to appear (assumed to be visible when a particular div exists on the
1022      # page).
1023      thread_xpath = '//span[@email]'
1024      self.WaitForDomNode(thread_xpath, frame_xpath=self._FRAME_XPATH)
1025      thread = self._GetElement(self._driver.find_element_by_xpath,
1026                                thread_xpath)
1027      self._ClickElementAndRecordLatency(
1028          thread, test_description, 'Conversation')
1029      self.WaitForDomNode('//div[text()="Click here to "]',
1030                          frame_xpath=self._FRAME_XPATH)
1031      time.sleep(1)
1032
1033      # Find the inbox link and click it.  Then wait for the inbox to be shown
1034      # (assumed to be true when the particular div from the conversation view
1035      # no longer appears on the page).
1036      inbox_xpath = '//a[starts-with(text(), "Inbox")]'
1037      self.WaitForDomNode(inbox_xpath, frame_xpath=self._FRAME_XPATH)
1038      inbox = self._GetElement(self._driver.find_element_by_xpath, inbox_xpath)
1039      self._ClickElementAndRecordLatency(inbox, test_description, 'Threadlist')
1040      self._wait.until(
1041          lambda _: not self._GetElement(
1042              self._driver.find_element_by_xpath,
1043              '//div[text()="Click here to "]'))
1044      time.sleep(1)
1045
1046    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
1047                        test_description, scenario,
1048                        frame_xpath=self._FRAME_XPATH)
1049
1050  def testGmailAlternateTwoLabels(self):
1051    """Continuously alternates between two labels.
1052
1053    This test continually clicks between the "Inbox" and "Sent Mail" labels,
1054    and periodically gathers performance stats that may reveal memory bloat.
1055    """
1056    test_description = 'AlternateLabels'
1057
1058    def scenario():
1059      # Click the "Sent Mail" label, wait for 1 second, click the "Inbox" label,
1060      # wait for 1 second.
1061
1062      # Click the "Sent Mail" label, then wait for the tab title to be updated
1063      # with the substring "sent".
1064      sent_xpath = '//a[starts-with(text(), "Sent Mail")]'
1065      self.WaitForDomNode(sent_xpath, frame_xpath=self._FRAME_XPATH)
1066      sent = self._GetElement(self._driver.find_element_by_xpath, sent_xpath)
1067      self._ClickElementAndRecordLatency(sent, test_description, 'SentMail')
1068      self.assertTrue(
1069          self.WaitUntil(lambda: 'Sent Mail' in self.GetActiveTabTitle(),
1070                         timeout=60, expect_retval=True, retry_sleep=1),
1071          msg='Timed out waiting for Sent Mail to appear.')
1072      time.sleep(1)
1073
1074      # Click the "Inbox" label, then wait for the tab title to be updated with
1075      # the substring "inbox".
1076      inbox_xpath = '//a[starts-with(text(), "Inbox")]'
1077      self.WaitForDomNode(inbox_xpath, frame_xpath=self._FRAME_XPATH)
1078      inbox = self._GetElement(self._driver.find_element_by_xpath, inbox_xpath)
1079      self._ClickElementAndRecordLatency(inbox, test_description, 'Inbox')
1080      self.assertTrue(
1081          self.WaitUntil(lambda: 'Inbox' in self.GetActiveTabTitle(),
1082                         timeout=60, expect_retval=True, retry_sleep=1),
1083          msg='Timed out waiting for Inbox to appear.')
1084      time.sleep(1)
1085
1086    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
1087                        test_description, scenario,
1088                        frame_xpath=self._FRAME_XPATH)
1089
1090  def testGmailExpandCollapseConversation(self):
1091    """Continuously expands/collapses all messages in a conversation.
1092
1093    This test opens up a conversation (e-mail thread) with several messages,
1094    then continually alternates between the "Expand all" and "Collapse all"
1095    views, while periodically gathering performance stats that may reveal memory
1096    bloat.
1097    """
1098    test_description = 'ExpandCollapse'
1099
1100    # Enter conversation view for a particular thread.
1101    thread_xpath = '//span[@email]'
1102    self.WaitForDomNode(thread_xpath, frame_xpath=self._FRAME_XPATH)
1103    thread = self._GetElement(self._driver.find_element_by_xpath, thread_xpath)
1104    thread.click()
1105    self.WaitForDomNode('//div[text()="Click here to "]',
1106                        frame_xpath=self._FRAME_XPATH)
1107
1108    def scenario():
1109      # Click on the "Expand all" icon, wait for 1 second, click on the
1110      # "Collapse all" icon, wait for 1 second.
1111
1112      # Click the "Expand all" icon, then wait for that icon to be removed from
1113      # the page.
1114      expand_xpath = '//img[@alt="Expand all"]'
1115      self.WaitForDomNode(expand_xpath, frame_xpath=self._FRAME_XPATH)
1116      expand = self._GetElement(self._driver.find_element_by_xpath,
1117                                expand_xpath)
1118      self._ClickElementAndRecordLatency(expand, test_description, 'ExpandAll')
1119      self.WaitForDomNode(
1120          '//img[@alt="Expand all"]/parent::*/parent::*/parent::*'
1121          '[@style="display: none;"]',
1122          frame_xpath=self._FRAME_XPATH)
1123      time.sleep(1)
1124
1125      # Click the "Collapse all" icon, then wait for that icon to be removed
1126      # from the page.
1127      collapse_xpath = '//img[@alt="Collapse all"]'
1128      self.WaitForDomNode(collapse_xpath, frame_xpath=self._FRAME_XPATH)
1129      collapse = self._GetElement(self._driver.find_element_by_xpath,
1130                                  collapse_xpath)
1131      collapse.click()
1132      self.WaitForDomNode(
1133          '//img[@alt="Collapse all"]/parent::*/parent::*/parent::*'
1134          '[@style="display: none;"]',
1135          frame_xpath=self._FRAME_XPATH)
1136      time.sleep(1)
1137
1138    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
1139                        test_description, scenario,
1140                        frame_xpath=self._FRAME_XPATH)
1141
1142
1143class ChromeEndureDocsTest(ChromeEndureBaseTest):
1144  """Long-running performance tests for Chrome using Google Docs."""
1145
1146  _WEBAPP_NAME = 'Docs'
1147  _TAB_TITLE_SUBSTRING = 'Google Drive'
1148
1149  def setUp(self):
1150    ChromeEndureBaseTest.setUp(self)
1151
1152    # Log into a test Google account and open up Google Docs.
1153    self._LoginToGoogleAccount()
1154    self.NavigateToURL(self._GetConfig().get('docs_url'))
1155    self.assertTrue(
1156        self.WaitUntil(lambda: self._TAB_TITLE_SUBSTRING in
1157                               self.GetActiveTabTitle(),
1158                       timeout=60, expect_retval=True, retry_sleep=1),
1159        msg='Timed out waiting for Docs to load. Tab title is: %s' %
1160            self.GetActiveTabTitle())
1161
1162    self._driver = self.NewWebDriver()
1163
1164  def _GetArchiveName(self):
1165    """Return Web Page Replay archive name."""
1166    return 'ChromeEndureDocsTest.wpr'
1167
1168  def testDocsAlternatelyClickLists(self):
1169    """Alternates between two different document lists.
1170
1171    This test alternately clicks the "Shared with me" and "My Drive" buttons in
1172    Google Docs, and periodically gathers performance stats that may reveal
1173    memory bloat.
1174    """
1175    test_description = 'AlternateLists'
1176
1177    def sort_menu_setup():
1178      # Open and close the "Sort" menu to get some DOM nodes to appear that are
1179      # used by the scenario in this test.
1180      sort_xpath = '//div[text()="Sort"]'
1181      self.WaitForDomNode(sort_xpath)
1182      sort_button = self._GetElement(self._driver.find_element_by_xpath,
1183                                     sort_xpath)
1184      sort_button.click()
1185      sort_button.click()
1186      sort_button.click()
1187
1188    def scenario():
1189      # Click the "Shared with me" button, wait for 1 second, click the
1190      # "My Drive" button, wait for 1 second.
1191
1192      # Click the "Shared with me" button and wait for a div to appear.
1193      if not self._ClickElementByXpath(
1194          self._driver, '//div[text()="Shared with me"]'):
1195        self._num_errors += 1
1196        logging.warning('Logging an automation error: click "shared with me".')
1197      try:
1198        self.WaitForDomNode('//div[text()="Share date"]')
1199      except pyauto_errors.JSONInterfaceError:
1200        # This case can occur when the page reloads; set things up again.
1201        sort_menu_setup()
1202      time.sleep(1)
1203
1204      # Click the "My Drive" button and wait for a resulting div to appear.
1205      if not self._ClickElementByXpath(
1206          self._driver, '//span[starts-with(text(), "My Drive")]'):
1207        self._num_errors += 1
1208        logging.warning('Logging an automation error: click "my drive".')
1209      try:
1210        self.WaitForDomNode('//div[text()="Quota used"]')
1211      except pyauto_errors.JSONInterfaceError:
1212        # This case can occur when the page reloads; set things up again.
1213        sort_menu_setup()
1214      time.sleep(1)
1215
1216    sort_menu_setup()
1217
1218    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
1219                        test_description, scenario)
1220
1221
1222class ChromeEndurePlusTest(ChromeEndureBaseTest):
1223  """Long-running performance tests for Chrome using Google Plus."""
1224
1225  _WEBAPP_NAME = 'Plus'
1226  _TAB_TITLE_SUBSTRING = 'Google+'
1227
1228  def setUp(self):
1229    ChromeEndureBaseTest.setUp(self)
1230
1231    # Log into a test Google account and open up Google Plus.
1232    self._LoginToGoogleAccount()
1233    self.NavigateToURL(self._GetConfig().get('plus_url'))
1234    loaded_tab_title = self.GetActiveTabTitle()
1235    self.assertTrue(self._TAB_TITLE_SUBSTRING in loaded_tab_title,
1236                    msg='Loaded tab title does not contain "%s": "%s"' %
1237                        (self._TAB_TITLE_SUBSTRING, loaded_tab_title))
1238
1239    self._driver = self.NewWebDriver()
1240
1241  def _GetArchiveName(self):
1242    """Return Web Page Replay archive name."""
1243    return 'ChromeEndurePlusTest.wpr'
1244
1245  def testPlusAlternatelyClickStreams(self):
1246    """Alternates between two different streams.
1247
1248    This test alternately clicks the "Friends" and "Family" buttons using
1249    Google Plus, and periodically gathers performance stats that may reveal
1250    memory bloat.
1251    """
1252    test_description = 'AlternateStreams'
1253
1254    def scenario():
1255      # Click the "Friends" button, wait for 1 second, click the "Family"
1256      # button, wait for 1 second.
1257
1258      # Click the "Friends" button and wait for a resulting div to appear.
1259      if not self._ClickElementByXpath(
1260          self._driver,
1261          '//div[text()="Friends" and '
1262          'starts-with(@data-dest, "stream/circles")]'):
1263        self._num_errors += 1
1264        logging.warning('Logging an automation error: click "Friends" button.')
1265
1266      try:
1267        self.WaitForDomNode('//span[contains(., "in Friends")]')
1268      except (pyauto_errors.JSONInterfaceError,
1269              pyauto_errors.JavascriptRuntimeError):
1270        self._num_errors += 1
1271        logging.warning('Logging an automation error: wait for "in Friends".')
1272
1273      time.sleep(1)
1274
1275      # Click the "Family" button and wait for a resulting div to appear.
1276      if not self._ClickElementByXpath(
1277          self._driver,
1278          '//div[text()="Family" and '
1279          'starts-with(@data-dest, "stream/circles")]'):
1280        self._num_errors += 1
1281        logging.warning('Logging an automation error: click "Family" button.')
1282
1283      try:
1284        self.WaitForDomNode('//span[contains(., "in Family")]')
1285      except (pyauto_errors.JSONInterfaceError,
1286              pyauto_errors.JavascriptRuntimeError):
1287        self._num_errors += 1
1288        logging.warning('Logging an automation error: wait for "in Family".')
1289
1290      time.sleep(1)
1291
1292    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
1293                        test_description, scenario)
1294
1295
1296class IndexedDBOfflineTest(ChromeEndureBaseTest):
1297  """Long-running performance tests for IndexedDB, modeling offline usage."""
1298
1299  _WEBAPP_NAME = 'IndexedDBOffline'
1300  _TAB_TITLE_SUBSTRING = 'IndexedDB Offline'
1301
1302  def setUp(self):
1303    ChromeEndureBaseTest.setUp(self)
1304
1305    url = self.GetHttpURLForDataPath('indexeddb', 'endure', 'app.html')
1306    self.NavigateToURL(url)
1307    loaded_tab_title = self.GetActiveTabTitle()
1308    self.assertTrue(self._TAB_TITLE_SUBSTRING in loaded_tab_title,
1309                    msg='Loaded tab title does not contain "%s": "%s"' %
1310                        (self._TAB_TITLE_SUBSTRING, loaded_tab_title))
1311
1312    self._driver = self.NewWebDriver()
1313
1314  def testOfflineOnline(self):
1315    """Simulates user input while offline and sync while online.
1316
1317    This test alternates between a simulated "Offline" state (where user
1318    input events are queued) and an "Online" state (where user input events
1319    are dequeued, sync data is staged, and sync data is unstaged).
1320    """
1321    test_description = 'OnlineOfflineSync'
1322
1323    def scenario():
1324      # Click the "Online" button and let simulated sync run for 1 second.
1325      if not self._ClickElementByXpath(self._driver, 'id("online")'):
1326        self._num_errors += 1
1327        logging.warning('Logging an automation error: click "online" button.')
1328
1329      try:
1330        self.WaitForDomNode('id("state")[text()="online"]')
1331      except (pyauto_errors.JSONInterfaceError,
1332              pyauto_errors.JavascriptRuntimeError):
1333        self._num_errors += 1
1334        logging.warning('Logging an automation error: wait for "online".')
1335
1336      time.sleep(1)
1337
1338      # Click the "Offline" button and let user input occur for 1 second.
1339      if not self._ClickElementByXpath(self._driver, 'id("offline")'):
1340        self._num_errors += 1
1341        logging.warning('Logging an automation error: click "offline" button.')
1342
1343      try:
1344        self.WaitForDomNode('id("state")[text()="offline"]')
1345      except (pyauto_errors.JSONInterfaceError,
1346              pyauto_errors.JavascriptRuntimeError):
1347        self._num_errors += 1
1348        logging.warning('Logging an automation error: wait for "offline".')
1349
1350      time.sleep(1)
1351
1352    self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
1353                        test_description, scenario)
1354
1355
1356class ChromeEndureReplay(object):
1357  """Run Chrome Endure tests with network simulation via Web Page Replay."""
1358
1359  _PATHS = {
1360      'archive':
1361      'src/chrome/test/data/pyauto_private/webpagereplay/{archive_name}',
1362      'scripts':
1363      'src/chrome/test/data/chrome_endure/webpagereplay/wpr_deterministic.js',
1364      }
1365
1366  WEBPAGEREPLAY_HOST = '127.0.0.1'
1367  WEBPAGEREPLAY_HTTP_PORT = 8080
1368  WEBPAGEREPLAY_HTTPS_PORT = 8413
1369
1370  CHROME_FLAGS = webpagereplay.GetChromeFlags(
1371      WEBPAGEREPLAY_HOST,
1372      WEBPAGEREPLAY_HTTP_PORT,
1373      WEBPAGEREPLAY_HTTPS_PORT)
1374
1375  @classmethod
1376  def Path(cls, key, **kwargs):
1377    return perf.FormatChromePath(cls._PATHS[key], **kwargs)
1378
1379  @classmethod
1380  def ReplayServer(cls, archive_path):
1381    """Create a replay server."""
1382    # Inject customized scripts for Google webapps.
1383    # See the javascript file for details.
1384    scripts = cls.Path('scripts')
1385    if not os.path.exists(scripts):
1386      raise IOError('Injected scripts %s not found.' % scripts)
1387    replay_options = ['--inject_scripts', scripts]
1388    if 'WPR_RECORD' in os.environ:
1389      replay_options.append('--append')
1390    return webpagereplay.ReplayServer(archive_path,
1391                                      cls.WEBPAGEREPLAY_HOST,
1392                                      cls.WEBPAGEREPLAY_HTTP_PORT,
1393                                      cls.WEBPAGEREPLAY_HTTPS_PORT,
1394                                      replay_options)
1395
1396
1397if __name__ == '__main__':
1398  pyauto_functional.Main()
1399