From 90d8e5460f0f42d52a48b78843130c00b9cb6766 Mon Sep 17 00:00:00 2001 From: Mengqi Guo Date: Wed, 13 Sep 2017 16:51:04 -0700 Subject: sweetberry: calculate statistics for sweetberry readings This CL provides the tool to calculate statistics for sweetberry readings and present them in a clear & easy to read format. It also provides the flag to store raw data and statistics summary, should the need arise. There are also some code cleanup for powerlog.py. BRANCH=None BUG=b:35578707 TEST=./powerlog.py -b xxx.board -c xxx.scenario --print_stats \ --save_stats --save_raw_data python -m unittest -v stats_manager_unittest Change-Id: I4aa732756fe6512f37acfcb59b11d950101887d7 Signed-off-by: Mengqi Guo Reviewed-on: https://chromium-review.googlesource.com/667241 Reviewed-by: Nick Sanders --- extra/usb_power/__init__.py | 0 extra/usb_power/powerlog.py | 81 ++++++++++++------ extra/usb_power/stats_manager.py | 137 ++++++++++++++++++++++++++++++ extra/usb_power/stats_manager_unittest.py | 87 +++++++++++++++++++ 4 files changed, 278 insertions(+), 27 deletions(-) create mode 100644 extra/usb_power/__init__.py create mode 100644 extra/usb_power/stats_manager.py create mode 100644 extra/usb_power/stats_manager_unittest.py diff --git a/extra/usb_power/__init__.py b/extra/usb_power/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/extra/usb_power/powerlog.py b/extra/usb_power/powerlog.py index 5daafaa07f..5f39d7dc1b 100755 --- a/extra/usb_power/powerlog.py +++ b/extra/usb_power/powerlog.py @@ -6,9 +6,12 @@ """Program to fetch power logging data from a sweetberry device or other usb device that exports a USB power logging interface. """ +from __future__ import print_function import argparse import array +import datetime import json +import os import struct import sys import time @@ -17,14 +20,16 @@ from pprint import pprint import usb +from stats_manager import StatsManager + # This can be overridden by -v. debug = False def debuglog(msg): if debug: - print msg + print(msg) def logoutput(msg): - print msg + print(msg) sys.stdout.flush() @@ -309,11 +314,6 @@ class Spower(object): else: debuglog("Command START: FAIL") - title = "ts:%dus" % actual_us - for i in range(0, len(self._inas)): - name = self._inas[i]['name'] - title += ", %s uW" % name - return actual_us def add_ina_name(self, name): @@ -343,7 +343,7 @@ class Spower(object): raise Exception("Power", "Failed to find INA %s" % name) def set_time(self, timestamp_us): - """Set sweetberry tie to match host time. + """Set sweetberry time to match host time. Args: timestamp_us: host timestmap in us. @@ -403,7 +403,7 @@ class Spower(object): cmd = struct.pack(" 0 or seconds > 0.: - forever = False - # Set up logging interface. powerlogger = powerlog(brdfile, cfgfile, serial_a=serial_a, - serial_b=serial_b, sync_date=sync_date, use_ms=use_ms) + serial_b=serial_b, sync_date=sync_date, use_ms=use_ms, + print_stats=print_stats, save_stats=save_stats, + save_raw_data=save_raw_data) # Start logging. powerlogger.start(integration_us_request, seconds, sync_speed=sync_speed) diff --git a/extra/usb_power/stats_manager.py b/extra/usb_power/stats_manager.py new file mode 100644 index 0000000000..02f984f097 --- /dev/null +++ b/extra/usb_power/stats_manager.py @@ -0,0 +1,137 @@ +# Copyright 2017 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Calculates statistics for lists of data and pretty print them.""" + +from __future__ import print_function +import collections +import numpy +import os + +STATS_PREFIX = '@@' +KEY_PREFIX = '__' +# This prefix is used for keys that should not be shown in the summary tab, such +# as timeline keys. +NOSHOW_PREFIX = '!!' + +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._summary = {} + + def AddValue(self, domain, value): + """Add one value for a domain. + + Args: + domain: the domain name for the value. + value: one time reading for domain, expect type float. + """ + if isinstance(value, int): + value = float(value) + if isinstance(value, float): + self._data[domain].append(value) + return + print('Warning: value %s for domain %s is not a number, thus ignored.' % + (value, domain)) + + def CalculateStats(self): + """Calculate stats for all domain-data pairs. + + First erases all previous stats, then calculate stats for all data. + """ + self._summary = {} + 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, + } + + def _SummaryToString(self, prefix=STATS_PREFIX): + """Format summary into a string, ready for pretty print. + + Args: + prefix: start every row in summary string with prefix, for easier reading. + """ + headers = ('NAME', 'COUNT', 'MEAN', 'STDDEV', 'MAX', 'MIN') + table = [headers] + for domain in sorted(self._summary.keys()): + if domain.startswith(NOSHOW_PREFIX): + continue + stats = self._summary[domain] + row = [domain.lstrip(KEY_PREFIX)] + row.append(str(stats['count'])) + for entry in headers[2:]: + row.append('%.2f' % stats[entry.lower()]) + table.append(row) + + max_col_width = [] + for col_idx in range(len(table[0])): + col_item_widths = [len(row[col_idx]) for row in table] + max_col_width.append(max(col_item_widths)) + + formatted_table = [] + for row in table: + formatted_row = prefix + ' ' + for i in range(len(row)): + formatted_row += row[i].rjust(max_col_width[i] + 2) + formatted_table.append(formatted_row) + return '\n'.join(formatted_table) + + def PrintSummary(self, prefix=STATS_PREFIX): + """Print the formatted summary. + + Args: + prefix: start every row in summary string with prefix, for easier reading. + """ + summary_str = self._SummaryToString(prefix=prefix) + print(summary_str) + + def GetSummary(self): + """Getter for summary.""" + return self._summary + + def SaveSummary(self, directory, fname='summary.txt', prefix=STATS_PREFIX): + """Save summary to file. + + Args: + directory: directory to save the summary in. + fname: filename to save summary under. + prefix: start every row in summary string with prefix, for easier reading. + """ + summary_str = self._SummaryToString(prefix=prefix) + '\n' + + if not os.path.exists(directory): + os.makedirs(directory) + fname = os.path.join(directory, fname) + with open(fname, 'w') as f: + f.write(summary_str) + + def GetRawData(self): + """Getter for all raw_data.""" + return self._data + + def SaveRawData(self, directory, dirname='raw_data'): + """Save raw data to file. + + Args: + directory: directory to create the raw data folder in. + dirname: folder in which raw data live. + """ + if not os.path.exists(directory): + os.makedirs(directory) + dirname = os.path.join(directory, dirname) + if not os.path.exists(dirname): + os.makedirs(dirname) + for domain, data in self._data.iteritems(): + fname = 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') diff --git a/extra/usb_power/stats_manager_unittest.py b/extra/usb_power/stats_manager_unittest.py new file mode 100644 index 0000000000..9b86b15ad4 --- /dev/null +++ b/extra/usb_power/stats_manager_unittest.py @@ -0,0 +1,87 @@ +# Copyright 2017 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Unit tests for StatsManager.""" + +from __future__ import print_function +import os +import shutil +import tempfile +import unittest + +from stats_manager import StatsManager + +class TestStatsManager(unittest.TestCase): + """Test to verify StatsManager methods work as expected. + + StatsManager should collect raw data, calculate their statistics, and save + them in expected format. + """ + + def setUp(self): + """Set up data and create a temporary directory to save data and stats.""" + self.tempdir = tempfile.mkdtemp() + self.data = StatsManager() + self.data.AddValue('A', 99999.5) + self.data.AddValue('A', 100000.5) + self.data.AddValue('A', 'ERROR') + self.data.AddValue('B', 1.5) + self.data.AddValue('B', 2.5) + self.data.AddValue('B', 3.5) + self.data.CalculateStats() + + def tearDown(self): + """Delete the temporary directory and its content.""" + shutil.rmtree(self.tempdir) + + def test_GetRawData(self): + raw_data = self.data.GetRawData() + self.assertListEqual([99999.5, 100000.5], raw_data['A']) + self.assertListEqual([1.5, 2.5, 3.5], raw_data['B']) + + def test_GetSummary(self): + summary = self.data.GetSummary() + self.assertEqual(2, summary['A']['count']) + self.assertAlmostEqual(100000.5, summary['A']['max']) + self.assertAlmostEqual(99999.5, summary['A']['min']) + self.assertAlmostEqual(0.5, summary['A']['stddev']) + self.assertAlmostEqual(100000.0, summary['A']['mean']) + self.assertEqual(3, summary['B']['count']) + self.assertAlmostEqual(3.5, summary['B']['max']) + self.assertAlmostEqual(1.5, summary['B']['min']) + self.assertAlmostEqual(0.81649658092773, summary['B']['stddev']) + self.assertAlmostEqual(2.5, summary['B']['mean']) + + def test_SaveRawData(self): + 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') + with open(fileA, 'r') as fA: + self.assertEqual('99999.50', fA.readline().strip()) + self.assertEqual('100000.50', fA.readline().strip()) + with open(fileB, 'r') as fB: + self.assertEqual('1.50', fB.readline().strip()) + self.assertEqual('2.50', fB.readline().strip()) + self.assertEqual('3.50', fB.readline().strip()) + + def test_SaveSummary(self): + fname = 'unittest_summary.txt' + self.data.SaveSummary(self.tempdir, fname) + fname = os.path.join(self.tempdir, fname) + with open(fname, 'r') as f: + self.assertEqual( + '@@ NAME COUNT MEAN STDDEV MAX MIN\n', + f.readline()) + self.assertEqual( + '@@ A 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', + f.readline()) + + +if __name__ == '__main__': + unittest.main() -- cgit v1.2.1