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