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
5import errno
6import hashlib
7import logging
8import math
9import mmap
10import os
11import re
12
13from contextlib import closing
14
15from autotest_lib.client.bin import utils
16from autotest_lib.client.common_lib import error
17from autotest_lib.client.common_lib import file_utils
18from autotest_lib.client.cros import chrome_binary_test
19
20
21DOWNLOAD_BASE = ('http://commondatastorage.googleapis.com'
22                 '/chromiumos-test-assets-public/')
23
24VEA_BINARY = 'video_encode_accelerator_unittest'
25TIME_BINARY = '/usr/local/bin/time'
26
27# The format used for 'time': <real time> <kernel time> <user time>
28TIME_OUTPUT_FORMAT = '%e %S %U'
29
30TEST_LOG_SUFFIX = 'test.log'
31TIME_LOG_SUFFIX = 'time.log'
32
33# Performance keys:
34# FPS (i.e. encoder throughput)
35KEY_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.
39KEY_ENCODE_LATENCY_50 = 'encode_latency.50_percentile'
40KEY_ENCODE_LATENCY_75 = 'encode_latency.75_percentile'
41KEY_ENCODE_LATENCY_95 = 'encode_latency.95_percentile'
42# CPU usage in kernel space
43KEY_CPU_KERNEL_USAGE = 'cpu_usage.kernel'
44# CPU usage in user space
45KEY_CPU_USER_USAGE = 'cpu_usage.user'
46
47# Units of performance values:
48# (These strings should match chromium/src/tools/perf/unit-info.json.)
49UNIT_MILLISECOND = 'milliseconds'
50UNIT_MICROSECOND = 'us'
51UNIT_PERCENT = 'percent'
52UNIT_FPS = 'fps'
53
54RE_FPS = re.compile(r'^Measured encoder FPS: ([+\-]?[0-9.]+)$', re.MULTILINE)
55RE_ENCODE_LATENCY_50 = re.compile(
56    r'^Encode latency for the 50th percentile: (\d+) us$',
57    re.MULTILINE)
58RE_ENCODE_LATENCY_75 = re.compile(
59    r'^Encode latency for the 75th percentile: (\d+) us$',
60    re.MULTILINE)
61RE_ENCODE_LATENCY_95 = re.compile(
62    r'^Encode latency for the 95th percentile: (\d+) us$',
63    re.MULTILINE)
64
65
66def _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
74class 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