xref: /aosp_15_r20/external/autotest/client/cros/power/power_status.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Lifrom __future__ import absolute_import
7*9c5db199SXin Lifrom __future__ import division
8*9c5db199SXin Lifrom __future__ import print_function
9*9c5db199SXin Li
10*9c5db199SXin Liimport collections
11*9c5db199SXin Liimport contextlib
12*9c5db199SXin Liimport ctypes
13*9c5db199SXin Liimport fcntl
14*9c5db199SXin Liimport glob
15*9c5db199SXin Liimport itertools
16*9c5db199SXin Liimport json
17*9c5db199SXin Liimport logging
18*9c5db199SXin Liimport math
19*9c5db199SXin Liimport numpy
20*9c5db199SXin Liimport os
21*9c5db199SXin Liimport re
22*9c5db199SXin Liimport struct
23*9c5db199SXin Liimport threading
24*9c5db199SXin Liimport time
25*9c5db199SXin Li
26*9c5db199SXin Lifrom autotest_lib.client.bin import utils
27*9c5db199SXin Lifrom autotest_lib.client.common_lib import autotest_enum
28*9c5db199SXin Lifrom autotest_lib.client.common_lib import error
29*9c5db199SXin Lifrom autotest_lib.client.common_lib.cros import retry
30*9c5db199SXin Lifrom autotest_lib.client.common_lib.utils import poll_for_condition_ex
31*9c5db199SXin Lifrom autotest_lib.client.cros import kernel_trace
32*9c5db199SXin Lifrom autotest_lib.client.cros.power import power_utils
33*9c5db199SXin Lifrom collections import namedtuple
34*9c5db199SXin Lifrom six.moves import range
35*9c5db199SXin Lifrom six.moves import zip
36*9c5db199SXin Li
37*9c5db199SXin LiBatteryDataReportType = autotest_enum.AutotestEnum('CHARGE', 'ENERGY')
38*9c5db199SXin Li
39*9c5db199SXin Li# battery data reported at 1e6 scale
40*9c5db199SXin LiBATTERY_DATA_SCALE = 1e6
41*9c5db199SXin Li# number of times to retry reading the battery in the case of bad data
42*9c5db199SXin LiBATTERY_RETRY_COUNT = 3
43*9c5db199SXin Li# default filename when saving CheckpointLogger data to file
44*9c5db199SXin LiCHECKPOINT_LOG_DEFAULT_FNAME = 'checkpoint_log.json'
45*9c5db199SXin Li
46*9c5db199SXin Li
47*9c5db199SXin Liclass DevStat(object):
48*9c5db199SXin Li    """
49*9c5db199SXin Li    Device power status. This class implements generic status initialization
50*9c5db199SXin Li    and parsing routines.
51*9c5db199SXin Li    """
52*9c5db199SXin Li
53*9c5db199SXin Li    def __init__(self, fields, path=None):
54*9c5db199SXin Li        self.fields = fields
55*9c5db199SXin Li        self.path = path
56*9c5db199SXin Li
57*9c5db199SXin Li
58*9c5db199SXin Li    def reset_fields(self):
59*9c5db199SXin Li        """
60*9c5db199SXin Li        Reset all class fields to None to mark their status as unknown.
61*9c5db199SXin Li        """
62*9c5db199SXin Li        for field in self.fields.keys():
63*9c5db199SXin Li            setattr(self, field, None)
64*9c5db199SXin Li
65*9c5db199SXin Li
66*9c5db199SXin Li    def read_val(self,  file_name, field_type):
67*9c5db199SXin Li        """Read a value from file.
68*9c5db199SXin Li        """
69*9c5db199SXin Li        try:
70*9c5db199SXin Li            path = file_name
71*9c5db199SXin Li            if not file_name.startswith('/'):
72*9c5db199SXin Li                path = os.path.join(self.path, file_name)
73*9c5db199SXin Li            f = open(path, 'r')
74*9c5db199SXin Li            out = f.readline().rstrip('\n')
75*9c5db199SXin Li            val = field_type(out)
76*9c5db199SXin Li            return val
77*9c5db199SXin Li
78*9c5db199SXin Li        except:
79*9c5db199SXin Li            return field_type(0)
80*9c5db199SXin Li
81*9c5db199SXin Li
82*9c5db199SXin Li    def read_all_vals(self):
83*9c5db199SXin Li        """Read all values.
84*9c5db199SXin Li        """
85*9c5db199SXin Li        for field, prop in self.fields.items():
86*9c5db199SXin Li            if prop[0]:
87*9c5db199SXin Li                val = self.read_val(prop[0], prop[1])
88*9c5db199SXin Li                setattr(self, field, val)
89*9c5db199SXin Li
90*9c5db199SXin Li    def update(self):
91*9c5db199SXin Li        """Update the DevStat.
92*9c5db199SXin Li
93*9c5db199SXin Li        Need to implement in subclass.
94*9c5db199SXin Li        """
95*9c5db199SXin Li        pass
96*9c5db199SXin Li
97*9c5db199SXin Liclass ThermalStatACPI(DevStat):
98*9c5db199SXin Li    """
99*9c5db199SXin Li    ACPI-based thermal status.
100*9c5db199SXin Li
101*9c5db199SXin Li    Fields:
102*9c5db199SXin Li    (All temperatures are in millidegrees Celsius.)
103*9c5db199SXin Li
104*9c5db199SXin Li    str   enabled:            Whether thermal zone is enabled
105*9c5db199SXin Li    int   temp:               Current temperature
106*9c5db199SXin Li    str   type:               Thermal zone type
107*9c5db199SXin Li    int   num_trip_points:    Number of thermal trip points that activate
108*9c5db199SXin Li                                cooling devices
109*9c5db199SXin Li    int   num_points_tripped: Temperature is above this many trip points
110*9c5db199SXin Li    str   trip_point_N_type:  Trip point #N's type
111*9c5db199SXin Li    int   trip_point_N_temp:  Trip point #N's temperature value
112*9c5db199SXin Li    int   cdevX_trip_point:   Trip point o cooling device #X (index)
113*9c5db199SXin Li    """
114*9c5db199SXin Li
115*9c5db199SXin Li    MAX_TRIP_POINTS = 20
116*9c5db199SXin Li
117*9c5db199SXin Li    thermal_fields = {
118*9c5db199SXin Li        'enabled':              ['enabled', str],
119*9c5db199SXin Li        'temp':                 ['temp', int],
120*9c5db199SXin Li        'type':                 ['type', str],
121*9c5db199SXin Li        'num_points_tripped':   ['', '']
122*9c5db199SXin Li        }
123*9c5db199SXin Li    path = '/sys/class/thermal/thermal_zone*'
124*9c5db199SXin Li
125*9c5db199SXin Li    def __init__(self, path=None):
126*9c5db199SXin Li        # Browse the thermal folder for trip point fields.
127*9c5db199SXin Li        self.num_trip_points = 0
128*9c5db199SXin Li
129*9c5db199SXin Li        if path is None:
130*9c5db199SXin Li            path = ThermalStatACPI.path
131*9c5db199SXin Li
132*9c5db199SXin Li        self.zones = {}
133*9c5db199SXin Li        thermal_zones = glob.glob(path)
134*9c5db199SXin Li        for (i, zone) in enumerate(thermal_zones):
135*9c5db199SXin Li            desc_path = os.path.join(zone, 'device/description')
136*9c5db199SXin Li            desc = ''
137*9c5db199SXin Li            if os.path.exists(desc_path):
138*9c5db199SXin Li                desc = utils.read_one_line(desc_path)
139*9c5db199SXin Li
140*9c5db199SXin Li            # If there's no description then use the type to create a description
141*9c5db199SXin Li            if desc == '':
142*9c5db199SXin Li                domain_path = os.path.join(zone, 'type')
143*9c5db199SXin Li                domain = utils.read_one_line(domain_path)
144*9c5db199SXin Li                desc = '%s%d' % (domain, i)
145*9c5db199SXin Li
146*9c5db199SXin Li            desc = desc.replace(' ', '_')
147*9c5db199SXin Li            self.zones[desc] = os.path.join(zone, 'temp')
148*9c5db199SXin Li
149*9c5db199SXin Li        thermal_fields = glob.glob(path + '/*')
150*9c5db199SXin Li        for file in thermal_fields:
151*9c5db199SXin Li            field = file[len(path + '/'):]
152*9c5db199SXin Li            if field.find('trip_point') != -1:
153*9c5db199SXin Li                if field.find('temp'):
154*9c5db199SXin Li                    field_type = int
155*9c5db199SXin Li                else:
156*9c5db199SXin Li                    field_type = str
157*9c5db199SXin Li                self.thermal_fields[field] = [field, field_type]
158*9c5db199SXin Li
159*9c5db199SXin Li                # Count the number of trip points.
160*9c5db199SXin Li                if field.find('_type') != -1:
161*9c5db199SXin Li                    self.num_trip_points += 1
162*9c5db199SXin Li
163*9c5db199SXin Li        super(ThermalStatACPI, self).__init__(self.thermal_fields, path)
164*9c5db199SXin Li        self.update()
165*9c5db199SXin Li
166*9c5db199SXin Li    def update(self):
167*9c5db199SXin Li        if not os.path.exists(self.path):
168*9c5db199SXin Li            return
169*9c5db199SXin Li
170*9c5db199SXin Li        self.read_all_vals()
171*9c5db199SXin Li        self.num_points_tripped = 0
172*9c5db199SXin Li
173*9c5db199SXin Li        for field in self.thermal_fields:
174*9c5db199SXin Li            if field.find('trip_point_') != -1 and field.find('_temp') != -1 \
175*9c5db199SXin Li                    and self.temp > self.read_val(field, int):
176*9c5db199SXin Li                self.num_points_tripped += 1
177*9c5db199SXin Li                logging.info('Temperature trip point #%s tripped.', \
178*9c5db199SXin Li                            field[len('trip_point_'):field.rfind('_temp')])
179*9c5db199SXin Li
180*9c5db199SXin Li
181*9c5db199SXin Liclass ThermalStatHwmon(DevStat):
182*9c5db199SXin Li    """
183*9c5db199SXin Li    hwmon-based thermal status. Excludes overlaps with thermal zones by default
184*9c5db199SXin Li    since thermal zones generally provide a more usable description.
185*9c5db199SXin Li
186*9c5db199SXin Li    Fields:
187*9c5db199SXin Li    int   <tname>_temp<num>_input: Current temperature in millidegrees Celsius
188*9c5db199SXin Li      where:
189*9c5db199SXin Li          <tname> : name of hwmon device in sysfs
190*9c5db199SXin Li          <num>   : number of temp as some hwmon devices have multiple
191*9c5db199SXin Li
192*9c5db199SXin Li    """
193*9c5db199SXin Li    path = '/sys/class/hwmon'
194*9c5db199SXin Li
195*9c5db199SXin Li    thermal_fields = {}
196*9c5db199SXin Li
197*9c5db199SXin Li    def __init__(self, rootpath=None, exclude_tz=True):
198*9c5db199SXin Li        excluded_domains = set()
199*9c5db199SXin Li        if exclude_tz:
200*9c5db199SXin Li            thermal_zones = glob.glob('/sys/class/thermal/thermal_zone*')
201*9c5db199SXin Li            for zone in thermal_zones:
202*9c5db199SXin Li                domain_path = os.path.join(zone, 'type')
203*9c5db199SXin Li                domain = utils.read_one_line(domain_path)
204*9c5db199SXin Li
205*9c5db199SXin Li                excluded_domains.add(domain)
206*9c5db199SXin Li
207*9c5db199SXin Li        if not rootpath:
208*9c5db199SXin Li            rootpath = self.path
209*9c5db199SXin Li        for subpath1 in glob.glob('%s/hwmon*' % rootpath):
210*9c5db199SXin Li            for subpath2 in ['','device/']:
211*9c5db199SXin Li                gpaths = glob.glob("%s/%stemp*_input" % (subpath1, subpath2))
212*9c5db199SXin Li                for gpath in gpaths:
213*9c5db199SXin Li                    bname = os.path.basename(gpath)
214*9c5db199SXin Li                    field_path = os.path.join(subpath1, subpath2, bname)
215*9c5db199SXin Li
216*9c5db199SXin Li                    domain_path = os.path.join(os.path.dirname(gpath), "name")
217*9c5db199SXin Li                    domain = utils.read_one_line(domain_path)
218*9c5db199SXin Li
219*9c5db199SXin Li                    if domain in excluded_domains:
220*9c5db199SXin Li                        continue
221*9c5db199SXin Li
222*9c5db199SXin Li                    field_key = "%s_%s" % (domain, bname)
223*9c5db199SXin Li                    self.thermal_fields[field_key] = [field_path, int]
224*9c5db199SXin Li
225*9c5db199SXin Li        super(ThermalStatHwmon, self).__init__(self.thermal_fields, rootpath)
226*9c5db199SXin Li        self.update()
227*9c5db199SXin Li
228*9c5db199SXin Li    def update(self):
229*9c5db199SXin Li        if not os.path.exists(self.path):
230*9c5db199SXin Li            return
231*9c5db199SXin Li
232*9c5db199SXin Li        self.read_all_vals()
233*9c5db199SXin Li
234*9c5db199SXin Li    def read_val(self,  file_name, field_type):
235*9c5db199SXin Li        try:
236*9c5db199SXin Li            path = os.path.join(self.path, file_name)
237*9c5db199SXin Li            f = open(path, 'r')
238*9c5db199SXin Li            out = f.readline()
239*9c5db199SXin Li            return field_type(out)
240*9c5db199SXin Li        except:
241*9c5db199SXin Li            return field_type(0)
242*9c5db199SXin Li
243*9c5db199SXin Li
244*9c5db199SXin Liclass ThermalStat(object):
245*9c5db199SXin Li    """helper class to instantiate various thermal devices."""
246*9c5db199SXin Li    def __init__(self):
247*9c5db199SXin Li        self._thermals = []
248*9c5db199SXin Li        self.min_temp = 999999999
249*9c5db199SXin Li        self.max_temp = -999999999
250*9c5db199SXin Li
251*9c5db199SXin Li        thermal_stat_types = [(ThermalStatHwmon.path, ThermalStatHwmon),
252*9c5db199SXin Li                              (ThermalStatACPI.path, ThermalStatACPI)]
253*9c5db199SXin Li        for thermal_glob_path, thermal_type in thermal_stat_types:
254*9c5db199SXin Li            try:
255*9c5db199SXin Li                thermal_path = glob.glob(thermal_glob_path)[0]
256*9c5db199SXin Li                logging.debug('Using %s for thermal info.', thermal_path)
257*9c5db199SXin Li                self._thermals.append(thermal_type(thermal_path))
258*9c5db199SXin Li            except:
259*9c5db199SXin Li                logging.debug('Could not find thermal path %s, skipping.',
260*9c5db199SXin Li                              thermal_glob_path)
261*9c5db199SXin Li
262*9c5db199SXin Li
263*9c5db199SXin Li    def get_temps(self):
264*9c5db199SXin Li        """Get temperature readings.
265*9c5db199SXin Li
266*9c5db199SXin Li        Returns:
267*9c5db199SXin Li            string of temperature readings.
268*9c5db199SXin Li        """
269*9c5db199SXin Li        temp_str = ''
270*9c5db199SXin Li        for thermal in self._thermals:
271*9c5db199SXin Li            thermal.update()
272*9c5db199SXin Li            for kname in thermal.fields:
273*9c5db199SXin Li                if kname is 'temp' or kname.endswith('_input'):
274*9c5db199SXin Li                    val = getattr(thermal, kname)
275*9c5db199SXin Li                    temp_str += '%s:%d ' % (kname, val)
276*9c5db199SXin Li                    if val > self.max_temp:
277*9c5db199SXin Li                        self.max_temp = val
278*9c5db199SXin Li                    if val < self.min_temp:
279*9c5db199SXin Li                        self.min_temp = val
280*9c5db199SXin Li
281*9c5db199SXin Li
282*9c5db199SXin Li        return temp_str
283*9c5db199SXin Li
284*9c5db199SXin Li
285*9c5db199SXin Liclass BatteryStat(DevStat):
286*9c5db199SXin Li    """
287*9c5db199SXin Li    Battery status.
288*9c5db199SXin Li
289*9c5db199SXin Li    Fields:
290*9c5db199SXin Li
291*9c5db199SXin Li    float charge_full:        Last full capacity reached [Ah]
292*9c5db199SXin Li    float charge_full_design: Full capacity by design [Ah]
293*9c5db199SXin Li    float charge_now:         Remaining charge [Ah]
294*9c5db199SXin Li    float current_now:        Battery discharge rate [A]
295*9c5db199SXin Li    int   cycle_count:        Battery cycle count
296*9c5db199SXin Li    float energy:             Current battery charge [Wh]
297*9c5db199SXin Li    float energy_full:        Last full capacity reached [Wh]
298*9c5db199SXin Li    float energy_full_design: Full capacity by design [Wh]
299*9c5db199SXin Li    float energy_rate:        Battery discharge rate [W]
300*9c5db199SXin Li    str   manufacturer:       Battery manufacturer
301*9c5db199SXin Li    str   model_name:         Battery model name
302*9c5db199SXin Li    float power_now:          Battery discharge rate [W]
303*9c5db199SXin Li    int   present:            Whether battery is present
304*9c5db199SXin Li    float remaining_time:     Remaining discharging time [h]
305*9c5db199SXin Li    str   serial_number:      Battery serial number
306*9c5db199SXin Li    str   status:             Charging status
307*9c5db199SXin Li    float voltage_min_design: Minimum voltage by design [V]
308*9c5db199SXin Li    float voltage_max_design: Maximum voltage by design [V]
309*9c5db199SXin Li    float voltage_now:        Voltage now [V]
310*9c5db199SXin Li    """
311*9c5db199SXin Li
312*9c5db199SXin Li    battery_fields = {
313*9c5db199SXin Li            'status': ['status', str],
314*9c5db199SXin Li            'charge_full': ['charge_full', float],
315*9c5db199SXin Li            'charge_full_design': ['charge_full_design', float],
316*9c5db199SXin Li            'charge_now': ['charge_now', float],
317*9c5db199SXin Li            'current_now': ['current_now', float],
318*9c5db199SXin Li            'cycle_count': ['cycle_count', int],
319*9c5db199SXin Li            'voltage_min_design': ['voltage_min_design', float],
320*9c5db199SXin Li            'voltage_max_design': ['voltage_max_design', float],
321*9c5db199SXin Li            'voltage_now': ['voltage_now', float],
322*9c5db199SXin Li            'energy': ['energy_now', float],
323*9c5db199SXin Li            'energy_full': ['energy_full', float],
324*9c5db199SXin Li            'energy_full_design': ['energy_full_design', float],
325*9c5db199SXin Li            'power_now': ['power_now', float],
326*9c5db199SXin Li            'present': ['present', int],
327*9c5db199SXin Li            'manufacturer': ['manufacturer', str],
328*9c5db199SXin Li            'model_name': ['model_name', str],
329*9c5db199SXin Li            'serial_number': ['serial_number', str],
330*9c5db199SXin Li            'energy_rate': ['', ''],
331*9c5db199SXin Li            'remaining_time': ['', '']
332*9c5db199SXin Li    }
333*9c5db199SXin Li
334*9c5db199SXin Li    def __init__(self, path=None):
335*9c5db199SXin Li        super(BatteryStat, self).__init__(self.battery_fields, path)
336*9c5db199SXin Li        self.update()
337*9c5db199SXin Li
338*9c5db199SXin Li
339*9c5db199SXin Li    def update(self):
340*9c5db199SXin Li        for _ in range(BATTERY_RETRY_COUNT):
341*9c5db199SXin Li            try:
342*9c5db199SXin Li                self._read_battery()
343*9c5db199SXin Li                return
344*9c5db199SXin Li            except error.TestError as e:
345*9c5db199SXin Li                logging.warning(e)
346*9c5db199SXin Li                for field, prop in self.battery_fields.items():
347*9c5db199SXin Li                    logging.warning(field + ': ' + repr(getattr(self, field)))
348*9c5db199SXin Li                continue
349*9c5db199SXin Li        raise error.TestError('Failed to read battery state')
350*9c5db199SXin Li
351*9c5db199SXin Li
352*9c5db199SXin Li    def _read_battery(self):
353*9c5db199SXin Li        self.read_all_vals()
354*9c5db199SXin Li
355*9c5db199SXin Li        if self.charge_full == 0 and self.energy_full != 0:
356*9c5db199SXin Li            battery_type = BatteryDataReportType.ENERGY
357*9c5db199SXin Li        else:
358*9c5db199SXin Li            battery_type = BatteryDataReportType.CHARGE
359*9c5db199SXin Li
360*9c5db199SXin Li        if self.voltage_min_design != 0:
361*9c5db199SXin Li            voltage_nominal = self.voltage_min_design
362*9c5db199SXin Li        else:
363*9c5db199SXin Li            voltage_nominal = self.voltage_now
364*9c5db199SXin Li
365*9c5db199SXin Li        if voltage_nominal == 0:
366*9c5db199SXin Li            raise error.TestError('Failed to determine battery voltage')
367*9c5db199SXin Li
368*9c5db199SXin Li        battery_design_full_scale = 1
369*9c5db199SXin Li
370*9c5db199SXin Li        # Since charge data is present, calculate parameters based upon
371*9c5db199SXin Li        # reported charge data.
372*9c5db199SXin Li        if battery_type == BatteryDataReportType.CHARGE:
373*9c5db199SXin Li            self.charge_full_design *= battery_design_full_scale
374*9c5db199SXin Li
375*9c5db199SXin Li            self.charge_full = self.charge_full / BATTERY_DATA_SCALE
376*9c5db199SXin Li            self.charge_full_design = self.charge_full_design / \
377*9c5db199SXin Li                                      BATTERY_DATA_SCALE
378*9c5db199SXin Li            self.charge_now = self.charge_now / BATTERY_DATA_SCALE
379*9c5db199SXin Li
380*9c5db199SXin Li            self.current_now = math.fabs(self.current_now) / \
381*9c5db199SXin Li                               BATTERY_DATA_SCALE
382*9c5db199SXin Li
383*9c5db199SXin Li            self.energy =  voltage_nominal * \
384*9c5db199SXin Li                           self.charge_now / \
385*9c5db199SXin Li                           BATTERY_DATA_SCALE
386*9c5db199SXin Li            self.energy_full = voltage_nominal * \
387*9c5db199SXin Li                               self.charge_full / \
388*9c5db199SXin Li                               BATTERY_DATA_SCALE
389*9c5db199SXin Li            self.energy_full_design = voltage_nominal * \
390*9c5db199SXin Li                                      self.charge_full_design / \
391*9c5db199SXin Li                                      BATTERY_DATA_SCALE
392*9c5db199SXin Li
393*9c5db199SXin Li        # Charge data not present, so calculate parameters based upon
394*9c5db199SXin Li        # reported energy data.
395*9c5db199SXin Li        elif battery_type == BatteryDataReportType.ENERGY:
396*9c5db199SXin Li            self.energy_full_design *= battery_design_full_scale
397*9c5db199SXin Li
398*9c5db199SXin Li            self.charge_full = self.energy_full / voltage_nominal
399*9c5db199SXin Li            self.charge_full_design = self.energy_full_design / \
400*9c5db199SXin Li                                      voltage_nominal
401*9c5db199SXin Li            self.charge_now = self.energy / voltage_nominal
402*9c5db199SXin Li
403*9c5db199SXin Li            # TODO(shawnn): check if power_now can really be reported
404*9c5db199SXin Li            # as negative, in the same way current_now can
405*9c5db199SXin Li            self.current_now = math.fabs(self.power_now) / \
406*9c5db199SXin Li                               voltage_nominal
407*9c5db199SXin Li
408*9c5db199SXin Li            self.energy = self.energy / BATTERY_DATA_SCALE
409*9c5db199SXin Li            self.energy_full = self.energy_full / BATTERY_DATA_SCALE
410*9c5db199SXin Li            self.energy_full_design = self.energy_full_design / \
411*9c5db199SXin Li                                      BATTERY_DATA_SCALE
412*9c5db199SXin Li
413*9c5db199SXin Li        self.voltage_min_design = self.voltage_min_design / \
414*9c5db199SXin Li                                  BATTERY_DATA_SCALE
415*9c5db199SXin Li        self.voltage_max_design = self.voltage_max_design / \
416*9c5db199SXin Li                                  BATTERY_DATA_SCALE
417*9c5db199SXin Li        self.voltage_now = self.voltage_now / \
418*9c5db199SXin Li                           BATTERY_DATA_SCALE
419*9c5db199SXin Li        voltage_nominal = voltage_nominal / \
420*9c5db199SXin Li                          BATTERY_DATA_SCALE
421*9c5db199SXin Li
422*9c5db199SXin Li        self.energy_rate =  self.voltage_now * self.current_now
423*9c5db199SXin Li
424*9c5db199SXin Li        self.remaining_time = 0
425*9c5db199SXin Li        if self.current_now and self.energy_rate:
426*9c5db199SXin Li            self.remaining_time =  self.energy / self.energy_rate
427*9c5db199SXin Li
428*9c5db199SXin Li        if self.charge_full > (self.charge_full_design * 1.5):
429*9c5db199SXin Li            raise error.TestError('Unreasonable charge_full value')
430*9c5db199SXin Li        if self.charge_now > (self.charge_full_design * 1.5):
431*9c5db199SXin Li            raise error.TestError('Unreasonable charge_now value')
432*9c5db199SXin Li
433*9c5db199SXin Li
434*9c5db199SXin Liclass LineStatPlaceholder(DevStat):
435*9c5db199SXin Li    """
436*9c5db199SXin Li    Placeholder line stat for devices which don't provide power_supply related
437*9c5db199SXin Li    sysfs interface.
438*9c5db199SXin Li    """
439*9c5db199SXin Li    def __init__(self):
440*9c5db199SXin Li        self.online = True
441*9c5db199SXin Li
442*9c5db199SXin Li
443*9c5db199SXin Li    def update(self):
444*9c5db199SXin Li        pass
445*9c5db199SXin Li
446*9c5db199SXin Liclass LineStat(DevStat):
447*9c5db199SXin Li    """
448*9c5db199SXin Li    Power line status.
449*9c5db199SXin Li
450*9c5db199SXin Li    Fields:
451*9c5db199SXin Li
452*9c5db199SXin Li    bool online:              Line power online
453*9c5db199SXin Li    """
454*9c5db199SXin Li
455*9c5db199SXin Li    linepower_fields = {
456*9c5db199SXin Li        'is_online':             ['online', int],
457*9c5db199SXin Li        'status':                ['status', str]
458*9c5db199SXin Li        }
459*9c5db199SXin Li
460*9c5db199SXin Li
461*9c5db199SXin Li    def __init__(self, path=None):
462*9c5db199SXin Li        super(LineStat, self).__init__(self.linepower_fields, path)
463*9c5db199SXin Li        logging.debug("line path: %s", path)
464*9c5db199SXin Li        self.update()
465*9c5db199SXin Li
466*9c5db199SXin Li
467*9c5db199SXin Li    def update(self):
468*9c5db199SXin Li        self.read_all_vals()
469*9c5db199SXin Li        self.online = self.is_online == 1
470*9c5db199SXin Li
471*9c5db199SXin Li
472*9c5db199SXin Liclass SysStat(object):
473*9c5db199SXin Li    """
474*9c5db199SXin Li    System power status for a given host.
475*9c5db199SXin Li
476*9c5db199SXin Li    Fields:
477*9c5db199SXin Li
478*9c5db199SXin Li    battery:   A BatteryStat object.
479*9c5db199SXin Li    linepower: A list of LineStat objects.
480*9c5db199SXin Li    """
481*9c5db199SXin Li    psu_types = ['Mains', 'USB', 'USB_ACA', 'USB_C', 'USB_CDP', 'USB_DCP',
482*9c5db199SXin Li                 'USB_PD', 'USB_PD_DRP', 'Unknown']
483*9c5db199SXin Li
484*9c5db199SXin Li    def __init__(self):
485*9c5db199SXin Li        power_supply_path = '/sys/class/power_supply/*'
486*9c5db199SXin Li        self.battery = None
487*9c5db199SXin Li        self.linepower = []
488*9c5db199SXin Li        self.thermal = None
489*9c5db199SXin Li        self.battery_path = None
490*9c5db199SXin Li        self.linepower_path = []
491*9c5db199SXin Li
492*9c5db199SXin Li        power_supplies = glob.glob(power_supply_path)
493*9c5db199SXin Li        for path in power_supplies:
494*9c5db199SXin Li            type_path = os.path.join(path,'type')
495*9c5db199SXin Li            if not os.path.exists(type_path):
496*9c5db199SXin Li                continue
497*9c5db199SXin Li            power_type = utils.read_one_line(type_path)
498*9c5db199SXin Li            if power_type == 'Battery':
499*9c5db199SXin Li                scope_path = os.path.join(path,'scope')
500*9c5db199SXin Li                if (os.path.exists(scope_path) and
501*9c5db199SXin Li                        utils.read_one_line(scope_path) == 'Device'):
502*9c5db199SXin Li                    continue
503*9c5db199SXin Li                self.battery_path = path
504*9c5db199SXin Li            elif power_type in self.psu_types:
505*9c5db199SXin Li                self.linepower_path.append(path)
506*9c5db199SXin Li
507*9c5db199SXin Li        if not self.battery_path or not self.linepower_path:
508*9c5db199SXin Li            logging.warning("System does not provide power sysfs interface")
509*9c5db199SXin Li
510*9c5db199SXin Li        self.thermal = ThermalStat()
511*9c5db199SXin Li        if self.battery_path:
512*9c5db199SXin Li            self.sys_low_batt_p = float(utils.system_output(
513*9c5db199SXin Li                    'check_powerd_config --low_battery_shutdown_percent',
514*9c5db199SXin Li                    ignore_status=True) or 4.0)
515*9c5db199SXin Li
516*9c5db199SXin Li
517*9c5db199SXin Li    def refresh(self):
518*9c5db199SXin Li        """
519*9c5db199SXin Li        Initialize device power status objects.
520*9c5db199SXin Li        """
521*9c5db199SXin Li        self.linepower = []
522*9c5db199SXin Li
523*9c5db199SXin Li        if self.battery_path:
524*9c5db199SXin Li            self.battery = BatteryStat(self.battery_path)
525*9c5db199SXin Li
526*9c5db199SXin Li        for path in self.linepower_path:
527*9c5db199SXin Li            self.linepower.append(LineStat(path))
528*9c5db199SXin Li        if not self.linepower:
529*9c5db199SXin Li            self.linepower = [ LineStatPlaceholder() ]
530*9c5db199SXin Li
531*9c5db199SXin Li        temp_str = self.thermal.get_temps()
532*9c5db199SXin Li        if temp_str:
533*9c5db199SXin Li            logging.info('Temperature reading: %s', temp_str)
534*9c5db199SXin Li        else:
535*9c5db199SXin Li            logging.error('Could not read temperature, skipping.')
536*9c5db199SXin Li
537*9c5db199SXin Li
538*9c5db199SXin Li    def on_ac(self):
539*9c5db199SXin Li        """
540*9c5db199SXin Li        Returns true if device is currently running from AC power.
541*9c5db199SXin Li        """
542*9c5db199SXin Li        on_ac = False
543*9c5db199SXin Li        for linepower in self.linepower:
544*9c5db199SXin Li            on_ac |= linepower.online
545*9c5db199SXin Li
546*9c5db199SXin Li        # Butterfly can incorrectly report AC online for some time after
547*9c5db199SXin Li        # unplug. Check battery discharge state to confirm.
548*9c5db199SXin Li        if utils.get_board() == 'butterfly':
549*9c5db199SXin Li            on_ac &= (not self.battery_discharging())
550*9c5db199SXin Li        return on_ac
551*9c5db199SXin Li
552*9c5db199SXin Li
553*9c5db199SXin Li    def battery_charging(self):
554*9c5db199SXin Li        """
555*9c5db199SXin Li        Returns true if battery is currently charging or false otherwise.
556*9c5db199SXin Li        """
557*9c5db199SXin Li        for linepower in self.linepower:
558*9c5db199SXin Li            if linepower.status == 'Charging':
559*9c5db199SXin Li                return True
560*9c5db199SXin Li
561*9c5db199SXin Li        if not self.battery_path:
562*9c5db199SXin Li            logging.warning('Unable to determine battery charge status')
563*9c5db199SXin Li            return False
564*9c5db199SXin Li
565*9c5db199SXin Li        return self.battery.status.rstrip() == 'Charging'
566*9c5db199SXin Li
567*9c5db199SXin Li
568*9c5db199SXin Li    def battery_discharging(self):
569*9c5db199SXin Li        """
570*9c5db199SXin Li        Returns true if battery is currently discharging or false otherwise.
571*9c5db199SXin Li        """
572*9c5db199SXin Li        if not self.battery_path:
573*9c5db199SXin Li            logging.warning('Unable to determine battery discharge status')
574*9c5db199SXin Li            return False
575*9c5db199SXin Li
576*9c5db199SXin Li        return self.battery.status.rstrip() == 'Discharging'
577*9c5db199SXin Li
578*9c5db199SXin Li    def battery_full(self):
579*9c5db199SXin Li        """
580*9c5db199SXin Li        Returns true if battery is currently full or false otherwise.
581*9c5db199SXin Li        """
582*9c5db199SXin Li        if not self.battery_path:
583*9c5db199SXin Li            logging.warning('Unable to determine battery fullness status')
584*9c5db199SXin Li            return False
585*9c5db199SXin Li
586*9c5db199SXin Li        return self.battery.status.rstrip() == 'Full'
587*9c5db199SXin Li
588*9c5db199SXin Li
589*9c5db199SXin Li    def battery_discharge_ok_on_ac(self):
590*9c5db199SXin Li        """Returns True if battery is ok to discharge on AC presently.
591*9c5db199SXin Li
592*9c5db199SXin Li        some devices cycle between charge & discharge above a certain
593*9c5db199SXin Li        SoC.  If AC is charging and SoC > 95% we can safely assume that.
594*9c5db199SXin Li        """
595*9c5db199SXin Li        return self.battery_charging() and (self.percent_current_charge() > 95)
596*9c5db199SXin Li
597*9c5db199SXin Li
598*9c5db199SXin Li    def percent_current_charge(self):
599*9c5db199SXin Li        """Returns current charge compare to design capacity in percent.
600*9c5db199SXin Li        """
601*9c5db199SXin Li        return self.battery.charge_now * 100 / \
602*9c5db199SXin Li               self.battery.charge_full_design
603*9c5db199SXin Li
604*9c5db199SXin Li
605*9c5db199SXin Li    def percent_display_charge(self):
606*9c5db199SXin Li        """Returns current display charge in percent.
607*9c5db199SXin Li        """
608*9c5db199SXin Li        keyvals = parse_power_supply_info()
609*9c5db199SXin Li        return float(keyvals['Battery']['display percentage'])
610*9c5db199SXin Li
611*9c5db199SXin Li
612*9c5db199SXin Li    def assert_battery_state(self, percent_initial_charge_min):
613*9c5db199SXin Li        """Check initial power configuration state is battery.
614*9c5db199SXin Li
615*9c5db199SXin Li        Args:
616*9c5db199SXin Li          percent_initial_charge_min: float between 0 -> 1.00 of
617*9c5db199SXin Li            percentage of battery that must be remaining.
618*9c5db199SXin Li            None|0|False means check not performed.
619*9c5db199SXin Li
620*9c5db199SXin Li        Raises:
621*9c5db199SXin Li          TestError: if one of battery assertions fails
622*9c5db199SXin Li        """
623*9c5db199SXin Li        if self.on_ac():
624*9c5db199SXin Li            raise error.TestError(
625*9c5db199SXin Li                'Running on AC power. Please remove AC power cable.')
626*9c5db199SXin Li
627*9c5db199SXin Li        percent_initial_charge = self.percent_current_charge()
628*9c5db199SXin Li
629*9c5db199SXin Li        if percent_initial_charge_min and percent_initial_charge < \
630*9c5db199SXin Li                                          percent_initial_charge_min:
631*9c5db199SXin Li            raise error.TestError('Initial charge (%f) less than min (%f)'
632*9c5db199SXin Li                      % (percent_initial_charge, percent_initial_charge_min))
633*9c5db199SXin Li
634*9c5db199SXin Li    def assert_battery_in_range(self, min_level, max_level):
635*9c5db199SXin Li        """Raise a error.TestFail if the battery level is not in range."""
636*9c5db199SXin Li        current_percent = self.percent_display_charge()
637*9c5db199SXin Li        if not (min_level <= current_percent <= max_level):
638*9c5db199SXin Li            raise error.TestFail('battery must be in range [{}, {}]'.format(
639*9c5db199SXin Li                                 min_level, max_level))
640*9c5db199SXin Li
641*9c5db199SXin Li    def is_low_battery(self, low_batt_margin_p=2.0):
642*9c5db199SXin Li        """Returns True if battery current charge is low
643*9c5db199SXin Li
644*9c5db199SXin Li        @param low_batt_margin_p: percentage of battery that would be added to
645*9c5db199SXin Li                                  system low battery level.
646*9c5db199SXin Li        """
647*9c5db199SXin Li        return (self.battery_discharging() and
648*9c5db199SXin Li                self.percent_current_charge() < self.sys_low_batt_p +
649*9c5db199SXin Li                                                low_batt_margin_p)
650*9c5db199SXin Li
651*9c5db199SXin Li
652*9c5db199SXin Lidef get_status():
653*9c5db199SXin Li    """
654*9c5db199SXin Li    Return a new power status object (SysStat). A new power status snapshot
655*9c5db199SXin Li    for a given host can be obtained by either calling this routine again and
656*9c5db199SXin Li    constructing a new SysStat object, or by using the refresh method of the
657*9c5db199SXin Li    SysStat object.
658*9c5db199SXin Li    """
659*9c5db199SXin Li    status = SysStat()
660*9c5db199SXin Li    status.refresh()
661*9c5db199SXin Li    return status
662*9c5db199SXin Li
663*9c5db199SXin Li
664*9c5db199SXin Lidef poll_for_charging_behavior(behavior, timeout):
665*9c5db199SXin Li    """
666*9c5db199SXin Li    Wait up to |timeout| seconds for the charging behavior to become |behavior|.
667*9c5db199SXin Li
668*9c5db199SXin Li    @param behavior: One of 'ON_AC_AND_CHARGING',
669*9c5db199SXin Li                            'ON_AC_AND_NOT_CHARGING',
670*9c5db199SXin Li                            'NOT_ON_AC_AND_NOT_CHARGING'.
671*9c5db199SXin Li    @param timeout: in seconds.
672*9c5db199SXin Li
673*9c5db199SXin Li    @raises: error.TestFail if the behavior does not match in time, or another
674*9c5db199SXin Li             exception if something else fails along the way.
675*9c5db199SXin Li    """
676*9c5db199SXin Li    ps = get_status()
677*9c5db199SXin Li
678*9c5db199SXin Li    def _verify_on_AC_and_charging():
679*9c5db199SXin Li        ps.refresh()
680*9c5db199SXin Li        if not ps.on_ac():
681*9c5db199SXin Li            raise error.TestFail('Device is not on AC, but should be')
682*9c5db199SXin Li        if not ps.battery_charging():
683*9c5db199SXin Li            raise error.TestFail('Device is not charging, but should be')
684*9c5db199SXin Li        return True
685*9c5db199SXin Li
686*9c5db199SXin Li    def _verify_on_AC_and_not_charging():
687*9c5db199SXin Li        ps.refresh()
688*9c5db199SXin Li        if not ps.on_ac():
689*9c5db199SXin Li            raise error.TestFail('Device is not on AC, but should be')
690*9c5db199SXin Li        if ps.battery_charging():
691*9c5db199SXin Li            raise error.TestFail('Device is charging, but should not be')
692*9c5db199SXin Li        return True
693*9c5db199SXin Li
694*9c5db199SXin Li    def _verify_not_on_AC_and_not_charging():
695*9c5db199SXin Li        ps.refresh()
696*9c5db199SXin Li        if ps.on_ac():
697*9c5db199SXin Li            raise error.TestFail('Device is on AC, but should not be')
698*9c5db199SXin Li        return True
699*9c5db199SXin Li
700*9c5db199SXin Li    poll_functions = {
701*9c5db199SXin Li        'ON_AC_AND_CHARGING'        : _verify_on_AC_and_charging,
702*9c5db199SXin Li        'ON_AC_AND_NOT_CHARGING'    : _verify_on_AC_and_not_charging,
703*9c5db199SXin Li        'NOT_ON_AC_AND_NOT_CHARGING': _verify_not_on_AC_and_not_charging,
704*9c5db199SXin Li    }
705*9c5db199SXin Li    poll_for_condition_ex(poll_functions[behavior],
706*9c5db199SXin Li                          timeout=timeout,
707*9c5db199SXin Li                          sleep_interval=1)
708*9c5db199SXin Li
709*9c5db199SXin Liclass AbstractStats(object):
710*9c5db199SXin Li    """
711*9c5db199SXin Li    Common superclass for measurements of percentages per state over time.
712*9c5db199SXin Li
713*9c5db199SXin Li    Public Attributes:
714*9c5db199SXin Li        incremental:  If False, stats returned are from a single
715*9c5db199SXin Li        _read_stats.  Otherwise, stats are from the difference between
716*9c5db199SXin Li        the current and last refresh.
717*9c5db199SXin Li    """
718*9c5db199SXin Li
719*9c5db199SXin Li    @staticmethod
720*9c5db199SXin Li    def to_percent(stats):
721*9c5db199SXin Li        """
722*9c5db199SXin Li        Turns a dict with absolute time values into a dict with percentages.
723*9c5db199SXin Li        """
724*9c5db199SXin Li        total = sum(stats.values())
725*9c5db199SXin Li        if total == 0:
726*9c5db199SXin Li            return {k: 0 for k in stats}
727*9c5db199SXin Li        return dict((k, v * 100.0 / total) for (k, v) in stats.items())
728*9c5db199SXin Li
729*9c5db199SXin Li
730*9c5db199SXin Li    @staticmethod
731*9c5db199SXin Li    def do_diff(new, old):
732*9c5db199SXin Li        """
733*9c5db199SXin Li        Returns a dict with value deltas from two dicts with matching keys.
734*9c5db199SXin Li        """
735*9c5db199SXin Li        return dict((k, new[k] - old.get(k, 0)) for k in new.keys())
736*9c5db199SXin Li
737*9c5db199SXin Li
738*9c5db199SXin Li    @staticmethod
739*9c5db199SXin Li    def format_results_percent(results, name, percent_stats):
740*9c5db199SXin Li        """
741*9c5db199SXin Li        Formats autotest result keys to format:
742*9c5db199SXin Li          percent_<name>_<key>_time
743*9c5db199SXin Li        """
744*9c5db199SXin Li        for key in percent_stats:
745*9c5db199SXin Li            results['percent_%s_%s_time' % (name, key)] = percent_stats[key]
746*9c5db199SXin Li
747*9c5db199SXin Li
748*9c5db199SXin Li    @staticmethod
749*9c5db199SXin Li    def format_results_wavg(results, name, wavg):
750*9c5db199SXin Li        """
751*9c5db199SXin Li        Add an autotest result keys to format: wavg_<name>
752*9c5db199SXin Li        """
753*9c5db199SXin Li        if wavg is not None:
754*9c5db199SXin Li            results['wavg_%s' % (name)] = wavg
755*9c5db199SXin Li
756*9c5db199SXin Li
757*9c5db199SXin Li    def __init__(self, name, incremental=True):
758*9c5db199SXin Li        self.name = name
759*9c5db199SXin Li        self.incremental = incremental
760*9c5db199SXin Li        self._stats = self._read_stats()
761*9c5db199SXin Li        self._first_stats = self._stats.copy()
762*9c5db199SXin Li
763*9c5db199SXin Li    def refresh(self):
764*9c5db199SXin Li        """
765*9c5db199SXin Li        Returns dict mapping state names to percentage of time spent in them.
766*9c5db199SXin Li        """
767*9c5db199SXin Li        raw_stats = result = self._read_stats()
768*9c5db199SXin Li        if self.incremental:
769*9c5db199SXin Li            result = self.do_diff(result, self._stats)
770*9c5db199SXin Li        self._stats = raw_stats
771*9c5db199SXin Li        return self.to_percent(result)
772*9c5db199SXin Li
773*9c5db199SXin Li
774*9c5db199SXin Li    def _automatic_weighted_average(self):
775*9c5db199SXin Li        """
776*9c5db199SXin Li        Turns a dict with absolute times (or percentages) into a weighted
777*9c5db199SXin Li        average value.
778*9c5db199SXin Li        """
779*9c5db199SXin Li        stats = self._stats
780*9c5db199SXin Li        if self.incremental:
781*9c5db199SXin Li            stats = self.do_diff(stats, self._first_stats)
782*9c5db199SXin Li
783*9c5db199SXin Li        total = sum(stats.values())
784*9c5db199SXin Li        if total == 0:
785*9c5db199SXin Li            return None
786*9c5db199SXin Li
787*9c5db199SXin Li        return sum(float(k) * v / total for k, v in stats.items())
788*9c5db199SXin Li
789*9c5db199SXin Li    def _supports_automatic_weighted_average(self):
790*9c5db199SXin Li        """
791*9c5db199SXin Li        Override!
792*9c5db199SXin Li
793*9c5db199SXin Li        Returns True if stats collected can be automatically converted from
794*9c5db199SXin Li        percent distribution to weighted average. False otherwise.
795*9c5db199SXin Li        """
796*9c5db199SXin Li        return False
797*9c5db199SXin Li
798*9c5db199SXin Li
799*9c5db199SXin Li    def weighted_average(self):
800*9c5db199SXin Li        """
801*9c5db199SXin Li        Return weighted average calculated using the automated average method
802*9c5db199SXin Li        (if supported) or using a custom method defined by the stat.
803*9c5db199SXin Li        """
804*9c5db199SXin Li        if self._supports_automatic_weighted_average():
805*9c5db199SXin Li            return self._automatic_weighted_average()
806*9c5db199SXin Li
807*9c5db199SXin Li        return self._weighted_avg_fn()
808*9c5db199SXin Li
809*9c5db199SXin Li
810*9c5db199SXin Li    def _weighted_avg_fn(self):
811*9c5db199SXin Li        """
812*9c5db199SXin Li        Override! Custom weighted average function.
813*9c5db199SXin Li
814*9c5db199SXin Li        Returns weighted average as a single floating point value.
815*9c5db199SXin Li        """
816*9c5db199SXin Li        return None
817*9c5db199SXin Li
818*9c5db199SXin Li
819*9c5db199SXin Li    def _read_stats(self):
820*9c5db199SXin Li        """
821*9c5db199SXin Li        Override! Reads the raw data values that shall be measured into a dict.
822*9c5db199SXin Li        """
823*9c5db199SXin Li        raise NotImplementedError('Override _read_stats in the subclass!')
824*9c5db199SXin Li
825*9c5db199SXin Li
826*9c5db199SXin LiCPU_BASE_PATH = '/sys/devices/system/cpu/'
827*9c5db199SXin Li
828*9c5db199SXin Lidef count_all_cpus():
829*9c5db199SXin Li    """
830*9c5db199SXin Li    Return count of cpus on system.
831*9c5db199SXin Li    """
832*9c5db199SXin Li    path = '%s/cpu[0-9]*' % CPU_BASE_PATH
833*9c5db199SXin Li    return len(glob.glob(path))
834*9c5db199SXin Li
835*9c5db199SXin Lidef get_online_cpus():
836*9c5db199SXin Li    """
837*9c5db199SXin Li    Return frozenset of integer cpu numbers that are online.
838*9c5db199SXin Li    """
839*9c5db199SXin Li    return frozenset(read_cpu_set('/sys/devices/system/cpu/online'))
840*9c5db199SXin Li
841*9c5db199SXin Lidef get_cpus_filepaths_for_suffix(cpus, suffix):
842*9c5db199SXin Li    """
843*9c5db199SXin Li    For each cpu in |cpus| check whether |CPU_BASE_PATH|/cpu%d/|suffix| exists.
844*9c5db199SXin Li    Return tuple of two lists t:
845*9c5db199SXin Li                    t[0]: all cpu ids where the condition above holds
846*9c5db199SXin Li                    t[1]: all full paths where condition above holds.
847*9c5db199SXin Li    """
848*9c5db199SXin Li    available_cpus = []
849*9c5db199SXin Li    available_paths = []
850*9c5db199SXin Li    for c in cpus:
851*9c5db199SXin Li        c_file_path = os.path.join(CPU_BASE_PATH, 'cpu%d' % c, suffix)
852*9c5db199SXin Li        if os.path.exists(c_file_path):
853*9c5db199SXin Li            available_cpus.append(c)
854*9c5db199SXin Li            available_paths.append(c_file_path)
855*9c5db199SXin Li    return (available_cpus, available_paths)
856*9c5db199SXin Li
857*9c5db199SXin Li
858*9c5db199SXin Liclass CPUFreqStatsPState(AbstractStats):
859*9c5db199SXin Li    """
860*9c5db199SXin Li    CPU Frequency statistics for intel_pstate
861*9c5db199SXin Li    """
862*9c5db199SXin Li    MSR_PLATFORM_INFO = 0xce
863*9c5db199SXin Li    MSR_IA32_MPERF = 0xe7
864*9c5db199SXin Li    MSR_IA32_APERF = 0xe8
865*9c5db199SXin Li
866*9c5db199SXin Li    def __init__(self, cpus=None):
867*9c5db199SXin Li        name = 'cpufreq'
868*9c5db199SXin Li        if not cpus:
869*9c5db199SXin Li            cpus = get_online_cpus()
870*9c5db199SXin Li        self._cpus = cpus
871*9c5db199SXin Li
872*9c5db199SXin Li        if len(cpus) and len(cpus) < count_all_cpus():
873*9c5db199SXin Li            name = '%s_%s' % (name, '_'.join([str(c) for c in cpus]))
874*9c5db199SXin Li
875*9c5db199SXin Li        self._initial_perf = None
876*9c5db199SXin Li        self._current_perf = None
877*9c5db199SXin Li
878*9c5db199SXin Li        # max_freq is supposed to be the same for all CPUs and remain
879*9c5db199SXin Li        # constant throughout. So, we set the entry only once.
880*9c5db199SXin Li        # Note that this is max non-turbo frequency, some CPU can run at
881*9c5db199SXin Li        # higher turbo frequency in some condition.
882*9c5db199SXin Li        platform_info = utils.rdmsr(self.MSR_PLATFORM_INFO)
883*9c5db199SXin Li        mul = platform_info >> 8 & 0xff
884*9c5db199SXin Li        bclk = utils.get_intel_bclk_khz()
885*9c5db199SXin Li        self._max_freq = mul * bclk
886*9c5db199SXin Li
887*9c5db199SXin Li        super(CPUFreqStatsPState, self).__init__(name)
888*9c5db199SXin Li
889*9c5db199SXin Li    def _read_stats(self):
890*9c5db199SXin Li        aperf = 0
891*9c5db199SXin Li        mperf = 0
892*9c5db199SXin Li
893*9c5db199SXin Li        for cpu in self._cpus:
894*9c5db199SXin Li            aperf += utils.rdmsr(self.MSR_IA32_APERF, cpu)
895*9c5db199SXin Li            mperf += utils.rdmsr(self.MSR_IA32_MPERF, cpu)
896*9c5db199SXin Li
897*9c5db199SXin Li        if not self._initial_perf:
898*9c5db199SXin Li            self._initial_perf = (aperf, mperf)
899*9c5db199SXin Li
900*9c5db199SXin Li        self._current_perf = (aperf, mperf)
901*9c5db199SXin Li
902*9c5db199SXin Li        return {}
903*9c5db199SXin Li
904*9c5db199SXin Li    def _weighted_avg_fn(self):
905*9c5db199SXin Li        if (self._current_perf
906*9c5db199SXin Li                    and self._current_perf[1] != self._initial_perf[1]):
907*9c5db199SXin Li            # Avg freq = max_freq * aperf_delta / mperf_delta
908*9c5db199SXin Li            return self._max_freq * \
909*9c5db199SXin Li                float(self._current_perf[0] - self._initial_perf[0]) / \
910*9c5db199SXin Li                (self._current_perf[1] - self._initial_perf[1])
911*9c5db199SXin Li        return 1.0
912*9c5db199SXin Li
913*9c5db199SXin Li
914*9c5db199SXin Liclass CPUFreqStats(AbstractStats):
915*9c5db199SXin Li    """
916*9c5db199SXin Li    CPU Frequency statistics
917*9c5db199SXin Li    """
918*9c5db199SXin Li
919*9c5db199SXin Li    def __init__(self, cpus=None):
920*9c5db199SXin Li        name = 'cpufreq'
921*9c5db199SXin Li        stats_suffix = 'cpufreq/stats/time_in_state'
922*9c5db199SXin Li        key_suffix = 'cpufreq/scaling_available_frequencies'
923*9c5db199SXin Li        if not cpus:
924*9c5db199SXin Li            cpus = get_online_cpus()
925*9c5db199SXin Li        _, self._file_paths = get_cpus_filepaths_for_suffix(cpus, stats_suffix)
926*9c5db199SXin Li
927*9c5db199SXin Li        if len(cpus) and len(cpus) < count_all_cpus():
928*9c5db199SXin Li            name = '%s_%s' % (name, '_'.join([str(c) for c in cpus]))
929*9c5db199SXin Li        self._cpus = cpus
930*9c5db199SXin Li        self._available_freqs = set()
931*9c5db199SXin Li
932*9c5db199SXin Li        if not self._file_paths:
933*9c5db199SXin Li            logging.debug('time_in_state file not found')
934*9c5db199SXin Li
935*9c5db199SXin Li        # assumes cpufreq driver for CPU0 is the same as the others.
936*9c5db199SXin Li        _, cpufreq_key_paths = get_cpus_filepaths_for_suffix(cpus, key_suffix)
937*9c5db199SXin Li        for path in cpufreq_key_paths:
938*9c5db199SXin Li            self._available_freqs |= set(
939*9c5db199SXin Li                    int(x) for x in utils.read_file(path).split())
940*9c5db199SXin Li
941*9c5db199SXin Li        super(CPUFreqStats, self).__init__(name)
942*9c5db199SXin Li
943*9c5db199SXin Li    def _read_stats(self):
944*9c5db199SXin Li        stats = dict((k, 0) for k in self._available_freqs)
945*9c5db199SXin Li        for path in self._file_paths:
946*9c5db199SXin Li            if not os.path.exists(path):
947*9c5db199SXin Li                logging.debug('%s is not found', path)
948*9c5db199SXin Li                continue
949*9c5db199SXin Li
950*9c5db199SXin Li            data = utils.read_file(path)
951*9c5db199SXin Li            for line in data.splitlines():
952*9c5db199SXin Li                pair = line.split()
953*9c5db199SXin Li                freq = int(pair[0])
954*9c5db199SXin Li                timeunits = int(pair[1])
955*9c5db199SXin Li                if freq in stats:
956*9c5db199SXin Li                    stats[freq] += timeunits
957*9c5db199SXin Li                else:
958*9c5db199SXin Li                    stats[freq] = timeunits
959*9c5db199SXin Li        return stats
960*9c5db199SXin Li
961*9c5db199SXin Li    def _supports_automatic_weighted_average(self):
962*9c5db199SXin Li        return True
963*9c5db199SXin Li
964*9c5db199SXin Li
965*9c5db199SXin Liclass CPUCStateStats(AbstractStats):
966*9c5db199SXin Li    """
967*9c5db199SXin Li    Base class for C-state residency statistics
968*9c5db199SXin Li    """
969*9c5db199SXin Li    def __init__(self, name, non_c0_stat=''):
970*9c5db199SXin Li        self._non_c0_stat = non_c0_stat
971*9c5db199SXin Li        super(CPUCStateStats, self).__init__(name)
972*9c5db199SXin Li
973*9c5db199SXin Li
974*9c5db199SXin Li    def to_percent(self, stats):
975*9c5db199SXin Li        """
976*9c5db199SXin Li        Turns a dict with absolute time values into a dict with percentages.
977*9c5db199SXin Li        Ignore the |non_c0_stat_name| which is aggegate stat in the total count.
978*9c5db199SXin Li        """
979*9c5db199SXin Li        total = sum(v for k, v in stats.items() if k != self._non_c0_stat)
980*9c5db199SXin Li        if total == 0:
981*9c5db199SXin Li            return {k: 0 for k in stats}
982*9c5db199SXin Li        return {k: v * 100.0 / total for k, v in stats.items()}
983*9c5db199SXin Li
984*9c5db199SXin Li
985*9c5db199SXin Liclass CPUIdleStats(CPUCStateStats):
986*9c5db199SXin Li    """
987*9c5db199SXin Li    CPU Idle statistics (refresh() will not work with incremental=False!)
988*9c5db199SXin Li    """
989*9c5db199SXin Li    # TODO (snanda): Handle changes in number of c-states due to events such
990*9c5db199SXin Li    # as ac <-> battery transitions.
991*9c5db199SXin Li    # TODO (snanda): Handle non-S0 states. Time spent in suspend states is
992*9c5db199SXin Li    # currently not factored out.
993*9c5db199SXin Li    def __init__(self, cpus=None):
994*9c5db199SXin Li        name = 'cpuidle'
995*9c5db199SXin Li        cpuidle_suffix = 'cpuidle'
996*9c5db199SXin Li        if not cpus:
997*9c5db199SXin Li            cpus = get_online_cpus()
998*9c5db199SXin Li        cpus, self._cpus = get_cpus_filepaths_for_suffix(cpus, cpuidle_suffix)
999*9c5db199SXin Li        if len(cpus) and len(cpus) < count_all_cpus():
1000*9c5db199SXin Li            name = '%s_%s' % (name, '_'.join([str(c) for c in cpus]))
1001*9c5db199SXin Li        super(CPUIdleStats, self).__init__(name=name, non_c0_stat='non-C0')
1002*9c5db199SXin Li
1003*9c5db199SXin Li
1004*9c5db199SXin Li    def _read_stats(self):
1005*9c5db199SXin Li        cpuidle_stats = collections.defaultdict(int)
1006*9c5db199SXin Li        epoch_usecs = int(time.time() * 1000 * 1000)
1007*9c5db199SXin Li        for cpu in self._cpus:
1008*9c5db199SXin Li            state_path = os.path.join(cpu, 'state*')
1009*9c5db199SXin Li            states = glob.glob(state_path)
1010*9c5db199SXin Li            cpuidle_stats['C0'] += epoch_usecs
1011*9c5db199SXin Li
1012*9c5db199SXin Li            for state in states:
1013*9c5db199SXin Li                name = utils.read_one_line(os.path.join(state, 'name'))
1014*9c5db199SXin Li                latency = utils.read_one_line(os.path.join(state, 'latency'))
1015*9c5db199SXin Li
1016*9c5db199SXin Li                if not int(latency) and name == 'POLL':
1017*9c5db199SXin Li                    # C0 state. Kernel stats aren't right, so calculate by
1018*9c5db199SXin Li                    # subtracting all other states from total time (using epoch
1019*9c5db199SXin Li                    # timer since we calculate differences in the end anyway).
1020*9c5db199SXin Li                    # NOTE: Only x86 lists C0 under cpuidle, ARM does not.
1021*9c5db199SXin Li                    continue
1022*9c5db199SXin Li
1023*9c5db199SXin Li                usecs = int(utils.read_one_line(os.path.join(state, 'time')))
1024*9c5db199SXin Li                cpuidle_stats['C0'] -= usecs
1025*9c5db199SXin Li
1026*9c5db199SXin Li                if name == '<null>':
1027*9c5db199SXin Li                    # Kernel race condition that can happen while a new C-state
1028*9c5db199SXin Li                    # gets added (e.g. AC->battery). Don't know the 'name' of
1029*9c5db199SXin Li                    # the state yet, but its 'time' would be 0 anyway.
1030*9c5db199SXin Li                    logging.warning('Read name: <null>, time: %d from %s...'
1031*9c5db199SXin Li                                    'skipping.', usecs, state)
1032*9c5db199SXin Li                    continue
1033*9c5db199SXin Li
1034*9c5db199SXin Li                cpuidle_stats[name] += usecs
1035*9c5db199SXin Li                cpuidle_stats['non-C0'] += usecs
1036*9c5db199SXin Li
1037*9c5db199SXin Li        return cpuidle_stats
1038*9c5db199SXin Li
1039*9c5db199SXin Li
1040*9c5db199SXin Liclass CPUPackageStats(CPUCStateStats):
1041*9c5db199SXin Li    """
1042*9c5db199SXin Li    Package C-state residency statistics for modern Intel CPUs.
1043*9c5db199SXin Li    """
1044*9c5db199SXin Li
1045*9c5db199SXin Li    ATOM         =              {'C2': 0x3F8, 'C4': 0x3F9, 'C6': 0x3FA}
1046*9c5db199SXin Li    NEHALEM      =              {'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA}
1047*9c5db199SXin Li    SANDY_BRIDGE = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA}
1048*9c5db199SXin Li    SILVERMONT   = {'C6': 0x3FA}
1049*9c5db199SXin Li    GOLDMONT     = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9,'C10': 0x632}
1050*9c5db199SXin Li    BROADWELL    = {'C2': 0x60D, 'C3': 0x3F8, 'C6': 0x3F9, 'C7': 0x3FA,
1051*9c5db199SXin Li                                 'C8': 0x630, 'C9': 0x631,'C10': 0x632}
1052*9c5db199SXin Li
1053*9c5db199SXin Li    def __init__(self):
1054*9c5db199SXin Li        def _get_platform_states():
1055*9c5db199SXin Li            """
1056*9c5db199SXin Li            Helper to decide what set of microarchitecture-specific MSRs to use.
1057*9c5db199SXin Li
1058*9c5db199SXin Li            Returns: dict that maps C-state name to MSR address, or None.
1059*9c5db199SXin Li            """
1060*9c5db199SXin Li            cpu_uarch = utils.get_intel_cpu_uarch()
1061*9c5db199SXin Li
1062*9c5db199SXin Li            return {
1063*9c5db199SXin Li                # model groups pulled from Intel SDM, volume 4
1064*9c5db199SXin Li                # Group same package cstate using the older uarch name
1065*9c5db199SXin Li                #
1066*9c5db199SXin Li                # TODO(harry.pan): As the keys represent microarchitecture
1067*9c5db199SXin Li                # names, we could consider to rename the PC state groups
1068*9c5db199SXin Li                # to avoid ambiguity.
1069*9c5db199SXin Li                'Airmont':      self.SILVERMONT,
1070*9c5db199SXin Li                'Atom':         self.ATOM,
1071*9c5db199SXin Li                'Broadwell':    self.BROADWELL,
1072*9c5db199SXin Li                'Comet Lake':   self.BROADWELL,
1073*9c5db199SXin Li                'Goldmont':     self.GOLDMONT,
1074*9c5db199SXin Li                'Haswell':      self.SANDY_BRIDGE,
1075*9c5db199SXin Li                'Ice Lake':     self.BROADWELL,
1076*9c5db199SXin Li                'Ivy Bridge':   self.SANDY_BRIDGE,
1077*9c5db199SXin Li                'Ivy Bridge-E': self.SANDY_BRIDGE,
1078*9c5db199SXin Li                'Kaby Lake':    self.BROADWELL,
1079*9c5db199SXin Li                'Nehalem':      self.NEHALEM,
1080*9c5db199SXin Li                'Sandy Bridge': self.SANDY_BRIDGE,
1081*9c5db199SXin Li                'Silvermont':   self.SILVERMONT,
1082*9c5db199SXin Li                'Skylake':      self.BROADWELL,
1083*9c5db199SXin Li                'Tiger Lake':   self.BROADWELL,
1084*9c5db199SXin Li                'Alder Lake':   self.BROADWELL,
1085*9c5db199SXin Li                'Tremont':      self.GOLDMONT,
1086*9c5db199SXin Li                'Westmere':     self.NEHALEM,
1087*9c5db199SXin Li                }.get(cpu_uarch, None)
1088*9c5db199SXin Li
1089*9c5db199SXin Li        self._platform_states = _get_platform_states()
1090*9c5db199SXin Li        super(CPUPackageStats, self).__init__(name='cpupkg',
1091*9c5db199SXin Li                                              non_c0_stat='non-C0_C1')
1092*9c5db199SXin Li
1093*9c5db199SXin Li
1094*9c5db199SXin Li    def _read_stats(self):
1095*9c5db199SXin Li        packages = set()
1096*9c5db199SXin Li        template = '/sys/devices/system/cpu/cpu%s/topology/physical_package_id'
1097*9c5db199SXin Li        if not self._platform_states:
1098*9c5db199SXin Li            return {}
1099*9c5db199SXin Li        stats = dict((state, 0) for state in self._platform_states)
1100*9c5db199SXin Li        stats['C0_C1'] = 0
1101*9c5db199SXin Li        stats['non-C0_C1'] = 0
1102*9c5db199SXin Li
1103*9c5db199SXin Li        for cpu in os.listdir('/dev/cpu'):
1104*9c5db199SXin Li            if not os.path.exists(template % cpu):
1105*9c5db199SXin Li                continue
1106*9c5db199SXin Li            package = utils.read_one_line(template % cpu)
1107*9c5db199SXin Li            if package in packages:
1108*9c5db199SXin Li                continue
1109*9c5db199SXin Li            packages.add(package)
1110*9c5db199SXin Li
1111*9c5db199SXin Li            stats['C0_C1'] += utils.rdmsr(0x10, cpu) # TSC
1112*9c5db199SXin Li            for (state, msr) in self._platform_states.items():
1113*9c5db199SXin Li                ticks = utils.rdmsr(msr, cpu)
1114*9c5db199SXin Li                stats[state] += ticks
1115*9c5db199SXin Li                stats['non-C0_C1'] += ticks
1116*9c5db199SXin Li                stats['C0_C1'] -= ticks
1117*9c5db199SXin Li
1118*9c5db199SXin Li        return stats
1119*9c5db199SXin Li
1120*9c5db199SXin Li
1121*9c5db199SXin Liclass DevFreqStats(AbstractStats):
1122*9c5db199SXin Li    """
1123*9c5db199SXin Li    Devfreq device frequency stats.
1124*9c5db199SXin Li    """
1125*9c5db199SXin Li
1126*9c5db199SXin Li    _DIR = '/sys/class/devfreq'
1127*9c5db199SXin Li
1128*9c5db199SXin Li
1129*9c5db199SXin Li    def __init__(self, path):
1130*9c5db199SXin Li        """Constructs DevFreqStats Object that track frequency stats
1131*9c5db199SXin Li        for the path of the given Devfreq device.
1132*9c5db199SXin Li
1133*9c5db199SXin Li        The frequencies for devfreq devices are listed in Hz.
1134*9c5db199SXin Li
1135*9c5db199SXin Li        Args:
1136*9c5db199SXin Li            path: the path to the devfreq device
1137*9c5db199SXin Li
1138*9c5db199SXin Li        Example:
1139*9c5db199SXin Li            /sys/class/devfreq/dmc
1140*9c5db199SXin Li        """
1141*9c5db199SXin Li        self._path = os.path.join(self._DIR, path)
1142*9c5db199SXin Li        if not os.path.exists(self._path):
1143*9c5db199SXin Li            raise error.TestError(
1144*9c5db199SXin Li                    'DevFreqStats: devfreq device does not exist: %s' %
1145*9c5db199SXin Li                    self._path)
1146*9c5db199SXin Li
1147*9c5db199SXin Li        fname = os.path.join(self._path, 'available_frequencies')
1148*9c5db199SXin Li        af = utils.read_one_line(fname).strip()
1149*9c5db199SXin Li        self._available_freqs = sorted(af.split(), key=int)
1150*9c5db199SXin Li
1151*9c5db199SXin Li        super(DevFreqStats, self).__init__(path)
1152*9c5db199SXin Li
1153*9c5db199SXin Li    def _read_stats(self):
1154*9c5db199SXin Li        stats = dict((freq, 0) for freq in self._available_freqs)
1155*9c5db199SXin Li        fname = os.path.join(self._path, 'trans_stat')
1156*9c5db199SXin Li
1157*9c5db199SXin Li        with open(fname) as fd:
1158*9c5db199SXin Li            # The lines that contain the time in each frequency start on the 3rd
1159*9c5db199SXin Li            # line, so skip the first 2 lines. The last line contains the number
1160*9c5db199SXin Li            # of transitions, so skip that line too.
1161*9c5db199SXin Li            # The time in each frequency is at the end of the line.
1162*9c5db199SXin Li            freq_pattern = re.compile(r'\d+(?=:)')
1163*9c5db199SXin Li            for line in fd.readlines()[2:-1]:
1164*9c5db199SXin Li                freq = freq_pattern.search(line)
1165*9c5db199SXin Li                if freq and freq.group() in self._available_freqs:
1166*9c5db199SXin Li                    stats[freq.group()] = int(line.strip().split()[-1])
1167*9c5db199SXin Li
1168*9c5db199SXin Li        return stats
1169*9c5db199SXin Li
1170*9c5db199SXin Li
1171*9c5db199SXin Liclass GPUFreqStats(AbstractStats):
1172*9c5db199SXin Li    """GPU Frequency statistics class.
1173*9c5db199SXin Li
1174*9c5db199SXin Li    TODO(tbroch): add stats for other GPUs
1175*9c5db199SXin Li    """
1176*9c5db199SXin Li
1177*9c5db199SXin Li    _MALI_DEV = '/sys/class/misc/mali0/device'
1178*9c5db199SXin Li    _MALI_EVENTS = ['mali_dvfs:mali_dvfs_set_clock']
1179*9c5db199SXin Li    _MALI_TRACE_CLK_RE = \
1180*9c5db199SXin Li            r'kworker.* (\d+\.\d+): mali_dvfs_set_clock: frequency=(\d+)\d{6}'
1181*9c5db199SXin Li
1182*9c5db199SXin Li    _I915_ROOT = '/sys/kernel/debug/dri/0'
1183*9c5db199SXin Li    _I915_EVENTS = ['i915:intel_gpu_freq_change']
1184*9c5db199SXin Li    _I915_CLKS_FILES = ['i915_cur_delayinfo', 'i915_frequency_info']
1185*9c5db199SXin Li    _I915_TRACE_CLK_RE = \
1186*9c5db199SXin Li            r'kworker.* (\d+\.\d+): intel_gpu_freq_change: new_freq=(\d+)'
1187*9c5db199SXin Li    _I915_CUR_FREQ_RE = r'CAGF:\s+(\d+)MHz'
1188*9c5db199SXin Li    _I915_MIN_FREQ_RE = r'Lowest \(RPN\) frequency:\s+(\d+)MHz'
1189*9c5db199SXin Li    _I915_MAX_FREQ_RE = r'Max non-overclocked \(RP0\) frequency:\s+(\d+)MHz'
1190*9c5db199SXin Li    # There are 6 frequency steps per 100 MHz
1191*9c5db199SXin Li    _I915_FREQ_STEPS = [0, 17, 33, 50, 67, 83]
1192*9c5db199SXin Li
1193*9c5db199SXin Li    _gpu_type = None
1194*9c5db199SXin Li
1195*9c5db199SXin Li
1196*9c5db199SXin Li    def _get_mali_freqs(self):
1197*9c5db199SXin Li        """Get mali clocks based on kernel version.
1198*9c5db199SXin Li
1199*9c5db199SXin Li        For 3.8-3.18:
1200*9c5db199SXin Li            # cat /sys/class/misc/mali0/device/clock
1201*9c5db199SXin Li            100000000
1202*9c5db199SXin Li            # cat /sys/class/misc/mali0/device/available_frequencies
1203*9c5db199SXin Li            100000000
1204*9c5db199SXin Li            160000000
1205*9c5db199SXin Li            266000000
1206*9c5db199SXin Li            350000000
1207*9c5db199SXin Li            400000000
1208*9c5db199SXin Li            450000000
1209*9c5db199SXin Li            533000000
1210*9c5db199SXin Li            533000000
1211*9c5db199SXin Li
1212*9c5db199SXin Li        For 4.4+:
1213*9c5db199SXin Li            Tracked in DevFreqStats
1214*9c5db199SXin Li
1215*9c5db199SXin Li        Returns:
1216*9c5db199SXin Li          cur_mhz: string of current GPU clock in mhz
1217*9c5db199SXin Li        """
1218*9c5db199SXin Li        cur_mhz = None
1219*9c5db199SXin Li        fqs = []
1220*9c5db199SXin Li
1221*9c5db199SXin Li        fname = os.path.join(self._MALI_DEV, 'clock')
1222*9c5db199SXin Li        if os.path.exists(fname):
1223*9c5db199SXin Li            cur_mhz = str(int(int(utils.read_one_line(fname).strip()) / 1e6))
1224*9c5db199SXin Li            fname = os.path.join(self._MALI_DEV, 'available_frequencies')
1225*9c5db199SXin Li            with open(fname) as fd:
1226*9c5db199SXin Li                for ln in fd.readlines():
1227*9c5db199SXin Li                    freq = int(int(ln.strip()) / 1e6)
1228*9c5db199SXin Li                    fqs.append(str(freq))
1229*9c5db199SXin Li                fqs.sort()
1230*9c5db199SXin Li
1231*9c5db199SXin Li        self._freqs = fqs
1232*9c5db199SXin Li        return cur_mhz
1233*9c5db199SXin Li
1234*9c5db199SXin Li
1235*9c5db199SXin Li    def __init__(self, incremental=False):
1236*9c5db199SXin Li
1237*9c5db199SXin Li
1238*9c5db199SXin Li        min_mhz = None
1239*9c5db199SXin Li        max_mhz = None
1240*9c5db199SXin Li        cur_mhz = None
1241*9c5db199SXin Li        events = None
1242*9c5db199SXin Li        i915_path = None
1243*9c5db199SXin Li        self._freqs = []
1244*9c5db199SXin Li        self._prev_sample = None
1245*9c5db199SXin Li        self._trace = None
1246*9c5db199SXin Li
1247*9c5db199SXin Li        if os.path.exists(self._MALI_DEV) and \
1248*9c5db199SXin Li           not os.path.exists(os.path.join(self._MALI_DEV, "devfreq")):
1249*9c5db199SXin Li            self._set_gpu_type('mali')
1250*9c5db199SXin Li        else:
1251*9c5db199SXin Li            for file_name in self._I915_CLKS_FILES:
1252*9c5db199SXin Li                full_path = os.path.join(self._I915_ROOT, file_name)
1253*9c5db199SXin Li                if os.path.exists(full_path):
1254*9c5db199SXin Li                    self._set_gpu_type('i915')
1255*9c5db199SXin Li                    i915_path = full_path
1256*9c5db199SXin Li                    break
1257*9c5db199SXin Li            else:
1258*9c5db199SXin Li                # We either don't know how to track GPU stats (yet) or the stats
1259*9c5db199SXin Li                # are tracked in DevFreqStats.
1260*9c5db199SXin Li                self._set_gpu_type(None)
1261*9c5db199SXin Li
1262*9c5db199SXin Li        logging.debug("gpu_type is %s", self._gpu_type)
1263*9c5db199SXin Li
1264*9c5db199SXin Li        if self._gpu_type is 'mali':
1265*9c5db199SXin Li            events = self._MALI_EVENTS
1266*9c5db199SXin Li            cur_mhz = self._get_mali_freqs()
1267*9c5db199SXin Li            if self._freqs:
1268*9c5db199SXin Li                min_mhz = self._freqs[0]
1269*9c5db199SXin Li                max_mhz = self._freqs[-1]
1270*9c5db199SXin Li
1271*9c5db199SXin Li        elif self._gpu_type is 'i915':
1272*9c5db199SXin Li            events = self._I915_EVENTS
1273*9c5db199SXin Li            with open(i915_path) as fd:
1274*9c5db199SXin Li                for ln in fd.readlines():
1275*9c5db199SXin Li                    logging.debug("ln = %s", ln.strip())
1276*9c5db199SXin Li                    result = re.findall(self._I915_CUR_FREQ_RE, ln)
1277*9c5db199SXin Li                    if result:
1278*9c5db199SXin Li                        cur_mhz = result[0]
1279*9c5db199SXin Li                        continue
1280*9c5db199SXin Li                    result = re.findall(self._I915_MIN_FREQ_RE, ln)
1281*9c5db199SXin Li                    if result:
1282*9c5db199SXin Li                        min_mhz = result[0]
1283*9c5db199SXin Li                        continue
1284*9c5db199SXin Li                    result = re.findall(self._I915_MAX_FREQ_RE, ln)
1285*9c5db199SXin Li                    if result:
1286*9c5db199SXin Li                        max_mhz = result[0]
1287*9c5db199SXin Li                        continue
1288*9c5db199SXin Li                if min_mhz and max_mhz:
1289*9c5db199SXin Li                    real_min_mhz = min(int(min_mhz), int(cur_mhz))
1290*9c5db199SXin Li                    for i in range(real_min_mhz, int(max_mhz) + 1):
1291*9c5db199SXin Li                        if i % 100 in self._I915_FREQ_STEPS:
1292*9c5db199SXin Li                            self._freqs.append(str(i))
1293*9c5db199SXin Li
1294*9c5db199SXin Li        logging.debug("cur_mhz = %s, min_mhz = %s, max_mhz = %s", cur_mhz,
1295*9c5db199SXin Li                      min_mhz, max_mhz)
1296*9c5db199SXin Li
1297*9c5db199SXin Li        if cur_mhz and min_mhz and max_mhz:
1298*9c5db199SXin Li            self._trace = kernel_trace.KernelTrace(events=events)
1299*9c5db199SXin Li
1300*9c5db199SXin Li        # Not all platforms or kernel versions support tracing.
1301*9c5db199SXin Li        if not self._trace or not self._trace.is_tracing():
1302*9c5db199SXin Li            logging.warning("GPU frequency tracing not enabled.")
1303*9c5db199SXin Li        else:
1304*9c5db199SXin Li            self._prev_sample = (cur_mhz, self._trace.uptime_secs())
1305*9c5db199SXin Li            logging.debug("Current GPU freq: %s", cur_mhz)
1306*9c5db199SXin Li            logging.debug("All GPU freqs: %s", self._freqs)
1307*9c5db199SXin Li
1308*9c5db199SXin Li        super(GPUFreqStats, self).__init__('gpufreq', incremental=incremental)
1309*9c5db199SXin Li
1310*9c5db199SXin Li
1311*9c5db199SXin Li    @classmethod
1312*9c5db199SXin Li    def _set_gpu_type(cls, gpu_type):
1313*9c5db199SXin Li        cls._gpu_type = gpu_type
1314*9c5db199SXin Li
1315*9c5db199SXin Li
1316*9c5db199SXin Li    def _read_stats(self):
1317*9c5db199SXin Li        if self._gpu_type:
1318*9c5db199SXin Li            return getattr(self, "_%s_read_stats" % self._gpu_type)()
1319*9c5db199SXin Li        return {}
1320*9c5db199SXin Li
1321*9c5db199SXin Li
1322*9c5db199SXin Li    def _trace_read_stats(self, regexp):
1323*9c5db199SXin Li        """Read GPU stats from kernel trace outputs.
1324*9c5db199SXin Li
1325*9c5db199SXin Li        Args:
1326*9c5db199SXin Li            regexp: regular expression to match trace output for frequency
1327*9c5db199SXin Li
1328*9c5db199SXin Li        Returns:
1329*9c5db199SXin Li            Dict with key string in mhz and val float in seconds.
1330*9c5db199SXin Li        """
1331*9c5db199SXin Li        if not self._prev_sample:
1332*9c5db199SXin Li            return {}
1333*9c5db199SXin Li
1334*9c5db199SXin Li        stats = dict((k, 0.0) for k in self._freqs)
1335*9c5db199SXin Li        results = self._trace.read(regexp=regexp)
1336*9c5db199SXin Li        for (tstamp_str, freq) in results:
1337*9c5db199SXin Li            tstamp = float(tstamp_str)
1338*9c5db199SXin Li
1339*9c5db199SXin Li            # do not reparse lines in trace buffer
1340*9c5db199SXin Li            if tstamp <= self._prev_sample[1]:
1341*9c5db199SXin Li                continue
1342*9c5db199SXin Li            delta = tstamp - self._prev_sample[1]
1343*9c5db199SXin Li            logging.debug("freq:%s tstamp:%f - %f delta:%f",
1344*9c5db199SXin Li                          self._prev_sample[0],
1345*9c5db199SXin Li                          tstamp, self._prev_sample[1],
1346*9c5db199SXin Li                          delta)
1347*9c5db199SXin Li            stats[self._prev_sample[0]] += delta
1348*9c5db199SXin Li            self._prev_sample = (freq, tstamp)
1349*9c5db199SXin Li
1350*9c5db199SXin Li        # Do last record
1351*9c5db199SXin Li        delta = self._trace.uptime_secs() - self._prev_sample[1]
1352*9c5db199SXin Li        logging.debug("freq:%s tstamp:uptime - %f delta:%f",
1353*9c5db199SXin Li                      self._prev_sample[0],
1354*9c5db199SXin Li                      self._prev_sample[1], delta)
1355*9c5db199SXin Li        stats[self._prev_sample[0]] += delta
1356*9c5db199SXin Li
1357*9c5db199SXin Li        logging.debug("GPU freq percents:%s", stats)
1358*9c5db199SXin Li        return stats
1359*9c5db199SXin Li
1360*9c5db199SXin Li
1361*9c5db199SXin Li    def _mali_read_stats(self):
1362*9c5db199SXin Li        """Read Mali GPU stats
1363*9c5db199SXin Li
1364*9c5db199SXin Li        Frequencies are reported in Hz, so use a regex that drops the last 6
1365*9c5db199SXin Li        digits.
1366*9c5db199SXin Li
1367*9c5db199SXin Li        Output in trace looks like this:
1368*9c5db199SXin Li
1369*9c5db199SXin Li            kworker/u:24-5220  [000] .... 81060.329232: mali_dvfs_set_clock: frequency=400
1370*9c5db199SXin Li            kworker/u:24-5220  [000] .... 81061.830128: mali_dvfs_set_clock: frequency=350
1371*9c5db199SXin Li
1372*9c5db199SXin Li        Returns:
1373*9c5db199SXin Li            Dict with frequency in mhz as key and float in seconds for time
1374*9c5db199SXin Li              spent at that frequency.
1375*9c5db199SXin Li        """
1376*9c5db199SXin Li        return self._trace_read_stats(self._MALI_TRACE_CLK_RE)
1377*9c5db199SXin Li
1378*9c5db199SXin Li
1379*9c5db199SXin Li    def _i915_read_stats(self):
1380*9c5db199SXin Li        """Read i915 GPU stats.
1381*9c5db199SXin Li
1382*9c5db199SXin Li        Output looks like this (kernel >= 3.8):
1383*9c5db199SXin Li
1384*9c5db199SXin Li          kworker/u:0-28247 [000] .... 259391.579610: intel_gpu_freq_change: new_freq=400
1385*9c5db199SXin Li          kworker/u:0-28247 [000] .... 259391.581797: intel_gpu_freq_change: new_freq=350
1386*9c5db199SXin Li
1387*9c5db199SXin Li        Returns:
1388*9c5db199SXin Li            Dict with frequency in mhz as key and float in seconds for time
1389*9c5db199SXin Li              spent at that frequency.
1390*9c5db199SXin Li        """
1391*9c5db199SXin Li        return self._trace_read_stats(self._I915_TRACE_CLK_RE)
1392*9c5db199SXin Li
1393*9c5db199SXin Li
1394*9c5db199SXin Li    def _supports_automatic_weighted_average(self):
1395*9c5db199SXin Li        return self._gpu_type is not None
1396*9c5db199SXin Li
1397*9c5db199SXin Li
1398*9c5db199SXin Liclass USBSuspendStats(AbstractStats):
1399*9c5db199SXin Li    """
1400*9c5db199SXin Li    USB active/suspend statistics (over all devices)
1401*9c5db199SXin Li    """
1402*9c5db199SXin Li    # TODO (snanda): handle hot (un)plugging of USB devices
1403*9c5db199SXin Li    # TODO (snanda): handle duration counters wraparound
1404*9c5db199SXin Li
1405*9c5db199SXin Li    def __init__(self):
1406*9c5db199SXin Li        usb_stats_path = '/sys/bus/usb/devices/*/power'
1407*9c5db199SXin Li        self._file_paths = glob.glob(usb_stats_path)
1408*9c5db199SXin Li        if not self._file_paths:
1409*9c5db199SXin Li            logging.debug('USB stats path not found')
1410*9c5db199SXin Li        super(USBSuspendStats, self).__init__('usb')
1411*9c5db199SXin Li
1412*9c5db199SXin Li
1413*9c5db199SXin Li    def _read_stats(self):
1414*9c5db199SXin Li        usb_stats = {'active': 0, 'suspended': 0}
1415*9c5db199SXin Li
1416*9c5db199SXin Li        for path in self._file_paths:
1417*9c5db199SXin Li            active_duration_path = os.path.join(path, 'active_duration')
1418*9c5db199SXin Li            total_duration_path = os.path.join(path, 'connected_duration')
1419*9c5db199SXin Li
1420*9c5db199SXin Li            if not os.path.exists(active_duration_path) or \
1421*9c5db199SXin Li               not os.path.exists(total_duration_path):
1422*9c5db199SXin Li                logging.debug('duration paths do not exist for: %s', path)
1423*9c5db199SXin Li                continue
1424*9c5db199SXin Li
1425*9c5db199SXin Li            active = int(utils.read_file(active_duration_path))
1426*9c5db199SXin Li            total = int(utils.read_file(total_duration_path))
1427*9c5db199SXin Li            logging.debug('device %s active for %.2f%%',
1428*9c5db199SXin Li                          path, active * 100.0 / total)
1429*9c5db199SXin Li
1430*9c5db199SXin Li            usb_stats['active'] += active
1431*9c5db199SXin Li            usb_stats['suspended'] += total - active
1432*9c5db199SXin Li
1433*9c5db199SXin Li        return usb_stats
1434*9c5db199SXin Li
1435*9c5db199SXin Li
1436*9c5db199SXin Lidef read_cpu_set(filename):
1437*9c5db199SXin Li    """
1438*9c5db199SXin Li    Parse data of form "0,2-4,9"
1439*9c5db199SXin Li
1440*9c5db199SXin Li    Return a set of ints
1441*9c5db199SXin Li    """
1442*9c5db199SXin Li    data = utils.read_file(filename)
1443*9c5db199SXin Li    ret = set()
1444*9c5db199SXin Li
1445*9c5db199SXin Li    for entry in data.split(','):
1446*9c5db199SXin Li        entry_data = entry.split('-')
1447*9c5db199SXin Li        start = end = int(entry_data[0])
1448*9c5db199SXin Li        if len(entry_data) > 1:
1449*9c5db199SXin Li            end = int(entry_data[1])
1450*9c5db199SXin Li        ret |= set(range(start, end + 1))
1451*9c5db199SXin Li    return ret
1452*9c5db199SXin Li
1453*9c5db199SXin Li
1454*9c5db199SXin Lidef get_cpu_sibling_groups():
1455*9c5db199SXin Li    """
1456*9c5db199SXin Li    Get CPU core groups in HMP systems.
1457*9c5db199SXin Li
1458*9c5db199SXin Li    In systems with both small core and big core,
1459*9c5db199SXin Li    returns groups of small and big sibling groups.
1460*9c5db199SXin Li    """
1461*9c5db199SXin Li    siblings_suffix = 'topology/core_siblings_list'
1462*9c5db199SXin Li    sibling_groups = []
1463*9c5db199SXin Li    cpus_processed = set()
1464*9c5db199SXin Li    cpus, sibling_file_paths = get_cpus_filepaths_for_suffix(get_online_cpus(),
1465*9c5db199SXin Li                                                             siblings_suffix)
1466*9c5db199SXin Li    for c, siblings_path in zip(cpus, sibling_file_paths):
1467*9c5db199SXin Li        if c in cpus_processed:
1468*9c5db199SXin Li            # This cpu is already part of a sibling group. Skip.
1469*9c5db199SXin Li            continue
1470*9c5db199SXin Li        sibling_group = read_cpu_set(siblings_path)
1471*9c5db199SXin Li        cpus_processed |= sibling_group
1472*9c5db199SXin Li        sibling_groups.append(frozenset(sibling_group))
1473*9c5db199SXin Li    return tuple(sibling_groups)
1474*9c5db199SXin Li
1475*9c5db199SXin Li
1476*9c5db199SXin Lidef get_available_cpu_stats():
1477*9c5db199SXin Li    """Return CPUFreq/CPUIdleStats groups by big-small siblings groups."""
1478*9c5db199SXin Li    ret = [CPUPackageStats()]
1479*9c5db199SXin Li    cpu_sibling_groups = get_cpu_sibling_groups()
1480*9c5db199SXin Li
1481*9c5db199SXin Li    cpufreq_stat_class = CPUFreqStats
1482*9c5db199SXin Li    # assumes cpufreq driver for CPU0 is the same as the others.
1483*9c5db199SXin Li    cpufreq_driver = '/sys/devices/system/cpu/cpu0/cpufreq/scaling_driver'
1484*9c5db199SXin Li    if (os.path.exists(cpufreq_driver) and
1485*9c5db199SXin Li        utils.read_one_line(cpufreq_driver) == 'intel_pstate'):
1486*9c5db199SXin Li        logging.debug('intel_pstate driver active')
1487*9c5db199SXin Li        cpufreq_stat_class = CPUFreqStatsPState
1488*9c5db199SXin Li
1489*9c5db199SXin Li    if not cpu_sibling_groups:
1490*9c5db199SXin Li        ret.append(cpufreq_stat_class())
1491*9c5db199SXin Li        ret.append(CPUIdleStats())
1492*9c5db199SXin Li    for cpu_group in cpu_sibling_groups:
1493*9c5db199SXin Li        ret.append(cpufreq_stat_class(cpu_group))
1494*9c5db199SXin Li        ret.append(CPUIdleStats(cpu_group))
1495*9c5db199SXin Li    if has_rc6_support():
1496*9c5db199SXin Li        ret.append(GPURC6Stats())
1497*9c5db199SXin Li    return ret
1498*9c5db199SXin Li
1499*9c5db199SXin Li
1500*9c5db199SXin Liclass StatoMatic(object):
1501*9c5db199SXin Li    """Class to aggregate and monitor a bunch of power related statistics."""
1502*9c5db199SXin Li    def __init__(self):
1503*9c5db199SXin Li        self._start_uptime_secs = kernel_trace.KernelTrace.uptime_secs()
1504*9c5db199SXin Li        self._astats = [USBSuspendStats(), GPUFreqStats(incremental=False)]
1505*9c5db199SXin Li        self._astats.extend(get_available_cpu_stats())
1506*9c5db199SXin Li        if os.path.isdir(DevFreqStats._DIR):
1507*9c5db199SXin Li            self._astats.extend([DevFreqStats(f) for f in \
1508*9c5db199SXin Li                                 os.listdir(DevFreqStats._DIR)])
1509*9c5db199SXin Li
1510*9c5db199SXin Li        self._disk = DiskStateLogger()
1511*9c5db199SXin Li        self._disk.start()
1512*9c5db199SXin Li
1513*9c5db199SXin Li
1514*9c5db199SXin Li    def publish(self):
1515*9c5db199SXin Li        """Publishes results of various statistics gathered.
1516*9c5db199SXin Li
1517*9c5db199SXin Li        Returns:
1518*9c5db199SXin Li            dict with
1519*9c5db199SXin Li              key = string 'percent_<name>_<key>_time'
1520*9c5db199SXin Li              value = float in percent
1521*9c5db199SXin Li        """
1522*9c5db199SXin Li        results = {}
1523*9c5db199SXin Li        tot_secs = kernel_trace.KernelTrace.uptime_secs() - \
1524*9c5db199SXin Li            self._start_uptime_secs
1525*9c5db199SXin Li        for stat_obj in self._astats:
1526*9c5db199SXin Li            percent_stats = stat_obj.refresh()
1527*9c5db199SXin Li            logging.debug("pstats = %s", percent_stats)
1528*9c5db199SXin Li            if stat_obj.name is 'gpu':
1529*9c5db199SXin Li                # TODO(tbroch) remove this once GPU freq stats have proved
1530*9c5db199SXin Li                # reliable
1531*9c5db199SXin Li                stats_secs = sum(stat_obj._stats.values())
1532*9c5db199SXin Li                if stats_secs < (tot_secs * 0.9) or \
1533*9c5db199SXin Li                        stats_secs > (tot_secs * 1.1):
1534*9c5db199SXin Li                    logging.warning('%s stats dont look right.  Not publishing.',
1535*9c5db199SXin Li                                 stat_obj.name)
1536*9c5db199SXin Li                    continue
1537*9c5db199SXin Li            new_res = {}
1538*9c5db199SXin Li            AbstractStats.format_results_percent(new_res, stat_obj.name,
1539*9c5db199SXin Li                                                 percent_stats)
1540*9c5db199SXin Li            wavg = stat_obj.weighted_average()
1541*9c5db199SXin Li            if wavg:
1542*9c5db199SXin Li                AbstractStats.format_results_wavg(new_res, stat_obj.name, wavg)
1543*9c5db199SXin Li
1544*9c5db199SXin Li            results.update(new_res)
1545*9c5db199SXin Li
1546*9c5db199SXin Li        new_res = {}
1547*9c5db199SXin Li        if self._disk.get_error():
1548*9c5db199SXin Li            new_res['disk_logging_error'] = str(self._disk.get_error())
1549*9c5db199SXin Li        else:
1550*9c5db199SXin Li            AbstractStats.format_results_percent(new_res, 'disk',
1551*9c5db199SXin Li                                                 self._disk.result())
1552*9c5db199SXin Li        results.update(new_res)
1553*9c5db199SXin Li
1554*9c5db199SXin Li        return results
1555*9c5db199SXin Li
1556*9c5db199SXin Li
1557*9c5db199SXin Liclass PowerMeasurement(object):
1558*9c5db199SXin Li    """Class to measure power.
1559*9c5db199SXin Li
1560*9c5db199SXin Li    Public attributes:
1561*9c5db199SXin Li        domain: String name of the power domain being measured.  Example is
1562*9c5db199SXin Li          'system' for total system power
1563*9c5db199SXin Li
1564*9c5db199SXin Li    Public methods:
1565*9c5db199SXin Li        refresh: Performs any power/energy sampling and calculation and returns
1566*9c5db199SXin Li            power as float in watts.  This method MUST be implemented in
1567*9c5db199SXin Li            subclass.
1568*9c5db199SXin Li    """
1569*9c5db199SXin Li
1570*9c5db199SXin Li    def __init__(self, domain):
1571*9c5db199SXin Li        """Constructor."""
1572*9c5db199SXin Li        self.domain = domain
1573*9c5db199SXin Li
1574*9c5db199SXin Li
1575*9c5db199SXin Li    def refresh(self):
1576*9c5db199SXin Li        """Performs any power/energy sampling and calculation.
1577*9c5db199SXin Li
1578*9c5db199SXin Li        MUST be implemented in subclass
1579*9c5db199SXin Li
1580*9c5db199SXin Li        Returns:
1581*9c5db199SXin Li            float, power in watts.
1582*9c5db199SXin Li        """
1583*9c5db199SXin Li        raise NotImplementedError("'refresh' method should be implemented in "
1584*9c5db199SXin Li                                  "subclass.")
1585*9c5db199SXin Li
1586*9c5db199SXin Li
1587*9c5db199SXin Lidef parse_power_supply_info():
1588*9c5db199SXin Li    """Parses power_supply_info command output.
1589*9c5db199SXin Li
1590*9c5db199SXin Li    Command output from power_manager ( tools/power_supply_info.cc ) looks like
1591*9c5db199SXin Li    this:
1592*9c5db199SXin Li
1593*9c5db199SXin Li        Device: Line Power
1594*9c5db199SXin Li          path:               /sys/class/power_supply/cros_ec-charger
1595*9c5db199SXin Li          ...
1596*9c5db199SXin Li        Device: Battery
1597*9c5db199SXin Li          path:               /sys/class/power_supply/sbs-9-000b
1598*9c5db199SXin Li          ...
1599*9c5db199SXin Li
1600*9c5db199SXin Li    """
1601*9c5db199SXin Li    rv = collections.defaultdict(dict)
1602*9c5db199SXin Li    dev = None
1603*9c5db199SXin Li    for ln in utils.system_output('power_supply_info').splitlines():
1604*9c5db199SXin Li        logging.debug("psu: %s", ln)
1605*9c5db199SXin Li        result = re.findall(r'^Device:\s+(.*)', ln)
1606*9c5db199SXin Li        if result:
1607*9c5db199SXin Li            dev = result[0]
1608*9c5db199SXin Li            continue
1609*9c5db199SXin Li        result = re.findall(r'\s+(.+):\s+(.+)', ln)
1610*9c5db199SXin Li        if result and dev:
1611*9c5db199SXin Li            kname = re.findall(r'(.*)\s+\(\w+\)', result[0][0])
1612*9c5db199SXin Li            if kname:
1613*9c5db199SXin Li                rv[dev][kname[0]] = result[0][1]
1614*9c5db199SXin Li            else:
1615*9c5db199SXin Li                rv[dev][result[0][0]] = result[0][1]
1616*9c5db199SXin Li
1617*9c5db199SXin Li    return rv
1618*9c5db199SXin Li
1619*9c5db199SXin Li
1620*9c5db199SXin Liclass SystemPower(PowerMeasurement):
1621*9c5db199SXin Li    """Class to measure system power.
1622*9c5db199SXin Li
1623*9c5db199SXin Li    TODO(tbroch): This class provides a subset of functionality in BatteryStat
1624*9c5db199SXin Li    in hopes of minimizing power draw.  Investigate whether its really
1625*9c5db199SXin Li    significant and if not, deprecate.
1626*9c5db199SXin Li
1627*9c5db199SXin Li    Private Attributes:
1628*9c5db199SXin Li      _voltage_file: path to retrieve voltage in uvolts
1629*9c5db199SXin Li      _current_file: path to retrieve current in uamps
1630*9c5db199SXin Li    """
1631*9c5db199SXin Li
1632*9c5db199SXin Li    def __init__(self, battery_dir):
1633*9c5db199SXin Li        """Constructor.
1634*9c5db199SXin Li
1635*9c5db199SXin Li        Args:
1636*9c5db199SXin Li            battery_dir: path to dir containing the files to probe and log.
1637*9c5db199SXin Li                usually something like /sys/class/power_supply/BAT0/
1638*9c5db199SXin Li        """
1639*9c5db199SXin Li        super(SystemPower, self).__init__('system')
1640*9c5db199SXin Li        # Files to log voltage and current from
1641*9c5db199SXin Li        self._voltage_file = os.path.join(battery_dir, 'voltage_now')
1642*9c5db199SXin Li        self._current_file = os.path.join(battery_dir, 'current_now')
1643*9c5db199SXin Li
1644*9c5db199SXin Li
1645*9c5db199SXin Li    def refresh(self):
1646*9c5db199SXin Li        """refresh method.
1647*9c5db199SXin Li
1648*9c5db199SXin Li        See superclass PowerMeasurement for details.
1649*9c5db199SXin Li        """
1650*9c5db199SXin Li        keyvals = parse_power_supply_info()
1651*9c5db199SXin Li        return float(keyvals['Battery']['energy rate'])
1652*9c5db199SXin Li
1653*9c5db199SXin Li
1654*9c5db199SXin Liclass BatteryStateOfCharge(PowerMeasurement):
1655*9c5db199SXin Li    """Class for logging battery state of charge."""
1656*9c5db199SXin Li
1657*9c5db199SXin Li    def __init__(self):
1658*9c5db199SXin Li        """Constructor."""
1659*9c5db199SXin Li        super(BatteryStateOfCharge, self).__init__('battery_soc')
1660*9c5db199SXin Li
1661*9c5db199SXin Li    def refresh(self):
1662*9c5db199SXin Li        """refresh method.
1663*9c5db199SXin Li
1664*9c5db199SXin Li        See superclass PowerMeasurement for details.
1665*9c5db199SXin Li        """
1666*9c5db199SXin Li        keyvals = parse_power_supply_info()
1667*9c5db199SXin Li        return float(keyvals['Battery']['percentage'])
1668*9c5db199SXin Li
1669*9c5db199SXin Li
1670*9c5db199SXin Liclass CheckpointLogger(object):
1671*9c5db199SXin Li    """Class to log checkpoint data.
1672*9c5db199SXin Li
1673*9c5db199SXin Li    Public attributes:
1674*9c5db199SXin Li        checkpoint_data: dictionary of (tname, tlist).
1675*9c5db199SXin Li            tname: String of testname associated with these time intervals
1676*9c5db199SXin Li            tlist: list of tuples.  Tuple contains:
1677*9c5db199SXin Li                tstart: Float of time when subtest started
1678*9c5db199SXin Li                tend: Float of time when subtest ended
1679*9c5db199SXin Li
1680*9c5db199SXin Li    Public methods:
1681*9c5db199SXin Li        start: records a start timestamp
1682*9c5db199SXin Li        checkpoint
1683*9c5db199SXin Li        checkblock
1684*9c5db199SXin Li        save_checkpoint_data
1685*9c5db199SXin Li        load_checkpoint_data
1686*9c5db199SXin Li
1687*9c5db199SXin Li    Static methods:
1688*9c5db199SXin Li        load_checkpoint_data_static
1689*9c5db199SXin Li
1690*9c5db199SXin Li    Private attributes:
1691*9c5db199SXin Li       _start_time: start timestamp for checkpoint logger
1692*9c5db199SXin Li    """
1693*9c5db199SXin Li
1694*9c5db199SXin Li    def __init__(self):
1695*9c5db199SXin Li        self.checkpoint_data = collections.defaultdict(list)
1696*9c5db199SXin Li        self.start()
1697*9c5db199SXin Li
1698*9c5db199SXin Li    # If multiple MeasurementLoggers call start() on the same CheckpointLogger,
1699*9c5db199SXin Li    # the latest one will register start time.
1700*9c5db199SXin Li    def start(self):
1701*9c5db199SXin Li        """Set start time for CheckpointLogger."""
1702*9c5db199SXin Li        self._start_time = time.time()
1703*9c5db199SXin Li
1704*9c5db199SXin Li    @contextlib.contextmanager
1705*9c5db199SXin Li    def checkblock(self, tname=''):
1706*9c5db199SXin Li        """Check point for the following block with test tname.
1707*9c5db199SXin Li
1708*9c5db199SXin Li        Args:
1709*9c5db199SXin Li            tname: String of testname associated with this time interval
1710*9c5db199SXin Li        """
1711*9c5db199SXin Li        start_time = time.time()
1712*9c5db199SXin Li        yield
1713*9c5db199SXin Li        self.checkpoint(tname, start_time)
1714*9c5db199SXin Li
1715*9c5db199SXin Li    def checkpoint(self, tname='', tstart=None, tend=None):
1716*9c5db199SXin Li        """Check point the times in seconds associated with test tname.
1717*9c5db199SXin Li
1718*9c5db199SXin Li        Args:
1719*9c5db199SXin Li            tname: String of testname associated with this time interval
1720*9c5db199SXin Li            tstart: Float in seconds of when tname test started. Should be based
1721*9c5db199SXin Li                off time.time(). If None, use start timestamp for the checkpoint
1722*9c5db199SXin Li                logger.
1723*9c5db199SXin Li            tend: Float in seconds of when tname test ended. Should be based
1724*9c5db199SXin Li                off time.time(). If None, then value computed in the method.
1725*9c5db199SXin Li        """
1726*9c5db199SXin Li        if not tstart and self._start_time:
1727*9c5db199SXin Li            tstart = self._start_time
1728*9c5db199SXin Li        if not tend:
1729*9c5db199SXin Li            tend = time.time()
1730*9c5db199SXin Li        self.checkpoint_data[tname].append((tstart, tend))
1731*9c5db199SXin Li        logging.info('Finished test "%s" between timestamps [%s, %s]',
1732*9c5db199SXin Li                     tname, tstart, tend)
1733*9c5db199SXin Li
1734*9c5db199SXin Li    def convert_relative(self, start_time=None):
1735*9c5db199SXin Li        """Convert data from power_status.CheckpointLogger object to relative
1736*9c5db199SXin Li        checkpoint data dictionary. Timestamps are converted to time in seconds
1737*9c5db199SXin Li        since the test started.
1738*9c5db199SXin Li
1739*9c5db199SXin Li        Args:
1740*9c5db199SXin Li            start_time: Float in seconds of the desired start time reference.
1741*9c5db199SXin Li                Should be based off time.time(). If None, use start timestamp
1742*9c5db199SXin Li                for the checkpoint logger.
1743*9c5db199SXin Li        """
1744*9c5db199SXin Li        if start_time is None:
1745*9c5db199SXin Li            start_time = self._start_time
1746*9c5db199SXin Li
1747*9c5db199SXin Li        checkpoint_dict = {}
1748*9c5db199SXin Li        for tname, tlist in self.checkpoint_data.items():
1749*9c5db199SXin Li            checkpoint_dict[tname] = [(tstart - start_time, tend - start_time)
1750*9c5db199SXin Li                    for tstart, tend in tlist]
1751*9c5db199SXin Li
1752*9c5db199SXin Li        return checkpoint_dict
1753*9c5db199SXin Li
1754*9c5db199SXin Li    def save_checkpoint_data(self, resultsdir, fname=CHECKPOINT_LOG_DEFAULT_FNAME):
1755*9c5db199SXin Li        """Save checkpoint data.
1756*9c5db199SXin Li
1757*9c5db199SXin Li        Args:
1758*9c5db199SXin Li            resultsdir: String, directory to write results to
1759*9c5db199SXin Li            fname: String, name of file to write results to
1760*9c5db199SXin Li        """
1761*9c5db199SXin Li        fname = os.path.join(resultsdir, fname)
1762*9c5db199SXin Li        with open(fname, 'wt') as f:
1763*9c5db199SXin Li            json.dump(self.checkpoint_data, f, indent=4, separators=(',', ': '))
1764*9c5db199SXin Li
1765*9c5db199SXin Li    def load_checkpoint_data(self, resultsdir,
1766*9c5db199SXin Li                             fname=CHECKPOINT_LOG_DEFAULT_FNAME):
1767*9c5db199SXin Li        """Load checkpoint data.
1768*9c5db199SXin Li
1769*9c5db199SXin Li        Args:
1770*9c5db199SXin Li            resultsdir: String, directory to load results from
1771*9c5db199SXin Li            fname: String, name of file to load results from
1772*9c5db199SXin Li        """
1773*9c5db199SXin Li        fname = os.path.join(resultsdir, fname)
1774*9c5db199SXin Li        try:
1775*9c5db199SXin Li            with open(fname, 'r') as f:
1776*9c5db199SXin Li                self.checkpoint_data = json.load(f,
1777*9c5db199SXin Li                                                 object_hook=to_checkpoint_data)
1778*9c5db199SXin Li                # Set start time to the earliest start timestamp in file.
1779*9c5db199SXin Li                self._start_time = min(
1780*9c5db199SXin Li                        ts_pair[0]
1781*9c5db199SXin Li                        for ts_pair in itertools.chain.from_iterable(
1782*9c5db199SXin Li                                list(self.checkpoint_data.values())))
1783*9c5db199SXin Li        except Exception as exc:
1784*9c5db199SXin Li            logging.warning('Failed to load checkpoint data from json file %s, '
1785*9c5db199SXin Li                            'see exception: %s', fname, exc)
1786*9c5db199SXin Li
1787*9c5db199SXin Li    @staticmethod
1788*9c5db199SXin Li    def load_checkpoint_data_static(resultsdir,
1789*9c5db199SXin Li                                    fname=CHECKPOINT_LOG_DEFAULT_FNAME):
1790*9c5db199SXin Li        """Load checkpoint data.
1791*9c5db199SXin Li
1792*9c5db199SXin Li        Args:
1793*9c5db199SXin Li            resultsdir: String, directory to load results from
1794*9c5db199SXin Li            fname: String, name of file to load results from
1795*9c5db199SXin Li        """
1796*9c5db199SXin Li        fname = os.path.join(resultsdir, fname)
1797*9c5db199SXin Li        with open(fname, 'r') as f:
1798*9c5db199SXin Li            checkpoint_data = json.load(f)
1799*9c5db199SXin Li        return checkpoint_data
1800*9c5db199SXin Li
1801*9c5db199SXin Li
1802*9c5db199SXin Lidef to_checkpoint_data(json_dict):
1803*9c5db199SXin Li    """Helper method to translate json object into CheckpointLogger format.
1804*9c5db199SXin Li
1805*9c5db199SXin Li    Args:
1806*9c5db199SXin Li        json_dict: a json object in the format of python dict
1807*9c5db199SXin Li    Returns:
1808*9c5db199SXin Li        a defaultdict in CheckpointLogger data format
1809*9c5db199SXin Li    """
1810*9c5db199SXin Li    checkpoint_data = collections.defaultdict(list)
1811*9c5db199SXin Li    for tname, tlist in json_dict.items():
1812*9c5db199SXin Li        checkpoint_data[tname].extend([tuple(ts_pair) for ts_pair in tlist])
1813*9c5db199SXin Li    return checkpoint_data
1814*9c5db199SXin Li
1815*9c5db199SXin Li
1816*9c5db199SXin Lidef get_checkpoint_logger_from_file(resultsdir,
1817*9c5db199SXin Li                                    fname=CHECKPOINT_LOG_DEFAULT_FNAME):
1818*9c5db199SXin Li    """Create a CheckpointLogger and load checkpoint data from file.
1819*9c5db199SXin Li
1820*9c5db199SXin Li    Args:
1821*9c5db199SXin Li        resultsdir: String, directory to load results from
1822*9c5db199SXin Li        fname: String, name of file to load results from
1823*9c5db199SXin Li    Returns:
1824*9c5db199SXin Li        CheckpointLogger with data from file
1825*9c5db199SXin Li    """
1826*9c5db199SXin Li    checkpoint_logger = CheckpointLogger()
1827*9c5db199SXin Li    checkpoint_logger.load_checkpoint_data(resultsdir, fname)
1828*9c5db199SXin Li    return checkpoint_logger
1829*9c5db199SXin Li
1830*9c5db199SXin Li
1831*9c5db199SXin Liclass MeasurementLogger(threading.Thread):
1832*9c5db199SXin Li    """A thread that logs measurement readings.
1833*9c5db199SXin Li
1834*9c5db199SXin Li    Example code snippet:
1835*9c5db199SXin Li        my_logger = MeasurementLogger([Measurement1, Measurement2])
1836*9c5db199SXin Li        my_logger.start()
1837*9c5db199SXin Li        for testname in tests:
1838*9c5db199SXin Li            # Option 1: use checkblock
1839*9c5db199SXin Li            with my_logger.checkblock(testname):
1840*9c5db199SXin Li               # run the test method for testname
1841*9c5db199SXin Li
1842*9c5db199SXin Li            # Option 2: use checkpoint
1843*9c5db199SXin Li            start_time = time.time()
1844*9c5db199SXin Li            # run the test method for testname
1845*9c5db199SXin Li            my_logger.checkpoint(testname, start_time)
1846*9c5db199SXin Li
1847*9c5db199SXin Li        keyvals = my_logger.calc()
1848*9c5db199SXin Li
1849*9c5db199SXin Li    or using CheckpointLogger:
1850*9c5db199SXin Li        checkpoint_logger = CheckpointLogger()
1851*9c5db199SXin Li        my_logger = MeasurementLogger([Measurement1, Measurement2],
1852*9c5db199SXin Li                                      checkpoint_logger)
1853*9c5db199SXin Li        my_logger.start()
1854*9c5db199SXin Li        for testname in tests:
1855*9c5db199SXin Li            # Option 1: use checkblock
1856*9c5db199SXin Li            with checkpoint_logger.checkblock(testname):
1857*9c5db199SXin Li               # run the test method for testname
1858*9c5db199SXin Li
1859*9c5db199SXin Li            # Option 2: use checkpoint
1860*9c5db199SXin Li            start_time = time.time()
1861*9c5db199SXin Li            # run the test method for testname
1862*9c5db199SXin Li            checkpoint_logger.checkpoint(testname, start_time)
1863*9c5db199SXin Li
1864*9c5db199SXin Li        keyvals = my_logger.calc()
1865*9c5db199SXin Li
1866*9c5db199SXin Li    Public attributes:
1867*9c5db199SXin Li        seconds_period: float, probing interval in seconds.
1868*9c5db199SXin Li        readings: list of lists of floats of measurements.
1869*9c5db199SXin Li        times: list of floats of time (since Epoch) of when measurements
1870*9c5db199SXin Li            occurred.  len(time) == len(readings).
1871*9c5db199SXin Li        done: flag to stop the logger.
1872*9c5db199SXin Li        domains: list of  domain strings being measured
1873*9c5db199SXin Li
1874*9c5db199SXin Li    Public methods:
1875*9c5db199SXin Li        run: launches the thread to gather measurements
1876*9c5db199SXin Li        refresh: perform data samplings for every measurements
1877*9c5db199SXin Li        calc: calculates
1878*9c5db199SXin Li        save_results:
1879*9c5db199SXin Li
1880*9c5db199SXin Li    Private attributes:
1881*9c5db199SXin Li        _measurements: list of Measurement objects to be sampled.
1882*9c5db199SXin Li        _checkpoint_data: dictionary of (tname, tlist).
1883*9c5db199SXin Li            tname: String of testname associated with these time intervals
1884*9c5db199SXin Li            tlist: list of tuples.  Tuple contains:
1885*9c5db199SXin Li                tstart: Float of time when subtest started
1886*9c5db199SXin Li                tend: Float of time when subtest ended
1887*9c5db199SXin Li        _results: list of results tuples.  Tuple contains:
1888*9c5db199SXin Li            prefix: String of subtest
1889*9c5db199SXin Li            mean: Float of mean  in watts
1890*9c5db199SXin Li            std: Float of standard deviation of measurements
1891*9c5db199SXin Li            tstart: Float of time when subtest started
1892*9c5db199SXin Li            tend: Float of time when subtest ended
1893*9c5db199SXin Li    """
1894*9c5db199SXin Li    def __init__(self, measurements, seconds_period=1.0, checkpoint_logger=None):
1895*9c5db199SXin Li        """Initialize a logger.
1896*9c5db199SXin Li
1897*9c5db199SXin Li        Args:
1898*9c5db199SXin Li            _measurements: list of Measurement objects to be sampled.
1899*9c5db199SXin Li            seconds_period: float, probing interval in seconds.  Default 1.0
1900*9c5db199SXin Li        """
1901*9c5db199SXin Li        threading.Thread.__init__(self)
1902*9c5db199SXin Li
1903*9c5db199SXin Li        self.seconds_period = seconds_period
1904*9c5db199SXin Li
1905*9c5db199SXin Li        self.readings = []
1906*9c5db199SXin Li        self.times = []
1907*9c5db199SXin Li
1908*9c5db199SXin Li        self._measurements = measurements
1909*9c5db199SXin Li        self.domains = [meas.domain for meas in self._measurements]
1910*9c5db199SXin Li
1911*9c5db199SXin Li        self._checkpoint_logger = \
1912*9c5db199SXin Li            checkpoint_logger if checkpoint_logger else CheckpointLogger()
1913*9c5db199SXin Li
1914*9c5db199SXin Li        self.done = False
1915*9c5db199SXin Li
1916*9c5db199SXin Li    def start(self):
1917*9c5db199SXin Li        self._checkpoint_logger.start()
1918*9c5db199SXin Li        super(MeasurementLogger, self).start()
1919*9c5db199SXin Li
1920*9c5db199SXin Li    def refresh(self):
1921*9c5db199SXin Li        """Perform data samplings for every measurements.
1922*9c5db199SXin Li
1923*9c5db199SXin Li        Returns:
1924*9c5db199SXin Li            list of sampled data for every measurements.
1925*9c5db199SXin Li        """
1926*9c5db199SXin Li        return [meas.refresh() for meas in self._measurements]
1927*9c5db199SXin Li
1928*9c5db199SXin Li    def run(self):
1929*9c5db199SXin Li        """Threads run method."""
1930*9c5db199SXin Li        loop = 1
1931*9c5db199SXin Li        start_time = time.time()
1932*9c5db199SXin Li        time.sleep(self.seconds_period)
1933*9c5db199SXin Li        while(not self.done):
1934*9c5db199SXin Li            # TODO (dbasehore): We probably need proper locking in this file
1935*9c5db199SXin Li            # since there have been race conditions with modifying and accessing
1936*9c5db199SXin Li            # data.
1937*9c5db199SXin Li            self.readings.append(self.refresh())
1938*9c5db199SXin Li            current_time = time.time()
1939*9c5db199SXin Li            self.times.append(current_time)
1940*9c5db199SXin Li            loop += 1
1941*9c5db199SXin Li            next_measurement_time = start_time + loop * self.seconds_period
1942*9c5db199SXin Li            time.sleep(next_measurement_time - current_time)
1943*9c5db199SXin Li
1944*9c5db199SXin Li    @contextlib.contextmanager
1945*9c5db199SXin Li    def checkblock(self, tname=''):
1946*9c5db199SXin Li        """Check point for the following block with test tname.
1947*9c5db199SXin Li
1948*9c5db199SXin Li        Args:
1949*9c5db199SXin Li            tname: String of testname associated with this time interval
1950*9c5db199SXin Li        """
1951*9c5db199SXin Li        start_time = time.time()
1952*9c5db199SXin Li        yield
1953*9c5db199SXin Li        self.checkpoint(tname, start_time)
1954*9c5db199SXin Li
1955*9c5db199SXin Li    def checkpoint(self, tname='', tstart=None, tend=None):
1956*9c5db199SXin Li        """Just a thin method calling the CheckpointLogger checkpoint method.
1957*9c5db199SXin Li
1958*9c5db199SXin Li        Args:
1959*9c5db199SXin Li           tname: String of testname associated with this time interval
1960*9c5db199SXin Li           tstart: Float in seconds of when tname test started.  Should be based
1961*9c5db199SXin Li                off time.time()
1962*9c5db199SXin Li           tend: Float in seconds of when tname test ended.  Should be based
1963*9c5db199SXin Li                off time.time().  If None, then value computed in the method.
1964*9c5db199SXin Li        """
1965*9c5db199SXin Li        self._checkpoint_logger.checkpoint(tname, tstart, tend)
1966*9c5db199SXin Li
1967*9c5db199SXin Li    # TODO(seankao): It might be useful to pull this method to CheckpointLogger,
1968*9c5db199SXin Li    # to allow checkpoint usage without an explicit MeasurementLogger.
1969*9c5db199SXin Li    def calc(self, mtype=None):
1970*9c5db199SXin Li        """Calculate average measurement during each of the sub-tests.
1971*9c5db199SXin Li
1972*9c5db199SXin Li        Method performs the following steps:
1973*9c5db199SXin Li            1. Signals the thread to stop running.
1974*9c5db199SXin Li            2. Calculates mean, max, min, count on the samples for each of the
1975*9c5db199SXin Li               measurements.
1976*9c5db199SXin Li            3. Stores results to be written later.
1977*9c5db199SXin Li            4. Creates keyvals for autotest publishing.
1978*9c5db199SXin Li
1979*9c5db199SXin Li        Args:
1980*9c5db199SXin Li            mtype: string of measurement type.  For example:
1981*9c5db199SXin Li                   pwr == power
1982*9c5db199SXin Li                   temp == temperature
1983*9c5db199SXin Li        Returns:
1984*9c5db199SXin Li            dict of keyvals suitable for autotest results.
1985*9c5db199SXin Li        """
1986*9c5db199SXin Li        if not mtype:
1987*9c5db199SXin Li            mtype = 'meas'
1988*9c5db199SXin Li
1989*9c5db199SXin Li        t = numpy.array(self.times)
1990*9c5db199SXin Li        keyvals = {}
1991*9c5db199SXin Li        results  = [('domain', 'mean', 'std', 'duration (s)', 'start ts',
1992*9c5db199SXin Li                     'end ts')]
1993*9c5db199SXin Li        # TODO(coconutruben): ensure that values is meaningful i.e. go through
1994*9c5db199SXin Li        # the Loggers and add a unit attribute to each so that the raw
1995*9c5db199SXin Li        # data is readable.
1996*9c5db199SXin Li        raw_results = [('domain', 'values (%s)' % mtype)]
1997*9c5db199SXin Li
1998*9c5db199SXin Li        if not self.done:
1999*9c5db199SXin Li            self.done = True
2000*9c5db199SXin Li        # times 2 the sleep time in order to allow for readings as well.
2001*9c5db199SXin Li        self.join(timeout=self.seconds_period * 2)
2002*9c5db199SXin Li
2003*9c5db199SXin Li        if not self._checkpoint_logger.checkpoint_data:
2004*9c5db199SXin Li            self._checkpoint_logger.checkpoint()
2005*9c5db199SXin Li
2006*9c5db199SXin Li        for i, domain_readings in enumerate(zip(*self.readings)):
2007*9c5db199SXin Li            meas = numpy.array(domain_readings)
2008*9c5db199SXin Li            try:
2009*9c5db199SXin Li                domain = self.domains[i]
2010*9c5db199SXin Li            except IndexError:
2011*9c5db199SXin Li                # TODO (evanbenn) temp logging for b:162610351
2012*9c5db199SXin Li                logging.debug('b:162610351 IndexError: %s, %d, %d, (%s)',
2013*9c5db199SXin Li                              type(self).__name__,
2014*9c5db199SXin Li                              len(self.readings),
2015*9c5db199SXin Li                              len(self.domains),
2016*9c5db199SXin Li                              ', '.join(str(len(r)) for r in self.readings))
2017*9c5db199SXin Li                logging.debug('b:162610351 domains: %s',
2018*9c5db199SXin Li                              ', '.join(self.domains))
2019*9c5db199SXin Li                raise
2020*9c5db199SXin Li
2021*9c5db199SXin Li            for tname, tlist in \
2022*9c5db199SXin Li                    self._checkpoint_logger.checkpoint_data.items():
2023*9c5db199SXin Li                if tname:
2024*9c5db199SXin Li                    prefix = '%s_%s' % (tname, domain)
2025*9c5db199SXin Li                else:
2026*9c5db199SXin Li                    prefix = domain
2027*9c5db199SXin Li                keyvals[prefix+'_duration'] = 0
2028*9c5db199SXin Li                # Select all readings taken between tstart and tend
2029*9c5db199SXin Li                # timestamps in tlist.
2030*9c5db199SXin Li                masks = []
2031*9c5db199SXin Li                for tstart, tend in tlist:
2032*9c5db199SXin Li                    keyvals[prefix+'_duration'] += tend - tstart
2033*9c5db199SXin Li                    # Try block just in case
2034*9c5db199SXin Li                    # code.google.com/p/chromium/issues/detail?id=318892
2035*9c5db199SXin Li                    # is not fixed.
2036*9c5db199SXin Li                    try:
2037*9c5db199SXin Li                        masks.append(numpy.logical_and(tstart < t, t < tend))
2038*9c5db199SXin Li                    except ValueError as e:
2039*9c5db199SXin Li                        logging.debug('Error logging measurements: %s', str(e))
2040*9c5db199SXin Li                        logging.debug('timestamps %d %s', t.len, t)
2041*9c5db199SXin Li                        logging.debug('timestamp start, end %f %f', tstart, tend)
2042*9c5db199SXin Li                        logging.debug('measurements %d %s', meas.len, meas)
2043*9c5db199SXin Li                mask = numpy.logical_or.reduce(masks)
2044*9c5db199SXin Li                meas_array = meas[mask]
2045*9c5db199SXin Li
2046*9c5db199SXin Li                # If sub-test terminated early, avoid calculating avg, std and
2047*9c5db199SXin Li                # min
2048*9c5db199SXin Li                if not meas_array.size:
2049*9c5db199SXin Li                    continue
2050*9c5db199SXin Li                meas_mean = meas_array.mean()
2051*9c5db199SXin Li                meas_std = meas_array.std()
2052*9c5db199SXin Li
2053*9c5db199SXin Li                # Results list can be used for pretty printing and saving as csv
2054*9c5db199SXin Li                # TODO(seankao): new results format?
2055*9c5db199SXin Li                result = (prefix, meas_mean, meas_std)
2056*9c5db199SXin Li                for tstart, tend in tlist:
2057*9c5db199SXin Li                    result = result + (tend - tstart, tstart, tend)
2058*9c5db199SXin Li                results.append(result)
2059*9c5db199SXin Li                raw_results.append((prefix,) + tuple(meas_array.tolist()))
2060*9c5db199SXin Li
2061*9c5db199SXin Li                keyvals[prefix + '_' + mtype + '_avg'] = meas_mean
2062*9c5db199SXin Li                keyvals[prefix + '_' + mtype + '_cnt'] = meas_array.size
2063*9c5db199SXin Li                keyvals[prefix + '_' + mtype + '_max'] = meas_array.max()
2064*9c5db199SXin Li                keyvals[prefix + '_' + mtype + '_min'] = meas_array.min()
2065*9c5db199SXin Li                keyvals[prefix + '_' + mtype + '_std'] = meas_std
2066*9c5db199SXin Li        self._results = results
2067*9c5db199SXin Li        self._raw_results = raw_results
2068*9c5db199SXin Li        return keyvals
2069*9c5db199SXin Li
2070*9c5db199SXin Li
2071*9c5db199SXin Li    def save_results(self, resultsdir, fname_prefix=None):
2072*9c5db199SXin Li        """Save computed results in a nice tab-separated format.
2073*9c5db199SXin Li        This is useful for long manual runs.
2074*9c5db199SXin Li
2075*9c5db199SXin Li        Args:
2076*9c5db199SXin Li            resultsdir: String, directory to write results to
2077*9c5db199SXin Li            fname_prefix: prefix to use for fname. If provided outfiles
2078*9c5db199SXin Li                          will be [fname]_[raw|summary].txt
2079*9c5db199SXin Li        """
2080*9c5db199SXin Li        if not fname_prefix:
2081*9c5db199SXin Li            fname_prefix = 'meas_results_%.0f' % time.time()
2082*9c5db199SXin Li        fname = '%s_summary.txt' % fname_prefix
2083*9c5db199SXin Li        raw_fname = fname.replace('summary', 'raw')
2084*9c5db199SXin Li        for name, data in [(fname, self._results),
2085*9c5db199SXin Li                           (raw_fname, self._raw_results)]:
2086*9c5db199SXin Li            with open(os.path.join(resultsdir, name), 'wt') as f:
2087*9c5db199SXin Li                # First row contains the headers
2088*9c5db199SXin Li                f.write('%s\n' % '\t'.join(data[0]))
2089*9c5db199SXin Li                for row in data[1:]:
2090*9c5db199SXin Li                    # First column name, rest are numbers. See _calc_power()
2091*9c5db199SXin Li                    fmt_row = [row[0]] + ['%.2f' % x for x in row[1:]]
2092*9c5db199SXin Li                    f.write('%s\n' % '\t'.join(fmt_row))
2093*9c5db199SXin Li
2094*9c5db199SXin Li
2095*9c5db199SXin Liclass CPUStatsLogger(MeasurementLogger):
2096*9c5db199SXin Li    """Class to measure CPU Frequency and CPU Idle Stats.
2097*9c5db199SXin Li
2098*9c5db199SXin Li    CPUStatsLogger derived from MeasurementLogger class but overload data
2099*9c5db199SXin Li    samplings method because MeasurementLogger assumed that each sampling is
2100*9c5db199SXin Li    independent to each other. However, in this case it is not. For example,
2101*9c5db199SXin Li    CPU time spent in C0 state is measure by time not spent in all other states.
2102*9c5db199SXin Li
2103*9c5db199SXin Li    CPUStatsLogger also collects the weight average in each time period if the
2104*9c5db199SXin Li    underlying AbstractStats support weight average function.
2105*9c5db199SXin Li
2106*9c5db199SXin Li    Private attributes:
2107*9c5db199SXin Li       _stats: list of CPU AbstractStats objects to be sampled.
2108*9c5db199SXin Li       _refresh_count: number of times refresh() has been called.
2109*9c5db199SXin Li       _last_wavg: dict of wavg when refresh() was last called.
2110*9c5db199SXin Li    """
2111*9c5db199SXin Li    def __init__(self, seconds_period=1.0, checkpoint_logger=None):
2112*9c5db199SXin Li        """Initialize a CPUStatsLogger.
2113*9c5db199SXin Li
2114*9c5db199SXin Li        Args:
2115*9c5db199SXin Li            seconds_period: float, probing interval in seconds.  Default 1.0
2116*9c5db199SXin Li        """
2117*9c5db199SXin Li        # We don't use measurements since CPU stats can't measure separately.
2118*9c5db199SXin Li        super(CPUStatsLogger, self).__init__([], seconds_period, checkpoint_logger)
2119*9c5db199SXin Li
2120*9c5db199SXin Li        self._stats = get_available_cpu_stats()
2121*9c5db199SXin Li        self._stats.append(GPUFreqStats())
2122*9c5db199SXin Li        self.domains = []
2123*9c5db199SXin Li        self._refresh_count = 0
2124*9c5db199SXin Li        self._last_wavg = collections.defaultdict(int)
2125*9c5db199SXin Li
2126*9c5db199SXin Li    def _set_domains(self):
2127*9c5db199SXin Li        self.domains = []
2128*9c5db199SXin Li        for stat in self._stats:
2129*9c5db199SXin Li            self.domains.extend([stat.name + '_' + str(state_name)
2130*9c5db199SXin Li                                 for state_name in stat.refresh()])
2131*9c5db199SXin Li            if stat.weighted_average():
2132*9c5db199SXin Li                self.domains.append('wavg_' + stat.name)
2133*9c5db199SXin Li
2134*9c5db199SXin Li    def refresh(self):
2135*9c5db199SXin Li        self._refresh_count += 1
2136*9c5db199SXin Li        count = self._refresh_count
2137*9c5db199SXin Li        ret = []
2138*9c5db199SXin Li        for stat in self._stats:
2139*9c5db199SXin Li            ret.extend(list(stat.refresh().values()))
2140*9c5db199SXin Li            wavg = stat.weighted_average()
2141*9c5db199SXin Li            if wavg:
2142*9c5db199SXin Li                if stat.incremental:
2143*9c5db199SXin Li                    last_wavg = self._last_wavg[stat.name]
2144*9c5db199SXin Li                    self._last_wavg[stat.name] = wavg
2145*9c5db199SXin Li                    # Calculate weight average in this period using current
2146*9c5db199SXin Li                    # total weight average and last total weight average.
2147*9c5db199SXin Li                    # The result will lose some precision with higher number of
2148*9c5db199SXin Li                    # count but still good enough for 11 significant digits even
2149*9c5db199SXin Li                    # if we logged the data every 1 second for a day.
2150*9c5db199SXin Li                    ret.append(wavg * count - last_wavg * (count - 1))
2151*9c5db199SXin Li                else:
2152*9c5db199SXin Li                    ret.append(wavg)
2153*9c5db199SXin Li        if not self.domains:
2154*9c5db199SXin Li            self._set_domains()
2155*9c5db199SXin Li        elif len(self.domains) != len(ret):
2156*9c5db199SXin Li            # This would make data jumble but better than IndexError.
2157*9c5db199SXin Li            # Add the log to help detecting the root cause.
2158*9c5db199SXin Li            logging.warning('b:162610351 len(self.domains) != len(ret)')
2159*9c5db199SXin Li            logging.warning('old_domains: (%s)', ', '.join(self.domains))
2160*9c5db199SXin Li            self._set_domains()
2161*9c5db199SXin Li            logging.warning('new_domains: (%s)', ', '.join(self.domains))
2162*9c5db199SXin Li        return ret
2163*9c5db199SXin Li
2164*9c5db199SXin Li    def save_results(self, resultsdir, fname_prefix=None):
2165*9c5db199SXin Li        if not fname_prefix:
2166*9c5db199SXin Li            fname_prefix = 'cpu_results_%.0f' % time.time()
2167*9c5db199SXin Li        super(CPUStatsLogger, self).save_results(resultsdir, fname_prefix)
2168*9c5db199SXin Li
2169*9c5db199SXin Li
2170*9c5db199SXin Liclass PowerLogger(MeasurementLogger):
2171*9c5db199SXin Li    """Class to measure power consumption."""
2172*9c5db199SXin Li
2173*9c5db199SXin Li    def __init__(self, measurements, seconds_period=1.0,
2174*9c5db199SXin Li                 checkpoint_logger=None):
2175*9c5db199SXin Li        if not measurements:
2176*9c5db199SXin Li            measurements = self.create_measurements()
2177*9c5db199SXin Li        super(PowerLogger, self).__init__(measurements, seconds_period,
2178*9c5db199SXin Li                                          checkpoint_logger)
2179*9c5db199SXin Li
2180*9c5db199SXin Li    def create_measurements(self):
2181*9c5db199SXin Li        """Create power measurements based on device config."""
2182*9c5db199SXin Li        # Import here to avoid import loop.
2183*9c5db199SXin Li        from autotest_lib.client.cros.power import power_rapl
2184*9c5db199SXin Li
2185*9c5db199SXin Li        measurements = []
2186*9c5db199SXin Li        status = get_status()
2187*9c5db199SXin Li        if status.battery:
2188*9c5db199SXin Li            measurements.append(BatteryStateOfCharge())
2189*9c5db199SXin Li        if status.battery_discharging():
2190*9c5db199SXin Li            measurements.append(SystemPower(status.battery_path))
2191*9c5db199SXin Li        if power_utils.has_powercap_support():
2192*9c5db199SXin Li            measurements += power_rapl.create_powercap()
2193*9c5db199SXin Li        elif power_utils.has_rapl_support():
2194*9c5db199SXin Li            measurements += power_rapl.create_rapl()
2195*9c5db199SXin Li        return measurements
2196*9c5db199SXin Li
2197*9c5db199SXin Li    def save_results(self, resultsdir, fname_prefix=None):
2198*9c5db199SXin Li        if not fname_prefix:
2199*9c5db199SXin Li            fname_prefix = 'power_results_%.0f' % time.time()
2200*9c5db199SXin Li        super(PowerLogger, self).save_results(resultsdir, fname_prefix)
2201*9c5db199SXin Li
2202*9c5db199SXin Li    def calc(self, mtype='pwr'):
2203*9c5db199SXin Li        return super(PowerLogger, self).calc(mtype)
2204*9c5db199SXin Li
2205*9c5db199SXin Li
2206*9c5db199SXin Liclass TempMeasurement(object):
2207*9c5db199SXin Li    """Class to measure temperature.
2208*9c5db199SXin Li
2209*9c5db199SXin Li    Public attributes:
2210*9c5db199SXin Li        domain: String name of the temperature domain being measured.  Example is
2211*9c5db199SXin Li          'cpu' for cpu temperature
2212*9c5db199SXin Li
2213*9c5db199SXin Li    Private attributes:
2214*9c5db199SXin Li        _path: Path to temperature file to read ( in millidegrees Celsius )
2215*9c5db199SXin Li
2216*9c5db199SXin Li    Public methods:
2217*9c5db199SXin Li        refresh: Performs any temperature sampling and calculation and returns
2218*9c5db199SXin Li            temperature as float in degrees Celsius.
2219*9c5db199SXin Li    """
2220*9c5db199SXin Li    def __init__(self, domain, path):
2221*9c5db199SXin Li        """Constructor."""
2222*9c5db199SXin Li        self.domain = domain
2223*9c5db199SXin Li        self._path = path
2224*9c5db199SXin Li
2225*9c5db199SXin Li
2226*9c5db199SXin Li    def refresh(self):
2227*9c5db199SXin Li        """Performs temperature
2228*9c5db199SXin Li
2229*9c5db199SXin Li        Returns:
2230*9c5db199SXin Li            float, temperature in degrees Celsius
2231*9c5db199SXin Li        """
2232*9c5db199SXin Li        return int(utils.read_one_line(self._path)) / 1000.
2233*9c5db199SXin Li
2234*9c5db199SXin Li
2235*9c5db199SXin Liclass BatteryTempMeasurement(TempMeasurement):
2236*9c5db199SXin Li    """Class to measure battery temperature."""
2237*9c5db199SXin Li    def __init__(self):
2238*9c5db199SXin Li        super(BatteryTempMeasurement, self).__init__('battery', 'battery_temp')
2239*9c5db199SXin Li
2240*9c5db199SXin Li
2241*9c5db199SXin Li    def refresh(self):
2242*9c5db199SXin Li        """Perform battery temperature reading.
2243*9c5db199SXin Li
2244*9c5db199SXin Li        Returns:
2245*9c5db199SXin Li            float, temperature in degrees Celsius.
2246*9c5db199SXin Li        """
2247*9c5db199SXin Li        result = utils.run(self._path, timeout=5, ignore_status=True)
2248*9c5db199SXin Li
2249*9c5db199SXin Li        value = float(result.stdout)
2250*9c5db199SXin Li
2251*9c5db199SXin Li        # `battery_temp` return in celsius unit.
2252*9c5db199SXin Li        if 0 < value < 100:
2253*9c5db199SXin Li            return round(value, 1)
2254*9c5db199SXin Li
2255*9c5db199SXin Li        # `battery_temp` return in kelvin unit.
2256*9c5db199SXin Li        if 273 < value < 373:
2257*9c5db199SXin Li            return round(value - 273.15, 1)
2258*9c5db199SXin Li
2259*9c5db199SXin Li        # `battery_temp` return in millicelsius unit.
2260*9c5db199SXin Li        if 1000 < value < 100000:
2261*9c5db199SXin Li            return round(value / 1000., 1)
2262*9c5db199SXin Li
2263*9c5db199SXin Li        # The command return value in millikelvin unit.
2264*9c5db199SXin Li        if 273150 < value < 373150:
2265*9c5db199SXin Li            return round(value / 1000. - 273.15, 1)
2266*9c5db199SXin Li
2267*9c5db199SXin Li        raise ValueError
2268*9c5db199SXin Li
2269*9c5db199SXin Li
2270*9c5db199SXin Lidef has_battery_temp():
2271*9c5db199SXin Li    """Determine if DUT can provide battery temperature.
2272*9c5db199SXin Li
2273*9c5db199SXin Li    Returns:
2274*9c5db199SXin Li        Boolean, True if battery temperature available.  False otherwise.
2275*9c5db199SXin Li    """
2276*9c5db199SXin Li    if not power_utils.has_battery():
2277*9c5db199SXin Li        return False
2278*9c5db199SXin Li
2279*9c5db199SXin Li    btemp = BatteryTempMeasurement()
2280*9c5db199SXin Li    try:
2281*9c5db199SXin Li        btemp.refresh()
2282*9c5db199SXin Li    except ValueError:
2283*9c5db199SXin Li        return False
2284*9c5db199SXin Li
2285*9c5db199SXin Li    return True
2286*9c5db199SXin Li
2287*9c5db199SXin Li
2288*9c5db199SXin Liclass TempLogger(MeasurementLogger):
2289*9c5db199SXin Li    """A thread that logs temperature readings in millidegrees Celsius."""
2290*9c5db199SXin Li
2291*9c5db199SXin Li    def __init__(self, measurements, seconds_period=30.0,
2292*9c5db199SXin Li                 checkpoint_logger=None):
2293*9c5db199SXin Li        if not measurements:
2294*9c5db199SXin Li            measurements = self.create_measurements()
2295*9c5db199SXin Li        super(TempLogger, self).__init__(measurements, seconds_period,
2296*9c5db199SXin Li                                         checkpoint_logger)
2297*9c5db199SXin Li
2298*9c5db199SXin Li    def create_measurements(self):
2299*9c5db199SXin Li        """Create measurements for TempLogger."""
2300*9c5db199SXin Li        measurements = []
2301*9c5db199SXin Li
2302*9c5db199SXin Li        zstats = ThermalStatACPI()
2303*9c5db199SXin Li        for desc, fpath in zstats.zones.items():
2304*9c5db199SXin Li            new_meas = TempMeasurement(desc, fpath)
2305*9c5db199SXin Li            measurements.append(new_meas)
2306*9c5db199SXin Li
2307*9c5db199SXin Li        tstats = ThermalStatHwmon()
2308*9c5db199SXin Li        for kname in tstats.fields:
2309*9c5db199SXin Li            match = re.match(r'(\S+)_temp(\d+)_input', kname)
2310*9c5db199SXin Li            if not match:
2311*9c5db199SXin Li                continue
2312*9c5db199SXin Li            desc = match.group(1) + '-t' + match.group(2)
2313*9c5db199SXin Li            fpath = tstats.fields[kname][0]
2314*9c5db199SXin Li            new_meas = TempMeasurement(desc, fpath)
2315*9c5db199SXin Li            measurements.append(new_meas)
2316*9c5db199SXin Li
2317*9c5db199SXin Li        if has_battery_temp():
2318*9c5db199SXin Li            measurements.append(BatteryTempMeasurement())
2319*9c5db199SXin Li
2320*9c5db199SXin Li        return measurements
2321*9c5db199SXin Li
2322*9c5db199SXin Li    def save_results(self, resultsdir, fname_prefix=None):
2323*9c5db199SXin Li        if not fname_prefix:
2324*9c5db199SXin Li            fname_prefix = 'temp_results_%.0f' % time.time()
2325*9c5db199SXin Li        super(TempLogger, self).save_results(resultsdir, fname_prefix)
2326*9c5db199SXin Li
2327*9c5db199SXin Li
2328*9c5db199SXin Li    def calc(self, mtype='temp'):
2329*9c5db199SXin Li        return super(TempLogger, self).calc(mtype)
2330*9c5db199SXin Li
2331*9c5db199SXin Li
2332*9c5db199SXin Liclass VideoFpsLogger(MeasurementLogger):
2333*9c5db199SXin Li    """Class to measure Video FPS."""
2334*9c5db199SXin Li
2335*9c5db199SXin Li    @classmethod
2336*9c5db199SXin Li    def time_until_ready(cls, tab, num_video=1, timeout=120):
2337*9c5db199SXin Li        """Wait until tab is ready for VideoFpsLogger and return time used.
2338*9c5db199SXin Li
2339*9c5db199SXin Li        Keep polling Chrome tab until these 2 conditions are met one by one.
2340*9c5db199SXin Li        - Number of <video> elements detected is equal to |num_video|.
2341*9c5db199SXin Li        - All videos are played for at least 1 ms.
2342*9c5db199SXin Li
2343*9c5db199SXin Li        Args:
2344*9c5db199SXin Li            tab: Chrome tab object
2345*9c5db199SXin Li            num_video: number of expected <video> elements, default 1.
2346*9c5db199SXin Li            timeout: timeout in seconds, default 120.
2347*9c5db199SXin Li
2348*9c5db199SXin Li        Returns:
2349*9c5db199SXin Li            float, number of seconds elasped until condition met.
2350*9c5db199SXin Li
2351*9c5db199SXin Li        Raises:
2352*9c5db199SXin Li            error.TestFail if condition are not met by timeout.
2353*9c5db199SXin Li        """
2354*9c5db199SXin Li        start_time = time.time()
2355*9c5db199SXin Li
2356*9c5db199SXin Li        # Number of <video> elements detected is equal to |num_video|.
2357*9c5db199SXin Li        c = 'document.getElementsByTagName("video").length == %d' % num_video
2358*9c5db199SXin Li        tab.WaitForJavaScriptCondition(c, timeout=timeout)
2359*9c5db199SXin Li
2360*9c5db199SXin Li        # All videos are played for at least 1 ms.
2361*9c5db199SXin Li        c = ('Math.min(...Array.from(document.getElementsByTagName("video"))'
2362*9c5db199SXin Li             '.map(v => v.currentTime)) >= 0.001')
2363*9c5db199SXin Li        timeout_left = timeout - (time.time() - start_time)
2364*9c5db199SXin Li        try:
2365*9c5db199SXin Li            tab.WaitForJavaScriptCondition(c, timeout=timeout_left)
2366*9c5db199SXin Li        # Broad exception because py_utils.TimeoutException require libchrome
2367*9c5db199SXin Li        except Exception:
2368*9c5db199SXin Li            times = tab.EvaluateJavaScript(
2369*9c5db199SXin Li                    'Array.from(document.getElementsByTagName("video"))'
2370*9c5db199SXin Li                    '.map(v => v.currentTime)')
2371*9c5db199SXin Li            # Not timeout exception, re-raise original exception
2372*9c5db199SXin Li            if min(times) > 0.001:
2373*9c5db199SXin Li                raise
2374*9c5db199SXin Li            videos = tab.EvaluateJavaScript(
2375*9c5db199SXin Li                    'Array.from(document.getElementsByTagName("video"))'
2376*9c5db199SXin Li                    '.map(v => v.id)')
2377*9c5db199SXin Li            failed_videos = [v for v, t in zip(videos, times) if t < 0.001]
2378*9c5db199SXin Li            raise error.TestFail('Media playback failed: %s' % failed_videos)
2379*9c5db199SXin Li        return time.time() - start_time
2380*9c5db199SXin Li
2381*9c5db199SXin Li    def __init__(self, tab, seconds_period=1.0, checkpoint_logger=None):
2382*9c5db199SXin Li        """Initialize a VideoFpsLogger.
2383*9c5db199SXin Li
2384*9c5db199SXin Li        Args:
2385*9c5db199SXin Li            tab: Chrome tab object
2386*9c5db199SXin Li        """
2387*9c5db199SXin Li        super(VideoFpsLogger, self).__init__([], seconds_period,
2388*9c5db199SXin Li                                             checkpoint_logger)
2389*9c5db199SXin Li        self._tab = tab
2390*9c5db199SXin Li        names = self._tab.EvaluateJavaScript(
2391*9c5db199SXin Li            'Array.from(document.getElementsByTagName("video")).map(v => v.id)')
2392*9c5db199SXin Li        self.domains =  [n or 'video_' + str(i) for i, n in enumerate(names)]
2393*9c5db199SXin Li        self._last = [0] * len(names)
2394*9c5db199SXin Li        self.refresh()
2395*9c5db199SXin Li
2396*9c5db199SXin Li    def refresh(self):
2397*9c5db199SXin Li        @retry.retry(Exception, timeout_min=0.5, delay_sec=0.1)
2398*9c5db199SXin Li        def get_fps():
2399*9c5db199SXin Li            return self._tab.EvaluateJavaScript(
2400*9c5db199SXin Li                'Array.from(document.getElementsByTagName("video")).map('
2401*9c5db199SXin Li                'v => v.webkitDecodedFrameCount)')
2402*9c5db199SXin Li        current = get_fps()
2403*9c5db199SXin Li        fps = [(b - a if b >= a else b) / self.seconds_period
2404*9c5db199SXin Li               for a, b in zip(self._last , current)]
2405*9c5db199SXin Li        self._last = current
2406*9c5db199SXin Li        return fps
2407*9c5db199SXin Li
2408*9c5db199SXin Li    def save_results(self, resultsdir, fname_prefix=None):
2409*9c5db199SXin Li        if not fname_prefix:
2410*9c5db199SXin Li            fname_prefix = 'video_fps_results_%.0f' % time.time()
2411*9c5db199SXin Li        super(VideoFpsLogger, self).save_results(resultsdir, fname_prefix)
2412*9c5db199SXin Li
2413*9c5db199SXin Li    def calc(self, mtype='fps'):
2414*9c5db199SXin Li        return super(VideoFpsLogger, self).calc(mtype)
2415*9c5db199SXin Li
2416*9c5db199SXin Li
2417*9c5db199SXin Lidef get_num_fans():
2418*9c5db199SXin Li    """Count how many fan DUT has.
2419*9c5db199SXin Li
2420*9c5db199SXin Li    Returns:
2421*9c5db199SXin Li        Integer, number of fans that DUT has.
2422*9c5db199SXin Li    """
2423*9c5db199SXin Li    res = utils.run('ectool pwmgetnumfans | grep -o [0-9]', ignore_status=True)
2424*9c5db199SXin Li    if not res or res.exit_status != 0:
2425*9c5db199SXin Li        return 0
2426*9c5db199SXin Li    return int(res.stdout)
2427*9c5db199SXin Li
2428*9c5db199SXin Li
2429*9c5db199SXin Lidef has_fan():
2430*9c5db199SXin Li    """Determine if DUT has fan.
2431*9c5db199SXin Li
2432*9c5db199SXin Li    Returns:
2433*9c5db199SXin Li        Boolean, True if dut has fan.  False otherwise.
2434*9c5db199SXin Li    """
2435*9c5db199SXin Li    return get_num_fans() > 0
2436*9c5db199SXin Li
2437*9c5db199SXin Li
2438*9c5db199SXin Liclass FanRpmLogger(MeasurementLogger):
2439*9c5db199SXin Li    """Class to measure Fan RPM."""
2440*9c5db199SXin Li
2441*9c5db199SXin Li    def __init__(self, seconds_period=1.0, checkpoint_logger=None):
2442*9c5db199SXin Li        """Initialize a FanRpmLogger."""
2443*9c5db199SXin Li        super(FanRpmLogger, self).__init__([], seconds_period,
2444*9c5db199SXin Li                                             checkpoint_logger)
2445*9c5db199SXin Li        self.domains =  ['fan_' + str(i) for i in range(get_num_fans())]
2446*9c5db199SXin Li        self.refresh()
2447*9c5db199SXin Li
2448*9c5db199SXin Li    def refresh(self):
2449*9c5db199SXin Li        @retry.retry(Exception, timeout_min=0.1, delay_sec=2)
2450*9c5db199SXin Li        def get_fan_rpm_all():
2451*9c5db199SXin Li            """Some example outputs from ectool
2452*9c5db199SXin Li
2453*9c5db199SXin Li            * Two fan system
2454*9c5db199SXin Li            localhost ~ # ectool pwmgetfanrpm all
2455*9c5db199SXin Li            Fan 0 RPM: 4012
2456*9c5db199SXin Li            Fan 1 RPM: 4009
2457*9c5db199SXin Li
2458*9c5db199SXin Li            * One fan but its stalled
2459*9c5db199SXin Li            localhost ~ # ectool pwmgetfanrpm all
2460*9c5db199SXin Li            Fan 0 stalled!
2461*9c5db199SXin Li            """
2462*9c5db199SXin Li            cmd = 'ectool pwmgetfanrpm all'
2463*9c5db199SXin Li            res = utils.run(cmd, ignore_status=True,
2464*9c5db199SXin Li                            stdout_tee=utils.TEE_TO_LOGS,
2465*9c5db199SXin Li                            stderr_tee=utils.TEE_TO_LOGS)
2466*9c5db199SXin Li            rpm_data = []
2467*9c5db199SXin Li            for i, ln in enumerate(res.stdout.splitlines()):
2468*9c5db199SXin Li                if ln.find('stalled') != -1:
2469*9c5db199SXin Li                    rpm_data.append(0)
2470*9c5db199SXin Li                else:
2471*9c5db199SXin Li                    rpm_data.append(int(ln.split(':')[1]))
2472*9c5db199SXin Li            return rpm_data
2473*9c5db199SXin Li        return get_fan_rpm_all()
2474*9c5db199SXin Li
2475*9c5db199SXin Li    def save_results(self, resultsdir, fname_prefix=None):
2476*9c5db199SXin Li        if not fname_prefix:
2477*9c5db199SXin Li            fname_prefix = 'fan_rpm_results_%.0f' % time.time()
2478*9c5db199SXin Li        super(FanRpmLogger, self).save_results(resultsdir, fname_prefix)
2479*9c5db199SXin Li
2480*9c5db199SXin Li    def calc(self, mtype='rpm'):
2481*9c5db199SXin Li        return super(FanRpmLogger, self).calc(mtype)
2482*9c5db199SXin Li
2483*9c5db199SXin Li
2484*9c5db199SXin Liclass FreeMemoryLogger(MeasurementLogger):
2485*9c5db199SXin Li    """Class to measure free memory from /proc/meminfo in KB unit."""
2486*9c5db199SXin Li
2487*9c5db199SXin Li    def __init__(self, seconds_period=1.0, checkpoint_logger=None):
2488*9c5db199SXin Li        """Initialize a FreeMemoryLogger."""
2489*9c5db199SXin Li        super(FreeMemoryLogger, self).__init__([], seconds_period,
2490*9c5db199SXin Li                                               checkpoint_logger)
2491*9c5db199SXin Li        self.domains = ['MemFree', 'MemAvailable']
2492*9c5db199SXin Li        self.refresh()
2493*9c5db199SXin Li
2494*9c5db199SXin Li    def refresh(self):
2495*9c5db199SXin Li        return [
2496*9c5db199SXin Li                utils.read_from_meminfo('MemFree'),
2497*9c5db199SXin Li                utils.read_from_meminfo('MemAvailable')
2498*9c5db199SXin Li        ]
2499*9c5db199SXin Li
2500*9c5db199SXin Li    def save_results(self, resultsdir, fname_prefix=None):
2501*9c5db199SXin Li        if not fname_prefix:
2502*9c5db199SXin Li            fname_prefix = 'free_memory_results_%.0f' % time.time()
2503*9c5db199SXin Li        super(FreeMemoryLogger, self).save_results(resultsdir, fname_prefix)
2504*9c5db199SXin Li
2505*9c5db199SXin Li    def calc(self, mtype='kB'):
2506*9c5db199SXin Li        return super(FreeMemoryLogger, self).calc(mtype)
2507*9c5db199SXin Li
2508*9c5db199SXin Li
2509*9c5db199SXin Lidef create_measurement_loggers(seconds_period=20.0, checkpoint_logger=None):
2510*9c5db199SXin Li    """Create loggers for power test that is not test-specific.
2511*9c5db199SXin Li
2512*9c5db199SXin Li    Args:
2513*9c5db199SXin Li       seconds_period: float, probing interval in seconds. Default 20.0
2514*9c5db199SXin Li       checkpoint_logger: CheckpointLogger class for the loggers
2515*9c5db199SXin Li
2516*9c5db199SXin Li    Returns:
2517*9c5db199SXin Li        list of loggers created.
2518*9c5db199SXin Li    """
2519*9c5db199SXin Li    loggers = [
2520*9c5db199SXin Li            PowerLogger(None, seconds_period, checkpoint_logger),
2521*9c5db199SXin Li            TempLogger(None, seconds_period, checkpoint_logger),
2522*9c5db199SXin Li            CPUStatsLogger(seconds_period, checkpoint_logger),
2523*9c5db199SXin Li            FreeMemoryLogger(seconds_period, checkpoint_logger),
2524*9c5db199SXin Li    ]
2525*9c5db199SXin Li    if has_fan():
2526*9c5db199SXin Li        loggers.append(FanRpmLogger(seconds_period, checkpoint_logger))
2527*9c5db199SXin Li
2528*9c5db199SXin Li    return loggers
2529*9c5db199SXin Li
2530*9c5db199SXin Li
2531*9c5db199SXin Liclass DiskStateLogger(threading.Thread):
2532*9c5db199SXin Li    """Records the time percentages the disk stays in its different power modes.
2533*9c5db199SXin Li
2534*9c5db199SXin Li    Example code snippet:
2535*9c5db199SXin Li        mylogger = power_status.DiskStateLogger()
2536*9c5db199SXin Li        mylogger.start()
2537*9c5db199SXin Li        result = mylogger.result()
2538*9c5db199SXin Li
2539*9c5db199SXin Li    Public methods:
2540*9c5db199SXin Li        start: Launches the thread and starts measurements.
2541*9c5db199SXin Li        result: Stops the thread if it's still running and returns measurements.
2542*9c5db199SXin Li        get_error: Returns the exception in _error if it exists.
2543*9c5db199SXin Li
2544*9c5db199SXin Li    Private functions:
2545*9c5db199SXin Li        _get_disk_state: Returns the disk's current ATA power mode as a string.
2546*9c5db199SXin Li
2547*9c5db199SXin Li    Private attributes:
2548*9c5db199SXin Li        _seconds_period: Disk polling interval in seconds.
2549*9c5db199SXin Li        _stats: Dict that maps disk states to seconds spent in them.
2550*9c5db199SXin Li        _running: Flag that is True as long as the logger should keep running.
2551*9c5db199SXin Li        _time: Timestamp of last disk state reading.
2552*9c5db199SXin Li        _device_path: The file system path of the disk's device node.
2553*9c5db199SXin Li        _error: Contains a TestError exception if an unexpected error occured
2554*9c5db199SXin Li    """
2555*9c5db199SXin Li    def __init__(self, seconds_period = 5.0, device_path = None):
2556*9c5db199SXin Li        """Initializes a logger.
2557*9c5db199SXin Li
2558*9c5db199SXin Li        Args:
2559*9c5db199SXin Li            seconds_period: Disk polling interval in seconds. Default 5.0
2560*9c5db199SXin Li            device_path: The path of the disk's device node. Default '/dev/sda'
2561*9c5db199SXin Li        """
2562*9c5db199SXin Li        threading.Thread.__init__(self)
2563*9c5db199SXin Li        self._seconds_period = seconds_period
2564*9c5db199SXin Li        self._device_path = device_path
2565*9c5db199SXin Li        self._stats = {}
2566*9c5db199SXin Li        self._running = False
2567*9c5db199SXin Li        self._error = None
2568*9c5db199SXin Li
2569*9c5db199SXin Li        result = utils.system_output('rootdev -s')
2570*9c5db199SXin Li        # TODO(tbroch) Won't work for emmc storage and will throw this error in
2571*9c5db199SXin Li        # keyvals : 'ioctl(SG_IO) error: [Errno 22] Invalid argument'
2572*9c5db199SXin Li        # Lets implement something complimentary for emmc
2573*9c5db199SXin Li        if not device_path:
2574*9c5db199SXin Li            self._device_path = \
2575*9c5db199SXin Li                re.sub('(sd[a-z]|mmcblk[0-9]+)p?[0-9]+', '\\1', result)
2576*9c5db199SXin Li        logging.debug("device_path = %s", self._device_path)
2577*9c5db199SXin Li
2578*9c5db199SXin Li
2579*9c5db199SXin Li    def start(self):
2580*9c5db199SXin Li        logging.debug("inside DiskStateLogger.start")
2581*9c5db199SXin Li        if os.path.exists(self._device_path):
2582*9c5db199SXin Li            logging.debug("DiskStateLogger started")
2583*9c5db199SXin Li            super(DiskStateLogger, self).start()
2584*9c5db199SXin Li
2585*9c5db199SXin Li
2586*9c5db199SXin Li    def _get_disk_state(self):
2587*9c5db199SXin Li        """Checks the disk's power mode and returns it as a string.
2588*9c5db199SXin Li
2589*9c5db199SXin Li        This uses the SG_IO ioctl to issue a raw SCSI command data block with
2590*9c5db199SXin Li        the ATA-PASS-THROUGH command that allows SCSI-to-ATA translation (see
2591*9c5db199SXin Li        T10 document 04-262r8). The ATA command issued is CHECKPOWERMODE1,
2592*9c5db199SXin Li        which returns the device's current power mode.
2593*9c5db199SXin Li        """
2594*9c5db199SXin Li
2595*9c5db199SXin Li        def _addressof(obj):
2596*9c5db199SXin Li            """Shortcut to return the memory address of an object as integer."""
2597*9c5db199SXin Li            return ctypes.cast(obj, ctypes.c_void_p).value
2598*9c5db199SXin Li
2599*9c5db199SXin Li        scsi_cdb = struct.pack("12B", # SCSI command data block (uint8[12])
2600*9c5db199SXin Li                               0xa1, # SCSI opcode: ATA-PASS-THROUGH
2601*9c5db199SXin Li                               3 << 1, # protocol: Non-data
2602*9c5db199SXin Li                               1 << 5, # flags: CK_COND
2603*9c5db199SXin Li                               0, # features
2604*9c5db199SXin Li                               0, # sector count
2605*9c5db199SXin Li                               0, 0, 0, # LBA
2606*9c5db199SXin Li                               1 << 6, # flags: ATA-USING-LBA
2607*9c5db199SXin Li                               0xe5, # ATA opcode: CHECKPOWERMODE1
2608*9c5db199SXin Li                               0, # reserved
2609*9c5db199SXin Li                               0, # control (no idea what this is...)
2610*9c5db199SXin Li                              )
2611*9c5db199SXin Li        scsi_sense = (ctypes.c_ubyte * 32)() # SCSI sense buffer (uint8[32])
2612*9c5db199SXin Li        sgio_header = struct.pack("iiBBHIPPPIIiPBBBBHHiII", # see <scsi/sg.h>
2613*9c5db199SXin Li                                  83, # Interface ID magic number (int32)
2614*9c5db199SXin Li                                  -1, # data transfer direction: none (int32)
2615*9c5db199SXin Li                                  12, # SCSI command data block length (uint8)
2616*9c5db199SXin Li                                  32, # SCSI sense data block length (uint8)
2617*9c5db199SXin Li                                  0, # iovec_count (not applicable?) (uint16)
2618*9c5db199SXin Li                                  0, # data transfer length (uint32)
2619*9c5db199SXin Li                                  0, # data block pointer
2620*9c5db199SXin Li                                  _addressof(scsi_cdb), # SCSI CDB pointer
2621*9c5db199SXin Li                                  _addressof(scsi_sense), # sense buffer pointer
2622*9c5db199SXin Li                                  500, # timeout in milliseconds (uint32)
2623*9c5db199SXin Li                                  0, # flags (uint32)
2624*9c5db199SXin Li                                  0, # pack ID (unused) (int32)
2625*9c5db199SXin Li                                  0, # user data pointer (unused)
2626*9c5db199SXin Li                                  0, 0, 0, 0, 0, 0, 0, 0, 0, # output params
2627*9c5db199SXin Li                                 )
2628*9c5db199SXin Li        try:
2629*9c5db199SXin Li            with open(self._device_path, 'r') as dev:
2630*9c5db199SXin Li                result = fcntl.ioctl(dev, 0x2285, sgio_header)
2631*9c5db199SXin Li        except IOError as e:
2632*9c5db199SXin Li            raise error.TestError('ioctl(SG_IO) error: %s' % str(e))
2633*9c5db199SXin Li        _, _, _, _, status, host_status, driver_status = \
2634*9c5db199SXin Li            struct.unpack("4x4xxx2x4xPPP4x4x4xPBxxxHH4x4x4x", result)
2635*9c5db199SXin Li        if status != 0x2: # status: CHECK_CONDITION
2636*9c5db199SXin Li            raise error.TestError('SG_IO status: %d' % status)
2637*9c5db199SXin Li        if host_status != 0:
2638*9c5db199SXin Li            raise error.TestError('SG_IO host status: %d' % host_status)
2639*9c5db199SXin Li        if driver_status != 0x8: # driver status: SENSE
2640*9c5db199SXin Li            raise error.TestError('SG_IO driver status: %d' % driver_status)
2641*9c5db199SXin Li
2642*9c5db199SXin Li        if scsi_sense[0] != 0x72: # resp. code: current error, descriptor format
2643*9c5db199SXin Li            raise error.TestError('SENSE response code: %d' % scsi_sense[0])
2644*9c5db199SXin Li        if scsi_sense[1] != 0: # sense key: No Sense
2645*9c5db199SXin Li            raise error.TestError('SENSE key: %d' % scsi_sense[1])
2646*9c5db199SXin Li        if scsi_sense[7] < 14: # additional length (ATA status is 14 - 1 bytes)
2647*9c5db199SXin Li            raise error.TestError('ADD. SENSE too short: %d' % scsi_sense[7])
2648*9c5db199SXin Li        if scsi_sense[8] != 0x9: # additional descriptor type: ATA Return Status
2649*9c5db199SXin Li            raise error.TestError('SENSE descriptor type: %d' % scsi_sense[8])
2650*9c5db199SXin Li        if scsi_sense[11] != 0: # errors: none
2651*9c5db199SXin Li            raise error.TestError('ATA error code: %d' % scsi_sense[11])
2652*9c5db199SXin Li
2653*9c5db199SXin Li        if scsi_sense[13] == 0x00:
2654*9c5db199SXin Li            return 'standby'
2655*9c5db199SXin Li        if scsi_sense[13] == 0x80:
2656*9c5db199SXin Li            return 'idle'
2657*9c5db199SXin Li        if scsi_sense[13] == 0xff:
2658*9c5db199SXin Li            return 'active'
2659*9c5db199SXin Li        return 'unknown(%d)' % scsi_sense[13]
2660*9c5db199SXin Li
2661*9c5db199SXin Li
2662*9c5db199SXin Li    def run(self):
2663*9c5db199SXin Li        """The Thread's run method."""
2664*9c5db199SXin Li        try:
2665*9c5db199SXin Li            self._time = time.time()
2666*9c5db199SXin Li            self._running = True
2667*9c5db199SXin Li            while(self._running):
2668*9c5db199SXin Li                time.sleep(self._seconds_period)
2669*9c5db199SXin Li                state = self._get_disk_state()
2670*9c5db199SXin Li                new_time = time.time()
2671*9c5db199SXin Li                if state in self._stats:
2672*9c5db199SXin Li                    self._stats[state] += new_time - self._time
2673*9c5db199SXin Li                else:
2674*9c5db199SXin Li                    self._stats[state] = new_time - self._time
2675*9c5db199SXin Li                self._time = new_time
2676*9c5db199SXin Li        except error.TestError as e:
2677*9c5db199SXin Li            self._error = e
2678*9c5db199SXin Li            self._running = False
2679*9c5db199SXin Li
2680*9c5db199SXin Li
2681*9c5db199SXin Li    def result(self):
2682*9c5db199SXin Li        """Stop the logger and return dict with result percentages."""
2683*9c5db199SXin Li        if (self._running):
2684*9c5db199SXin Li            self._running = False
2685*9c5db199SXin Li            self.join(self._seconds_period * 2)
2686*9c5db199SXin Li        return AbstractStats.to_percent(self._stats)
2687*9c5db199SXin Li
2688*9c5db199SXin Li
2689*9c5db199SXin Li    def get_error(self):
2690*9c5db199SXin Li        """Returns the _error exception... please only call after result()."""
2691*9c5db199SXin Li        return self._error
2692*9c5db199SXin Li
2693*9c5db199SXin LiS0ixAmdStats = namedtuple('S0ixAmdStats',['entry','exit','residency'])
2694*9c5db199SXin Li
2695*9c5db199SXin Lidef parse_amd_pmc_s0ix_residency_info():
2696*9c5db199SXin Li    """
2697*9c5db199SXin Li    Parses S0ix residency for AMD systems
2698*9c5db199SXin Li
2699*9c5db199SXin Li    @returns S0ixAmdStats
2700*9c5db199SXin Li    @raises error.TestNAError if the debugfs file not found.
2701*9c5db199SXin Li    """
2702*9c5db199SXin Li    s = []
2703*9c5db199SXin Li    with open('/sys/kernel/debug/amd_pmc/s0ix_stats',"r") as f:
2704*9c5db199SXin Li        for line in f:
2705*9c5db199SXin Li            if ':' in line:
2706*9c5db199SXin Li                val = line.split(": ")
2707*9c5db199SXin Li                s.append(int(val[1]))
2708*9c5db199SXin Li        stat = S0ixAmdStats(entry=s[0], exit=s[1], residency=s[2])
2709*9c5db199SXin Li        return stat
2710*9c5db199SXin Li    raise error.TestNAError('AMD S0ix residency not supported')
2711*9c5db199SXin Li
2712*9c5db199SXin Lidef parse_pmc_s0ix_residency_info():
2713*9c5db199SXin Li    """
2714*9c5db199SXin Li    Parses S0ix residency for PMC based Intel systems
2715*9c5db199SXin Li    (skylake/kabylake/apollolake), the debugfs paths might be
2716*9c5db199SXin Li    different from platform to platform, yet the format is
2717*9c5db199SXin Li    unified in microseconds.
2718*9c5db199SXin Li
2719*9c5db199SXin Li    @returns residency in seconds.
2720*9c5db199SXin Li    @raises error.TestNAError if the debugfs file not found.
2721*9c5db199SXin Li    """
2722*9c5db199SXin Li    info_path = None
2723*9c5db199SXin Li    for node in ['/sys/kernel/debug/pmc_core/slp_s0_residency_usec',
2724*9c5db199SXin Li                 '/sys/kernel/debug/telemetry/s0ix_residency_usec']:
2725*9c5db199SXin Li        if os.path.exists(node):
2726*9c5db199SXin Li            info_path = node
2727*9c5db199SXin Li            break
2728*9c5db199SXin Li    if not info_path:
2729*9c5db199SXin Li        raise error.TestNAError('S0ix residency file not found')
2730*9c5db199SXin Li    return float(utils.read_one_line(info_path)) * 1e-6
2731*9c5db199SXin Li
2732*9c5db199SXin Li
2733*9c5db199SXin Liclass S0ixResidencyStats(object):
2734*9c5db199SXin Li    """
2735*9c5db199SXin Li    Measures the S0ix residency of a given board over time.
2736*9c5db199SXin Li    """
2737*9c5db199SXin Li    def __init__(self):
2738*9c5db199SXin Li        if "amd" in utils.get_cpu_soc_family():
2739*9c5db199SXin Li            self._initial_residency = parse_amd_pmc_s0ix_residency_info()
2740*9c5db199SXin Li        else:
2741*9c5db199SXin Li            self._initial_residency = parse_pmc_s0ix_residency_info()
2742*9c5db199SXin Li
2743*9c5db199SXin Li    def get_accumulated_residency_secs(self):
2744*9c5db199SXin Li        """
2745*9c5db199SXin Li        @returns S0ix Residency since the class has been initialized.
2746*9c5db199SXin Li        """
2747*9c5db199SXin Li        if "amd" in utils.get_cpu_soc_family():
2748*9c5db199SXin Li            s0ix = parse_amd_pmc_s0ix_residency_info()
2749*9c5db199SXin Li            if s0ix != self._initial_residency:
2750*9c5db199SXin Li                return s0ix.residency
2751*9c5db199SXin Li            else:
2752*9c5db199SXin Li                return 0
2753*9c5db199SXin Li        else:
2754*9c5db199SXin Li            return parse_pmc_s0ix_residency_info() - self._initial_residency
2755*9c5db199SXin Li
2756*9c5db199SXin Li
2757*9c5db199SXin Liclass S2IdleStateStats(object):
2758*9c5db199SXin Li    """
2759*9c5db199SXin Li    Usage stats of an s2idle state.
2760*9c5db199SXin Li    """
2761*9c5db199SXin Li
2762*9c5db199SXin Li    def __init__(self, usage, time):
2763*9c5db199SXin Li        self.usage = usage
2764*9c5db199SXin Li        self.time = time
2765*9c5db199SXin Li
2766*9c5db199SXin Li
2767*9c5db199SXin Lidef get_s2idle_path(cpu, state):
2768*9c5db199SXin Li    path = os.path.join(CPU_BASE_PATH,
2769*9c5db199SXin Li                        "cpu{}/cpuidle/{}/s2idle".format(cpu, state))
2770*9c5db199SXin Li    if not os.path.exists(path):
2771*9c5db199SXin Li        return None
2772*9c5db199SXin Li
2773*9c5db199SXin Li    return path
2774*9c5db199SXin Li
2775*9c5db199SXin Li
2776*9c5db199SXin Lidef get_s2idle_stats_for_state(cpu, state):
2777*9c5db199SXin Li    """
2778*9c5db199SXin Li    Returns the s2idle stats for a given idle state of a CPU.
2779*9c5db199SXin Li    """
2780*9c5db199SXin Li    s2idle_path = get_s2idle_path(cpu, state)
2781*9c5db199SXin Li
2782*9c5db199SXin Li    path = os.path.join(s2idle_path, 'usage')
2783*9c5db199SXin Li    if not os.path.exists(path):
2784*9c5db199SXin Li        raise error.TestFail("File not found: {}" % path)
2785*9c5db199SXin Li
2786*9c5db199SXin Li    usage = int(utils.read_one_line(path))
2787*9c5db199SXin Li
2788*9c5db199SXin Li    path = os.path.join(s2idle_path, 'time')
2789*9c5db199SXin Li    if not os.path.exists(path):
2790*9c5db199SXin Li        raise error.TestFail("File not found: {}" % path)
2791*9c5db199SXin Li
2792*9c5db199SXin Li    time = int(utils.read_one_line(path))
2793*9c5db199SXin Li
2794*9c5db199SXin Li    return S2IdleStateStats(usage, time)
2795*9c5db199SXin Li
2796*9c5db199SXin Li
2797*9c5db199SXin Lidef get_cpuidle_states(cpu):
2798*9c5db199SXin Li    """
2799*9c5db199SXin Li    Returns the cpuidle states of a CPU.
2800*9c5db199SXin Li    """
2801*9c5db199SXin Li    cpuidle_path = os.path.join(CPU_BASE_PATH, "cpu{}/cpuidle".format(cpu))
2802*9c5db199SXin Li
2803*9c5db199SXin Li    pattern = os.path.join(cpuidle_path, 'state*')
2804*9c5db199SXin Li    state_paths = glob.glob(pattern)
2805*9c5db199SXin Li
2806*9c5db199SXin Li    return [s.split('/')[-1] for s in state_paths]
2807*9c5db199SXin Li
2808*9c5db199SXin Li
2809*9c5db199SXin Lidef get_s2idle_stats_for_cpu(cpu):
2810*9c5db199SXin Li    """
2811*9c5db199SXin Li    Returns the s2idle stats for a CPU.
2812*9c5db199SXin Li    """
2813*9c5db199SXin Li    return {s: get_s2idle_stats_for_state(cpu, s)
2814*9c5db199SXin Li            for s in get_cpuidle_states(cpu)
2815*9c5db199SXin Li            if get_s2idle_path(cpu, s) is not None}
2816*9c5db199SXin Li
2817*9c5db199SXin Li
2818*9c5db199SXin Lidef get_s2idle_stats():
2819*9c5db199SXin Li    """
2820*9c5db199SXin Li    Returns the s2idle stats for all CPUs.
2821*9c5db199SXin Li    """
2822*9c5db199SXin Li    return {cpu: get_s2idle_stats_for_cpu(cpu) for cpu in get_online_cpus()}
2823*9c5db199SXin Li
2824*9c5db199SXin Li
2825*9c5db199SXin Lidef get_s2idle_residency_total_usecs():
2826*9c5db199SXin Li    """
2827*9c5db199SXin Li    Get total s2idle residency time for all CPUs and states.
2828*9c5db199SXin Li    """
2829*9c5db199SXin Li    total_usecs = 0
2830*9c5db199SXin Li
2831*9c5db199SXin Li    all_stats = get_s2idle_stats()
2832*9c5db199SXin Li    for stats in all_stats.values():
2833*9c5db199SXin Li        for st in stats.values():
2834*9c5db199SXin Li            total_usecs += st.time
2835*9c5db199SXin Li
2836*9c5db199SXin Li    return total_usecs
2837*9c5db199SXin Li
2838*9c5db199SXin Li
2839*9c5db199SXin Liclass S2IdleResidencyStats(object):
2840*9c5db199SXin Li    """
2841*9c5db199SXin Li    Measures the s2idle residency of a given board over time.
2842*9c5db199SXin Li    """
2843*9c5db199SXin Li
2844*9c5db199SXin Li    def __init__(self):
2845*9c5db199SXin Li        self._initial_residency = get_s2idle_residency_total_usecs()
2846*9c5db199SXin Li
2847*9c5db199SXin Li    def get_accumulated_residency_usecs(self):
2848*9c5db199SXin Li        """
2849*9c5db199SXin Li        @returns s2idle residency since the class has been initialized.
2850*9c5db199SXin Li        """
2851*9c5db199SXin Li        return get_s2idle_residency_total_usecs() - self._initial_residency
2852*9c5db199SXin Li
2853*9c5db199SXin Li
2854*9c5db199SXin Liclass DMCFirmwareStats(object):
2855*9c5db199SXin Li    """
2856*9c5db199SXin Li    Collect DMC firmware stats of Intel based system (SKL+), (APL+).
2857*9c5db199SXin Li    """
2858*9c5db199SXin Li    # Intel CPUs only transition to DC6 from DC5. https://git.io/vppcG
2859*9c5db199SXin Li    DC6_ENTRY_KEY = 'DC5 -> DC6 count'
2860*9c5db199SXin Li
2861*9c5db199SXin Li    def __init__(self):
2862*9c5db199SXin Li        self._initial_stat = DMCFirmwareStats._parse_dmc_info()
2863*9c5db199SXin Li
2864*9c5db199SXin Li    def check_fw_loaded(self):
2865*9c5db199SXin Li        """Check that DMC firmware is loaded
2866*9c5db199SXin Li
2867*9c5db199SXin Li        @returns boolean of DMC firmware loaded.
2868*9c5db199SXin Li        """
2869*9c5db199SXin Li        return self._initial_stat['fw loaded']
2870*9c5db199SXin Li
2871*9c5db199SXin Li    def is_dc6_supported(self):
2872*9c5db199SXin Li        """Check that DMC support DC6 state."""
2873*9c5db199SXin Li        return self.DC6_ENTRY_KEY in self._initial_stat
2874*9c5db199SXin Li
2875*9c5db199SXin Li    def get_accumulated_dc6_entry(self):
2876*9c5db199SXin Li        """Check number of DC6 state entry since the class has been initialized.
2877*9c5db199SXin Li
2878*9c5db199SXin Li        @returns number of DC6 state entry.
2879*9c5db199SXin Li        """
2880*9c5db199SXin Li        if not self.is_dc6_supported():
2881*9c5db199SXin Li            return 0
2882*9c5db199SXin Li
2883*9c5db199SXin Li        key = self.DC6_ENTRY_KEY
2884*9c5db199SXin Li        current_stat = DMCFirmwareStats._parse_dmc_info()
2885*9c5db199SXin Li        return current_stat[key] - self._initial_stat[key]
2886*9c5db199SXin Li
2887*9c5db199SXin Li    @staticmethod
2888*9c5db199SXin Li    def _parse_dmc_info():
2889*9c5db199SXin Li        """
2890*9c5db199SXin Li        Parses DMC firmware info for Intel based systems.
2891*9c5db199SXin Li
2892*9c5db199SXin Li        @returns dictionary of dmc_fw info
2893*9c5db199SXin Li        @raises error.TestFail if the debugfs file not found.
2894*9c5db199SXin Li        """
2895*9c5db199SXin Li        path = '/sys/kernel/debug/dri/0/i915_dmc_info'
2896*9c5db199SXin Li        if not os.path.exists(path):
2897*9c5db199SXin Li            raise error.TestFail('DMC info file not found.')
2898*9c5db199SXin Li
2899*9c5db199SXin Li        with open(path, 'r') as f:
2900*9c5db199SXin Li            lines = [line.strip() for line in f.readlines()]
2901*9c5db199SXin Li
2902*9c5db199SXin Li        # For pre 4.16 kernel. https://git.io/vhThb
2903*9c5db199SXin Li        if lines[0] == 'not supported':
2904*9c5db199SXin Li            raise error.TestFail('DMC not supported.')
2905*9c5db199SXin Li
2906*9c5db199SXin Li        ret = dict()
2907*9c5db199SXin Li        for line in lines:
2908*9c5db199SXin Li            key, val = line.rsplit(': ', 1)
2909*9c5db199SXin Li
2910*9c5db199SXin Li            if key == 'fw loaded':
2911*9c5db199SXin Li                val = val == 'yes'
2912*9c5db199SXin Li            elif re.match(r'DC\d -> DC\d count', key):
2913*9c5db199SXin Li                val = int(val)
2914*9c5db199SXin Li            ret[key] = val
2915*9c5db199SXin Li        return ret
2916*9c5db199SXin Li
2917*9c5db199SXin Li
2918*9c5db199SXin Liclass RC6ResidencyStats(object):
2919*9c5db199SXin Li    """
2920*9c5db199SXin Li    Collect RC6 residency stats of Intel based system.
2921*9c5db199SXin Li    """
2922*9c5db199SXin Li    def __init__(self):
2923*9c5db199SXin Li        self._rc6_enable_checked = False
2924*9c5db199SXin Li        self._previous_stat = self._parse_rc6_residency_info()
2925*9c5db199SXin Li        self._accumulated_stat = 0
2926*9c5db199SXin Li
2927*9c5db199SXin Li        # Setup max RC6 residency count for modern chips. The table order
2928*9c5db199SXin Li        # is in small/big-core first, follows by the uarch name. We don't
2929*9c5db199SXin Li        # need to deal with the wraparound for devices with v4.17+ kernel
2930*9c5db199SXin Li        # which has the commit 817cc0791823 ("drm/i915: Handle RC6 counter wrap").
2931*9c5db199SXin Li        cpu_uarch = utils.get_intel_cpu_uarch()
2932*9c5db199SXin Li        self._max_counter = {
2933*9c5db199SXin Li          # Small-core w/ GEN9 LP graphics
2934*9c5db199SXin Li          'Airmont':      3579125,
2935*9c5db199SXin Li          'Goldmont':     3579125,
2936*9c5db199SXin Li          # Big-core
2937*9c5db199SXin Li          'Broadwell':    5497558,
2938*9c5db199SXin Li          'Haswell':      5497558,
2939*9c5db199SXin Li          'Kaby Lake':    5497558,
2940*9c5db199SXin Li          'Skylake':      5497558,
2941*9c5db199SXin Li        }.get(cpu_uarch, None)
2942*9c5db199SXin Li
2943*9c5db199SXin Li    def get_accumulated_residency_msecs(self):
2944*9c5db199SXin Li        """Check number of RC6 state entry since the class has been initialized.
2945*9c5db199SXin Li
2946*9c5db199SXin Li        @returns int of RC6 residency in milliseconds since instantiation.
2947*9c5db199SXin Li        """
2948*9c5db199SXin Li        current_stat = self._parse_rc6_residency_info()
2949*9c5db199SXin Li
2950*9c5db199SXin Li        # The problem here is that we cannot assume the rc6_residency_ms is
2951*9c5db199SXin Li        # monotonically increasing by current kernel i915 implementation.
2952*9c5db199SXin Li        #
2953*9c5db199SXin Li        # Considering different hardware has different wraparound period,
2954*9c5db199SXin Li        # this is a mitigation plan to deal with different wraparound period
2955*9c5db199SXin Li        # on various platforms, in order to make the test platform agnostic.
2956*9c5db199SXin Li        #
2957*9c5db199SXin Li        # This scarifes the accuracy of RC6 residency a bit, up on the calling
2958*9c5db199SXin Li        # period.
2959*9c5db199SXin Li        #
2960*9c5db199SXin Li        # Reference: Bug 94852 - [SKL] rc6_residency_ms unreliable
2961*9c5db199SXin Li        # (https://bugs.freedesktop.org/show_bug.cgi?id=94852)
2962*9c5db199SXin Li        #
2963*9c5db199SXin Li        # However for modern processors with a known overflow count, apply
2964*9c5db199SXin Li        # constant of RC6 max counter to improve accuracy.
2965*9c5db199SXin Li        #
2966*9c5db199SXin Li        # Note that the max counter is bound for sysfs overflow, while the
2967*9c5db199SXin Li        # accumulated residency here is the diff against the first reading.
2968*9c5db199SXin Li        if current_stat < self._previous_stat:
2969*9c5db199SXin Li            if self._max_counter is None:
2970*9c5db199SXin Li                logging.warning('GPU: Detect rc6_residency_ms wraparound')
2971*9c5db199SXin Li                self._accumulated_stat += current_stat
2972*9c5db199SXin Li            else:
2973*9c5db199SXin Li                self._accumulated_stat += current_stat + (self._max_counter -
2974*9c5db199SXin Li                                                          self._previous_stat)
2975*9c5db199SXin Li        else:
2976*9c5db199SXin Li            self._accumulated_stat += current_stat - self._previous_stat
2977*9c5db199SXin Li
2978*9c5db199SXin Li        self._previous_stat = current_stat
2979*9c5db199SXin Li        return self._accumulated_stat
2980*9c5db199SXin Li
2981*9c5db199SXin Li    def _is_rc6_enable(self):
2982*9c5db199SXin Li        """
2983*9c5db199SXin Li        Verified that RC6 is enable.
2984*9c5db199SXin Li
2985*9c5db199SXin Li        @returns Boolean of RC6 enable status.
2986*9c5db199SXin Li        @raises error.TestFail if the sysfs file not found.
2987*9c5db199SXin Li        """
2988*9c5db199SXin Li        path = '/sys/class/drm/card0/power/rc6_enable'
2989*9c5db199SXin Li        if not os.path.exists(path):
2990*9c5db199SXin Li            raise error.TestFail('RC6 enable file not found.')
2991*9c5db199SXin Li
2992*9c5db199SXin Li        return (int(utils.read_one_line(path)) & 0x1) == 0x1
2993*9c5db199SXin Li
2994*9c5db199SXin Li    def _parse_rc6_residency_info(self):
2995*9c5db199SXin Li        """
2996*9c5db199SXin Li        Parses RC6 residency info for Intel based systems.
2997*9c5db199SXin Li
2998*9c5db199SXin Li        @returns int of RC6 residency in millisec since boot.
2999*9c5db199SXin Li        @raises error.TestFail if the sysfs file not found or RC6 not enabled.
3000*9c5db199SXin Li        """
3001*9c5db199SXin Li        if not self._rc6_enable_checked:
3002*9c5db199SXin Li            if not self._is_rc6_enable():
3003*9c5db199SXin Li                raise error.TestFail('RC6 is not enabled.')
3004*9c5db199SXin Li            self._rc6_enable_checked = True
3005*9c5db199SXin Li
3006*9c5db199SXin Li        path = '/sys/class/drm/card0/power/rc6_residency_ms'
3007*9c5db199SXin Li        if not os.path.exists(path):
3008*9c5db199SXin Li            raise error.TestFail('RC6 residency file not found.')
3009*9c5db199SXin Li
3010*9c5db199SXin Li        return int(utils.read_one_line(path))
3011*9c5db199SXin Li
3012*9c5db199SXin Li
3013*9c5db199SXin Liclass PCHPowergatingStats(object):
3014*9c5db199SXin Li    """
3015*9c5db199SXin Li    Collect PCH powergating status of intel based system.
3016*9c5db199SXin Li    """
3017*9c5db199SXin Li    PMC_CORE_PATH = '/sys/kernel/debug/pmc_core/pch_ip_power_gating_status'
3018*9c5db199SXin Li    TELEMETRY_PATH = '/sys/kernel/debug/telemetry/soc_states'
3019*9c5db199SXin Li
3020*9c5db199SXin Li    def __init__(self):
3021*9c5db199SXin Li        self._stat = {}
3022*9c5db199SXin Li
3023*9c5db199SXin Li    def check_s0ix_requirement(self):
3024*9c5db199SXin Li        """
3025*9c5db199SXin Li        Check PCH powergating status with S0ix requirement.
3026*9c5db199SXin Li
3027*9c5db199SXin Li        @returns list of PCH IP block name that need to be powergated for low
3028*9c5db199SXin Li                 power consumption S0ix, empty list if none.
3029*9c5db199SXin Li        """
3030*9c5db199SXin Li        # PCH IP block that is on for S0ix. Ignore these IP block.
3031*9c5db199SXin Li        S0IX_ALLOWLIST = set([
3032*9c5db199SXin Li                'PMC', 'OPI-DMI', 'SPI / eSPI', 'XHCI', 'xHCI', 'FUSE', 'Fuse',
3033*9c5db199SXin Li                'PCIE0', 'NPKVRC', 'NPKVNN', 'NPK_VNN', 'PSF1', 'PSF2', 'PSF3',
3034*9c5db199SXin Li                'PSF4', 'SBR0', 'SBR1', 'SBR2', 'SBR4', 'SBR5', 'SBR6', 'SBR7'])
3035*9c5db199SXin Li
3036*9c5db199SXin Li        # PCH IP block that is on/off for S0ix depend on features enabled.
3037*9c5db199SXin Li        # Add log when these IPs state are on.
3038*9c5db199SXin Li        S0IX_WARNLIST = set([
3039*9c5db199SXin Li                'HDA-PGD0', 'HDA-PGD1', 'HDA-PGD2', 'HDA-PGD3', 'LPSS',
3040*9c5db199SXin Li                'AVSPGD1', 'AVSPGD4'])
3041*9c5db199SXin Li
3042*9c5db199SXin Li        # CNV device has 0x31dc as devid .
3043*9c5db199SXin Li        if len(utils.system_output('lspci -d :31dc')) > 0:
3044*9c5db199SXin Li            S0IX_ALLOWLIST.add('CNV')
3045*9c5db199SXin Li
3046*9c5db199SXin Li        # HrP2 device has 0x02f0(CML) or 0x4df0(JSL) as devid.
3047*9c5db199SXin Li        if (len(utils.system_output('lspci -d :02f0')) > 0 or
3048*9c5db199SXin Li            len(utils.system_output('lspci -d :4df0')) > 0):
3049*9c5db199SXin Li            S0IX_ALLOWLIST.update(['CNVI', 'NPK_AON'])
3050*9c5db199SXin Li
3051*9c5db199SXin Li        on_ip = set(ip['name'] for ip in self._stat if ip['state'])
3052*9c5db199SXin Li        on_ip -= S0IX_ALLOWLIST
3053*9c5db199SXin Li
3054*9c5db199SXin Li        if on_ip:
3055*9c5db199SXin Li            on_ip_in_warn_list = on_ip & S0IX_WARNLIST
3056*9c5db199SXin Li            if on_ip_in_warn_list:
3057*9c5db199SXin Li                logging.warning('Found PCH IP that may be able to powergate: %s',
3058*9c5db199SXin Li                             ', '.join(on_ip_in_warn_list))
3059*9c5db199SXin Li            on_ip -= S0IX_WARNLIST
3060*9c5db199SXin Li
3061*9c5db199SXin Li        if on_ip:
3062*9c5db199SXin Li            logging.error('Found PCH IP that need to powergate: %s',
3063*9c5db199SXin Li                          ', '.join(on_ip))
3064*9c5db199SXin Li            return on_ip
3065*9c5db199SXin Li        return []
3066*9c5db199SXin Li
3067*9c5db199SXin Li    def read_pch_powergating_info(self, sleep_seconds=1):
3068*9c5db199SXin Li        """
3069*9c5db199SXin Li        Read PCH powergating status info for Intel based systems.
3070*9c5db199SXin Li
3071*9c5db199SXin Li        Intel currently shows powergating status in 2 different place in debugfs
3072*9c5db199SXin Li        depend on which CPU platform.
3073*9c5db199SXin Li
3074*9c5db199SXin Li        @param sleep_seconds: sleep time to make DUT idle before read the data.
3075*9c5db199SXin Li
3076*9c5db199SXin Li        @raises error.TestFail if the debugfs file not found or parsing error.
3077*9c5db199SXin Li        """
3078*9c5db199SXin Li        if os.path.exists(self.PMC_CORE_PATH):
3079*9c5db199SXin Li            logging.info('Use PCH powergating info at %s', self.PMC_CORE_PATH)
3080*9c5db199SXin Li            time.sleep(sleep_seconds)
3081*9c5db199SXin Li            self._read_pcm_core_powergating_info()
3082*9c5db199SXin Li            return
3083*9c5db199SXin Li
3084*9c5db199SXin Li        if os.path.exists(self.TELEMETRY_PATH):
3085*9c5db199SXin Li            logging.info('Use PCH powergating info at %s', self.TELEMETRY_PATH)
3086*9c5db199SXin Li            time.sleep(sleep_seconds)
3087*9c5db199SXin Li            self._read_telemetry_powergating_info()
3088*9c5db199SXin Li            return
3089*9c5db199SXin Li
3090*9c5db199SXin Li        raise error.TestFail('PCH powergating info file not found.')
3091*9c5db199SXin Li
3092*9c5db199SXin Li    def _read_pcm_core_powergating_info(self):
3093*9c5db199SXin Li        """
3094*9c5db199SXin Li        read_pch_powergating_info() for Intel Core KBL+
3095*9c5db199SXin Li
3096*9c5db199SXin Li        @raises error.TestFail if parsing error.
3097*9c5db199SXin Li        """
3098*9c5db199SXin Li        with open(self.PMC_CORE_PATH, 'r') as f:
3099*9c5db199SXin Li            lines = [line.strip() for line in f.readlines()]
3100*9c5db199SXin Li
3101*9c5db199SXin Li        # Example pattern to match:
3102*9c5db199SXin Li        # PCH IP: 0  - PMC                                State: On
3103*9c5db199SXin Li        # PCH IP: 1  - SATA                               State: Off
3104*9c5db199SXin Li        pattern = r'PCH IP:\s+(?P<id>\d+)\s+' \
3105*9c5db199SXin Li                  r'- (?P<name>.*\w)\s+'      \
3106*9c5db199SXin Li                  r'State: (?P<state>Off|On)'
3107*9c5db199SXin Li        matcher = re.compile(pattern)
3108*9c5db199SXin Li        ret = []
3109*9c5db199SXin Li        for i, line in enumerate(lines):
3110*9c5db199SXin Li            match = matcher.match(line)
3111*9c5db199SXin Li            if not match:
3112*9c5db199SXin Li                raise error.TestFail('Can not parse PCH powergating info: ',
3113*9c5db199SXin Li                                     line)
3114*9c5db199SXin Li
3115*9c5db199SXin Li            index = int(match.group('id'))
3116*9c5db199SXin Li            if i != index:
3117*9c5db199SXin Li                raise error.TestFail('Wrong index for PCH powergating info: ',
3118*9c5db199SXin Li                                     line)
3119*9c5db199SXin Li
3120*9c5db199SXin Li            name = match.group('name')
3121*9c5db199SXin Li            state = match.group('state') == 'On'
3122*9c5db199SXin Li
3123*9c5db199SXin Li            ret.append({'name': name, 'state': state})
3124*9c5db199SXin Li        self._stat = ret
3125*9c5db199SXin Li
3126*9c5db199SXin Li    def _read_telemetry_powergating_info(self):
3127*9c5db199SXin Li        """
3128*9c5db199SXin Li        read_pch_powergating_info() for Intel Atom APL+
3129*9c5db199SXin Li
3130*9c5db199SXin Li        @raises error.TestFail if parsing error.
3131*9c5db199SXin Li        """
3132*9c5db199SXin Li        with open(self.TELEMETRY_PATH, 'r') as f:
3133*9c5db199SXin Li            raw_str = f.read()
3134*9c5db199SXin Li
3135*9c5db199SXin Li        # Example pattern to match:
3136*9c5db199SXin Li        # --------------------------------------
3137*9c5db199SXin Li        # South Complex PowerGate Status
3138*9c5db199SXin Li        # --------------------------------------
3139*9c5db199SXin Li        # Device           PG
3140*9c5db199SXin Li        # LPSS             1
3141*9c5db199SXin Li        # SPI              1
3142*9c5db199SXin Li        # FUSE             0
3143*9c5db199SXin Li        #
3144*9c5db199SXin Li        # ---------------------------------------
3145*9c5db199SXin Li        trimed_pattern = r'.*South Complex PowerGate Status\n'    \
3146*9c5db199SXin Li                         r'-+\n'                                  \
3147*9c5db199SXin Li                         r'Device\s+PG\n'                         \
3148*9c5db199SXin Li                         r'(?P<trimmed_section>(\w+\s+[0|1]\n)+)' \
3149*9c5db199SXin Li                         r'\n-+\n.*'
3150*9c5db199SXin Li        trimed_match = re.match(trimed_pattern, raw_str, re.DOTALL)
3151*9c5db199SXin Li        if not trimed_match:
3152*9c5db199SXin Li            raise error.TestFail('Can not parse PCH powergating info: ',
3153*9c5db199SXin Li                                 raw_str)
3154*9c5db199SXin Li        trimmed_str = trimed_match.group('trimmed_section').strip()
3155*9c5db199SXin Li        lines = [line.strip() for line in trimmed_str.split('\n')]
3156*9c5db199SXin Li
3157*9c5db199SXin Li        matcher = re.compile(r'(?P<name>\w+)\s+(?P<state>[0|1])')
3158*9c5db199SXin Li        ret = []
3159*9c5db199SXin Li        for line in lines:
3160*9c5db199SXin Li            match = matcher.match(line)
3161*9c5db199SXin Li            if not match:
3162*9c5db199SXin Li                raise error.TestFail('Can not parse PCH powergating info: %s',
3163*9c5db199SXin Li                                     line)
3164*9c5db199SXin Li
3165*9c5db199SXin Li            name = match.group('name')
3166*9c5db199SXin Li            state = match.group('state') == '0' # 0 means on and 1 means off
3167*9c5db199SXin Li
3168*9c5db199SXin Li            ret.append({'name': name, 'state': state})
3169*9c5db199SXin Li        self._stat = ret
3170*9c5db199SXin Li
3171*9c5db199SXin Lidef has_rc6_support():
3172*9c5db199SXin Li    """
3173*9c5db199SXin Li    Helper to examine that RC6 is enabled with residency counter.
3174*9c5db199SXin Li
3175*9c5db199SXin Li    @returns Boolean of RC6 support status.
3176*9c5db199SXin Li    """
3177*9c5db199SXin Li    enable_path = '/sys/class/drm/card0/power/rc6_enable'
3178*9c5db199SXin Li    residency_path = '/sys/class/drm/card0/power/rc6_residency_ms'
3179*9c5db199SXin Li
3180*9c5db199SXin Li    has_rc6_enabled = os.path.exists(enable_path)
3181*9c5db199SXin Li    has_rc6_residency = False
3182*9c5db199SXin Li    rc6_enable_mask = 0
3183*9c5db199SXin Li
3184*9c5db199SXin Li    if has_rc6_enabled:
3185*9c5db199SXin Li        # TODO (harry.pan): Some old chip has RC6P and RC6PP
3186*9c5db199SXin Li        # in the bits[1:2]; in case of that, ideally these time
3187*9c5db199SXin Li        # slice will fall into RC0, fix it up if required.
3188*9c5db199SXin Li        rc6_enable_mask = int(utils.read_one_line(enable_path))
3189*9c5db199SXin Li        has_rc6_enabled &= (rc6_enable_mask) & 0x1 == 0x1
3190*9c5db199SXin Li        has_rc6_residency = os.path.exists(residency_path)
3191*9c5db199SXin Li
3192*9c5db199SXin Li    logging.debug("GPU: RC6 residency support: %s, mask: 0x%x",
3193*9c5db199SXin Li                  {True: "yes", False: "no"} [has_rc6_enabled and has_rc6_residency],
3194*9c5db199SXin Li                  rc6_enable_mask)
3195*9c5db199SXin Li
3196*9c5db199SXin Li    return (has_rc6_enabled and has_rc6_residency)
3197*9c5db199SXin Li
3198*9c5db199SXin Liclass GPURC6Stats(AbstractStats):
3199*9c5db199SXin Li    """
3200*9c5db199SXin Li    GPU RC6 statistics to give ratio of RC6 and RC0 residency
3201*9c5db199SXin Li
3202*9c5db199SXin Li    Protected Attributes:
3203*9c5db199SXin Li      _rc6: object of RC6ResidencyStats
3204*9c5db199SXin Li    """
3205*9c5db199SXin Li    def __init__(self):
3206*9c5db199SXin Li        self._rc6 = RC6ResidencyStats()
3207*9c5db199SXin Li        super(GPURC6Stats, self).__init__(name='gpuidle')
3208*9c5db199SXin Li
3209*9c5db199SXin Li    def _read_stats(self):
3210*9c5db199SXin Li        total = int(time.time() * 1000)
3211*9c5db199SXin Li        msecs = self._rc6.get_accumulated_residency_msecs()
3212*9c5db199SXin Li        stats = collections.defaultdict(int)
3213*9c5db199SXin Li        stats['RC6'] += msecs
3214*9c5db199SXin Li        stats['RC0'] += total - msecs
3215*9c5db199SXin Li        logging.debug("GPU: RC6 residency: %d ms", msecs)
3216*9c5db199SXin Li        return stats
3217