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