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