• 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 site_utils, 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
14# pylint: disable=W0611
15from autotest_lib.client.cros import flimflam_test_path  # Needed for flimflam
16from autotest_lib.client.cros import httpd
17from autotest_lib.client.cros import power_rapl, power_status, power_utils
18from autotest_lib.client.cros import service_stopper
19from autotest_lib.client.cros.graphics import graphics_utils
20import flimflam  # Requires flimflam_test_path to be imported first.
21
22
23class power_Consumption(test.test):
24    """Measure power consumption for different types of loads.
25
26    This test runs a series of different tasks like media playback, flash
27    animation, large file download etc. It measures and reports power
28    consumptions during each of those tasks.
29    """
30
31    version = 2
32
33
34    def initialize(self, ac_ok=False):
35        """Initialize test.
36
37        Args:
38            ac_ok: boolean to allow running on AC
39        """
40        # Objects that need to be taken care of in cleanup() are initialized
41        # here to None. Otherwise we run the risk of AttributeError raised in
42        # cleanup() masking a real error that caused the test to fail during
43        # initialize() before those variables were assigned.
44        self._backlight = None
45        self._tmp_keyvals = {}
46
47        self._services = service_stopper.ServiceStopper(
48            service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
49        self._services.stop_services()
50
51
52        # Time to exclude from calculation after firing a task [seconds]
53        self._stabilization_seconds = 5
54        self._power_status = power_status.get_status()
55        self._tmp_keyvals['b_on_ac'] = self._power_status.on_ac()
56
57        if not ac_ok:
58            # Verify that we are running on battery and the battery is
59            # sufficiently charged
60            self._power_status.assert_battery_state(30)
61
62        # Find the battery capacity to report expected battery life in hours
63        batinfo = self._power_status.battery[0]
64        self.energy_full_design = batinfo.energy_full_design
65        logging.info("energy_full_design = %0.3f Wh", self.energy_full_design)
66
67        # Local data and web server settings. Tarballs with traditional names
68        # like *.tgz don't get copied to the image by ebuilds (see
69        # AUTOTEST_FILE_MASK in autotest-chrome ebuild).
70        self._static_sub_dir = 'static_sites'
71        utils.extract_tarball_to_dir(
72                'static_sites.tgz.keep',
73                os.path.join(self.bindir, self._static_sub_dir))
74        self._media_dir = '/home/chronos/user/Downloads/'
75        self._httpd_port = 8000
76        self._url_base = 'http://localhost:%s/' % self._httpd_port
77        self._test_server = httpd.HTTPListener(self._httpd_port,
78                                               docroot=self.bindir)
79
80        self._test_server.run()
81
82        logging.info('initialize() finished')
83
84
85    def _download_test_data(self):
86        """Download audio and video files.
87
88        This is also used as payload for download test.
89
90        Note, can reach payload via browser at
91          https://console.developers.google.com/storage/chromeos-test-public/big_buck_bunny
92        Start with README
93        """
94
95        repo = 'http://commondatastorage.googleapis.com/chromeos-test-public/'
96        file_list = [repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.mp4', ]
97        if not self.short:
98            file_list += [
99                repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.ogg',
100                repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp8.webm',
101                repo + 'big_buck_bunny/big_buck_bunny_trailer_400p.vp9.webm',
102                repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.mp4',
103                repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.ogg',
104                repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp8.webm',
105                repo + 'big_buck_bunny/big_buck_bunny_trailer_720p.vp9.webm',
106                repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.mp4',
107                repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.ogg',
108                repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp8.webm',
109                repo + 'big_buck_bunny/big_buck_bunny_trailer_1080p.vp9.webm',
110                repo + 'wikimedia/Greensleeves.ogg',
111                ]
112
113        for url in file_list:
114            logging.info('Downloading %s', url)
115            utils.unmap_url('', url, self._media_dir)
116
117
118    def _toggle_fullscreen(self):
119        """Toggle full screen mode."""
120        # Note: full screen mode toggled with F11 is different from clicking the
121        # full screen icon on video player controls. This needs improvement.
122        # Bug: http://crbug.com/248939
123        graphics_utils.screen_toggle_fullscreen()
124
125
126    # Below are a series of generic sub-test runners. They run a given task
127    # and record the task name and start-end timestamps for future computation
128    # of power consumption during the task.
129    def _run_func(self, name, func, repeat=1, save_checkpoint=True):
130        """Run a given python function as a sub-test."""
131        start_time = time.time() + self._stabilization_seconds
132        for _ in xrange(repeat):
133            ret = func()
134        if save_checkpoint:
135            self._plog.checkpoint(name, start_time)
136        return ret
137
138
139    def _run_sleep(self, name, seconds=60):
140        """Just sleep and record it as a named sub-test"""
141        start_time = time.time() + self._stabilization_seconds
142        time.sleep(seconds)
143        self._plog.checkpoint(name, start_time)
144
145
146    def _run_cmd(self, name, cmd, repeat=1):
147        """Run command in a shell as a sub-test"""
148        start_time = time.time() + self._stabilization_seconds
149        for _ in xrange(repeat):
150            logging.info('Executing command: %s', cmd)
151            exit_status = utils.system(cmd, ignore_status=True)
152            if exit_status != 0:
153                logging.error('run_cmd: the following command terminated with'
154                                'a non zero exit status: %s', cmd)
155        self._plog.checkpoint(name, start_time)
156        return exit_status
157
158
159    def _run_until(self, name, predicate, timeout=60):
160        """Probe the |predicate| function  and wait until it returns true.
161        Record the waiting time as a sub-test
162        """
163        start_time = time.time() + self._stabilization_seconds
164        utils.poll_for_condition(predicate, timeout=timeout)
165        self._plog.checkpoint(name, start_time)
166
167
168    def _run_url(self, name, url, duration):
169        """Navigate to URL, sleep for some time and record it as a sub-test."""
170        logging.info('Navigating to %s', url)
171        self._tab.Activate()
172        self._tab.Navigate(url)
173        self._run_sleep(name, duration)
174        tab_title = self._tab.EvaluateJavaScript('document.title')
175        logging.info('Sub-test name: %s Tab title: %s.', name, tab_title)
176
177
178    def _run_url_bg(self, name, url, duration):
179        """Run a web site in background tab.
180
181        Navigate to the given URL, open an empty tab to put the one with the
182        URL in background, then sleep and record it as a sub-test.
183
184        Args:
185            name: sub-test name.
186            url: url to open in background tab.
187            duration: number of seconds to sleep while taking measurements.
188        """
189        bg_tab = self._tab
190        bg_tab.Navigate(url)
191        # Let it load and settle
192        time.sleep(self._stabilization_seconds / 2.)
193        tab_title = bg_tab.EvaluateJavaScript('document.title')
194        logging.info('App name: %s Tab title: %s.', name, tab_title)
195        # Open a new empty tab to cover the one with test payload.
196        fg_tab = self._browser.tabs.New()
197        fg_tab.Activate()
198        self._run_sleep(name, duration)
199        fg_tab.Close()
200        bg_tab.Activate()
201
202
203    def _run_group_download(self):
204        """Download over ethernet. Using video test data as payload."""
205
206        # For short run, the payload is too small to take measurement
207        self._run_func('download_eth',
208                       self._download_test_data ,
209                       repeat=self._repeats,
210                       save_checkpoint=not(self.short))
211
212
213    def _run_group_webpages(self):
214        """Runs a series of web pages as sub-tests."""
215        data_url = self._url_base + self._static_sub_dir + '/'
216
217        # URLs to be only tested in foreground tab.
218        # Can't use about:blank here - crbug.com/248945
219        # but chrome://version is just as good for our needs.
220        urls = [('ChromeVer', 'chrome://version/')]
221        # URLs to be tested in both, background and foreground modes.
222        bg_urls = []
223
224        more_urls = [('BallsDHTML',
225                      data_url + 'balls/DHTMLBalls/dhtml.htm'),
226                     # Disabling FlexBalls as experiment http://crbug.com/309403
227                     # ('BallsFlex',
228                     #  data_url + 'balls/FlexBalls/flexballs.html'),
229                    ]
230
231        if self.short:
232            urls += more_urls
233        else:
234            bg_urls += more_urls
235            bg_urls += [('Parapluesch',
236                         'http://www.parapluesch.de/whiskystore/test.htm'),
237                         ('PosterCircle',
238                          'http://www.webkit.org'
239                          '/blog-files/3d-transforms/poster-circle.html'), ]
240
241        for name, url in urls + bg_urls:
242            self._run_url(name, url, duration=self._duration_secs)
243
244        for name, url in bg_urls:
245            self._run_url_bg('bg_' + name, url, duration=self._duration_secs)
246
247
248    def _run_group_v8(self):
249        """Run the V8 benchmark suite as a sub-test.
250
251        Fire it up and wait until it displays "Score".
252        """
253
254        url = 'http://v8.googlecode.com/svn/data/benchmarks/v7/run.html'
255        js = "document.getElementById('status').textContent"
256        tab = self._tab
257
258        def v8_func():
259            """To be passed as the callable to self._run_func()"""
260            tab.Navigate(url)
261            # V8 test will usually take 17-25 seconds. Need some sleep here
262            # to let the V8 page load and create the 'status' div.
263            is_done = lambda: tab.EvaluateJavaScript(js).startswith('Score')
264            time.sleep(self._stabilization_seconds)
265            utils.poll_for_condition(is_done, timeout=60, desc='V8 score found')
266
267        self._run_func('V8', v8_func, repeat=self._repeats)
268
269        # Write v8 score from the last run to log
270        score = tab.EvaluateJavaScript(js)
271        score = score.strip().split()[1]
272        logging.info('V8 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 = site_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        wifi_ap = 'GoogleGuest'
377        wifi_sec = 'none'
378        wifi_pw = ''
379
380        flim = flimflam.FlimFlam()
381        conn = flim.ConnectService(retries=3,
382                              retry=True,
383                              service_type='wifi',
384                              ssid=wifi_ap,
385                              security=wifi_sec,
386                              passphrase=wifi_pw,
387                              mode='managed')
388        if not conn[0]:
389            logging.error("Could not connect to WiFi")
390            return
391
392        logging.info('Starting Backchannel')
393        with backchannel.Backchannel():
394            # Wifi needs some time to recover after backchanel is activated
395            # TODO (kamrik) remove this sleep, once backchannel handles this
396            time.sleep(15)
397
398            cmd = 'ping -c %s www.google.com' % (self._duration_secs)
399            self._run_cmd('ping_wifi', cmd)
400
401            # This URL must be visible from WiFi network used for test
402            big_file_url = ('http://googleappengine.googlecode.com'
403                            '/files/GoogleAppEngine-1.6.2.msi')
404            cmd = 'curl %s > /dev/null' % big_file_url
405            self._run_cmd('download_wifi', cmd, repeat=self._repeats)
406
407
408    def _run_group_backlight(self):
409        """Vary backlight brightness and record power at each setting."""
410        for i in [100, 50, 0]:
411            self._backlight.set_percent(i)
412            start_time = time.time() + self._stabilization_seconds
413            time.sleep(30 * self._repeats)
414            self._plog.checkpoint('backlight_%03d' % i, start_time)
415        self._backlight.set_default()
416
417
418    def _web_echo(self, msg):
419        """ Displays a message in the browser."""
420        url = self._url_base + 'echo.html?'
421        url += urllib.quote(msg)
422        self._tab.Navigate(url)
423
424
425    def _run_test_groups(self, groups):
426        """ Run all the test groups.
427
428        Args:
429            groups: list of sub-test groups to run. Each sub-test group refers
430                to a _run_group_...() function.
431        """
432
433        for group in groups:
434            logging.info('Running group %s', group)
435            # The _web_echo here is important for some tests (esp. non UI)
436            # it gets the previous web page replaced with an almost empty one.
437            self._tab.Activate()
438            self._web_echo('Running test %s' % group)
439            test_func = getattr(self, '_run_group_%s' % group)
440            test_func()
441
442
443    def run_once(self, short=False, test_groups=None, reps=1):
444        # Some sub-tests have duration specified directly, _base_secs * reps
445        # is used in this case. Others complete whenever the underlying task
446        # completes, those are manually tuned to be roughly around
447        # reps * 30 seconds. Don't change _base_secs unless you also
448        # change the manual tuning in sub-tests
449        self._base_secs = 30
450        self._repeats = reps
451        self._duration_secs = self._base_secs * reps
452
453        # Lists of default tests to run
454        UI_TESTS = ['backlight', 'download', 'webpages', 'video', 'v8']
455        NONUI_TESTS = ['backchannel', 'sound', 'lowlevel']
456        DEFAULT_TESTS = UI_TESTS + NONUI_TESTS
457        DEFAULT_SHORT_TESTS = ['download', 'webpages', 'video']
458
459        self.short = short
460        if test_groups is None:
461            if self.short:
462                test_groups = DEFAULT_SHORT_TESTS
463            else:
464                test_groups = DEFAULT_TESTS
465        logging.info('Test groups to run: %s', ', '.join(test_groups))
466
467        self._backlight = power_utils.Backlight()
468        self._backlight.set_default()
469
470        measurements = \
471            [power_status.SystemPower(self._power_status.battery_path)]
472        if power_utils.has_rapl_support():
473            measurements += power_rapl.create_rapl()
474        self._plog = power_status.PowerLogger(measurements)
475        self._plog.start()
476
477        # Log in.
478        with chrome.Chrome() as cr:
479            self._browser = cr.browser
480            graphics_utils.screen_disable_energy_saving()
481            # Most of the tests will be running in this tab.
482            self._tab = cr.browser.tabs[0]
483
484            # Verify that we have a functioning browser and local web server.
485            self._tab.Activate()
486            self._web_echo("Sanity_test")
487            self._tab.WaitForDocumentReadyStateToBeComplete()
488
489            # Video test must have the data from download test
490            if ('video' in test_groups):
491                iv = test_groups.index('video')
492                if 'download' not in test_groups[:iv]:
493                    msg = '"download" test must run before "video".'
494                    raise error.TestError(msg)
495
496            # Run all the test groups
497            self._run_test_groups(test_groups)
498
499        # Wrap up
500        keyvals = self._plog.calc()
501        keyvals.update(self._tmp_keyvals)
502
503        # Calculate expected battery life time with ChromeVer power draw
504        idle_name = 'ChromeVer_system_pwr'
505        if idle_name in keyvals:
506            hours_life = self.energy_full_design / keyvals[idle_name]
507            keyvals['hours_battery_ChromeVer'] = hours_life
508
509        # Calculate a weighted power draw and battery life time. The weights
510        # are intended to represent "typical" usage. Some video, some Flash ...
511        # and most of the time idle.
512        # see http://www.chromium.org/chromium-os/testing/power-testing
513        weights = {'vid400p_h264_system_pwr':0.1,
514                   # TODO(chromium:309403) re-enable BallsFlex once Flash in
515                   # test-lab understood and re-distribute back to 60/20/10/10.
516                   # 'BallsFlex_system_pwr':0.1,
517                   'BallsDHTML_system_pwr':0.3,
518                   }
519        weights[idle_name] = 1 - sum(weights.values())
520
521        if set(weights).issubset(set(keyvals)):
522            p = sum(w * keyvals[k] for (k, w) in weights.items())
523            keyvals['w_Weighted_system_pwr'] = p
524            keyvals['hours_battery_Weighted'] = self.energy_full_design / p
525
526        self.write_perf_keyval(keyvals)
527        self._plog.save_results(self.resultsdir)
528
529
530    def cleanup(self):
531        # cleanup() is run by common_lib/test.py
532        try:
533            self._test_server.stop()
534        except AttributeError:
535            logging.debug('test_server could not be stopped in cleanup')
536
537        if self._backlight:
538            self._backlight.restore()
539        if self._services:
540            self._services.restore_services()
541
542        super(power_Consumption, self).cleanup()
543