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.
6 """Performance tests for Chrome Endure (long-running perf tests on Chrome).
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.
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.
24 BUILDBOT_GOT_REVISION: Similar to 'REVISION', but checked only if 'REVISION'
25 is not specified. Not mandatory.
28 from datetime import datetime
38 import pyauto_functional # Must be imported before pyauto.
42 import remote_inspector_client
43 import selenium.common.exceptions
44 from selenium.webdriver.support.ui import WebDriverWait
47 class NotSupportedEnvironmentError(RuntimeError):
48 """Represent an error raised since the environment (OS) is not supported."""
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 = ''
58 _WORKDIR_PATTERN = re.compile('endure\.[0-9]+\.[0-9]+\.[A-Za-z0-9]+')
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']
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('/'):
79 self._json_file = None
80 self._last_json_filename = ''
83 for policy in self._POLICIES:
84 self._last_time[policy] = -1.0
86 def __nonzero__(self):
90 def GetEnvironmentVariable(env_name, converter, default):
91 """Returns a converted environment variable for Deep Memory Profiler.
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.
99 A value converted from the environment variable with 'converter'.
101 return converter(os.environ.get(env_name, default))
103 def SetUp(self, is_linux, revision, gsutil):
104 """Sets up Deep Memory Profiler settings for a Chrome process.
106 It sets environment variables and makes a working directory.
108 if not self._enabled:
112 raise NotSupportedEnvironmentError(
113 'Deep Memory Profiler is not supported in this environment (OS).')
115 self._revision = revision
116 self._gsutil = gsutil
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()
124 for filename in sorted(os.listdir(tempdir), reverse=True):
125 if self._WORKDIR_PATTERN.match(filename):
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)
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'
139 """Tear down Deep Memory Profiler settings for the Chrome process.
141 It removes the environment variables and the temporary directory.
142 Call it after Chrome finishes. Chrome may dump last files at the end.
144 if not self._enabled:
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)
153 def LogFirstMessage(self):
154 """Logs first messages."""
155 if not self._enabled:
158 logging.info('Running with the Deep Memory Profiler.')
160 logging.info(' Dumped files won\'t be cleaned.')
162 logging.info(' Dumped files will be cleaned up after every test.')
164 def StartProfiler(self, proc_info, is_last, webapp_name, test_description):
165 """Starts Deep Memory Profiler in background."""
166 if not self._enabled:
169 logging.info(' Profiling with the Deep Memory Profiler...')
171 # Wait for a running dmprof process for last _GetPerformanceStat call to
172 # cover last dump files.
174 logging.info(' Waiting for the last dmprof.')
175 self._WaitForDeepMemoryProfiler()
177 if self._proc and self._proc.poll() is None:
178 logging.info(' Last dmprof is still running.')
181 self._last_json_filename = self._json_file.name
182 self._json_file.close()
183 self._json_file = None
186 for filename in sorted(os.listdir(self._workdir)):
187 if re.match('^endure.%05d.\d+.heap$' % proc_info['tab_pid'],
189 logging.info(' Profiled dump file: %s' % filename)
192 first_dump = filename
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)
206 # Wait only when it is the last profiling. dmprof may take long time.
207 self._WaitForDeepMemoryProfiler()
209 # Upload the dumped files.
210 if first_dump and self._upload and self._gsutil:
212 destination_path = '%s%s/' % (self._upload, self._revision)
214 destination_path = self._upload
215 destination_path += '%s-%s-%s.zip' % (
218 os.path.basename(self._workdir))
219 gsutil_command = '%s upload --gsutil %s %s %s' % (
220 self._DMPROF_SCRIPT_PATH,
222 os.path.join(self._workdir, first_dump),
224 logging.info('Uploading: %s' % gsutil_command)
226 returncode = subprocess.call(gsutil_command, shell=True)
227 logging.info(' Return code: %d' % returncode)
229 logging.error(' Error while uploading: %s', e)
231 logging.info('Note that the dumped files are not uploaded.')
233 logging.info(' No dump files.')
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:
242 for policy in self._POLICIES:
243 if self._last_json_filename:
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]:
254 for legend in json_data['policies'][policy]['legends']:
255 if legend == 'FROM_HERE_FOR_TOTAL':
257 elif legend == 'UNTIL_HERE_FOR_TOTAL':
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]],
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']
271 def _WaitForDeepMemoryProfiler(self):
272 """Waits for the Deep Memory Profiler to finish if running."""
273 if not self._enabled or not self._proc:
279 self._last_json_filename = self._json_file.name
280 self._json_file.close()
281 self._json_file = None
284 class ChromeEndureBaseTest(perf.BasePerfTest):
285 """Implements common functionality for all Chrome Endure tests.
287 All Chrome Endure test classes should inherit from this class.
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.
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',
305 self._gsutil = str(os.environ.get('GSUTIL', self._GSUTIL))
307 self._dmprof.SetUp(self.IsLinux(), self._revision, self._gsutil)
309 perf.BasePerfTest.setUp(self)
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))
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)
321 self._dmprof.LogFirstMessage()
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.')
329 self._test_start_time = 0
331 self._events_to_output = []
334 logging.info('Terminating connection to remote inspector...')
335 self._remote_inspector_client.Stop()
336 logging.info('Connection to remote inspector terminated.')
338 # Must be done at end of this function except for post-cleaning after
340 perf.BasePerfTest.tearDown(self)
342 # Must be done after perf.BasePerfTest.tearDown()
344 self._dmprof.TearDown()
346 def ExtraChromeFlags(self):
347 """Ensures Chrome is launched with custom flags.
350 A list of extra flags to pass to Chrome when it is launched.
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)
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
366 def _OnTimelineEvent(self, event_info):
367 """Invoked by the Remote Inspector Client when a timeline event occurs.
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.
375 elapsed_time = int(round(time.time() - self._test_start_time))
377 if event_info['type'] == 'GCEvent':
378 self._events_to_output.append({
379 'type': 'GarbageCollection',
380 'time': elapsed_time,
382 {'collected_bytes': event_info['data']['usedHeapSizeDelta']},
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.
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.
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).
408 self._test_start_time = time.time()
409 last_perf_stats_time = time.time()
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.
416 self._remote_inspector_client.StartTimelineEventMonitoring(
417 self._OnTimelineEvent)
419 while time.time() - self._test_start_time < self._test_length_sec:
420 self._iteration_num += 1
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)
427 if time.time() - last_perf_stats_time >= self._get_perf_stats_interval:
428 last_perf_stats_time = time.time()
430 self.HeapProfilerDump('renderer', 'Chrome Endure')
431 self._GetPerformanceStats(
432 webapp_name, test_description, tab_title_substring)
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))
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.
451 delete document.$wdc_;
452 window.domAutomationController.send('done');
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 '
462 self._remote_inspector_client.StopTimelineEventMonitoring()
465 self.HeapProfilerDump('renderer', 'Chrome Endure (last)')
466 self._GetPerformanceStats(
467 webapp_name, test_description, tab_title_substring, is_last=True)
469 def _GetProcessInfo(self, tab_title_substring):
470 """Gets process info associated with an open browser/tab.
473 tab_title_substring: A unique substring contained within the title of
474 the tab to use; needed for locating the tab info.
477 A dictionary containing information about the browser and specified tab
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
484 'tab_pid': integer, # Process ID of the tab process.
487 browser_process_name = (
488 self.GetBrowserInfo()['properties']['BrowserProcessExecutableName'])
489 info = self.GetProcessInfo()
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)))
503 # Get the process information associated with the specified tab.
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),
516 browser_proc_info = browser_proc_info[0]
517 tab_proc_info = tab_proc_info[0]
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'],
524 def _GetPerformanceStats(self, webapp_name, test_description,
525 tab_title_substring, is_last=False):
526 """Gets performance statistics and outputs the results.
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.
539 logging.info('Gathering performance stats...')
540 elapsed_time = int(round(time.time() - self._test_start_time))
542 memory_counts = self._remote_inspector_client.GetMemoryObjectCounts()
543 proc_info = self._GetProcessInfo(tab_title_substring)
546 self._dmprof.StartProfiler(
547 proc_info, is_last, webapp_name, test_description)
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),
556 # Event listener count.
557 event_listener_count = memory_counts['EventListenerCount']
558 self._OutputPerfGraphValue(
559 'EventListenerCount', [(elapsed_time, event_listener_count)],
561 graph_name='%s%s-EventListeners' % (webapp_name, test_description),
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),
571 # Tab process private memory.
572 self._OutputPerfGraphValue(
574 [(elapsed_time, proc_info['tab_private_mem'])], 'KB',
575 graph_name='%s%s-TabMem-Private' % (webapp_name, test_description),
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),
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),
594 self._dmprof.ParseResultAndOutputPerfGraphValues(
595 webapp_name, test_description, self._OutputPerfGraphValue)
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)
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 = []
619 logging.info('No new timeline events to log.')
621 def _GetElement(self, find_by, value):
622 """Gets a WebDriver element object from the webpage DOM.
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.
629 The identified WebDriver element object, if found in the DOM, or
633 return find_by(value)
634 except selenium.common.exceptions.NoSuchElementException:
637 def _ClickElementByXpath(self, driver, xpath):
638 """Given the xpath for a DOM element, clicks on it using WebDriver.
641 driver: A WebDriver object, as returned by self.NewWebDriver().
642 xpath: The string xpath associated with the DOM element to click.
645 True, if the DOM element was found and clicked successfully, or
649 self.WaitForDomNode(xpath)
650 except (pyauto_errors.JSONInterfaceError,
651 pyauto_errors.JavascriptRuntimeError) as e:
652 logging.exception('PyAuto exception: %s' % e)
656 element = self._GetElement(driver.find_element_by_xpath, xpath)
658 except (selenium.common.exceptions.StaleElementReferenceException,
659 selenium.common.exceptions.TimeoutException) as e:
660 logging.exception('WebDriver exception: %s' % e)
666 class ChromeEndureControlTest(ChromeEndureBaseTest):
667 """Control tests for Chrome Endure."""
669 _WEBAPP_NAME = 'Control'
670 _TAB_TITLE_SUBSTRING = 'Chrome Endure Control Test'
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))
683 # Just sleep. Javascript in the webpage itself does the work.
686 self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
687 test_description, scenario)
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))
700 driver = self.NewWebDriver()
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")')
708 self._ClickElementByXpath(driver, 'id("detach")')
711 self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
712 test_description, lambda: scenario(driver))
715 class IndexedDBOfflineTest(ChromeEndureBaseTest):
716 """Long-running performance tests for IndexedDB, modeling offline usage."""
718 _WEBAPP_NAME = 'IndexedDBOffline'
719 _TAB_TITLE_SUBSTRING = 'IndexedDB Offline'
722 ChromeEndureBaseTest.setUp(self)
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))
731 self._driver = self.NewWebDriver()
733 def testOfflineOnline(self):
734 """Simulates user input while offline and sync while online.
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).
740 test_description = 'OnlineOfflineSync'
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.')
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".')
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.')
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".')
771 self._RunEndureTest(self._WEBAPP_NAME, self._TAB_TITLE_SUBSTRING,
772 test_description, scenario)
775 if __name__ == '__main__':
776 pyauto_functional.Main()