Home | History | Annotate | Download | only in cros
      1 import logging
      2 import os
      3 import time
      4 
      5 from autotest_lib.client.bin import utils
      6 from autotest_lib.client.common_lib import error
      7 from autotest_lib.client.common_lib.cros import chrome
      8 from autotest_lib.client.common_lib.cros import system_metrics_collector
      9 from autotest_lib.client.common_lib.cros import webrtc_utils
     10 from autotest_lib.client.cros.graphics import graphics_utils
     11 from autotest_lib.client.cros.multimedia import system_facade_native
     12 from autotest_lib.client.cros.video import helper_logger
     13 from telemetry.core import exceptions
     14 from telemetry.util import image_util
     15 
     16 
     17 EXTRA_BROWSER_ARGS = ['--use-fake-ui-for-media-stream',
     18                       '--use-fake-device-for-media-stream']
     19 
     20 
     21 class WebRtcPeerConnectionTest(object):
     22     """
     23     Runs a WebRTC peer connection test.
     24 
     25     This class runs a test that uses WebRTC peer connections to stress Chrome
     26     and WebRTC. It interacts with HTML and JS files that contain the actual test
     27     logic. It makes many assumptions about how these files behave. See one of
     28     the existing tests and the documentation for run_test() for reference.
     29     """
     30     def __init__(
     31             self,
     32             title,
     33             own_script,
     34             common_script,
     35             bindir,
     36             tmpdir,
     37             debugdir,
     38             timeout = 70,
     39             test_runtime_seconds = 60,
     40             num_peer_connections = 5,
     41             iteration_delay_millis = 500,
     42             before_start_hook = None):
     43         """
     44         Sets up a peer connection test.
     45 
     46         @param title: Title of the test, shown on the test HTML page.
     47         @param own_script: Name of the test's own JS file in bindir.
     48         @param tmpdir: Directory to store tmp files, should be in the autotest
     49                 tree.
     50         @param bindir: The directory that contains the test files and
     51                 own_script.
     52         @param debugdir: The directory to which debug data, e.g. screenshots,
     53                 should be written.
     54         @param timeout: Timeout in seconds for the test.
     55         @param test_runtime_seconds: How long to run the test. If errors occur
     56                 the test can exit earlier.
     57         @param num_peer_connections: Number of peer connections to use.
     58         @param iteration_delay_millis: delay in millis between each test
     59                 iteration.
     60         @param before_start_hook: function accepting a Chrome browser tab as
     61                 argument. Is executed before the startTest() JS method call is
     62                 made.
     63         """
     64         self.title = title
     65         self.own_script = own_script
     66         self.common_script = common_script
     67         self.bindir = bindir
     68         self.tmpdir = tmpdir
     69         self.debugdir = debugdir
     70         self.timeout = timeout
     71         self.test_runtime_seconds = test_runtime_seconds
     72         self.num_peer_connections = num_peer_connections
     73         self.iteration_delay_millis = iteration_delay_millis
     74         self.before_start_hook = before_start_hook
     75         self.tab = None
     76 
     77     def start_test(self, cr, html_file):
     78         """
     79         Opens the test page.
     80 
     81         @param cr: Autotest Chrome instance.
     82         @param html_file: File object containing the HTML code to use in the
     83                 test. The html file needs to have the following JS methods:
     84                 startTest(runtimeSeconds, numPeerConnections, iterationDelay)
     85                         Starts the test. Arguments are all numbers.
     86                 getStatus()
     87                         Gets the status of the test. Returns a string with the
     88                         failure message. If the string starts with 'failure', it
     89                         is interpreted as failure. The string 'ok-done' denotes
     90                         that the test is complete. This method should not throw
     91                         an exception.
     92         """
     93         self.tab = cr.browser.tabs[0]
     94         self.tab.Navigate(cr.browser.platform.http_server.UrlOf(
     95                 os.path.join(self.bindir, html_file.name)))
     96         self.tab.WaitForDocumentReadyStateToBeComplete()
     97         if self.before_start_hook is not None:
     98             self.before_start_hook(self.tab)
     99         self.tab.EvaluateJavaScript(
    100                 "startTest(%d, %d, %d)" % (
    101                         self.test_runtime_seconds,
    102                         self.num_peer_connections,
    103                         self.iteration_delay_millis))
    104 
    105     def _test_done(self):
    106         """
    107         Determines if the test is done or not.
    108 
    109         Does so by querying status of the JavaScript test runner.
    110         @return True if the test is done, false if it is still in progress.
    111         @raise TestFail if the status check returns a failure status.
    112         """
    113         status = self.tab.EvaluateJavaScript('getStatus()')
    114         if status.startswith('failure'):
    115             raise error.TestFail(
    116                     'Test status starts with failure, status is: ' + status)
    117         logging.debug(status)
    118         return status == 'ok-done'
    119 
    120     def wait_test_completed(self, timeout_secs):
    121         """
    122         Waits until the test is done.
    123 
    124         @param timeout_secs Max time to wait in seconds.
    125 
    126         @raises TestError on timeout, or javascript eval fails, or
    127                 error status from the getStatus() JS method.
    128         """
    129         start_secs = time.time()
    130         while not self._test_done():
    131             spent_time = time.time() - start_secs
    132             if spent_time > timeout_secs:
    133                 raise utils.TimeoutError(
    134                         'Test timed out after {} seconds'.format(spent_time))
    135             self.do_in_wait_loop()
    136 
    137     def do_in_wait_loop(self):
    138         """
    139         Called repeatedly in a loop while the test waits for completion.
    140 
    141         Subclasses can override and provide specific behavior.
    142         """
    143         time.sleep(1)
    144 
    145     @helper_logger.video_log_wrapper
    146     def run_test(self):
    147         """
    148         Starts the test and waits until it is completed.
    149         """
    150         with chrome.Chrome(extra_browser_args = EXTRA_BROWSER_ARGS + \
    151                            [helper_logger.chrome_vmodule_flag()],
    152                            init_network_controller = True) as cr:
    153             own_script_path = os.path.join(
    154                     self.bindir, self.own_script)
    155             common_script_path = webrtc_utils.get_common_script_path(
    156                     self.common_script)
    157 
    158             # Create the URLs to the JS scripts to include in the html file.
    159             # Normally we would use the http_server.UrlOf method. However,
    160             # that requires starting the server first. The server reads
    161             # all file contents on startup, meaning we must completely
    162             # create the html file first. Hence we create the url
    163             # paths relative to the common prefix, which will be used as the
    164             # base of the server.
    165             base_dir = os.path.commonprefix(
    166                     [own_script_path, common_script_path])
    167             base_dir = base_dir.rstrip('/')
    168             own_script_url = own_script_path[len(base_dir):]
    169             common_script_url = common_script_path[len(base_dir):]
    170 
    171             html_file = webrtc_utils.create_temp_html_file(
    172                     self.title,
    173                     self.tmpdir,
    174                     own_script_url,
    175                     common_script_url)
    176             # Don't bother deleting the html file, the autotest tmp dir will be
    177             # cleaned up by the autotest framework.
    178             try:
    179                 cr.browser.platform.SetHTTPServerDirectories(
    180                     [own_script_path, html_file.name, common_script_path])
    181                 self.start_test(cr, html_file)
    182                 self.wait_test_completed(self.timeout)
    183                 self.verify_status_ok()
    184             finally:
    185                 # Ensure we always have a screenshot, both when succesful and
    186                 # when failed - useful for debugging.
    187                 self.take_screenshots()
    188 
    189     def verify_status_ok(self):
    190         """
    191         Verifies that the status of the test is 'ok-done'.
    192 
    193         @raises TestError the status is different from 'ok-done'.
    194         """
    195         status = self.tab.EvaluateJavaScript('getStatus()')
    196         if status != 'ok-done':
    197             raise error.TestFail('Failed: %s' % status)
    198 
    199     def take_screenshots(self):
    200         """
    201         Takes screenshots using two different mechanisms.
    202 
    203         Takes one screenshot using graphics_utils which is a really low level
    204         api that works between the kernel and userspace. The advantage is that
    205         this captures the entire screen regardless of Chrome state. Disadvantage
    206         is that it does not always work.
    207 
    208         Takes one screenshot of the current tab using Telemetry.
    209 
    210         Saves the screenshot in the results directory.
    211         """
    212         # Replace spaces with _ and lowercase the screenshot name for easier
    213         # tab completion in terminals.
    214         screenshot_name = self.title.replace(' ', '-').lower() + '-screenshot'
    215         self.take_graphics_utils_screenshot(screenshot_name)
    216         self.take_browser_tab_screenshot(screenshot_name)
    217 
    218     def take_graphics_utils_screenshot(self, screenshot_name):
    219         """
    220         Takes a screenshot of what is currently displayed.
    221 
    222         Uses the low level graphics_utils API.
    223 
    224         @param screenshot_name: Name of the screenshot.
    225         """
    226         try:
    227             full_filename = screenshot_name + '_graphics_utils'
    228             graphics_utils.take_screenshot(self.debugdir, full_filename)
    229         except StandardError as e:
    230             logging.warn('Screenshot using graphics_utils failed', exc_info = e)
    231 
    232     def take_browser_tab_screenshot(self, screenshot_name):
    233         """
    234         Takes a screenshot of the current browser tab.
    235 
    236         @param screenshot_name: Name of the screenshot.
    237         """
    238         if self.tab is not None and self.tab.screenshot_supported:
    239             try:
    240                 screenshot = self.tab.Screenshot(timeout = 10)
    241                 full_filename = os.path.join(
    242                         self.debugdir, screenshot_name + '_browser_tab.png')
    243                 image_util.WritePngFile(screenshot, full_filename)
    244             except exceptions.Error as e:
    245                 # This can for example occur if Chrome crashes. It will
    246                 # cause the Screenshot call to timeout.
    247                 logging.warn(
    248                         'Screenshot using telemetry tab.Screenshot failed',
    249                         exc_info = e)
    250         else:
    251             logging.warn(
    252                     'Screenshot using telemetry tab.Screenshot() not supported')
    253 
    254 
    255 
    256 class WebRtcPeerConnectionPerformanceTest(WebRtcPeerConnectionTest):
    257     """
    258     Runs a WebRTC performance test.
    259     """
    260     def __init__(
    261             self,
    262             title,
    263             own_script,
    264             common_script,
    265             bindir,
    266             tmpdir,
    267             debugdir,
    268             timeout = 70,
    269             test_runtime_seconds = 60,
    270             num_peer_connections = 5,
    271             iteration_delay_millis = 500,
    272             before_start_hook = None):
    273 
    274           def perf_before_start_hook(tab):
    275               """
    276               Before start hook to disable cpu overuse detection.
    277               """
    278               if before_start_hook:
    279                   before_start_hook(tab)
    280               tab.EvaluateJavaScript('cpuOveruseDetection = false')
    281 
    282           super(WebRtcPeerConnectionPerformanceTest, self).__init__(
    283                   title,
    284                   own_script,
    285                   common_script,
    286                   bindir,
    287                   tmpdir,
    288                   debugdir,
    289                   timeout,
    290                   test_runtime_seconds,
    291                   num_peer_connections,
    292                   iteration_delay_millis,
    293                   perf_before_start_hook)
    294           self.collector = system_metrics_collector.SystemMetricsCollector(
    295                 system_facade_native.SystemFacadeNative())
    296           # TODO(crbug/784365): If this proves to work fine, move to a separate
    297           # module and make more generic.
    298           delay = 5
    299           iterations = self.test_runtime_seconds / delay + 1
    300           utils.BgJob('top -b -d %d -n %d -w 512 -c > %s/top_output.txt'
    301                       % (delay, iterations, self.debugdir))
    302           utils.BgJob('iostat -x %d %d > %s/iostat_output.txt'
    303                       % (delay, iterations, self.debugdir))
    304           utils.BgJob('for i in $(seq %d);'
    305                       'do netstat -s >> %s/netstat_output.txt'
    306                       ';sleep %d;done'
    307                       % (delay, self.debugdir, iterations))
    308 
    309     def do_in_wait_loop(self):
    310         self.collector.collect_snapshot()
    311         time.sleep(1)
    312 
    313