1# Copyright 2014 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 hashlib, logging, os, re, time, tempfile
6
7from autotest_lib.client.bin import utils
8from autotest_lib.client.common_lib import error
9from autotest_lib.client.common_lib import file_utils
10from autotest_lib.client.cros import chrome_binary_test
11from autotest_lib.client.cros import service_stopper
12from autotest_lib.client.cros.audio import cmd_utils
13from autotest_lib.client.cros.power import power_status, power_utils
14from autotest_lib.client.cros.video import helper_logger
15
16# The download base for test assets.
17DOWNLOAD_BASE = ('http://commondatastorage.googleapis.com'
18                 '/chromiumos-test-assets-public/')
19
20# The executable name of the vda unittest
21VDA_BINARY = 'video_decode_accelerator_unittest'
22
23# The executable name of the vea unittest
24VEA_BINARY = 'video_encode_accelerator_unittest'
25
26# The input frame rate for the vea_unittest.
27INPUT_FPS = 30
28
29# The rendering fps in the vda_unittest.
30RENDERING_FPS = 30
31
32UNIT_PERCENT = '%'
33UNIT_WATT = 'W'
34
35# The regex of the versioning file.
36# e.g., crowd720-3cfe7b096f765742b4aa79e55fe7c994.yuv
37RE_VERSIONING_FILE = re.compile(r'(.+)-([0-9a-fA-F]{32})(\..+)?')
38
39# Time in seconds to wait for cpu idle until giveup.
40WAIT_FOR_IDLE_CPU_TIMEOUT = 60
41
42# Maximum percent of cpu usage considered as idle.
43CPU_IDLE_USAGE = 0.1
44
45# List of thermal throttling services that should be disabled.
46# - temp_metrics for link.
47# - thermal for daisy, snow, pit etc.
48THERMAL_SERVICES = ['temp_metrics', 'thermal']
49
50# Measurement duration in seconds.
51MEASUREMENT_DURATION = 30
52
53# Time to exclude from calculation after playing a video [seconds].
54STABILIZATION_DURATION = 10
55
56# The number of frames used to warm up the rendering.
57RENDERING_WARM_UP = 15
58
59# A big number, used to keep the [vda|vea]_unittest running during the
60# measurement.
61MAX_INT = 2 ** 31 - 1
62
63# Minimum battery charge percentage to run the test
64BATTERY_INITIAL_CHARGED_MIN = 10
65
66
67class CpuUsageMeasurer(object):
68    """ Class used to measure the CPU usage."""
69
70    def __init__(self):
71        self._service_stopper = None
72        self._original_governors = None
73
74    def __enter__(self):
75        # Stop the thermal service that may change the cpu frequency.
76        self._service_stopper = service_stopper.ServiceStopper(THERMAL_SERVICES)
77        self._service_stopper.stop_services()
78
79        if not utils.wait_for_idle_cpu(
80                WAIT_FOR_IDLE_CPU_TIMEOUT, CPU_IDLE_USAGE):
81            raise error.TestError('Could not get idle CPU.')
82        if not utils.wait_for_cool_machine():
83            raise error.TestError('Could not get cold machine.')
84
85        # Set the scaling governor to performance mode to set the cpu to the
86        # highest frequency available.
87        self._original_governors = utils.set_high_performance_mode()
88        return self
89
90    def start(self):
91        self.start_cpu_usage_ = utils.get_cpu_usage()
92
93    def stop(self):
94        return utils.compute_active_cpu_time(
95                self.start_cpu_usage_, utils.get_cpu_usage())
96
97    def __exit__(self, type, value, tb):
98        if self._service_stopper:
99            self._service_stopper.restore_services()
100            self._service_stopper = None
101        if self._original_governors:
102            utils.restore_scaling_governor_states(self._original_governors)
103            self._original_governors = None
104
105
106class PowerMeasurer(object):
107    """ Class used to measure the power consumption."""
108
109    def __init__(self):
110        self._backlight = None
111        self._service_stopper = None
112
113    def __enter__(self):
114        self._backlight = power_utils.Backlight()
115        self._backlight.set_default()
116
117        self._service_stopper = service_stopper.ServiceStopper(
118                service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
119        self._service_stopper.stop_services()
120
121        status = power_status.get_status()
122
123        # Verify that we are running on battery and the battery is sufficiently
124        # charged.
125        status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN)
126        self._system_power = power_status.SystemPower(status.battery_path)
127        self._power_logger = power_status.PowerLogger([self._system_power])
128        return self
129
130    def start(self):
131        self._power_logger.start()
132
133    def stop(self):
134        self._power_logger.checkpoint('result')
135        keyval = self._power_logger.calc()
136        logging.info(keyval)
137        return keyval['result_' + self._system_power.domain + '_pwr']
138
139    def __exit__(self, type, value, tb):
140        if self._backlight:
141            self._backlight.restore()
142        if self._service_stopper:
143            self._service_stopper.restore_services()
144
145
146class DownloadManager(object):
147    """Use this class to download and manage the resources for testing."""
148
149    def __init__(self, tmpdir=None):
150        self._download_map = {}
151        self._tmpdir = tmpdir
152
153    def get_path(self, name):
154        return self._download_map[name]
155
156    def clear(self):
157        map(os.unlink, self._download_map.values())
158        self._download_map.clear()
159
160    def _download_single_file(self, remote_path):
161        url = DOWNLOAD_BASE + remote_path
162        tmp = tempfile.NamedTemporaryFile(delete=False, dir=self._tmpdir)
163        logging.info('download "%s" to "%s"', url, tmp.name)
164
165        file_utils.download_file(url, tmp.name)
166        md5 = hashlib.md5()
167        with open(tmp.name, 'r') as r:
168            while True:
169                block = r.read(128 * 1024)
170                if not block:
171                    break
172                md5.update(block)
173
174        filename = os.path.basename(remote_path)
175        m = RE_VERSIONING_FILE.match(filename)
176        if m:
177            prefix, md5sum, suffix = m.groups()
178            if md5.hexdigest() != md5sum:
179                raise error.TestError(
180                        'unmatched md5 sum: %s' % md5.hexdigest())
181            filename = prefix + (suffix or '')
182        self._download_map[filename] = tmp.name
183
184    def download_all(self, resources):
185        for r in resources:
186            self._download_single_file(r)
187
188
189class video_HangoutHardwarePerf(chrome_binary_test.ChromeBinaryTest):
190    """
191    The test outputs the cpu usage when doing video encoding and video
192    decoding concurrently.
193    """
194
195    version = 1
196
197    def get_vda_unittest_cmd_line(self, decode_videos):
198        test_video_data = []
199        for v in decode_videos:
200            assert len(v) == 6
201            # Convert to strings, also make a copy of the list.
202            v = map(str, v)
203            v[0] = self._downloads.get_path(v[0])
204            v[-1:-1] = ['0', '0'] # no fps requirements
205            test_video_data.append(':'.join(v))
206        cmd_line = [
207            self.get_chrome_binary_path(VDA_BINARY),
208            '--gtest_filter=DecodeVariations/*/0',
209            '--test_video_data=%s' % ';'.join(test_video_data),
210            '--rendering_warm_up=%d' % RENDERING_WARM_UP,
211            '--rendering_fps=%f' % RENDERING_FPS,
212            '--num_play_throughs=%d' % MAX_INT,
213            helper_logger.chrome_vmodule_flag(),
214        ]
215        cmd_line.append('--ozone-platform=gbm')
216        return cmd_line
217
218
219    def get_vea_unittest_cmd_line(self, encode_videos):
220        test_stream_data = []
221        for v in encode_videos:
222            assert len(v) == 5
223            # Convert to strings, also make a copy of the list.
224            v = map(str, v)
225            v[0] = self._downloads.get_path(v[0])
226            # The output destination, ignore the output.
227            v.insert(4, '/dev/null')
228            # Insert the FPS requirement
229            v.append(str(INPUT_FPS))
230            test_stream_data.append(':'.join(v))
231        cmd_line = [
232            self.get_chrome_binary_path(VEA_BINARY),
233            '--gtest_filter=SimpleEncode/*/0',
234            '--test_stream_data=%s' % ';'.join(test_stream_data),
235            '--run_at_fps',
236            '--num_frames_to_encode=%d' % MAX_INT,
237            helper_logger.chrome_vmodule_flag(),
238        ]
239        cmd_line.append('--ozone-platform=gbm')
240        return cmd_line
241
242    def run_in_parallel(self, *commands):
243        env = os.environ.copy()
244
245        # To clear the temparory files created by vea_unittest.
246        env['TMPDIR'] = self.tmpdir
247        return map(lambda c: cmd_utils.popen(c, env=env), commands)
248
249    def simulate_hangout(self, decode_videos, encode_videos, measurer):
250        popens = self.run_in_parallel(
251            self.get_vda_unittest_cmd_line(decode_videos),
252            self.get_vea_unittest_cmd_line(encode_videos))
253        try:
254            time.sleep(STABILIZATION_DURATION)
255            measurer.start()
256            time.sleep(MEASUREMENT_DURATION)
257            measurement = measurer.stop()
258
259            # Ensure both encoding and decoding are still alive
260            if any(p.poll() is not None for p in popens):
261                raise error.TestError('vea/vda_unittest failed')
262
263            return measurement
264        finally:
265            cmd_utils.kill_or_log_returncode(*popens)
266
267    @helper_logger.video_log_wrapper
268    @chrome_binary_test.nuke_chrome
269    def run_once(self, resources, decode_videos, encode_videos, measurement):
270        self._downloads = DownloadManager(tmpdir = self.tmpdir)
271        try:
272            self._downloads.download_all(resources)
273            if measurement == 'cpu':
274                with CpuUsageMeasurer() as measurer:
275                    value = self.simulate_hangout(
276                            decode_videos, encode_videos, measurer)
277                    self.output_perf_value(
278                            description='cpu_usage', value=value * 100,
279                            units=UNIT_PERCENT, higher_is_better=False)
280            elif measurement == 'power':
281                with PowerMeasurer() as measurer:
282                    value = self.simulate_hangout(
283                            decode_videos, encode_videos, measurer)
284                    self.output_perf_value(
285                            description='power_usage', value=value,
286                            units=UNIT_WATT, higher_is_better=False)
287            else:
288                raise error.TestError('Unknown measurement: ' + measurement)
289        finally:
290            self._downloads.clear()
291