sweetberry: fix stats_manager and refactor

This CL updates stats_manager to match the new functionalities
in powerlog.py and refactors powerlog.py to more easily find
config files and print timestamps in seconds since epoch.

The unit test for stats_manager is also updated accordingly.

BUG=b:72973433
BRANCH=None
TEST=powerlog -b nami_rev0_loc.board -c nami_rev0_loc.scenario \
--print_stats --save_stats /tmp --save_stats_json /tmp \
--save_raw_data /tmp --mW
and looking at the printed data
python -m unittest stats_manager_unittest
CQ-DEPEND=CL:1003522

Change-Id: Ic6e4aadfcd3ad245572788094ee3d3a30106044c
Signed-off-by: Mengqi Guo <mqg@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/1002546
Reviewed-by: Todd Broch <tbroch@chromium.org>
This commit is contained in:
Mengqi Guo
2018-04-05 11:37:22 -07:00
committed by chrome-bot
parent d786f8285b
commit a820df3fd6
4 changed files with 149 additions and 41 deletions

View File

@@ -1,7 +1,7 @@
Sweetberry USB power monitoring
This tool allows high speed monitoring of power rails via a special USB
endpoint. Currently this is implemented for the sweetberry baord.
endpoint. Currently this is implemented for the sweetberry board.
To use on a board, you'll need two config files, one describing the board,
a ".board" file, and one describing the particular rails you want to
@@ -10,6 +10,8 @@ monitor in this session, a ".scenario" file.
Converting from servo_ina configs:
Method 1 -
Many configs can be found for the servo_ina_board in hdctools/servo/data/.
Sweetberry is plug compatible with servo_ina headers, and config files
can be converted with the following tool:
@@ -19,6 +21,20 @@ can be converted with the following tool:
This will produce kevin_r0_loc.board and kevin_r0_loc.scenario which
can be used with powerlog.py.
Method 2 (preferred) -
If you are using powerlog.py within the chroot, copy kevin_r0_loc.py to
src/third_party/hdctools/servo/data, then add line to file:
config_type = 'sweetberry'
and run command in terminal:
sudo emerge hdctools
The command will install the corresponding .board and .scenario file in the
chroot. To use powerlog.py use the command:
./powerlog.py -b kevin_r0_loc.board -c kevin_r0_loc.scenario
There is no need to specify the absolute path to the .board and .scenario file,
once they are installed into the chroot. If there is any changes to
kevin_r0_loc.py, you need to emerge hdctools again.
Board files:
@@ -137,3 +153,15 @@ If --save_stats flag is not set, stats will not be saved.
--save_stats_json is designed for power_telemetry_logger for easy reading and
writing.
Making developer changes to powerlog.py:
powerlog.py is installed in chroot, and the developer can import powerlog or use
powerlog directly anywhere within chroot. Anytime the developer makes a change
to powerlog.py, the developer needs to re-install powerlog.py so that anything
that imports powerlog does not break. The following is how the developer
installs powerlog.py during development.
Run command in the terminal:
cros_workon --host start ec-devutils # just the first time
sudo emerge ec-devutils # everytime powerlog gets changed

View File

