• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2012 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
8import urllib
9
10from autotest_lib.client.bin import test, utils
11from autotest_lib.client.common_lib import error
12from autotest_lib.client.common_lib.cros import chrome
13from autotest_lib.client.cros import backchannel
14from autotest_lib.client.cros import httpd
15from autotest_lib.client.cros import service_stopper
16from autotest_lib.client.cros.graphics import graphics_utils
17from autotest_lib.client.cros.networking import wifi_proxy
18from autotest_lib.client.cros.power import power_rapl, power_status, power_utils
19
20
21class power_Consumption(test.test):
22    """Measure power consumption for different types of loads.
23
24    This test runs a series of different tasks like media playback, flash
25    animation, large file download etc. It measures and reports power
26    consumptions during each of those tasks.
27    """
28
29    version = 2
30
31
32    def initialize(self, ac_ok=False):
33        """Initialize test.
34
35        Args:
36            ac_ok: boolean to allow running on AC
37        """
38        # Objects that need to be taken care of in cleanup() are initialized
39        # here to None. Otherwise we run the risk of AttributeError raised in
40        # cleanup() masking a real error that caused the test to fail during
41        # initialize() before those variables were assigned.
42        self._backlight = None
43        self._tmp_keyvals = {}
44
45        self._services = service_stopper.ServiceStopper(
46            service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
47        self._services.stop_services()
48
49
50        # Time to exclude from calculation after firing a task [seconds]
51        self._stabilization_seconds = 5
52        self._power_status = power_status.get_status()
53        self._tmp_keyvals['b_on_ac'] = self._power_status.on_ac()
54
55        if not ac_ok:
56            # Verify that we are running on battery and the battery is
57            # sufficiently charged
58            self._power_status.assert_battery_state(30)
59
60        # Local data and web server settings. Tarballs with traditional names
61        # like *.tgz don't get copied to the image by ebuilds (see
62        # AUTOTEST_FILE_MASK in autotest-chrome ebuild).
63        self._static_sub_dir = 'static_sites'
64        utils.extract_tarball_to_dir(
65                'static_sites.tgz.keep',
66                os.path.join(self.bindir, self._static_sub_dir))
67        self._media_dir = '/home/chronos/user/Downloads/'
68        self._httpd_port = 8000
69        self._url_base = 'http://localhost:%s/' % self._httpd_port
70        self._test_server = httpd.HTTPListener(self._httpd_port,
71                                               docroot=self.bindir)
72
73        # initialize various interesting power related stats
74        self._statomatic = power_status.StatoMatic()
75        self._test_server.run()
76
77
78        logging.info('initialize() finished')
79
80
81    def _download_test_data(self):
82        """Download audio and video files.
83
84        This is also used as payload for download test.
85
86        Note, can reach payload via browser at
87          https://console.developers.google.com/storage/chromeos-test-public/big_buck_bunny
88        Start with README
89        """
90
91        repo = 'http://commondatastorage.googleapis.com/chromeos-test-public/'
92        file_list = [repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.mp4', ]
93        if not self.short:
94            file_list += [
95                repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.ogg',
96                repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp8.webm',
97                repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp9.webm',
98                repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.mp4',
99                repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.ogg',
100                repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp8.webm',
101                repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp9.webm',
102                repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.mp4',
103                repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.ogg',
104                repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp8.webm',
105                repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp9.webm',
106                repo + 'wikimedia/Greensleeves.ogg',
107                ]
108
109        for url in file_list:
110            logging.info('Downloading %s', url)
111            utils.unmap_url('', url, self._media_dir)
112
113
114    def _toggle_fullscreen(self):
115        """Toggle full screen mode."""
116        # Note: full screen mode toggled with F11 is different from clicking the
117        # full screen icon on video player controls. This needs improvement.
118        # Bug: http://crbug.com/248939
119        graphics_utils.screen_toggle_fullscreen()
120
121
122    # Below are a series of generic sub-test runners. They run a given task
123    # and record the task name and start-end timestamps for future computation
124    # of power consumption during the task.
125    def _run_func(self, name, func, repeat=1, save_checkpoint=True):
126        """Run a given python function as a sub-test."""
127        start_time = time.time() + self._stabilization_seconds
128        for _ in xrange(repeat):
129            ret = func()
130        if save_checkpoint:
131            self._plog.checkpoint(name, start_time)
132        return ret
133
134
135    def _run_sleep(self, name, seconds=60):
136        """Just sleep and record it as a named sub-test"""
137        start_time = time.time() + self._stabilization_seconds
138        time.sleep(seconds)
139        self._plog.checkpoint(name, start_time)
140
141
142    def _run_cmd(self, name, cmd, repeat=1):
143        """Run command in a shell as a sub-test"""
144        start_time = time.time() + self._stabilization_seconds
145        for _ in xrange(repeat):
146            logging.info('Executing command: %s', cmd)
147            exit_status = utils.system(cmd, ignore_status=True)
148            if exit_status != 0:
149                logging.error('run_cmd: the following command terminated with'
150                                'a non zero exit status: %s', cmd)
151        self._plog.checkpoint(name, start_time)
152        return exit_status
153
154
155    def _run_until(self, name, predicate, timeout=60):
156        """Probe the |predicate| function  and wait until it returns true.
157        Record the waiting time as a sub-test
158        """
159        start_time = time.time() + self._stabilization_seconds
160        utils.poll_for_condition(predicate, timeout=timeout)
161        self._plog.checkpoint(name, start_time)
162
163
164    def _run_url(self, name, url, duration):
165        """Navigate to URL, sleep for some time and record it as a sub-test."""
166        logging.info('Navigating to %s', url)
167        self._tab.Activate()
168        self._tab.Navigate(url)
169        self._run_sleep(name, duration)
170        tab_title = self._tab.EvaluateJavaScript('document.title')
171        logging.info('Sub-test name: %s Tab title: %s.', name, tab_title)
172
173
174    def _run_url_bg(self, name, url, duration):
175        """Run a web site in background tab.
176
177        Navigate to the given URL, open an empty tab to put the one with the
178        URL in background, then sleep and record it as a sub-test.
179
180        Args:
181            name: sub-test name.
182            url: url to open in background tab.
183            duration: number of seconds to sleep while taking measurements.
184        """
185        bg_tab = self._tab
186        bg_tab.Navigate(url)
187        # Let it load and settle
188        time.sleep(self._stabilization_seconds / 2.)
189        tab_title = bg_tab.EvaluateJavaScript('document.title')
190        logging.info('App name: %s Tab title: %s.', name, tab_title)
191        # Open a new empty tab to cover the one with test payload.
192        fg_tab = self._browser.tabs.New()
193        fg_tab.Activate()
194        self._run_sleep(name, duration)
195        fg_tab.Close()
196        bg_tab.Activate()
197
198
199    def _run_group_download(self):
200        """Download over ethernet. Using video test data as payload."""
201
202        # For short run, the payload is too small to take measurement
203        self._run_func('download_eth',
204                       self._download_test_data ,
205                       repeat=self._repeats,
206                       save_checkpoint=not(self.short))
207
208
209    def _run_group_webpages(self):
210        """Runs a series of web pages as sub-tests."""
211        data_url = self._url_base + self._static_sub_dir + '/'
212
213        # URLs to be only tested in foreground tab.
214        # Can't use about:blank here - crbug.com/248945
215        # but chrome://version is just as good for our needs.
216        urls = [('ChromeVer', 'chrome://version/')]
217        # URLs to be tested in both, background and foreground modes.
218        bg_urls = []
219
220        more_urls = [('BallsDHTML',
221                      data_url + 'balls/DHTMLBalls/dhtml.htm'),
222                     ('BallsFlex',
223                      data_url + 'balls/FlexBalls/flexballs.html'),
224                    ]
225
226        if self.short:
227            urls += more_urls
228        else:
229            bg_urls += more_urls
230            bg_urls += [('Parapluesch',
231                         'http://www.parapluesch.de/whiskystore/test.htm'),
232                         ('PosterCircle',
233                          'http://www.webkit.org'
234                          '/blog-files/3d-transforms/poster-circle.html'), ]
235
236        for name, url in urls + bg_urls:
237            self._run_url(name, url, duration=self._duration_secs)
238
239        for name, url in bg_urls:
240            self._run_url_bg('bg_' + name, url, duration=self._duration_secs)
241
242
243    def _run_group_speedometer(self):
244        """Run the Speedometer benchmark suite as a sub-test.
245
246        Fire it up and wait until it displays "Score".
247        """
248
249        # TODO: check in a local copy of the test if we can get permission if
250        # the network causes problems.
251        url = 'http://browserbench.org/Speedometer/'
252        start_js = 'startTest()'
253        score_js = "document.getElementById('result-number').innerText"
254        tab = self._tab
255
256        def speedometer_func():
257            """To be passed as the callable to self._run_func()"""
258            tab.Navigate(url)
259            tab.WaitForDocumentReadyStateToBeComplete()
260            tab.EvaluateJavaScript(start_js)
261            # Speedometer test should be done in less than 15 minutes (actual
262            # runs are closer to 5).
263            is_done = lambda: tab.EvaluateJavaScript(score_js) != ""
264            time.sleep(self._stabilization_seconds)
265            utils.poll_for_condition(is_done, timeout=900,
266                                     desc='Speedometer score found')
267
268        self._run_func('Speedometer', speedometer_func, repeat=self._repeats)
269
270        # Write speedometer score from the last run to log
271        score = tab.EvaluateJavaScript(score_js)
272        logging.info('Speedometer Score: %s', score)
273
274
275    def _run_group_video(self):
276        """Run video and audio playback in the browser."""
277
278        # Note: for perf keyvals, key names are defined as VARCHAR(30) in the
279        # results DB. Chars above 30 are truncated when saved to DB.
280        urls = [('vid400p_h264', 'big_buck_bunny_trailer_400p.mp4'), ]
281        fullscreen_urls = []
282        bg_urls = []
283
284        if not self.short:
285            urls += [
286                ('vid400p_ogg', 'big_buck_bunny_trailer_400p.ogg'),
287                ('vid400p_vp8', 'big_buck_bunny_trailer_400p.vp8.webm'),
288                ('vid400p_vp9', 'big_buck_bunny_trailer_400p.vp9.webm'),
289                ('vid720_h264', 'big_buck_bunny_trailer_720p.mp4'),
290                ('vid720_ogg', 'big_buck_bunny_trailer_720p.ogg'),
291                ('vid720_vp8', 'big_buck_bunny_trailer_720p.vp8.webm'),
292                ('vid720_vp9', 'big_buck_bunny_trailer_720p.vp9.webm'),
293                ('vid1080_h264', 'big_buck_bunny_trailer_1080p.mp4'),
294                ('vid1080_ogg', 'big_buck_bunny_trailer_1080p.ogg'),
295                ('vid1080_vp8', 'big_buck_bunny_trailer_1080p.vp8.webm'),
296                ('vid1080_vp9', 'big_buck_bunny_trailer_1080p.vp9.webm'),
297                ('audio', 'Greensleeves.ogg'),
298                ]
299
300            fullscreen_urls += [
301                ('vid720_h264_fs', 'big_buck_bunny_trailer_720p.mp4'),
302                ('vid720_vp8_fs', 'big_buck_bunny_trailer_720p.vp8.webm'),
303                ('vid720_vp9_fs', 'big_buck_bunny_trailer_720p.vp9.webm'),
304                ('vid1080_h264_fs', 'big_buck_bunny_trailer_1080p.mp4'),
305                ('vid1080_vp8_fs', 'big_buck_bunny_trailer_1080p.vp8.webm'),
306                ('vid1080_vp9_fs', 'big_buck_bunny_trailer_1080p.vp9.webm'),
307                ]
308
309            bg_urls += [
310                ('bg_vid400p', 'big_buck_bunny_trailer_400p.vp8.webm'),
311                ]
312
313        # The video files are run from a file:// url. In order to work properly
314        # from an http:// url, some careful web server configuration is needed
315        def full_url(filename):
316            """Create a file:// url for the media file and verify it exists.
317
318            @param filename: string
319            """
320            p = os.path.join(self._media_dir, filename)
321            if not os.path.isfile(p):
322                raise error.TestError('Media file %s is missing.', p)
323            return 'file://' + p
324
325        js_loop_enable = """ve = document.getElementsByTagName('video')[0];
326                         ve.loop = true;
327                         ve.play();
328                         """
329
330        for name, url in urls:
331            logging.info('Playing video %s', url)
332            self._tab.Navigate(full_url(url))
333            self._tab.ExecuteJavaScript(js_loop_enable)
334            self._run_sleep(name, self._duration_secs)
335
336        for name, url in fullscreen_urls:
337            self._toggle_fullscreen()
338            self._tab.Navigate(full_url(url))
339            self._tab.ExecuteJavaScript(js_loop_enable)
340            self._run_sleep(name, self._duration_secs)
341            self._toggle_fullscreen()
342
343        for name, url in bg_urls:
344            logging.info('Playing video in background tab %s', url)
345            self._tab.Navigate(full_url(url))
346            self._tab.ExecuteJavaScript(js_loop_enable)
347            fg_tab = self._browser.tabs.New()
348            self._run_sleep(name, self._duration_secs)
349            fg_tab.Close()
350            self._tab.Activate()
351
352
353    def _run_group_sound(self):
354        """Run non-UI sound test using 'speaker-test'."""
355        # For some reason speaker-test won't work on CrOS without a reasonable
356        # buffer size specified with -b.
357        # http://crbug.com/248955
358        cmd = 'speaker-test -l %s -t sine -c 2 -b 16384' % (self._repeats * 6)
359        self._run_cmd('speaker_test', cmd)
360
361
362    def _run_group_lowlevel(self):
363        """Low level system stuff"""
364        mb = min(1024, 32 * self._repeats)
365        self._run_cmd('memtester', '/usr/local/sbin/memtester %s 1' % mb)
366
367        # one rep of dd takes about 15 seconds
368        root_dev = utils.get_root_partition()
369        cmd = 'dd if=%s of=/dev/null' % root_dev
370        self._run_cmd('dd', cmd, repeat=2 * self._repeats)
371
372
373    def _run_group_backchannel(self):
374        """WiFi sub-tests."""
375
376        shill = wifi_proxy.WifiProxy()
377        for _ in xrange(3):
378            succeeded, _, _, _, _ = shill.connect_to_wifi_network(
379                    ssid='GoogleGuest',
380                    security='none',
381                    security_parameters={},
382                    save_credentials=False)
383            if succeeded:
384                break
385
386        if not succeeded:
387            logging.error("Could not connect to WiFi")
388            return
389
390        logging.info('Starting Backchannel')
391        with backchannel.Backchannel():
392            # Wifi needs some time to recover after backchanel is activated
393            # TODO (kamrik) remove this sleep, once backchannel handles this
394            time.sleep(15)
395
396            cmd = 'ping -c %s www.google.com' % (self._duration_secs)
397            self._run_cmd('ping_wifi', cmd)
398
399            # This URL must be visible from WiFi network used for test
400            big_file_url = ('http://googleappengine.googlecode.com'
401                            '/files/GoogleAppEngine-1.6.2.msi')
402            cmd = 'curl %s > /dev/null' % big_file_url
403            self._run_cmd('download_wifi', cmd, repeat=self._repeats)
404
405
406    def _run_group_backlight(self):
407        """Vary backlight brightness and record power at each setting."""
408        for i in [100, 50, 0]:
409            self._backlight.set_percent(i)
410            start_time = time.time() + self._stabilization_seconds
411            time.sleep(30 * self._repeats)
412            self._plog.checkpoint('backlight_%03d' % i, start_time)
413        self._backlight.set_default()
414
415
416    def _web_echo(self, msg):
417        """ Displays a message in the browser."""
418        url = self._url_base + 'echo.html?'
419        url += urllib.quote(msg)
420        self._tab.Navigate(url)
421
422
423    def _run_test_groups(self, groups):
424        """ Run all the test groups.
425
426        Args:
427            groups: list of sub-test groups to run. Each sub-test group refers
428                to a _run_group_...() function.
429        """
430
431        for group in groups:
432            logging.info('Running group %s', group)
433            # The _web_echo here is important for some tests (esp. non UI)
434            # it gets the previous web page replaced with an almost empty one.
435            self._tab.Activate()
436            self._web_echo('Running test %s' % group)
437            test_func = getattr(self, '_run_group_%s' % group)
438            test_func()
439
440
441    def run_once(self, short=False, test_groups=None, reps=1):
442        # Some sub-tests have duration specified directly, _base_secs * reps
443        # is used in this case. Others complete whenever the underlying task
444        # completes, those are manually tuned to be roughly around
445        # reps * 30 seconds. Don't change _base_secs unless you also
446        # change the manual tuning in sub-tests
447        self._base_secs = 30
448        self._repeats = reps
449        self._duration_secs = self._base_secs * reps
450
451        # Lists of default tests to run
452        UI_TESTS = ['backlight', 'download', 'webpages', 'video', 'speedometer']
453        NONUI_TESTS = ['backchannel', 'sound', 'lowlevel']
454        DEFAULT_TESTS = UI_TESTS + NONUI_TESTS
455        DEFAULT_SHORT_TESTS = ['download', 'webpages', 'video']
456
457        self.short = short
458        if test_groups is None:
459            if self.short:
460                test_groups = DEFAULT_SHORT_TESTS
461            else:
462                test_groups = DEFAULT_TESTS
463        logging.info('Test groups to run: %s', ', '.join(test_groups))
464
465        self._backlight = power_utils.Backlight()
466        self._backlight.set_default()
467
468        measure = []
469        if not self._power_status.on_ac():
470            measure += \
471                [power_status.SystemPower(self._power_status.battery_path)]
472        if power_utils.has_powercap_support():
473            measure += power_rapl.create_powercap()
474        elif power_utils.has_rapl_support():
475            measure += power_rapl.create_rapl()
476        self._plog = power_status.PowerLogger(measure)
477        self._plog.start()
478
479        # Log in.
480        with chrome.Chrome() as cr:
481            self._browser = cr.browser
482            graphics_utils.screen_disable_energy_saving()
483            # Most of the tests will be running in this tab.
484            self._tab = cr.browser.tabs[0]
485
486            # Verify that we have a functioning browser and local web server.
487            self._tab.Activate()
488            self._web_echo("Sanity_test")
489            self._tab.WaitForDocumentReadyStateToBeComplete()
490
491            # Video test must have the data from download test
492            if ('video' in test_groups):
493                iv = test_groups.index('video')
494                if 'download' not in test_groups[:iv]:
495                    msg = '"download" test must run before "video".'
496                    raise error.TestError(msg)
497
498            # Run all the test groups
499            self._run_test_groups(test_groups)
500
501        # Wrap up
502        keyvals = self._plog.calc()
503        keyvals.update(self._tmp_keyvals)
504        keyvals.update(self._statomatic.publish())
505
506        # check AC status is still the same as init
507        self._power_status.refresh()
508        on_ac = self._power_status.on_ac()
509        if keyvals['b_on_ac'] != on_ac:
510            raise error.TestError('on AC changed between start & stop of test')
511
512        if not on_ac:
513            whrs = self._power_status.battery.energy_full_design
514            logging.info("energy_full_design = %0.3f Wh", whrs)
515
516            # Calculate expected battery life time with ChromeVer power draw
517            idle_name = 'ChromeVer_system_pwr_avg'
518            if idle_name in keyvals:
519                hours_life = whrs / keyvals[idle_name]
520                keyvals['hours_battery_ChromeVer'] = hours_life
521
522            # Calculate a weighted power draw and battery life time. The weights
523            # are intended to represent "typical" usage. Some video, some Flash
524            # ... and most of the time idle. see,
525            # http://www.chromium.org/chromium-os/testing/power-testing
526            weights = {'vid400p_h264_system_pwr_avg':0.1,
527                       'BallsFlex_system_pwr_avg':0.1,
528                       'BallsDHTML_system_pwr_avg':0.3,
529                      }
530            weights[idle_name] = 1 - sum(weights.values())
531
532            if set(weights).issubset(set(keyvals)):
533                p = sum(w * keyvals[k] for (k, w) in weights.items())
534                keyvals['w_Weighted_system_pwr_avg'] = p
535                keyvals['hours_battery_Weighted'] = whrs / p
536
537        self.write_perf_keyval(keyvals)
538        self._plog.save_results(self.resultsdir)
539
540
541    def cleanup(self):
542        # cleanup() is run by common_lib/test.py
543        try:
544            self._test_server.stop()
545        except AttributeError:
546            logging.debug('test_server could not be stopped in cleanup')
547
548        if self._backlight:
549            self._backlight.restore()
550        if self._services:
551            self._services.restore_services()
552
553        super(power_Consumption, self).cleanup()
554