| # Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import collections, ctypes, fcntl, glob, logging, math, numpy, os, re, struct |
| import threading, time |
| |
| from autotest_lib.client.bin import utils |
| from autotest_lib.client.common_lib import error, enum |
| from autotest_lib.client.cros import kernel_trace |
| |
| BatteryDataReportType = enum.Enum('CHARGE', 'ENERGY') |
| |
| # battery data reported at 1e6 scale |
| BATTERY_DATA_SCALE = 1e6 |
| |
| class DevStat(object): |
| """ |
| Device power status. This class implements generic status initialization |
| and parsing routines. |
| """ |
| |
| def __init__(self, fields, path=None): |
| self.fields = fields |
| self.path = path |
| |
| |
| def reset_fields(self): |
| """ |
| Reset all class fields to None to mark their status as unknown. |
| """ |
| for field in self.fields.iterkeys(): |
| setattr(self, field, None) |
| |
| |
| def read_val(self, file_name, field_type): |
| try: |
| path = file_name |
| if not file_name.startswith('/'): |
| path = os.path.join(self.path, file_name) |
| f = open(path, 'r') |
| out = f.readline() |
| val = field_type(out) |
| return val |
| |
| except: |
| return field_type(0) |
| |
| |
| def read_all_vals(self): |
| for field, prop in self.fields.iteritems(): |
| if prop[0]: |
| val = self.read_val(prop[0], prop[1]) |
| setattr(self, field, val) |
| |
| |
| class ThermalStatACPI(DevStat): |
| """ |
| ACPI-based thermal status. |
| |
| Fields: |
| (All temperatures are in millidegrees Celsius.) |
| |
| str enabled: Whether thermal zone is enabled |
| int temp: Current temperature |
| str type: Thermal zone type |
| int num_trip_points: Number of thermal trip points that activate |
| cooling devices |
| int num_points_tripped: Temperature is above this many trip points |
| str trip_point_N_type: Trip point #N's type |
| int trip_point_N_temp: Trip point #N's temperature value |
| int cdevX_trip_point: Trip point o cooling device #X (index) |
| """ |
| |
| MAX_TRIP_POINTS = 20 |
| |
| thermal_fields = { |
| 'enabled': ['enabled', str], |
| 'temp': ['temp', int], |
| 'type': ['type', str], |
| 'num_points_tripped': ['', ''] |
| } |
| path = '/sys/class/thermal/thermal_zone*' |
| |
| def __init__(self, path=None): |
| # Browse the thermal folder for trip point fields. |
| self.num_trip_points = 0 |
| |
| thermal_fields = glob.glob(path + '/*') |
| for file in thermal_fields: |
| field = file[len(path + '/'):] |
| if field.find('trip_point') != -1: |
| if field.find('temp'): |
| field_type = int |
| else: |
| field_type = str |
| self.thermal_fields[field] = [field, field_type] |
| |
| # Count the number of trip points. |
| if field.find('_type') != -1: |
| self.num_trip_points += 1 |
| |
| super(ThermalStatACPI, self).__init__(self.thermal_fields, path) |
| self.update() |
| |
| def update(self): |
| if not os.path.exists(self.path): |
| return |
| |
| self.read_all_vals() |
| self.num_points_tripped = 0 |
| |
| for field in self.thermal_fields: |
| if field.find('trip_point_') != -1 and field.find('_temp') != -1 \ |
| and self.temp > self.read_val(field, int): |
| self.num_points_tripped += 1 |
| logging.info('Temperature trip point #' + \ |
| field[len('trip_point_'):field.rfind('_temp')] + \ |
| ' tripped.') |
| |
| |
| class ThermalStatHwmon(DevStat): |
| """ |
| hwmon-based thermal status. |
| |
| Fields: |
| int <tname>_temp<num>_input: Current temperature in millidegrees Celsius |
| where: |
| <tname> : name of hwmon device in sysfs |
| <num> : number of temp as some hwmon devices have multiple |
| |
| """ |
| path = '/sys/class/hwmon' |
| |
| thermal_fields = {} |
| def __init__(self, rootpath=None): |
| if not rootpath: |
| rootpath = self.path |
| for subpath1 in glob.glob('%s/hwmon*' % rootpath): |
| for subpath2 in ['','device/']: |
| gpaths = glob.glob("%s/%stemp*_input" % (subpath1, subpath2)) |
| for gpath in gpaths: |
| bname = os.path.basename(gpath) |
| field_path = os.path.join(subpath1, subpath2, bname) |
| |
| tname_path = os.path.join(os.path.dirname(gpath), "name") |
| tname = utils.read_one_line(tname_path) |
| |
| field_key = "%s_%s" % (tname, bname) |
| self.thermal_fields[field_key] = [field_path, int] |
| |
| super(ThermalStatHwmon, self).__init__(self.thermal_fields, rootpath) |
| self.update() |
| |
| def update(self): |
| if not os.path.exists(self.path): |
| return |
| |
| self.read_all_vals() |
| |
| def read_val(self, file_name, field_type): |
| try: |
| path = os.path.join(self.path, file_name) |
| f = open(path, 'r') |
| out = f.readline() |
| return field_type(out) |
| except: |
| return field_type(0) |
| |
| |
| class ThermalStat(object): |
| """helper class to instantiate various thermal devices.""" |
| def __init__(self): |
| self._thermals = [] |
| self.min_temp = 999999999 |
| self.max_temp = -999999999 |
| |
| thermal_stat_types = [(ThermalStatHwmon.path, ThermalStatHwmon), |
| (ThermalStatACPI.path, ThermalStatACPI)] |
| for thermal_glob_path, thermal_type in thermal_stat_types: |
| try: |
| thermal_path = glob.glob(thermal_glob_path)[0] |
| logging.debug('Using %s for thermal info.' % thermal_path) |
| self._thermals.append(thermal_type(thermal_path)) |
| except: |
| logging.debug('Could not find thermal path %s, skipping.' % |
| thermal_glob_path) |
| |
| |
| def get_temps(self): |
| """Get temperature readings. |
| |
| Returns: |
| string of temperature readings. |
| """ |
| temp_str = '' |
| for thermal in self._thermals: |
| thermal.update() |
| for kname in thermal.fields: |
| if kname is 'temp' or kname.endswith('_input'): |
| val = getattr(thermal, kname) |
| temp_str += '%s:%d ' % (kname, val) |
| if val > self.max_temp: |
| self.max_temp = val |
| if val < self.min_temp: |
| self.min_temp = val |
| |
| |
| return temp_str |
| |
| |
| class BatteryStat(DevStat): |
| """ |
| Battery status. |
| |
| Fields: |
| |
| float charge_full: Last full capacity reached [Ah] |
| float charge_full_design: Full capacity by design [Ah] |
| float charge_now: Remaining charge [Ah] |
| float current_now: Battery discharge rate [A] |
| float energy: Current battery charge [Wh] |
| float energy_full: Last full capacity reached [Wh] |
| float energy_full_design: Full capacity by design [Wh] |
| float energy_rate: Battery discharge rate [W] |
| float power_now: Battery discharge rate [W] |
| float remaining_time: Remaining discharging time [h] |
| float voltage_min_design: Minimum voltage by design [V] |
| float voltage_max_design: Maximum voltage by design [V] |
| float voltage_now: Voltage now [V] |
| """ |
| |
| battery_fields = { |
| 'status': ['status', str], |
| 'charge_full': ['charge_full', float], |
| 'charge_full_design': ['charge_full_design', float], |
| 'charge_now': ['charge_now', float], |
| 'current_now': ['current_now', float], |
| 'voltage_min_design': ['voltage_min_design', float], |
| 'voltage_max_design': ['voltage_max_design', float], |
| 'voltage_now': ['voltage_now', float], |
| 'energy': ['energy_now', float], |
| 'energy_full': ['energy_full', float], |
| 'energy_full_design': ['energy_full_design', float], |
| 'power_now': ['power_now', float], |
| 'energy_rate': ['', ''], |
| 'remaining_time': ['', ''] |
| } |
| |
| def __init__(self, path=None): |
| super(BatteryStat, self).__init__(self.battery_fields, path) |
| self.update() |
| |
| |
| def update(self): |
| self.read_all_vals() |
| |
| if self.charge_full == 0 and self.energy_full != 0: |
| battery_type = BatteryDataReportType.ENERGY |
| else: |
| battery_type = BatteryDataReportType.CHARGE |
| |
| if self.voltage_min_design != 0: |
| voltage_nominal = self.voltage_min_design |
| else: |
| voltage_nominal = self.voltage_now |
| |
| # Since charge data is present, calculate parameters based upon |
| # reported charge data. |
| if battery_type == BatteryDataReportType.CHARGE: |
| self.charge_full = self.charge_full / BATTERY_DATA_SCALE |
| self.charge_full_design = self.charge_full_design / \ |
| BATTERY_DATA_SCALE |
| self.charge_now = self.charge_now / BATTERY_DATA_SCALE |
| |
| self.current_now = math.fabs(self.current_now) / \ |
| BATTERY_DATA_SCALE |
| |
| self.energy = voltage_nominal * \ |
| self.charge_now / \ |
| BATTERY_DATA_SCALE |
| self.energy_full = voltage_nominal * \ |
| self.charge_full / \ |
| BATTERY_DATA_SCALE |
| self.energy_full_design = voltage_nominal * \ |
| self.charge_full_design / \ |
| BATTERY_DATA_SCALE |
| |
| # Charge data not present, so calculate parameters based upon |
| # reported energy data. |
| elif battery_type == BatteryDataReportType.ENERGY: |
| self.charge_full = self.energy_full / voltage_nominal |
| self.charge_full_design = self.energy_full_design / \ |
| voltage_nominal |
| self.charge_now = self.energy / voltage_nominal |
| |
| # TODO(shawnn): check if power_now can really be reported |
| # as negative, in the same way current_now can |
| self.current_now = math.fabs(self.power_now) / \ |
| voltage_nominal |
| |
| self.energy = self.energy / BATTERY_DATA_SCALE |
| self.energy_full = self.energy_full / BATTERY_DATA_SCALE |
| self.energy_full_design = self.energy_full_design / \ |
| BATTERY_DATA_SCALE |
| |
| self.voltage_min_design = self.voltage_min_design / \ |
| BATTERY_DATA_SCALE |
| self.voltage_max_design = self.voltage_max_design / \ |
| BATTERY_DATA_SCALE |
| self.voltage_now = self.voltage_now / \ |
| BATTERY_DATA_SCALE |
| voltage_nominal = voltage_nominal / \ |
| BATTERY_DATA_SCALE |
| |
| if self.charge_full > (self.charge_full_design * 1.5): |
| raise error.TestError('Unreasonable charge_full value') |
| if self.charge_now > (self.charge_full_design * 1.5): |
| raise error.TestError('Unreasonable charge_now value') |
| |
| self.energy_rate = self.voltage_now * self.current_now |
| |
| self.remaining_time = 0 |
| if self.current_now: |
| self.remaining_time = self.energy / self.energy_rate |
| |
| |
| class LineStatDummy(object): |
| """ |
| Dummy line stat for devices which don't provide power_supply related sysfs |
| interface. |
| """ |
| def __init__(self): |
| self.online = True |
| |
| |
| def update(self): |
| pass |
| |
| class LineStat(DevStat): |
| """ |
| Power line status. |
| |
| Fields: |
| |
| bool online: Line power online |
| """ |
| |
| linepower_fields = { |
| 'is_online': ['online', int] |
| } |
| |
| |
| def __init__(self, path=None): |
| super(LineStat, self).__init__(self.linepower_fields, path) |
| self.update() |
| |
| |
| def update(self): |
| self.read_all_vals() |
| self.online = self.is_online == 1 |
| |
| |
| class SysStat(object): |
| """ |
| System power status for a given host. |
| |
| Fields: |
| |
| battery: A list of BatteryStat objects. |
| linepower: A list of LineStat objects. |
| """ |
| psu_types = ['Mains', 'USB', 'USB_ACA', 'USB_CDP', 'USB_DCP', 'Unknown'] |
| |
| def __init__(self): |
| power_supply_path = '/sys/class/power_supply/*' |
| self.battery = None |
| self.linepower = None |
| self.thermal = None |
| self.battery_path = None |
| self.linepower_path = None |
| |
| power_supplies = glob.glob(power_supply_path) |
| for path in power_supplies: |
| type_path = os.path.join(path,'type') |
| if not os.path.exists(type_path): |
| continue |
| power_type = utils.read_one_line(type_path) |
| if power_type == 'Battery': |
| self.battery_path = path |
| elif power_type in self.psu_types: |
| self.linepower_path = path |
| |
| if not self.battery_path or not self.linepower_path: |
| logging.warn("System does not provide power sysfs interface") |
| |
| self.thermal = ThermalStat() |
| |
| |
| def refresh(self): |
| """ |
| Initialize device power status objects. |
| """ |
| if self.battery_path: |
| self.battery = [ BatteryStat(self.battery_path) ] |
| if self.linepower_path: |
| self.linepower = [ LineStat(self.linepower_path) ] |
| else: |
| self.linepower = [ LineStatDummy() ] |
| |
| temp_str = self.thermal.get_temps() |
| if temp_str: |
| logging.info('Temperature reading: ' + temp_str) |
| else: |
| logging.error('Could not read temperature, skipping.') |
| |
| |
| def on_ac(self): |
| """ |
| Returns true if device is currently running from AC power. |
| """ |
| on_ac = self.linepower[0].online |
| # Butterfly can incorrectly report AC online for some time after |
| # unplug. Check battery discharge state to confirm. |
| if utils.get_board() == 'butterfly': |
| on_ac &= (not self.battery_discharging()) |
| return on_ac |
| |
| def battery_discharging(self): |
| """ |
| Returns true if battery is currently discharging. |
| """ |
| return(self.battery[0].status.rstrip() == 'Discharging') |
| |
| def percent_current_charge(self): |
| return self.battery[0].charge_now * 100 / \ |
| self.battery[0].charge_full_design |
| |
| |
| def assert_battery_state(self, percent_initial_charge_min): |
| """Check initial power configuration state is battery. |
| |
| Args: |
| percent_initial_charge_min: float between 0 -> 1.00 of |
| percentage of battery that must be remaining. |
| None|0|False means check not performed. |
| |
| Raises: |
| TestError: if one of battery assertions fails |
| """ |
| if self.on_ac(): |
| # TODO(shawnn): This is debug code. Need to remove it later. |
| if utils.get_board() == 'butterfly': |
| logging.debug('Butterfly on_ac, delay and re-check') |
| tries = 0 |
| while self.on_ac(): |
| logging.debug('Butterfly {on_ac, pcc, tries}: %d %d %d' % |
| (self.on_ac(), self.percent_current_charge(), tries)) |
| tries += 1 |
| if tries > 300: |
| logging.debug('on_ac never deasserted') |
| break |
| time.sleep(5) |
| self.refresh() |
| |
| raise error.TestError( |
| 'Running on AC power. Please remove AC power cable.') |
| |
| percent_initial_charge = self.percent_current_charge() |
| |
| if percent_initial_charge_min and percent_initial_charge < \ |
| percent_initial_charge_min: |
| raise error.TestError('Initial charge (%f) less than min (%f)' |
| % (percent_initial_charge, percent_initial_charge_min)) |
| |
| |
| def get_status(): |
| """ |
| Return a new power status object (SysStat). A new power status snapshot |
| for a given host can be obtained by either calling this routine again and |
| constructing a new SysStat object, or by using the refresh method of the |
| SysStat object. |
| """ |
| status = SysStat() |
| status.refresh() |
| return status |
| |
| |
| class AbstractStats(object): |
| """ |
| Common superclass for measurements of percentages per state over time. |
| """ |
| |
| @staticmethod |
| def to_percent(stats): |
| """ |
| Turns a dict with absolute time values into a dict with percentages. |
| """ |
| total = sum(stats.itervalues()) |
| if total == 0: |
| return {} |
| return dict((k, v * 100.0 / total) for (k, v) in stats.iteritems()) |
| |
| |
| @staticmethod |
| def do_diff(new, old): |
| """ |
| Returns a dict with value deltas from two dicts with matching keys. |
| """ |
| return dict((k, new[k] - old.get(k, 0)) for k in new.iterkeys()) |
| |
| |
| @staticmethod |
| def format_results_percent(results, name, percent_stats): |
| """ |
| Formats autotest result keys to format: |
| percent_<name>_<key>_time |
| """ |
| for key in percent_stats: |
| results['percent_%s_%s_time' % (name, key)] = percent_stats[key] |
| |
| |
| def __init__(self, name=None, incremental=True): |
| if not name: |
| error.TestFail("Need to name AbstractStats instance please.") |
| self.name = name |
| self._incremental = incremental |
| self._stats = self._read_stats() |
| |
| |
| def refresh(self): |
| """ |
| Returns dict mapping state names to percentage of time spent in them. |
| |
| @incremental: If False, stats returned are from a single _read_stats. |
| Otherwise, stats are from the difference between the |
| current and last refresh. |
| """ |
| raw_stats = result = self._read_stats() |
| if self._incremental: |
| result = self.do_diff(result, self._stats) |
| self._stats = raw_stats |
| return self.to_percent(result) |
| |
| |
| def _read_stats(self): |
| """ |
| Override! Reads the raw data values that shall be measured into a dict. |
| """ |
| raise NotImplementedError('Override _read_stats in the subclass!') |
| |
| |
| class CPUFreqStats(AbstractStats): |
| """ |
| CPU Frequency statistics |
| """ |
| |
| def __init__(self): |
| cpufreq_stats_path = '/sys/devices/system/cpu/cpu*/cpufreq/stats/' + \ |
| 'time_in_state' |
| self._file_paths = glob.glob(cpufreq_stats_path) |
| if not self._file_paths: |
| logging.debug('time_in_state file not found') |
| super(CPUFreqStats, self).__init__(name='cpufreq') |
| |
| |
| def _read_stats(self): |
| stats = {} |
| for path in self._file_paths: |
| if not os.path.exists(path): |
| logging.debug('%s is not found', path) |
| continue |
| |
| data = utils.read_file(path) |
| for line in data.splitlines(): |
| pair = line.split() |
| freq = int(pair[0]) |
| timeunits = int(pair[1]) |
| if freq in stats: |
| stats[freq] += timeunits |
| else: |
| stats[freq] = timeunits |
| return stats |
| |
| |
| class CPUIdleStats(AbstractStats): |
| """ |
| CPU Idle statistics (refresh() will not work with incremental=False!) |
| """ |
| # TODO (snanda): Handle changes in number of c-states due to events such |
| # as ac <-> battery transitions. |
| # TODO (snanda): Handle non-S0 states. Time spent in suspend states is |
| # currently not factored out. |
| def __init__(self): |
| super(CPUIdleStats, self).__init__(name='cpuidle') |
| |
| |
| def _read_stats(self): |
| cpuidle_stats = collections.defaultdict(int) |
| cpuidle_path = '/sys/devices/system/cpu/cpu*/cpuidle' |
| epoch_usecs = int(time.time() * 1000 * 1000) |
| cpus = glob.glob(cpuidle_path) |
| |
| for cpu in cpus: |
| state_path = os.path.join(cpu, 'state*') |
| states = glob.glob(state_path) |
| cpuidle_stats['C0'] += epoch_usecs |
| |
| for state in states: |
| if not int(utils.read_one_line(os.path.join(state, 'latency'))): |
| # C0 state. Kernel stats aren't right, so calculate by |
| # subtracting all other states from total time (using epoch |
| # timer since we calculate differences in the end anyway). |
| # NOTE: Only x86 lists C0 under cpuidle, ARM does not. |
| continue |
| |
| name = utils.read_one_line(os.path.join(state, 'name')) |
| usecs = int(utils.read_one_line(os.path.join(state, 'time'))) |
| cpuidle_stats['C0'] -= usecs |
| |
| if name == '<null>': |
| # Kernel race condition that can happen while a new C-state |
| # gets added (e.g. AC->battery). Don't know the 'name' of |
| # the state yet, but its 'time' would be 0 anyway. |
| logging.warn('Read name: <null>, time: %d from %s' |
| % (usecs, state) + '... skipping.') |
| continue |
| |
| cpuidle_stats[name] += usecs |
| |
| return cpuidle_stats |
| |
| |
| class CPUPackageStats(AbstractStats): |
| """ |
| Package C-state residency statistics for modern Intel CPUs. |
| """ |
| |
| ATOM = {'C2': 0x3F8, 'C4': 0x3F9, 'C6': 0x3FA} |
| NEHALEM = {'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA} |
| SANDY_BRIDGE = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA} |
| |
| def __init__(self): |
| def _get_platform_states(): |
| """ |
| Helper to decide what set of microarchitecture-specific MSRs to use. |
| |
| Returns: dict that maps C-state name to MSR address, or None. |
| """ |
| modalias = '/sys/devices/system/cpu/modalias' |
| if not os.path.exists(modalias): |
| return None |
| |
| values = utils.read_one_line(modalias).split(':') |
| # values[2]: vendor, values[4]: family, values[6]: model (CPUID) |
| if values[2] != '0000' or values[4] != '0006': |
| return None |
| |
| return { |
| # model groups pulled from Intel manual, volume 3 chapter 35 |
| '0027': self.ATOM, # unreleased? (Next Generation Atom) |
| '001A': self.NEHALEM, # Bloomfield, Nehalem-EP (i7/Xeon) |
| '001E': self.NEHALEM, # Clarks-/Lynnfield, Jasper (i5/i7/X) |
| '001F': self.NEHALEM, # unreleased? (abandoned?) |
| '0025': self.NEHALEM, # Arran-/Clarksdale (i3/i5/i7/C/X) |
| '002C': self.NEHALEM, # Gulftown, Westmere-EP (i7/Xeon) |
| '002E': self.NEHALEM, # Nehalem-EX (Xeon) |
| '002F': self.NEHALEM, # Westmere-EX (Xeon) |
| '002A': self.SANDY_BRIDGE, # SandyBridge (i3/i5/i7/C/X) |
| '002D': self.SANDY_BRIDGE, # SandyBridge-E (i7) |
| '003A': self.SANDY_BRIDGE, # IvyBridge (i3/i5/i7/X) |
| '003C': self.SANDY_BRIDGE, # unclear (Haswell?) |
| '003E': self.SANDY_BRIDGE, # IvyBridge (Xeon) |
| }.get(values[6], None) |
| |
| self._platform_states = _get_platform_states() |
| super(CPUPackageStats, self).__init__(name='cpupkg') |
| |
| |
| def _read_stats(self): |
| packages = set() |
| template = '/sys/devices/system/cpu/cpu%s/topology/physical_package_id' |
| if not self._platform_states: |
| return {} |
| stats = dict((state, 0) for state in self._platform_states) |
| stats['C0_C1'] = 0 |
| |
| for cpu in os.listdir('/dev/cpu'): |
| if not os.path.exists(template % cpu): |
| continue |
| package = utils.read_one_line(template % cpu) |
| if package in packages: |
| continue |
| packages.add(package) |
| |
| stats['C0_C1'] += utils.rdmsr(0x10, cpu) # TSC |
| for (state, msr) in self._platform_states.iteritems(): |
| ticks = utils.rdmsr(msr, cpu) |
| stats[state] += ticks |
| stats['C0_C1'] -= ticks |
| |
| return stats |
| |
| |
| class GPUFreqStats(AbstractStats): |
| """GPU Frequency statistics class. |
| |
| TODO(tbroch): add stats for other GPUs |
| """ |
| |
| _MALI_DEV = '/sys/class/misc/mali0/device' |
| _MALI_EVENTS = ['mali_dvfs:mali_dvfs_set_clock'] |
| _MALI_34_TRACE_CLK_RE = r'(\d+.\d+): mali_dvfs_set_clock: frequency=(\d+)' |
| _MALI_TRACE_CLK_RE = r'(\d+.\d+): mali_dvfs_set_clock: frequency=(\d+)\d{6}' |
| |
| _I915_ROOT = '/sys/kernel/debug/dri/0' |
| _I915_EVENTS = ['i915:intel_gpu_freq_change'] |
| _I915_CLK = os.path.join(_I915_ROOT, 'i915_cur_delayinfo') |
| _I915_TRACE_CLK_RE = r'(\d+.\d+): intel_gpu_freq_change: new_freq=(\d+)' |
| _I915_CUR_FREQ_RE = r'CAGF:\s+(\d+)MHz' |
| _I915_MIN_FREQ_RE = r'Lowest \(RPN\) frequency:\s+(\d+)MHz' |
| _I915_MAX_FREQ_RE = r'Max non-overclocked \(RP0\) frequency:\s+(\d+)MHz' |
| # TODO(dbasehore) parse this from debugfs if/when this value is added |
| _I915_FREQ_STEP = 50 |
| |
| _gpu_type = None |
| |
| |
| def _get_mali_freqs(self): |
| """Get mali clocks based on kernel version. |
| |
| For 3.4: |
| # cat /sys/class/misc/mali0/device/clock |
| Current sclk_g3d[G3D_BLK] = 100Mhz |
| Possible settings : 533, 450, 400, 350, 266, 160, 100Mhz |
| |
| For 3.8 (and beyond): |
| # cat /sys/class/misc/mali0/device/clock |
| 100000000 |
| # cat /sys/class/misc/mali0/device/available_frequencies |
| 100000000 |
| 160000000 |
| 266000000 |
| 350000000 |
| 400000000 |
| 450000000 |
| 533000000 |
| 533000000 |
| |
| Returns: |
| cur_mhz: string of current GPU clock in mhz |
| """ |
| cur_mhz = None |
| fqs = [] |
| |
| fname = os.path.join(self._MALI_DEV, 'clock') |
| if os.uname()[2].startswith('3.4'): |
| with open(fname) as fd: |
| for ln in fd.readlines(): |
| result = re.findall(r'Current.* = (\d+)Mhz', ln) |
| if result: |
| cur_mhz = result[0] |
| continue |
| result = re.findall(r'(\d+)[,M]', ln) |
| if result: |
| fqs = result |
| fd.close() |
| else: |
| cur_mhz = str(int(int(utils.read_one_line(fname).strip()) / 1e6)) |
| fname = os.path.join(self._MALI_DEV, 'available_frequencies') |
| with open(fname) as fd: |
| for ln in fd.readlines(): |
| freq = int(int(ln.strip()) / 1e6) |
| fqs.append(str(freq)) |
| fqs.sort() |
| |
| self._freqs = fqs |
| return cur_mhz |
| |
| |
| def __init__(self, incremental=False): |
| |
| |
| min_mhz = None |
| max_mhz = None |
| cur_mhz = None |
| events = None |
| self._freqs = [] |
| self._prev_sample = None |
| self._trace = None |
| |
| if os.path.exists(self._MALI_DEV): |
| self._set_gpu_type('mali') |
| elif os.path.exists(self._I915_CLK): |
| self._set_gpu_type('i915') |
| |
| logging.debug("gpu_type is %s", self._gpu_type) |
| |
| if self._gpu_type is 'mali': |
| events = self._MALI_EVENTS |
| cur_mhz = self._get_mali_freqs() |
| if self._freqs: |
| min_mhz = self._freqs[0] |
| max_mhz = self._freqs[-1] |
| |
| elif self._gpu_type is 'i915': |
| events = self._I915_EVENTS |
| with open(self._I915_CLK) as fd: |
| for ln in fd.readlines(): |
| logging.debug("ln = %s", ln) |
| result = re.findall(self._I915_CUR_FREQ_RE, ln) |
| if result: |
| cur_mhz = result[0] |
| continue |
| result = re.findall(self._I915_MIN_FREQ_RE, ln) |
| if result: |
| min_mhz = result[0] |
| continue |
| result = re.findall(self._I915_MAX_FREQ_RE, ln) |
| if result: |
| max_mhz = result[0] |
| continue |
| if min_mhz and max_mhz: |
| for i in xrange(int(min_mhz), int(max_mhz) + |
| self._I915_FREQ_STEP, self._I915_FREQ_STEP): |
| self._freqs.append(str(i)) |
| |
| logging.debug("cur_mhz = %s, min_mhz = %s, max_mhz = %s", cur_mhz, |
| min_mhz, max_mhz) |
| |
| if cur_mhz and min_mhz and max_mhz: |
| self._trace = kernel_trace.KernelTrace(events=events) |
| |
| # Not all platforms or kernel versions support tracing. |
| if not self._trace or not self._trace.is_tracing(): |
| logging.warn("GPU frequency tracing not enabled.") |
| else: |
| self._prev_sample = (cur_mhz, self._trace.uptime_secs()) |
| logging.debug("Current GPU freq: %s", cur_mhz) |
| logging.debug("All GPU freqs: %s", self._freqs) |
| |
| super(GPUFreqStats, self).__init__(name='gpu', incremental=incremental) |
| |
| |
| @classmethod |
| def _set_gpu_type(cls, gpu_type): |
| cls._gpu_type = gpu_type |
| |
| |
| def _read_stats(self): |
| if self._gpu_type: |
| return getattr(self, "_%s_read_stats" % self._gpu_type)() |
| return {} |
| |
| |
| def _trace_read_stats(self, regexp): |
| """Read GPU stats from kernel trace outputs. |
| |
| Args: |
| regexp: regular expression to match trace output for frequency |
| |
| Returns: |
| Dict with key string in mhz and val float in seconds. |
| """ |
| if not self._prev_sample: |
| return {} |
| |
| stats = dict((k, 0.0) for k in self._freqs) |
| results = self._trace.read(regexp=regexp) |
| for (tstamp_str, freq) in results: |
| tstamp = float(tstamp_str) |
| |
| # do not reparse lines in trace buffer |
| if tstamp <= self._prev_sample[1]: |
| continue |
| delta = tstamp - self._prev_sample[1] |
| logging.debug("freq:%s tstamp:%f - %f delta:%f", |
| self._prev_sample[0], |
| tstamp, self._prev_sample[1], |
| delta) |
| stats[self._prev_sample[0]] += delta |
| self._prev_sample = (freq, tstamp) |
| |
| # Do last record |
| delta = self._trace.uptime_secs() - self._prev_sample[1] |
| logging.debug("freq:%s tstamp:uptime - %f delta:%f", |
| self._prev_sample[0], |
| self._prev_sample[1], delta) |
| stats[self._prev_sample[0]] += delta |
| |
| logging.debug("GPU freq percents:%s", stats) |
| return stats |
| |
| |
| def _mali_read_stats(self): |
| """Read Mali GPU stats |
| |
| For 3.4: |
| Frequencies are reported in MHz. |
| |
| For 3.8+: |
| Frequencies are reported in Hz, so use a regex that drops the last 6 |
| digits. |
| |
| Output in trace looks like this: |
| |
| kworker/u:24-5220 [000] .... 81060.329232: mali_dvfs_set_clock: frequency=400 |
| kworker/u:24-5220 [000] .... 81061.830128: mali_dvfs_set_clock: frequency=350 |
| |
| Returns: |
| Dict with frequency in mhz as key and float in seconds for time |
| spent at that frequency. |
| """ |
| regexp = None |
| if os.uname()[2].startswith('3.4'): |
| regexp = self._MALI_34_TRACE_CLK_RE |
| else: |
| regexp = self._MALI_TRACE_CLK_RE |
| |
| return self._trace_read_stats(regexp) |
| |
| |
| def _i915_read_stats(self): |
| """Read i915 GPU stats. |
| |
| Output looks like this (kernel >= 3.8): |
| |
| kworker/u:0-28247 [000] .... 259391.579610: intel_gpu_freq_change: new_freq=400 |
| kworker/u:0-28247 [000] .... 259391.581797: intel_gpu_freq_change: new_freq=350 |
| |
| Returns: |
| Dict with frequency in mhz as key and float in seconds for time |
| spent at that frequency. |
| """ |
| return self._trace_read_stats(self._I915_TRACE_CLK_RE) |
| |
| |
| class USBSuspendStats(AbstractStats): |
| """ |
| USB active/suspend statistics (over all devices) |
| """ |
| # TODO (snanda): handle hot (un)plugging of USB devices |
| # TODO (snanda): handle duration counters wraparound |
| |
| def __init__(self): |
| usb_stats_path = '/sys/bus/usb/devices/*/power' |
| self._file_paths = glob.glob(usb_stats_path) |
| if not self._file_paths: |
| logging.debug('USB stats path not found') |
| super(USBSuspendStats, self).__init__(name='usb') |
| |
| |
| def _read_stats(self): |
| usb_stats = {'active': 0, 'suspended': 0} |
| |
| for path in self._file_paths: |
| active_duration_path = os.path.join(path, 'active_duration') |
| total_duration_path = os.path.join(path, 'connected_duration') |
| |
| if not os.path.exists(active_duration_path) or \ |
| not os.path.exists(total_duration_path): |
| logging.debug('duration paths do not exist for: %s', path) |
| continue |
| |
| active = int(utils.read_file(active_duration_path)) |
| total = int(utils.read_file(total_duration_path)) |
| logging.debug('device %s active for %.2f%%', |
| path, active * 100.0 / total) |
| |
| usb_stats['active'] += active |
| usb_stats['suspended'] += total - active |
| |
| return usb_stats |
| |
| |
| class StatoMatic(object): |
| """Class to aggregate and monitor a bunch of power related statistics.""" |
| def __init__(self): |
| self._start_uptime_secs = kernel_trace.KernelTrace.uptime_secs() |
| self._astats = [USBSuspendStats(), |
| CPUFreqStats(), |
| GPUFreqStats(incremental=False), |
| CPUIdleStats()] |
| self._disk = DiskStateLogger() |
| self._disk.start() |
| |
| |
| def publish(self): |
| """Publishes results of various statistics gathered. |
| |
| Returns: |
| dict with |
| key = string 'percent_<name>_<key>_time' |
| value = float in percent |
| """ |
| results = {} |
| tot_secs = kernel_trace.KernelTrace.uptime_secs() - \ |
| self._start_uptime_secs |
| for stat_obj in self._astats: |
| percent_stats = stat_obj.refresh() |
| logging.debug("pstats = %s", percent_stats) |
| if stat_obj.name is 'gpu': |
| # TODO(tbroch) remove this once GPU freq stats have proved |
| # reliable |
| stats_secs = sum(stat_obj._stats.itervalues()) |
| if stats_secs < (tot_secs * 0.9) or \ |
| stats_secs > (tot_secs * 1.1): |
| logging.warn('%s stats dont look right. Not publishing.', |
| stat_obj.name) |
| continue |
| new_res = {} |
| AbstractStats.format_results_percent(new_res, stat_obj.name, |
| percent_stats) |
| results.update(new_res) |
| |
| new_res = {} |
| if self._disk.get_error(): |
| new_res['disk_logging_error'] = str(self._disk.get_error()) |
| else: |
| AbstractStats.format_results_percent(new_res, 'disk', |
| self._disk.result()) |
| results.update(new_res) |
| |
| return results |
| |
| |
| class PowerMeasurement(object): |
| """Class to measure power. |
| |
| Public attributes: |
| domain: String name of the power domain being measured. Example is |
| 'system' for total system power |
| |
| Public methods: |
| refresh: Performs any power/energy sampling and calculation and returns |
| power as float in watts. This method MUST be implemented in |
| subclass. |
| """ |
| |
| def __init__(self, domain): |
| """Constructor.""" |
| self.domain = domain |
| |
| |
| def refresh(self): |
| """Performs any power/energy sampling and calculation. |
| |
| MUST be implemented in subclass |
| |
| Returns: |
| float, power in watts. |
| """ |
| raise NotImplementedError("'refresh' method should be implemented in " |
| "subclass.") |
| |
| |
| def parse_power_supply_info(): |
| """Parses power_supply_info command output. |
| |
| Command output from power_manager ( tools/power_supply_info.cc ) looks like |
| this: |
| |
| Device: Line Power |
| path: /sys/class/power_supply/cros_ec-charger |
| ... |
| Device: Battery |
| path: /sys/class/power_supply/sbs-9-000b |
| ... |
| |
| """ |
| rv = collections.defaultdict(dict) |
| dev = None |
| for ln in utils.system_output('power_supply_info').splitlines(): |
| logging.debug("psu: %s", ln) |
| result = re.findall(r'^Device:\s+(.*)', ln) |
| if result: |
| dev = result[0] |
| continue |
| result = re.findall(r'\s+(.+):\s+(.+)', ln) |
| if result and dev: |
| kname = re.findall(r'(.*)\s+\(\w+\)', result[0][0]) |
| if kname: |
| rv[dev][kname[0]] = result[0][1] |
| else: |
| rv[dev][result[0][0]] = result[0][1] |
| |
| return rv |
| |
| |
| class SystemPower(PowerMeasurement): |
| """Class to measure system power. |
| |
| TODO(tbroch): This class provides a subset of functionality in BatteryStat |
| in hopes of minimizing power draw. Investigate whether its really |
| significant and if not, deprecate. |
| |
| Private Attributes: |
| _voltage_file: path to retrieve voltage in uvolts |
| _current_file: path to retrieve current in uamps |
| """ |
| |
| def __init__(self, battery_dir): |
| """Constructor. |
| |
| Args: |
| battery_dir: path to dir containing the files to probe and log. |
| usually something like /sys/class/power_supply/BAT0/ |
| """ |
| super(SystemPower, self).__init__('system') |
| # Files to log voltage and current from |
| self._voltage_file = os.path.join(battery_dir, 'voltage_now') |
| self._current_file = os.path.join(battery_dir, 'current_now') |
| |
| |
| def refresh(self): |
| """refresh method. |
| |
| See superclass PowerMeasurement for details. |
| """ |
| keyvals = parse_power_supply_info() |
| return float(keyvals['Battery']['energy rate']) |
| |
| |
| class MeasurementLogger(threading.Thread): |
| """A thread that logs measurement readings. |
| |
| Example code snippet: |
| mylogger = MeasurementLogger([Measurent1, Measurent2]) |
| mylogger.run() |
| for testname in tests: |
| start_time = time.time() |
| #run the test method for testname |
| mlogger.checkpoint(testname, start_time) |
| keyvals = mylogger.calc() |
| |
| Public attributes: |
| seconds_period: float, probing interval in seconds. |
| readings: list of lists of floats of measurements. |
| times: list of floats of time (since Epoch) of when measurements |
| occurred. len(time) == len(readings). |
| done: flag to stop the logger. |
| domains: list of domain strings being measured |
| |
| Public methods: |
| run: launches the thread to gather measuremnts |
| calc: calculates |
| save_results: |
| |
| Private attributes: |
| _measurements: list of Measurement objects to be sampled. |
| _checkpoint_data: list of tuples. Tuple contains: |
| tname: String of testname associated with this time interval |
| tstart: Float of time when subtest started |
| tend: Float of time when subtest ended |
| _results: list of results tuples. Tuple contains: |
| prefix: String of subtest |
| mean: Float of mean in watts |
| std: Float of standard deviation of measurements |
| tstart: Float of time when subtest started |
| tend: Float of time when subtest ended |
| """ |
| def __init__(self, measurements, seconds_period=1.0): |
| """Initialize a logger. |
| |
| Args: |
| _measurements: list of Measurement objects to be sampled. |
| seconds_period: float, probing interval in seconds. Default 1.0 |
| """ |
| threading.Thread.__init__(self) |
| |
| self.seconds_period = seconds_period |
| |
| self.readings = [] |
| self.times = [] |
| self._checkpoint_data = [] |
| |
| self.domains = [] |
| self._measurements = measurements |
| for meas in self._measurements: |
| self.domains.append(meas.domain) |
| |
| self.done = False |
| |
| |
| def run(self): |
| """Threads run method.""" |
| while(not self.done): |
| self.times.append(time.time()) |
| readings = [] |
| for meas in self._measurements: |
| readings.append(meas.refresh()) |
| self.readings.append(readings) |
| time.sleep(self.seconds_period) |
| |
| |
| def checkpoint(self, tname='', tstart=None, tend=None): |
| """Check point the times in seconds associated with test tname. |
| |
| Args: |
| tname: String of testname associated with this time interval |
| tstart: Float in seconds of when tname test started. Should be based |
| off time.time() |
| tend: Float in seconds of when tname test ended. Should be based |
| off time.time(). If None, then value computed in the method. |
| """ |
| if not tstart and self.times: |
| tstart = self.times[0] |
| if not tend: |
| tend = time.time() |
| self._checkpoint_data.append((tname, tstart, tend)) |
| logging.info('Finished test "%s" between timestamps [%s, %s]', |
| tname, tstart, tend) |
| |
| |
| def calc(self, mtype=None): |
| """Calculate average measurement during each of the sub-tests. |
| |
| Method performs the following steps: |
| 1. Signals the thread to stop running. |
| 2. Calculates mean, max, min, count on the samples for each of the |
| measurements. |
| 3. Stores results to be written later. |
| 4. Creates keyvals for autotest publishing. |
| |
| Args: |
| mtype: string of measurement type. For example: |
| pwr == power |
| temp == temperature |
| |
| Returns: |
| dict of keyvals suitable for autotest results. |
| """ |
| if not mtype: |
| mtype = 'meas' |
| |
| t = numpy.array(self.times) |
| keyvals = {} |
| results = [] |
| |
| if not self.done: |
| self.done = True |
| # times 2 the sleep time in order to allow for readings as well. |
| self.join(timeout=self.seconds_period * 2) |
| |
| if not self._checkpoint_data: |
| self.checkpoint() |
| |
| for i, domain_readings in enumerate(zip(*self.readings)): |
| meas = numpy.array(domain_readings) |
| domain = self.domains[i] |
| |
| for tname, tstart, tend in self._checkpoint_data: |
| if tname: |
| prefix = '%s_%s' % (tname, domain) |
| else: |
| prefix = domain |
| keyvals[prefix+'_duration'] = tend - tstart |
| # Select all readings taken between tstart and tend timestamps |
| meas_array = meas[numpy.bitwise_and(tstart < t, t < tend)] |
| # If sub-test terminated early, avoid calculating avg, std and |
| # min |
| if not meas_array.size: |
| continue |
| meas_mean = meas_array.mean() |
| meas_std = meas_array.std() |
| |
| # Results list can be used for pretty printing and saving as csv |
| results.append((prefix, meas_mean, meas_std, |
| tend - tstart, tstart, tend)) |
| |
| keyvals[prefix + '_' + mtype] = meas_mean |
| keyvals[prefix + '_' + mtype + '_cnt'] = meas_array.size |
| keyvals[prefix + '_' + mtype + '_max'] = meas_array.max() |
| keyvals[prefix + '_' + mtype + '_min'] = meas_array.min() |
| keyvals[prefix + '_' + mtype + '_std'] = meas_std |
| |
| self._results = results |
| return keyvals |
| |
| |
| def save_results(self, resultsdir, fname=None): |
| """Save computed results in a nice tab-separated format. |
| This is useful for long manual runs. |
| |
| Args: |
| resultsdir: String, directory to write results to |
| fname: String name of file to write results to |
| """ |
| if not fname: |
| fname = 'meas_results_%.0f.txt' % time.time() |
| fname = os.path.join(resultsdir, fname) |
| with file(fname, 'wt') as f: |
| for row in self._results: |
| # First column is name, the rest are numbers. See _calc_power() |
| fmt_row = [row[0]] + ['%.2f' % x for x in row[1:]] |
| line = '\t'.join(fmt_row) |
| f.write(line + '\n') |
| |
| |
| class PowerLogger(MeasurementLogger): |
| def save_results(self, resultsdir, fname=None): |
| if not fname: |
| fname = 'power_results_%.0f.txt' % time.time() |
| super(PowerLogger, self).save_results(resultsdir, fname) |
| |
| |
| def calc(self, mtype='pwr'): |
| return super(PowerLogger, self).calc(mtype) |
| |
| |
| class TempMeasurement(object): |
| """Class to measure temperature. |
| |
| Public attributes: |
| domain: String name of the temperature domain being measured. Example is |
| 'cpu' for cpu temperature |
| |
| Private attributes: |
| _path: Path to temperature file to read ( in millidegrees Celsius ) |
| |
| Public methods: |
| refresh: Performs any temperature sampling and calculation and returns |
| temperature as float in degrees Celsius. |
| """ |
| def __init__(self, domain, path): |
| """Constructor.""" |
| self.domain = domain |
| self._path = path |
| |
| |
| def refresh(self): |
| """Performs temperature |
| |
| Returns: |
| float, temperature in degrees Celsius |
| """ |
| return int(utils.read_one_line(self._path)) / 1000. |
| |
| |
| class TempLogger(MeasurementLogger): |
| """A thread that logs temperature readings in millidegrees Celsius.""" |
| def __init__(self, measurements, seconds_period=30.0): |
| if not measurements: |
| measurements = [] |
| tstats = ThermalStatHwmon() |
| for kname in tstats.fields: |
| match = re.match(r'(\S+)_temp(\d+)_input', kname) |
| if not match: |
| continue |
| domain = match.group(1) + '-t' + match.group(2) |
| fpath = tstats.fields[kname][0] |
| new_meas = TempMeasurement(domain, fpath) |
| measurements.append(new_meas) |
| super(TempLogger, self).__init__(measurements, seconds_period) |
| |
| |
| def save_results(self, resultsdir, fname=None): |
| if not fname: |
| fname = 'temp_results_%.0f.txt' % time.time() |
| super(TempLogger, self).save_results(resultsdir, fname) |
| |
| |
| def calc(self, mtype='temp'): |
| return super(TempLogger, self).calc(mtype) |
| |
| |
| class DiskStateLogger(threading.Thread): |
| """Records the time percentages the disk stays in its different power modes. |
| |
| Example code snippet: |
| mylogger = power_status.DiskStateLogger() |
| mylogger.start() |
| result = mylogger.result() |
| |
| Public methods: |
| start: Launches the thread and starts measurements. |
| result: Stops the thread if it's still running and returns measurements. |
| get_error: Returns the exception in _error if it exists. |
| |
| Private functions: |
| _get_disk_state: Returns the disk's current ATA power mode as a string. |
| |
| Private attributes: |
| _seconds_period: Disk polling interval in seconds. |
| _stats: Dict that maps disk states to seconds spent in them. |
| _running: Flag that is True as long as the logger should keep running. |
| _time: Timestamp of last disk state reading. |
| _device_path: The file system path of the disk's device node. |
| _error: Contains a TestError exception if an unexpected error occured |
| """ |
| def __init__(self, seconds_period = 5.0, device_path = None): |
| """Initializes a logger. |
| |
| Args: |
| seconds_period: Disk polling interval in seconds. Default 5.0 |
| device_path: The path of the disk's device node. Default '/dev/sda' |
| """ |
| threading.Thread.__init__(self) |
| self._seconds_period = seconds_period |
| self._device_path = device_path |
| self._stats = {} |
| self._running = False |
| self._error = None |
| |
| result = utils.system_output('rootdev -s') |
| # TODO(tbroch) Won't work for emmc storage and will throw this error in |
| # keyvals : 'ioctl(SG_IO) error: [Errno 22] Invalid argument' |
| # Lets implement something complimentary for emmc |
| if not device_path: |
| self._device_path = \ |
| re.sub('(sd[a-z]|mmcblk[0-9]+)p?[0-9]+', '\\1', result) |
| logging.debug("device_path = %s", self._device_path) |
| |
| |
| def start(self): |
| logging.debug("inside DiskStateLogger.start") |
| if os.path.exists(self._device_path): |
| logging.debug("DiskStateLogger started") |
| super(DiskStateLogger, self).start() |
| |
| |
| def _get_disk_state(self): |
| """Checks the disk's power mode and returns it as a string. |
| |
| This uses the SG_IO ioctl to issue a raw SCSI command data block with |
| the ATA-PASS-THROUGH command that allows SCSI-to-ATA translation (see |
| T10 document 04-262r8). The ATA command issued is CHECKPOWERMODE1, |
| which returns the device's current power mode. |
| """ |
| |
| def _addressof(obj): |
| """Shortcut to return the memory address of an object as integer.""" |
| return ctypes.cast(obj, ctypes.c_void_p).value |
| |
| scsi_cdb = struct.pack("12B", # SCSI command data block (uint8[12]) |
| 0xa1, # SCSI opcode: ATA-PASS-THROUGH |
| 3 << 1, # protocol: Non-data |
| 1 << 5, # flags: CK_COND |
| 0, # features |
| 0, # sector count |
| 0, 0, 0, # LBA |
| 1 << 6, # flags: ATA-USING-LBA |
| 0xe5, # ATA opcode: CHECKPOWERMODE1 |
| 0, # reserved |
| 0, # control (no idea what this is...) |
| ) |
| scsi_sense = (ctypes.c_ubyte * 32)() # SCSI sense buffer (uint8[32]) |
| sgio_header = struct.pack("iiBBHIPPPIIiPBBBBHHiII", # see <scsi/sg.h> |
| 83, # Interface ID magic number (int32) |
| -1, # data transfer direction: none (int32) |
| 12, # SCSI command data block length (uint8) |
| 32, # SCSI sense data block length (uint8) |
| 0, # iovec_count (not applicable?) (uint16) |
| 0, # data transfer length (uint32) |
| 0, # data block pointer |
| _addressof(scsi_cdb), # SCSI CDB pointer |
| _addressof(scsi_sense), # sense buffer pointer |
| 500, # timeout in milliseconds (uint32) |
| 0, # flags (uint32) |
| 0, # pack ID (unused) (int32) |
| 0, # user data pointer (unused) |
| 0, 0, 0, 0, 0, 0, 0, 0, 0, # output params |
| ) |
| try: |
| with open(self._device_path, 'r') as dev: |
| result = fcntl.ioctl(dev, 0x2285, sgio_header) |
| except IOError, e: |
| raise error.TestError('ioctl(SG_IO) error: %s' % str(e)) |
| _, _, _, _, status, host_status, driver_status = \ |
| struct.unpack("4x4xxx2x4xPPP4x4x4xPBxxxHH4x4x4x", result) |
| if status != 0x2: # status: CHECK_CONDITION |
| raise error.TestError('SG_IO status: %d' % status) |
| if host_status != 0: |
| raise error.TestError('SG_IO host status: %d' % host_status) |
| if driver_status != 0x8: # driver status: SENSE |
| raise error.TestError('SG_IO driver status: %d' % driver_status) |
| |
| if scsi_sense[0] != 0x72: # resp. code: current error, descriptor format |
| raise error.TestError('SENSE response code: %d' % scsi_sense[0]) |
| if scsi_sense[1] != 0: # sense key: No Sense |
| raise error.TestError('SENSE key: %d' % scsi_sense[1]) |
| if scsi_sense[7] < 14: # additional length (ATA status is 14 - 1 bytes) |
| raise error.TestError('ADD. SENSE too short: %d' % scsi_sense[7]) |
| if scsi_sense[8] != 0x9: # additional descriptor type: ATA Return Status |
| raise error.TestError('SENSE descriptor type: %d' % scsi_sense[8]) |
| if scsi_sense[11] != 0: # errors: none |
| raise error.TestError('ATA error code: %d' % scsi_sense[11]) |
| |
| if scsi_sense[13] == 0x00: |
| return 'standby' |
| if scsi_sense[13] == 0x80: |
| return 'idle' |
| if scsi_sense[13] == 0xff: |
| return 'active' |
| return 'unknown(%d)' % scsi_sense[13] |
| |
| |
| def run(self): |
| """The Thread's run method.""" |
| try: |
| self._time = time.time() |
| self._running = True |
| while(self._running): |
| time.sleep(self._seconds_period) |
| state = self._get_disk_state() |
| new_time = time.time() |
| if state in self._stats: |
| self._stats[state] += new_time - self._time |
| else: |
| self._stats[state] = new_time - self._time |
| self._time = new_time |
| except error.TestError, e: |
| self._error = e |
| self._running = False |
| |
| |
| def result(self): |
| """Stop the logger and return dict with result percentages.""" |
| if (self._running): |
| self._running = False |
| self.join(self._seconds_period * 2) |
| return AbstractStats.to_percent(self._stats) |
| |
| |
| def get_error(self): |
| """Returns the _error exception... please only call after result().""" |
| return self._error |