@@ -10,7 +10,7 @@
from __future__ import print_function
import argparse
import array
import datetime
from distutils import sysconfig
import json
import os
import struct
@@ -23,6 +23,9 @@ import usb
from stats_manager import StatsManager
LIB_DIR = os.path.join(sysconfig.get_python_lib(standard_lib=False), 'servo',
'data')
# This can be overridden by -v.
debug = False
def debuglog(msg):
@@ -33,6 +36,40 @@ def logoutput(msg):
print(msg)
sys.stdout.flush()
def process_filename(filename):
"""Find the file path from the filename.
If filename is already the complete path, return that directly. If filename is
just the short name, look for the file in the current working directory, in
the directory of the current .py file, and then in the directory installed by
hdctools. If the file is found, return the complete path of the file.
Args:
filename: complete file path or short file name.
Returns:
a complete file path.
Raises:
IOError if filename does not exist.
"""
# Check if filename is absolute path.
if os.path.isabs(filename) and os.path.isfile(filename):
return filename
# Check if filename is relative to current working directory.
cwd = os.path.join(os.getcwd(), filename)
if os.path.isfile(cwd):
return cwd
# Check if filename is relative to same directory as current .py file.
sd = os.path.join(os.path.dirname(os.path.realpath(__file__)), filename)
if os.path.isfile(sd):
return sd
# Check if file is installed by hdctools.
hdc = os.path.join(LIB_DIR, filename)
if os.path.isfile(hdc):
return hdc
raise IOError('No such file or directory: \'%s\'' % filename)
class Spower(object):
"""Power class to access devices on the bus.
@@ -51,6 +88,10 @@ class Spower(object):
INA_BUSV = 2
INA_CURRENT = 3
INA_SHUNTV = 4
# INA_SUFFIX is used to differentiate multiple ina types for the same power
# rail. No suffix for when ina type is 0 (non-existent) and when ina type is 1
# (power, no suffix for backward compatibility).
INA_SUFFIX = ['', '', '_busv', '_cur', '_shuntv']
# usb power commands
CMD_RESET = 0x0000
@@ -503,7 +544,7 @@ class Spower(object):
Args:
brdfile: Filename of a json file decribing the INA wiring of this board.
"""
with open(brdfile) as data_file:
with open(process_filename(brdfile)) as data_file:
data = json.load(data_file)
#TODO: validate this.
@@ -519,7 +560,8 @@ class powerlog(object):
obj = powerlog()
Instance Variables:
_data: records sweetberries readings and calculates statistics.
_data: a StatsManager object that records sweetberry readings and calculates
statistics.
_pwr[]: Spower objects for individual sweetberries.
"""
@@ -564,7 +606,7 @@ class powerlog(object):
if serial_b:
self._pwr['B'] = Spower('B', serialname=serial_b)
with open(cfgfile) as data_file:
with open(process_filename(cfgfile)) as data_file:
names = json.load(data_file)
self._names = self.process_scenario(names)
@@ -648,22 +690,24 @@ class powerlog(object):
integration_us = integration_us_new
# CSV header
title = "ts:%dus" % integration_us
for name_tuple in self._names:
name, ina_type = name_tuple
if ina_type == Spower.INA_POWER:
unit = "mW" if self._use_mW else "uW"
elif ina_type == Spower.INA_BUSV:
unit = "mV"
elif ina_type == Spower.INA_CURRENT:
unit = "uA"
elif ina_type == Spower.INA_SHUNTV:
unit = "uV"
title += ", %s %s" % (name, unit)
name_type = name + Spower.INA_SUFFIX[ina_type]
self._data.SetUnit(name_type, unit)
title += ", status"
if self._print_raw_data:
title = "ts:%dus" % integration_us
for name_tuple in self._names:
name, ina_type = name_tuple
if ina_type == Spower.INA_POWER:
unit = "mW" if self._use_mW else "uW"
elif ina_type == Spower.INA_BUSV:
unit = "mV"
elif ina_type == Spower.INA_CURRENT:
unit = "uA"
elif ina_type == Spower.INA_SHUNTV:
unit = "uV"
title += ", %s %s" % (name, unit)
title += ", status"
logoutput(title)
forever = False
@@ -704,7 +748,8 @@ class powerlog(object):
name[1]==Spower.INA_POWER) else 1
value = aggregate_record[name] * multiplier
csv += ", %.2f" % value
self._data.AddValue(name, value)
name_type = name[0] + Spower.INA_SUFFIX[name[1]]
self._data.AddValue(name_type, value)
else:
csv += ", "
csv += ", %d" % aggregate_record["status"]
@@ -724,7 +769,7 @@ class powerlog(object):
self._data.CalculateStats()
if self._print_stats:
self._data.PrintSummary()
save_dir = datetime.datetime.now().strftime('sweetberry%Y%m%d%H%M%S.%f')
save_dir = 'sweetberry%s' % time.time()
if self._stats_dir:
stats_dir = os.path.join(self._stats_dir, save_dir)
self._data.SaveSummary(stats_dir)

View File

@@ -16,12 +16,22 @@ KEY_PREFIX = '__'
# as timeline keys.
NOSHOW_PREFIX = '!!'
LONG_UNIT = {
'mW': 'milliwatt',
'uW': 'microwatt',
'mV': 'millivolt',
'uA': 'microamp',
'uV': 'microvolt'
}
class StatsManager(object):
"""Calculates statistics for several lists of data(float)."""
def __init__(self):
"""Initialize infrastructure for data and their statistics."""
self._data = collections.defaultdict(list)
self._unit = {}
self._summary = {}
def AddValue(self, domain, value):
@@ -39,6 +49,21 @@ class StatsManager(object):
print('Warning: value %s for domain %s is not a number, thus ignored.' %
(value, domain))
def SetUnit(self, domain, unit):
"""Set the unit for a domain.
There can be only one unit for each domain. Setting unit twice will
overwrite the original unit.
Args:
domain: the domain name.
unit: unit of the domain.
"""
if domain in self._unit:
print('Warning: overwriting the unit of %s, old unit is %s, new unit is '
'%s.' % (domain, self._unit[domain], unit))
self._unit[domain] = unit
def CalculateStats(self):
"""Calculate stats for all domain-data pairs.
@@ -48,11 +73,11 @@ class StatsManager(object):
for domain, data in self._data.iteritems():
data_np = numpy.array(data)
self._summary[domain] = {
'mean' : data_np.mean(),
'min' : data_np.min(),
'max' : data_np.max(),
'stddev' : data_np.std(),
'count' : data_np.size,
'mean': data_np.mean(),
'min': data_np.min(),
'max': data_np.max(),
'stddev': data_np.std(),
'count': data_np.size,
}
def _SummaryToString(self, prefix=STATS_PREFIX):
@@ -67,7 +92,9 @@ class StatsManager(object):
if domain.startswith(NOSHOW_PREFIX):
continue
stats = self._summary[domain]
row = [domain.lstrip(KEY_PREFIX)]
unit = self._unit[domain]
domain_unit = domain.lstrip(KEY_PREFIX) + '_' + unit
row = [domain_unit]
row.append(str(stats['count']))
for entry in headers[2:]:
row.append('%.2f' % stats[entry.lower()])
@@ -122,11 +149,13 @@ class StatsManager(object):
directory: directory to save the JSON summary in.
fname: filename to save summary under.
"""
data = {
domain: self._summary[domain]['mean']
for domain in sorted(self._summary.keys())
if not domain.startswith(NOSHOW_PREFIX)
}
data = {}
for domain in self._summary:
if domain.startswith(NOSHOW_PREFIX):
continue
unit = LONG_UNIT.get(self._unit[domain], self._unit[domain])
data_entry = {'mean': self._summary[domain]['mean'], 'unit': unit}
data[domain] = data_entry
if not os.path.exists(directory):
os.makedirs(directory)
fname = os.path.join(directory, fname)
@@ -150,7 +179,7 @@ class StatsManager(object):
if not os.path.exists(dirname):
os.makedirs(dirname)
for domain, data in self._data.iteritems():
fname = domain + '.txt'
fname = domain + '_' + self._unit[domain] + '.txt'
fname = os.path.join(dirname, fname)
with open(fname, 'w') as f:
f.write('\n'.join('%.2f' % value for value in data) + '\n')

View File

@@ -13,6 +13,7 @@ import unittest
from stats_manager import StatsManager
class TestStatsManager(unittest.TestCase):
"""Test to verify StatsManager methods work as expected.
@@ -27,9 +28,12 @@ class TestStatsManager(unittest.TestCase):
self.data.AddValue('A', 99999.5)
self.data.AddValue('A', 100000.5)
self.data.AddValue('A', 'ERROR')
self.data.SetUnit('A', 'uW')
self.data.SetUnit('A', 'mW')
self.data.AddValue('B', 1.5)
self.data.AddValue('B', 2.5)
self.data.AddValue('B', 3.5)
self.data.SetUnit('B', 'mV')
self.data.CalculateStats()
def tearDown(self):
@@ -58,8 +62,8 @@ class TestStatsManager(unittest.TestCase):
dirname = 'unittest_raw_data'
self.data.SaveRawData(self.tempdir, dirname)
dirname = os.path.join(self.tempdir, dirname)
fileA = os.path.join(dirname, 'A.txt')
fileB = os.path.join(dirname, 'B.txt')
fileA = os.path.join(dirname, 'A_mW.txt')
fileB = os.path.join(dirname, 'B_mV.txt')
with open(fileA, 'r') as fA:
self.assertEqual('99999.50', fA.readline().strip())
self.assertEqual('100000.50', fA.readline().strip())
@@ -77,10 +81,10 @@ class TestStatsManager(unittest.TestCase):
'@@ NAME COUNT MEAN STDDEV MAX MIN\n',
f.readline())
self.assertEqual(
'@@ A 2 100000.00 0.50 100000.50 99999.50\n',
'@@ A_mW 2 100000.00 0.50 100000.50 99999.50\n',
f.readline())
self.assertEqual(
'@@ B 3 2.50 0.82 3.50 1.50\n',
'@@ B_mV 3 2.50 0.82 3.50 1.50\n',
f.readline())
def test_SaveSummaryJSON(self):
@@ -88,9 +92,11 @@ class TestStatsManager(unittest.TestCase):
self.data.SaveSummaryJSON(self.tempdir, fname)
fname = os.path.join(self.tempdir, fname)
with open(fname, 'r') as f:
mean_json = json.load(f)
self.assertAlmostEqual(100000.0, mean_json['A'])
self.assertAlmostEqual(2.5, mean_json['B'])
summary = json.load(f)
self.assertAlmostEqual(100000.0, summary['A']['mean'])
self.assertEqual('milliwatt', summary['A']['unit'])
self.assertAlmostEqual(2.5, summary['B']['mean'])
self.assertEqual('millivolt', summary['B']['unit'])
if __name__ == '__main__':