• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Lint as: python2, python3
2import logging
3import os
4import time
5
6from autotest_lib.client.bin import utils
7from autotest_lib.client.common_lib import error
8from autotest_lib.client.common_lib.cros import chrome
9from autotest_lib.client.common_lib.cros import system_metrics_collector
10from autotest_lib.client.common_lib.cros import webrtc_utils
11from autotest_lib.client.cros.graphics import graphics_utils
12from autotest_lib.client.cros.multimedia import system_facade
13from autotest_lib.client.cros.video import helper_logger
14from telemetry.util import image_util
15
16
17EXTRA_BROWSER_ARGS = ['--use-fake-ui-for-media-stream',
18                      '--use-fake-device-for-media-stream']
19
20
21class 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 stop_test(self):
106        """
107        Hook that always get called after the test has run.
108        """
109        pass
110
111    def _test_done(self):
112        """
113        Determines if the test is done or not.
114
115        Does so by querying status of the JavaScript test runner.
116        @return True if the test is done, false if it is still in progress.
117        @raise TestFail if the status check returns a failure status.
118        """
119        status = self.tab.EvaluateJavaScript('getStatus()')
120        if status.startswith('failure'):
121            raise error.TestFail(
122                    'Test status starts with failure, status is: ' + status)
123        logging.debug(status)
124        return status == 'ok-done'
125
126    def wait_test_completed(self, timeout_secs):
127        """
128        Waits until the test is done.
129
130        @param timeout_secs Max time to wait in seconds.
131
132        @raises TestError on timeout, or javascript eval fails, or
133                error status from the getStatus() JS method.
134        """
135        start_secs = time.time()
136        while not self._test_done():
137            spent_time = time.time() - start_secs
138            if spent_time > timeout_secs:
139                raise utils.TimeoutError(
140                        'Test timed out after {} seconds'.format(spent_time))
141            self.do_in_wait_loop()
142
143    def do_in_wait_loop(self):
144        """
145        Called repeatedly in a loop while the test waits for completion.
146
147        Subclasses can override and provide specific behavior.
148        """
149        time.sleep(1)
150
151    @helper_logger.video_log_wrapper
152    def run_test(self):
153        """
154        Starts the test and waits until it is completed.
155        """
156        with chrome.Chrome(extra_browser_args = EXTRA_BROWSER_ARGS + \
157                           [helper_logger.chrome_vmodule_flag()],
158                           init_network_controller = True) as cr:
159            own_script_path = os.path.join(
160                    self.bindir, self.own_script)
161            common_script_path = webrtc_utils.get_common_script_path(
162                    self.common_script)
163
164            # Create the URLs to the JS scripts to include in the html file.
165            # Normally we would use the http_server.UrlOf method. However,
166            # that requires starting the server first. The server reads
167            # all file contents on startup, meaning we must completely
168            # create the html file first. Hence we create the url
169            # paths relative to the common prefix, which will be used as the
170            # base of the server.
171            base_dir = os.path.commonprefix(
172                    [own_script_path, common_script_path])
173            base_dir = base_dir.rstrip('/')
174            own_script_url = own_script_path[len(base_dir):]
175            common_script_url = common_script_path[len(base_dir):]
176
177            html_file = webrtc_utils.create_temp_html_file(
178                    self.title,
179                    self.tmpdir,
180                    own_script_url,
181                    common_script_url)
182            # Don't bother deleting the html file, the autotest tmp dir will be
183            # cleaned up by the autotest framework.
184            try:
185                cr.browser.platform.SetHTTPServerDirectories(
186                    [own_script_path, html_file.name, common_script_path])
187                self.start_test(cr, html_file)
188                self.wait_test_completed(self.timeout)
189                self.verify_status_ok()
190            finally:
191                # Ensure we always have a screenshot, both when succesful and
192                # when failed - useful for debugging.
193                self.take_screenshots()
194                self.stop_test()
195
196    def verify_status_ok(self):
197        """
198        Verifies that the status of the test is 'ok-done'.
199
200        @raises TestError the status is different from 'ok-done'.
201        """
202        status = self.tab.EvaluateJavaScript('getStatus()')
203        if status != 'ok-done':
204            raise error.TestFail('Failed: %s' % status)
205
206    def take_screenshots(self):
207        """
208        Takes screenshots using two different mechanisms.
209
210        Takes one screenshot using graphics_utils which is a really low level
211        api that works between the kernel and userspace. The advantage is that
212        this captures the entire screen regardless of Chrome state. Disadvantage
213        is that it does not always work.
214
215        Takes one screenshot of the current tab using Telemetry.
216
217        Saves the screenshot in the results directory.
218        """
219        # Replace spaces with _ and lowercase the screenshot name for easier
220        # tab completion in terminals.
221        screenshot_name = self.title.replace(' ', '-').lower() + '-screenshot'
222        self.take_graphics_utils_screenshot(screenshot_name)
223        self.take_browser_tab_screenshot(screenshot_name)
224
225    def take_graphics_utils_screenshot(self, screenshot_name):
226        """
227        Takes a screenshot of what is currently displayed.
228
229        Uses the low level graphics_utils API.
230
231        @param screenshot_name: Name of the screenshot.
232        """
233        try:
234            full_filename = screenshot_name + '_graphics_utils'
235            graphics_utils.take_screenshot(self.debugdir, full_filename)
236        except Exception as e:
237            logging.warning('Screenshot using graphics_utils failed', exc_info = e)
238
239    def take_browser_tab_screenshot(self, screenshot_name):
240        """
241        Takes a screenshot of the current browser tab.
242
243        @param screenshot_name: Name of the screenshot.
244        """
245        if self.tab is not None and self.tab.screenshot_supported:
246            try:
247                screenshot = self.tab.Screenshot(timeout = 10)
248                full_filename = os.path.join(
249                        self.debugdir, screenshot_name + '_browser_tab.png')
250                image_util.WritePngFile(screenshot, full_filename)
251            except Exception:
252                # This can for example occur if Chrome crashes. It will
253                # cause the Screenshot call to timeout.
254                logging.warning(
255                        'Screenshot using telemetry tab.Screenshot failed',
256                        exc_info=True)
257        else:
258            logging.warning(
259                    'Screenshot using telemetry tab.Screenshot() not supported')
260
261
262
263class WebRtcPeerConnectionPerformanceTest(WebRtcPeerConnectionTest):
264    """
265    Runs a WebRTC performance test.
266    """
267    def __init__(
268            self,
269            title,
270            own_script,
271            common_script,
272            bindir,
273            tmpdir,
274            debugdir,
275            timeout = 70,
276            test_runtime_seconds = 60,
277            num_peer_connections = 5,
278            iteration_delay_millis = 500,
279            before_start_hook = None):
280
281          def perf_before_start_hook(tab):
282              """
283              Before start hook to disable cpu overuse detection.
284              """
285              if before_start_hook:
286                  before_start_hook(tab)
287              tab.EvaluateJavaScript('cpuOveruseDetection = false')
288
289          super(WebRtcPeerConnectionPerformanceTest, self).__init__(
290                  title,
291                  own_script,
292                  common_script,
293                  bindir,
294                  tmpdir,
295                  debugdir,
296                  timeout,
297                  test_runtime_seconds,
298                  num_peer_connections,
299                  iteration_delay_millis,
300                  perf_before_start_hook)
301          self.collector = system_metrics_collector.SystemMetricsCollector(
302                system_facade.SystemFacadeLocal())
303          # TODO(crbug/784365): If this proves to work fine, move to a separate
304          # module and make more generic.
305          delay = 5
306          iterations = self.test_runtime_seconds / delay + 1
307          utils.BgJob('top -b -d %d -n %d -w 512 -c > %s/top_output.txt'
308                      % (delay, iterations, self.debugdir))
309          utils.BgJob('iostat -x %d %d > %s/iostat_output.txt'
310                      % (delay, iterations, self.debugdir))
311          utils.BgJob('for i in $(seq %d);'
312                      'do netstat -s >> %s/netstat_output.txt'
313                      ';sleep %d;done'
314                      % (delay, self.debugdir, iterations))
315
316    def start_test(self, cr, html_file):
317        super(WebRtcPeerConnectionPerformanceTest, self).start_test(
318                cr, html_file)
319        self.collector.pre_collect()
320
321    def stop_test(self):
322        self.collector.post_collect()
323        super(WebRtcPeerConnectionPerformanceTest, self).stop_test()
324
325    def do_in_wait_loop(self):
326        self.collector.collect_snapshot()
327        time.sleep(1)
328