1# Copyright (c) 2013 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"""This is a client side WebGL aquarium test."""
5
6import logging
7import math
8import os
9import sampler
10import threading
11import time
12
13from autotest_lib.client.bin import test, utils
14from autotest_lib.client.common_lib import error
15from autotest_lib.client.common_lib.cros import chrome
16from autotest_lib.client.cros.graphics import graphics_utils
17from autotest_lib.client.cros import power_status, power_utils
18from autotest_lib.client.cros import service_stopper
19
20# Minimum battery charge percentage to run the test
21BATTERY_INITIAL_CHARGED_MIN = 10
22
23# Measurement duration in seconds.
24MEASUREMENT_DURATION = 30
25
26POWER_DESCRIPTION = 'avg_energy_rate_1000_fishes'
27
28# Time to exclude from calculation after playing a webgl demo [seconds].
29STABILIZATION_DURATION = 10
30
31
32class graphics_WebGLAquarium(test.test):
33    """WebGL aquarium graphics test."""
34    version = 1
35
36    _backlight = None
37    _power_status = None
38    _service_stopper = None
39    _test_power = False
40    active_tab = None
41    flip_stats = {}
42    GSC = None
43    kernel_sampler = None
44    perf_keyval = {}
45    sampler_lock = None
46    test_duration_secs = 30
47    test_setting_num_fishes = 50
48    test_settings = {50: ('setSetting2', 2), 1000: ('setSetting6', 6),}
49
50    def setup(self):
51        tarball_path = os.path.join(self.bindir,
52                                    'webgl_aquarium_static.tar.bz2')
53        utils.extract_tarball_to_dir(tarball_path, self.srcdir)
54
55    def initialize(self):
56        self.GSC = graphics_utils.GraphicsStateChecker()
57        self.sampler_lock = threading.Lock()
58        # TODO: Create samplers for other platforms (e.g. x86).
59        if utils.get_board().lower() in ['daisy', 'daisy_spring']:
60            # Enable ExynosSampler on Exynos platforms.  The sampler looks for
61            # exynos-drm page flip states: 'wait_kds', 'rendered', 'prepared',
62            # and 'flipped' in kernel debugfs.
63
64            # Sample 3-second durtaion for every 5 seconds.
65            self.kernel_sampler = sampler.ExynosSampler(period=5, duration=3)
66            self.kernel_sampler.sampler_callback = self.exynos_sampler_callback
67            self.kernel_sampler.output_flip_stats = (
68                self.exynos_output_flip_stats)
69
70    def cleanup(self):
71        if self._backlight:
72            self._backlight.restore()
73        if self._service_stopper:
74            self._service_stopper.restore_services()
75        if self.GSC:
76            keyvals = self.GSC.get_memory_keyvals()
77            if not self._test_power:
78                for key, val in keyvals.iteritems():
79                    self.output_perf_value(description=key,
80                                           value=val,
81                                           units='bytes',
82                                           higher_is_better=False)
83            self.GSC.finalize()
84            self.write_perf_keyval(keyvals)
85
86    def run_fish_test(self, browser, test_url, num_fishes, perf_log=True):
87        """Run the test with the given number of fishes.
88
89        @param browser: The Browser object to run the test with.
90        @param test_url: The URL to the aquarium test site.
91        @param num_fishes: The number of fishes to run the test with.
92        @param perf_log: Report perf data only if it's set to True.
93        """
94        # Create tab and load page. Set the number of fishes when page is fully
95        # loaded.
96        tab = browser.tabs.New()
97        tab.Navigate(test_url)
98        tab.Activate()
99        self.active_tab = tab
100        tab.WaitForDocumentReadyStateToBeComplete()
101
102        # Set the number of fishes when document finishes loading.  Also reset
103        # our own FPS counter and start recording FPS and rendering time.
104        utils.wait_for_value(
105            lambda: tab.EvaluateJavaScript(
106                'if (document.readyState === "complete") {'
107                '  setSetting(document.getElementById("%s"), %d);'
108                '  g_crosFpsCounter.reset();'
109                '  true;'
110                '} else {'
111                '  false;'
112                '}' % self.test_settings[num_fishes]),
113            expected_value=True,
114            timeout_sec=30)
115
116        if self.kernel_sampler:
117            self.kernel_sampler.start_sampling_thread()
118        time.sleep(self.test_duration_secs)
119        if self.kernel_sampler:
120            self.kernel_sampler.stop_sampling_thread()
121            self.kernel_sampler.output_flip_stats('flip_stats_%d' % num_fishes)
122            self.flip_stats = {}
123
124        if perf_log:
125            # Get average FPS and rendering time, then close the tab.
126            avg_fps = tab.EvaluateJavaScript('g_crosFpsCounter.getAvgFps();')
127            if math.isnan(float(avg_fps)):
128                raise error.TestFail('Could not get FPS count.')
129            avg_render_time = tab.EvaluateJavaScript(
130                'g_crosFpsCounter.getAvgRenderTime();')
131            self.perf_keyval['avg_fps_%04d_fishes' % num_fishes] = avg_fps
132            self.perf_keyval['avg_render_time_%04d_fishes' % num_fishes] = (
133                avg_render_time)
134            self.output_perf_value(
135                description='avg_fps_%04d_fishes' % num_fishes,
136                value=avg_fps,
137                units='fps',
138                higher_is_better=True)
139            logging.info('%d fish(es): Average FPS = %f, '
140                         'average render time = %f', num_fishes, avg_fps,
141                         avg_render_time)
142
143    def run_power_test(self, browser, test_url):
144        """Runs the webgl power consumption test and reports the perf results.
145
146        @param browser: The Browser object to run the test with.
147        @param test_url: The URL to the aquarium test site.
148        """
149
150        self._backlight = power_utils.Backlight()
151        self._backlight.set_default()
152
153        self._service_stopper = service_stopper.ServiceStopper(
154            service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
155        self._service_stopper.stop_services()
156
157        self._power_status = power_status.get_status()
158        # Verify that we are running on battery and the battery is sufficiently
159        # charged.
160        self._power_status.assert_battery_state(BATTERY_INITIAL_CHARGED_MIN)
161
162        measurements = [
163            power_status.SystemPower(self._power_status.battery_path)
164        ]
165
166        def get_power():
167            power_logger = power_status.PowerLogger(measurements)
168            power_logger.start()
169            time.sleep(STABILIZATION_DURATION)
170            start_time = time.time()
171            time.sleep(MEASUREMENT_DURATION)
172            power_logger.checkpoint('result', start_time)
173            keyval = power_logger.calc()
174            logging.info('Power output %s', keyval)
175            return keyval['result_' + measurements[0].domain + '_pwr']
176
177        self.run_fish_test(browser, test_url, 1000, perf_log=False)
178        energy_rate = get_power()
179        # This is a power specific test so we are not capturing
180        # avg_fps and avg_render_time in this test.
181        self.perf_keyval[POWER_DESCRIPTION] = energy_rate
182        self.output_perf_value(description=POWER_DESCRIPTION,
183                               value=energy_rate,
184                               units='W',
185                               higher_is_better=False)
186
187    def exynos_sampler_callback(self, sampler_obj):
188        """Sampler callback function for ExynosSampler.
189
190        @param sampler_obj: The ExynosSampler object that invokes this callback
191                function.
192        """
193        if sampler_obj.stopped:
194            return
195
196        with self.sampler_lock:
197            now = time.time()
198            results = {}
199            info_str = ['\nfb_id wait_kds flipped']
200            for value in sampler_obj.frame_buffers.itervalues():
201                results[value.fb] = {}
202                for state, stats in value.states.iteritems():
203                    results[value.fb][state] = (stats.avg, stats.stdev)
204                info_str.append('%s: %s %s' %
205                                (value.fb, results[value.fb]['wait_kds'][0],
206                                 results[value.fb]['flipped'][0]))
207            results['avg_fps'] = self.active_tab.EvaluateJavaScript(
208                'g_crosFpsCounter.getAvgFps();')
209            results['avg_render_time'] = self.active_tab.EvaluateJavaScript(
210                'g_crosFpsCounter.getAvgRenderTime();')
211            self.active_tab.ExecuteJavaScript('g_crosFpsCounter.reset();')
212            info_str.append('avg_fps: %s, avg_render_time: %s' %
213                            (results['avg_fps'], results['avg_render_time']))
214            self.flip_stats[now] = results
215            logging.info('\n'.join(info_str))
216
217    def exynos_output_flip_stats(self, file_name):
218        """Pageflip statistics output function for ExynosSampler.
219
220        @param file_name: The output file name.
221        """
222        # output format:
223        # time fb_id avg_rendered avg_prepared avg_wait_kds avg_flipped
224        # std_rendered std_prepared std_wait_kds std_flipped
225        with open(file_name, 'w') as f:
226            for t in sorted(self.flip_stats.keys()):
227                if ('avg_fps' in self.flip_stats[t] and
228                        'avg_render_time' in self.flip_stats[t]):
229                    f.write('%s %s %s\n' %
230                            (t, self.flip_stats[t]['avg_fps'],
231                             self.flip_stats[t]['avg_render_time']))
232                for fb, stats in self.flip_stats[t].iteritems():
233                    if not isinstance(fb, int):
234                        continue
235                    f.write('%s %s ' % (t, fb))
236                    f.write('%s %s %s %s ' % (stats['rendered'][0],
237                                              stats['prepared'][0],
238                                              stats['wait_kds'][0],
239                                              stats['flipped'][0]))
240                    f.write('%s %s %s %s\n' % (stats['rendered'][1],
241                                               stats['prepared'][1],
242                                               stats['wait_kds'][1],
243                                               stats['flipped'][1]))
244    def run_once(self,
245                 test_duration_secs=30,
246                 test_setting_num_fishes=(50, 1000),
247                 power_test=False):
248        """Find a brower with telemetry, and run the test.
249
250        @param test_duration_secs: The duration in seconds to run each scenario
251                for.
252        @param test_setting_num_fishes: A list of the numbers of fishes to
253                enable in the test.
254        """
255        self.test_duration_secs = test_duration_secs
256        self.test_setting_num_fishes = test_setting_num_fishes
257
258        with chrome.Chrome(logged_in=False) as cr:
259            cr.browser.platform.SetHTTPServerDirectories(self.srcdir)
260            test_url = cr.browser.platform.http_server.UrlOf(os.path.join(
261                self.srcdir, 'aquarium.html'))
262
263            if not utils.wait_for_idle_cpu(60.0, 0.1):
264                if not utils.wait_for_idle_cpu(20.0, 0.2):
265                    raise error.TestFail('Could not get idle CPU.')
266            if not utils.wait_for_cool_machine():
267                raise error.TestFail('Could not get cold machine.')
268            if power_test:
269                self._test_power = True
270                self.run_power_test(cr.browser, test_url)
271                with self.sampler_lock:
272                    self.active_tab.Close()
273                    self.active_tab = None
274            else:
275                for n in self.test_setting_num_fishes:
276                    self.run_fish_test(cr.browser, test_url, n)
277                    # Do not close the tab when the sampler_callback is doing
278                    # his work.
279                    with self.sampler_lock:
280                        self.active_tab.Close()
281                        self.active_tab = None
282        self.write_perf_keyval(self.perf_keyval)
283