Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / test / functional / perf_endure.py
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
8 This 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
13 The 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
28 from datetime import datetime
29 import json
30 import logging
31 import os
32 import re
33 import subprocess
34 import tempfile
35 import time
36
37 import perf
38 import pyauto_functional  # Must be imported before pyauto.
39 import pyauto
40 import pyauto_errors
41 import pyauto_utils
42 import remote_inspector_client
43 import selenium.common.exceptions
44 from selenium.webdriver.support.ui import WebDriverWait
45
46
47 class NotSupportedEnvironmentError(RuntimeError):
48   """Represent an error raised since the environment (OS) is not supported."""
49   pass
50
51
52 class DeepMemoryProfiler(object):
53   """Controls Deep Memory Profiler (dmprof) for endurance tests."""
54   DEEP_MEMORY_PROFILE = False
55   DEEP_MEMORY_PROFILE_SAVE = False
56   DEEP_MEMORY_PROFILE_UPLOAD = ''
57
58   _WORKDIR_PATTERN = re.compile('endure\.[0-9]+\.[0-9]+\.[A-Za-z0-9]+')
59   _SAVED_WORKDIRS = 8
60
61   _DMPROF_DIR_PATH = os.path.abspath(os.path.join(
62       os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
63       'tools', 'deep_memory_profiler'))
64   _DMPROF_SCRIPT_PATH = os.path.join(_DMPROF_DIR_PATH, 'dmprof')
65   _POLICIES = ['l0', 'l1', 'l2', 't0']
66
67   def __init__(self):
68     self._enabled = self.GetEnvironmentVariable(
69         'DEEP_MEMORY_PROFILE', bool, self.DEEP_MEMORY_PROFILE)
70     self._save = self.GetEnvironmentVariable(
71         'DEEP_MEMORY_PROFILE_SAVE', bool, self.DEEP_MEMORY_PROFILE_SAVE)
72     self._upload = self.GetEnvironmentVariable(
73         'DEEP_MEMORY_PROFILE_UPLOAD', str, self.DEEP_MEMORY_PROFILE_UPLOAD)
74     if self._upload and not self._upload.endswith('/'):
75       self._upload += '/'
76
77     self._revision = ''
78     self._gsutil = ''
79     self._json_file = None
80     self._last_json_filename = ''
81     self._proc = None
82     self._last_time = {}
83     for policy in self._POLICIES:
84       self._last_time[policy] = -1.0
85
86   def __nonzero__(self):
87     return self._enabled
88
89   @staticmethod
90   def GetEnvironmentVariable(env_name, converter, default):
91     """Returns a converted environment variable for Deep Memory Profiler.
92
93     Args:
94       env_name: A string name of an environment variable.
95       converter: A function taking a string to convert an environment variable.
96       default: A value used if the environment variable is not specified.
97
98     Returns:
99       A value converted from the environment variable with 'converter'.
100     """
101     return converter(os.environ.get(env_name, default))
102
103   def SetUp(self, is_linux, revision, gsutil):
104     """Sets up Deep Memory Profiler settings for a Chrome process.
105
106     It sets environment variables and makes a working directory.
107     """
108     if not self._enabled:
109       return
110
111     if not is_linux:
112       raise NotSupportedEnvironmentError(
113           'Deep Memory Profiler is not supported in this environment (OS).')
114
115     self._revision = revision
116     self._gsutil = gsutil
117
118     # Remove old dumped files with keeping latest _SAVED_WORKDIRS workdirs.
119     # It keeps the latest workdirs not to miss data by failure in uploading
120     # and other operations.  Dumped files are no longer available if they are
121     # removed.  Re-execution doesn't generate the same files.
122     tempdir = tempfile.gettempdir()
123     saved_workdirs = 0
124     for filename in sorted(os.listdir(tempdir), reverse=True):
125       if self._WORKDIR_PATTERN.match(filename):
126         saved_workdirs += 1
127         if saved_workdirs > self._SAVED_WORKDIRS:
128           fullpath = os.path.abspath(os.path.join(tempdir, filename))
129           logging.info('Removing an old workdir: %s' % fullpath)
130           pyauto_utils.RemovePath(fullpath)
131
132     dir_prefix = 'endure.%s.' % datetime.today().strftime('%Y%m%d.%H%M%S')
133     self._workdir = tempfile.mkdtemp(prefix=dir_prefix, dir=tempdir)
134     os.environ['HEAPPROFILE'] = os.path.join(self._workdir, 'endure')
135     os.environ['HEAP_PROFILE_MMAP'] = '1'
136     os.environ['DEEP_HEAP_PROFILE'] = '1'
137
138   def TearDown(self):
139     """Tear down Deep Memory Profiler settings for the Chrome process.
140
141     It removes the environment variables and the temporary directory.
142     Call it after Chrome finishes.  Chrome may dump last files at the end.
143     """
144     if not self._enabled:
145       return
146
147     del os.environ['DEEP_HEAP_PROFILE']
148     del os.environ['HEAP_PROFILE_MMAP']
149     del os.environ['HEAPPROFILE']
150     if not self._save and self._workdir:
151       pyauto_utils.RemovePath(self._workdir)
152
153   def LogFirstMessage(self):
154     """Logs first messages."""
155     if not self._enabled:
156       return
157
158     logging.info('Running with the Deep Memory Profiler.')
159     if self._save:
160       logging.info('  Dumped files won\'t be cleaned.')
161     else:
162       logging.info('  Dumped files will be cleaned up after every test.')
163
164   def StartProfiler(self, proc_info, is_last, webapp_name, test_description):
165     """Starts Deep Memory Profiler in background."""
166     if not self._enabled:
167       return
168
169     logging.info('  Profiling with the Deep Memory Profiler...')
170
171     # Wait for a running dmprof process for last _GetPerformanceStat call to
172     # cover last dump files.
173     if is_last:
174       logging.info('    Waiting for the last dmprof.')
175       self._WaitForDeepMemoryProfiler()
176
177     if self._proc and self._proc.poll() is None:
178       logging.info('    Last dmprof is still running.')
179     else:
180       if self._json_file:
181         self._last_json_filename = self._json_file.name
182         self._json_file.close()
183         self._json_file = None
184       first_dump = ''
185       last_dump = ''
186       for filename in sorted(os.listdir(self._workdir)):
187         if re.match('^endure.%05d.\d+.heap$' % proc_info['tab_pid'],
188                     filename):
189           logging.info('    Profiled dump file: %s' % filename)
190           last_dump = filename
191           if not first_dump:
192             first_dump = filename
193       if first_dump:
194         logging.info('    First dump file: %s' % first_dump)
195         matched = re.match('^endure.\d+.(\d+).heap$', last_dump)
196         last_sequence_id = matched.group(1)
197         self._json_file = open(
198             os.path.join(self._workdir,
199                          'endure.%05d.%s.json' % (proc_info['tab_pid'],
200                                                   last_sequence_id)), 'w+')
201         self._proc = subprocess.Popen(
202             '%s json %s' % (self._DMPROF_SCRIPT_PATH,
203                             os.path.join(self._workdir, first_dump)),
204             shell=True, stdout=self._json_file)
205         if is_last:
206           # Wait only when it is the last profiling.  dmprof may take long time.
207           self._WaitForDeepMemoryProfiler()
208
209           # Upload the dumped files.
210           if first_dump and self._upload and self._gsutil:
211             if self._revision:
212               destination_path = '%s%s/' % (self._upload, self._revision)
213             else:
214               destination_path = self._upload
215             destination_path += '%s-%s-%s.zip' % (
216                 webapp_name,
217                 test_description,
218                 os.path.basename(self._workdir))
219             gsutil_command = '%s upload --gsutil %s %s %s' % (
220                 self._DMPROF_SCRIPT_PATH,
221                 self._gsutil,
222                 os.path.join(self._workdir, first_dump),
223                 destination_path)
224             logging.info('Uploading: %s' % gsutil_command)
225             try:
226               returncode = subprocess.call(gsutil_command, shell=True)
227               logging.info('  Return code: %d' % returncode)
228             except OSError, e:
229               logging.error('  Error while uploading: %s', e)
230           else:
231             logging.info('Note that the dumped files are not uploaded.')
232       else:
233         logging.info('    No dump files.')
234
235   def ParseResultAndOutputPerfGraphValues(
236       self, webapp_name, test_description, output_perf_graph_value):
237     """Parses Deep Memory Profiler result, and outputs perf graph values."""
238     if not self._enabled:
239       return
240
241     results = {}
242     for policy in self._POLICIES:
243       if self._last_json_filename:
244         json_data = {}
245         with open(self._last_json_filename) as json_f:
246           json_data = json.load(json_f)
247         if json_data['version'] == 'JSON_DEEP_1':
248           results[policy] = json_data['snapshots']
249         elif json_data['version'] == 'JSON_DEEP_2':
250           results[policy] = json_data['policies'][policy]['snapshots']
251     for policy, result in results.iteritems():
252       if result and result[-1]['second'] > self._last_time[policy]:
253         started = False
254         for legend in json_data['policies'][policy]['legends']:
255           if legend == 'FROM_HERE_FOR_TOTAL':
256             started = True
257           elif legend == 'UNTIL_HERE_FOR_TOTAL':
258             break
259           elif started:
260             output_perf_graph_value(
261                 legend.encode('utf-8'), [
262                     (int(round(snapshot['second'])), snapshot[legend] / 1024)
263                     for snapshot in result
264                     if snapshot['second'] > self._last_time[policy]],
265                 'KB',
266                 graph_name='%s%s-%s-DMP' % (
267                     webapp_name, test_description, policy),
268                 units_x='seconds', is_stacked=True)
269         self._last_time[policy] = result[-1]['second']
270
271   def _WaitForDeepMemoryProfiler(self):
272     """Waits for the Deep Memory Profiler to finish if running."""
273     if not self._enabled or not self._proc:
274       return
275
276     self._proc.wait()
277     self._proc = None
278     if self._json_file:
279       self._last_json_filename = self._json_file.name
280       self._json_file.close()
281       self._json_file = None
282
283
284 class ChromeEndureBaseTest(perf.BasePerfTest):
285   """Implements common functionality for all Chrome Endure tests.
286
287   All Chrome Endure test classes should inherit from this class.
288   """
289
290   _DEFAULT_TEST_LENGTH_SEC = 60 * 60 * 6  # Tests run for 6 hours.
291   _GET_PERF_STATS_INTERVAL = 60 * 5  # Measure perf stats every 5 minutes.
292   # TODO(dennisjeffrey): Do we still need to tolerate errors?
293   _ERROR_COUNT_THRESHOLD = 50  # Number of errors to tolerate.
294   _REVISION = ''
295   _GSUTIL = 'gsutil'
296
297   def setUp(self):
298     # The environment variables for the Deep Memory Profiler must be set
299     # before perf.BasePerfTest.setUp() to inherit them to Chrome.
300     self._dmprof = DeepMemoryProfiler()
301     self._revision = str(os.environ.get('REVISION', self._REVISION))
302     if not self._revision:
303       self._revision = str(os.environ.get('BUILDBOT_GOT_REVISION',
304                                           self._REVISION))
305     self._gsutil = str(os.environ.get('GSUTIL', self._GSUTIL))
306     if self._dmprof:
307       self._dmprof.SetUp(self.IsLinux(), self._revision, self._gsutil)
308
309     perf.BasePerfTest.setUp(self)
310
311     self._test_length_sec = int(
312         os.environ.get('TEST_LENGTH', self._DEFAULT_TEST_LENGTH_SEC))
313     self._get_perf_stats_interval = int(
314         os.environ.get('PERF_STATS_INTERVAL', self._GET_PERF_STATS_INTERVAL))
315
316     logging.info('Running test for %d seconds.', self._test_length_sec)
317     logging.info('Gathering perf stats every %d seconds.',
318                  self._get_perf_stats_interval)
319
320     if self._dmprof:
321       self._dmprof.LogFirstMessage()
322
323     # Set up a remote inspector client associated with tab 0.
324     logging.info('Setting up connection to remote inspector...')
325     self._remote_inspector_client = (
326         remote_inspector_client.RemoteInspectorClient())
327     logging.info('Connection to remote inspector set up successfully.')
328
329     self._test_start_time = 0
330     self._num_errors = 0
331     self._events_to_output = []
332
333   def tearDown(self):
334     logging.info('Terminating connection to remote inspector...')
335     self._remote_inspector_client.Stop()
336     logging.info('Connection to remote inspector terminated.')
337
338     # Must be done at end of this function except for post-cleaning after
339     # Chrome finishes.
340     perf.BasePerfTest.tearDown(self)
341
342     # Must be done after perf.BasePerfTest.tearDown()
343     if self._dmprof:
344       self._dmprof.TearDown()
345
346   def ExtraChromeFlags(self):
347     """Ensures Chrome is launched with custom flags.
348
349     Returns:
350       A list of extra flags to pass to Chrome when it is launched.
351     """
352     # The same with setUp, but need to fetch the environment variable since
353     # ExtraChromeFlags is called before setUp.
354     deep_memory_profile = DeepMemoryProfiler.GetEnvironmentVariable(
355         'DEEP_MEMORY_PROFILE', bool, DeepMemoryProfiler.DEEP_MEMORY_PROFILE)
356
357     # Ensure Chrome enables remote debugging on port 9222.  This is required to
358     # interact with Chrome's remote inspector.
359     # Also, enable the memory benchmarking V8 extension for heap dumps.
360     extra_flags = ['--remote-debugging-port=9222',
361                    '--enable-memory-benchmarking']
362     if deep_memory_profile:
363       extra_flags.append('--no-sandbox')
364     return perf.BasePerfTest.ExtraChromeFlags(self) + extra_flags
365
366   def _OnTimelineEvent(self, event_info):
367     """Invoked by the Remote Inspector Client when a timeline event occurs.
368
369     Args:
370       event_info: A dictionary containing raw information associated with a
371          timeline event received from Chrome's remote inspector.  Refer to
372          chrome/src/third_party/WebKit/Source/WebCore/inspector/Inspector.json
373          for the format of this dictionary.
374     """
375     elapsed_time = int(round(time.time() - self._test_start_time))
376
377     if event_info['type'] == 'GCEvent':
378       self._events_to_output.append({
379         'type': 'GarbageCollection',
380         'time': elapsed_time,
381         'data':
382             {'collected_bytes': event_info['data']['usedHeapSizeDelta']},
383       })
384
385   def _RunEndureTest(self, webapp_name, tab_title_substring, test_description,
386                      do_scenario, frame_xpath=''):
387     """The main test harness function to run a general Chrome Endure test.
388
389     After a test has performed any setup work and has navigated to the proper
390     starting webpage, this function should be invoked to run the endurance test.
391
392     Args:
393       webapp_name: A string name for the webapp being tested.  Should not
394           include spaces.  For example, 'Gmail', 'Docs', or 'Plus'.
395       tab_title_substring: A unique substring contained within the title of
396           the tab to use, for identifying the appropriate tab.
397       test_description: A string description of what the test does, used for
398           outputting results to be graphed.  Should not contain spaces.  For
399           example, 'ComposeDiscard' for Gmail.
400       do_scenario: A callable to be invoked that implements the scenario to be
401           performed by this test.  The callable is invoked iteratively for the
402           duration of the test.
403       frame_xpath: The string xpath of the frame in which to inject javascript
404           to clear chromedriver's cache (a temporary workaround until the
405           WebDriver team changes how they handle their DOM node cache).
406     """
407     self._num_errors = 0
408     self._test_start_time = time.time()
409     last_perf_stats_time = time.time()
410     if self._dmprof:
411       self.HeapProfilerDump('renderer', 'Chrome Endure (first)')
412     self._GetPerformanceStats(
413         webapp_name, test_description, tab_title_substring)
414     self._iteration_num = 0  # Available to |do_scenario| if needed.
415
416     self._remote_inspector_client.StartTimelineEventMonitoring(
417         self._OnTimelineEvent)
418
419     while time.time() - self._test_start_time < self._test_length_sec:
420       self._iteration_num += 1
421
422       if self._num_errors >= self._ERROR_COUNT_THRESHOLD:
423         logging.error('Error count threshold (%d) reached. Terminating test '
424                       'early.' % self._ERROR_COUNT_THRESHOLD)
425         break
426
427       if time.time() - last_perf_stats_time >= self._get_perf_stats_interval:
428         last_perf_stats_time = time.time()
429         if self._dmprof:
430           self.HeapProfilerDump('renderer', 'Chrome Endure')
431         self._GetPerformanceStats(
432             webapp_name, test_description, tab_title_substring)
433
434       if self._iteration_num % 10 == 0:
435         remaining_time = self._test_length_sec - (time.time() -
436                                                   self._test_start_time)
437         logging.info('Chrome interaction #%d. Time remaining in test: %d sec.' %
438                      (self._iteration_num, remaining_time))
439
440       do_scenario()
441       # Clear ChromeDriver's DOM node cache so its growth doesn't affect the
442       # results of Chrome Endure.
443       # TODO(dennisjeffrey): Once the WebDriver team implements changes to
444       # handle their DOM node cache differently, we need to revisit this.  It
445       # may no longer be necessary at that point to forcefully delete the cache.
446       # Additionally, the Javascript below relies on an internal property of
447       # WebDriver that may change at any time.  This is only a temporary
448       # workaround to stabilize the Chrome Endure test results.
449       js = """
450         (function() {
451           delete document.$wdc_;
452           window.domAutomationController.send('done');
453         })();
454       """
455       try:
456         self.ExecuteJavascript(js, frame_xpath=frame_xpath)
457       except pyauto_errors.AutomationCommandTimeout:
458         self._num_errors += 1
459         logging.warning('Logging an automation timeout: delete chromedriver '
460                         'cache.')
461
462     self._remote_inspector_client.StopTimelineEventMonitoring()
463
464     if self._dmprof:
465       self.HeapProfilerDump('renderer', 'Chrome Endure (last)')
466     self._GetPerformanceStats(
467         webapp_name, test_description, tab_title_substring, is_last=True)
468
469   def _GetProcessInfo(self, tab_title_substring):
470     """Gets process info associated with an open browser/tab.
471
472     Args:
473       tab_title_substring: A unique substring contained within the title of
474           the tab to use; needed for locating the tab info.
475
476     Returns:
477       A dictionary containing information about the browser and specified tab
478       process:
479       {
480         'browser_private_mem': integer,  # Private memory associated with the
481                                          # browser process, in KB.
482         'tab_private_mem': integer,  # Private memory associated with the tab
483                                      # process, in KB.
484         'tab_pid': integer,  # Process ID of the tab process.
485       }
486     """
487     browser_process_name = (
488         self.GetBrowserInfo()['properties']['BrowserProcessExecutableName'])
489     info = self.GetProcessInfo()
490
491     # Get the information associated with the browser process.
492     browser_proc_info = []
493     for browser_info in info['browsers']:
494       if browser_info['process_name'] == browser_process_name:
495         for proc_info in browser_info['processes']:
496           if proc_info['child_process_type'] == 'Browser':
497             browser_proc_info.append(proc_info)
498     self.assertEqual(len(browser_proc_info), 1,
499                      msg='Expected to find 1 Chrome browser process, but found '
500                          '%d instead.\nCurrent process info:\n%s.' % (
501                          len(browser_proc_info), self.pformat(info)))
502
503     # Get the process information associated with the specified tab.
504     tab_proc_info = []
505     for browser_info in info['browsers']:
506       for proc_info in browser_info['processes']:
507         if (proc_info['child_process_type'] == 'Tab' and
508             [x for x in proc_info['titles'] if tab_title_substring in x]):
509           tab_proc_info.append(proc_info)
510     self.assertEqual(len(tab_proc_info), 1,
511                      msg='Expected to find 1 %s tab process, but found %d '
512                          'instead.\nCurrent process info:\n%s.' % (
513                          tab_title_substring, len(tab_proc_info),
514                          self.pformat(info)))
515
516     browser_proc_info = browser_proc_info[0]
517     tab_proc_info = tab_proc_info[0]
518     return {
519       'browser_private_mem': browser_proc_info['working_set_mem']['priv'],
520       'tab_private_mem': tab_proc_info['working_set_mem']['priv'],
521       'tab_pid': tab_proc_info['pid'],
522     }
523
524   def _GetPerformanceStats(self, webapp_name, test_description,
525                            tab_title_substring, is_last=False):
526     """Gets performance statistics and outputs the results.
527
528     Args:
529       webapp_name: A string name for the webapp being tested.  Should not
530           include spaces.  For example, 'Gmail', 'Docs', or 'Plus'.
531       test_description: A string description of what the test does, used for
532           outputting results to be graphed.  Should not contain spaces.  For
533           example, 'ComposeDiscard' for Gmail.
534       tab_title_substring: A unique substring contained within the title of
535           the tab to use, for identifying the appropriate tab.
536       is_last: A boolean value which should be True if it's the last call of
537           _GetPerformanceStats.  The default is False.
538     """
539     logging.info('Gathering performance stats...')
540     elapsed_time = int(round(time.time() - self._test_start_time))
541
542     memory_counts = self._remote_inspector_client.GetMemoryObjectCounts()
543     proc_info = self._GetProcessInfo(tab_title_substring)
544
545     if self._dmprof:
546       self._dmprof.StartProfiler(
547           proc_info, is_last, webapp_name, test_description)
548
549     # DOM node count.
550     dom_node_count = memory_counts['DOMNodeCount']
551     self._OutputPerfGraphValue(
552         'TotalDOMNodeCount', [(elapsed_time, dom_node_count)], 'nodes',
553         graph_name='%s%s-Nodes-DOM' % (webapp_name, test_description),
554         units_x='seconds')
555
556     # Event listener count.
557     event_listener_count = memory_counts['EventListenerCount']
558     self._OutputPerfGraphValue(
559         'EventListenerCount', [(elapsed_time, event_listener_count)],
560         'listeners',
561         graph_name='%s%s-EventListeners' % (webapp_name, test_description),
562         units_x='seconds')
563
564     # Browser process private memory.
565     self._OutputPerfGraphValue(
566         'BrowserPrivateMemory',
567         [(elapsed_time, proc_info['browser_private_mem'])], 'KB',
568         graph_name='%s%s-BrowserMem-Private' % (webapp_name, test_description),
569         units_x='seconds')
570
571     # Tab process private memory.
572     self._OutputPerfGraphValue(
573         'TabPrivateMemory',
574         [(elapsed_time, proc_info['tab_private_mem'])], 'KB',
575         graph_name='%s%s-TabMem-Private' % (webapp_name, test_description),
576         units_x='seconds')
577
578     # V8 memory used.
579     v8_info = self.GetV8HeapStats()  # First window, first tab.
580     v8_mem_used = v8_info['v8_memory_used'] / 1024.0  # Convert to KB.
581     self._OutputPerfGraphValue(
582         'V8MemoryUsed', [(elapsed_time, v8_mem_used)], 'KB',
583         graph_name='%s%s-V8MemUsed' % (webapp_name, test_description),
584         units_x='seconds')
585
586     # V8 memory allocated.
587     v8_mem_allocated = v8_info['v8_memory_allocated'] / 1024.0  # Convert to KB.
588     self._OutputPerfGraphValue(
589         'V8MemoryAllocated', [(elapsed_time, v8_mem_allocated)], 'KB',
590         graph_name='%s%s-V8MemAllocated' % (webapp_name, test_description),
591         units_x='seconds')
592
593     if self._dmprof:
594       self._dmprof.ParseResultAndOutputPerfGraphValues(
595           webapp_name, test_description, self._OutputPerfGraphValue)
596
597     logging.info('  Total DOM node count: %d nodes' % dom_node_count)
598     logging.info('  Event listener count: %d listeners' % event_listener_count)
599     logging.info('  Browser process private memory: %d KB' %
600                  proc_info['browser_private_mem'])
601     logging.info('  Tab process private memory: %d KB' %
602                  proc_info['tab_private_mem'])
603     logging.info('  V8 memory used: %f KB' % v8_mem_used)
604     logging.info('  V8 memory allocated: %f KB' % v8_mem_allocated)
605
606     # Output any new timeline events that have occurred.
607     if self._events_to_output:
608       logging.info('Logging timeline events...')
609       event_type_to_value_list = {}
610       for event_info in self._events_to_output:
611         if not event_info['type'] in event_type_to_value_list:
612           event_type_to_value_list[event_info['type']] = []
613         event_type_to_value_list[event_info['type']].append(
614             (event_info['time'], event_info['data']))
615       for event_type, value_list in event_type_to_value_list.iteritems():
616         self._OutputEventGraphValue(event_type, value_list)
617       self._events_to_output = []
618     else:
619       logging.info('No new timeline events to log.')
620
621   def _GetElement(self, find_by, value):
622     """Gets a WebDriver element object from the webpage DOM.
623
624     Args:
625       find_by: A callable that queries WebDriver for an element from the DOM.
626       value: A string value that can be passed to the |find_by| callable.
627
628     Returns:
629       The identified WebDriver element object, if found in the DOM, or
630       None, otherwise.
631     """
632     try:
633       return find_by(value)
634     except selenium.common.exceptions.NoSuchElementException:
635       return None
636
637   def _ClickElementByXpath(self, driver, xpath):
638     """Given the xpath for a DOM element, clicks on it using WebDriver.
639
640     Args:
641       driver: A WebDriver object, as returned by self.NewWebDriver().
642       xpath: The string xpath associated with the DOM element to click.
643
644     Returns:
645       True, if the DOM element was found and clicked successfully, or
646       False, otherwise.
647     """
648     try:
649       self.WaitForDomNode(xpath)
650     except (pyauto_errors.JSONInterfaceError,
651             pyauto_errors.JavascriptRuntimeError) as e:
652       logging.exception('PyAuto exception: %s' % e)
653       return False
654
655     try:
656       element = self._GetElement(driver.find_element_by_xpath, xpath)
657       element.click()
658     except (selenium.common.exceptions.StaleElementReferenceException,
659             selenium.common.exceptions.TimeoutException) as e:
660       logging.exception('WebDriver exception: %s' % e)
661       return False
662
663     return True
664
665
666 class ChromeEndureControlTest(ChromeEndureBaseTest):
667   """Control tests for Chrome Endure."""
668
669   _WEBAPP_NAME = 'Control'
670   _TAB_TITLE_SUBSTRING = 'Chrome Endure Control Test'
671
672   def testControlAttachDetachDOMTree(self):
673     """Continually attach and detach a DOM tree from a basic document."""
674     test_description = 'AttachDetachDOMTree'
675     url = self.GetHttpURLForDataPath('chrome_endure', 'endurance_control.html')
676     self.NavigateToURL(url)
677     loaded_tab_title = self.GetActiveTabTitle()
678     self.assertTrue(self._TAB_TITLE_SUBSTRING in loaded_tab_title,
679                     msg='Loaded tab title does not contain "%s": "%s"' %
680                         (self._TAB_TITLE_SUBSTRING, loaded_tab_title))
681
682     def scenario():
683       # Just sleep.  Javascript in the webpage itself does the work.
684       time.sleep(5)
685
686     self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
687                         test_description, scenario)
688
689   def testControlAttachDetachDOMTreeWebDriver(self):
690     """Use WebDriver to attach and detach a DOM tree from a basic document."""
691     test_description = 'AttachDetachDOMTreeWebDriver'
692     url = self.GetHttpURLForDataPath('chrome_endure',
693                                      'endurance_control_webdriver.html')
694     self.NavigateToURL(url)
695     loaded_tab_title = self.GetActiveTabTitle()
696     self.assertTrue(self._TAB_TITLE_SUBSTRING in loaded_tab_title,
697                     msg='Loaded tab title does not contain "%s": "%s"' %
698                         (self._TAB_TITLE_SUBSTRING, loaded_tab_title))
699
700     driver = self.NewWebDriver()
701
702     def scenario(driver):
703       # Click the "attach" button to attach a large DOM tree (with event
704       # listeners) to the document, wait half a second, click "detach" to detach
705       # the DOM tree from the document, wait half a second.
706       self._ClickElementByXpath(driver, 'id("attach")')
707       time.sleep(0.5)
708       self._ClickElementByXpath(driver, 'id("detach")')
709       time.sleep(0.5)
710
711     self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
712                         test_description, lambda: scenario(driver))
713
714
715 class IndexedDBOfflineTest(ChromeEndureBaseTest):
716   """Long-running performance tests for IndexedDB, modeling offline usage."""
717
718   _WEBAPP_NAME = 'IndexedDBOffline'
719   _TAB_TITLE_SUBSTRING = 'IndexedDB Offline'
720
721   def setUp(self):
722     ChromeEndureBaseTest.setUp(self)
723
724     url = self.GetHttpURLForDataPath('indexeddb', 'endure', 'app.html')
725     self.NavigateToURL(url)
726     loaded_tab_title = self.GetActiveTabTitle()
727     self.assertTrue(self._TAB_TITLE_SUBSTRING in loaded_tab_title,
728                     msg='Loaded tab title does not contain "%s": "%s"' %
729                         (self._TAB_TITLE_SUBSTRING, loaded_tab_title))
730
731     self._driver = self.NewWebDriver()
732
733   def testOfflineOnline(self):
734     """Simulates user input while offline and sync while online.
735
736     This test alternates between a simulated "Offline" state (where user
737     input events are queued) and an "Online" state (where user input events
738     are dequeued, sync data is staged, and sync data is unstaged).
739     """
740     test_description = 'OnlineOfflineSync'
741
742     def scenario():
743       # Click the "Online" button and let simulated sync run for 1 second.
744       if not self._ClickElementByXpath(self._driver, 'id("online")'):
745         self._num_errors += 1
746         logging.warning('Logging an automation error: click "online" button.')
747
748       try:
749         self.WaitForDomNode('id("state")[text()="online"]')
750       except (pyauto_errors.JSONInterfaceError,
751               pyauto_errors.JavascriptRuntimeError):
752         self._num_errors += 1
753         logging.warning('Logging an automation error: wait for "online".')
754
755       time.sleep(1)
756
757       # Click the "Offline" button and let user input occur for 1 second.
758       if not self._ClickElementByXpath(self._driver, 'id("offline")'):
759         self._num_errors += 1
760         logging.warning('Logging an automation error: click "offline" button.')
761
762       try:
763         self.WaitForDomNode('id("state")[text()="offline"]')
764       except (pyauto_errors.JSONInterfaceError,
765               pyauto_errors.JavascriptRuntimeError):
766         self._num_errors += 1
767         logging.warning('Logging an automation error: wait for "offline".')
768
769       time.sleep(1)
770
771     self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
772                         test_description, scenario)
773
774
775 if __name__ == '__main__':
776   pyauto_functional.Main()