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, ctypes, fcntl, glob, logging, math, numpy, os, re, struct
6import threading, time
7
8from autotest_lib.client.bin import utils
9from autotest_lib.client.common_lib import error, enum
10from autotest_lib.client.cros import kernel_trace
11
12BatteryDataReportType = enum.Enum('CHARGE', 'ENERGY')
13
14# battery data reported at 1e6 scale
15BATTERY_DATA_SCALE = 1e6
16# number of times to retry reading the battery in the case of bad data
17BATTERY_RETRY_COUNT = 3
18
19class 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
59class 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
124class 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
173class 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
214class 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
344class 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
356class 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
381class 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            # TODO(shawnn): This is debug code. Need to remove it later.
477            if utils.get_board() == 'butterfly':
478                logging.debug('Butterfly on_ac, delay and re-check')
479                tries = 0
480                while self.on_ac():
481                    logging.debug('Butterfly {on_ac, pcc, tries}: %d %d %d' %
482                      (self.on_ac(), self.percent_current_charge(), tries))
483                    tries += 1
484                    if tries > 300:
485                        logging.debug('on_ac never deasserted')
486                        break
487                    time.sleep(5)
488                    self.refresh()
489
490            raise error.TestError(
491                'Running on AC power. Please remove AC power cable.')
492
493        percent_initial_charge = self.percent_current_charge()
494
495        if percent_initial_charge_min and percent_initial_charge < \
496                                          percent_initial_charge_min:
497            raise error.TestError('Initial charge (%f) less than min (%f)'
498                      % (percent_initial_charge, percent_initial_charge_min))
499
500
501def get_status():
502    """
503    Return a new power status object (SysStat). A new power status snapshot
504    for a given host can be obtained by either calling this routine again and
505    constructing a new SysStat object, or by using the refresh method of the
506    SysStat object.
507    """
508    status = SysStat()
509    status.refresh()
510    return status
511
512
513class AbstractStats(object):
514    """
515    Common superclass for measurements of percentages per state over time.
516
517    Public Attributes:
518        incremental:  If False, stats returned are from a single
519        _read_stats.  Otherwise, stats are from the difference between
520        the current and last refresh.
521    """
522
523    @staticmethod
524    def to_percent(stats):
525        """
526        Turns a dict with absolute time values into a dict with percentages.
527        """
528        total = sum(stats.itervalues())
529        if total == 0:
530            return {}
531        return dict((k, v * 100.0 / total) for (k, v) in stats.iteritems())
532
533
534    @staticmethod
535    def do_diff(new, old):
536        """
537        Returns a dict with value deltas from two dicts with matching keys.
538        """
539        return dict((k, new[k] - old.get(k, 0)) for k in new.iterkeys())
540
541
542    @staticmethod
543    def format_results_percent(results, name, percent_stats):
544        """
545        Formats autotest result keys to format:
546          percent_<name>_<key>_time
547        """
548        for key in percent_stats:
549            results['percent_%s_%s_time' % (name, key)] = percent_stats[key]
550
551
552    @staticmethod
553    def format_results_wavg(results, name, wavg):
554        """
555        Add an autotest result keys to format: wavg_<name>
556        """
557        if wavg is not None:
558            results['wavg_%s' % (name)] = wavg
559
560
561    def __init__(self, name=None, incremental=True):
562        if not name:
563            error.TestFail("Need to name AbstractStats instance please.")
564        self.name = name
565        self.incremental = incremental
566        self._stats = self._read_stats()
567
568
569    def refresh(self):
570        """
571        Returns dict mapping state names to percentage of time spent in them.
572        """
573        raw_stats = result = self._read_stats()
574        if self.incremental:
575            result = self.do_diff(result, self._stats)
576        self._stats = raw_stats
577        return self.to_percent(result)
578
579
580    def _automatic_weighted_average(self):
581        """
582        Turns a dict with absolute times (or percentages) into a weighted
583        average value.
584        """
585        total = sum(self._stats.itervalues())
586        if total == 0:
587            return None
588
589        return sum((float(k)*v) / total for (k, v) in self._stats.iteritems())
590
591
592    def _supports_automatic_weighted_average(self):
593        """
594        Override!
595
596        Returns True if stats collected can be automatically converted from
597        percent distribution to weighted average. False otherwise.
598        """
599        return False
600
601
602    def weighted_average(self):
603        """
604        Return weighted average calculated using the automated average method
605        (if supported) or using a custom method defined by the stat.
606        """
607        if self._supports_automatic_weighted_average():
608            return self._automatic_weighted_average()
609
610        return self._weighted_avg_fn()
611
612
613    def _weighted_avg_fn(self):
614        """
615        Override! Custom weighted average function.
616
617        Returns weighted average as a single floating point value.
618        """
619        return None
620
621
622    def _read_stats(self):
623        """
624        Override! Reads the raw data values that shall be measured into a dict.
625        """
626        raise NotImplementedError('Override _read_stats in the subclass!')
627
628
629class CPUFreqStats(AbstractStats):
630    """
631    CPU Frequency statistics
632    """
633
634    def __init__(self):
635        cpufreq_stats_path = '/sys/devices/system/cpu/cpu*/cpufreq/stats/' + \
636                             'time_in_state'
637        intel_pstate_stats_path = '/sys/devices/system/cpu/intel_pstate/' + \
638                             'aperf_mperf'
639        self._file_paths = glob.glob(cpufreq_stats_path)
640        self._intel_pstate_file_paths = glob.glob(intel_pstate_stats_path)
641        self._running_intel_pstate = False
642        self._initial_perf = None
643        self._current_perf = None
644        self._max_freq = 0
645
646        if not self._file_paths:
647            logging.debug('time_in_state file not found')
648            if self._intel_pstate_file_paths:
649                logging.debug('intel_pstate frequency stats file found')
650                self._running_intel_pstate = True
651
652        super(CPUFreqStats, self).__init__(name='cpufreq')
653
654
655    def _read_stats(self):
656        if self._running_intel_pstate:
657            aperf = 0
658            mperf = 0
659
660            for path in self._intel_pstate_file_paths:
661                if not os.path.exists(path):
662                    logging.debug('%s is not found', path)
663                    continue
664                data = utils.read_file(path)
665                for line in data.splitlines():
666                    pair = line.split()
667                    # max_freq is supposed to be the same for all CPUs
668                    # and remain constant throughout.
669                    # So, we set the entry only once
670                    if not self._max_freq:
671                        self._max_freq = int(pair[0])
672                    aperf += int(pair[1])
673                    mperf += int(pair[2])
674
675            if not self._initial_perf:
676                self._initial_perf = (aperf, mperf)
677
678            self._current_perf = (aperf, mperf)
679
680        stats = {}
681        for path in self._file_paths:
682            if not os.path.exists(path):
683                logging.debug('%s is not found', path)
684                continue
685
686            data = utils.read_file(path)
687            for line in data.splitlines():
688                pair = line.split()
689                freq = int(pair[0])
690                timeunits = int(pair[1])
691                if freq in stats:
692                    stats[freq] += timeunits
693                else:
694                    stats[freq] = timeunits
695        return stats
696
697
698    def _supports_automatic_weighted_average(self):
699        return not self._running_intel_pstate
700
701
702    def _weighted_avg_fn(self):
703        if not self._running_intel_pstate:
704            return None
705
706        if self._current_perf[1] != self._initial_perf[1]:
707            # Avg freq = max_freq * aperf_delta / mperf_delta
708            return self._max_freq * \
709                float(self._current_perf[0] - self._initial_perf[0]) / \
710                (self._current_perf[1] - self._initial_perf[1])
711        return 1.0
712
713
714class CPUIdleStats(AbstractStats):
715    """
716    CPU Idle statistics (refresh() will not work with incremental=False!)
717    """
718    # TODO (snanda): Handle changes in number of c-states due to events such
719    # as ac <-> battery transitions.
720    # TODO (snanda): Handle non-S0 states. Time spent in suspend states is
721    # currently not factored out.
722    def __init__(self):
723        super(CPUIdleStats, self).__init__(name='cpuidle')
724
725
726    def _read_stats(self):
727        cpuidle_stats = collections.defaultdict(int)
728        cpuidle_path = '/sys/devices/system/cpu/cpu*/cpuidle'
729        epoch_usecs = int(time.time() * 1000 * 1000)
730        cpus = glob.glob(cpuidle_path)
731
732        for cpu in cpus:
733            state_path = os.path.join(cpu, 'state*')
734            states = glob.glob(state_path)
735            cpuidle_stats['C0'] += epoch_usecs
736
737            for state in states:
738                name = utils.read_one_line(os.path.join(state, 'name'))
739                latency = utils.read_one_line(os.path.join(state, 'latency'))
740
741                if not int(latency) and name == 'POLL':
742                    # C0 state. Kernel stats aren't right, so calculate by
743                    # subtracting all other states from total time (using epoch
744                    # timer since we calculate differences in the end anyway).
745                    # NOTE: Only x86 lists C0 under cpuidle, ARM does not.
746                    continue
747
748                usecs = int(utils.read_one_line(os.path.join(state, 'time')))
749                cpuidle_stats['C0'] -= usecs
750
751                if name == '<null>':
752                    # Kernel race condition that can happen while a new C-state
753                    # gets added (e.g. AC->battery). Don't know the 'name' of
754                    # the state yet, but its 'time' would be 0 anyway.
755                    logging.warning('Read name: <null>, time: %d from %s'
756                        % (usecs, state) + '... skipping.')
757                    continue
758
759                cpuidle_stats[name] += usecs
760
761        return cpuidle_stats
762
763
764class CPUPackageStats(AbstractStats):
765    """
766    Package C-state residency statistics for modern Intel CPUs.
767    """
768
769    ATOM         =              {'C2': 0x3F8, 'C4': 0x3F9, 'C6': 0x3FA}
770    NEHALEM      =              {'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA}
771    SANDY_BRIDGE = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA}
772    HASWELL      = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA,
773                                 'C8': 0x630, 'C9': 0x631,'C10': 0x632}
774
775    def __init__(self):
776        def _get_platform_states():
777            """
778            Helper to decide what set of microarchitecture-specific MSRs to use.
779
780            Returns: dict that maps C-state name to MSR address, or None.
781            """
782            modalias = '/sys/devices/system/cpu/modalias'
783            if not os.path.exists(modalias):
784                return None
785
786            values = utils.read_one_line(modalias).split(':')
787            # values[2]: vendor, values[4]: family, values[6]: model (CPUID)
788            if values[2] != '0000' or values[4] != '0006':
789                return None
790
791            return {
792                # model groups pulled from Intel manual, volume 3 chapter 35
793                '0027': self.ATOM,         # unreleased? (Next Generation Atom)
794                '001A': self.NEHALEM,      # Bloomfield, Nehalem-EP (i7/Xeon)
795                '001E': self.NEHALEM,      # Clarks-/Lynnfield, Jasper (i5/i7/X)
796                '001F': self.NEHALEM,      # unreleased? (abandoned?)
797                '0025': self.NEHALEM,      # Arran-/Clarksdale (i3/i5/i7/C/X)
798                '002C': self.NEHALEM,      # Gulftown, Westmere-EP (i7/Xeon)
799                '002E': self.NEHALEM,      # Nehalem-EX (Xeon)
800                '002F': self.NEHALEM,      # Westmere-EX (Xeon)
801                '002A': self.SANDY_BRIDGE, # SandyBridge (i3/i5/i7/C/X)
802                '002D': self.SANDY_BRIDGE, # SandyBridge-E (i7)
803                '003A': self.SANDY_BRIDGE, # IvyBridge (i3/i5/i7/X)
804                '003C': self.HASWELL,      # Haswell (Core/Xeon)
805                '003D': self.HASWELL,      # Broadwell (Core)
806                '003E': self.SANDY_BRIDGE, # IvyBridge (Xeon)
807                '003F': self.HASWELL,      # Haswell-E (Core/Xeon)
808                '004F': self.HASWELL,      # Broadwell (Xeon)
809                '0056': self.HASWELL,      # Broadwell (Xeon D)
810                }.get(values[6], None)
811
812        self._platform_states = _get_platform_states()
813        super(CPUPackageStats, self).__init__(name='cpupkg')
814
815
816    def _read_stats(self):
817        packages = set()
818        template = '/sys/devices/system/cpu/cpu%s/topology/physical_package_id'
819        if not self._platform_states:
820            return {}
821        stats = dict((state, 0) for state in self._platform_states)
822        stats['C0_C1'] = 0
823
824        for cpu in os.listdir('/dev/cpu'):
825            if not os.path.exists(template % cpu):
826                continue
827            package = utils.read_one_line(template % cpu)
828            if package in packages:
829                continue
830            packages.add(package)
831
832            stats['C0_C1'] += utils.rdmsr(0x10, cpu) # TSC
833            for (state, msr) in self._platform_states.iteritems():
834                ticks = utils.rdmsr(msr, cpu)
835                stats[state] += ticks
836                stats['C0_C1'] -= ticks
837
838        return stats
839
840
841class GPUFreqStats(AbstractStats):
842    """GPU Frequency statistics class.
843
844    TODO(tbroch): add stats for other GPUs
845    """
846
847    _MALI_DEV = '/sys/class/misc/mali0/device'
848    _MALI_EVENTS = ['mali_dvfs:mali_dvfs_set_clock']
849    _MALI_34_TRACE_CLK_RE = r'(\d+.\d+): mali_dvfs_set_clock: frequency=(\d+)'
850    _MALI_TRACE_CLK_RE = r'(\d+.\d+): mali_dvfs_set_clock: frequency=(\d+)\d{6}'
851
852    _I915_ROOT = '/sys/kernel/debug/dri/0'
853    _I915_EVENTS = ['i915:intel_gpu_freq_change']
854    _I915_CLK = os.path.join(_I915_ROOT, 'i915_cur_delayinfo')
855    _I915_TRACE_CLK_RE = r'(\d+.\d+): intel_gpu_freq_change: new_freq=(\d+)'
856    _I915_CUR_FREQ_RE = r'CAGF:\s+(\d+)MHz'
857    _I915_MIN_FREQ_RE = r'Lowest \(RPN\) frequency:\s+(\d+)MHz'
858    _I915_MAX_FREQ_RE = r'Max non-overclocked \(RP0\) frequency:\s+(\d+)MHz'
859    # TODO(dbasehore) parse this from debugfs if/when this value is added
860    _I915_FREQ_STEP = 50
861
862    _gpu_type = None
863
864
865    def _get_mali_freqs(self):
866        """Get mali clocks based on kernel version.
867
868        For 3.4:
869            # cat /sys/class/misc/mali0/device/clock
870            Current sclk_g3d[G3D_BLK] = 100Mhz
871            Possible settings : 533, 450, 400, 350, 266, 160, 100Mhz
872
873        For 3.8 (and beyond):
874            # cat /sys/class/misc/mali0/device/clock
875            100000000
876            # cat /sys/class/misc/mali0/device/available_frequencies
877            100000000
878            160000000
879            266000000
880            350000000
881            400000000
882            450000000
883            533000000
884            533000000
885
886        Returns:
887          cur_mhz: string of current GPU clock in mhz
888        """
889        cur_mhz = None
890        fqs = []
891
892        fname = os.path.join(self._MALI_DEV, 'clock')
893        if os.uname()[2].startswith('3.4'):
894            with open(fname) as fd:
895                for ln in fd.readlines():
896                    result = re.findall(r'Current.* = (\d+)Mhz', ln)
897                    if result:
898                        cur_mhz = result[0]
899                        continue
900                    result = re.findall(r'(\d+)[,M]', ln)
901                    if result:
902                        fqs = result
903                        fd.close()
904        else:
905            cur_mhz = str(int(int(utils.read_one_line(fname).strip()) / 1e6))
906            fname = os.path.join(self._MALI_DEV, 'available_frequencies')
907            with open(fname) as fd:
908                for ln in fd.readlines():
909                    freq = int(int(ln.strip()) / 1e6)
910                    fqs.append(str(freq))
911                fqs.sort()
912
913        self._freqs = fqs
914        return cur_mhz
915
916
917    def __init__(self, incremental=False):
918
919
920        min_mhz = None
921        max_mhz = None
922        cur_mhz = None
923        events = None
924        self._freqs = []
925        self._prev_sample = None
926        self._trace = None
927
928        if os.path.exists(self._MALI_DEV):
929            self._set_gpu_type('mali')
930        elif os.path.exists(self._I915_CLK):
931            self._set_gpu_type('i915')
932
933        logging.debug("gpu_type is %s", self._gpu_type)
934
935        if self._gpu_type is 'mali':
936            events = self._MALI_EVENTS
937            cur_mhz = self._get_mali_freqs()
938            if self._freqs:
939                min_mhz = self._freqs[0]
940                max_mhz = self._freqs[-1]
941
942        elif self._gpu_type is 'i915':
943            events = self._I915_EVENTS
944            with open(self._I915_CLK) as fd:
945                for ln in fd.readlines():
946                    logging.debug("ln = %s", ln)
947                    result = re.findall(self._I915_CUR_FREQ_RE, ln)
948                    if result:
949                        cur_mhz = result[0]
950                        continue
951                    result = re.findall(self._I915_MIN_FREQ_RE, ln)
952                    if result:
953                        min_mhz = result[0]
954                        continue
955                    result = re.findall(self._I915_MAX_FREQ_RE, ln)
956                    if result:
957                        max_mhz = result[0]
958                        continue
959                if min_mhz and max_mhz:
960                    for i in xrange(int(min_mhz), int(max_mhz) +
961                                    self._I915_FREQ_STEP, self._I915_FREQ_STEP):
962                        self._freqs.append(str(i))
963
964        logging.debug("cur_mhz = %s, min_mhz = %s, max_mhz = %s", cur_mhz,
965                      min_mhz, max_mhz)
966
967        if cur_mhz and min_mhz and max_mhz:
968            self._trace = kernel_trace.KernelTrace(events=events)
969
970        # Not all platforms or kernel versions support tracing.
971        if not self._trace or not self._trace.is_tracing():
972            logging.warning("GPU frequency tracing not enabled.")
973        else:
974            self._prev_sample = (cur_mhz, self._trace.uptime_secs())
975            logging.debug("Current GPU freq: %s", cur_mhz)
976            logging.debug("All GPU freqs: %s", self._freqs)
977
978        super(GPUFreqStats, self).__init__(name='gpu', incremental=incremental)
979
980
981    @classmethod
982    def _set_gpu_type(cls, gpu_type):
983        cls._gpu_type = gpu_type
984
985
986    def _read_stats(self):
987        if self._gpu_type:
988            return getattr(self, "_%s_read_stats" % self._gpu_type)()
989        return {}
990
991
992    def _trace_read_stats(self, regexp):
993        """Read GPU stats from kernel trace outputs.
994
995        Args:
996            regexp: regular expression to match trace output for frequency
997
998        Returns:
999            Dict with key string in mhz and val float in seconds.
1000        """
1001        if not self._prev_sample:
1002            return {}
1003
1004        stats = dict((k, 0.0) for k in self._freqs)
1005        results = self._trace.read(regexp=regexp)
1006        for (tstamp_str, freq) in results:
1007            tstamp = float(tstamp_str)
1008
1009            # do not reparse lines in trace buffer
1010            if tstamp <= self._prev_sample[1]:
1011                continue
1012            delta = tstamp - self._prev_sample[1]
1013            logging.debug("freq:%s tstamp:%f - %f delta:%f",
1014                          self._prev_sample[0],
1015                          tstamp, self._prev_sample[1],
1016                          delta)
1017            stats[self._prev_sample[0]] += delta
1018            self._prev_sample = (freq, tstamp)
1019
1020        # Do last record
1021        delta = self._trace.uptime_secs() - self._prev_sample[1]
1022        logging.debug("freq:%s tstamp:uptime - %f delta:%f",
1023                      self._prev_sample[0],
1024                      self._prev_sample[1], delta)
1025        stats[self._prev_sample[0]] += delta
1026
1027        logging.debug("GPU freq percents:%s", stats)
1028        return stats
1029
1030
1031    def _mali_read_stats(self):
1032        """Read Mali GPU stats
1033
1034        For 3.4:
1035            Frequencies are reported in MHz.
1036
1037        For 3.8+:
1038            Frequencies are reported in Hz, so use a regex that drops the last 6
1039            digits.
1040
1041        Output in trace looks like this:
1042
1043            kworker/u:24-5220  [000] .... 81060.329232: mali_dvfs_set_clock: frequency=400
1044            kworker/u:24-5220  [000] .... 81061.830128: mali_dvfs_set_clock: frequency=350
1045
1046        Returns:
1047            Dict with frequency in mhz as key and float in seconds for time
1048              spent at that frequency.
1049        """
1050        regexp = None
1051        if os.uname()[2].startswith('3.4'):
1052            regexp = self._MALI_34_TRACE_CLK_RE
1053        else:
1054            regexp = self._MALI_TRACE_CLK_RE
1055
1056        return self._trace_read_stats(regexp)
1057
1058
1059    def _i915_read_stats(self):
1060        """Read i915 GPU stats.
1061
1062        Output looks like this (kernel >= 3.8):
1063
1064          kworker/u:0-28247 [000] .... 259391.579610: intel_gpu_freq_change: new_freq=400
1065          kworker/u:0-28247 [000] .... 259391.581797: intel_gpu_freq_change: new_freq=350
1066
1067        Returns:
1068            Dict with frequency in mhz as key and float in seconds for time
1069              spent at that frequency.
1070        """
1071        return self._trace_read_stats(self._I915_TRACE_CLK_RE)
1072
1073
1074class USBSuspendStats(AbstractStats):
1075    """
1076    USB active/suspend statistics (over all devices)
1077    """
1078    # TODO (snanda): handle hot (un)plugging of USB devices
1079    # TODO (snanda): handle duration counters wraparound
1080
1081    def __init__(self):
1082        usb_stats_path = '/sys/bus/usb/devices/*/power'
1083        self._file_paths = glob.glob(usb_stats_path)
1084        if not self._file_paths:
1085            logging.debug('USB stats path not found')
1086        super(USBSuspendStats, self).__init__(name='usb')
1087
1088
1089    def _read_stats(self):
1090        usb_stats = {'active': 0, 'suspended': 0}
1091
1092        for path in self._file_paths:
1093            active_duration_path = os.path.join(path, 'active_duration')
1094            total_duration_path = os.path.join(path, 'connected_duration')
1095
1096            if not os.path.exists(active_duration_path) or \
1097               not os.path.exists(total_duration_path):
1098                logging.debug('duration paths do not exist for: %s', path)
1099                continue
1100
1101            active = int(utils.read_file(active_duration_path))
1102            total = int(utils.read_file(total_duration_path))
1103            logging.debug('device %s active for %.2f%%',
1104                          path, active * 100.0 / total)
1105
1106            usb_stats['active'] += active
1107            usb_stats['suspended'] += total - active
1108
1109        return usb_stats
1110
1111
1112class StatoMatic(object):
1113    """Class to aggregate and monitor a bunch of power related statistics."""
1114    def __init__(self):
1115        self._start_uptime_secs = kernel_trace.KernelTrace.uptime_secs()
1116        self._astats = [USBSuspendStats(),
1117                        CPUFreqStats(),
1118                        GPUFreqStats(incremental=False),
1119                        CPUIdleStats(),
1120                        CPUPackageStats()]
1121        self._disk = DiskStateLogger()
1122        self._disk.start()
1123
1124
1125    def publish(self):
1126        """Publishes results of various statistics gathered.
1127
1128        Returns:
1129            dict with
1130              key = string 'percent_<name>_<key>_time'
1131              value = float in percent
1132        """
1133        results = {}
1134        tot_secs = kernel_trace.KernelTrace.uptime_secs() - \
1135            self._start_uptime_secs
1136        for stat_obj in self._astats:
1137            percent_stats = stat_obj.refresh()
1138            logging.debug("pstats = %s", percent_stats)
1139            if stat_obj.name is 'gpu':
1140                # TODO(tbroch) remove this once GPU freq stats have proved
1141                # reliable
1142                stats_secs = sum(stat_obj._stats.itervalues())
1143                if stats_secs < (tot_secs * 0.9) or \
1144                        stats_secs > (tot_secs * 1.1):
1145                    logging.warning('%s stats dont look right.  Not publishing.',
1146                                 stat_obj.name)
1147                    continue
1148            new_res = {}
1149            AbstractStats.format_results_percent(new_res, stat_obj.name,
1150                                                 percent_stats)
1151            wavg = stat_obj.weighted_average()
1152            if wavg:
1153                AbstractStats.format_results_wavg(new_res, stat_obj.name, wavg)
1154
1155            results.update(new_res)
1156
1157        new_res = {}
1158        if self._disk.get_error():
1159            new_res['disk_logging_error'] = str(self._disk.get_error())
1160        else:
1161            AbstractStats.format_results_percent(new_res, 'disk',
1162                                                 self._disk.result())
1163        results.update(new_res)
1164
1165        return results
1166
1167
1168class PowerMeasurement(object):
1169    """Class to measure power.
1170
1171    Public attributes:
1172        domain: String name of the power domain being measured.  Example is
1173          'system' for total system power
1174
1175    Public methods:
1176        refresh: Performs any power/energy sampling and calculation and returns
1177            power as float in watts.  This method MUST be implemented in
1178            subclass.
1179    """
1180
1181    def __init__(self, domain):
1182        """Constructor."""
1183        self.domain = domain
1184
1185
1186    def refresh(self):
1187        """Performs any power/energy sampling and calculation.
1188
1189        MUST be implemented in subclass
1190
1191        Returns:
1192            float, power in watts.
1193        """
1194        raise NotImplementedError("'refresh' method should be implemented in "
1195                                  "subclass.")
1196
1197
1198def parse_power_supply_info():
1199    """Parses power_supply_info command output.
1200
1201    Command output from power_manager ( tools/power_supply_info.cc ) looks like
1202    this:
1203
1204        Device: Line Power
1205          path:               /sys/class/power_supply/cros_ec-charger
1206          ...
1207        Device: Battery
1208          path:               /sys/class/power_supply/sbs-9-000b
1209          ...
1210
1211    """
1212    rv = collections.defaultdict(dict)
1213    dev = None
1214    for ln in utils.system_output('power_supply_info').splitlines():
1215        logging.debug("psu: %s", ln)
1216        result = re.findall(r'^Device:\s+(.*)', ln)
1217        if result:
1218            dev = result[0]
1219            continue
1220        result = re.findall(r'\s+(.+):\s+(.+)', ln)
1221        if result and dev:
1222            kname = re.findall(r'(.*)\s+\(\w+\)', result[0][0])
1223            if kname:
1224                rv[dev][kname[0]] = result[0][1]
1225            else:
1226                rv[dev][result[0][0]] = result[0][1]
1227
1228    return rv
1229
1230
1231class SystemPower(PowerMeasurement):
1232    """Class to measure system power.
1233
1234    TODO(tbroch): This class provides a subset of functionality in BatteryStat
1235    in hopes of minimizing power draw.  Investigate whether its really
1236    significant and if not, deprecate.
1237
1238    Private Attributes:
1239      _voltage_file: path to retrieve voltage in uvolts
1240      _current_file: path to retrieve current in uamps
1241    """
1242
1243    def __init__(self, battery_dir):
1244        """Constructor.
1245
1246        Args:
1247            battery_dir: path to dir containing the files to probe and log.
1248                usually something like /sys/class/power_supply/BAT0/
1249        """
1250        super(SystemPower, self).__init__('system')
1251        # Files to log voltage and current from
1252        self._voltage_file = os.path.join(battery_dir, 'voltage_now')
1253        self._current_file = os.path.join(battery_dir, 'current_now')
1254
1255
1256    def refresh(self):
1257        """refresh method.
1258
1259        See superclass PowerMeasurement for details.
1260        """
1261        keyvals = parse_power_supply_info()
1262        return float(keyvals['Battery']['energy rate'])
1263
1264
1265class MeasurementLogger(threading.Thread):
1266    """A thread that logs measurement readings.
1267
1268    Example code snippet:
1269         mylogger = MeasurementLogger([Measurent1, Measurent2])
1270         mylogger.run()
1271         for testname in tests:
1272             start_time = time.time()
1273             #run the test method for testname
1274             mlogger.checkpoint(testname, start_time)
1275         keyvals = mylogger.calc()
1276
1277    Public attributes:
1278        seconds_period: float, probing interval in seconds.
1279        readings: list of lists of floats of measurements.
1280        times: list of floats of time (since Epoch) of when measurements
1281            occurred.  len(time) == len(readings).
1282        done: flag to stop the logger.
1283        domains: list of  domain strings being measured
1284
1285    Public methods:
1286        run: launches the thread to gather measuremnts
1287        calc: calculates
1288        save_results:
1289
1290    Private attributes:
1291       _measurements: list of Measurement objects to be sampled.
1292       _checkpoint_data: list of tuples.  Tuple contains:
1293           tname: String of testname associated with this time interval
1294           tstart: Float of time when subtest started
1295           tend: Float of time when subtest ended
1296       _results: list of results tuples.  Tuple contains:
1297           prefix: String of subtest
1298           mean: Float of mean  in watts
1299           std: Float of standard deviation of measurements
1300           tstart: Float of time when subtest started
1301           tend: Float of time when subtest ended
1302    """
1303    def __init__(self, measurements, seconds_period=1.0):
1304        """Initialize a logger.
1305
1306        Args:
1307            _measurements: list of Measurement objects to be sampled.
1308            seconds_period: float, probing interval in seconds.  Default 1.0
1309        """
1310        threading.Thread.__init__(self)
1311
1312        self.seconds_period = seconds_period
1313
1314        self.readings = []
1315        self.times = []
1316        self._checkpoint_data = []
1317
1318        self.domains = []
1319        self._measurements = measurements
1320        for meas in self._measurements:
1321            self.domains.append(meas.domain)
1322
1323        self.done = False
1324
1325
1326    def run(self):
1327        """Threads run method."""
1328        while(not self.done):
1329            readings = []
1330            for meas in self._measurements:
1331                readings.append(meas.refresh())
1332            # TODO (dbasehore): We probably need proper locking in this file
1333            # since there have been race conditions with modifying and accessing
1334            # data.
1335            self.readings.append(readings)
1336            self.times.append(time.time())
1337            time.sleep(self.seconds_period)
1338
1339
1340    def checkpoint(self, tname='', tstart=None, tend=None):
1341        """Check point the times in seconds associated with test tname.
1342
1343        Args:
1344           tname: String of testname associated with this time interval
1345           tstart: Float in seconds of when tname test started.  Should be based
1346                off time.time()
1347           tend: Float in seconds of when tname test ended.  Should be based
1348                off time.time().  If None, then value computed in the method.
1349        """
1350        if not tstart and self.times:
1351            tstart = self.times[0]
1352        if not tend:
1353            tend = time.time()
1354        self._checkpoint_data.append((tname, tstart, tend))
1355        logging.info('Finished test "%s" between timestamps [%s, %s]',
1356                     tname, tstart, tend)
1357
1358
1359    def calc(self, mtype=None):
1360        """Calculate average measurement during each of the sub-tests.
1361
1362        Method performs the following steps:
1363            1. Signals the thread to stop running.
1364            2. Calculates mean, max, min, count on the samples for each of the
1365               measurements.
1366            3. Stores results to be written later.
1367            4. Creates keyvals for autotest publishing.
1368
1369        Args:
1370            mtype: string of measurement type.  For example:
1371                   pwr == power
1372                   temp == temperature
1373
1374        Returns:
1375            dict of keyvals suitable for autotest results.
1376        """
1377        if not mtype:
1378            mtype = 'meas'
1379
1380        t = numpy.array(self.times)
1381        keyvals = {}
1382        results  = []
1383
1384        if not self.done:
1385            self.done = True
1386        # times 2 the sleep time in order to allow for readings as well.
1387        self.join(timeout=self.seconds_period * 2)
1388
1389        if not self._checkpoint_data:
1390            self.checkpoint()
1391
1392        for i, domain_readings in enumerate(zip(*self.readings)):
1393            meas = numpy.array(domain_readings)
1394            domain = self.domains[i]
1395
1396            for tname, tstart, tend in self._checkpoint_data:
1397                if tname:
1398                    prefix = '%s_%s' % (tname, domain)
1399                else:
1400                    prefix = domain
1401                keyvals[prefix+'_duration'] = tend - tstart
1402                # Select all readings taken between tstart and tend timestamps.
1403                # Try block just in case
1404                # code.google.com/p/chromium/issues/detail?id=318892
1405                # is not fixed.
1406                try:
1407                    meas_array = meas[numpy.bitwise_and(tstart < t, t < tend)]
1408                except ValueError, e:
1409                    logging.debug('Error logging measurements: %s', str(e))
1410                    logging.debug('timestamps %d %s' % (t.len, t))
1411                    logging.debug('timestamp start, end %f %f' % (tstart, tend))
1412                    logging.debug('measurements %d %s' % (meas.len, meas))
1413
1414                # If sub-test terminated early, avoid calculating avg, std and
1415                # min
1416                if not meas_array.size:
1417                    continue
1418                meas_mean = meas_array.mean()
1419                meas_std = meas_array.std()
1420
1421                # Results list can be used for pretty printing and saving as csv
1422                results.append((prefix, meas_mean, meas_std,
1423                                tend - tstart, tstart, tend))
1424
1425                keyvals[prefix + '_' + mtype] = meas_mean
1426                keyvals[prefix + '_' + mtype + '_cnt'] = meas_array.size
1427                keyvals[prefix + '_' + mtype + '_max'] = meas_array.max()
1428                keyvals[prefix + '_' + mtype + '_min'] = meas_array.min()
1429                keyvals[prefix + '_' + mtype + '_std'] = meas_std
1430
1431        self._results = results
1432        return keyvals
1433
1434
1435    def save_results(self, resultsdir, fname=None):
1436        """Save computed results in a nice tab-separated format.
1437        This is useful for long manual runs.
1438
1439        Args:
1440            resultsdir: String, directory to write results to
1441            fname: String name of file to write results to
1442        """
1443        if not fname:
1444            fname = 'meas_results_%.0f.txt' % time.time()
1445        fname = os.path.join(resultsdir, fname)
1446        with file(fname, 'wt') as f:
1447            for row in self._results:
1448                # First column is name, the rest are numbers. See _calc_power()
1449                fmt_row = [row[0]] + ['%.2f' % x for x in row[1:]]
1450                line = '\t'.join(fmt_row)
1451                f.write(line + '\n')
1452
1453
1454class PowerLogger(MeasurementLogger):
1455    def save_results(self, resultsdir, fname=None):
1456        if not fname:
1457            fname = 'power_results_%.0f.txt' % time.time()
1458        super(PowerLogger, self).save_results(resultsdir, fname)
1459
1460
1461    def calc(self, mtype='pwr'):
1462        return super(PowerLogger, self).calc(mtype)
1463
1464
1465class TempMeasurement(object):
1466    """Class to measure temperature.
1467
1468    Public attributes:
1469        domain: String name of the temperature domain being measured.  Example is
1470          'cpu' for cpu temperature
1471
1472    Private attributes:
1473        _path: Path to temperature file to read ( in millidegrees Celsius )
1474
1475    Public methods:
1476        refresh: Performs any temperature sampling and calculation and returns
1477            temperature as float in degrees Celsius.
1478    """
1479    def __init__(self, domain, path):
1480        """Constructor."""
1481        self.domain = domain
1482        self._path = path
1483
1484
1485    def refresh(self):
1486        """Performs temperature
1487
1488        Returns:
1489            float, temperature in degrees Celsius
1490        """
1491        return int(utils.read_one_line(self._path)) / 1000.
1492
1493
1494class TempLogger(MeasurementLogger):
1495    """A thread that logs temperature readings in millidegrees Celsius."""
1496    def __init__(self, measurements, seconds_period=30.0):
1497        if not measurements:
1498            measurements = []
1499            tstats = ThermalStatHwmon()
1500            for kname in tstats.fields:
1501                match = re.match(r'(\S+)_temp(\d+)_input', kname)
1502                if not match:
1503                    continue
1504                domain = match.group(1) + '-t' + match.group(2)
1505                fpath = tstats.fields[kname][0]
1506                new_meas = TempMeasurement(domain, fpath)
1507                measurements.append(new_meas)
1508        super(TempLogger, self).__init__(measurements, seconds_period)
1509
1510
1511    def save_results(self, resultsdir, fname=None):
1512        if not fname:
1513            fname = 'temp_results_%.0f.txt' % time.time()
1514        super(TempLogger, self).save_results(resultsdir, fname)
1515
1516
1517    def calc(self, mtype='temp'):
1518        return super(TempLogger, self).calc(mtype)
1519
1520
1521class DiskStateLogger(threading.Thread):
1522    """Records the time percentages the disk stays in its different power modes.
1523
1524    Example code snippet:
1525        mylogger = power_status.DiskStateLogger()
1526        mylogger.start()
1527        result = mylogger.result()
1528
1529    Public methods:
1530        start: Launches the thread and starts measurements.
1531        result: Stops the thread if it's still running and returns measurements.
1532        get_error: Returns the exception in _error if it exists.
1533
1534    Private functions:
1535        _get_disk_state: Returns the disk's current ATA power mode as a string.
1536
1537    Private attributes:
1538        _seconds_period: Disk polling interval in seconds.
1539        _stats: Dict that maps disk states to seconds spent in them.
1540        _running: Flag that is True as long as the logger should keep running.
1541        _time: Timestamp of last disk state reading.
1542        _device_path: The file system path of the disk's device node.
1543        _error: Contains a TestError exception if an unexpected error occured
1544    """
1545    def __init__(self, seconds_period = 5.0, device_path = None):
1546        """Initializes a logger.
1547
1548        Args:
1549            seconds_period: Disk polling interval in seconds. Default 5.0
1550            device_path: The path of the disk's device node. Default '/dev/sda'
1551        """
1552        threading.Thread.__init__(self)
1553        self._seconds_period = seconds_period
1554        self._device_path = device_path
1555        self._stats = {}
1556        self._running = False
1557        self._error = None
1558
1559        result = utils.system_output('rootdev -s')
1560        # TODO(tbroch) Won't work for emmc storage and will throw this error in
1561        # keyvals : 'ioctl(SG_IO) error: [Errno 22] Invalid argument'
1562        # Lets implement something complimentary for emmc
1563        if not device_path:
1564            self._device_path = \
1565                re.sub('(sd[a-z]|mmcblk[0-9]+)p?[0-9]+', '\\1', result)
1566        logging.debug("device_path = %s", self._device_path)
1567
1568
1569    def start(self):
1570        logging.debug("inside DiskStateLogger.start")
1571        if os.path.exists(self._device_path):
1572            logging.debug("DiskStateLogger started")
1573            super(DiskStateLogger, self).start()
1574
1575
1576    def _get_disk_state(self):
1577        """Checks the disk's power mode and returns it as a string.
1578
1579        This uses the SG_IO ioctl to issue a raw SCSI command data block with
1580        the ATA-PASS-THROUGH command that allows SCSI-to-ATA translation (see
1581        T10 document 04-262r8). The ATA command issued is CHECKPOWERMODE1,
1582        which returns the device's current power mode.
1583        """
1584
1585        def _addressof(obj):
1586            """Shortcut to return the memory address of an object as integer."""
1587            return ctypes.cast(obj, ctypes.c_void_p).value
1588
1589        scsi_cdb = struct.pack("12B", # SCSI command data block (uint8[12])
1590                               0xa1, # SCSI opcode: ATA-PASS-THROUGH
1591                               3 << 1, # protocol: Non-data
1592                               1 << 5, # flags: CK_COND
1593                               0, # features
1594                               0, # sector count
1595                               0, 0, 0, # LBA
1596                               1 << 6, # flags: ATA-USING-LBA
1597                               0xe5, # ATA opcode: CHECKPOWERMODE1
1598                               0, # reserved
1599                               0, # control (no idea what this is...)
1600                              )
1601        scsi_sense = (ctypes.c_ubyte * 32)() # SCSI sense buffer (uint8[32])
1602        sgio_header = struct.pack("iiBBHIPPPIIiPBBBBHHiII", # see <scsi/sg.h>
1603                                  83, # Interface ID magic number (int32)
1604                                  -1, # data transfer direction: none (int32)
1605                                  12, # SCSI command data block length (uint8)
1606                                  32, # SCSI sense data block length (uint8)
1607                                  0, # iovec_count (not applicable?) (uint16)
1608                                  0, # data transfer length (uint32)
1609                                  0, # data block pointer
1610                                  _addressof(scsi_cdb), # SCSI CDB pointer
1611                                  _addressof(scsi_sense), # sense buffer pointer
1612                                  500, # timeout in milliseconds (uint32)
1613                                  0, # flags (uint32)
1614                                  0, # pack ID (unused) (int32)
1615                                  0, # user data pointer (unused)
1616                                  0, 0, 0, 0, 0, 0, 0, 0, 0, # output params
1617                                 )
1618        try:
1619            with open(self._device_path, 'r') as dev:
1620                result = fcntl.ioctl(dev, 0x2285, sgio_header)
1621        except IOError, e:
1622            raise error.TestError('ioctl(SG_IO) error: %s' % str(e))
1623        _, _, _, _, status, host_status, driver_status = \
1624            struct.unpack("4x4xxx2x4xPPP4x4x4xPBxxxHH4x4x4x", result)
1625        if status != 0x2: # status: CHECK_CONDITION
1626            raise error.TestError('SG_IO status: %d' % status)
1627        if host_status != 0:
1628            raise error.TestError('SG_IO host status: %d' % host_status)
1629        if driver_status != 0x8: # driver status: SENSE
1630            raise error.TestError('SG_IO driver status: %d' % driver_status)
1631
1632        if scsi_sense[0] != 0x72: # resp. code: current error, descriptor format
1633            raise error.TestError('SENSE response code: %d' % scsi_sense[0])
1634        if scsi_sense[1] != 0: # sense key: No Sense
1635            raise error.TestError('SENSE key: %d' % scsi_sense[1])
1636        if scsi_sense[7] < 14: # additional length (ATA status is 14 - 1 bytes)
1637            raise error.TestError('ADD. SENSE too short: %d' % scsi_sense[7])
1638        if scsi_sense[8] != 0x9: # additional descriptor type: ATA Return Status
1639            raise error.TestError('SENSE descriptor type: %d' % scsi_sense[8])
1640        if scsi_sense[11] != 0: # errors: none
1641            raise error.TestError('ATA error code: %d' % scsi_sense[11])
1642
1643        if scsi_sense[13] == 0x00:
1644            return 'standby'
1645        if scsi_sense[13] == 0x80:
1646            return 'idle'
1647        if scsi_sense[13] == 0xff:
1648            return 'active'
1649        return 'unknown(%d)' % scsi_sense[13]
1650
1651
1652    def run(self):
1653        """The Thread's run method."""
1654        try:
1655            self._time = time.time()
1656            self._running = True
1657            while(self._running):
1658                time.sleep(self._seconds_period)
1659                state = self._get_disk_state()
1660                new_time = time.time()
1661                if state in self._stats:
1662                    self._stats[state] += new_time - self._time
1663                else:
1664                    self._stats[state] = new_time - self._time
1665                self._time = new_time
1666        except error.TestError, e:
1667            self._error = e
1668            self._running = False
1669
1670
1671    def result(self):
1672        """Stop the logger and return dict with result percentages."""
1673        if (self._running):
1674            self._running = False
1675            self.join(self._seconds_period * 2)
1676        return AbstractStats.to_percent(self._stats)
1677
1678
1679    def get_error(self):
1680        """Returns the _error exception... please only call after result()."""
1681        return self._error
1682