3081 lines
107 KiB
Python
3081 lines
107 KiB
Python
# 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
|
|
import contextlib
|
|
import ctypes
|
|
import fcntl
|
|
import glob
|
|
import itertools
|
|
import json
|
|
import logging
|
|
import math
|
|
import numpy
|
|
import os
|
|
import re
|
|
import struct
|
|
import threading
|
|
import time
|
|
|
|
from autotest_lib.client.bin import utils
|
|
from autotest_lib.client.common_lib import autotest_enum
|
|
from autotest_lib.client.common_lib import error
|
|
from autotest_lib.client.common_lib.cros import retry
|
|
from autotest_lib.client.common_lib.utils import poll_for_condition_ex
|
|
from autotest_lib.client.cros import kernel_trace
|
|
from autotest_lib.client.cros.power import power_utils
|
|
|
|
BatteryDataReportType = autotest_enum.AutotestEnum('CHARGE', 'ENERGY')
|
|
|
|
# For devices whose full capacity is significantly lower than design full
|
|
# capacity, scale down their design full capacity.
|
|
BATTERY_DESIGN_FULL_SCALE = {'jinlon': 0.95, # b/161307060
|
|
'berknip': 0.94, # b/172625511
|
|
}
|
|
# battery data reported at 1e6 scale
|
|
BATTERY_DATA_SCALE = 1e6
|
|
# number of times to retry reading the battery in the case of bad data
|
|
BATTERY_RETRY_COUNT = 3
|
|
# default filename when saving CheckpointLogger data to file
|
|
CHECKPOINT_LOG_DEFAULT_FNAME = 'checkpoint_log.json'
|
|
|
|
|
|
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):
|
|
"""Read a value from file.
|
|
"""
|
|
try:
|
|
path = file_name
|
|
if not file_name.startswith('/'):
|
|
path = os.path.join(self.path, file_name)
|
|
f = open(path, 'r')
|
|
out = f.readline().rstrip('\n')
|
|
val = field_type(out)
|
|
return val
|
|
|
|
except:
|
|
return field_type(0)
|
|
|
|
|
|
def read_all_vals(self):
|
|
"""Read all values.
|
|
"""
|
|
for field, prop in self.fields.iteritems():
|
|
if prop[0]:
|
|
val = self.read_val(prop[0], prop[1])
|
|
setattr(self, field, val)
|
|
|
|
def update(self):
|
|
"""Update the DevStat.
|
|
|
|
Need to implement in subclass.
|
|
"""
|
|
pass
|
|
|
|
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 #%s tripped.', \
|
|
field[len('trip_point_'):field.rfind('_temp')])
|
|
|
|
|
|
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],
|
|
'present': ['present', int],
|
|
'energy_rate': ['', ''],
|
|
'remaining_time': ['', '']
|
|
}
|
|
|
|
def __init__(self, path=None):
|
|
super(BatteryStat, self).__init__(self.battery_fields, path)
|
|
self.update()
|
|
|
|
|
|
def update(self):
|
|
for _ in xrange(BATTERY_RETRY_COUNT):
|
|
try:
|
|
self._read_battery()
|
|
return
|
|
except error.TestError as e:
|
|
logging.warn(e)
|
|
for field, prop in self.battery_fields.iteritems():
|
|
logging.warn(field + ': ' + repr(getattr(self, field)))
|
|
continue
|
|
raise error.TestError('Failed to read battery state')
|
|
|
|
|
|
def _read_battery(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
|
|
|
|
if voltage_nominal == 0:
|
|
raise error.TestError('Failed to determine battery voltage')
|
|
|
|
battery_design_full_scale = 1
|
|
model = utils.get_platform()
|
|
if model in BATTERY_DESIGN_FULL_SCALE:
|
|
battery_design_full_scale = BATTERY_DESIGN_FULL_SCALE.get(model)
|
|
logging.info(
|
|
'Apply %f scale to design full battery capacity for model '
|
|
'%s', battery_design_full_scale, model)
|
|
|
|
# Since charge data is present, calculate parameters based upon
|
|
# reported charge data.
|
|
if battery_type == BatteryDataReportType.CHARGE:
|
|
self.charge_full_design *= battery_design_full_scale
|
|
|
|
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.energy_full_design *= battery_design_full_scale
|
|
|
|
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
|
|
|
|
self.energy_rate = self.voltage_now * self.current_now
|
|
|
|
self.remaining_time = 0
|
|
if self.current_now and self.energy_rate:
|
|
self.remaining_time = self.energy / self.energy_rate
|
|
|
|
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')
|
|
|
|
|
|
class LineStatDummy(DevStat):
|
|
"""
|
|
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],
|
|
'status': ['status', str]
|
|
}
|
|
|
|
|
|
def __init__(self, path=None):
|
|
super(LineStat, self).__init__(self.linepower_fields, path)
|
|
logging.debug("line path: %s", 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 BatteryStat object.
|
|
linepower: A list of LineStat objects.
|
|
"""
|
|
psu_types = ['Mains', 'USB', 'USB_ACA', 'USB_C', 'USB_CDP', 'USB_DCP',
|
|
'USB_PD', 'USB_PD_DRP', 'Unknown']
|
|
|
|
def __init__(self):
|
|
power_supply_path = '/sys/class/power_supply/*'
|
|
self.battery = None
|
|
self.linepower = []
|
|
self.thermal = None
|
|
self.battery_path = None
|
|
self.linepower_path = []
|
|
|
|
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':
|
|
scope_path = os.path.join(path,'scope')
|
|
if (os.path.exists(scope_path) and
|
|
utils.read_one_line(scope_path) == 'Device'):
|
|
continue
|
|
self.battery_path = path
|
|
elif power_type in self.psu_types:
|
|
self.linepower_path.append(path)
|
|
|
|
if not self.battery_path or not self.linepower_path:
|
|
logging.warning("System does not provide power sysfs interface")
|
|
|
|
self.thermal = ThermalStat()
|
|
if self.battery_path:
|
|
self.sys_low_batt_p = float(utils.system_output(
|
|
'check_powerd_config --low_battery_shutdown_percent',
|
|
ignore_status=True) or 4.0)
|
|
|
|
|
|
def refresh(self):
|
|
"""
|
|
Initialize device power status objects.
|
|
"""
|
|
self.linepower = []
|
|
|
|
if self.battery_path:
|
|
self.battery = BatteryStat(self.battery_path)
|
|
|
|
for path in self.linepower_path:
|
|
self.linepower.append(LineStat(path))
|
|
if not self.linepower:
|
|
self.linepower = [ LineStatDummy() ]
|
|
|
|
temp_str = self.thermal.get_temps()
|
|
if temp_str:
|
|
logging.info('Temperature reading: %s', 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 = False
|
|
for linepower in self.linepower:
|
|
on_ac |= linepower.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_charging(self):
|
|
"""
|
|
Returns true if battery is currently charging or false otherwise.
|
|
"""
|
|
for linepower in self.linepower:
|
|
if linepower.status == 'Charging':
|
|
return True
|
|
|
|
if not self.battery_path:
|
|
logging.warn('Unable to determine battery charge status')
|
|
return False
|
|
|
|
return self.battery.status.rstrip() == 'Charging'
|
|
|
|
|
|
def battery_discharging(self):
|
|
"""
|
|
Returns true if battery is currently discharging or false otherwise.
|
|
"""
|
|
if not self.battery_path:
|
|
logging.warn('Unable to determine battery discharge status')
|
|
return False
|
|
|
|
return self.battery.status.rstrip() == 'Discharging'
|
|
|
|
def battery_full(self):
|
|
"""
|
|
Returns true if battery is currently full or false otherwise.
|
|
"""
|
|
if not self.battery_path:
|
|
logging.warn('Unable to determine battery fullness status')
|
|
return False
|
|
|
|
return self.battery.status.rstrip() == 'Full'
|
|
|
|
|
|
def battery_discharge_ok_on_ac(self):
|
|
"""Returns True if battery is ok to discharge on AC presently.
|
|
|
|
some devices cycle between charge & discharge above a certain
|
|
SoC. If AC is charging and SoC > 95% we can safely assume that.
|
|
"""
|
|
return self.battery_charging() and (self.percent_current_charge() > 95)
|
|
|
|
|
|
def percent_current_charge(self):
|
|
"""Returns current charge compare to design capacity in percent.
|
|
"""
|
|
return self.battery.charge_now * 100 / \
|
|
self.battery.charge_full_design
|
|
|
|
|
|
def percent_display_charge(self):
|
|
"""Returns current display charge in percent.
|
|
"""
|
|
keyvals = parse_power_supply_info()
|
|
return float(keyvals['Battery']['display percentage'])
|
|
|
|
|
|
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():
|
|
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 assert_battery_in_range(self, min_level, max_level):
|
|
"""Raise a error.TestFail if the battery level is not in range."""
|
|
current_percent = self.percent_display_charge()
|
|
if not (min_level <= current_percent <= max_level):
|
|
raise error.TestFail('battery must be in range [{}, {}]'.format(
|
|
min_level, max_level))
|
|
|
|
def is_low_battery(self, low_batt_margin_p=2.0):
|
|
"""Returns True if battery current charge is low
|
|
|
|
@param low_batt_margin_p: percentage of battery that would be added to
|
|
system low battery level.
|
|
"""
|
|
return (self.battery_discharging() and
|
|
self.percent_current_charge() < self.sys_low_batt_p +
|
|
low_batt_margin_p)
|
|
|
|
|
|
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
|
|
|
|
|
|
def poll_for_charging_behavior(behavior, timeout):
|
|
"""
|
|
Wait up to |timeout| seconds for the charging behavior to become |behavior|.
|
|
|
|
@param behavior: One of 'ON_AC_AND_CHARGING',
|
|
'ON_AC_AND_NOT_CHARGING',
|
|
'NOT_ON_AC_AND_NOT_CHARGING'.
|
|
@param timeout: in seconds.
|
|
|
|
@raises: error.TestFail if the behavior does not match in time, or another
|
|
exception if something else fails along the way.
|
|
"""
|
|
ps = get_status()
|
|
|
|
def _verify_on_AC_and_charging():
|
|
ps.refresh()
|
|
if not ps.on_ac():
|
|
raise error.TestFail('Device is not on AC, but should be')
|
|
if not ps.battery_charging():
|
|
raise error.TestFail('Device is not charging, but should be')
|
|
return True
|
|
|
|
def _verify_on_AC_and_not_charging():
|
|
ps.refresh()
|
|
if not ps.on_ac():
|
|
raise error.TestFail('Device is not on AC, but should be')
|
|
if ps.battery_charging():
|
|
raise error.TestFail('Device is charging, but should not be')
|
|
return True
|
|
|
|
def _verify_not_on_AC_and_not_charging():
|
|
ps.refresh()
|
|
if ps.on_ac():
|
|
raise error.TestFail('Device is on AC, but should not be')
|
|
return True
|
|
|
|
poll_functions = {
|
|
'ON_AC_AND_CHARGING' : _verify_on_AC_and_charging,
|
|
'ON_AC_AND_NOT_CHARGING' : _verify_on_AC_and_not_charging,
|
|
'NOT_ON_AC_AND_NOT_CHARGING': _verify_not_on_AC_and_not_charging,
|
|
}
|
|
poll_for_condition_ex(poll_functions[behavior],
|
|
timeout=timeout,
|
|
sleep_interval=1)
|
|
|
|
class AbstractStats(object):
|
|
"""
|
|
Common superclass for measurements of percentages per state over time.
|
|
|
|
Public Attributes:
|
|
incremental: If False, stats returned are from a single
|
|
_read_stats. Otherwise, stats are from the difference between
|
|
the current and last refresh.
|
|
"""
|
|
|
|
@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 {k: 0 for k in stats}
|
|
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]
|
|
|
|
|
|
@staticmethod
|
|
def format_results_wavg(results, name, wavg):
|
|
"""
|
|
Add an autotest result keys to format: wavg_<name>
|
|
"""
|
|
if wavg is not None:
|
|
results['wavg_%s' % (name)] = wavg
|
|
|
|
|
|
def __init__(self, name, incremental=True):
|
|
self.name = name
|
|
self.incremental = incremental
|
|
self._stats = self._read_stats()
|
|
self._first_stats = self._stats.copy()
|
|
|
|
def refresh(self):
|
|
"""
|
|
Returns dict mapping state names to percentage of time spent in them.
|
|
"""
|
|
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 _automatic_weighted_average(self):
|
|
"""
|
|
Turns a dict with absolute times (or percentages) into a weighted
|
|
average value.
|
|
"""
|
|
stats = self._stats
|
|
if self.incremental:
|
|
stats = self.do_diff(stats, self._first_stats)
|
|
|
|
total = sum(stats.itervalues())
|
|
if total == 0:
|
|
return None
|
|
|
|
return sum(float(k) * v / total for k, v in stats.iteritems())
|
|
|
|
def _supports_automatic_weighted_average(self):
|
|
"""
|
|
Override!
|
|
|
|
Returns True if stats collected can be automatically converted from
|
|
percent distribution to weighted average. False otherwise.
|
|
"""
|
|
return False
|
|
|
|
|
|
def weighted_average(self):
|
|
"""
|
|
Return weighted average calculated using the automated average method
|
|
(if supported) or using a custom method defined by the stat.
|
|
"""
|
|
if self._supports_automatic_weighted_average():
|
|
return self._automatic_weighted_average()
|
|
|
|
return self._weighted_avg_fn()
|
|
|
|
|
|
def _weighted_avg_fn(self):
|
|
"""
|
|
Override! Custom weighted average function.
|
|
|
|
Returns weighted average as a single floating point value.
|
|
"""
|
|
return None
|
|
|
|
|
|
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!')
|
|
|
|
|
|
CPU_BASE_PATH = '/sys/devices/system/cpu/'
|
|
|
|
def count_all_cpus():
|
|
"""
|
|
Return count of cpus on system.
|
|
"""
|
|
path = '%s/cpu[0-9]*' % CPU_BASE_PATH
|
|
return len(glob.glob(path))
|
|
|
|
def get_online_cpus():
|
|
"""
|
|
Return frozenset of integer cpu numbers that are online.
|
|
"""
|
|
return frozenset(read_cpu_set('/sys/devices/system/cpu/online'))
|
|
|
|
def get_cpus_filepaths_for_suffix(cpus, suffix):
|
|
"""
|
|
For each cpu in |cpus| check whether |CPU_BASE_PATH|/cpu%d/|suffix| exists.
|
|
Return tuple of two lists t:
|
|
t[0]: all cpu ids where the condition above holds
|
|
t[1]: all full paths where condition above holds.
|
|
"""
|
|
available_cpus = []
|
|
available_paths = []
|
|
for c in cpus:
|
|
c_file_path = os.path.join(CPU_BASE_PATH, 'cpu%d' % c, suffix)
|
|
if os.path.exists(c_file_path):
|
|
available_cpus.append(c)
|
|
available_paths.append(c_file_path)
|
|
return (available_cpus, available_paths)
|
|
|
|
|
|
class CPUFreqStatsPState(AbstractStats):
|
|
"""
|
|
CPU Frequency statistics for intel_pstate
|
|
"""
|
|
MSR_PLATFORM_INFO = 0xce
|
|
MSR_IA32_MPERF = 0xe7
|
|
MSR_IA32_APERF = 0xe8
|
|
|
|
def __init__(self, cpus=None):
|
|
name = 'cpufreq'
|
|
if not cpus:
|
|
cpus = get_online_cpus()
|
|
self._cpus = cpus
|
|
|
|
if len(cpus) and len(cpus) < count_all_cpus():
|
|
name = '%s_%s' % (name, '_'.join([str(c) for c in cpus]))
|
|
|
|
self._initial_perf = None
|
|
self._current_perf = None
|
|
|
|
# max_freq is supposed to be the same for all CPUs and remain
|
|
# constant throughout. So, we set the entry only once.
|
|
# Note that this is max non-turbo frequency, some CPU can run at
|
|
# higher turbo frequency in some condition.
|
|
platform_info = utils.rdmsr(self.MSR_PLATFORM_INFO)
|
|
mul = platform_info >> 8 & 0xff
|
|
bclk = utils.get_intel_bclk_khz()
|
|
self._max_freq = mul * bclk
|
|
|
|
super(CPUFreqStatsPState, self).__init__(name)
|
|
|
|
def _read_stats(self):
|
|
aperf = 0
|
|
mperf = 0
|
|
|
|
for cpu in self._cpus:
|
|
aperf += utils.rdmsr(self.MSR_IA32_APERF, cpu)
|
|
mperf += utils.rdmsr(self.MSR_IA32_MPERF, cpu)
|
|
|
|
if not self._initial_perf:
|
|
self._initial_perf = (aperf, mperf)
|
|
|
|
self._current_perf = (aperf, mperf)
|
|
|
|
return {}
|
|
|
|
def _weighted_avg_fn(self):
|
|
if (self._current_perf
|
|
and self._current_perf[1] != self._initial_perf[1]):
|
|
# Avg freq = max_freq * aperf_delta / mperf_delta
|
|
return self._max_freq * \
|
|
float(self._current_perf[0] - self._initial_perf[0]) / \
|
|
(self._current_perf[1] - self._initial_perf[1])
|
|
return 1.0
|
|
|
|
|
|
class CPUFreqStats(AbstractStats):
|
|
"""
|
|
CPU Frequency statistics
|
|
"""
|
|
|
|
def __init__(self, cpus=None):
|
|
name = 'cpufreq'
|
|
stats_suffix = 'cpufreq/stats/time_in_state'
|
|
key_suffix = 'cpufreq/scaling_available_frequencies'
|
|
if not cpus:
|
|
cpus = get_online_cpus()
|
|
_, self._file_paths = get_cpus_filepaths_for_suffix(cpus, stats_suffix)
|
|
|
|
if len(cpus) and len(cpus) < count_all_cpus():
|
|
name = '%s_%s' % (name, '_'.join([str(c) for c in cpus]))
|
|
self._cpus = cpus
|
|
self._available_freqs = set()
|
|
|
|
if not self._file_paths:
|
|
logging.debug('time_in_state file not found')
|
|
|
|
# assumes cpufreq driver for CPU0 is the same as the others.
|
|
_, cpufreq_key_paths = get_cpus_filepaths_for_suffix(cpus, key_suffix)
|
|
for path in cpufreq_key_paths:
|
|
self._available_freqs |= set(
|
|
int(x) for x in utils.read_file(path).split())
|
|
|
|
super(CPUFreqStats, self).__init__(name)
|
|
|
|
def _read_stats(self):
|
|
stats = dict((k, 0) for k in self._available_freqs)
|
|
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
|
|
|
|
def _supports_automatic_weighted_average(self):
|
|
return True
|
|
|
|
|
|
class CPUCStateStats(AbstractStats):
|
|
"""
|
|
Base class for C-state residency statistics
|
|
"""
|
|
def __init__(self, name, non_c0_stat=''):
|
|
self._non_c0_stat = non_c0_stat
|
|
super(CPUCStateStats, self).__init__(name)
|
|
|
|
|
|
def to_percent(self, stats):
|
|
"""
|
|
Turns a dict with absolute time values into a dict with percentages.
|
|
Ignore the |non_c0_stat_name| which is aggegate stat in the total count.
|
|
"""
|
|
total = sum(v for k, v in stats.iteritems() if k != self._non_c0_stat)
|
|
if total == 0:
|
|
return {k: 0 for k in stats}
|
|
return {k: v * 100.0 / total for k, v in stats.iteritems()}
|
|
|
|
|
|
class CPUIdleStats(CPUCStateStats):
|
|
"""
|
|
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, cpus=None):
|
|
name = 'cpuidle'
|
|
cpuidle_suffix = 'cpuidle'
|
|
if not cpus:
|
|
cpus = get_online_cpus()
|
|
cpus, self._cpus = get_cpus_filepaths_for_suffix(cpus, cpuidle_suffix)
|
|
if len(cpus) and len(cpus) < count_all_cpus():
|
|
name = '%s_%s' % (name, '_'.join([str(c) for c in cpus]))
|
|
super(CPUIdleStats, self).__init__(name=name, non_c0_stat='non-C0')
|
|
|
|
|
|
def _read_stats(self):
|
|
cpuidle_stats = collections.defaultdict(int)
|
|
epoch_usecs = int(time.time() * 1000 * 1000)
|
|
for cpu in self._cpus:
|
|
state_path = os.path.join(cpu, 'state*')
|
|
states = glob.glob(state_path)
|
|
cpuidle_stats['C0'] += epoch_usecs
|
|
|
|
for state in states:
|
|
name = utils.read_one_line(os.path.join(state, 'name'))
|
|
latency = utils.read_one_line(os.path.join(state, 'latency'))
|
|
|
|
if not int(latency) and name == 'POLL':
|
|
# 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
|
|
|
|
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.warning('Read name: <null>, time: %d from %s...'
|
|
'skipping.', usecs, state)
|
|
continue
|
|
|
|
cpuidle_stats[name] += usecs
|
|
cpuidle_stats['non-C0'] += usecs
|
|
|
|
return cpuidle_stats
|
|
|
|
|
|
class CPUPackageStats(CPUCStateStats):
|
|
"""
|
|
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}
|
|
SILVERMONT = {'C6': 0x3FA}
|
|
GOLDMONT = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9,'C10': 0x632}
|
|
BROADWELL = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA,
|
|
'C8': 0x630, 'C9': 0x631,'C10': 0x632}
|
|
|
|
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.
|
|
"""
|
|
cpu_uarch = utils.get_intel_cpu_uarch()
|
|
|
|
return {
|
|
# model groups pulled from Intel SDM, volume 4
|
|
# Group same package cstate using the older uarch name
|
|
#
|
|
# TODO(harry.pan): As the keys represent microarchitecture
|
|
# names, we could consider to rename the PC state groups
|
|
# to avoid ambiguity.
|
|
'Airmont': self.SILVERMONT,
|
|
'Atom': self.ATOM,
|
|
'Broadwell': self.BROADWELL,
|
|
'Comet Lake': self.BROADWELL,
|
|
'Goldmont': self.GOLDMONT,
|
|
'Haswell': self.SANDY_BRIDGE,
|
|
'Ice Lake': self.BROADWELL,
|
|
'Ivy Bridge': self.SANDY_BRIDGE,
|
|
'Ivy Bridge-E': self.SANDY_BRIDGE,
|
|
'Kaby Lake': self.BROADWELL,
|
|
'Nehalem': self.NEHALEM,
|
|
'Sandy Bridge': self.SANDY_BRIDGE,
|
|
'Silvermont': self.SILVERMONT,
|
|
'Skylake': self.BROADWELL,
|
|
'Tiger Lake': self.BROADWELL,
|
|
'Tremont': self.GOLDMONT,
|
|
'Westmere': self.NEHALEM,
|
|
}.get(cpu_uarch, None)
|
|
|
|
self._platform_states = _get_platform_states()
|
|
super(CPUPackageStats, self).__init__(name='cpupkg',
|
|
non_c0_stat='non-C0_C1')
|
|
|
|
|
|
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
|
|
stats['non-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['non-C0_C1'] += ticks
|
|
stats['C0_C1'] -= ticks
|
|
|
|
return stats
|
|
|
|
|
|
class DevFreqStats(AbstractStats):
|
|
"""
|
|
Devfreq device frequency stats.
|
|
"""
|
|
|
|
_DIR = '/sys/class/devfreq'
|
|
|
|
|
|
def __init__(self, path):
|
|
"""Constructs DevFreqStats Object that track frequency stats
|
|
for the path of the given Devfreq device.
|
|
|
|
The frequencies for devfreq devices are listed in Hz.
|
|
|
|
Args:
|
|
path: the path to the devfreq device
|
|
|
|
Example:
|
|
/sys/class/devfreq/dmc
|
|
"""
|
|
self._path = os.path.join(self._DIR, path)
|
|
if not os.path.exists(self._path):
|
|
raise error.TestError(
|
|
'DevFreqStats: devfreq device does not exist: %s' %
|
|
self._path)
|
|
|
|
fname = os.path.join(self._path, 'available_frequencies')
|
|
af = utils.read_one_line(fname).strip()
|
|
self._available_freqs = sorted(af.split(), key=int)
|
|
|
|
super(DevFreqStats, self).__init__(path)
|
|
|
|
def _read_stats(self):
|
|
stats = dict((freq, 0) for freq in self._available_freqs)
|
|
fname = os.path.join(self._path, 'trans_stat')
|
|
|
|
with open(fname) as fd:
|
|
# The lines that contain the time in each frequency start on the 3rd
|
|
# line, so skip the first 2 lines. The last line contains the number
|
|
# of transitions, so skip that line too.
|
|
# The time in each frequency is at the end of the line.
|
|
freq_pattern = re.compile(r'\d+(?=:)')
|
|
for line in fd.readlines()[2:-1]:
|
|
freq = freq_pattern.search(line)
|
|
if freq and freq.group() in self._available_freqs:
|
|
stats[freq.group()] = int(line.strip().split()[-1])
|
|
|
|
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_TRACE_CLK_RE = \
|
|
r'kworker.* (\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_CLKS_FILES = ['i915_cur_delayinfo', 'i915_frequency_info']
|
|
_I915_TRACE_CLK_RE = \
|
|
r'kworker.* (\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'
|
|
# There are 6 frequency steps per 100 MHz
|
|
_I915_FREQ_STEPS = [0, 17, 33, 50, 67, 83]
|
|
|
|
_gpu_type = None
|
|
|
|
|
|
def _get_mali_freqs(self):
|
|
"""Get mali clocks based on kernel version.
|
|
|
|
For 3.8-3.18:
|
|
# cat /sys/class/misc/mali0/device/clock
|
|
100000000
|
|
# cat /sys/class/misc/mali0/device/available_frequencies
|
|
100000000
|
|
160000000
|
|
266000000
|
|
350000000
|
|
400000000
|
|
450000000
|
|
533000000
|
|
533000000
|
|
|
|
For 4.4+:
|
|
Tracked in DevFreqStats
|
|
|
|
Returns:
|
|
cur_mhz: string of current GPU clock in mhz
|
|
"""
|
|
cur_mhz = None
|
|
fqs = []
|
|
|
|
fname = os.path.join(self._MALI_DEV, 'clock')
|
|
if os.path.exists(fname):
|
|
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
|
|
i915_path = None
|
|
self._freqs = []
|
|
self._prev_sample = None
|
|
self._trace = None
|
|
|
|
if os.path.exists(self._MALI_DEV) and \
|
|
not os.path.exists(os.path.join(self._MALI_DEV, "devfreq")):
|
|
self._set_gpu_type('mali')
|
|
else:
|
|
for file_name in self._I915_CLKS_FILES:
|
|
full_path = os.path.join(self._I915_ROOT, file_name)
|
|
if os.path.exists(full_path):
|
|
self._set_gpu_type('i915')
|
|
i915_path = full_path
|
|
break
|
|
else:
|
|
# We either don't know how to track GPU stats (yet) or the stats
|
|
# are tracked in DevFreqStats.
|
|
self._set_gpu_type(None)
|
|
|
|
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(i915_path) as fd:
|
|
for ln in fd.readlines():
|
|
logging.debug("ln = %s", ln.strip())
|
|
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) + 1):
|
|
if i % 100 in self._I915_FREQ_STEPS:
|
|
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.warning("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__('gpufreq', 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
|
|
|
|
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.
|
|
"""
|
|
return self._trace_read_stats(self._MALI_TRACE_CLK_RE)
|
|
|
|
|
|
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)
|
|
|
|
|
|
def _supports_automatic_weighted_average(self):
|
|
return self._gpu_type is not None
|
|
|
|
|
|
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__('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
|
|
|
|
|
|
def read_cpu_set(filename):
|
|
"""
|
|
Parse data of form "0,2-4,9"
|
|
|
|
Return a set of ints
|
|
"""
|
|
data = utils.read_file(filename)
|
|
ret = set()
|
|
|
|
for entry in data.split(','):
|
|
entry_data = entry.split('-')
|
|
start = end = int(entry_data[0])
|
|
if len(entry_data) > 1:
|
|
end = int(entry_data[1])
|
|
ret |= set(range(start, end + 1))
|
|
return ret
|
|
|
|
|
|
def get_cpu_sibling_groups():
|
|
"""
|
|
Get CPU core groups in HMP systems.
|
|
|
|
In systems with both small core and big core,
|
|
returns groups of small and big sibling groups.
|
|
"""
|
|
siblings_suffix = 'topology/core_siblings_list'
|
|
sibling_groups = []
|
|
cpus_processed = set()
|
|
cpus, sibling_file_paths = get_cpus_filepaths_for_suffix(get_online_cpus(),
|
|
siblings_suffix)
|
|
for c, siblings_path in zip(cpus, sibling_file_paths):
|
|
if c in cpus_processed:
|
|
# This cpu is already part of a sibling group. Skip.
|
|
continue
|
|
sibling_group = read_cpu_set(siblings_path)
|
|
cpus_processed |= sibling_group
|
|
sibling_groups.append(frozenset(sibling_group))
|
|
return tuple(sibling_groups)
|
|
|
|
|
|
def get_available_cpu_stats():
|
|
"""Return CPUFreq/CPUIdleStats groups by big-small siblings groups."""
|
|
ret = [CPUPackageStats()]
|
|
cpu_sibling_groups = get_cpu_sibling_groups()
|
|
|
|
cpufreq_stat_class = CPUFreqStats
|
|
# assumes cpufreq driver for CPU0 is the same as the others.
|
|
cpufreq_driver = '/sys/devices/system/cpu/cpu0/cpufreq/scaling_driver'
|
|
if utils.read_one_line(cpufreq_driver) == 'intel_pstate':
|
|
logging.debug('intel_pstate driver active')
|
|
cpufreq_stat_class = CPUFreqStatsPState
|
|
|
|
if not cpu_sibling_groups:
|
|
ret.append(cpufreq_stat_class())
|
|
ret.append(CPUIdleStats())
|
|
for cpu_group in cpu_sibling_groups:
|
|
ret.append(cpufreq_stat_class(cpu_group))
|
|
ret.append(CPUIdleStats(cpu_group))
|
|
if has_rc6_support():
|
|
ret.append(GPURC6Stats())
|
|
return ret
|
|
|
|
|
|
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(), GPUFreqStats(incremental=False)]
|
|
self._astats.extend(get_available_cpu_stats())
|
|
if os.path.isdir(DevFreqStats._DIR):
|
|
self._astats.extend([DevFreqStats(f) for f in \
|
|
os.listdir(DevFreqStats._DIR)])
|
|
|
|
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.warning('%s stats dont look right. Not publishing.',
|
|
stat_obj.name)
|
|
continue
|
|
new_res = {}
|
|
AbstractStats.format_results_percent(new_res, stat_obj.name,
|
|
percent_stats)
|
|
wavg = stat_obj.weighted_average()
|
|
if wavg:
|
|
AbstractStats.format_results_wavg(new_res, stat_obj.name, wavg)
|
|
|
|
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 BatteryStateOfCharge(PowerMeasurement):
|
|
"""Class for logging battery state of charge."""
|
|
|
|
def __init__(self):
|
|
"""Constructor."""
|
|
super(BatteryStateOfCharge, self).__init__('battery_soc')
|
|
|
|
def refresh(self):
|
|
"""refresh method.
|
|
|
|
See superclass PowerMeasurement for details.
|
|
"""
|
|
keyvals = parse_power_supply_info()
|
|
return float(keyvals['Battery']['percentage'])
|
|
|
|
|
|
class CheckpointLogger(object):
|
|
"""Class to log checkpoint data.
|
|
|
|
Public attributes:
|
|
checkpoint_data: dictionary of (tname, tlist).
|
|
tname: String of testname associated with these time intervals
|
|
tlist: list of tuples. Tuple contains:
|
|
tstart: Float of time when subtest started
|
|
tend: Float of time when subtest ended
|
|
|
|
Public methods:
|
|
start: records a start timestamp
|
|
checkpoint
|
|
checkblock
|
|
save_checkpoint_data
|
|
load_checkpoint_data
|
|
|
|
Static methods:
|
|
load_checkpoint_data_static
|
|
|
|
Private attributes:
|
|
_start_time: start timestamp for checkpoint logger
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.checkpoint_data = collections.defaultdict(list)
|
|
self.start()
|
|
|
|
# If multiple MeasurementLoggers call start() on the same CheckpointLogger,
|
|
# the latest one will register start time.
|
|
def start(self):
|
|
"""Set start time for CheckpointLogger."""
|
|
self._start_time = time.time()
|
|
|
|
@contextlib.contextmanager
|
|
def checkblock(self, tname=''):
|
|
"""Check point for the following block with test tname.
|
|
|
|
Args:
|
|
tname: String of testname associated with this time interval
|
|
"""
|
|
start_time = time.time()
|
|
yield
|
|
self.checkpoint(tname, start_time)
|
|
|
|
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(). If None, use start timestamp for the checkpoint
|
|
logger.
|
|
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._start_time:
|
|
tstart = self._start_time
|
|
if not tend:
|
|
tend = time.time()
|
|
self.checkpoint_data[tname].append((tstart, tend))
|
|
logging.info('Finished test "%s" between timestamps [%s, %s]',
|
|
tname, tstart, tend)
|
|
|
|
def convert_relative(self, start_time=None):
|
|
"""Convert data from power_status.CheckpointLogger object to relative
|
|
checkpoint data dictionary. Timestamps are converted to time in seconds
|
|
since the test started.
|
|
|
|
Args:
|
|
start_time: Float in seconds of the desired start time reference.
|
|
Should be based off time.time(). If None, use start timestamp
|
|
for the checkpoint logger.
|
|
"""
|
|
if start_time is None:
|
|
start_time = self._start_time
|
|
|
|
checkpoint_dict = {}
|
|
for tname, tlist in self.checkpoint_data.iteritems():
|
|
checkpoint_dict[tname] = [(tstart - start_time, tend - start_time)
|
|
for tstart, tend in tlist]
|
|
|
|
return checkpoint_dict
|
|
|
|
def save_checkpoint_data(self, resultsdir, fname=CHECKPOINT_LOG_DEFAULT_FNAME):
|
|
"""Save checkpoint data.
|
|
|
|
Args:
|
|
resultsdir: String, directory to write results to
|
|
fname: String, name of file to write results to
|
|
"""
|
|
fname = os.path.join(resultsdir, fname)
|
|
with file(fname, 'wt') as f:
|
|
json.dump(self.checkpoint_data, f, indent=4, separators=(',', ': '))
|
|
|
|
def load_checkpoint_data(self, resultsdir,
|
|
fname=CHECKPOINT_LOG_DEFAULT_FNAME):
|
|
"""Load checkpoint data.
|
|
|
|
Args:
|
|
resultsdir: String, directory to load results from
|
|
fname: String, name of file to load results from
|
|
"""
|
|
fname = os.path.join(resultsdir, fname)
|
|
try:
|
|
with open(fname, 'r') as f:
|
|
self.checkpoint_data = json.load(f,
|
|
object_hook=to_checkpoint_data)
|
|
# Set start time to the earliest start timestamp in file.
|
|
self._start_time = min(
|
|
ts_pair[0] for ts_pair in itertools.chain.from_iterable(
|
|
self.checkpoint_data.itervalues()))
|
|
except Exception as exc:
|
|
logging.warning('Failed to load checkpoint data from json file %s, '
|
|
'see exception: %s', fname, exc)
|
|
|
|
@staticmethod
|
|
def load_checkpoint_data_static(resultsdir,
|
|
fname=CHECKPOINT_LOG_DEFAULT_FNAME):
|
|
"""Load checkpoint data.
|
|
|
|
Args:
|
|
resultsdir: String, directory to load results from
|
|
fname: String, name of file to load results from
|
|
"""
|
|
fname = os.path.join(resultsdir, fname)
|
|
with file(fname, 'r') as f:
|
|
checkpoint_data = json.load(f)
|
|
return checkpoint_data
|
|
|
|
|
|
def to_checkpoint_data(json_dict):
|
|
"""Helper method to translate json object into CheckpointLogger format.
|
|
|
|
Args:
|
|
json_dict: a json object in the format of python dict
|
|
Returns:
|
|
a defaultdict in CheckpointLogger data format
|
|
"""
|
|
checkpoint_data = collections.defaultdict(list)
|
|
for tname, tlist in json_dict.iteritems():
|
|
checkpoint_data[tname].extend([tuple(ts_pair) for ts_pair in tlist])
|
|
return checkpoint_data
|
|
|
|
|
|
def get_checkpoint_logger_from_file(resultsdir,
|
|
fname=CHECKPOINT_LOG_DEFAULT_FNAME):
|
|
"""Create a CheckpointLogger and load checkpoint data from file.
|
|
|
|
Args:
|
|
resultsdir: String, directory to load results from
|
|
fname: String, name of file to load results from
|
|
Returns:
|
|
CheckpointLogger with data from file
|
|
"""
|
|
checkpoint_logger = CheckpointLogger()
|
|
checkpoint_logger.load_checkpoint_data(resultsdir, fname)
|
|
return checkpoint_logger
|
|
|
|
|
|
class MeasurementLogger(threading.Thread):
|
|
"""A thread that logs measurement readings.
|
|
|
|
Example code snippet:
|
|
my_logger = MeasurementLogger([Measurement1, Measurement2])
|
|
my_logger.start()
|
|
for testname in tests:
|
|
# Option 1: use checkblock
|
|
with my_logger.checkblock(testname):
|
|
# run the test method for testname
|
|
|
|
# Option 2: use checkpoint
|
|
start_time = time.time()
|
|
# run the test method for testname
|
|
my_logger.checkpoint(testname, start_time)
|
|
|
|
keyvals = my_logger.calc()
|
|
|
|
or using CheckpointLogger:
|
|
checkpoint_logger = CheckpointLogger()
|
|
my_logger = MeasurementLogger([Measurement1, Measurement2],
|
|
checkpoint_logger)
|
|
my_logger.start()
|
|
for testname in tests:
|
|
# Option 1: use checkblock
|
|
with checkpoint_logger.checkblock(testname):
|
|
# run the test method for testname
|
|
|
|
# Option 2: use checkpoint
|
|
start_time = time.time()
|
|
# run the test method for testname
|
|
checkpoint_logger.checkpoint(testname, start_time)
|
|
|
|
keyvals = my_logger.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 measurements
|
|
refresh: perform data samplings for every measurements
|
|
calc: calculates
|
|
save_results:
|
|
|
|
Private attributes:
|
|
_measurements: list of Measurement objects to be sampled.
|
|
_checkpoint_data: dictionary of (tname, tlist).
|
|
tname: String of testname associated with these time intervals
|
|
tlist: list of tuples. Tuple contains:
|
|
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, checkpoint_logger=None):
|
|
"""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._measurements = measurements
|
|
self.domains = [meas.domain for meas in self._measurements]
|
|
|
|
self._checkpoint_logger = \
|
|
checkpoint_logger if checkpoint_logger else CheckpointLogger()
|
|
|
|
self.done = False
|
|
|
|
def start(self):
|
|
self._checkpoint_logger.start()
|
|
super(MeasurementLogger, self).start()
|
|
|
|
def refresh(self):
|
|
"""Perform data samplings for every measurements.
|
|
|
|
Returns:
|
|
list of sampled data for every measurements.
|
|
"""
|
|
return [meas.refresh() for meas in self._measurements]
|
|
|
|
def run(self):
|
|
"""Threads run method."""
|
|
loop = 0
|
|
start_time = time.time()
|
|
while(not self.done):
|
|
# TODO (dbasehore): We probably need proper locking in this file
|
|
# since there have been race conditions with modifying and accessing
|
|
# data.
|
|
self.readings.append(self.refresh())
|
|
current_time = time.time()
|
|
self.times.append(current_time)
|
|
loop += 1
|
|
next_measurement_time = start_time + loop * self.seconds_period
|
|
time.sleep(next_measurement_time - current_time)
|
|
|
|
@contextlib.contextmanager
|
|
def checkblock(self, tname=''):
|
|
"""Check point for the following block with test tname.
|
|
|
|
Args:
|
|
tname: String of testname associated with this time interval
|
|
"""
|
|
start_time = time.time()
|
|
yield
|
|
self.checkpoint(tname, start_time)
|
|
|
|
def checkpoint(self, tname='', tstart=None, tend=None):
|
|
"""Just a thin method calling the CheckpointLogger checkpoint method.
|
|
|
|
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.
|
|
"""
|
|
self._checkpoint_logger.checkpoint(tname, tstart, tend)
|
|
|
|
# TODO(seankao): It might be useful to pull this method to CheckpointLogger,
|
|
# to allow checkpoint usage without an explicit MeasurementLogger.
|
|
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 = [('domain', 'mean', 'std', 'duration (s)', 'start ts',
|
|
'end ts')]
|
|
# TODO(coconutruben): ensure that values is meaningful i.e. go through
|
|
# the Loggers and add a unit attribute to each so that the raw
|
|
# data is readable.
|
|
raw_results = [('domain', 'values (%s)' % mtype)]
|
|
|
|
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_logger.checkpoint_data:
|
|
self._checkpoint_logger.checkpoint()
|
|
|
|
for i, domain_readings in enumerate(zip(*self.readings)):
|
|
meas = numpy.array(domain_readings)
|
|
try:
|
|
domain = self.domains[i]
|
|
except IndexError:
|
|
# TODO (evanbenn) temp logging for b:162610351
|
|
logging.debug('b:162610351 IndexError: %s, %d, %d, (%s)',
|
|
type(self).__name__,
|
|
len(self.readings),
|
|
len(self.domains),
|
|
', '.join(str(len(r)) for r in self.readings))
|
|
logging.debug('b:162610351 domains: %s',
|
|
', '.join(self.domains))
|
|
raise
|
|
|
|
for tname, tlist in self._checkpoint_logger.checkpoint_data.iteritems():
|
|
if tname:
|
|
prefix = '%s_%s' % (tname, domain)
|
|
else:
|
|
prefix = domain
|
|
keyvals[prefix+'_duration'] = 0
|
|
# Select all readings taken between tstart and tend
|
|
# timestamps in tlist.
|
|
masks = []
|
|
for tstart, tend in tlist:
|
|
keyvals[prefix+'_duration'] += tend - tstart
|
|
# Try block just in case
|
|
# code.google.com/p/chromium/issues/detail?id=318892
|
|
# is not fixed.
|
|
try:
|
|
masks.append(numpy.logical_and(tstart < t, t < tend))
|
|
except ValueError, e:
|
|
logging.debug('Error logging measurements: %s', str(e))
|
|
logging.debug('timestamps %d %s', t.len, t)
|
|
logging.debug('timestamp start, end %f %f', tstart, tend)
|
|
logging.debug('measurements %d %s', meas.len, meas)
|
|
mask = numpy.logical_or.reduce(masks)
|
|
meas_array = meas[mask]
|
|
|
|
# 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
|
|
# TODO(seankao): new results format?
|
|
result = (prefix, meas_mean, meas_std)
|
|
for tstart, tend in tlist:
|
|
result = result + (tend - tstart, tstart, tend)
|
|
results.append(result)
|
|
raw_results.append((prefix,) + tuple(meas_array.tolist()))
|
|
|
|
keyvals[prefix + '_' + mtype + '_avg'] = 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
|
|
self._raw_results = raw_results
|
|
return keyvals
|
|
|
|
|
|
def save_results(self, resultsdir, fname_prefix=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_prefix: prefix to use for fname. If provided outfiles
|
|
will be [fname]_[raw|summary].txt
|
|
"""
|
|
if not fname_prefix:
|
|
fname_prefix = 'meas_results_%.0f' % time.time()
|
|
fname = '%s_summary.txt' % fname_prefix
|
|
raw_fname = fname.replace('summary', 'raw')
|
|
for name, data in [(fname, self._results),
|
|
(raw_fname, self._raw_results)]:
|
|
with open(os.path.join(resultsdir, name), 'wt') as f:
|
|
# First row contains the headers
|
|
f.write('%s\n' % '\t'.join(data[0]))
|
|
for row in data[1:]:
|
|
# First column name, rest are numbers. See _calc_power()
|
|
fmt_row = [row[0]] + ['%.2f' % x for x in row[1:]]
|
|
f.write('%s\n' % '\t'.join(fmt_row))
|
|
|
|
|
|
class CPUStatsLogger(MeasurementLogger):
|
|
"""Class to measure CPU Frequency and CPU Idle Stats.
|
|
|
|
CPUStatsLogger derived from MeasurementLogger class but overload data
|
|
samplings method because MeasurementLogger assumed that each sampling is
|
|
independent to each other. However, in this case it is not. For example,
|
|
CPU time spent in C0 state is measure by time not spent in all other states.
|
|
|
|
CPUStatsLogger also collects the weight average in each time period if the
|
|
underlying AbstractStats support weight average function.
|
|
|
|
Private attributes:
|
|
_stats: list of CPU AbstractStats objects to be sampled.
|
|
_refresh_count: number of times refresh() has been called.
|
|
_last_wavg: dict of wavg when refresh() was last called.
|
|
"""
|
|
def __init__(self, seconds_period=1.0, checkpoint_logger=None):
|
|
"""Initialize a CPUStatsLogger.
|
|
|
|
Args:
|
|
seconds_period: float, probing interval in seconds. Default 1.0
|
|
"""
|
|
# We don't use measurements since CPU stats can't measure separately.
|
|
super(CPUStatsLogger, self).__init__([], seconds_period, checkpoint_logger)
|
|
|
|
self._stats = get_available_cpu_stats()
|
|
self._stats.append(GPUFreqStats())
|
|
self.domains = []
|
|
for stat in self._stats:
|
|
self.domains.extend([stat.name + '_' + str(state_name)
|
|
for state_name in stat.refresh()])
|
|
if stat.weighted_average():
|
|
self.domains.append('wavg_' + stat.name)
|
|
self._refresh_count = 0
|
|
self._last_wavg = collections.defaultdict(int)
|
|
|
|
def refresh(self):
|
|
self._refresh_count += 1
|
|
count = self._refresh_count
|
|
ret = []
|
|
for stat in self._stats:
|
|
ret.extend(stat.refresh().values())
|
|
wavg = stat.weighted_average()
|
|
if wavg:
|
|
if stat.incremental:
|
|
last_wavg = self._last_wavg[stat.name]
|
|
self._last_wavg[stat.name] = wavg
|
|
# Calculate weight average in this period using current
|
|
# total weight average and last total weight average.
|
|
# The result will lose some precision with higher number of
|
|
# count but still good enough for 11 significant digits even
|
|
# if we logged the data every 1 second for a day.
|
|
ret.append(wavg * count - last_wavg * (count - 1))
|
|
else:
|
|
ret.append(wavg)
|
|
return ret
|
|
|
|
def save_results(self, resultsdir, fname_prefix=None):
|
|
if not fname_prefix:
|
|
fname_prefix = 'cpu_results_%.0f' % time.time()
|
|
super(CPUStatsLogger, self).save_results(resultsdir, fname_prefix)
|
|
|
|
|
|
class PowerLogger(MeasurementLogger):
|
|
"""Class to measure power consumption."""
|
|
|
|
def __init__(self, measurements, seconds_period=1.0,
|
|
checkpoint_logger=None):
|
|
if not measurements:
|
|
measurements = self.create_measurements()
|
|
super(PowerLogger, self).__init__(measurements, seconds_period,
|
|
checkpoint_logger)
|
|
|
|
def create_measurements(self):
|
|
"""Create power measurements based on device config."""
|
|
# Import here to avoid import loop.
|
|
from autotest_lib.client.cros.power import power_rapl
|
|
|
|
measurements = []
|
|
status = get_status()
|
|
if status.battery:
|
|
measurements.append(BatteryStateOfCharge())
|
|
if status.battery_discharging():
|
|
measurements.append(SystemPower(status.battery_path))
|
|
if power_utils.has_powercap_support():
|
|
measurements += power_rapl.create_powercap()
|
|
elif power_utils.has_rapl_support():
|
|
measurements += power_rapl.create_rapl()
|
|
return measurements
|
|
|
|
def save_results(self, resultsdir, fname_prefix=None):
|
|
if not fname_prefix:
|
|
fname_prefix = 'power_results_%.0f' % time.time()
|
|
super(PowerLogger, self).save_results(resultsdir, fname_prefix)
|
|
|
|
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 BatteryTempMeasurement(TempMeasurement):
|
|
"""Class to measure battery temperature."""
|
|
def __init__(self):
|
|
super(BatteryTempMeasurement, self).__init__('battery', 'battery_temp')
|
|
|
|
|
|
def refresh(self):
|
|
"""Perform battery temperature reading.
|
|
|
|
Returns:
|
|
float, temperature in degrees Celsius.
|
|
"""
|
|
result = utils.run(self._path, timeout=5, ignore_status=True)
|
|
return float(result.stdout)
|
|
|
|
|
|
def has_battery_temp():
|
|
"""Determine if DUT can provide battery temperature.
|
|
|
|
Returns:
|
|
Boolean, True if battery temperature available. False otherwise.
|
|
"""
|
|
if not power_utils.has_battery():
|
|
return False
|
|
|
|
btemp = BatteryTempMeasurement()
|
|
try:
|
|
btemp.refresh()
|
|
except ValueError:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class TempLogger(MeasurementLogger):
|
|
"""A thread that logs temperature readings in millidegrees Celsius."""
|
|
|
|
def __init__(self, measurements, seconds_period=30.0,
|
|
checkpoint_logger=None):
|
|
if not measurements:
|
|
measurements = self.create_measurements()
|
|
super(TempLogger, self).__init__(measurements, seconds_period,
|
|
checkpoint_logger)
|
|
|
|
def create_measurements(self):
|
|
"""Create measurements for TempLogger."""
|
|
domains = set()
|
|
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)
|
|
domains.add(domain)
|
|
|
|
if has_battery_temp():
|
|
measurements.append(BatteryTempMeasurement())
|
|
|
|
sysfs_paths = '/sys/class/thermal/thermal_zone*'
|
|
paths = glob.glob(sysfs_paths)
|
|
for path in paths:
|
|
domain_path = os.path.join(path, 'type')
|
|
temp_path = os.path.join(path, 'temp')
|
|
|
|
domain = utils.read_one_line(domain_path)
|
|
|
|
# Skip when thermal_zone and hwmon have same domain.
|
|
if domain in domains:
|
|
continue
|
|
|
|
domain = domain.replace(' ', '_')
|
|
new_meas = TempMeasurement(domain, temp_path)
|
|
measurements.append(new_meas)
|
|
return measurements
|
|
|
|
def save_results(self, resultsdir, fname_prefix=None):
|
|
if not fname_prefix:
|
|
fname_prefix = 'temp_results_%.0f' % time.time()
|
|
super(TempLogger, self).save_results(resultsdir, fname_prefix)
|
|
|
|
|
|
def calc(self, mtype='temp'):
|
|
return super(TempLogger, self).calc(mtype)
|
|
|
|
|
|
class VideoFpsLogger(MeasurementLogger):
|
|
"""Class to measure Video FPS."""
|
|
|
|
@classmethod
|
|
def time_until_ready(cls, tab, num_video=1, timeout=120):
|
|
"""Wait until tab is ready for VideoFpsLogger and return time used.
|
|
|
|
Keep polling Chrome tab until these 2 conditions are met one by one.
|
|
- Number of <video> elements detected is equal to |num_video|.
|
|
- All videos are played for at least 1 ms.
|
|
|
|
Args:
|
|
tab: Chrome tab object
|
|
num_video: number of expected <video> elements, default 1.
|
|
timeout: timeout in seconds, default 120.
|
|
|
|
Returns:
|
|
float, number of seconds elasped until condition met.
|
|
|
|
Raises:
|
|
py_utils.TimeoutException if condition are not met by timeout.
|
|
"""
|
|
start_time = time.time()
|
|
|
|
# Number of <video> elements detected is equal to |num_video|.
|
|
c = 'document.getElementsByTagName("video").length == %d' % num_video
|
|
tab.WaitForJavaScriptCondition(c, timeout=timeout)
|
|
|
|
# All videos are played for at least 1 ms.
|
|
c = ('Math.min(...Array.from(document.getElementsByTagName("video"))'
|
|
'.map(v => v.currentTime)) >= 0.001')
|
|
timeout_left = timeout - (time.time() - start_time)
|
|
tab.WaitForJavaScriptCondition(c, timeout=timeout_left)
|
|
return time.time() - start_time
|
|
|
|
def __init__(self, tab, seconds_period=1.0, checkpoint_logger=None):
|
|
"""Initialize a VideoFpsLogger.
|
|
|
|
Args:
|
|
tab: Chrome tab object
|
|
"""
|
|
super(VideoFpsLogger, self).__init__([], seconds_period,
|
|
checkpoint_logger)
|
|
self._tab = tab
|
|
names = self._tab.EvaluateJavaScript(
|
|
'Array.from(document.getElementsByTagName("video")).map(v => v.id)')
|
|
self.domains = [n or 'video_' + str(i) for i, n in enumerate(names)]
|
|
self._last = [0] * len(names)
|
|
self.refresh()
|
|
|
|
def refresh(self):
|
|
@retry.retry(Exception, timeout_min=0.5, delay_sec=0.1)
|
|
def get_fps():
|
|
return self._tab.EvaluateJavaScript(
|
|
'Array.from(document.getElementsByTagName("video")).map('
|
|
'v => v.webkitDecodedFrameCount)')
|
|
current = get_fps()
|
|
fps = [(b - a if b >= a else b) / self.seconds_period
|
|
for a, b in zip(self._last , current)]
|
|
self._last = current
|
|
return fps
|
|
|
|
def save_results(self, resultsdir, fname_prefix=None):
|
|
if not fname_prefix:
|
|
fname_prefix = 'video_fps_results_%.0f' % time.time()
|
|
super(VideoFpsLogger, self).save_results(resultsdir, fname_prefix)
|
|
|
|
def calc(self, mtype='fps'):
|
|
return super(VideoFpsLogger, self).calc(mtype)
|
|
|
|
|
|
def get_num_fans():
|
|
"""Count how many fan DUT has.
|
|
|
|
Returns:
|
|
Integer, number of fans that DUT has.
|
|
"""
|
|
res = utils.run('ectool pwmgetnumfans | grep -o [0-9]', ignore_status=True)
|
|
if not res or res.exit_status != 0:
|
|
return 0
|
|
return int(res.stdout)
|
|
|
|
|
|
def has_fan():
|
|
"""Determine if DUT has fan.
|
|
|
|
Returns:
|
|
Boolean, True if dut has fan. False otherwise.
|
|
"""
|
|
return get_num_fans() > 0
|
|
|
|
|
|
class FanRpmLogger(MeasurementLogger):
|
|
"""Class to measure Fan RPM."""
|
|
|
|
def __init__(self, seconds_period=1.0, checkpoint_logger=None):
|
|
"""Initialize a FanRpmLogger."""
|
|
super(FanRpmLogger, self).__init__([], seconds_period,
|
|
checkpoint_logger)
|
|
self.domains = ['fan_' + str(i) for i in range(get_num_fans())]
|
|
self.refresh()
|
|
|
|
def refresh(self):
|
|
@retry.retry(Exception, timeout_min=0.1, delay_sec=2)
|
|
def get_fan_rpm_all():
|
|
"""Some example outputs from ectool
|
|
|
|
* Two fan system
|
|
localhost ~ # ectool pwmgetfanrpm all
|
|
Fan 0 RPM: 4012
|
|
Fan 1 RPM: 4009
|
|
|
|
* One fan but its stalled
|
|
localhost ~ # ectool pwmgetfanrpm all
|
|
Fan 0 stalled!
|
|
"""
|
|
cmd = 'ectool pwmgetfanrpm all'
|
|
res = utils.run(cmd, ignore_status=True,
|
|
stdout_tee=utils.TEE_TO_LOGS,
|
|
stderr_tee=utils.TEE_TO_LOGS)
|
|
rpm_data = []
|
|
for i, ln in enumerate(res.stdout.splitlines()):
|
|
if ln.find('stalled') != -1:
|
|
rpm_data.append(0)
|
|
else:
|
|
rpm_data.append(int(ln.split(':')[1]))
|
|
return rpm_data
|
|
return get_fan_rpm_all()
|
|
|
|
def save_results(self, resultsdir, fname_prefix=None):
|
|
if not fname_prefix:
|
|
fname_prefix = 'fan_rpm_results_%.0f' % time.time()
|
|
super(FanRpmLogger, self).save_results(resultsdir, fname_prefix)
|
|
|
|
def calc(self, mtype='rpm'):
|
|
return super(FanRpmLogger, self).calc(mtype)
|
|
|
|
|
|
def create_measurement_loggers(seconds_period=20.0, checkpoint_logger=None):
|
|
"""Create loggers for power test that is not test-specific.
|
|
|
|
Args:
|
|
seconds_period: float, probing interval in seconds. Default 20.0
|
|
checkpoint_logger: CheckpointLogger class for the loggers
|
|
|
|
Returns:
|
|
list of loggers created.
|
|
"""
|
|
loggers = [
|
|
PowerLogger(None, seconds_period, checkpoint_logger),
|
|
TempLogger(None, seconds_period, checkpoint_logger),
|
|
CPUStatsLogger(seconds_period, checkpoint_logger),
|
|
]
|
|
if has_fan():
|
|
loggers.append(FanRpmLogger(seconds_period, checkpoint_logger))
|
|
|
|
return loggers
|
|
|
|
|
|
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
|
|
|
|
def parse_pmc_s0ix_residency_info():
|
|
"""
|
|
Parses S0ix residency for PMC based Intel systems
|
|
(skylake/kabylake/apollolake), the debugfs paths might be
|
|
different from platform to platform, yet the format is
|
|
unified in microseconds.
|
|
|
|
@returns residency in seconds.
|
|
@raises error.TestNAError if the debugfs file not found.
|
|
"""
|
|
info_path = None
|
|
for node in ['/sys/kernel/debug/pmc_core/slp_s0_residency_usec',
|
|
'/sys/kernel/debug/telemetry/s0ix_residency_usec']:
|
|
if os.path.exists(node):
|
|
info_path = node
|
|
break
|
|
if not info_path:
|
|
raise error.TestNAError('S0ix residency file not found')
|
|
return float(utils.read_one_line(info_path)) * 1e-6
|
|
|
|
|
|
class S0ixResidencyStats(object):
|
|
"""
|
|
Measures the S0ix residency of a given board over time.
|
|
"""
|
|
def __init__(self):
|
|
self._initial_residency = parse_pmc_s0ix_residency_info()
|
|
|
|
def get_accumulated_residency_secs(self):
|
|
"""
|
|
@returns S0ix Residency since the class has been initialized.
|
|
"""
|
|
return parse_pmc_s0ix_residency_info() - self._initial_residency
|
|
|
|
|
|
class S2IdleStateStats(object):
|
|
"""
|
|
Usage stats of an s2idle state.
|
|
"""
|
|
|
|
def __init__(self, usage, time):
|
|
self.usage = usage
|
|
self.time = time
|
|
|
|
|
|
def get_s2idle_path(cpu, state):
|
|
path = os.path.join(CPU_BASE_PATH,
|
|
"cpu{}/cpuidle/{}/s2idle".format(cpu, state))
|
|
if not os.path.exists(path):
|
|
return None
|
|
|
|
return path
|
|
|
|
|
|
def get_s2idle_stats_for_state(cpu, state):
|
|
"""
|
|
Returns the s2idle stats for a given idle state of a CPU.
|
|
"""
|
|
s2idle_path = get_s2idle_path(cpu, state)
|
|
|
|
path = os.path.join(s2idle_path, 'usage')
|
|
if not os.path.exists(path):
|
|
raise error.TestFail("File not found: {}" % path)
|
|
|
|
usage = int(utils.read_one_line(path))
|
|
|
|
path = os.path.join(s2idle_path, 'time')
|
|
if not os.path.exists(path):
|
|
raise error.TestFail("File not found: {}" % path)
|
|
|
|
time = int(utils.read_one_line(path))
|
|
|
|
return S2IdleStateStats(usage, time)
|
|
|
|
|
|
def get_cpuidle_states(cpu):
|
|
"""
|
|
Returns the cpuidle states of a CPU.
|
|
"""
|
|
cpuidle_path = os.path.join(CPU_BASE_PATH, "cpu{}/cpuidle".format(cpu))
|
|
|
|
pattern = os.path.join(cpuidle_path, 'state*')
|
|
state_paths = glob.glob(pattern)
|
|
|
|
return [s.split('/')[-1] for s in state_paths]
|
|
|
|
|
|
def get_s2idle_stats_for_cpu(cpu):
|
|
"""
|
|
Returns the s2idle stats for a CPU.
|
|
"""
|
|
return {s: get_s2idle_stats_for_state(cpu, s)
|
|
for s in get_cpuidle_states(cpu)
|
|
if get_s2idle_path(cpu, s) is not None}
|
|
|
|
|
|
def get_s2idle_stats():
|
|
"""
|
|
Returns the s2idle stats for all CPUs.
|
|
"""
|
|
return {cpu: get_s2idle_stats_for_cpu(cpu) for cpu in get_online_cpus()}
|
|
|
|
|
|
def get_s2idle_residency_total_usecs():
|
|
"""
|
|
Get total s2idle residency time for all CPUs and states.
|
|
"""
|
|
total_usecs = 0
|
|
|
|
all_stats = get_s2idle_stats()
|
|
for stats in all_stats.itervalues():
|
|
for st in stats.itervalues():
|
|
total_usecs += st.time
|
|
|
|
return total_usecs
|
|
|
|
|
|
class S2IdleResidencyStats(object):
|
|
"""
|
|
Measures the s2idle residency of a given board over time.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._initial_residency = get_s2idle_residency_total_usecs()
|
|
|
|
def get_accumulated_residency_usecs(self):
|
|
"""
|
|
@returns s2idle residency since the class has been initialized.
|
|
"""
|
|
return get_s2idle_residency_total_usecs() - self._initial_residency
|
|
|
|
|
|
class DMCFirmwareStats(object):
|
|
"""
|
|
Collect DMC firmware stats of Intel based system (SKL+), (APL+).
|
|
"""
|
|
# Intel CPUs only transition to DC6 from DC5. https://git.io/vppcG
|
|
DC6_ENTRY_KEY = 'DC5 -> DC6 count'
|
|
|
|
def __init__(self):
|
|
self._initial_stat = DMCFirmwareStats._parse_dmc_info()
|
|
|
|
def check_fw_loaded(self):
|
|
"""Check that DMC firmware is loaded
|
|
|
|
@returns boolean of DMC firmware loaded.
|
|
"""
|
|
return self._initial_stat['fw loaded']
|
|
|
|
def is_dc6_supported(self):
|
|
"""Check that DMC support DC6 state."""
|
|
return self.DC6_ENTRY_KEY in self._initial_stat
|
|
|
|
def get_accumulated_dc6_entry(self):
|
|
"""Check number of DC6 state entry since the class has been initialized.
|
|
|
|
@returns number of DC6 state entry.
|
|
"""
|
|
if not self.is_dc6_supported():
|
|
return 0
|
|
|
|
key = self.DC6_ENTRY_KEY
|
|
current_stat = DMCFirmwareStats._parse_dmc_info()
|
|
return current_stat[key] - self._initial_stat[key]
|
|
|
|
@staticmethod
|
|
def _parse_dmc_info():
|
|
"""
|
|
Parses DMC firmware info for Intel based systems.
|
|
|
|
@returns dictionary of dmc_fw info
|
|
@raises error.TestFail if the debugfs file not found.
|
|
"""
|
|
path = '/sys/kernel/debug/dri/0/i915_dmc_info'
|
|
if not os.path.exists(path):
|
|
raise error.TestFail('DMC info file not found.')
|
|
|
|
with open(path, 'r') as f:
|
|
lines = [line.strip() for line in f.readlines()]
|
|
|
|
# For pre 4.16 kernel. https://git.io/vhThb
|
|
if lines[0] == 'not supported':
|
|
raise error.TestFail('DMC not supported.')
|
|
|
|
ret = dict()
|
|
for line in lines:
|
|
key, val = line.rsplit(': ', 1)
|
|
|
|
if key == 'fw loaded':
|
|
val = val == 'yes'
|
|
elif re.match(r'DC\d -> DC\d count', key):
|
|
val = int(val)
|
|
ret[key] = val
|
|
return ret
|
|
|
|
|
|
class RC6ResidencyStats(object):
|
|
"""
|
|
Collect RC6 residency stats of Intel based system.
|
|
"""
|
|
def __init__(self):
|
|
self._rc6_enable_checked = False
|
|
self._previous_stat = self._parse_rc6_residency_info()
|
|
self._accumulated_stat = 0
|
|
|
|
# Setup max RC6 residency count for modern chips. The table order
|
|
# is in small/big-core first, follows by the uarch name. We don't
|
|
# need to deal with the wraparound for devices with v4.17+ kernel
|
|
# which has the commit 817cc0791823 ("drm/i915: Handle RC6 counter wrap").
|
|
cpu_uarch = utils.get_intel_cpu_uarch()
|
|
self._max_counter = {
|
|
# Small-core w/ GEN9 LP graphics
|
|
'Airmont': 3579125,
|
|
'Goldmont': 3579125,
|
|
# Big-core
|
|
'Broadwell': 5497558,
|
|
'Haswell': 5497558,
|
|
'Kaby Lake': 5497558,
|
|
'Skylake': 5497558,
|
|
}.get(cpu_uarch, None)
|
|
|
|
def get_accumulated_residency_msecs(self):
|
|
"""Check number of RC6 state entry since the class has been initialized.
|
|
|
|
@returns int of RC6 residency in milliseconds since instantiation.
|
|
"""
|
|
current_stat = self._parse_rc6_residency_info()
|
|
|
|
# The problem here is that we cannot assume the rc6_residency_ms is
|
|
# monotonically increasing by current kernel i915 implementation.
|
|
#
|
|
# Considering different hardware has different wraparound period,
|
|
# this is a mitigation plan to deal with different wraparound period
|
|
# on various platforms, in order to make the test platform agnostic.
|
|
#
|
|
# This scarifes the accuracy of RC6 residency a bit, up on the calling
|
|
# period.
|
|
#
|
|
# Reference: Bug 94852 - [SKL] rc6_residency_ms unreliable
|
|
# (https://bugs.freedesktop.org/show_bug.cgi?id=94852)
|
|
#
|
|
# However for modern processors with a known overflow count, apply
|
|
# constant of RC6 max counter to improve accuracy.
|
|
#
|
|
# Note that the max counter is bound for sysfs overflow, while the
|
|
# accumulated residency here is the diff against the first reading.
|
|
if current_stat < self._previous_stat:
|
|
if self._max_counter is None:
|
|
logging.warning('GPU: Detect rc6_residency_ms wraparound')
|
|
self._accumulated_stat += current_stat
|
|
else:
|
|
self._accumulated_stat += current_stat + (self._max_counter -
|
|
self._previous_stat)
|
|
else:
|
|
self._accumulated_stat += current_stat - self._previous_stat
|
|
|
|
self._previous_stat = current_stat
|
|
return self._accumulated_stat
|
|
|
|
def _is_rc6_enable(self):
|
|
"""
|
|
Verified that RC6 is enable.
|
|
|
|
@returns Boolean of RC6 enable status.
|
|
@raises error.TestFail if the sysfs file not found.
|
|
"""
|
|
path = '/sys/class/drm/card0/power/rc6_enable'
|
|
if not os.path.exists(path):
|
|
raise error.TestFail('RC6 enable file not found.')
|
|
|
|
return (int(utils.read_one_line(path)) & 0x1) == 0x1
|
|
|
|
def _parse_rc6_residency_info(self):
|
|
"""
|
|
Parses RC6 residency info for Intel based systems.
|
|
|
|
@returns int of RC6 residency in millisec since boot.
|
|
@raises error.TestFail if the sysfs file not found or RC6 not enabled.
|
|
"""
|
|
if not self._rc6_enable_checked:
|
|
if not self._is_rc6_enable():
|
|
raise error.TestFail('RC6 is not enabled.')
|
|
self._rc6_enable_checked = True
|
|
|
|
path = '/sys/class/drm/card0/power/rc6_residency_ms'
|
|
if not os.path.exists(path):
|
|
raise error.TestFail('RC6 residency file not found.')
|
|
|
|
return int(utils.read_one_line(path))
|
|
|
|
|
|
class PCHPowergatingStats(object):
|
|
"""
|
|
Collect PCH powergating status of intel based system.
|
|
"""
|
|
PMC_CORE_PATH = '/sys/kernel/debug/pmc_core/pch_ip_power_gating_status'
|
|
TELEMETRY_PATH = '/sys/kernel/debug/telemetry/soc_states'
|
|
|
|
def __init__(self):
|
|
self._stat = {}
|
|
|
|
def check_s0ix_requirement(self):
|
|
"""
|
|
Check PCH powergating status with S0ix requirement.
|
|
|
|
@returns list of PCH IP block name that need to be powergated for low
|
|
power consumption S0ix, empty list if none.
|
|
"""
|
|
# PCH IP block that is on for S0ix. Ignore these IP block.
|
|
S0IX_ALLOWLIST = set([
|
|
'PMC', 'OPI-DMI', 'SPI / eSPI', 'XHCI', 'xHCI', 'FUSE', 'Fuse',
|
|
'PCIE0', 'NPKVRC', 'NPKVNN', 'NPK_VNN', 'PSF1', 'PSF2', 'PSF3',
|
|
'PSF4', 'SBR0', 'SBR1', 'SBR2', 'SBR4', 'SBR5', 'SBR6', 'SBR7'])
|
|
|
|
# PCH IP block that is on/off for S0ix depend on features enabled.
|
|
# Add log when these IPs state are on.
|
|
S0IX_WARNLIST = set([
|
|
'HDA-PGD0', 'HDA-PGD1', 'HDA-PGD2', 'HDA-PGD3', 'LPSS',
|
|
'AVSPGD1', 'AVSPGD4'])
|
|
|
|
# CNV device has 0x31dc as devid .
|
|
if len(utils.system_output('lspci -d :31dc')) > 0:
|
|
S0IX_ALLOWLIST.add('CNV')
|
|
|
|
# HrP2 device has 0x02f0(CML) or 0x4df0(JSL) as devid.
|
|
if (len(utils.system_output('lspci -d :02f0')) > 0 or
|
|
len(utils.system_output('lspci -d :4df0')) > 0):
|
|
S0IX_ALLOWLIST.update(['CNVI', 'NPK_AON'])
|
|
|
|
on_ip = set(ip['name'] for ip in self._stat if ip['state'])
|
|
on_ip -= S0IX_ALLOWLIST
|
|
|
|
if on_ip:
|
|
on_ip_in_warn_list = on_ip & S0IX_WARNLIST
|
|
if on_ip_in_warn_list:
|
|
logging.warn('Found PCH IP that may be able to powergate: %s',
|
|
', '.join(on_ip_in_warn_list))
|
|
on_ip -= S0IX_WARNLIST
|
|
|
|
if on_ip:
|
|
logging.error('Found PCH IP that need to powergate: %s',
|
|
', '.join(on_ip))
|
|
return on_ip
|
|
return []
|
|
|
|
def read_pch_powergating_info(self, sleep_seconds=1):
|
|
"""
|
|
Read PCH powergating status info for Intel based systems.
|
|
|
|
Intel currently shows powergating status in 2 different place in debugfs
|
|
depend on which CPU platform.
|
|
|
|
@param sleep_seconds: sleep time to make DUT idle before read the data.
|
|
|
|
@raises error.TestFail if the debugfs file not found or parsing error.
|
|
"""
|
|
if os.path.exists(self.PMC_CORE_PATH):
|
|
logging.info('Use PCH powergating info at %s', self.PMC_CORE_PATH)
|
|
time.sleep(sleep_seconds)
|
|
self._read_pcm_core_powergating_info()
|
|
return
|
|
|
|
if os.path.exists(self.TELEMETRY_PATH):
|
|
logging.info('Use PCH powergating info at %s', self.TELEMETRY_PATH)
|
|
time.sleep(sleep_seconds)
|
|
self._read_telemetry_powergating_info()
|
|
return
|
|
|
|
raise error.TestFail('PCH powergating info file not found.')
|
|
|
|
def _read_pcm_core_powergating_info(self):
|
|
"""
|
|
read_pch_powergating_info() for Intel Core KBL+
|
|
|
|
@raises error.TestFail if parsing error.
|
|
"""
|
|
with open(self.PMC_CORE_PATH, 'r') as f:
|
|
lines = [line.strip() for line in f.readlines()]
|
|
|
|
# Example pattern to match:
|
|
# PCH IP: 0 - PMC State: On
|
|
# PCH IP: 1 - SATA State: Off
|
|
pattern = r'PCH IP:\s+(?P<id>\d+)\s+' \
|
|
r'- (?P<name>.*\w)\s+' \
|
|
r'State: (?P<state>Off|On)'
|
|
matcher = re.compile(pattern)
|
|
ret = []
|
|
for i, line in enumerate(lines):
|
|
match = matcher.match(line)
|
|
if not match:
|
|
raise error.TestFail('Can not parse PCH powergating info: ',
|
|
line)
|
|
|
|
index = int(match.group('id'))
|
|
if i != index:
|
|
raise error.TestFail('Wrong index for PCH powergating info: ',
|
|
line)
|
|
|
|
name = match.group('name')
|
|
state = match.group('state') == 'On'
|
|
|
|
ret.append({'name': name, 'state': state})
|
|
self._stat = ret
|
|
|
|
def _read_telemetry_powergating_info(self):
|
|
"""
|
|
read_pch_powergating_info() for Intel Atom APL+
|
|
|
|
@raises error.TestFail if parsing error.
|
|
"""
|
|
with open(self.TELEMETRY_PATH, 'r') as f:
|
|
raw_str = f.read()
|
|
|
|
# Example pattern to match:
|
|
# --------------------------------------
|
|
# South Complex PowerGate Status
|
|
# --------------------------------------
|
|
# Device PG
|
|
# LPSS 1
|
|
# SPI 1
|
|
# FUSE 0
|
|
#
|
|
# ---------------------------------------
|
|
trimed_pattern = r'.*South Complex PowerGate Status\n' \
|
|
r'-+\n' \
|
|
r'Device\s+PG\n' \
|
|
r'(?P<trimmed_section>(\w+\s+[0|1]\n)+)' \
|
|
r'\n-+\n.*'
|
|
trimed_match = re.match(trimed_pattern, raw_str, re.DOTALL)
|
|
if not trimed_match:
|
|
raise error.TestFail('Can not parse PCH powergating info: ',
|
|
raw_str)
|
|
trimmed_str = trimed_match.group('trimmed_section').strip()
|
|
lines = [line.strip() for line in trimmed_str.split('\n')]
|
|
|
|
matcher = re.compile(r'(?P<name>\w+)\s+(?P<state>[0|1])')
|
|
ret = []
|
|
for line in lines:
|
|
match = matcher.match(line)
|
|
if not match:
|
|
raise error.TestFail('Can not parse PCH powergating info: %s',
|
|
line)
|
|
|
|
name = match.group('name')
|
|
state = match.group('state') == '0' # 0 means on and 1 means off
|
|
|
|
ret.append({'name': name, 'state': state})
|
|
self._stat = ret
|
|
|
|
def has_rc6_support():
|
|
"""
|
|
Helper to examine that RC6 is enabled with residency counter.
|
|
|
|
@returns Boolean of RC6 support status.
|
|
"""
|
|
enable_path = '/sys/class/drm/card0/power/rc6_enable'
|
|
residency_path = '/sys/class/drm/card0/power/rc6_residency_ms'
|
|
|
|
has_rc6_enabled = os.path.exists(enable_path)
|
|
has_rc6_residency = False
|
|
rc6_enable_mask = 0
|
|
|
|
if has_rc6_enabled:
|
|
# TODO (harry.pan): Some old chip has RC6P and RC6PP
|
|
# in the bits[1:2]; in case of that, ideally these time
|
|
# slice will fall into RC0, fix it up if required.
|
|
rc6_enable_mask = int(utils.read_one_line(enable_path))
|
|
has_rc6_enabled &= (rc6_enable_mask) & 0x1 == 0x1
|
|
has_rc6_residency = os.path.exists(residency_path)
|
|
|
|
logging.debug("GPU: RC6 residency support: %s, mask: 0x%x",
|
|
{True: "yes", False: "no"} [has_rc6_enabled and has_rc6_residency],
|
|
rc6_enable_mask)
|
|
|
|
return (has_rc6_enabled and has_rc6_residency)
|
|
|
|
class GPURC6Stats(AbstractStats):
|
|
"""
|
|
GPU RC6 statistics to give ratio of RC6 and RC0 residency
|
|
|
|
Protected Attributes:
|
|
_rc6: object of RC6ResidencyStats
|
|
"""
|
|
def __init__(self):
|
|
self._rc6 = RC6ResidencyStats()
|
|
super(GPURC6Stats, self).__init__(name='gpuidle')
|
|
|
|
def _read_stats(self):
|
|
total = int(time.time() * 1000)
|
|
msecs = self._rc6.get_accumulated_residency_msecs()
|
|
stats = collections.defaultdict(int)
|
|
stats['RC6'] += msecs
|
|
stats['RC0'] += total - msecs
|
|
logging.debug("GPU: RC6 residency: %d ms", msecs)
|
|
return stats
|