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