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