1 # Copyright (c) 2014 The Chromium OS Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 import logging 6 import os 7 import time 8 9 from autotest_lib.client.bin import test, utils 10 from autotest_lib.client.common_lib import error 11 from autotest_lib.client.common_lib import file_utils 12 from autotest_lib.client.common_lib.cros import chrome 13 from autotest_lib.client.cros import service_stopper 14 from autotest_lib.client.cros.power import power_status, power_utils 15 from autotest_lib.client.cros.video import histogram_verifier 16 from autotest_lib.client.cros.video import constants 17 from autotest_lib.client.cros.video import helper_logger 18 19 20 EXTRA_BROWSER_ARGS = ['--use-fake-device-for-media-stream', 21 '--use-fake-ui-for-media-stream'] 22 FAKE_FILE_ARG = '--use-file-for-fake-video-capture="%s"' 23 DISABLE_ACCELERATED_VIDEO_DECODE_BROWSER_ARGS = [ 24 '--disable-accelerated-video-decode'] 25 26 DOWNLOAD_BASE = ('http://commondatastorage.googleapis.com/' 27 'chromiumos-test-assets-public/crowd/') 28 VIDEO_NAME = 'crowd720_25frames.y4m' 29 30 WEBRTC_INTERNALS_URL = 'chrome://webrtc-internals' 31 32 WEBRTC_WITH_HW_ACCELERATION = 'webrtc_with_hw_acceleration' 33 WEBRTC_WITHOUT_HW_ACCELERATION = 'webrtc_without_hw_acceleration' 34 35 # Measurement duration in seconds. 36 MEASUREMENT_DURATION = 30 37 # Time to exclude from calculation after start the loopback [seconds]. 38 STABILIZATION_DURATION = 10 39 40 # Time in seconds to wait for cpu idle until giving up. 41 WAIT_FOR_IDLE_CPU_TIMEOUT = 60.0 42 # Maximum percent of cpu usage considered as idle. 43 CPU_IDLE_USAGE = 0.1 44 45 MAX_DECODE_TIME_DESCRIPTION = 'decode_time.max' 46 MEDIAN_DECODE_TIME_DESCRIPTION = 'decode_time.percentile_0.50' 47 CPU_USAGE_DESCRIPTION = 'video_cpu_usage' 48 POWER_DESCRIPTION = 'video_mean_energy_rate' 49 50 # Minimum battery charge percentage to run the power test. 51 BATTERY_INITIAL_CHARGED_MIN = 10 52 53 # These are the variable names in WebRTC internals. 54 # Maximum decode time of the frames of the last 10 seconds. 55 GOOG_MAX_DECODE_MS = 'googMaxDecodeMs' 56 # The decode time of the last frame. 57 GOOG_DECODE_MS = 'googDecodeMs' 58 59 # Javascript function to get the decode time. addStats is a function called by 60 # Chrome to pass WebRTC statistics every second. 61 ADD_STATS_JAVASCRIPT = ( 62 'var googMaxDecodeMs = new Array();' 63 'var googDecodeMs = new Array();' 64 'addStats = function(data) {' 65 ' reports = data.reports;' 66 ' for (var i=0; i < reports.length; i++) {' 67 ' if (reports[i].type == "ssrc") {' 68 ' values = reports[i].stats.values;' 69 ' for (var j=0; j < values.length; j++) {' 70 ' if (values[j] == "googMaxDecodeMs")' 71 ' googMaxDecodeMs[googMaxDecodeMs.length] = values[j+1];' 72 ' else if (values[j] == "googDecodeMs")' 73 ' googDecodeMs[googDecodeMs.length] = values[j+1];' 74 ' }' 75 ' }' 76 ' }' 77 '}') 78 79 # Measure the stats until getting 10 samples or exceeding 30 seconds. addStats 80 # is called once per second for now. 81 NUM_DECODE_TIME_SAMPLES = 10 82 TIMEOUT = 60 83 84 85 class video_WebRtcPerf(test.test): 86 """ 87 The test outputs the decode time, cpu usage and the power consumption for 88 WebRTC to performance dashboard. 89 """ 90 version = 1 91 arc_mode = None 92 93 94 def initialize(self): 95 self._service_stopper = None 96 self._original_governors = None 97 self._backlight = None 98 99 100 def cleanup(self): 101 if self._backlight: 102 self._backlight.restore() 103 if self._service_stopper: 104 self._service_stopper.restore_services() 105 if self._original_governors: 106 utils.restore_scaling_governor_states(self._original_governors) 107 108 super(video_WebRtcPerf, self).cleanup() 109 110 111 def start_loopback(self, cr): 112 """ 113 Opens WebRTC loopback page. 114 115 @param cr: Autotest Chrome instance. 116 """ 117 cr.browser.platform.SetHTTPServerDirectories(self.bindir) 118 119 tab = cr.browser.tabs[0] 120 tab.Navigate(cr.browser.platform.http_server.UrlOf( 121 os.path.join(self.bindir, 'loopback.html'))) 122 tab.WaitForDocumentReadyStateToBeComplete() 123 124 125 def open_stats_page(self, cr): 126 """ 127 Opens WebRTC internal statistics page. 128 129 @param cr: Autotest Chrome instance. 130 131 @returns the tab of the stats page. 132 """ 133 tab = cr.browser.tabs.New() 134 tab.Navigate(WEBRTC_INTERNALS_URL) 135 tab.WaitForDocumentReadyStateToBeComplete() 136 # Inject stats callback function. 137 tab.EvaluateJavaScript(ADD_STATS_JAVASCRIPT) 138 return tab 139 140 141 @helper_logger.video_log_wrapper 142 def run_once(self, decode_time_test=False, cpu_test=False, 143 power_test=False, arc_mode=None): 144 """ 145 Runs the video_WebRtcPerf test. 146 147 @param decode_time_test: Pass True to run decode time test. 148 @param cpu_test: Pass True to run CPU usage test. 149 @param power_test: Pass True to run power consumption test. 150 @param arc_mode: if 'enabled', run the test with Android enabled. 151 """ 152 # Download test video. 153 url = DOWNLOAD_BASE + VIDEO_NAME 154 local_path = os.path.join(self.bindir, VIDEO_NAME) 155 file_utils.download_file(url, local_path) 156 self.arc_mode = arc_mode 157 158 if decode_time_test: 159 keyvals = self.test_decode_time(local_path) 160 # The first value is max decode time. The second value is median 161 # decode time. Construct a dictionary containing one value to log 162 # the result. 163 max_decode_time = { 164 key:value[0] for (key, value) in keyvals.items()} 165 self.log_result(max_decode_time, MAX_DECODE_TIME_DESCRIPTION, 166 'milliseconds') 167 median_decode_time = { 168 key:value[1] for (key, value) in keyvals.items()} 169 self.log_result(median_decode_time, MEDIAN_DECODE_TIME_DESCRIPTION, 170 'milliseconds') 171 172 if cpu_test: 173 keyvals = self.test_cpu_usage(local_path) 174 self.log_result(keyvals, CPU_USAGE_DESCRIPTION, 'percent') 175 176 if power_test: 177 keyvals = self.test_power(local_path) 178 self.log_result(keyvals, POWER_DESCRIPTION , 'W') 179 180 181 def test_webrtc(self, local_path, gather_result): 182 """ 183 Runs the webrtc test with and without hardware acceleration. 184 185 @param local_path: the path to the video file. 186 @param gather_result: a function to run and return the test result 187 after chrome opens. The input parameter of the funciton is 188 Autotest chrome instance. 189 190 @return a dictionary that contains test the result. 191 """ 192 keyvals = {} 193 EXTRA_BROWSER_ARGS.append(FAKE_FILE_ARG % local_path) 194 195 with chrome.Chrome(extra_browser_args=EXTRA_BROWSER_ARGS +\ 196 [helper_logger.chrome_vmodule_flag()], 197 arc_mode=self.arc_mode, 198 init_network_controller=True) as cr: 199 # On daisy, Chrome freezes about 30 seconds after login because of 200 # TPM error. See http://crbug.com/588579. 201 if utils.get_board() == 'daisy': 202 logging.warning('Delay 30s for issue 588579 on daisy') 203 time.sleep(30) 204 # Open WebRTC loopback page and start the loopback. 205 self.start_loopback(cr) 206 result = gather_result(cr) 207 208 # Check if decode is hardware accelerated. 209 if histogram_verifier.is_bucket_present( 210 cr, 211 constants.RTC_INIT_HISTOGRAM, 212 constants.RTC_VIDEO_INIT_BUCKET): 213 keyvals[WEBRTC_WITH_HW_ACCELERATION] = result 214 else: 215 logging.info("Can not use hardware decoding.") 216 keyvals[WEBRTC_WITHOUT_HW_ACCELERATION] = result 217 return keyvals 218 219 # Start chrome with disabled video hardware decode flag. 220 with chrome.Chrome(extra_browser_args= 221 DISABLE_ACCELERATED_VIDEO_DECODE_BROWSER_ARGS + 222 EXTRA_BROWSER_ARGS, arc_mode=self.arc_mode, 223 init_network_controller=True) as cr: 224 225 # crbug/753292 - enforce the idle checks after login 226 if not utils.wait_for_idle_cpu(WAIT_FOR_IDLE_CPU_TIMEOUT, 227 CPU_IDLE_USAGE): 228 logging.warning('Could not get idle CPU post login.') 229 if not utils.wait_for_cool_machine(): 230 logging.warning('Could not get cold machine post login.') 231 232 if utils.get_board() == 'daisy': 233 logging.warning('Delay 30s for issue 588579 on daisy') 234 time.sleep(30) 235 # Open the webrtc loopback page and start the loopback. 236 self.start_loopback(cr) 237 result = gather_result(cr) 238 239 # Make sure decode is not hardware accelerated. 240 if histogram_verifier.is_bucket_present( 241 cr, 242 constants.RTC_INIT_HISTOGRAM, 243 constants.RTC_VIDEO_INIT_BUCKET): 244 raise error.TestError('HW decode should not be used.') 245 keyvals[WEBRTC_WITHOUT_HW_ACCELERATION] = result 246 247 return keyvals 248 249 250 def test_cpu_usage(self, local_path): 251 """ 252 Runs the video cpu usage test. 253 254 @param local_path: the path to the video file. 255 256 @return a dictionary that contains the test result. 257 """ 258 def get_cpu_usage(cr): 259 time.sleep(STABILIZATION_DURATION) 260 cpu_usage_start = utils.get_cpu_usage() 261 time.sleep(MEASUREMENT_DURATION) 262 cpu_usage_end = utils.get_cpu_usage() 263 return utils.compute_active_cpu_time(cpu_usage_start, 264 cpu_usage_end) * 100 265 266 # crbug/753292 - APNG login pictures increase CPU usage. Move the more 267 # strict idle checks after the login phase. 268 utils.wait_for_idle_cpu(WAIT_FOR_IDLE_CPU_TIMEOUT, CPU_IDLE_USAGE) 269 utils.wait_for_cool_machine() 270 if not utils.wait_for_idle_cpu(WAIT_FOR_IDLE_CPU_TIMEOUT, 271 CPU_IDLE_USAGE): 272 logging.warning('Could not get idle CPU pre login.') 273 if not utils.wait_for_cool_machine(): 274 logging.warning('Could not get cold machine pre login.') 275 276 # Stop the thermal service that may change the cpu frequency. 277 self._service_stopper = service_stopper.get_thermal_service_stopper() 278 self._service_stopper.stop_services() 279 # Set the scaling governor to performance mode to set the cpu to the 280 # highest frequency available. 281 self._original_governors = utils.set_high_performance_mode() 282 return self.test_webrtc(local_path, get_cpu_usage) 283 284 285 def test_power(self, local_path): 286 """ 287 Runs the video power consumption test. 288 289 @param local_path: the path to the video file. 290 291 @return a dictionary that contains the test result. 292 """ 293 self._backlight = power_utils.Backlight() 294 self._backlight.set_default() 295 self._service_stopper = service_stopper.ServiceStopper( 296 service_stopper.ServiceStopper.POWER_DRAW_SERVICES) 297 self._service_stopper.stop_services() 298 299 current_power_status = power_status.get_status() 300 # We expect the DUT is powered by battery now. But this is not always 301 # true due to other bugs. Disable this test temporarily as workaround. 302 # TODO(kcwu): remove this workaround after AC control is stable 303 # crbug.com/723968 304 if current_power_status.on_ac(): 305 logging.warning('Still powered by AC. Skip this test') 306 return {} 307 # Verify that the battery is sufficiently charged. 308 current_power_status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN) 309 310 measurements = [power_status.SystemPower( 311 current_power_status.battery_path)] 312 313 def get_power(cr): 314 power_logger = power_status.PowerLogger(measurements) 315 power_logger.start() 316 time.sleep(STABILIZATION_DURATION) 317 start_time = time.time() 318 time.sleep(MEASUREMENT_DURATION) 319 power_logger.checkpoint('result', start_time) 320 keyval = power_logger.calc() 321 # save_results() will save <fname_prefix>_raw.txt and 322 # <fname_prefix>_summary.txt, where the former contains raw data. 323 fname_prefix = 'result_%.0f' % time.time() 324 power_logger.save_results(self.resultsdir, fname_prefix) 325 metric_name = 'result_' + measurements[0].domain 326 with open(os.path.join( 327 self.resultsdir, fname_prefix + '_raw.txt')) as f: 328 for line in f.readlines(): 329 if line.startswith(metric_name): 330 split_data = line.split('\t') 331 # split_data[0] is metric_name, [1:] are raw data. 332 return [float(data) for data in split_data[1:]] 333 # Return a list contains the average power only for fallback. 334 return [keyval[metric_name + '_pwr_avg']] 335 336 return self.test_webrtc(local_path, get_power) 337 338 339 def test_decode_time(self, local_path): 340 """ 341 Runs the decode time test. 342 343 @param local_path: the path to the video file. 344 345 @return a dictionary that contains the test result. 346 """ 347 def get_decode_time(cr): 348 tab = self.open_stats_page(cr) 349 # Collect the decode time until there are enough samples. 350 start_time = time.time() 351 max_decode_time_list = [] 352 decode_time_list = [] 353 while (time.time() - start_time < TIMEOUT and 354 len(decode_time_list) < NUM_DECODE_TIME_SAMPLES): 355 time.sleep(1) 356 max_decode_time_list = [] 357 decode_time_list = [] 358 try: 359 max_decode_time_list = [int(x) for x in 360 tab.EvaluateJavaScript(GOOG_MAX_DECODE_MS)] 361 decode_time_list = [int(x) for x in 362 tab.EvaluateJavaScript(GOOG_DECODE_MS)] 363 except: 364 pass 365 # Output the values if they are valid. 366 if len(max_decode_time_list) < NUM_DECODE_TIME_SAMPLES: 367 raise error.TestError('Not enough ' + GOOG_MAX_DECODE_MS) 368 if len(decode_time_list) < NUM_DECODE_TIME_SAMPLES: 369 raise error.TestError('Not enough ' + GOOG_DECODE_MS) 370 max_decode_time = max(max_decode_time_list) 371 decode_time_median = self.get_median(decode_time_list) 372 logging.info("Max decode time list=%s", str(max_decode_time_list)) 373 logging.info("Decode time list=%s", str(decode_time_list)) 374 logging.info("Maximum decode time=%d, median=%d", max_decode_time, 375 decode_time_median) 376 return (max_decode_time, decode_time_median) 377 378 return self.test_webrtc(local_path, get_decode_time) 379 380 381 def get_median(self, seq): 382 """ 383 Calculates the median of a sequence of numbers. 384 385 @param seq: a list with numbers. 386 387 @returns the median of the numbers. 388 """ 389 seq.sort() 390 size = len(seq) 391 if size % 2 != 0: 392 return seq[size / 2] 393 return (seq[size / 2] + seq[size / 2 - 1]) / 2.0 394 395 def log_result(self, keyvals, description, units): 396 """ 397 Logs the test result output to the performance dashboard. 398 399 @param keyvals: a dictionary that contains results returned by 400 test_webrtc. 401 @param description: a string that describes the video and test result 402 and it will be part of the entry name in the dashboard. 403 @param units: the units of test result. 404 """ 405 result_with_hw = keyvals.get(WEBRTC_WITH_HW_ACCELERATION) 406 if result_with_hw: 407 self.output_perf_value( 408 description= 'hw_' + description, value=result_with_hw, 409 units=units, higher_is_better=False) 410 411 result_without_hw = keyvals.get(WEBRTC_WITHOUT_HW_ACCELERATION) 412 if result_without_hw: 413 self.output_perf_value( 414 description= 'sw_' + description, value=result_without_hw, 415 units=units, higher_is_better=False) 416