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