Home | History | Annotate | Download | only in video_VEAPerf
      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