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 logging
6import os
7import time
8
9from autotest_lib.client.bin import site_utils, test, utils
10from autotest_lib.client.common_lib import error
11from autotest_lib.client.common_lib import file_utils
12from autotest_lib.client.common_lib.cros import chrome
13from autotest_lib.client.cros import power_status, power_utils
14from autotest_lib.client.cros import service_stopper
15from autotest_lib.client.cros.video import histogram_verifier
16from autotest_lib.client.cros.video import constants
17
18
19EXTRA_BROWSER_ARGS = ['--use-fake-device-for-media-stream',
20                      '--use-fake-ui-for-media-stream']
21FAKE_FILE_ARG = '--use-file-for-fake-video-capture="%s"'
22DISABLE_ACCELERATED_VIDEO_DECODE_BROWSER_ARGS = [
23        '--disable-accelerated-video-decode']
24
25DOWNLOAD_BASE = ('http://commondatastorage.googleapis.com/'
26                 'chromiumos-test-assets-public/crowd/')
27VIDEO_NAME = 'crowd720_25frames.y4m'
28
29WEBRTC_INTERNALS_URL = 'chrome://webrtc-internals'
30
31WEBRTC_WITH_HW_ACCELERATION = 'webrtc_with_hw_acceleration'
32WEBRTC_WITHOUT_HW_ACCELERATION = 'webrtc_without_hw_acceleration'
33
34# Measurement duration in seconds.
35MEASUREMENT_DURATION = 30
36# Time to exclude from calculation after start the loopback [seconds].
37STABILIZATION_DURATION = 10
38
39# List of thermal throttling services that should be disabled.
40# - temp_metrics for link.
41# - thermal for daisy, snow, pit etc.
42THERMAL_SERVICES = ['temp_metrics', 'thermal']
43
44# Time in seconds to wait for cpu idle until giving up.
45WAIT_FOR_IDLE_CPU_TIMEOUT = 60.0
46# Maximum percent of cpu usage considered as idle.
47CPU_IDLE_USAGE = 0.1
48
49MAX_DECODE_TIME_DESCRIPTION = 'decode_time.max'
50MEDIAN_DECODE_TIME_DESCRIPTION = 'decode_time.percentile_0.50'
51CPU_USAGE_DESCRIPTION = 'video_cpu_usage'
52POWER_DESCRIPTION = 'video_mean_energy_rate'
53
54# Minimum battery charge percentage to run the power test.
55BATTERY_INITIAL_CHARGED_MIN = 10
56
57# These are the variable names in WebRTC internals.
58# Maximum decode time of the frames of the last 10 seconds.
59GOOG_MAX_DECODE_MS = 'googMaxDecodeMs'
60# The decode time of the last frame.
61GOOG_DECODE_MS = 'googDecodeMs'
62
63# Javascript function to get the decode time. addStats is a function called by
64# Chrome to pass WebRTC statistics every second.
65ADD_STATS_JAVASCRIPT = (
66        'var googMaxDecodeMs = new Array();'
67        'var googDecodeMs = new Array();'
68        'addStats = function(data) {'
69        '  reports = data.reports;'
70        '  for (var i=0; i < reports.length; i++) {'
71        '    if (reports[i].type == "ssrc") {'
72        '      values = reports[i].stats.values;'
73        '      for (var j=0; j < values.length; j++) {'
74        '        if (values[j] == "googMaxDecodeMs")'
75        '          googMaxDecodeMs[googMaxDecodeMs.length] = values[j+1];'
76        '        else if (values[j] == "googDecodeMs")'
77        '          googDecodeMs[googDecodeMs.length] = values[j+1];'
78        '      }'
79        '    }'
80        '  }'
81        '}')
82
83# Measure the stats until getting 10 samples or exceeding 15 seconds. addStats
84# is called once per second for now.
85NUM_DECODE_TIME_SAMPLES = 10
86TIMEOUT = 15
87
88
89class video_WebRtcPerf(test.test):
90    """
91    The test outputs the decode time, cpu usage and the power consumption for
92    WebRTC to performance dashboard.
93    """
94    version = 1
95    arc_mode = None
96
97
98    def start_loopback(self, cr):
99        """
100        Opens WebRTC loopback page.
101
102        @param cr: Autotest Chrome instance.
103        """
104        cr.browser.platform.SetHTTPServerDirectories(self.bindir)
105
106        tab = cr.browser.tabs[0]
107        tab.Navigate(cr.browser.platform.http_server.UrlOf(
108                os.path.join(self.bindir, 'loopback.html')))
109        tab.WaitForDocumentReadyStateToBeComplete()
110
111
112    def open_stats_page(self, cr):
113        """
114        Opens WebRTC internal statistics page.
115
116        @param cr: Autotest Chrome instance.
117
118        @returns the tab of the stats page.
119        """
120        tab = cr.browser.tabs.New()
121        tab.Navigate(WEBRTC_INTERNALS_URL)
122        tab.WaitForDocumentReadyStateToBeComplete()
123        # Inject stats callback function.
124        tab.EvaluateJavaScript(ADD_STATS_JAVASCRIPT)
125        return tab
126
127
128    def run_once(self, decode_time_test=False, cpu_test=False,
129                 power_test=False, arc_mode=None):
130        """
131        Runs the video_WebRtcPerf test.
132
133        @param decode_time_test: Pass True to run decode time test.
134        @param cpu_test: Pass True to run CPU usage test.
135        @param power_test: Pass True to run power consumption test.
136        @param arc_mode: if 'enabled', run the test with Android enabled.
137        """
138        # Download test video.
139        url = DOWNLOAD_BASE + VIDEO_NAME
140        local_path = os.path.join(self.bindir, VIDEO_NAME)
141        file_utils.download_file(url, local_path)
142        self.arc_mode = arc_mode
143
144        if decode_time_test:
145            keyvals = self.test_decode_time(local_path)
146            # The first value is max decode time. The second value is median
147            # decode time. Construct a dictionary containing one value to log
148            # the result.
149            max_decode_time = {
150                    key:value[0] for (key, value) in keyvals.items()}
151            self.log_result(max_decode_time, MAX_DECODE_TIME_DESCRIPTION,
152                            'milliseconds')
153            median_decode_time = {
154                    key:value[1] for (key, value) in keyvals.items()}
155            self.log_result(median_decode_time, MEDIAN_DECODE_TIME_DESCRIPTION,
156                            'milliseconds')
157
158        if cpu_test:
159            keyvals = self.test_cpu_usage(local_path)
160            self.log_result(keyvals, CPU_USAGE_DESCRIPTION, 'percent')
161
162        if power_test:
163            keyvals = self.test_power(local_path)
164            self.log_result(keyvals, POWER_DESCRIPTION , 'W')
165
166
167    def test_webrtc(self, local_path, gather_result):
168        """
169        Runs the webrtc test with and without hardware acceleration.
170
171        @param local_path: the path to the video file.
172        @param gather_result: a function to run and return the test result
173                after chrome opens. The input parameter of the funciton is
174                Autotest chrome instance.
175
176        @return a dictionary that contains test the result.
177        """
178        keyvals = {}
179        EXTRA_BROWSER_ARGS.append(FAKE_FILE_ARG % local_path)
180
181        with chrome.Chrome(extra_browser_args=EXTRA_BROWSER_ARGS,
182                           arc_mode=self.arc_mode,
183                           init_network_controller=True) as cr:
184            # On daisy, Chrome freezes about 30 seconds after login because of
185            # TPM error. See http://crbug.com/588579.
186            if utils.get_board() == 'daisy':
187              logging.warning('Delay 30s for issue 588579 on daisy')
188              time.sleep(30)
189            # Open WebRTC loopback page and start the loopback.
190            self.start_loopback(cr)
191            result = gather_result(cr)
192
193            # Check if decode is hardware accelerated.
194            if histogram_verifier.is_bucket_present(
195                    cr,
196                    constants.RTC_INIT_HISTOGRAM,
197                    constants.RTC_VIDEO_INIT_BUCKET):
198                keyvals[WEBRTC_WITH_HW_ACCELERATION] = result
199            else:
200                logging.info("Can not use hardware decoding.")
201                keyvals[WEBRTC_WITHOUT_HW_ACCELERATION] = result
202                return keyvals
203
204        # Start chrome with disabled video hardware decode flag.
205        with chrome.Chrome(extra_browser_args=
206                DISABLE_ACCELERATED_VIDEO_DECODE_BROWSER_ARGS +
207                EXTRA_BROWSER_ARGS, arc_mode=self.arc_mode) as cr:
208            if utils.get_board() == 'daisy':
209              logging.warning('Delay 30s for issue 588579 on daisy')
210              time.sleep(30)
211            # Open the webrtc loopback page and start the loopback.
212            self.start_loopback(cr)
213            result = gather_result(cr)
214
215            # Make sure decode is not hardware accelerated.
216            if histogram_verifier.is_bucket_present(
217                    cr,
218                    constants.RTC_INIT_HISTOGRAM,
219                    constants.RTC_VIDEO_INIT_BUCKET):
220                raise error.TestError('HW decode should not be used.')
221            keyvals[WEBRTC_WITHOUT_HW_ACCELERATION] = result
222
223        return keyvals
224
225
226    def test_cpu_usage(self, local_path):
227        """
228        Runs the video cpu usage test.
229
230        @param local_path: the path to the video file.
231
232        @return a dictionary that contains the test result.
233        """
234        def get_cpu_usage(cr):
235            time.sleep(STABILIZATION_DURATION)
236            cpu_usage_start = site_utils.get_cpu_usage()
237            time.sleep(MEASUREMENT_DURATION)
238            cpu_usage_end = site_utils.get_cpu_usage()
239            return site_utils.compute_active_cpu_time(cpu_usage_start,
240                                                      cpu_usage_end) * 100
241        if not utils.wait_for_idle_cpu(WAIT_FOR_IDLE_CPU_TIMEOUT,
242                                       CPU_IDLE_USAGE):
243            raise error.TestError('Could not get idle CPU.')
244        if not utils.wait_for_cool_machine():
245            raise error.TestError('Could not get cool machine.')
246        # Stop the thermal service that may change the cpu frequency.
247        services = service_stopper.ServiceStopper(THERMAL_SERVICES)
248        services.stop_services()
249        # Set the scaling governor to performance mode to set the cpu to the
250        # highest frequency available.
251        original_governors = utils.set_high_performance_mode()
252        try:
253            return self.test_webrtc(local_path, get_cpu_usage)
254        finally:
255            services.restore_services()
256            utils.restore_scaling_governor_states(original_governors)
257
258
259    def test_power(self, local_path):
260        """
261        Runs the video power consumption test.
262
263        @param local_path: the path to the video file.
264
265        @return a dictionary that contains the test result.
266        """
267        # Verify that we are running on battery and the battery is sufficiently
268        # charged.
269        current_power_status = power_status.get_status()
270        current_power_status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN)
271
272        measurements = [power_status.SystemPower(
273                current_power_status.battery_path)]
274
275        def get_power(cr):
276            power_logger = power_status.PowerLogger(measurements)
277            power_logger.start()
278            time.sleep(STABILIZATION_DURATION)
279            start_time = time.time()
280            time.sleep(MEASUREMENT_DURATION)
281            power_logger.checkpoint('result', start_time)
282            keyval = power_logger.calc()
283            return keyval['result_' + measurements[0].domain + '_pwr']
284
285        backlight = power_utils.Backlight()
286        backlight.set_default()
287        services = service_stopper.ServiceStopper(
288                service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
289        services.stop_services()
290        try:
291            return self.test_webrtc(local_path, get_power)
292        finally:
293            backlight.restore()
294            services.restore_services()
295
296
297    def test_decode_time(self, local_path):
298        """
299        Runs the decode time test.
300
301        @param local_path: the path to the video file.
302
303        @return a dictionary that contains the test result.
304        """
305        def get_decode_time(cr):
306            tab = self.open_stats_page(cr)
307            # Collect the decode time until there are enough samples.
308            start_time = time.time()
309            max_decode_time_list = []
310            decode_time_list = []
311            while (time.time() - start_time < TIMEOUT and
312                   len(decode_time_list) < NUM_DECODE_TIME_SAMPLES):
313                time.sleep(1)
314                max_decode_time_list = []
315                decode_time_list = []
316                try:
317                    max_decode_time_list = [int(x) for x in
318                            tab.EvaluateJavaScript(GOOG_MAX_DECODE_MS)]
319                    decode_time_list = [int(x) for x in
320                            tab.EvaluateJavaScript(GOOG_DECODE_MS)]
321                except:
322                    pass
323            # Output the values if they are valid.
324            if len(max_decode_time_list) < NUM_DECODE_TIME_SAMPLES:
325                raise error.TestError('Not enough ' + GOOG_MAX_DECODE_MS)
326            if len(decode_time_list) < NUM_DECODE_TIME_SAMPLES:
327                raise error.TestError('Not enough ' + GOOG_DECODE_MS)
328            max_decode_time = max(max_decode_time_list)
329            decode_time_median = self.get_median(decode_time_list)
330            logging.info("Max decode time list=%s", str(max_decode_time_list))
331            logging.info("Decode time list=%s", str(decode_time_list))
332            logging.info("Maximum decode time=%d, median=%d", max_decode_time,
333                         decode_time_median)
334            return (max_decode_time, decode_time_median)
335
336        return self.test_webrtc(local_path, get_decode_time)
337
338
339    def get_median(self, seq):
340        """
341        Calculates the median of a sequence of numbers.
342
343        @param seq: a list with numbers.
344
345        @returns the median of the numbers.
346        """
347        seq.sort()
348        size = len(seq)
349        if size % 2 != 0:
350            return seq[size / 2]
351        return (seq[size / 2] + seq[size / 2 - 1]) / 2.0
352
353    def log_result(self, keyvals, description, units):
354        """
355        Logs the test result output to the performance dashboard.
356
357        @param keyvals: a dictionary that contains results returned by
358                test_webrtc.
359        @param description: a string that describes the video and test result
360                and it will be part of the entry name in the dashboard.
361        @param units: the units of test result.
362        """
363        result_with_hw = keyvals.get(WEBRTC_WITH_HW_ACCELERATION)
364        if result_with_hw:
365            self.output_perf_value(
366                    description= 'hw_' + description, value=result_with_hw,
367                    units=units, higher_is_better=False)
368
369        result_without_hw = keyvals.get(WEBRTC_WITHOUT_HW_ACCELERATION)
370        if result_without_hw:
371            self.output_perf_value(
372                    description= 'sw_' + description, value=result_without_hw,
373                    units=units, higher_is_better=False)
374