Home | History | Annotate | Download | only in cros
      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 
      5 import collections, ctypes, fcntl, glob, logging, math, numpy, os, re, struct
      6 import threading, time
      7 
      8 from autotest_lib.client.bin import utils
      9 from autotest_lib.client.common_lib import error, enum
     10 from autotest_lib.client.cros import kernel_trace
     11 
     12 BatteryDataReportType = enum.Enum('CHARGE', 'ENERGY')
     13 
     14 # battery data reported at 1e6 scale
     15 BATTERY_DATA_SCALE = 1e6
     16 # number of times to retry reading the battery in the case of bad data
     17 BATTERY_RETRY_COUNT = 3
     18 
     19 class DevStat(object):
     20     """
     21     Device power status. This class implements generic status initialization
     22     and parsing routines.
     23     """
     24 
     25     def __init__(self, fields, path=None):
     26         self.fields = fields
     27         self.path = path
     28 
     29 
     30     def reset_fields(self):
     31         """
     32         Reset all class fields to None to mark their status as unknown.
     33         """
     34         for field in self.fields.iterkeys():
     35             setattr(self, field, None)
     36 
     37 
     38     def read_val(self,  file_name, field_type):
     39         try:
     40             path = file_name
     41             if not file_name.startswith('/'):
     42                 path = os.path.join(self.path, file_name)
     43             f = open(path, 'r')
     44             out = f.readline()
     45             val = field_type(out)
     46             return val
     47 
     48         except:
     49             return field_type(0)
     50 
     51 
     52     def read_all_vals(self):
     53         for field, prop in self.fields.iteritems():
     54             if prop[0]:
     55                 val = self.read_val(prop[0], prop[1])
     56                 setattr(self, field, val)
     57 
     58 
     59 class ThermalStatACPI(DevStat):
     60     """
     61     ACPI-based thermal status.
     62 
     63     Fields:
     64     (All temperatures are in millidegrees Celsius.)
     65 
     66     str   enabled:            Whether thermal zone is enabled
     67     int   temp:               Current temperature
     68     str   type:               Thermal zone type
     69     int   num_trip_points:    Number of thermal trip points that activate
     70                                 cooling devices
     71     int   num_points_tripped: Temperature is above this many trip points
     72     str   trip_point_N_type:  Trip point #N's type
     73     int   trip_point_N_temp:  Trip point #N's temperature value
     74     int   cdevX_trip_point:   Trip point o cooling device #X (index)
     75     """
     76 
     77     MAX_TRIP_POINTS = 20
     78 
     79     thermal_fields = {
     80         'enabled':              ['enabled', str],
     81         'temp':                 ['temp', int],
     82         'type':                 ['type', str],
     83         'num_points_tripped':   ['', '']
     84         }
     85     path = '/sys/class/thermal/thermal_zone*'
     86 
     87     def __init__(self, path=None):
     88         # Browse the thermal folder for trip point fields.
     89         self.num_trip_points = 0
     90 
     91         thermal_fields = glob.glob(path + '/*')
     92         for file in thermal_fields:
     93             field = file[len(path + '/'):]
     94             if field.find('trip_point') != -1:
     95                 if field.find('temp'):
     96                     field_type = int
     97                 else:
     98                     field_type = str
     99                 self.thermal_fields[field] = [field, field_type]
    100 
    101                 # Count the number of trip points.
    102                 if field.find('_type') != -1:
    103                     self.num_trip_points += 1
    104 
    105         super(ThermalStatACPI, self).__init__(self.thermal_fields, path)
    106         self.update()
    107 
    108     def update(self):
    109         if not os.path.exists(self.path):
    110             return
    111 
    112         self.read_all_vals()
    113         self.num_points_tripped = 0
    114 
    115         for field in self.thermal_fields:
    116             if field.find('trip_point_') != -1 and field.find('_temp') != -1 \
    117                     and self.temp > self.read_val(field, int):
    118                 self.num_points_tripped += 1
    119                 logging.info('Temperature trip point #' + \
    120                             field[len('trip_point_'):field.rfind('_temp')] + \
    121                             ' tripped.')
    122 
    123 
    124 class ThermalStatHwmon(DevStat):
    125     """
    126     hwmon-based thermal status.
    127 
    128     Fields:
    129     int   <tname>_temp<num>_input: Current temperature in millidegrees Celsius
    130       where:
    131           <tname> : name of hwmon device in sysfs
    132           <num>   : number of temp as some hwmon devices have multiple
    133 
    134     """
    135     path = '/sys/class/hwmon'
    136 
    137     thermal_fields = {}
    138     def __init__(self, rootpath=None):
    139         if not rootpath:
    140             rootpath = self.path
    141         for subpath1 in glob.glob('%s/hwmon*' % rootpath):
    142             for subpath2 in ['','device/']:
    143                 gpaths = glob.glob("%s/%stemp*_input" % (subpath1, subpath2))
    144                 for gpath in gpaths:
    145                     bname = os.path.basename(gpath)
    146                     field_path = os.path.join(subpath1, subpath2, bname)
    147 
    148                     tname_path = os.path.join(os.path.dirname(gpath), "name")
    149                     tname = utils.read_one_line(tname_path)
    150 
    151                     field_key = "%s_%s" % (tname, bname)
    152                     self.thermal_fields[field_key] = [field_path, int]
    153 
    154         super(ThermalStatHwmon, self).__init__(self.thermal_fields, rootpath)
    155         self.update()
    156 
    157     def update(self):
    158         if not os.path.exists(self.path):
    159             return
    160 
    161         self.read_all_vals()
    162 
    163     def read_val(self,  file_name, field_type):
    164         try:
    165             path = os.path.join(self.path, file_name)
    166             f = open(path, 'r')
    167             out = f.readline()
    168             return field_type(out)
    169         except:
    170             return field_type(0)
    171 
    172 
    173 class ThermalStat(object):
    174     """helper class to instantiate various thermal devices."""
    175     def __init__(self):
    176         self._thermals = []
    177         self.min_temp = 999999999
    178         self.max_temp = -999999999
    179 
    180         thermal_stat_types = [(ThermalStatHwmon.path, ThermalStatHwmon),
    181                               (ThermalStatACPI.path, ThermalStatACPI)]
    182         for thermal_glob_path, thermal_type in thermal_stat_types:
    183             try:
    184                 thermal_path = glob.glob(thermal_glob_path)[0]
    185                 logging.debug('Using %s for thermal info.' % thermal_path)
    186                 self._thermals.append(thermal_type(thermal_path))
    187             except:
    188                 logging.debug('Could not find thermal path %s, skipping.' %
    189                               thermal_glob_path)
    190 
    191 
    192     def get_temps(self):
    193         """Get temperature readings.
    194 
    195         Returns:
    196             string of temperature readings.
    197         """
    198         temp_str = ''
    199         for thermal in self._thermals:
    200             thermal.update()
    201             for kname in thermal.fields:
    202                 if kname is 'temp' or kname.endswith('_input'):
    203                     val = getattr(thermal, kname)
    204                     temp_str += '%s:%d ' % (kname, val)
    205                     if val > self.max_temp:
    206                         self.max_temp = val
    207                     if val < self.min_temp:
    208                         self.min_temp = val
    209 
    210 
    211         return temp_str
    212 
    213 
    214 class BatteryStat(DevStat):
    215     """
    216     Battery status.
    217 
    218     Fields:
    219 
    220     float charge_full:        Last full capacity reached [Ah]
    221     float charge_full_design: Full capacity by design [Ah]
    222     float charge_now:         Remaining charge [Ah]
    223     float current_now:        Battery discharge rate [A]
    224     float energy:             Current battery charge [Wh]
    225     float energy_full:        Last full capacity reached [Wh]
    226     float energy_full_design: Full capacity by design [Wh]
    227     float energy_rate:        Battery discharge rate [W]
    228     float power_now:          Battery discharge rate [W]
    229     float remaining_time:     Remaining discharging time [h]
    230     float voltage_min_design: Minimum voltage by design [V]
    231     float voltage_max_design: Maximum voltage by design [V]
    232     float voltage_now:        Voltage now [V]
    233     """
    234 
    235     battery_fields = {
    236         'status':               ['status', str],
    237         'charge_full':          ['charge_full', float],
    238         'charge_full_design':   ['charge_full_design', float],
    239         'charge_now':           ['charge_now', float],
    240         'current_now':          ['current_now', float],
    241         'voltage_min_design':   ['voltage_min_design', float],
    242         'voltage_max_design':   ['voltage_max_design', float],
    243         'voltage_now':          ['voltage_now', float],
    244         'energy':               ['energy_now', float],
    245         'energy_full':          ['energy_full', float],
    246         'energy_full_design':   ['energy_full_design', float],
    247         'power_now':            ['power_now', float],
    248         'energy_rate':          ['', ''],
    249         'remaining_time':       ['', '']
    250         }
    251 
    252     def __init__(self, path=None):
    253         super(BatteryStat, self).__init__(self.battery_fields, path)
    254         self.update()
    255 
    256 
    257     def update(self):
    258         for _ in xrange(BATTERY_RETRY_COUNT):
    259             try:
    260                 self._read_battery()
    261                 return
    262             except error.TestError as e:
    263                 logging.warn(e)
    264                 continue
    265         raise error.TestError('Failed to read battery state')
    266 
    267 
    268     def _read_battery(self):
    269         self.read_all_vals()
    270 
    271         if self.charge_full == 0 and self.energy_full != 0:
    272             battery_type = BatteryDataReportType.ENERGY
    273         else:
    274             battery_type = BatteryDataReportType.CHARGE
    275 
    276         if self.voltage_min_design != 0:
    277             voltage_nominal = self.voltage_min_design
    278         else:
    279             voltage_nominal = self.voltage_now
    280 
    281         if voltage_nominal == 0:
    282             raise error.TestError('Failed to determine battery voltage')
    283 
    284         # Since charge data is present, calculate parameters based upon
    285         # reported charge data.
    286         if battery_type == BatteryDataReportType.CHARGE:
    287             self.charge_full = self.charge_full / BATTERY_DATA_SCALE
    288             self.charge_full_design = self.charge_full_design / \
    289                                       BATTERY_DATA_SCALE
    290             self.charge_now = self.charge_now / BATTERY_DATA_SCALE
    291 
    292             self.current_now = math.fabs(self.current_now) / \
    293                                BATTERY_DATA_SCALE
    294 
    295             self.energy =  voltage_nominal * \
    296                            self.charge_now / \
    297                            BATTERY_DATA_SCALE
    298             self.energy_full = voltage_nominal * \
    299                                self.charge_full / \
    300                                BATTERY_DATA_SCALE
    301             self.energy_full_design = voltage_nominal * \
    302                                       self.charge_full_design / \
    303                                       BATTERY_DATA_SCALE
    304 
    305         # Charge data not present, so calculate parameters based upon
    306         # reported energy data.
    307         elif battery_type == BatteryDataReportType.ENERGY:
    308             self.charge_full = self.energy_full / voltage_nominal
    309             self.charge_full_design = self.energy_full_design / \
    310                                       voltage_nominal
    311             self.charge_now = self.energy / voltage_nominal
    312 
    313             # TODO(shawnn): check if power_now can really be reported
    314             # as negative, in the same way current_now can
    315             self.current_now = math.fabs(self.power_now) / \
    316                                voltage_nominal
    317 
    318             self.energy = self.energy / BATTERY_DATA_SCALE
    319             self.energy_full = self.energy_full / BATTERY_DATA_SCALE
    320             self.energy_full_design = self.energy_full_design / \
    321                                       BATTERY_DATA_SCALE
    322 
    323         self.voltage_min_design = self.voltage_min_design / \
    324                                   BATTERY_DATA_SCALE
    325         self.voltage_max_design = self.voltage_max_design / \
    326                                   BATTERY_DATA_SCALE
    327         self.voltage_now = self.voltage_now / \
    328                            BATTERY_DATA_SCALE
    329         voltage_nominal = voltage_nominal / \
    330                           BATTERY_DATA_SCALE
    331 
    332         if self.charge_full > (self.charge_full_design * 1.5):
    333             raise error.TestError('Unreasonable charge_full value')
    334         if self.charge_now > (self.charge_full_design * 1.5):
    335             raise error.TestError('Unreasonable charge_now value')
    336 
    337         self.energy_rate =  self.voltage_now * self.current_now
    338 
    339         self.remaining_time = 0
    340         if self.current_now and self.energy_rate:
    341             self.remaining_time =  self.energy / self.energy_rate
    342 
    343 
    344 class LineStatDummy(object):
    345     """
    346     Dummy line stat for devices which don't provide power_supply related sysfs
    347     interface.
    348     """
    349     def __init__(self):
    350         self.online = True
    351 
    352 
    353     def update(self):
    354         pass
    355 
    356 class LineStat(DevStat):
    357     """
    358     Power line status.
    359 
    360     Fields:
    361 
    362     bool online:              Line power online
    363     """
    364 
    365     linepower_fields = {
    366         'is_online':             ['online', int]
    367         }
    368 
    369 
    370     def __init__(self, path=None):
    371         super(LineStat, self).__init__(self.linepower_fields, path)
    372         logging.debug("line path: %s", path)
    373         self.update()
    374 
    375 
    376     def update(self):
    377         self.read_all_vals()
    378         self.online = self.is_online == 1
    379 
    380 
    381 class SysStat(object):
    382     """
    383     System power status for a given host.
    384 
    385     Fields:
    386 
    387     battery:   A list of BatteryStat objects.
    388     linepower: A list of LineStat objects.
    389     """
    390     psu_types = ['Mains', 'USB', 'USB_ACA', 'USB_C', 'USB_CDP', 'USB_DCP',
    391                  'USB_PD', 'USB_PD_DRP', 'Unknown']
    392 
    393     def __init__(self):
    394         power_supply_path = '/sys/class/power_supply/*'
    395         self.battery = None
    396         self.linepower = []
    397         self.thermal = None
    398         self.battery_path = None
    399         self.linepower_path = []
    400 
    401         power_supplies = glob.glob(power_supply_path)
    402         for path in power_supplies:
    403             type_path = os.path.join(path,'type')
    404             if not os.path.exists(type_path):
    405                 continue
    406             power_type = utils.read_one_line(type_path)
    407             if power_type == 'Battery':
    408                 self.battery_path = path
    409             elif power_type in self.psu_types:
    410                 self.linepower_path.append(path)
    411 
    412         if not self.battery_path or not self.linepower_path:
    413             logging.warning("System does not provide power sysfs interface")
    414 
    415         self.thermal = ThermalStat()
    416 
    417 
    418     def refresh(self):
    419         """
    420         Initialize device power status objects.
    421         """
    422         self.linepower = []
    423 
    424         if self.battery_path:
    425             self.battery = [ BatteryStat(self.battery_path) ]
    426 
    427         for path in self.linepower_path:
    428             self.linepower.append(LineStat(path))
    429         if not self.linepower:
    430             self.linepower = [ LineStatDummy() ]
    431 
    432         temp_str = self.thermal.get_temps()
    433         if temp_str:
    434             logging.info('Temperature reading: ' + temp_str)
    435         else:
    436             logging.error('Could not read temperature, skipping.')
    437 
    438 
    439     def on_ac(self):
    440         """
    441         Returns true if device is currently running from AC power.
    442         """
    443         on_ac = False
    444         for linepower in self.linepower:
    445             on_ac |= linepower.online
    446 
    447         # Butterfly can incorrectly report AC online for some time after
    448         # unplug. Check battery discharge state to confirm.
    449         if utils.get_board() == 'butterfly':
    450             on_ac &= (not self.battery_discharging())
    451         return on_ac
    452 
    453     def battery_discharging(self):
    454         """
    455         Returns true if battery is currently discharging.
    456         """
    457         return(self.battery[0].status.rstrip() == 'Discharging')
    458 
    459     def percent_current_charge(self):
    460         return self.battery[0].charge_now * 100 / \
    461                self.battery[0].charge_full_design
    462 
    463 
    464     def assert_battery_state(self, percent_initial_charge_min):
    465         """Check initial power configuration state is battery.
    466 
    467         Args:
    468           percent_initial_charge_min: float between 0 -> 1.00 of
    469             percentage of battery that must be remaining.
    470             None|0|False means check not performed.
    471 
    472         Raises:
    473           TestError: if one of battery assertions fails
    474         """
    475         if self.on_ac():
    476             raise error.TestError(
    477                 'Running on AC power. Please remove AC power cable.')
    478 
    479         percent_initial_charge = self.percent_current_charge()
    480 
    481         if percent_initial_charge_min and percent_initial_charge < \
    482                                           percent_initial_charge_min:
    483             raise error.TestError('Initial charge (%f) less than min (%f)'
    484                       % (percent_initial_charge, percent_initial_charge_min))
    485 
    486 
    487 def get_status():
    488     """
    489     Return a new power status object (SysStat). A new power status snapshot
    490     for a given host can be obtained by either calling this routine again and
    491     constructing a new SysStat object, or by using the refresh method of the
    492     SysStat object.
    493     """
    494     status = SysStat()
    495     status.refresh()
    496     return status
    497 
    498 
    499 class AbstractStats(object):
    500     """
    501     Common superclass for measurements of percentages per state over time.
    502 
    503     Public Attributes:
    504         incremental:  If False, stats returned are from a single
    505         _read_stats.  Otherwise, stats are from the difference between
    506         the current and last refresh.
    507     """
    508 
    509     @staticmethod
    510     def to_percent(stats):
    511         """
    512         Turns a dict with absolute time values into a dict with percentages.
    513         """
    514         total = sum(stats.itervalues())
    515         if total == 0:
    516             return {}
    517         return dict((k, v * 100.0 / total) for (k, v) in stats.iteritems())
    518 
    519 
    520     @staticmethod
    521     def do_diff(new, old):
    522         """
    523         Returns a dict with value deltas from two dicts with matching keys.
    524         """
    525         return dict((k, new[k] - old.get(k, 0)) for k in new.iterkeys())
    526 
    527 
    528     @staticmethod
    529     def format_results_percent(results, name, percent_stats):
    530         """
    531         Formats autotest result keys to format:
    532           percent_<name>_<key>_time
    533         """
    534         for key in percent_stats:
    535             results['percent_%s_%s_time' % (name, key)] = percent_stats[key]
    536 
    537 
    538     @staticmethod
    539     def format_results_wavg(results, name, wavg):
    540         """
    541         Add an autotest result keys to format: wavg_<name>
    542         """
    543         if wavg is not None:
    544             results['wavg_%s' % (name)] = wavg
    545 
    546 
    547     def __init__(self, name=None, incremental=True):
    548         if not name:
    549             error.TestFail("Need to name AbstractStats instance please.")
    550         self.name = name
    551         self.incremental = incremental
    552         self._stats = self._read_stats()
    553 
    554 
    555     def refresh(self):
    556         """
    557         Returns dict mapping state names to percentage of time spent in them.
    558         """
    559         raw_stats = result = self._read_stats()
    560         if self.incremental:
    561             result = self.do_diff(result, self._stats)
    562         self._stats = raw_stats
    563         return self.to_percent(result)
    564 
    565 
    566     def _automatic_weighted_average(self):
    567         """
    568         Turns a dict with absolute times (or percentages) into a weighted
    569         average value.
    570         """
    571         total = sum(self._stats.itervalues())
    572         if total == 0:
    573             return None
    574 
    575         return sum((float(k)*v) / total for (k, v) in self._stats.iteritems())
    576 
    577 
    578     def _supports_automatic_weighted_average(self):
    579         """
    580         Override!
    581 
    582         Returns True if stats collected can be automatically converted from
    583         percent distribution to weighted average. False otherwise.
    584         """
    585         return False
    586 
    587 
    588     def weighted_average(self):
    589         """
    590         Return weighted average calculated using the automated average method
    591         (if supported) or using a custom method defined by the stat.
    592         """
    593         if self._supports_automatic_weighted_average():
    594             return self._automatic_weighted_average()
    595 
    596         return self._weighted_avg_fn()
    597 
    598 
    599     def _weighted_avg_fn(self):
    600         """
    601         Override! Custom weighted average function.
    602 
    603         Returns weighted average as a single floating point value.
    604         """
    605         return None
    606 
    607 
    608     def _read_stats(self):
    609         """
    610         Override! Reads the raw data values that shall be measured into a dict.
    611         """
    612         raise NotImplementedError('Override _read_stats in the subclass!')
    613 
    614 
    615 class CPUFreqStats(AbstractStats):
    616     """
    617     CPU Frequency statistics
    618     """
    619 
    620     def __init__(self, start_cpu=-1, end_cpu=-1):
    621         cpufreq_stats_path = '/sys/devices/system/cpu/cpu*/cpufreq/stats/' + \
    622                              'time_in_state'
    623         intel_pstate_stats_path = '/sys/devices/system/cpu/intel_pstate/' + \
    624                              'aperf_mperf'
    625         self._file_paths = glob.glob(cpufreq_stats_path)
    626         num_cpus = len(self._file_paths)
    627         self._intel_pstate_file_paths = glob.glob(intel_pstate_stats_path)
    628         self._running_intel_pstate = False
    629         self._initial_perf = None
    630         self._current_perf = None
    631         self._max_freq = 0
    632         name = 'cpufreq'
    633         if not self._file_paths:
    634             logging.debug('time_in_state file not found')
    635             if self._intel_pstate_file_paths:
    636                 logging.debug('intel_pstate frequency stats file found')
    637                 self._running_intel_pstate = True
    638         else:
    639             if (start_cpu >= 0 and end_cpu >= 0
    640                     and not (start_cpu == 0 and end_cpu == num_cpus - 1)):
    641                 self._file_paths = self._file_paths[start_cpu : end_cpu]
    642                 name += '_' + str(start_cpu) + '_' + str(end_cpu)
    643 
    644         super(CPUFreqStats, self).__init__(name=name)
    645 
    646 
    647     def _read_stats(self):
    648         if self._running_intel_pstate:
    649             aperf = 0
    650             mperf = 0
    651 
    652             for path in self._intel_pstate_file_paths:
    653                 if not os.path.exists(path):
    654                     logging.debug('%s is not found', path)
    655                     continue
    656                 data = utils.read_file(path)
    657                 for line in data.splitlines():
    658                     pair = line.split()
    659                     # max_freq is supposed to be the same for all CPUs
    660                     # and remain constant throughout.
    661                     # So, we set the entry only once
    662                     if not self._max_freq:
    663                         self._max_freq = int(pair[0])
    664                     aperf += int(pair[1])
    665                     mperf += int(pair[2])
    666 
    667             if not self._initial_perf:
    668                 self._initial_perf = (aperf, mperf)
    669 
    670             self._current_perf = (aperf, mperf)
    671 
    672         stats = {}
    673         for path in self._file_paths:
    674             if not os.path.exists(path):
    675                 logging.debug('%s is not found', path)
    676                 continue
    677 
    678             data = utils.read_file(path)
    679             for line in data.splitlines():
    680                 pair = line.split()
    681                 freq = int(pair[0])
    682                 timeunits = int(pair[1])
    683                 if freq in stats:
    684                     stats[freq] += timeunits
    685                 else:
    686                     stats[freq] = timeunits
    687         return stats
    688 
    689 
    690     def _supports_automatic_weighted_average(self):
    691         return not self._running_intel_pstate
    692 
    693 
    694     def _weighted_avg_fn(self):
    695         if not self._running_intel_pstate:
    696             return None
    697 
    698         if self._current_perf[1] != self._initial_perf[1]:
    699             # Avg freq = max_freq * aperf_delta / mperf_delta
    700             return self._max_freq * \
    701                 float(self._current_perf[0] - self._initial_perf[0]) / \
    702                 (self._current_perf[1] - self._initial_perf[1])
    703         return 1.0
    704 
    705 
    706 class CPUIdleStats(AbstractStats):
    707     """
    708     CPU Idle statistics (refresh() will not work with incremental=False!)
    709     """
    710     # TODO (snanda): Handle changes in number of c-states due to events such
    711     # as ac <-> battery transitions.
    712     # TODO (snanda): Handle non-S0 states. Time spent in suspend states is
    713     # currently not factored out.
    714     def __init__(self, start_cpu=-1, end_cpu=-1):
    715         cpuidle_path = '/sys/devices/system/cpu/cpu*/cpuidle'
    716         self._cpus = glob.glob(cpuidle_path)
    717         num_cpus = len(self._cpus)
    718         name = 'cpuidle'
    719         if (start_cpu >= 0 and end_cpu >= 0
    720                 and not (start_cpu == 0 and end_cpu == num_cpus - 1)):
    721             self._cpus = self._cpus[start_cpu : end_cpu]
    722             name = name + '_' + str(start_cpu) + '_' + str(end_cpu)
    723         super(CPUIdleStats, self).__init__(name=name)
    724 
    725 
    726     def _read_stats(self):
    727         cpuidle_stats = collections.defaultdict(int)
    728         epoch_usecs = int(time.time() * 1000 * 1000)
    729         for cpu in self._cpus:
    730             state_path = os.path.join(cpu, 'state*')
    731             states = glob.glob(state_path)
    732             cpuidle_stats['C0'] += epoch_usecs
    733 
    734             for state in states:
    735                 name = utils.read_one_line(os.path.join(state, 'name'))
    736                 latency = utils.read_one_line(os.path.join(state, 'latency'))
    737 
    738                 if not int(latency) and name == 'POLL':
    739                     # C0 state. Kernel stats aren't right, so calculate by
    740                     # subtracting all other states from total time (using epoch
    741                     # timer since we calculate differences in the end anyway).
    742                     # NOTE: Only x86 lists C0 under cpuidle, ARM does not.
    743                     continue
    744 
    745                 usecs = int(utils.read_one_line(os.path.join(state, 'time')))
    746                 cpuidle_stats['C0'] -= usecs
    747 
    748                 if name == '<null>':
    749                     # Kernel race condition that can happen while a new C-state
    750                     # gets added (e.g. AC->battery). Don't know the 'name' of
    751                     # the state yet, but its 'time' would be 0 anyway.
    752                     logging.warning('Read name: <null>, time: %d from %s'
    753                         % (usecs, state) + '... skipping.')
    754                     continue
    755 
    756                 cpuidle_stats[name] += usecs
    757 
    758         return cpuidle_stats
    759 
    760 
    761 class CPUPackageStats(AbstractStats):
    762     """
    763     Package C-state residency statistics for modern Intel CPUs.
    764     """
    765 
    766     ATOM         =              {'C2': 0x3F8, 'C4': 0x3F9, 'C6': 0x3FA}
    767     NEHALEM      =              {'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA}
    768     SANDY_BRIDGE = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA}
    769     HASWELL      = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA,
    770                                  'C8': 0x630, 'C9': 0x631,'C10': 0x632}
    771 
    772     def __init__(self):
    773         def _get_platform_states():
    774             """
    775             Helper to decide what set of microarchitecture-specific MSRs to use.
    776 
    777             Returns: dict that maps C-state name to MSR address, or None.
    778             """
    779             modalias = '/sys/devices/system/cpu/modalias'
    780             if not os.path.exists(modalias):
    781                 return None
    782 
    783             values = utils.read_one_line(modalias).split(':')
    784             # values[2]: vendor, values[4]: family, values[6]: model (CPUID)
    785             if values[2] != '0000' or values[4] != '0006':
    786                 return None
    787 
    788             return {
    789                 # model groups pulled from Intel manual, volume 3 chapter 35
    790                 '0027': self.ATOM,         # unreleased? (Next Generation Atom)
    791                 '001A': self.NEHALEM,      # Bloomfield, Nehalem-EP (i7/Xeon)
    792                 '001E': self.NEHALEM,      # Clarks-/Lynnfield, Jasper (i5/i7/X)
    793                 '001F': self.NEHALEM,      # unreleased? (abandoned?)
    794                 '0025': self.NEHALEM,      # Arran-/Clarksdale (i3/i5/i7/C/X)
    795                 '002C': self.NEHALEM,      # Gulftown, Westmere-EP (i7/Xeon)
    796                 '002E': self.NEHALEM,      # Nehalem-EX (Xeon)
    797                 '002F': self.NEHALEM,      # Westmere-EX (Xeon)
    798                 '002A': self.SANDY_BRIDGE, # SandyBridge (i3/i5/i7/C/X)
    799                 '002D': self.SANDY_BRIDGE, # SandyBridge-E (i7)
    800                 '003A': self.SANDY_BRIDGE, # IvyBridge (i3/i5/i7/X)
    801                 '003C': self.HASWELL,      # Haswell (Core/Xeon)
    802                 '003D': self.HASWELL,      # Broadwell (Core)
    803                 '003E': self.SANDY_BRIDGE, # IvyBridge (Xeon)
    804                 '003F': self.HASWELL,      # Haswell-E (Core/Xeon)
    805                 '004F': self.HASWELL,      # Broadwell (Xeon)
    806                 '0056': self.HASWELL,      # Broadwell (Xeon D)
    807                 }.get(values[6], None)
    808 
    809         self._platform_states = _get_platform_states()
    810         super(CPUPackageStats, self).__init__(name='cpupkg')
    811 
    812 
    813     def _read_stats(self):
    814         packages = set()
    815         template = '/sys/devices/system/cpu/cpu%s/topology/physical_package_id'
    816         if not self._platform_states:
    817             return {}
    818         stats = dict((state, 0) for state in self._platform_states)
    819         stats['C0_C1'] = 0
    820 
    821         for cpu in os.listdir('/dev/cpu'):
    822             if not os.path.exists(template % cpu):
    823                 continue
    824             package = utils.read_one_line(template % cpu)
    825             if package in packages:
    826                 continue
    827             packages.add(package)
    828 
    829             stats['C0_C1'] += utils.rdmsr(0x10, cpu) # TSC
    830             for (state, msr) in self._platform_states.iteritems():
    831                 ticks = utils.rdmsr(msr, cpu)
    832                 stats[state] += ticks
    833                 stats['C0_C1'] -= ticks
    834 
    835         return stats
    836 
    837 
    838 class DevFreqStats(AbstractStats):
    839     """
    840     Devfreq device frequency stats.
    841     """
    842 
    843     _DIR = '/sys/class/devfreq'
    844 
    845 
    846     def __init__(self, f):
    847         """Constructs DevFreqStats Object that track frequency stats
    848         for the path of the given Devfreq device.
    849 
    850         The frequencies for devfreq devices are listed in Hz.
    851 
    852         Args:
    853             path: the path to the devfreq device
    854 
    855         Example:
    856             /sys/class/devfreq/dmc
    857         """
    858         self._path = os.path.join(self._DIR, f)
    859         if not os.path.exists(self._path):
    860             raise error.TestError('DevFreqStats: devfreq device does not exist')
    861 
    862         fname = os.path.join(self._path, 'available_frequencies')
    863         af = utils.read_one_line(fname).strip()
    864         self._available_freqs = sorted(af.split(), key=int)
    865 
    866         super(DevFreqStats, self).__init__(name=f)
    867 
    868     def _read_stats(self):
    869         stats = dict((freq, 0) for freq in self._available_freqs)
    870         fname = os.path.join(self._path, 'trans_stat')
    871 
    872         with open(fname) as fd:
    873             # The lines that contain the time in each frequency start on the 3rd
    874             # line, so skip the first 2 lines. The last line contains the number
    875             # of transitions, so skip that line too.
    876             # The time in each frequency is at the end of the line.
    877             freq_pattern = re.compile(r'\d+(?=:)')
    878             for line in fd.readlines()[2:-1]:
    879                 freq = freq_pattern.search(line)
    880                 if freq and freq.group() in self._available_freqs:
    881                     stats[freq.group()] = int(line.strip().split()[-1])
    882 
    883         return stats
    884 
    885 
    886 class GPUFreqStats(AbstractStats):
    887     """GPU Frequency statistics class.
    888 
    889     TODO(tbroch): add stats for other GPUs
    890     """
    891 
    892     _MALI_DEV = '/sys/class/misc/mali0/device'
    893     _MALI_EVENTS = ['mali_dvfs:mali_dvfs_set_clock']
    894     _MALI_TRACE_CLK_RE = r'(\d+.\d+): mali_dvfs_set_clock: frequency=(\d+)\d{6}'
    895 
    896     _I915_ROOT = '/sys/kernel/debug/dri/0'
    897     _I915_EVENTS = ['i915:intel_gpu_freq_change']
    898     _I915_CLK = os.path.join(_I915_ROOT, 'i915_cur_delayinfo')
    899     _I915_TRACE_CLK_RE = r'(\d+.\d+): intel_gpu_freq_change: new_freq=(\d+)'
    900     _I915_CUR_FREQ_RE = r'CAGF:\s+(\d+)MHz'
    901     _I915_MIN_FREQ_RE = r'Lowest \(RPN\) frequency:\s+(\d+)MHz'
    902     _I915_MAX_FREQ_RE = r'Max non-overclocked \(RP0\) frequency:\s+(\d+)MHz'
    903     # TODO(dbasehore) parse this from debugfs if/when this value is added
    904     _I915_FREQ_STEP = 50
    905 
    906     _gpu_type = None
    907 
    908 
    909     def _get_mali_freqs(self):
    910         """Get mali clocks based on kernel version.
    911 
    912         For 3.8-3.18:
    913             # cat /sys/class/misc/mali0/device/clock
    914             100000000
    915             # cat /sys/class/misc/mali0/device/available_frequencies
    916             100000000
    917             160000000
    918             266000000
    919             350000000
    920             400000000
    921             450000000
    922             533000000
    923             533000000
    924 
    925         For 4.4+:
    926             Tracked in DevFreqStats
    927 
    928         Returns:
    929           cur_mhz: string of current GPU clock in mhz
    930         """
    931         cur_mhz = None
    932         fqs = []
    933 
    934         fname = os.path.join(self._MALI_DEV, 'clock')
    935         if os.path.exists(fname):
    936             cur_mhz = str(int(int(utils.read_one_line(fname).strip()) / 1e6))
    937             fname = os.path.join(self._MALI_DEV, 'available_frequencies')
    938             with open(fname) as fd:
    939                 for ln in fd.readlines():
    940                     freq = int(int(ln.strip()) / 1e6)
    941                     fqs.append(str(freq))
    942                 fqs.sort()
    943 
    944         self._freqs = fqs
    945         return cur_mhz
    946 
    947 
    948     def __init__(self, incremental=False):
    949 
    950 
    951         min_mhz = None
    952         max_mhz = None
    953         cur_mhz = None
    954         events = None
    955         self._freqs = []
    956         self._prev_sample = None
    957         self._trace = None
    958 
    959         if os.path.exists(self._MALI_DEV) and \
    960            not os.path.exists(os.path.join(self._MALI_DEV, "devfreq")):
    961             self._set_gpu_type('mali')
    962         elif os.path.exists(self._I915_CLK):
    963             self._set_gpu_type('i915')
    964         else:
    965             # We either don't know how to track GPU stats (yet) or the stats are
    966             # tracked in DevFreqStats.
    967             self._set_gpu_type(None)
    968 
    969         logging.debug("gpu_type is %s", self._gpu_type)
    970 
    971         if self._gpu_type is 'mali':
    972             events = self._MALI_EVENTS
    973             cur_mhz = self._get_mali_freqs()
    974             if self._freqs:
    975                 min_mhz = self._freqs[0]
    976                 max_mhz = self._freqs[-1]
    977 
    978         elif self._gpu_type is 'i915':
    979             events = self._I915_EVENTS
    980             with open(self._I915_CLK) as fd:
    981                 for ln in fd.readlines():
    982                     logging.debug("ln = %s", ln)
    983                     result = re.findall(self._I915_CUR_FREQ_RE, ln)
    984                     if result:
    985                         cur_mhz = result[0]
    986                         continue
    987                     result = re.findall(self._I915_MIN_FREQ_RE, ln)
    988                     if result:
    989                         min_mhz = result[0]
    990                         continue
    991                     result = re.findall(self._I915_MAX_FREQ_RE, ln)
    992                     if result:
    993                         max_mhz = result[0]
    994                         continue
    995                 if min_mhz and max_mhz:
    996                     for i in xrange(int(min_mhz), int(max_mhz) +
    997                                     self._I915_FREQ_STEP, self._I915_FREQ_STEP):
    998                         self._freqs.append(str(i))
    999 
   1000         logging.debug("cur_mhz = %s, min_mhz = %s, max_mhz = %s", cur_mhz,
   1001                       min_mhz, max_mhz)
   1002 
   1003         if cur_mhz and min_mhz and max_mhz:
   1004             self._trace = kernel_trace.KernelTrace(events=events)
   1005 
   1006         # Not all platforms or kernel versions support tracing.
   1007         if not self._trace or not self._trace.is_tracing():
   1008             logging.warning("GPU frequency tracing not enabled.")
   1009         else:
   1010             self._prev_sample = (cur_mhz, self._trace.uptime_secs())
   1011             logging.debug("Current GPU freq: %s", cur_mhz)
   1012             logging.debug("All GPU freqs: %s", self._freqs)
   1013 
   1014         super(GPUFreqStats, self).__init__(name='gpu', incremental=incremental)
   1015 
   1016 
   1017     @classmethod
   1018     def _set_gpu_type(cls, gpu_type):
   1019         cls._gpu_type = gpu_type
   1020 
   1021 
   1022     def _read_stats(self):
   1023         if self._gpu_type:
   1024             return getattr(self, "_%s_read_stats" % self._gpu_type)()
   1025         return {}
   1026 
   1027 
   1028     def _trace_read_stats(self, regexp):
   1029         """Read GPU stats from kernel trace outputs.
   1030 
   1031         Args:
   1032             regexp: regular expression to match trace output for frequency
   1033 
   1034         Returns:
   1035             Dict with key string in mhz and val float in seconds.
   1036         """
   1037         if not self._prev_sample:
   1038             return {}
   1039 
   1040         stats = dict((k, 0.0) for k in self._freqs)
   1041         results = self._trace.read(regexp=regexp)
   1042         for (tstamp_str, freq) in results:
   1043             tstamp = float(tstamp_str)
   1044 
   1045             # do not reparse lines in trace buffer
   1046             if tstamp <= self._prev_sample[1]:
   1047                 continue
   1048             delta = tstamp - self._prev_sample[1]
   1049             logging.debug("freq:%s tstamp:%f - %f delta:%f",
   1050                           self._prev_sample[0],
   1051                           tstamp, self._prev_sample[1],
   1052                           delta)
   1053             stats[self._prev_sample[0]] += delta
   1054             self._prev_sample = (freq, tstamp)
   1055 
   1056         # Do last record
   1057         delta = self._trace.uptime_secs() - self._prev_sample[1]
   1058         logging.debug("freq:%s tstamp:uptime - %f delta:%f",
   1059                       self._prev_sample[0],
   1060                       self._prev_sample[1], delta)
   1061         stats[self._prev_sample[0]] += delta
   1062 
   1063         logging.debug("GPU freq percents:%s", stats)
   1064         return stats
   1065 
   1066 
   1067     def _mali_read_stats(self):
   1068         """Read Mali GPU stats
   1069 
   1070         Frequencies are reported in Hz, so use a regex that drops the last 6
   1071         digits.
   1072 
   1073         Output in trace looks like this:
   1074 
   1075             kworker/u:24-5220  [000] .... 81060.329232: mali_dvfs_set_clock: frequency=400
   1076             kworker/u:24-5220  [000] .... 81061.830128: mali_dvfs_set_clock: frequency=350
   1077 
   1078         Returns:
   1079             Dict with frequency in mhz as key and float in seconds for time
   1080               spent at that frequency.
   1081         """
   1082         return self._trace_read_stats(self._MALI_TRACE_CLK_RE)
   1083 
   1084 
   1085     def _i915_read_stats(self):
   1086         """Read i915 GPU stats.
   1087 
   1088         Output looks like this (kernel >= 3.8):
   1089 
   1090           kworker/u:0-28247 [000] .... 259391.579610: intel_gpu_freq_change: new_freq=400
   1091           kworker/u:0-28247 [000] .... 259391.581797: intel_gpu_freq_change: new_freq=350
   1092 
   1093         Returns:
   1094             Dict with frequency in mhz as key and float in seconds for time
   1095               spent at that frequency.
   1096         """
   1097         return self._trace_read_stats(self._I915_TRACE_CLK_RE)
   1098 
   1099 
   1100 class USBSuspendStats(AbstractStats):
   1101     """
   1102     USB active/suspend statistics (over all devices)
   1103     """
   1104     # TODO (snanda): handle hot (un)plugging of USB devices
   1105     # TODO (snanda): handle duration counters wraparound
   1106 
   1107     def __init__(self):
   1108         usb_stats_path = '/sys/bus/usb/devices/*/power'
   1109         self._file_paths = glob.glob(usb_stats_path)
   1110         if not self._file_paths:
   1111             logging.debug('USB stats path not found')
   1112         super(USBSuspendStats, self).__init__(name='usb')
   1113 
   1114 
   1115     def _read_stats(self):
   1116         usb_stats = {'active': 0, 'suspended': 0}
   1117 
   1118         for path in self._file_paths:
   1119             active_duration_path = os.path.join(path, 'active_duration')
   1120             total_duration_path = os.path.join(path, 'connected_duration')
   1121 
   1122             if not os.path.exists(active_duration_path) or \
   1123                not os.path.exists(total_duration_path):
   1124                 logging.debug('duration paths do not exist for: %s', path)
   1125                 continue
   1126 
   1127             active = int(utils.read_file(active_duration_path))
   1128             total = int(utils.read_file(total_duration_path))
   1129             logging.debug('device %s active for %.2f%%',
   1130                           path, active * 100.0 / total)
   1131 
   1132             usb_stats['active'] += active
   1133             usb_stats['suspended'] += total - active
   1134 
   1135         return usb_stats
   1136 
   1137 
   1138 def get_cpu_sibling_groups():
   1139     """
   1140     Get CPU core groups in HMP systems.
   1141 
   1142     In systems with both small core and big core,
   1143     returns groups of small and big sibling groups.
   1144     """
   1145     siblings_paths = '/sys/devices/system/cpu/cpu*/topology/' + \
   1146                     'core_siblings_list'
   1147     sibling_groups = []
   1148     sibling_file_paths = glob.glob(siblings_paths)
   1149     if not len(sibling_file_paths) > 0:
   1150         return sibling_groups;
   1151     total_cpus = len(sibling_file_paths)
   1152     i = 0
   1153     sibling_list_pattern = re.compile('(\d+)-(\d+)')
   1154     while (i <  total_cpus):
   1155         siblings_data = utils.read_file(sibling_file_paths[i])
   1156         sibling_match = sibling_list_pattern.match(siblings_data)
   1157         sibling_start, sibling_end = (int(x) for x in sibling_match.groups())
   1158         sibling_groups.append((sibling_start, sibling_end))
   1159         i = sibling_end + 1
   1160     return sibling_groups
   1161 
   1162 
   1163 
   1164 class StatoMatic(object):
   1165     """Class to aggregate and monitor a bunch of power related statistics."""
   1166     def __init__(self):
   1167         self._start_uptime_secs = kernel_trace.KernelTrace.uptime_secs()
   1168         self._astats = [USBSuspendStats(),
   1169                         GPUFreqStats(incremental=False),
   1170                         CPUPackageStats()]
   1171         cpu_sibling_groups = get_cpu_sibling_groups()
   1172         if not len(cpu_sibling_groups):
   1173             self._astats.append(CPUFreqStats())
   1174             self._astats.append(CPUIdleStats())
   1175         for cpu_start, cpu_end in cpu_sibling_groups:
   1176             self._astats.append(CPUFreqStats(cpu_start, cpu_end))
   1177             self._astats.append(CPUIdleStats(cpu_start, cpu_end))
   1178         if os.path.isdir(DevFreqStats._DIR):
   1179             self._astats.extend([DevFreqStats(f) for f in \
   1180                                  os.listdir(DevFreqStats._DIR)])
   1181 
   1182         self._disk = DiskStateLogger()
   1183         self._disk.start()
   1184 
   1185 
   1186     def publish(self):
   1187         """Publishes results of various statistics gathered.
   1188 
   1189         Returns:
   1190             dict with
   1191               key = string 'percent_<name>_<key>_time'
   1192               value = float in percent
   1193         """
   1194         results = {}
   1195         tot_secs = kernel_trace.KernelTrace.uptime_secs() - \
   1196             self._start_uptime_secs
   1197         for stat_obj in self._astats:
   1198             percent_stats = stat_obj.refresh()
   1199             logging.debug("pstats = %s", percent_stats)
   1200             if stat_obj.name is 'gpu':
   1201                 # TODO(tbroch) remove this once GPU freq stats have proved
   1202                 # reliable
   1203                 stats_secs = sum(stat_obj._stats.itervalues())
   1204                 if stats_secs < (tot_secs * 0.9) or \
   1205                         stats_secs > (tot_secs * 1.1):
   1206                     logging.warning('%s stats dont look right.  Not publishing.',
   1207                                  stat_obj.name)
   1208                     continue
   1209             new_res = {}
   1210             AbstractStats.format_results_percent(new_res, stat_obj.name,
   1211                                                  percent_stats)
   1212             wavg = stat_obj.weighted_average()
   1213             if wavg:
   1214                 AbstractStats.format_results_wavg(new_res, stat_obj.name, wavg)
   1215 
   1216             results.update(new_res)
   1217 
   1218         new_res = {}
   1219         if self._disk.get_error():
   1220             new_res['disk_logging_error'] = str(self._disk.get_error())
   1221         else:
   1222             AbstractStats.format_results_percent(new_res, 'disk',
   1223                                                  self._disk.result())
   1224         results.update(new_res)
   1225 
   1226         return results
   1227 
   1228 
   1229 class PowerMeasurement(object):
   1230     """Class to measure power.
   1231 
   1232     Public attributes:
   1233         domain: String name of the power domain being measured.  Example is
   1234           'system' for total system power
   1235 
   1236     Public methods:
   1237         refresh: Performs any power/energy sampling and calculation and returns
   1238             power as float in watts.  This method MUST be implemented in
   1239             subclass.
   1240     """
   1241 
   1242     def __init__(self, domain):
   1243         """Constructor."""
   1244         self.domain = domain
   1245 
   1246 
   1247     def refresh(self):
   1248         """Performs any power/energy sampling and calculation.
   1249 
   1250         MUST be implemented in subclass
   1251 
   1252         Returns:
   1253             float, power in watts.
   1254         """
   1255         raise NotImplementedError("'refresh' method should be implemented in "
   1256                                   "subclass.")
   1257 
   1258 
   1259 def parse_power_supply_info():
   1260     """Parses power_supply_info command output.
   1261 
   1262     Command output from power_manager ( tools/power_supply_info.cc ) looks like
   1263     this:
   1264 
   1265         Device: Line Power
   1266           path:               /sys/class/power_supply/cros_ec-charger
   1267           ...
   1268         Device: Battery
   1269           path:               /sys/class/power_supply/sbs-9-000b
   1270           ...
   1271 
   1272     """
   1273     rv = collections.defaultdict(dict)
   1274     dev = None
   1275     for ln in utils.system_output('power_supply_info').splitlines():
   1276         logging.debug("psu: %s", ln)
   1277         result = re.findall(r'^Device:\s+(.*)', ln)
   1278         if result:
   1279             dev = result[0]
   1280             continue
   1281         result = re.findall(r'\s+(.+):\s+(.+)', ln)
   1282         if result and dev:
   1283             kname = re.findall(r'(.*)\s+\(\w+\)', result[0][0])
   1284             if kname:
   1285                 rv[dev][kname[0]] = result[0][1]
   1286             else:
   1287                 rv[dev][result[0][0]] = result[0][1]
   1288 
   1289     return rv
   1290 
   1291 
   1292 class SystemPower(PowerMeasurement):
   1293     """Class to measure system power.
   1294 
   1295     TODO(tbroch): This class provides a subset of functionality in BatteryStat
   1296     in hopes of minimizing power draw.  Investigate whether its really
   1297     significant and if not, deprecate.
   1298 
   1299     Private Attributes:
   1300       _voltage_file: path to retrieve voltage in uvolts
   1301       _current_file: path to retrieve current in uamps
   1302     """
   1303 
   1304     def __init__(self, battery_dir):
   1305         """Constructor.
   1306 
   1307         Args:
   1308             battery_dir: path to dir containing the files to probe and log.
   1309                 usually something like /sys/class/power_supply/BAT0/
   1310         """
   1311         super(SystemPower, self).__init__('system')
   1312         # Files to log voltage and current from
   1313         self._voltage_file = os.path.join(battery_dir, 'voltage_now')
   1314         self._current_file = os.path.join(battery_dir, 'current_now')
   1315 
   1316 
   1317     def refresh(self):
   1318         """refresh method.
   1319 
   1320         See superclass PowerMeasurement for details.
   1321         """
   1322         keyvals = parse_power_supply_info()
   1323         return float(keyvals['Battery']['energy rate'])
   1324 
   1325 
   1326 class MeasurementLogger(threading.Thread):
   1327     """A thread that logs measurement readings.
   1328 
   1329     Example code snippet:
   1330          mylogger = MeasurementLogger([Measurent1, Measurent2])
   1331          mylogger.run()
   1332          for testname in tests:
   1333              start_time = time.time()
   1334              #run the test method for testname
   1335              mlogger.checkpoint(testname, start_time)
   1336          keyvals = mylogger.calc()
   1337 
   1338     Public attributes:
   1339         seconds_period: float, probing interval in seconds.
   1340         readings: list of lists of floats of measurements.
   1341         times: list of floats of time (since Epoch) of when measurements
   1342             occurred.  len(time) == len(readings).
   1343         done: flag to stop the logger.
   1344         domains: list of  domain strings being measured
   1345 
   1346     Public methods:
   1347         run: launches the thread to gather measuremnts
   1348         calc: calculates
   1349         save_results:
   1350 
   1351     Private attributes:
   1352        _measurements: list of Measurement objects to be sampled.
   1353        _checkpoint_data: list of tuples.  Tuple contains:
   1354            tname: String of testname associated with this time interval
   1355            tstart: Float of time when subtest started
   1356            tend: Float of time when subtest ended
   1357        _results: list of results tuples.  Tuple contains:
   1358            prefix: String of subtest
   1359            mean: Float of mean  in watts
   1360            std: Float of standard deviation of measurements
   1361            tstart: Float of time when subtest started
   1362            tend: Float of time when subtest ended
   1363     """
   1364     def __init__(self, measurements, seconds_period=1.0):
   1365         """Initialize a logger.
   1366 
   1367         Args:
   1368             _measurements: list of Measurement objects to be sampled.
   1369             seconds_period: float, probing interval in seconds.  Default 1.0
   1370         """
   1371         threading.Thread.__init__(self)
   1372 
   1373         self.seconds_period = seconds_period
   1374 
   1375         self.readings = []
   1376         self.times = []
   1377         self._checkpoint_data = []
   1378 
   1379         self.domains = []
   1380         self._measurements = measurements
   1381         for meas in self._measurements:
   1382             self.domains.append(meas.domain)
   1383 
   1384         self.done = False
   1385 
   1386 
   1387     def run(self):
   1388         """Threads run method."""
   1389         while(not self.done):
   1390             readings = []
   1391             for meas in self._measurements:
   1392                 readings.append(meas.refresh())
   1393             # TODO (dbasehore): We probably need proper locking in this file
   1394             # since there have been race conditions with modifying and accessing
   1395             # data.
   1396             self.readings.append(readings)
   1397             self.times.append(time.time())
   1398             time.sleep(self.seconds_period)
   1399 
   1400 
   1401     def checkpoint(self, tname='', tstart=None, tend=None):
   1402         """Check point the times in seconds associated with test tname.
   1403 
   1404         Args:
   1405            tname: String of testname associated with this time interval
   1406            tstart: Float in seconds of when tname test started.  Should be based
   1407                 off time.time()
   1408            tend: Float in seconds of when tname test ended.  Should be based
   1409                 off time.time().  If None, then value computed in the method.
   1410         """
   1411         if not tstart and self.times:
   1412             tstart = self.times[0]
   1413         if not tend:
   1414             tend = time.time()
   1415         self._checkpoint_data.append((tname, tstart, tend))
   1416         logging.info('Finished test "%s" between timestamps [%s, %s]',
   1417                      tname, tstart, tend)
   1418 
   1419 
   1420     def calc(self, mtype=None):
   1421         """Calculate average measurement during each of the sub-tests.
   1422 
   1423         Method performs the following steps:
   1424             1. Signals the thread to stop running.
   1425             2. Calculates mean, max, min, count on the samples for each of the
   1426                measurements.
   1427             3. Stores results to be written later.
   1428             4. Creates keyvals for autotest publishing.
   1429 
   1430         Args:
   1431             mtype: string of measurement type.  For example:
   1432                    pwr == power
   1433                    temp == temperature
   1434 
   1435         Returns:
   1436             dict of keyvals suitable for autotest results.
   1437         """
   1438         if not mtype:
   1439             mtype = 'meas'
   1440 
   1441         t = numpy.array(self.times)
   1442         keyvals = {}
   1443         results  = []
   1444 
   1445         if not self.done:
   1446             self.done = True
   1447         # times 2 the sleep time in order to allow for readings as well.
   1448         self.join(timeout=self.seconds_period * 2)
   1449 
   1450         if not self._checkpoint_data:
   1451             self.checkpoint()
   1452 
   1453         for i, domain_readings in enumerate(zip(*self.readings)):
   1454             meas = numpy.array(domain_readings)
   1455             domain = self.domains[i]
   1456 
   1457             for tname, tstart, tend in self._checkpoint_data:
   1458                 if tname:
   1459                     prefix = '%s_%s' % (tname, domain)
   1460                 else:
   1461                     prefix = domain
   1462                 keyvals[prefix+'_duration'] = tend - tstart
   1463                 # Select all readings taken between tstart and tend timestamps.
   1464                 # Try block just in case
   1465                 # code.google.com/p/chromium/issues/detail?id=318892
   1466                 # is not fixed.
   1467                 try:
   1468                     meas_array = meas[numpy.bitwise_and(tstart < t, t < tend)]
   1469                 except ValueError, e:
   1470                     logging.debug('Error logging measurements: %s', str(e))
   1471                     logging.debug('timestamps %d %s' % (t.len, t))
   1472                     logging.debug('timestamp start, end %f %f' % (tstart, tend))
   1473                     logging.debug('measurements %d %s' % (meas.len, meas))
   1474 
   1475                 # If sub-test terminated early, avoid calculating avg, std and
   1476                 # min
   1477                 if not meas_array.size:
   1478                     continue
   1479                 meas_mean = meas_array.mean()
   1480                 meas_std = meas_array.std()
   1481 
   1482                 # Results list can be used for pretty printing and saving as csv
   1483                 results.append((prefix, meas_mean, meas_std,
   1484                                 tend - tstart, tstart, tend))
   1485 
   1486                 keyvals[prefix + '_' + mtype] = meas_mean
   1487                 keyvals[prefix + '_' + mtype + '_cnt'] = meas_array.size
   1488                 keyvals[prefix + '_' + mtype + '_max'] = meas_array.max()
   1489                 keyvals[prefix + '_' + mtype + '_min'] = meas_array.min()
   1490                 keyvals[prefix + '_' + mtype + '_std'] = meas_std
   1491 
   1492         self._results = results
   1493         return keyvals
   1494 
   1495 
   1496     def save_results(self, resultsdir, fname=None):
   1497         """Save computed results in a nice tab-separated format.
   1498         This is useful for long manual runs.
   1499 
   1500         Args:
   1501             resultsdir: String, directory to write results to
   1502             fname: String name of file to write results to
   1503         """
   1504         if not fname:
   1505             fname = 'meas_results_%.0f.txt' % time.time()
   1506         fname = os.path.join(resultsdir, fname)
   1507         with file(fname, 'wt') as f:
   1508             for row in self._results:
   1509                 # First column is name, the rest are numbers. See _calc_power()
   1510                 fmt_row = [row[0]] + ['%.2f' % x for x in row[1:]]
   1511                 line = '\t'.join(fmt_row)
   1512                 f.write(line + '\n')
   1513 
   1514 
   1515 class PowerLogger(MeasurementLogger):
   1516     def save_results(self, resultsdir, fname=None):
   1517         if not fname:
   1518             fname = 'power_results_%.0f.txt' % time.time()
   1519         super(PowerLogger, self).save_results(resultsdir, fname)
   1520 
   1521 
   1522     def calc(self, mtype='pwr'):
   1523         return super(PowerLogger, self).calc(mtype)
   1524 
   1525 
   1526 class TempMeasurement(object):
   1527     """Class to measure temperature.
   1528 
   1529     Public attributes:
   1530         domain: String name of the temperature domain being measured.  Example is
   1531           'cpu' for cpu temperature
   1532 
   1533     Private attributes:
   1534         _path: Path to temperature file to read ( in millidegrees Celsius )
   1535 
   1536     Public methods:
   1537         refresh: Performs any temperature sampling and calculation and returns
   1538             temperature as float in degrees Celsius.
   1539     """
   1540     def __init__(self, domain, path):
   1541         """Constructor."""
   1542         self.domain = domain
   1543         self._path = path
   1544 
   1545 
   1546     def refresh(self):
   1547         """Performs temperature
   1548 
   1549         Returns:
   1550             float, temperature in degrees Celsius
   1551         """
   1552         return int(utils.read_one_line(self._path)) / 1000.
   1553 
   1554 
   1555 class TempLogger(MeasurementLogger):
   1556     """A thread that logs temperature readings in millidegrees Celsius."""
   1557     def __init__(self, measurements, seconds_period=30.0):
   1558         if not measurements:
   1559             measurements = []
   1560             tstats = ThermalStatHwmon()
   1561             for kname in tstats.fields:
   1562                 match = re.match(r'(\S+)_temp(\d+)_input', kname)
   1563                 if not match:
   1564                     continue
   1565                 domain = match.group(1) + '-t' + match.group(2)
   1566                 fpath = tstats.fields[kname][0]
   1567                 new_meas = TempMeasurement(domain, fpath)
   1568                 measurements.append(new_meas)
   1569         super(TempLogger, self).__init__(measurements, seconds_period)
   1570 
   1571 
   1572     def save_results(self, resultsdir, fname=None):
   1573         if not fname:
   1574             fname = 'temp_results_%.0f.txt' % time.time()
   1575         super(TempLogger, self).save_results(resultsdir, fname)
   1576 
   1577 
   1578     def calc(self, mtype='temp'):
   1579         return super(TempLogger, self).calc(mtype)
   1580 
   1581 
   1582 class DiskStateLogger(threading.Thread):
   1583     """Records the time percentages the disk stays in its different power modes.
   1584 
   1585     Example code snippet:
   1586         mylogger = power_status.DiskStateLogger()
   1587         mylogger.start()
   1588         result = mylogger.result()
   1589 
   1590     Public methods:
   1591         start: Launches the thread and starts measurements.
   1592         result: Stops the thread if it's still running and returns measurements.
   1593         get_error: Returns the exception in _error if it exists.
   1594 
   1595     Private functions:
   1596         _get_disk_state: Returns the disk's current ATA power mode as a string.
   1597 
   1598     Private attributes:
   1599         _seconds_period: Disk polling interval in seconds.
   1600         _stats: Dict that maps disk states to seconds spent in them.
   1601         _running: Flag that is True as long as the logger should keep running.
   1602         _time: Timestamp of last disk state reading.
   1603         _device_path: The file system path of the disk's device node.
   1604         _error: Contains a TestError exception if an unexpected error occured
   1605     """
   1606     def __init__(self, seconds_period = 5.0, device_path = None):
   1607         """Initializes a logger.
   1608 
   1609         Args:
   1610             seconds_period: Disk polling interval in seconds. Default 5.0
   1611             device_path: The path of the disk's device node. Default '/dev/sda'
   1612         """
   1613         threading.Thread.__init__(self)
   1614         self._seconds_period = seconds_period
   1615         self._device_path = device_path
   1616         self._stats = {}
   1617         self._running = False
   1618         self._error = None
   1619 
   1620         result = utils.system_output('rootdev -s')
   1621         # TODO(tbroch) Won't work for emmc storage and will throw this error in
   1622         # keyvals : 'ioctl(SG_IO) error: [Errno 22] Invalid argument'
   1623         # Lets implement something complimentary for emmc
   1624         if not device_path:
   1625             self._device_path = \
   1626                 re.sub('(sd[a-z]|mmcblk[0-9]+)p?[0-9]+', '\\1', result)
   1627         logging.debug("device_path = %s", self._device_path)
   1628 
   1629 
   1630     def start(self):
   1631         logging.debug("inside DiskStateLogger.start")
   1632         if os.path.exists(self._device_path):
   1633             logging.debug("DiskStateLogger started")
   1634             super(DiskStateLogger, self).start()
   1635 
   1636 
   1637     def _get_disk_state(self):
   1638         """Checks the disk's power mode and returns it as a string.
   1639 
   1640         This uses the SG_IO ioctl to issue a raw SCSI command data block with
   1641         the ATA-PASS-THROUGH command that allows SCSI-to-ATA translation (see
   1642         T10 document 04-262r8). The ATA command issued is CHECKPOWERMODE1,
   1643         which returns the device's current power mode.
   1644         """
   1645 
   1646         def _addressof(obj):
   1647             """Shortcut to return the memory address of an object as integer."""
   1648             return ctypes.cast(obj, ctypes.c_void_p).value
   1649 
   1650         scsi_cdb = struct.pack("12B", # SCSI command data block (uint8[12])
   1651                                0xa1, # SCSI opcode: ATA-PASS-THROUGH
   1652                                3 << 1, # protocol: Non-data
   1653                                1 << 5, # flags: CK_COND
   1654                                0, # features
   1655                                0, # sector count
   1656                                0, 0, 0, # LBA
   1657                                1 << 6, # flags: ATA-USING-LBA
   1658                                0xe5, # ATA opcode: CHECKPOWERMODE1
   1659                                0, # reserved
   1660                                0, # control (no idea what this is...)
   1661                               )
   1662         scsi_sense = (ctypes.c_ubyte * 32)() # SCSI sense buffer (uint8[32])
   1663         sgio_header = struct.pack("iiBBHIPPPIIiPBBBBHHiII", # see <scsi/sg.h>
   1664                                   83, # Interface ID magic number (int32)
   1665                                   -1, # data transfer direction: none (int32)
   1666                                   12, # SCSI command data block length (uint8)
   1667                                   32, # SCSI sense data block length (uint8)
   1668                                   0, # iovec_count (not applicable?) (uint16)
   1669                                   0, # data transfer length (uint32)
   1670                                   0, # data block pointer
   1671                                   _addressof(scsi_cdb), # SCSI CDB pointer
   1672                                   _addressof(scsi_sense), # sense buffer pointer
   1673                                   500, # timeout in milliseconds (uint32)
   1674                                   0, # flags (uint32)
   1675                                   0, # pack ID (unused) (int32)
   1676                                   0, # user data pointer (unused)
   1677                                   0, 0, 0, 0, 0, 0, 0, 0, 0, # output params
   1678                                  )
   1679         try:
   1680             with open(self._device_path, 'r') as dev:
   1681                 result = fcntl.ioctl(dev, 0x2285, sgio_header)
   1682         except IOError, e:
   1683             raise error.TestError('ioctl(SG_IO) error: %s' % str(e))
   1684         _, _, _, _, status, host_status, driver_status = \
   1685             struct.unpack("4x4xxx2x4xPPP4x4x4xPBxxxHH4x4x4x", result)
   1686         if status != 0x2: # status: CHECK_CONDITION
   1687             raise error.TestError('SG_IO status: %d' % status)
   1688         if host_status != 0:
   1689             raise error.TestError('SG_IO host status: %d' % host_status)
   1690         if driver_status != 0x8: # driver status: SENSE
   1691             raise error.TestError('SG_IO driver status: %d' % driver_status)
   1692 
   1693         if scsi_sense[0] != 0x72: # resp. code: current error, descriptor format
   1694             raise error.TestError('SENSE response code: %d' % scsi_sense[0])
   1695         if scsi_sense[1] != 0: # sense key: No Sense
   1696             raise error.TestError('SENSE key: %d' % scsi_sense[1])
   1697         if scsi_sense[7] < 14: # additional length (ATA status is 14 - 1 bytes)
   1698             raise error.TestError('ADD. SENSE too short: %d' % scsi_sense[7])
   1699         if scsi_sense[8] != 0x9: # additional descriptor type: ATA Return Status
   1700             raise error.TestError('SENSE descriptor type: %d' % scsi_sense[8])
   1701         if scsi_sense[11] != 0: # errors: none
   1702             raise error.TestError('ATA error code: %d' % scsi_sense[11])
   1703 
   1704         if scsi_sense[13] == 0x00:
   1705             return 'standby'
   1706         if scsi_sense[13] == 0x80:
   1707             return 'idle'
   1708         if scsi_sense[13] == 0xff:
   1709             return 'active'
   1710         return 'unknown(%d)' % scsi_sense[13]
   1711 
   1712 
   1713     def run(self):
   1714         """The Thread's run method."""
   1715         try:
   1716             self._time = time.time()
   1717             self._running = True
   1718             while(self._running):
   1719                 time.sleep(self._seconds_period)
   1720                 state = self._get_disk_state()
   1721                 new_time = time.time()
   1722                 if state in self._stats:
   1723                     self._stats[state] += new_time - self._time
   1724                 else:
   1725                     self._stats[state] = new_time - self._time
   1726                 self._time = new_time
   1727         except error.TestError, e:
   1728             self._error = e
   1729             self._running = False
   1730 
   1731 
   1732     def result(self):
   1733         """Stop the logger and return dict with result percentages."""
   1734         if (self._running):
   1735             self._running = False
   1736             self.join(self._seconds_period * 2)
   1737         return AbstractStats.to_percent(self._stats)
   1738 
   1739 
   1740     def get_error(self):
   1741         """Returns the _error exception... please only call after result()."""
   1742         return self._error
   1743 
   1744 def parse_reef_s0ix_residency_info():
   1745     """
   1746     Parses the ioss_info file which contains the S0ix residency counter
   1747     on reef variants.
   1748     Example file :
   1749     --------------------------------------
   1750     I0SS TELEMETRY EVENTLOG
   1751     --------------------------------------
   1752     SOC_S0IX_TOTAL_RES               0xd241b68
   1753 
   1754     @returns Residency(secs) for Reef platform.
   1755     @raises TestError if the debugfs file for this
   1756         specific board is not found or if S0ix residency info is not
   1757         found in the debugfs file.
   1758     """
   1759 
   1760     ioss_info_path = '/sys/kernel/debug/telemetry/ioss_info'
   1761     S0IX_CLOCK_HZ = 19.2e6
   1762     if not os.path.exists(ioss_info_path):
   1763         raise error.TestNAError('File: ' + ioss_info_path + ' used to'
   1764                                 ' measure s0ix residency does not exist')
   1765 
   1766     with open(ioss_info_path) as fd:
   1767         residency = -1
   1768         for line in fd:
   1769             if line.startswith('SOC_S0IX_TOTAL_RES'):
   1770                 #residency here is a clock pulse with XTAL of 19.2mhz.
   1771                 residency = int(line.rsplit(None, 1)[-1], 0)
   1772                 logging.debug("S0ix Residency: %d", residency)
   1773             # Helps in debugging scenarios where the residency count has not increased.
   1774             elif 'BLOCK' in line:
   1775                 logging.debug(line)
   1776         if residency is not -1:
   1777             return residency / S0IX_CLOCK_HZ
   1778     raise error.TestNAError('Could not find s0ix residency in ' +
   1779                             ioss_info_path)
   1780 
   1781 
   1782 class S0ixResidencyStats(object):
   1783     """
   1784     Measures the S0ix residency of a given board over time. Since
   1785     the debugfs path and the format of the file with the information
   1786     about S0ix residency might differ for every platform, we have a platform
   1787     specific parser.
   1788     """
   1789     S0IX_PARSERS_PER_PLATFORM = {
   1790         'Google_Reef' : parse_reef_s0ix_residency_info,
   1791     }
   1792 
   1793     def __init__(self):
   1794         try:
   1795             current_plat = utils.run('mosys platform family',
   1796                                      verbose=False).stdout.strip()
   1797         except error.CmdError:
   1798             raise error.TestNAError('Could not find the platform family.')
   1799         if current_plat not in self.S0IX_PARSERS_PER_PLATFORM:
   1800             raise error.TestNAError('No Residency counter parser for' +
   1801                                     ' the board: ' + current_plat)
   1802         self._parse_function = \
   1803                 self.S0IX_PARSERS_PER_PLATFORM[current_plat]
   1804         self._initial_residency = self._parse_function()
   1805 
   1806     def get_accumulated_residency_secs(self):
   1807         """
   1808         @returns S0ix Residency since the class has been initialized.
   1809         """
   1810         return self._parse_function() - self._initial_residency
   1811