• 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
6import json
7import logging
8import numpy
9import os
10import time
11
12from autotest_lib.client.bin import utils
13from autotest_lib.client.common_lib import error
14from autotest_lib.client.common_lib import utils as _utils
15from autotest_lib.client.common_lib.cros import arc
16from autotest_lib.client.common_lib.cros import arc_common
17from autotest_lib.client.common_lib.cros import chrome
18from autotest_lib.client.common_lib.cros import power_load_util
19from autotest_lib.client.common_lib.cros.network import interface
20from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
21from autotest_lib.client.common_lib.cros.network import xmlrpc_security_types
22from autotest_lib.client.cros import backchannel, httpd
23from autotest_lib.client.cros import memory_bandwidth_logger
24from autotest_lib.client.cros import service_stopper
25from autotest_lib.client.cros.audio import audio_helper
26from autotest_lib.client.cros.networking import wifi_proxy
27from autotest_lib.client.cros.power import power_dashboard
28from autotest_lib.client.cros.power import power_rapl
29from autotest_lib.client.cros.power import power_status
30from autotest_lib.client.cros.power import power_utils
31from telemetry.core import exceptions
32
33params_dict = {
34    'test_time_ms': '_mseconds',
35    'should_scroll': '_should_scroll',
36    'should_scroll_up': '_should_scroll_up',
37    'scroll_loop': '_scroll_loop',
38    'scroll_interval_ms': '_scroll_interval_ms',
39    'scroll_by_pixels': '_scroll_by_pixels',
40    'tasks': '_tasks',
41}
42
43class power_LoadTest(arc.ArcTest):
44    """test class"""
45    version = 2
46
47    def initialize(self, percent_initial_charge_min=None,
48                 check_network=True, loop_time=3600, loop_count=1,
49                 should_scroll='true', should_scroll_up='true',
50                 scroll_loop='false', scroll_interval_ms='10000',
51                 scroll_by_pixels='600', test_low_batt_p=3,
52                 verbose=True, force_wifi=False, wifi_ap='', wifi_sec='none',
53                 wifi_pw='', wifi_timeout=60, tasks='',
54                 volume_level=10, mic_gain=10, low_batt_margin_p=2,
55                 ac_ok=False, log_mem_bandwidth=False, gaia_login=None,
56                 force_discharge=False, pdash_note=''):
57        """
58        percent_initial_charge_min: min battery charge at start of test
59        check_network: check that Ethernet interface is not running
60        loop_time: length of time to run the test for in each loop
61        loop_count: number of times to loop the test for
62        should_scroll: should the extension scroll pages
63        should_scroll_up: should scroll in up direction
64        scroll_loop: continue scrolling indefinitely
65        scroll_interval_ms: how often to scoll
66        scroll_by_pixels: number of pixels to scroll each time
67        test_low_batt_p: percent battery at which test should stop
68        verbose: add more logging information
69        force_wifi: should we force to test to run on wifi
70        wifi_ap: the name (ssid) of the wifi access point
71        wifi_sec: the type of security for the wifi ap
72        wifi_pw: password for the wifi ap
73        wifi_timeout: The timeout for wifi configuration
74        volume_level: percent audio volume level
75        mic_gain: percent audio microphone gain level
76        low_batt_margin_p: percent low battery margin to be added to
77            sys_low_batt_p to guarantee test completes prior to powerd shutdown
78        ac_ok: boolean to allow running on AC
79        log_mem_bandwidth: boolean to log memory bandwidth during the test
80        gaia_login: whether real GAIA login should be attempted.  If 'None'
81            (default) then boolean is determined from URL.
82        force_discharge: boolean of whether to tell ec to discharge battery even
83            when the charger is plugged in.
84        pdash_note: note of the current run to send to power dashboard.
85        """
86        self._backlight = None
87        self._services = None
88        self._browser = None
89        self._loop_time = loop_time
90        self._loop_count = loop_count
91        self._mseconds = self._loop_time * 1000
92        self._verbose = verbose
93
94        self._sys_low_batt_p = 0.
95        self._sys_low_batt_s = 0.
96        self._test_low_batt_p = test_low_batt_p
97        self._should_scroll = should_scroll
98        self._should_scroll_up = should_scroll_up
99        self._scroll_loop = scroll_loop
100        self._scroll_interval_ms = scroll_interval_ms
101        self._scroll_by_pixels = scroll_by_pixels
102        self._tmp_keyvals = {}
103        self._power_status = None
104        self._force_wifi = force_wifi
105        self._testServer = None
106        self._tasks = tasks.replace(' ','')
107        self._backchannel = None
108        self._shill_proxy = None
109        self._volume_level = volume_level
110        self._mic_gain = mic_gain
111        self._ac_ok = ac_ok
112        self._log_mem_bandwidth = log_mem_bandwidth
113        self._wait_time = 60
114        self._stats = collections.defaultdict(list)
115        self._force_discharge = force_discharge
116        self._pdash_note = pdash_note
117
118        if not power_utils.has_battery():
119            if ac_ok and (power_utils.has_powercap_support() or
120                          power_utils.has_rapl_support()):
121                logging.info("Device has no battery but has powercap data.")
122            else:
123                rsp = "Skipping test for device without battery and powercap."
124                raise error.TestNAError(rsp)
125        self._power_status = power_status.get_status()
126        self._tmp_keyvals['b_on_ac'] = self._power_status.on_ac()
127
128        self._gaia_login = gaia_login
129        if gaia_login is None:
130            self._gaia_login = power_load_util.use_gaia_login()
131
132        self._username = power_load_util.get_username()
133        self._password = power_load_util.get_password()
134
135        if not ac_ok:
136            self._power_status.assert_battery_state(percent_initial_charge_min)
137
138        if force_discharge:
139            if not power_utils.charge_control_by_ectool(False):
140                raise error.TestError('Could not run battery force discharge.')
141
142        # If force wifi enabled, convert eth0 to backchannel and connect to the
143        # specified WiFi AP.
144        if self._force_wifi:
145            sec_config = None
146            # TODO(dbasehore): Fix this when we get a better way of figuring out
147            # the wifi security configuration.
148            if wifi_sec == 'rsn' or wifi_sec == 'wpa':
149                sec_config = xmlrpc_security_types.WPAConfig(
150                        psk=wifi_pw,
151                        wpa_mode=xmlrpc_security_types.WPAConfig.MODE_PURE_WPA2,
152                        wpa2_ciphers=
153                                [xmlrpc_security_types.WPAConfig.CIPHER_CCMP])
154            wifi_config = xmlrpc_datatypes.AssociationParameters(
155                    ssid=wifi_ap, security_config=sec_config,
156                    configuration_timeout=wifi_timeout)
157            # If backchannel is already running, don't run it again.
158            self._backchannel = backchannel.Backchannel()
159            if not self._backchannel.setup():
160                raise error.TestError('Could not setup Backchannel network.')
161
162            self._shill_proxy = wifi_proxy.WifiProxy()
163            self._shill_proxy.remove_all_wifi_entries()
164            for i in xrange(1,4):
165                raw_output = self._shill_proxy.connect_to_wifi_network(
166                        wifi_config.ssid,
167                        wifi_config.security,
168                        wifi_config.security_parameters,
169                        wifi_config.save_credentials,
170                        station_type=wifi_config.station_type,
171                        hidden_network=wifi_config.is_hidden,
172                        discovery_timeout_seconds=
173                                wifi_config.discovery_timeout,
174                        association_timeout_seconds=
175                                wifi_config.association_timeout,
176                        configuration_timeout_seconds=
177                                wifi_config.configuration_timeout * i)
178                result = xmlrpc_datatypes.AssociationResult. \
179                        from_dbus_proxy_output(raw_output)
180                if result.success:
181                    break
182                logging.warn('wifi connect: disc:%d assoc:%d config:%d fail:%s',
183                             result.discovery_time, result.association_time,
184                             result.configuration_time, result.failure_reason)
185            else:
186                raise error.TestError('Could not connect to WiFi network.')
187
188        else:
189            # Find all wired ethernet interfaces.
190            ifaces = [ iface for iface in interface.get_interfaces()
191                if (not iface.is_wifi_device() and
192                    iface.name.startswith('eth')) ]
193            logging.debug(str([iface.name for iface in ifaces]))
194            for iface in ifaces:
195                if check_network and iface.is_lower_up:
196                    raise error.TestError('Ethernet interface is active. ' +
197                                          'Please remove Ethernet cable')
198
199        # record the max backlight level
200        self._backlight = power_utils.Backlight()
201        self._tmp_keyvals['level_backlight_max'] = \
202            self._backlight.get_max_level()
203
204        self._services = service_stopper.ServiceStopper(
205            service_stopper.ServiceStopper.POWER_DRAW_SERVICES)
206        self._services.stop_services()
207
208        self._detachable_handler = power_utils.BaseActivitySimulator()
209
210        # fix up file perms for the power test extension so that chrome
211        # can access it
212        os.system('chmod -R 755 %s' % self.bindir)
213
214        # setup a HTTP Server to listen for status updates from the power
215        # test extension
216        self._testServer = httpd.HTTPListener(8001, docroot=self.bindir)
217        self._testServer.run()
218
219        # initialize various interesting power related stats
220        self._statomatic = power_status.StatoMatic()
221
222        self._power_status.refresh()
223        self._sys_low_batt_p = float(utils.system_output(
224                 'check_powerd_config --low_battery_shutdown_percent'))
225        self._sys_low_batt_s = int(utils.system_output(
226                 'check_powerd_config --low_battery_shutdown_time'))
227
228        if self._sys_low_batt_p and self._sys_low_batt_s:
229            raise error.TestError(
230                    "Low battery percent and seconds are non-zero.")
231
232        min_low_batt_p = min(self._sys_low_batt_p + low_batt_margin_p, 100)
233        if self._sys_low_batt_p and (min_low_batt_p > self._test_low_batt_p):
234            logging.warning("test low battery threshold is below system " +
235                         "low battery requirement.  Setting to %f",
236                         min_low_batt_p)
237            self._test_low_batt_p = min_low_batt_p
238
239        if self._power_status.battery:
240            self._ah_charge_start = self._power_status.battery[0].charge_now
241            self._wh_energy_start = self._power_status.battery[0].energy
242
243
244    def run_once(self):
245        """Test main loop."""
246        t0 = time.time()
247
248        # record the PSR related info.
249        psr = power_utils.DisplayPanelSelfRefresh(init_time=t0)
250
251        try:
252            self._keyboard_backlight = power_utils.KbdBacklight()
253            self._set_keyboard_backlight_level()
254        except power_utils.KbdBacklightException as e:
255            logging.info("Assuming no keyboard backlight due to :: %s", str(e))
256            self._keyboard_backlight = None
257
258        measurements = []
259        if self._power_status.battery:
260            measurements += \
261                    [power_status.SystemPower(self._power_status.battery_path)]
262        if power_utils.has_powercap_support():
263            measurements += power_rapl.create_powercap()
264        elif power_utils.has_rapl_support():
265            measurements += power_rapl.create_rapl()
266        self._checkpoint_logger = power_status.CheckpointLogger()
267        self._plog = power_status.PowerLogger(measurements,
268                seconds_period=20,
269                checkpoint_logger=self._checkpoint_logger)
270        self._tlog = power_status.TempLogger([],
271                seconds_period=20,
272                checkpoint_logger=self._checkpoint_logger)
273        self._clog = power_status.CPUStatsLogger(
274                seconds_period=20,
275                checkpoint_logger=self._checkpoint_logger)
276        self._meas_logs = [self._plog, self._tlog, self._clog]
277        for log in self._meas_logs:
278            log.start()
279        if self._log_mem_bandwidth:
280            self._mlog = memory_bandwidth_logger.MemoryBandwidthLogger(
281                raw=False, seconds_period=2)
282            self._mlog.start()
283
284        # record start time and end time for each task
285        self._task_tracker = []
286
287        ext_path = os.path.join(os.path.dirname(__file__), 'extension')
288        self._tmp_keyvals['username'] = self._username
289
290        arc_mode = arc_common.ARC_MODE_DISABLED
291        if utils.is_arc_available():
292            arc_mode = arc_common.ARC_MODE_ENABLED
293
294        try:
295            self._browser = chrome.Chrome(extension_paths=[ext_path],
296                                          gaia_login=self._gaia_login,
297                                          username=self._username,
298                                          password=self._password,
299                                          arc_mode=arc_mode)
300        except exceptions.LoginException:
301            # already failed guest login
302            if not self._gaia_login:
303                raise
304            self._gaia_login = False
305            logging.warn("Unable to use GAIA acct %s.  Using GUEST instead.\n",
306                         self._username)
307            self._browser = chrome.Chrome(extension_paths=[ext_path],
308                                          gaia_login=self._gaia_login)
309        if not self._gaia_login:
310            self._tmp_keyvals['username'] = 'GUEST'
311
312        extension = self._browser.get_extension(ext_path)
313        for k in params_dict:
314            if getattr(self, params_dict[k]) is not '':
315                extension.ExecuteJavaScript('var %s = %s;' %
316                                            (k, getattr(self, params_dict[k])))
317
318        # This opens a trap start page to capture tabs opened for first login.
319        # It will be closed when startTest is run.
320        extension.ExecuteJavaScript('chrome.windows.create(null, null);')
321
322        for i in range(self._loop_count):
323            start_time = time.time()
324            extension.ExecuteJavaScript('startTest();')
325            # the power test extension will report its status here
326            latch = self._testServer.add_wait_url('/status')
327
328            # this starts a thread in the server that listens to log
329            # information from the script
330            script_logging = self._testServer.add_wait_url(url='/log')
331
332            # dump any log entry that comes from the script into
333            # the debug log
334            self._testServer.add_url_handler(url='/log',\
335                handler_func=(lambda handler, forms, loop_counter=i:\
336                    _extension_log_handler(handler, forms, loop_counter)))
337
338            pagetime_tracking = self._testServer.add_wait_url(url='/pagetime')
339
340            self._testServer.add_url_handler(url='/pagetime',\
341                handler_func=(lambda handler, forms, test_instance=self,
342                              loop_counter=i:\
343                    _extension_page_time_info_handler(handler, forms,
344                                                      loop_counter,
345                                                      test_instance)))
346
347            # setup a handler to simulate waking up the base of a detachable
348            # on user interaction. On scrolling, wake for 1s, on page
349            # navigation, wake for 10s.
350            self._testServer.add_url(url='/pagenav')
351            self._testServer.add_url(url='/scroll')
352
353            self._testServer.add_url_handler(url='/pagenav',
354                handler_func=(lambda handler, args, plt=self:
355                              plt._detachable_handler.wake_base(10000)))
356
357            self._testServer.add_url_handler(url='/scroll',
358                handler_func=(lambda handler, args, plt=self:
359                              plt._detachable_handler.wake_base(1000)))
360            # reset backlight level since powerd might've modified it
361            # based on ambient light
362            self._set_backlight_level()
363            self._set_lightbar_level()
364            if self._keyboard_backlight:
365                self._set_keyboard_backlight_level()
366            audio_helper.set_volume_levels(self._volume_level,
367                                           self._mic_gain)
368
369            low_battery = self._do_wait(self._verbose, self._loop_time,
370                                        latch)
371
372            script_logging.set()
373            pagetime_tracking.set()
374
375            self._log_loop_checkpoint(i, start_time, time.time())
376
377            if self._verbose:
378                logging.debug('loop %d completed', i)
379
380            if low_battery:
381                logging.info('Exiting due to low battery')
382                break
383
384        # done with logging from the script, so we can collect that thread
385        t1 = time.time()
386        psr.refresh()
387        self._tmp_keyvals['minutes_battery_life_tested'] = (t1 - t0) / 60
388        self._tmp_keyvals.update(psr.get_keyvals())
389
390    def postprocess_iteration(self):
391        """Postprocess: write keyvals / log and send data to power dashboard."""
392        def _log_stats(prefix, stats):
393            if not len(stats):
394                return
395            np = numpy.array(stats)
396            logging.debug("%s samples: %d", prefix, len(np))
397            logging.debug("%s mean:    %.2f", prefix, np.mean())
398            logging.debug("%s stdev:   %.2f", prefix, np.std())
399            logging.debug("%s max:     %.2f", prefix, np.max())
400            logging.debug("%s min:     %.2f", prefix, np.min())
401
402
403        def _log_per_loop_stats():
404            samples_per_loop = self._loop_time / self._wait_time + 1
405            for kname in self._stats:
406                start_idx = 0
407                loop = 1
408                for end_idx in xrange(samples_per_loop, len(self._stats[kname]),
409                                      samples_per_loop):
410                    _log_stats("%s loop %d" % (kname, loop),
411                               self._stats[kname][start_idx:end_idx])
412                    loop += 1
413                    start_idx = end_idx
414
415
416        def _log_all_stats():
417            for kname in self._stats:
418                _log_stats(kname, self._stats[kname])
419
420
421        for task, tstart, tend in self._task_tracker:
422            self._checkpoint_logger.checkpoint('_' + task, tstart, tend)
423
424        keyvals = {}
425        for log in self._meas_logs:
426            keyvals.update(log.calc())
427        keyvals.update(self._statomatic.publish())
428
429        if self._log_mem_bandwidth:
430            self._mlog.stop()
431            self._mlog.join()
432
433        _log_all_stats()
434        _log_per_loop_stats()
435
436        # record battery stats
437        if self._power_status.battery:
438            keyvals['a_current_now'] = self._power_status.battery[0].current_now
439            keyvals['ah_charge_full'] = \
440                    self._power_status.battery[0].charge_full
441            keyvals['ah_charge_full_design'] = \
442                    self._power_status.battery[0].charge_full_design
443            keyvals['ah_charge_start'] = self._ah_charge_start
444            keyvals['ah_charge_now'] = self._power_status.battery[0].charge_now
445            keyvals['ah_charge_used'] = keyvals['ah_charge_start'] - \
446                                        keyvals['ah_charge_now']
447            keyvals['wh_energy_start'] = self._wh_energy_start
448            keyvals['wh_energy_now'] = self._power_status.battery[0].energy
449            keyvals['wh_energy_used'] = keyvals['wh_energy_start'] - \
450                                        keyvals['wh_energy_now']
451            keyvals['v_voltage_min_design'] = \
452                    self._power_status.battery[0].voltage_min_design
453            keyvals['wh_energy_full_design'] = \
454                    self._power_status.battery[0].energy_full_design
455            keyvals['v_voltage_now'] = self._power_status.battery[0].voltage_now
456
457        keyvals.update(self._tmp_keyvals)
458
459        keyvals['percent_sys_low_battery'] = self._sys_low_batt_p
460        keyvals['seconds_sys_low_battery'] = self._sys_low_batt_s
461        voltage_np = numpy.array(self._stats['v_voltage_now'])
462        voltage_mean = voltage_np.mean()
463        keyvals['v_voltage_mean'] = voltage_mean
464
465        keyvals['wh_energy_powerlogger'] = \
466                             self._energy_use_from_powerlogger(keyvals)
467
468        if not self._power_status.on_ac() and keyvals['ah_charge_used'] > 0:
469            # For full runs, we should use charge to scale for battery life,
470            # since the voltage swing is accounted for.
471            # For short runs, energy will be a better estimate.
472            if self._loop_count > 1:
473                estimated_reps = (keyvals['ah_charge_full_design'] /
474                                  keyvals['ah_charge_used'])
475            else:
476                estimated_reps = (keyvals['wh_energy_full_design'] /
477                                  keyvals['wh_energy_powerlogger'])
478
479            bat_life_scale =  estimated_reps * \
480                              ((100 - keyvals['percent_sys_low_battery']) / 100)
481
482            keyvals['minutes_battery_life'] = bat_life_scale * \
483                keyvals['minutes_battery_life_tested']
484            # In the case where sys_low_batt_s is non-zero subtract those
485            # minutes from the final extrapolation.
486            if self._sys_low_batt_s:
487                keyvals['minutes_battery_life'] -= self._sys_low_batt_s / 60
488
489            keyvals['a_current_rate'] = keyvals['ah_charge_used'] * 60 / \
490                                        keyvals['minutes_battery_life_tested']
491            keyvals['w_energy_rate'] = keyvals['wh_energy_used'] * 60 / \
492                                       keyvals['minutes_battery_life_tested']
493            if self._gaia_login:
494                self.output_perf_value(description='minutes_battery_life',
495                                       value=keyvals['minutes_battery_life'],
496                                       units='minutes',
497                                       higher_is_better=True)
498
499        minutes_battery_life_tested = keyvals['minutes_battery_life_tested']
500
501        # TODO(coconutruben): overwrite write_perf_keyvals for all power
502        # tests and replace this once power_LoadTest inherits from power_Test.
503        # Dump all keyvals into debug keyvals.
504        _utils.write_keyval(os.path.join(self.resultsdir, 'debug_keyval'),
505                            keyvals)
506        # Avoid polluting the keyvals with non-core domains.
507        core_keyvals = power_utils.get_core_keyvals(keyvals)
508        if not self._gaia_login:
509            core_keyvals = {'INVALID_%s' % str(k): v for k, v in
510                            core_keyvals.iteritems()}
511        else:
512            for key, value in core_keyvals.iteritems():
513                if key.startswith('percent_cpuidle') and \
514                   key.endswith('C0_time'):
515                    self.output_perf_value(description=key,
516                                           value=value,
517                                           units='percent',
518                                           higher_is_better=False)
519
520        self.write_perf_keyval(core_keyvals)
521        for log in self._meas_logs:
522            log.save_results(self.resultsdir)
523        self._checkpoint_logger.save_checkpoint_data(self.resultsdir)
524
525        if minutes_battery_life_tested * 60 < self._loop_time :
526            logging.info('Data is less than 1 loop, skip sending to dashboard.')
527            return
528        pdash = power_dashboard.PowerLoggerDashboard( \
529                self._plog, self.tagged_testname, self.resultsdir,
530                note=self._pdash_note)
531        pdash.upload()
532        cdash = power_dashboard.CPUStatsLoggerDashboard( \
533                self._clog, self.tagged_testname, self.resultsdir,
534                note=self._pdash_note)
535        cdash.upload()
536        tdash = power_dashboard.TempLoggerDashboard( \
537                self._tlog, self.tagged_testname, self.resultsdir,
538                note=self._pdash_note)
539        tdash.upload()
540
541
542    def cleanup(self):
543        if self._force_discharge:
544            power_utils.charge_control_by_ectool(True)
545        if self._backlight:
546            self._backlight.restore()
547        if self._services:
548            self._services.restore_services()
549        self._detachable_handler.restore()
550
551        # cleanup backchannel interface
552        # Prevent wifi congestion in test lab by forcing machines to forget the
553        # wifi AP we connected to at the start of the test.
554        if self._shill_proxy:
555            self._shill_proxy.remove_all_wifi_entries()
556        if self._backchannel:
557            self._backchannel.teardown()
558        if self._browser:
559            self._browser.close()
560        if self._testServer:
561            self._testServer.stop()
562
563
564    def _do_wait(self, verbose, seconds, latch):
565        latched = False
566        low_battery = False
567        total_time = seconds + self._wait_time
568        elapsed_time = 0
569
570        while elapsed_time < total_time:
571            time.sleep(self._wait_time)
572            elapsed_time += self._wait_time
573
574            self._power_status.refresh()
575
576            if not self._ac_ok and self._power_status.on_ac():
577                raise error.TestError('Running on AC power now.')
578
579            if self._power_status.battery:
580                charge_now = self._power_status.battery[0].charge_now
581                energy_rate = self._power_status.battery[0].energy_rate
582                voltage_now = self._power_status.battery[0].voltage_now
583                self._stats['w_energy_rate'].append(energy_rate)
584                self._stats['v_voltage_now'].append(voltage_now)
585                if verbose:
586                    logging.debug('ah_charge_now %f', charge_now)
587                    logging.debug('w_energy_rate %f', energy_rate)
588                    logging.debug('v_voltage_now %f', voltage_now)
589
590                low_battery = (self._power_status.percent_current_charge() <
591                               self._test_low_batt_p)
592
593            latched = latch.is_set()
594
595            if latched or low_battery:
596                break
597
598        if latched:
599            # record chrome power extension stats
600            form_data = self._testServer.get_form_entries()
601            logging.debug(form_data)
602            for e in form_data:
603                key = 'ext_' + e
604                if key in self._tmp_keyvals:
605                    self._tmp_keyvals[key] += "_%s" % form_data[e]
606                else:
607                    self._tmp_keyvals[key] = form_data[e]
608        else:
609            logging.debug("Didn't get status back from power extension")
610
611        return low_battery
612
613    def _set_backlight_level(self):
614        self._backlight.set_default()
615        # record brightness level
616        self._tmp_keyvals['level_backlight_current'] = \
617            self._backlight.get_level()
618
619
620    def _set_lightbar_level(self, level='off'):
621        """Set lightbar level.
622
623        Args:
624          level: string value to set lightbar to.  See ectool for more details.
625        """
626        rv = utils.system('which ectool', ignore_status=True)
627        if rv:
628            return
629        rv = utils.system('ectool lightbar %s' % level, ignore_status=True)
630        if rv:
631            logging.info('Assuming no lightbar due to non-zero exit status')
632        else:
633            logging.info('Setting lightbar to %s', level)
634            self._tmp_keyvals['level_lightbar_current'] = level
635
636
637    def _has_light_sensor(self):
638        """
639        Determine if there is a light sensor on the board.
640
641        @returns True if this host has a light sensor or
642                 False if it does not.
643        """
644        # If the command exits with a failure status,
645        # we do not have a light sensor
646        cmd = 'check_powerd_config --ambient_light_sensor'
647        result = utils.run(cmd, ignore_status=True)
648        if result.exit_status:
649            logging.debug('Ambient light sensor not present')
650            return False
651        logging.debug('Ambient light sensor present')
652        return True
653
654
655    def _energy_use_from_powerlogger(self, keyval):
656        """
657        Calculates the energy use, in Wh, used over the course of the run as
658        reported by the PowerLogger.
659
660        Args:
661          keyval: the dictionary of keyvals containing PowerLogger output
662
663        Returns:
664          energy_wh: total energy used over the course of this run
665
666        """
667        energy_wh = 0
668        loop = 0
669        while True:
670            duration_key = 'loop%d_system_duration' % loop
671            avg_power_key = 'loop%d_system_pwr_avg' % loop
672            if duration_key not in keyval or avg_power_key not in keyval:
673                break
674            energy_wh += keyval[duration_key] * keyval[avg_power_key] / 3600
675            loop += 1
676        return energy_wh
677
678
679    def _has_hover_detection(self):
680        """
681        Checks if hover is detected by the device.
682
683        Returns:
684            Returns True if the hover detection support is enabled.
685            Else returns false.
686        """
687
688        cmd = 'check_powerd_config --hover_detection'
689        result = utils.run(cmd, ignore_status=True)
690        if result.exit_status:
691            logging.debug('Hover not present')
692            return False
693        logging.debug('Hover present')
694        return True
695
696
697    def _set_keyboard_backlight_level(self):
698        """
699        Sets keyboard backlight based on light sensor and hover.
700        These values are based on UMA as mentioned in
701        https://bugs.chromium.org/p/chromium/issues/detail?id=603233#c10
702
703        ALS  | hover | keyboard backlight level
704        ---------------------------------------
705        No   | No    | default
706        ---------------------------------------
707        Yes  | No    | 40% of default
708        --------------------------------------
709        No   | Yes   | System with this configuration does not exist
710        --------------------------------------
711        Yes  | Yes   | 30% of default
712        --------------------------------------
713
714        Here default is no Ambient Light Sensor, no hover,
715        default always-on brightness level.
716        """
717
718        default_level = self._keyboard_backlight.get_default_level()
719        level_to_set = default_level
720        has_light_sensor = self._has_light_sensor()
721        has_hover = self._has_hover_detection()
722        # TODO(ravisadineni):if (crbug: 603233) becomes default
723        # change this to reflect it.
724        if has_light_sensor and has_hover:
725            level_to_set = (30 * default_level) / 100
726        elif has_light_sensor:
727            level_to_set = (40 * default_level) / 100
728        elif has_hover:
729            logging.warn('Device has hover but no light sensor')
730
731        logging.info('Setting keyboard backlight to %d', level_to_set)
732        self._keyboard_backlight.set_level(level_to_set)
733        self._tmp_keyvals['percent_kbd_backlight'] = \
734            self._keyboard_backlight.get_percent()
735
736    def _log_loop_checkpoint(self, loop, start, end):
737        loop_str = 'loop%d' % loop
738        self._checkpoint_logger.checkpoint(loop_str, start, end)
739
740        # Don't log section if we run custom tasks.
741        if self._tasks != '':
742            return
743
744        sections = [
745            ('browsing', (0, 0.6)),
746            ('email', (0.6, 0.8)),
747            ('document', (0.8, 0.9)),
748            ('video', (0.9, 1)),
749        ]
750
751        # Use start time from extension if found by look for google.com start.
752        goog_str = loop_str+ '_web_page_www.google.com'
753        for item, start_extension, _ in self._task_tracker:
754            if item == goog_str:
755                if start_extension >= start:
756                    start = start_extension
757                    break
758                logging.warn('Timestamp from extension (%.2f) is earlier than'
759                             'timestamp from autotest (%.2f).',
760                             start_extension, start)
761
762        # Use default loop duration for incomplete loop.
763        duration = max(end - start, self._loop_time)
764
765        for section, fractions in sections:
766            s_start, s_end = (start + duration * fraction
767                              for fraction in fractions)
768            if s_start > end:
769                break
770            if s_end > end:
771                s_end = end
772            self._checkpoint_logger.checkpoint(section, s_start, s_end)
773            loop_section = '_' + loop_str + '_' + section
774            self._checkpoint_logger.checkpoint(loop_section, s_start, s_end)
775
776
777def _extension_log_handler(handler, form, loop_number):
778    """
779    We use the httpd library to allow us to log whatever we
780    want from the extension JS script into the log files.
781
782    This method is provided to the server as a handler for
783    all requests that come for the log url in the testServer
784
785    unused parameter, because httpd passes the server itself
786    into the handler.
787    """
788    if form:
789        for field in form.keys():
790            logging.debug("[extension] @ loop_%d %s", loop_number,
791            form[field].value)
792            # we don't want to add url information to our keyvals.
793            # httpd adds them automatically so we remove them again
794            del handler.server._form_entries[field]
795
796def _extension_page_time_info_handler(handler, form, loop_number,
797                                      test_instance):
798    page_timestamps = []
799
800    stats_ids = ['mean', 'min', 'max', 'std']
801    loadtime_measurements = []
802    sorted_pagelt = []
803    #show up to this number of slow page-loads
804    num_slow_page_loads = 5
805
806    if not form:
807        logging.debug("no page time information returned")
808        return
809
810    for field in form.keys():
811        url = field[str.find(field, "http"):]  # remove unique url salt
812        page = json.loads(form[field].value)
813
814        logging.debug("[extension] @ loop_%d url: %s start_time: %d",
815            loop_number, url, page['start_time'])
816
817        if page['end_load_time']:
818            logging.debug("[extension] @ loop_%d url: %s end_load_time: %d",
819                loop_number, url, page['end_load_time'])
820
821            load_time = page['end_load_time'] - page['start_time']
822
823            loadtime_measurements.append(load_time)
824            sorted_pagelt.append((url, load_time))
825
826            logging.debug("[extension] @ loop_%d url: %s load time: %d ms",
827                loop_number, url, load_time)
828
829        logging.debug("[extension] @ loop_%d url: %s end_browse_time: %d",
830            loop_number, url, page['end_browse_time'])
831
832        page_timestamps.append(page)
833
834        # we don't want to add url information to our keyvals.
835        # httpd adds them automatically so we remove them again
836        del handler.server._form_entries[field]
837
838    page_base = 'loop%d_web_page_' % loop_number
839    for page in page_timestamps:
840        page_failed = "_failed"
841        # timestamps from javascript are in milliseconds, change to seconds
842        scale = 1.0/1000
843        if page['end_load_time']:
844            tagname = page_base + page['url'] + "_load"
845            test_instance._task_tracker.append((tagname,
846                page['start_time'] * scale, page['end_load_time'] * scale))
847
848            tagname = page_base + page['url'] + "_browse"
849            test_instance._task_tracker.append((tagname,
850                page['end_load_time'] * scale, page['end_browse_time'] * scale))
851
852            page_failed = ""
853
854        tagname = page_base + page['url'] + page_failed
855        test_instance._task_tracker.append((tagname,
856            page['start_time'] * scale, page['end_browse_time'] * scale))
857
858    loadtime_measurements = numpy.array(loadtime_measurements)
859    stats_vals = [loadtime_measurements.mean(), loadtime_measurements.min(),
860        loadtime_measurements.max(),loadtime_measurements.std()]
861
862    key_base = 'ext_ms_page_load_time_'
863    for i in range(len(stats_ids)):
864        key = key_base + stats_ids[i]
865        if key in test_instance._tmp_keyvals:
866            test_instance._tmp_keyvals[key] += "_%.2f" % stats_vals[i]
867        else:
868            test_instance._tmp_keyvals[key] = "%.2f" % stats_vals[i]
869
870
871    sorted_pagelt.sort(key=lambda item: item[1], reverse=True)
872
873    message = "The %d slowest page-load-times are:\n" % (num_slow_page_loads)
874    for url, msecs in sorted_pagelt[:num_slow_page_loads]:
875        message += "\t%s w/ %d ms" % (url, msecs)
876
877    logging.debug("%s\n", message)
878