1 # Copyright (c) 2013 The Chromium 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 errno 6 import hashlib 7 import logging 8 import os 9 import re 10 11 from autotest_lib.client.bin import utils 12 from autotest_lib.client.common_lib import error 13 from autotest_lib.client.common_lib import file_utils 14 from autotest_lib.client.cros import chrome_binary_test 15 from autotest_lib.client.cros.video import device_capability 16 from autotest_lib.client.cros.video import helper_logger 17 18 from contextlib import closing 19 from math import ceil, floor 20 21 KEY_DELIVERY_TIME = 'delivery_time' 22 KEY_DELIVERY_TIME_FIRST = 'delivery_time.first' 23 KEY_DELIVERY_TIME_75 = 'delivery_time.percentile_0.75' 24 KEY_DELIVERY_TIME_50 = 'delivery_time.percentile_0.50' 25 KEY_DELIVERY_TIME_25 = 'delivery_time.percentile_0.25' 26 KEY_FRAME_DROP_RATE = 'frame_drop_rate' 27 KEY_CPU_KERNEL_USAGE = 'cpu_usage.kernel' 28 KEY_CPU_USER_USAGE = 'cpu_usage.user' 29 KEY_DECODE_TIME_50 = 'decode_time.percentile_0.50' 30 31 DOWNLOAD_BASE = ('http://commondatastorage.googleapis.com' 32 '/chromiumos-test-assets-public/') 33 BINARY = 'video_decode_accelerator_unittest' 34 OUTPUT_LOG = 'test_output.log' 35 TIME_LOG = 'time.log' 36 37 TIME_BINARY = '/usr/local/bin/time' 38 MICROSECONDS_PER_SECOND = 1000000 39 40 UNIT_MILLISECOND = 'milliseconds' 41 UNIT_MICROSECOND = 'us' 42 UNIT_RATIO = 'ratio' 43 44 # The format used for 'time': <real time>, <kernel time>, <user time> 45 TIME_OUTPUT_FORMAT = '%e %S %U' 46 47 RE_FRAME_DELIVERY_TIME = re.compile('frame \d+: (\d+) us') 48 RE_DECODE_TIME_MEDIAN = re.compile('Decode time median: (\d+)') 49 50 51 def _percentile(values, k): 52 assert k >= 0 and k <= 1 53 i = k * (len(values) - 1) 54 c, f = int(ceil(i)), int(floor(i)) 55 56 if c == f: return values[c] 57 return (i - f) * values[c] + (c - i) * values[f] 58 59 60 def _remove_if_exists(filepath): 61 if filepath and os.path.exists(filepath): 62 try: 63 os.remove(filepath) 64 except OSError, e: 65 raise 66 67 68 class video_VDAPerf(chrome_binary_test.ChromeBinaryTest): 69 """ 70 This test monitors several performance metrics reported by Chrome test 71 binary, video_decode_accelerator_unittest. 72 """ 73 74 version = 1 75 76 77 def _logperf(self, name, key, value, units, higher_is_better=False): 78 description = '%s.%s' % (name, key) 79 self.output_perf_value( 80 description=description, value=value, units=units, 81 higher_is_better=higher_is_better) 82 83 84 def _analyze_frame_delivery_times(self, name, frame_delivery_times): 85 """ 86 Analyzes the frame delivery times and output the statistics to the 87 Chrome Performance dashboard. 88 89 @param name: The name of the test video. 90 @param frame_delivery_times: The delivery time of each frame in the 91 test video. 92 """ 93 # Log the delivery time of the first frame. 94 self._logperf(name, KEY_DELIVERY_TIME_FIRST, frame_delivery_times[0], 95 UNIT_MICROSECOND) 96 97 # Log all the delivery times, the Chrome performance dashboard will do 98 # the statistics. 99 self._logperf(name, KEY_DELIVERY_TIME, frame_delivery_times, 100 UNIT_MICROSECOND) 101 102 # Log the 25%, 50%, and 75% percentile of the frame delivery times. 103 t = sorted(frame_delivery_times) 104 self._logperf(name, KEY_DELIVERY_TIME_75, _percentile(t, 0.75), 105 UNIT_MICROSECOND) 106 self._logperf(name, KEY_DELIVERY_TIME_50, _percentile(t, 0.50), 107 UNIT_MICROSECOND) 108 self._logperf(name, KEY_DELIVERY_TIME_25, _percentile(t, 0.25), 109 UNIT_MICROSECOND) 110 111 112 def _analyze_frame_drop_rate( 113 self, name, frame_delivery_times, rendering_fps): 114 frame_duration = MICROSECONDS_PER_SECOND / rendering_fps 115 116 render_time = frame_duration; 117 delivery_time = 0; 118 drop_count = 0 119 120 # Ignore the delivery time of the first frame since we delay the 121 # rendering until we get the first frame. 122 # 123 # Note that we keep accumulating delivery times and don't use deltas 124 # between current and previous delivery time. If the decoder cannot 125 # catch up after falling behind, it will keep dropping frames. 126 for t in frame_delivery_times[1:]: 127 render_time += frame_duration 128 delivery_time += t 129 if delivery_time > render_time: 130 drop_count += 1 131 132 n = len(frame_delivery_times) 133 134 # Since we won't drop the first frame, don't add it to the number of 135 # frames. 136 drop_rate = float(drop_count) / (n - 1) if n > 1 else 0 137 self._logperf(name, KEY_FRAME_DROP_RATE, drop_rate, UNIT_RATIO) 138 139 # The performance keys would be used as names of python variables when 140 # evaluating the test constraints. So we cannot use '.' as we did in 141 # _logperf. 142 self._perf_keyvals['%s_%s' % (name, KEY_FRAME_DROP_RATE)] = drop_rate 143 144 145 def _analyze_cpu_usage(self, name, time_log_file): 146 with open(time_log_file) as f: 147 content = f.read() 148 r, s, u = (float(x) for x in content.split()) 149 150 self._logperf(name, KEY_CPU_USER_USAGE, u / r, UNIT_RATIO) 151 self._logperf(name, KEY_CPU_KERNEL_USAGE, s / r, UNIT_RATIO) 152 153 154 def _load_frame_delivery_times(self, test_log_file): 155 """ 156 Gets the frame delivery times from the |test_log_file|. 157 158 The |test_log_file| could contain frame delivery times for more than 159 one decoder. However, we use only one in this test. 160 161 The expected content in the |test_log_file|: 162 163 The first line is the frame number of the first decoder. For exmplae: 164 frame count: 250 165 It is followed by the delivery time of each frame. For example: 166 frame 0000: 16123 us 167 frame 0001: 16305 us 168 : 169 170 Then it is the frame number of the second decoder followed by the 171 delivery times, and so on so forth. 172 173 @param test_log_file: The test log file where we load the frame 174 delivery times from. 175 @returns a list of integers which are the delivery times of all frames 176 (in microsecond). 177 """ 178 result = [] 179 with open(test_log_file, 'r') as f: 180 while True: 181 line = f.readline() 182 if not line: break 183 _, count = line.split(':') 184 times = [] 185 for i in xrange(int(count)): 186 line = f.readline() 187 m = RE_FRAME_DELIVERY_TIME.match(line) 188 assert m, 'invalid format: %s' % line 189 times.append(int(m.group(1))) 190 result.append(times) 191 if len(result) != 1: 192 raise error.TestError('Frame delivery times load failed.') 193 return result[0] 194 195 196 def _get_test_case_name(self, path): 197 """Gets the test_case_name from the video's path. 198 199 For example: for the path 200 "/crowd/crowd1080-1edaaca36b67e549c51e5fea4ed545c3.vp8" 201 We will derive the test case's name as "crowd1080_vp8". 202 """ 203 s = path.split('/')[-1] # get the filename 204 return '%s_%s' % (s[:s.rfind('-')], s[s.rfind('.') + 1:]) 205 206 207 def _download_video(self, download_path, local_file): 208 url = '%s%s' % (DOWNLOAD_BASE, download_path) 209 logging.info('download "%s" to "%s"', url, local_file) 210 211 file_utils.download_file(url, local_file) 212 213 with open(local_file, 'r') as r: 214 md5sum = hashlib.md5(r.read()).hexdigest() 215 if md5sum not in download_path: 216 raise error.TestError('unmatched md5 sum: %s' % md5sum) 217 218 219 def _results_file(self, test_name, type_name, filename): 220 return os.path.join(self.resultsdir, 221 '%s_%s_%s' % (test_name, type_name, filename)) 222 223 224 def _append_freon_switch_if_needed(self, cmd_line): 225 return cmd_line + ' --ozone-platform=gbm' 226 227 228 def _run_test_case(self, name, test_video_data, frame_num, rendering_fps): 229 230 # Get frame delivery time, decode as fast as possible. 231 test_log_file = self._results_file(name, 'no_rendering', OUTPUT_LOG) 232 cmd_line_list = [ 233 '--test_video_data="%s"' % test_video_data, 234 '--gtest_filter=DecodeVariations/*/0', 235 '--rendering_fps=0', 236 '--output_log="%s"' % test_log_file, 237 '--ozone-platform=gbm', 238 helper_logger.chrome_vmodule_flag(), 239 ] 240 cmd_line = ' '.join(cmd_line_list) 241 self.run_chrome_test_binary(BINARY, cmd_line) 242 243 frame_delivery_times = self._load_frame_delivery_times(test_log_file) 244 if len(frame_delivery_times) != frame_num: 245 raise error.TestError( 246 "frames number mismatch - expected: %d, got: %d" % 247 (frame_num, len(frame_delivery_times))); 248 self._analyze_frame_delivery_times(name, frame_delivery_times) 249 250 # Get frame drop rate & CPU usage, decode at the specified fps 251 test_log_file = self._results_file(name, 'with_rendering', OUTPUT_LOG) 252 time_log_file = self._results_file(name, 'with_rendering', TIME_LOG) 253 cmd_line_list = [ 254 '--test_video_data="%s"' % test_video_data, 255 '--gtest_filter=DecodeVariations/*/0', 256 '--rendering_fps=%s' % rendering_fps, 257 '--output_log="%s"' % test_log_file, 258 '--ozone-platform=gbm', 259 helper_logger.chrome_vmodule_flag(), 260 ] 261 cmd_line = ' '.join(cmd_line_list) 262 time_cmd = ('%s -f "%s" -o "%s" ' % 263 (TIME_BINARY, TIME_OUTPUT_FORMAT, time_log_file)) 264 self.run_chrome_test_binary(BINARY, cmd_line, prefix=time_cmd) 265 266 frame_delivery_times = self._load_frame_delivery_times(test_log_file) 267 self._analyze_frame_drop_rate(name, frame_delivery_times, rendering_fps) 268 self._analyze_cpu_usage(name, time_log_file) 269 270 # Get decode time median. 271 test_log_file = self._results_file(name, 'decode_time', OUTPUT_LOG) 272 cmd_line_list = [ 273 '--test_video_data="%s"' % test_video_data, 274 '--gtest_filter=*TestDecodeTimeMedian', 275 '--output_log="%s"' % test_log_file, 276 '--ozone-platform=gbm', 277 helper_logger.chrome_vmodule_flag(), 278 ] 279 cmd_line = ' '.join(cmd_line_list) 280 self.run_chrome_test_binary(BINARY, cmd_line) 281 line = open(test_log_file, 'r').read() 282 m = RE_DECODE_TIME_MEDIAN.match(line) 283 assert m, 'invalid format: %s' % line 284 decode_time = int(m.group(1)) 285 self._logperf(name, KEY_DECODE_TIME_50, decode_time, UNIT_MICROSECOND) 286 287 @helper_logger.video_log_wrapper 288 @chrome_binary_test.nuke_chrome 289 def run_once(self, test_cases): 290 self._perf_keyvals = {} 291 video_path = None 292 last_error = None 293 dc = device_capability.DeviceCapability() 294 for (path, width, height, frame_num, frag_num, profile, 295 fps, required_cap) in test_cases: 296 try: 297 name = self._get_test_case_name(path) 298 if not dc.have_capability(required_cap): 299 logging.info("%s is unavailable. Skip %s", 300 required_cap, name) 301 continue 302 video_path = os.path.join(self.bindir, '%s.download' % name) 303 test_video_data = '%s:%s:%s:%s:%s:%s:%s:%s' % (video_path, 304 width, height, frame_num, frag_num, 0, 0, profile) 305 self._download_video(path, video_path) 306 self._run_test_case(name, test_video_data, frame_num, fps) 307 except Exception as last_error: 308 # log the error and continue to the next test case. 309 logging.exception(last_error) 310 finally: 311 _remove_if_exists(video_path) 312 313 self.write_perf_keyval(self._perf_keyvals) 314 315 if last_error: 316 raise # the last error 317