• 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 collections, logging, numpy, os, tempfile, time
6from autotest_lib.client.bin import utils, test
7from autotest_lib.client.common_lib import error
8from autotest_lib.client.common_lib import file_utils
9from autotest_lib.client.common_lib.cros import chrome
10from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
11from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
12from autotest_lib.client.cros import backchannel, httpd
13from autotest_lib.client.cros import memory_bandwidth_logger
14from autotest_lib.client.cros import power_rapl, power_status, power_utils
15from autotest_lib.client.cros import service_stopper
16from autotest_lib.client.cros.audio import audio_helper
17from autotest_lib.client.cros.networking import wifi_proxy
18
19params_dict = {
20    'test_time_ms': '_mseconds',
21    'should_scroll': '_should_scroll',
22    'should_scroll_up': '_should_scroll_up',
23    'scroll_loop': '_scroll_loop',
24    'scroll_interval_ms': '_scroll_interval_ms',
25    'scroll_by_pixels': '_scroll_by_pixels',
26    'tasks': '_tasks',
27}
28
29
30class power_LoadTest(test.test):
31    """test class"""
32    version = 2
33    _username = 'powerloadtest@gmail.com'
34    _pltp_url = 'https://sites.google.com/a/chromium.org/dev/chromium-os' \
35                '/testing/power-testing/pltp/pltp'
36
37
38    def initialize(self, percent_initial_charge_min=None,
39                 check_network=True, loop_time=3600, loop_count=1,
40                 should_scroll='true', should_scroll_up='true',
41                 scroll_loop='false', scroll_interval_ms='10000',
42                 scroll_by_pixels='600', test_low_batt_p=3,
43                 verbose=True, force_wifi=False, wifi_ap='', wifi_sec='none',
44                 wifi_pw='', wifi_timeout=60, tasks='', kblight_percent=10,
45                 volume_level=10, mic_gain=10, low_batt_margin_p=2,
46                 ac_ok=False, log_mem_bandwidth=False):
47        """
48        percent_initial_charge_min: min battery charge at start of test
49        check_network: check that Ethernet interface is not running
50        loop_time: length of time to run the test for in each loop
51        loop_count: number of times to loop the test for
52        should_scroll: should the extension scroll pages
53        should_scroll_up: should scroll in up direction
54        scroll_loop: continue scrolling indefinitely
55        scroll_interval_ms: how often to scoll
56        scroll_by_pixels: number of pixels to scroll each time
57        test_low_batt_p: percent battery at which test should stop
58        verbose: add more logging information
59        force_wifi: should we force to test to run on wifi
60        wifi_ap: the name (ssid) of the wifi access point
61        wifi_sec: the type of security for the wifi ap
62        wifi_pw: password for the wifi ap
63        wifi_timeout: The timeout for wifi configuration
64        kblight_percent: percent brightness of keyboard backlight
65        volume_level: percent audio volume level
66        mic_gain: percent audio microphone gain level
67        low_batt_margin_p: percent low battery margin to be added to
68            sys_low_batt_p to guarantee test completes prior to powerd shutdown
69        ac_ok: boolean to allow running on AC
70        log_mem_bandwidth: boolean to log memory bandwidth during the test
71        """
72        self._backlight = None
73        self._services = None
74        self._browser = None
75        self._loop_time = loop_time
76        self._loop_count = loop_count
77        self._mseconds = self._loop_time * 1000
78        self._verbose = verbose
79
80        self._sys_low_batt_p = 0.
81        self._sys_low_batt_s = 0.
82        self._test_low_batt_p = test_low_batt_p
83        self._should_scroll = should_scroll
84        self._should_scroll_up = should_scroll_up
85        self._scroll_loop = scroll_loop
86        self._scroll_interval_ms = scroll_interval_ms
87        self._scroll_by_pixels = scroll_by_pixels
88        self._tmp_keyvals = {}
89        self._power_status = power_status.get_status()
90        self._tmp_keyvals['b_on_ac'] = self._power_status.on_ac()
91        self._force_wifi = force_wifi
92        self._testServer = None
93        self._tasks = tasks.replace(' ','')
94        self._backchannel = None
95        self._shill_proxy = None
96        self._kblight_percent = kblight_percent
97        self._volume_level = volume_level
98        self._mic_gain = mic_gain
99        self._ac_ok = ac_ok
100        self._log_mem_bandwidth = log_mem_bandwidth
101        self._wait_time = 60
102        self._stats = collections.defaultdict(list)
103
104        with tempfile.NamedTemporaryFile() as pltp:
105            file_utils.download_file(self._pltp_url, pltp.name)
106            self._password = pltp.read().rstrip()
107
108        if not ac_ok:
109            self._power_status.assert_battery_state(percent_initial_charge_min)
110        # If force wifi enabled, convert eth0 to backchannel and connect to the
111        # specified WiFi AP.
112        if self._force_wifi:
113            sec_config = None
114            # TODO(dbasehore): Fix this when we get a better way of figuring out
115            # the wifi security configuration.
116            if wifi_sec == 'rsn' or wifi_sec == 'wpa':
117                sec_config = xmlrpc_security_types.WPAConfig(
118                        psk=wifi_pw,
119                        wpa_mode=xmlrpc_security_types.WPAConfig.MODE_PURE_WPA2,
120                        wpa2_ciphers=
121                                [xmlrpc_security_types.WPAConfig.CIPHER_CCMP])
122            wifi_config = xmlrpc_datatypes.AssociationParameters(
123                    ssid=wifi_ap, security_config=sec_config,
124                    configuration_timeout=wifi_timeout)
125            # If backchannel is already running, don't run it again.
126            self._backchannel = backchannel.Backchannel()
127            if not self._backchannel.setup():
128                raise error.TestError('Could not setup Backchannel network.')
129
130            self._shill_proxy = wifi_proxy.WifiProxy()
131            self._shill_proxy.remove_all_wifi_entries()
132            for i in xrange(1,4):
133                raw_output = self._shill_proxy.connect_to_wifi_network(
134                        wifi_config.ssid,
135                        wifi_config.security,
136                        wifi_config.security_parameters,
137                        wifi_config.save_credentials,
138                        station_type=wifi_config.station_type,
139                        hidden_network=wifi_config.is_hidden,
140                        discovery_timeout_seconds=
141                                wifi_config.discovery_timeout,
142                        association_timeout_seconds=
143                                wifi_config.association_timeout,
144                        configuration_timeout_seconds=
145                                wifi_config.configuration_timeout * i)
146                result = xmlrpc_datatypes.AssociationResult. \
147                        from_dbus_proxy_output(raw_output)
148                if result.success:
149                    break
150                logging.warn('wifi connect: disc:%d assoc:%d config:%d fail:%s',
151                             result.discovery_time, result.association_time,
152                             result.configuration_time, result.failure_reason)
153            else:
154                raise error.TestError('Could not connect to WiFi network.')
155
156        else:
157            # Find all wired ethernet interfaces.
158            # TODO: combine this with code in network_DisableInterface, in a
159            # common library somewhere.
160            ifaces = [ nic.strip() for nic in os.listdir('/sys/class/net/')
161                if ((not os.path.exists('/sys/class/net/' + nic + '/phy80211'))
162                    and nic.find('eth') != -1) ]
163            logging.debug(str(ifaces))
164            for iface in ifaces:
165                if check_network and self._is_network_iface_running(iface):
166                    raise error.TestError('Ethernet interface is active. ' +
167                                          'Please remove Ethernet cable')
168
169        # record the max backlight level
170        self._backlight = power_utils.Backlight()
171        self._tmp_keyvals['level_backlight_max'] = \
172            self._backlight.get_max_level()
173
174        self._services = service_stopper.ServiceStopper(
175            service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
176        self._services.stop_services()
177
178        # fix up file perms for the power test extension so that chrome
179        # can access it
180        os.system('chmod -R 755 %s' % self.bindir)
181
182        # setup a HTTP Server to listen for status updates from the power
183        # test extension
184        self._testServer = httpd.HTTPListener(8001, docroot=self.bindir)
185        self._testServer.run()
186
187        # initialize various interesting power related stats
188        self._statomatic = power_status.StatoMatic()
189
190        self._power_status.refresh()
191        (self._sys_low_batt_p, self._sys_low_batt_s) = \
192            self._get_sys_low_batt_values()
193        min_low_batt_p = min(self._sys_low_batt_p + low_batt_margin_p, 100)
194        if self._sys_low_batt_p and (min_low_batt_p > self._test_low_batt_p):
195            logging.warning("test low battery threshold is below system " +
196                         "low battery requirement.  Setting to %f",
197                         min_low_batt_p)
198            self._test_low_batt_p = min_low_batt_p
199
200        self._ah_charge_start = self._power_status.battery[0].charge_now
201        self._wh_energy_start = self._power_status.battery[0].energy
202
203    def run_once(self):
204        t0 = time.time()
205
206        # record the PSR counter
207        psr_t0 = self._get_psr_counter()
208
209        try:
210            kblight = power_utils.KbdBacklight()
211            kblight.set(self._kblight_percent)
212            self._tmp_keyvals['percent_kbd_backlight'] = kblight.get()
213        except power_utils.KbdBacklightException as e:
214            logging.info("Assuming no keyboard backlight due to :: %s", str(e))
215            kblight = None
216
217        measurements = \
218            [power_status.SystemPower(self._power_status.battery_path)]
219        if power_utils.has_rapl_support():
220            measurements += power_rapl.create_rapl()
221        self._plog = power_status.PowerLogger(measurements, seconds_period=20)
222        self._tlog = power_status.TempLogger([], seconds_period=20)
223        self._plog.start()
224        self._tlog.start()
225        if self._log_mem_bandwidth:
226            self._mlog = memory_bandwidth_logger.MemoryBandwidthLogger(
227                raw=False, seconds_period=2)
228            self._mlog.start()
229
230        ext_path = os.path.join(os.path.dirname(__file__), 'extension')
231        self._browser = chrome.Chrome(extension_paths=[ext_path],
232                                gaia_login=True,
233                                username=self._username,
234                                password=self._password)
235        extension = self._browser.get_extension(ext_path)
236        for k in params_dict:
237            if getattr(self, params_dict[k]) is not '':
238                extension.ExecuteJavaScript('var %s = %s;' %
239                                            (k, getattr(self, params_dict[k])))
240
241        # This opens a trap start page to capture tabs opened for first login.
242        # It will be closed when startTest is run.
243        extension.ExecuteJavaScript('chrome.windows.create(null, null);')
244
245        for i in range(self._loop_count):
246            start_time = time.time()
247            extension.ExecuteJavaScript('startTest();')
248            # the power test extension will report its status here
249            latch = self._testServer.add_wait_url('/status')
250
251            # reset backlight level since powerd might've modified it
252            # based on ambient light
253            self._set_backlight_level()
254            self._set_lightbar_level()
255            if kblight:
256                kblight.set(self._kblight_percent)
257            audio_helper.set_volume_levels(self._volume_level,
258                                           self._mic_gain)
259
260            low_battery = self._do_wait(self._verbose, self._loop_time,
261                                        latch)
262
263            self._plog.checkpoint('loop%d' % (i), start_time)
264            self._tlog.checkpoint('loop%d' % (i), start_time)
265            if self._verbose:
266                logging.debug('loop %d completed', i)
267
268            if low_battery:
269                logging.info('Exiting due to low battery')
270                break
271
272        t1 = time.time()
273        self._tmp_keyvals['minutes_battery_life_tested'] = (t1 - t0) / 60
274        if psr_t0:
275            self._tmp_keyvals['psr_residency'] = \
276                (self._get_psr_counter() - psr_t0) / (10 * (t1 - t0))
277
278
279    def postprocess_iteration(self):
280        def _log_stats(prefix, stats):
281            if not len(stats):
282                return
283            np = numpy.array(stats)
284            logging.debug("%s samples: %d", prefix, len(np))
285            logging.debug("%s mean:    %.2f", prefix, np.mean())
286            logging.debug("%s stdev:   %.2f", prefix, np.std())
287            logging.debug("%s max:     %.2f", prefix, np.max())
288            logging.debug("%s min:     %.2f", prefix, np.min())
289
290
291        def _log_per_loop_stats():
292            samples_per_loop = self._loop_time / self._wait_time + 1
293            for kname in self._stats:
294                start_idx = 0
295                loop = 1
296                for end_idx in xrange(samples_per_loop, len(self._stats[kname]),
297                                      samples_per_loop):
298                    _log_stats("%s loop %d" % (kname, loop),
299                               self._stats[kname][start_idx:end_idx])
300                    loop += 1
301                    start_idx = end_idx
302
303
304        def _log_all_stats():
305            for kname in self._stats:
306                _log_stats(kname, self._stats[kname])
307
308
309        keyvals = self._plog.calc()
310        keyvals.update(self._tlog.calc())
311        keyvals.update(self._statomatic.publish())
312
313        if self._log_mem_bandwidth:
314            self._mlog.stop()
315            self._mlog.join()
316
317        _log_all_stats()
318        _log_per_loop_stats()
319
320        # record battery stats
321        keyvals['a_current_now'] = self._power_status.battery[0].current_now
322        keyvals['ah_charge_full'] = self._power_status.battery[0].charge_full
323        keyvals['ah_charge_full_design'] = \
324                             self._power_status.battery[0].charge_full_design
325        keyvals['ah_charge_start'] = self._ah_charge_start
326        keyvals['ah_charge_now'] = self._power_status.battery[0].charge_now
327        keyvals['ah_charge_used'] = keyvals['ah_charge_start'] - \
328                                    keyvals['ah_charge_now']
329        keyvals['wh_energy_start'] = self._wh_energy_start
330        keyvals['wh_energy_now'] = self._power_status.battery[0].energy
331        keyvals['wh_energy_used'] = keyvals['wh_energy_start'] - \
332                                    keyvals['wh_energy_now']
333        keyvals['v_voltage_min_design'] = \
334                             self._power_status.battery[0].voltage_min_design
335        keyvals['wh_energy_full_design'] = \
336                             self._power_status.battery[0].energy_full_design
337        keyvals['v_voltage_now'] = self._power_status.battery[0].voltage_now
338
339        keyvals.update(self._tmp_keyvals)
340
341        keyvals['percent_sys_low_battery'] = self._sys_low_batt_p
342        keyvals['seconds_sys_low_battery'] = self._sys_low_batt_s
343        voltage_np = numpy.array(self._stats['v_voltage_now'])
344        voltage_mean = voltage_np.mean()
345        keyvals['v_voltage_mean'] = voltage_mean
346
347        keyvals['wh_energy_powerlogger'] = \
348                             self._energy_use_from_powerlogger(keyvals)
349
350        if keyvals['ah_charge_used'] > 0:
351            # For full runs, we should use charge to scale for battery life,
352            # since the voltage swing is accounted for.
353            # For short runs, energy will be a better estimate.
354            if self._loop_count > 1:
355                estimated_reps = (keyvals['ah_charge_full_design'] /
356                                  keyvals['ah_charge_used'])
357            else:
358                estimated_reps = (keyvals['wh_energy_full_design'] /
359                                  keyvals['wh_energy_powerlogger'])
360
361            bat_life_scale =  estimated_reps * \
362                              ((100 - keyvals['percent_sys_low_battery']) / 100)
363
364            keyvals['minutes_battery_life'] = bat_life_scale * \
365                keyvals['minutes_battery_life_tested']
366            # In the case where sys_low_batt_s is non-zero subtract those
367            # minutes from the final extrapolation.
368            if self._sys_low_batt_s:
369                keyvals['minutes_battery_life'] -= self._sys_low_batt_s / 60
370
371            keyvals['a_current_rate'] = keyvals['ah_charge_used'] * 60 / \
372                                        keyvals['minutes_battery_life_tested']
373            keyvals['w_energy_rate'] = keyvals['wh_energy_used'] * 60 / \
374                                       keyvals['minutes_battery_life_tested']
375            self.output_perf_value(description='minutes_battery_life',
376                                   value=keyvals['minutes_battery_life'],
377                                   units='minutes')
378
379        self.write_perf_keyval(keyvals)
380        self._plog.save_results(self.resultsdir)
381        self._tlog.save_results(self.resultsdir)
382
383
384    def cleanup(self):
385        if self._backlight:
386            self._backlight.restore()
387        if self._services:
388            self._services.restore_services()
389
390        # cleanup backchannel interface
391        # Prevent wifi congestion in test lab by forcing machines to forget the
392        # wifi AP we connected to at the start of the test.
393        if self._shill_proxy:
394            self._shill_proxy.remove_all_wifi_entries()
395        if self._backchannel:
396            self._backchannel.teardown()
397        if self._browser:
398            self._browser.close()
399        if self._testServer:
400            self._testServer.stop()
401
402
403    def _do_wait(self, verbose, seconds, latch):
404        latched = False
405        low_battery = False
406        total_time = seconds + self._wait_time
407        elapsed_time = 0
408
409        while elapsed_time < total_time:
410            time.sleep(self._wait_time)
411            elapsed_time += self._wait_time
412
413            self._power_status.refresh()
414            charge_now = self._power_status.battery[0].charge_now
415            energy_rate = self._power_status.battery[0].energy_rate
416            voltage_now = self._power_status.battery[0].voltage_now
417            self._stats['w_energy_rate'].append(energy_rate)
418            self._stats['v_voltage_now'].append(voltage_now)
419            if verbose:
420                logging.debug('ah_charge_now %f', charge_now)
421                logging.debug('w_energy_rate %f', energy_rate)
422                logging.debug('v_voltage_now %f', voltage_now)
423
424            low_battery = (self._power_status.percent_current_charge() <
425                           self._test_low_batt_p)
426
427            latched = latch.is_set()
428
429            if latched or low_battery:
430                break
431
432        if latched:
433            # record chrome power extension stats
434            form_data = self._testServer.get_form_entries()
435            logging.debug(form_data)
436            for e in form_data:
437                key = 'ext_' + e
438                if key in self._tmp_keyvals:
439                    self._tmp_keyvals[key] += "_%s" % form_data[e]
440                else:
441                    self._tmp_keyvals[key] = form_data[e]
442        else:
443            logging.debug("Didn't get status back from power extension")
444
445        return low_battery
446
447
448    def _set_backlight_level(self):
449        self._backlight.set_default()
450        # record brightness level
451        self._tmp_keyvals['level_backlight_current'] = \
452            self._backlight.get_level()
453
454
455    def _set_lightbar_level(self, level='off'):
456        """Set lightbar level.
457
458        Args:
459          level: string value to set lightbar to.  See ectool for more details.
460        """
461        rv = utils.system('which ectool', ignore_status=True)
462        if rv:
463            return
464        rv = utils.system('ectool lightbar %s' % level, ignore_status=True)
465        if rv:
466            logging.info('Assuming no lightbar due to non-zero exit status')
467        else:
468            logging.info('Setting lightbar to %s', level)
469            self._tmp_keyvals['level_lightbar_current'] = level
470
471
472    def _get_sys_low_batt_values(self):
473        """Determine the low battery values for device and return.
474
475        2012/11/01: power manager (powerd.cc) parses parameters in filesystem
476          and outputs a log message like:
477
478           [1101/173837:INFO:powerd.cc(258)] Using low battery time threshold
479                     of 0 secs and using low battery percent threshold of 3.5
480
481           It currently checks to make sure that only one of these values is
482           defined.
483
484        Returns:
485          Tuple of (percent, seconds)
486            percent: float of low battery percentage
487            seconds: float of low battery seconds
488
489        """
490        split_re = 'threshold of'
491
492        powerd_log = '/var/log/power_manager/powerd.LATEST'
493        cmd = 'grep "low battery time" %s' % powerd_log
494        line = utils.system_output(cmd)
495        secs = float(line.split(split_re)[1].split()[0])
496        percent = float(line.split(split_re)[2].split()[0])
497        if secs and percent:
498            raise error.TestError("Low battery percent and seconds " +
499                                  "are non-zero.")
500        return (percent, secs)
501
502
503    def _get_psr_counter(self):
504        """Get the current value of the system PSR counter.
505        This counts the number of milliseconds the system has resided in PSR.
506
507        Returns:
508          count: amount of time PSR has been active since boot in ms, or
509              None if the performance counter can't be read
510
511        """
512        psr_status_file = '/sys/kernel/debug/dri/0/i915_edp_psr_status'
513        try:
514            count = utils.get_field(utils.read_file(psr_status_file),
515                                    0,
516                                    linestart='Performance_Counter:')
517        except IOError:
518            logging.info("Can't find or read PSR status file")
519            return None
520
521        logging.debug("PSR performance counter: %s", count)
522        return int(count) if count else None
523
524
525    def _is_network_iface_running(self, name):
526        """
527        Checks to see if the interface is running.
528
529        Args:
530          name: name of the interface to check.
531
532        Returns:
533          True if the interface is running.
534
535        """
536        try:
537            # TODO: Switch to 'ip' (crbug.com/410601).
538            out = utils.system_output('ifconfig %s' % name)
539        except error.CmdError, e:
540            logging.info(e)
541            return False
542
543        return out.find('RUNNING') >= 0
544
545
546    def _energy_use_from_powerlogger(self, keyval):
547        """
548        Calculates the energy use, in Wh, used over the course of the run as
549        reported by the PowerLogger.
550
551        Args:
552          keyval: the dictionary of keyvals containing PowerLogger output
553
554        Returns:
555          energy_wh: total energy used over the course of this run
556
557        """
558        energy_wh = 0
559        loop = 0
560        while True:
561            duration_key = 'loop%d_system_duration' % loop
562            avg_power_key = 'loop%d_system_pwr' % loop
563            if duration_key not in keyval or avg_power_key not in keyval:
564                break
565            energy_wh += keyval[duration_key] * keyval[avg_power_key] / 3600
566            loop += 1
567        return energy_wh
568