1# Copyright (c) 2014 The Chromium OS 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 collections
6import logging
7import os
8import time
9
10from autotest_lib.client.bin import test, utils
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.common_lib import file_utils
13from autotest_lib.client.common_lib.cros import chrome
14from autotest_lib.client.cros import service_stopper
15from autotest_lib.client.cros.power import power_rapl
16from autotest_lib.client.cros.power import power_status
17from autotest_lib.client.cros.power import power_utils
18from autotest_lib.client.cros.video import histogram_verifier
19from autotest_lib.client.cros.video import constants
20from autotest_lib.client.cros.video import helper_logger
21
22
23DISABLE_ACCELERATED_VIDEO_DECODE_BROWSER_ARGS = [
24        '--disable-accelerated-video-decode']
25DOWNLOAD_BASE = 'http://commondatastorage.googleapis.com/chromiumos-test-assets-public/'
26
27PLAYBACK_WITH_HW_ACCELERATION = 'playback_with_hw_acceleration'
28PLAYBACK_WITHOUT_HW_ACCELERATION = 'playback_without_hw_acceleration'
29
30# Measurement duration in seconds.
31MEASUREMENT_DURATION = 30
32# Time to exclude from calculation after playing a video [seconds].
33STABILIZATION_DURATION = 10
34
35# List of thermal throttling services that should be disabled.
36# - temp_metrics for link.
37# - thermal for daisy, snow, pit etc.
38THERMAL_SERVICES = ['temp_metrics', 'thermal']
39
40# Time in seconds to wait for cpu idle until giveup.
41WAIT_FOR_IDLE_CPU_TIMEOUT = 60.0
42# Maximum percent of cpu usage considered as idle.
43CPU_IDLE_USAGE = 0.1
44
45CPU_USAGE_DESCRIPTION = 'video_cpu_usage_'
46DROPPED_FRAMES_DESCRIPTION = 'video_dropped_frames_'
47DROPPED_FRAMES_PERCENT_DESCRIPTION = 'video_dropped_frames_percent_'
48POWER_DESCRIPTION = 'video_mean_energy_rate_'
49RAPL_GRAPH_NAME = 'rapl_power_consumption'
50
51# Minimum battery charge percentage to run the test
52BATTERY_INITIAL_CHARGED_MIN = 10
53
54
55class video_PlaybackPerf(test.test):
56    """
57    The test outputs the cpu usage, the dropped frame count and the power
58    consumption for video playback to performance dashboard.
59    """
60    version = 1
61    arc_mode = None
62
63
64    def initialize(self):
65        self._service_stopper = None
66        self._original_governors = None
67        self._backlight = None
68
69
70    def start_playback(self, cr, local_path):
71        """
72        Opens the video and plays it.
73
74        @param cr: Autotest Chrome instance.
75        @param local_path: path to the local video file to play.
76        """
77        cr.browser.platform.SetHTTPServerDirectories(self.bindir)
78
79        tab = cr.browser.tabs[0]
80        tab.Navigate(cr.browser.platform.http_server.UrlOf(local_path))
81        tab.WaitForDocumentReadyStateToBeComplete()
82        tab.EvaluateJavaScript("document.getElementsByTagName('video')[0]."
83                               "loop=true")
84
85
86    @helper_logger.video_log_wrapper
87    def run_once(self, video_name, video_description, power_test=False,
88                 arc_mode=None):
89        """
90        Runs the video_PlaybackPerf test.
91
92        @param video_name: the name of video to play in the DOWNLOAD_BASE
93        @param video_description: a string describes the video to play which
94                will be part of entry name in dashboard.
95        @param power_test: True if this is a power test and it would only run
96                the power test. If False, it would run the cpu usage test and
97                the dropped frame count test.
98        @param arc_mode: if 'enabled', run the test with Android enabled.
99        """
100        # Download test video.
101        url = DOWNLOAD_BASE + video_name
102        local_path = os.path.join(self.bindir, os.path.basename(video_name))
103        logging.info("Downloading %s to %s", url, local_path);
104        file_utils.download_file(url, local_path)
105        self.arc_mode = arc_mode
106
107        if not power_test:
108            # Run the video playback dropped frame tests.
109            keyvals = self.test_dropped_frames(local_path)
110
111            # Every dictionary value is a tuple. The first element of the tuple
112            # is dropped frames. The second is dropped frames percent.
113            keyvals_dropped_frames = {k: v[0] for k, v in keyvals.iteritems()}
114            keyvals_dropped_frames_percent = {
115                    k: v[1] for k, v in keyvals.iteritems()}
116
117            self.log_result(keyvals_dropped_frames, DROPPED_FRAMES_DESCRIPTION +
118                                video_description, 'frames')
119            self.log_result(keyvals_dropped_frames_percent,
120                            DROPPED_FRAMES_PERCENT_DESCRIPTION +
121                                video_description, 'percent')
122
123            # Run the video playback cpu usage tests.
124            keyvals = self.test_cpu_usage(local_path)
125            self.log_result(keyvals, CPU_USAGE_DESCRIPTION + video_description,
126                            'percent')
127        else:
128            tmp = self.test_power(local_path)
129            # Power measurement with rapl(4 domains) and system drain are
130            # reported. Reformat the data to align with the log_result.
131            keyvals = collections.defaultdict(dict)
132            for top_key in tmp.keys():
133                for key in tmp[top_key].keys():
134                    keyvals[key][top_key] = tmp[top_key][key]
135
136            for key in keyvals:
137                rapl_type = [keyword for keyword in power_rapl.VALID_DOMAINS
138                             if keyword in key]
139                if rapl_type:
140                    description = "%s_%s_pwr" % (video_description,
141                                                 rapl_type[0])
142                    self.log_result(keyvals[key], description, 'W',
143                                    graph=RAPL_GRAPH_NAME)
144                else:
145                    self.log_result(keyvals[key],
146                                    POWER_DESCRIPTION + video_description, 'W')
147
148
149    def test_dropped_frames(self, local_path):
150        """
151        Runs the video dropped frame test.
152
153        @param local_path: the path to the video file.
154
155        @return a dictionary that contains the test result.
156        """
157        def get_dropped_frames(cr):
158            time.sleep(MEASUREMENT_DURATION)
159            tab = cr.browser.tabs[0]
160            decoded_frame_count = tab.EvaluateJavaScript(
161                    "document.getElementsByTagName"
162                    "('video')[0].webkitDecodedFrameCount")
163            dropped_frame_count = tab.EvaluateJavaScript(
164                    "document.getElementsByTagName"
165                    "('video')[0].webkitDroppedFrameCount")
166            if decoded_frame_count != 0:
167                dropped_frame_percent = \
168                        100.0 * dropped_frame_count / decoded_frame_count
169            else:
170                logging.error("No frame is decoded. Set drop percent to 100.")
171                dropped_frame_percent = 100.0
172            logging.info("Decoded frames=%d, dropped frames=%d, percent=%f",
173                              decoded_frame_count,
174                              dropped_frame_count,
175                              dropped_frame_percent)
176            return (dropped_frame_count, dropped_frame_percent)
177        return self.test_playback(local_path, get_dropped_frames)
178
179
180    def test_cpu_usage(self, local_path):
181        """
182        Runs the video cpu usage test.
183
184        @param local_path: the path to the video file.
185
186        @return a dictionary that contains the test result.
187        """
188        def get_cpu_usage(cr):
189            time.sleep(STABILIZATION_DURATION)
190            cpu_usage_start = utils.get_cpu_usage()
191            time.sleep(MEASUREMENT_DURATION)
192            cpu_usage_end = utils.get_cpu_usage()
193            return utils.compute_active_cpu_time(cpu_usage_start,
194                                                      cpu_usage_end) * 100
195
196        # crbug/753292 - APNG login pictures increase CPU usage. Move the more
197        # strict idle checks after the login phase.
198        if not utils.wait_for_idle_cpu(WAIT_FOR_IDLE_CPU_TIMEOUT,
199                                       CPU_IDLE_USAGE):
200            logging.warning('Could not get idle CPU pre login.')
201        if not utils.wait_for_cool_machine():
202            logging.warning('Could not get cold machine pre login.')
203
204        # Stop the thermal service that may change the cpu frequency.
205        self._service_stopper = service_stopper.ServiceStopper(THERMAL_SERVICES)
206        self._service_stopper.stop_services()
207        # Set the scaling governor to performance mode to set the cpu to the
208        # highest frequency available.
209        self._original_governors = utils.set_high_performance_mode()
210        return self.test_playback(local_path, get_cpu_usage)
211
212
213    def test_power(self, local_path):
214        """
215        Runs the video power consumption test.
216
217        @param local_path: the path to the video file.
218
219        @return a dictionary that contains the test result.
220        """
221
222        self._backlight = power_utils.Backlight()
223        self._backlight.set_default()
224
225        self._service_stopper = service_stopper.ServiceStopper(
226                service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
227        self._service_stopper.stop_services()
228
229        self._power_status = power_status.get_status()
230        # We expect the DUT is powered by battery now. But this is not always
231        # true due to other bugs. Disable this test temporarily as workaround.
232        # TODO(kcwu): remove this workaround after AC control is stable
233        #             crbug.com/723968
234        if self._power_status.on_ac():
235            logging.warning('Still powered by AC. Skip this test')
236            return {}
237        # Verify that the battery is sufficiently charged.
238        self._power_status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN)
239
240        measurements = [power_status.SystemPower(
241                self._power_status.battery_path)]
242        if power_utils.has_rapl_support():
243            measurements += power_rapl.create_rapl()
244
245        def get_power(cr):
246            power_logger = power_status.PowerLogger(measurements)
247            power_logger.start()
248            time.sleep(STABILIZATION_DURATION)
249            start_time = time.time()
250            time.sleep(MEASUREMENT_DURATION)
251            power_logger.checkpoint('result', start_time)
252            keyval = power_logger.calc()
253            keyval = {key: keyval[key]
254                      for key in keyval if key.endswith('_pwr')}
255            return keyval
256
257        return self.test_playback(local_path, get_power)
258
259
260    def test_playback(self, local_path, gather_result):
261        """
262        Runs the video playback test with and without hardware acceleration.
263
264        @param local_path: the path to the video file.
265        @param gather_result: a function to run and return the test result
266                after chrome opens. The input parameter of the funciton is
267                Autotest chrome instance.
268
269        @return a dictionary that contains test the result.
270        """
271        keyvals = {}
272
273        with chrome.Chrome(
274                extra_browser_args=helper_logger.chrome_vmodule_flag(),
275                arc_mode=self.arc_mode,
276                init_network_controller=True) as cr:
277
278            # crbug/753292 - enforce the idle checks after login
279            if not utils.wait_for_idle_cpu(WAIT_FOR_IDLE_CPU_TIMEOUT,
280                                           CPU_IDLE_USAGE):
281                logging.warning('Could not get idle CPU post login.')
282            if not utils.wait_for_cool_machine():
283                logging.warning('Could not get cold machine post login.')
284
285            # Open the video playback page and start playing.
286            self.start_playback(cr, local_path)
287            result = gather_result(cr)
288
289            # Check if decode is hardware accelerated.
290            if histogram_verifier.is_bucket_present(
291                    cr,
292                    constants.MEDIA_GVD_INIT_STATUS,
293                    constants.MEDIA_GVD_BUCKET):
294                keyvals[PLAYBACK_WITH_HW_ACCELERATION] = result
295            else:
296                logging.info("Can not use hardware decoding.")
297                keyvals[PLAYBACK_WITHOUT_HW_ACCELERATION] = result
298                return keyvals
299
300        # Start chrome with disabled video hardware decode flag.
301        with chrome.Chrome(extra_browser_args=
302                DISABLE_ACCELERATED_VIDEO_DECODE_BROWSER_ARGS,
303                arc_mode=self.arc_mode, init_network_controller=True) as cr:
304            # Open the video playback page and start playing.
305            self.start_playback(cr, local_path)
306            result = gather_result(cr)
307
308            # Make sure decode is not hardware accelerated.
309            if histogram_verifier.is_bucket_present(
310                    cr,
311                    constants.MEDIA_GVD_INIT_STATUS,
312                    constants.MEDIA_GVD_BUCKET):
313                raise error.TestError(
314                        'Video decode acceleration should not be working.')
315            keyvals[PLAYBACK_WITHOUT_HW_ACCELERATION] = result
316
317        return keyvals
318
319
320    def log_result(self, keyvals, description, units, graph=None):
321        """
322        Logs the test result output to the performance dashboard.
323
324        @param keyvals: a dictionary that contains results returned by
325                test_playback.
326        @param description: a string that describes the video and test result
327                and it will be part of the entry name in the dashboard.
328        @param units: the units of test result.
329        @param graph: a string that indicates which graph should the result
330                      belongs to.
331        """
332        result_with_hw = keyvals.get(PLAYBACK_WITH_HW_ACCELERATION)
333        if result_with_hw is not None:
334            self.output_perf_value(
335                    description= 'hw_' + description, value=result_with_hw,
336                    units=units, higher_is_better=False, graph=graph)
337
338        result_without_hw = keyvals.get(PLAYBACK_WITHOUT_HW_ACCELERATION)
339        if result_without_hw is not None:
340            self.output_perf_value(
341                    description= 'sw_' + description, value=result_without_hw,
342                    units=units, higher_is_better=False, graph=graph)
343
344
345    def cleanup(self):
346        # cleanup() is run by common_lib/test.py.
347        if self._backlight:
348            self._backlight.restore()
349        if self._service_stopper:
350            self._service_stopper.restore_services()
351        if self._original_governors:
352            utils.restore_scaling_governor_states(self._original_governors)
353
354        super(video_PlaybackPerf, self).cleanup()
355