1 # Copyright 2015 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 math 9 import mmap 10 import os 11 import re 12 13 from contextlib import closing 14 15 from autotest_lib.client.bin import utils 16 from autotest_lib.client.common_lib import error 17 from autotest_lib.client.common_lib import file_utils 18 from autotest_lib.client.cros import chrome_binary_test 19 20 21 DOWNLOAD_BASE = ('http://commondatastorage.googleapis.com' 22 '/chromiumos-test-assets-public/') 23 24 VEA_BINARY = 'video_encode_accelerator_unittest' 25 TIME_BINARY = '/usr/local/bin/time' 26 27 # The format used for 'time': <real time> <kernel time> <user time> 28 TIME_OUTPUT_FORMAT = '%e %S %U' 29 30 TEST_LOG_SUFFIX = 'test.log' 31 TIME_LOG_SUFFIX = 'time.log' 32 33 # Performance keys: 34 # FPS (i.e. encoder throughput) 35 KEY_FPS = 'fps' 36 # Encode latencies at the 50th, 75th, and 95th percentiles. 37 # Encode latency is the delay from input of a frame to output of the encoded 38 # bitstream. 39 KEY_ENCODE_LATENCY_50 = 'encode_latency.50_percentile' 40 KEY_ENCODE_LATENCY_75 = 'encode_latency.75_percentile' 41 KEY_ENCODE_LATENCY_95 = 'encode_latency.95_percentile' 42 # CPU usage in kernel space 43 KEY_CPU_KERNEL_USAGE = 'cpu_usage.kernel' 44 # CPU usage in user space 45 KEY_CPU_USER_USAGE = 'cpu_usage.user' 46 47 # Units of performance values: 48 # (These strings should match chromium/src/tools/perf/unit-info.json.) 49 UNIT_MILLISECOND = 'milliseconds' 50 UNIT_MICROSECOND = 'us' 51 UNIT_PERCENT = 'percent' 52 UNIT_FPS = 'fps' 53 54 RE_FPS = re.compile(r'^Measured encoder FPS: ([+\-]?[0-9.]+)$', re.MULTILINE) 55 RE_ENCODE_LATENCY_50 = re.compile( 56 r'^Encode latency for the 50th percentile: (\d+) us$', 57 re.MULTILINE) 58 RE_ENCODE_LATENCY_75 = re.compile( 59 r'^Encode latency for the 75th percentile: (\d+) us$', 60 re.MULTILINE) 61 RE_ENCODE_LATENCY_95 = re.compile( 62 r'^Encode latency for the 95th percentile: (\d+) us$', 63 re.MULTILINE) 64 65 66 def _remove_if_exists(filepath): 67 try: 68 os.remove(filepath) 69 except OSError, e: 70 if e.errno != errno.ENOENT: # no such file 71 raise 72 73 74 class video_VEAPerf(chrome_binary_test.ChromeBinaryTest): 75 """ 76 This test monitors several performance metrics reported by Chrome test 77 binary, video_encode_accelerator_unittest. 78 """ 79 80 version = 1 81 82 def _logperf(self, test_name, key, value, units, higher_is_better=False): 83 description = '%s.%s' % (test_name, key) 84 self.output_perf_value( 85 description=description, value=value, units=units, 86 higher_is_better=higher_is_better) 87 88 89 def _analyze_fps(self, test_name, log_file): 90 """ 91 Analyzes FPS info from result log file. 92 """ 93 with open(log_file, 'r') as f: 94 mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) 95 fps = [float(m.group(1)) for m in RE_FPS.finditer(mm)] 96 mm.close() 97 if len(fps) != 1: 98 raise error.TestError('Parsing FPS failed w/ %d occurrence(s).' % 99 len(fps)) 100 self._logperf(test_name, KEY_FPS, fps[0], UNIT_FPS, True) 101 102 103 def _analyze_encode_latency(self, test_name, log_file): 104 """ 105 Analyzes encode latency from result log file. 106 """ 107 with open(log_file, 'r') as f: 108 mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) 109 latency_50 = [int(m.group(1)) for m in 110 RE_ENCODE_LATENCY_50.finditer(mm)] 111 latency_75 = [int(m.group(1)) for m in 112 RE_ENCODE_LATENCY_75.finditer(mm)] 113 latency_95 = [int(m.group(1)) for m in 114 RE_ENCODE_LATENCY_95.finditer(mm)] 115 mm.close() 116 if any([len(l) != 1 for l in [latency_50, latency_75, latency_95]]): 117 raise error.TestError('Parsing encode latency failed.') 118 self._logperf(test_name, KEY_ENCODE_LATENCY_50, latency_50[0], 119 UNIT_MICROSECOND) 120 self._logperf(test_name, KEY_ENCODE_LATENCY_75, latency_75[0], 121 UNIT_MICROSECOND) 122 self._logperf(test_name, KEY_ENCODE_LATENCY_95, latency_95[0], 123 UNIT_MICROSECOND) 124 125 126 def _analyze_cpu_usage(self, test_name, time_log_file): 127 """ 128 Analyzes CPU usage from the output of 'time' command. 129 """ 130 with open(time_log_file) as f: 131 content = f.read() 132 r, s, u = (float(x) for x in content.split()) 133 self._logperf(test_name, KEY_CPU_USER_USAGE, u / r, UNIT_PERCENT) 134 self._logperf(test_name, KEY_CPU_KERNEL_USAGE, s / r, UNIT_PERCENT) 135 136 137 def _get_profile_name(self, profile): 138 """ 139 Gets profile name from a profile index. 140 """ 141 if profile == 1: 142 return 'h264' 143 elif profile == 11: 144 return 'vp8' 145 else: 146 raise error.TestError('Internal error.') 147 148 149 def _convert_test_name(self, path, on_cloud, profile): 150 """Converts source path to test name and output video file name. 151 152 For example: for the path on cloud 153 "tulip2/tulip2-1280x720-1b95123232922fe0067869c74e19cd09.yuv" 154 155 We will derive the test case's name as "tulip2-1280x720.vp8" or 156 "tulip2-1280x720.h264" depending on the profile. The MD5 checksum in 157 path will be stripped. 158 159 For the local file, we use the base name directly. 160 161 @param path: The local path or download path. 162 @param on_cloud: Whether the file is on cloud. 163 @param profile: Profile index. 164 165 @returns a pair of (test name, output video file name) 166 """ 167 s = os.path.basename(path) 168 name = s[:s.rfind('-' if on_cloud else '.')] 169 profile_name = self._get_profile_name(profile) 170 return (name + '_' + profile_name, name + '.' + profile_name) 171 172 173 def _download_video(self, path_on_cloud, local_file): 174 url = '%s%s' % (DOWNLOAD_BASE, path_on_cloud) 175 logging.info('download "%s" to "%s"', url, local_file) 176 177 file_utils.download_file(url, local_file) 178 179 with open(local_file, 'r') as r: 180 md5sum = hashlib.md5(r.read()).hexdigest() 181 if md5sum not in path_on_cloud: 182 raise error.TestError('unmatched md5 sum: %s' % md5sum) 183 184 185 def _get_result_filename(self, test_name, subtype, suffix): 186 return os.path.join(self.resultsdir, 187 '%s_%s_%s' % (test_name, subtype, suffix)) 188 189 190 def _append_freon_switch_if_needed(self, cmd_line): 191 if utils.is_freon(): 192 cmd_line.append('--ozone-platform=gbm') 193 194 195 def _run_test_case(self, test_name, test_stream_data): 196 """ 197 Runs a VEA unit test. 198 199 @param test_name: Name of this test case. 200 @param test_stream_data: Parameter to --test_stream_data in vea_unittest. 201 """ 202 # Get FPS. 203 test_log_file = self._get_result_filename(test_name, 'fullspeed', 204 TEST_LOG_SUFFIX) 205 vea_args = [ 206 '--gtest_filter=EncoderPerf/*/0', 207 '--test_stream_data=%s' % test_stream_data, 208 '--output_log="%s"' % test_log_file] 209 self._append_freon_switch_if_needed(vea_args) 210 self.run_chrome_test_binary(VEA_BINARY, ' '.join(vea_args)) 211 self._analyze_fps(test_name, test_log_file) 212 213 # Get CPU usage and encode latency under specified frame rate. 214 test_log_file = self._get_result_filename(test_name, 'fixedspeed', 215 TEST_LOG_SUFFIX) 216 time_log_file = self._get_result_filename(test_name, 'fixedspeed', 217 TIME_LOG_SUFFIX) 218 vea_args = [ 219 '--gtest_filter=SimpleEncode/*/0', 220 '--test_stream_data=%s' % test_stream_data, 221 '--run_at_fps', '--measure_latency', 222 '--output_log="%s"' % test_log_file] 223 self._append_freon_switch_if_needed(vea_args) 224 time_cmd = ('%s -f "%s" -o "%s" ' % 225 (TIME_BINARY, TIME_OUTPUT_FORMAT, time_log_file)) 226 self.run_chrome_test_binary(VEA_BINARY, ' '.join(vea_args), 227 prefix=time_cmd) 228 self._analyze_encode_latency(test_name, test_log_file) 229 self._analyze_cpu_usage(test_name, time_log_file) 230 231 232 @chrome_binary_test.nuke_chrome 233 def run_once(self, test_cases): 234 last_error = None 235 for (path, on_cloud, width, height, requested_bit_rate, 236 profile, requested_frame_rate) in test_cases: 237 try: 238 test_name, output_name = self._convert_test_name( 239 path, on_cloud, profile) 240 if on_cloud: 241 input_path = os.path.join(self.tmpdir, 242 os.path.basename(path)) 243 self._download_video(path, input_path) 244 else: 245 input_path = os.path.join(self.cr_source_dir, path) 246 output_path = os.path.join(self.tmpdir, output_name) 247 test_stream_data = '%s:%s:%s:%s:%s:%s:%s' % ( 248 input_path, width, height, profile, output_path, 249 requested_bit_rate, requested_frame_rate) 250 self._run_test_case(test_name, test_stream_data) 251 except Exception as last_error: 252 # Log the error and continue to the next test case. 253 logging.exception(last_error) 254 finally: 255 if on_cloud: 256 _remove_if_exists(input_path) 257 _remove_if_exists(output_path) 258 259 if last_error: 260 raise last_error 261 262