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