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