blob: 58979437695fb8a275be375437127c1cb15c7ec3 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2018 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Servod power measurement utility."""
import argparse
import logging
import os
import shutil
import signal
import sys
import tempfile
import threading
# This module is just a wrapper around measure_power functionality
from servo import client
from servo import dut_power_data
from servo import http_server
from servo import measure_power
from servo import servo_parsing
class ProgressPrinter(threading.Thread):
"""Print a marker every few seconds to indicate progress.
Public Attributes:
stop: Event object to signal end to printing.
"""
# Default progress marker.
PROGRESS_MARKER = "."
# Default wait marker.
WAIT_MARKER = "-"
# Default rate to print markers.
PROGRESS_UPDATE_RATE = 1.0
def __init__(
self,
marker=PROGRESS_MARKER,
rate=PROGRESS_UPDATE_RATE,
stop_signal=None,
max_duration=float("inf"),
):
"""Initialize constants & prepare thread to run."""
super(ProgressPrinter, self).__init__()
self._marker = marker
self._rate = rate
self._remaining_markers = max_duration / rate
if not stop_signal:
stop_signal = threading.Event()
self.stop = stop_signal
def run(self):
"""Print |_marker|s.
Every |_rate| seconds until |stop| is set or we've printed the maximum
markers if the max_duration field was set.
"""
while not self.stop.is_set() and self._remaining_markers >= 1.0:
sys.stdout.write(self._marker)
self._remaining_markers -= 1.0
sys.stdout.flush()
self.stop.wait(self._rate)
class DutPower:
"""Dut-power class to execute dut-power cmdline."""
def _add_mutually_exclusive_action(self, name, parser, default=True, action="save"):
"""Add both '--do-something' and '--no-do-something' pair to parser.
This adds a mutually exclusive switch for a boolean action into a parser.
Adds two flags:
--%{action}-%{name}
--no-%{action}-%{name}
Args:
name: object on which to perform the action
parser: parser to attach mutually exclusive group to
default: default value for boolean switch
action: action to perform on name
"""
saver = parser.add_mutually_exclusive_group()
argname = "--%s-%s" % (action, name)
noargname = "--no-%s-%s" % (action, name)
dest = "%s_%s" % (action, name.replace("-", "_"))
arghelp = "%s %s" % (action, name)
saver.add_argument(
argname, default=default, dest=dest, action="store_true", help=arghelp
)
noarghelp = "don't %s %s" % (action, name)
saver.add_argument(
noargname,
default=argparse.SUPPRESS,
dest=dest,
action="store_false",
help=noarghelp,
)
def _build_parser(self):
"""Create a parser to parse the cmdline dut-power command.
Returns:
a parser that parses dut-power cmdline.
"""
description = "Measure power using servod."
# BaseServodParser provides port, host, debug arguments
parser = servo_parsing.ServodClientParser(description=description)
# overwriting/providing measurement information so the servo device
# does not need to query for it.
parser.add_argument(
"--powerstate",
default=measure_power.DEFAULT_POWERSTATE,
choices=measure_power.POWERSTATES,
help="powerstate being measured (determines data dst)",
)
parser.add_argument(
"-b",
"--board",
default=measure_power.DEFAULT_BOARD,
help="board being measured (determines data dst)",
)
# power measurement logistics
parser.add_argument(
"-f",
"--fast",
default=False,
action="store_true",
help="if fast no verification cmds are done",
)
parser.add_argument(
"-w",
"--wait",
default=0,
type=float,
help="time (sec) to wait before measuring power",
)
parser.add_argument(
"-t",
"--time",
default=60,
type=float,
help="time (sec) to measure power for",
)
# This is the filter group - to either remove or only keep depending on regex
fg = parser.add_mutually_exclusive_group()
fg.add_argument(
"--filter-out",
default=None,
type=str,
help="filter out rails names that match this regex",
)
fg.add_argument(
"--filter",
default=None,
type=str,
help="only measure rail names that match this regex",
)
adcg = parser.add_mutually_exclusive_group()
# TODO(coconutruben): remove --ina-rate as legacy name once all dependencies
# are removed, and people have had time to switch scripts/docs/workflows
adcg.add_argument(
"--ina-rate",
default=measure_power.DEFAULT_ADC_RATE,
dest="adc_rate",
type=float,
help="rate (sec) to query the "
"ADCs, if <= 0 then ADCs will not be queried",
)
adcg.add_argument(
"--adc-rate",
default=measure_power.DEFAULT_ADC_RATE,
dest="adc_rate",
type=float,
help="rate (sec) to query the "
"ADCs, if <= 0 then ADCs will not be queried",
)
parser.add_argument(
"--adc-accum-rate",
default=measure_power.DEFAULT_ADC_ACCUM_RATE,
type=float,
help="rate (sec) to query the ADCs accumulators for avg "
"power numbers (if applicable), if <= 0 then ADC "
"accumulators will not be queried",
)
parser.add_argument(
"--vbat-rate",
default=measure_power.DEFAULT_VBAT_RATE,
type=float,
help="rate (sec) to query the ec vbat command, if <= 0 "
"then ec vbat will not be queried",
)
# output and logging logic
parser.add_argument(
"--no-output",
default=False,
action="store_true",
help="do not output anything into stdout",
)
parser.add_argument(
"-o", "--outdir", default=None, help="directory to save data into"
)
parser.add_argument(
"-m",
"--message",
default=None,
help="message to append to each summary file stored",
)
self._add_mutually_exclusive_action("raw-data", parser, default=False)
self._add_mutually_exclusive_action("summary", parser)
self._add_mutually_exclusive_action("json", parser, default=False)
# NOTE: if logging gets too verbose, turn default off
self._add_mutually_exclusive_action("logs", parser)
parser.add_argument(
"--save-all",
default=False,
action="store_true",
help="Equivalent to --save-summary --save-logs "
"--save-raw-data. Overwrites any of those if specified.",
)
# Start the visualization server
parser.add_argument(
"--visualization",
default=False,
action="store_true",
help="Visualization the power measurement results on a local server.",
)
# Specify the http server port for passing the information to html
parser.add_argument(
"--visualization-port",
default=9998,
type=int,
help="A port number between 0 and 9998 which is used for"
"the server to serve the visualized power measurement results."
"Choose 0 to get a random port number.",
)
return parser
def _parse_cmdline(self, parser, cmdline):
"""Parse the cmdline using a parser.
Args:
parser: a parser that can parse a cmdline sys.argv[1:]
cmdline: a cmdline generated from sys.argv[1:]
Returns:
Args parsed by the parser from the cmdline
"""
args = parser.parse_args(cmdline)
# Save all logic
if args.save_all:
args.save_logs = True
args.save_raw_data = True
args.save_summary = True
args.save_json = True
return args
def _setup_logging(self, args):
"""Set up logging of dut-power.
Args:
args: args parsed from the dut-power cmdline.
"""
pm_logger = logging.getLogger("")
pm_logger.setLevel(logging.INFO)
pm_logger.handlers.clear()
if not args.port:
args.port = client.DEFAULT_PORT
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.INFO)
if args.debug:
pm_logger.setLevel(logging.DEBUG)
stdout_handler.setLevel(logging.DEBUG)
if not args.no_output:
pm_logger.addHandler(stdout_handler)
if args.save_logs:
# Default mode is 'w+b', but the messages passed are strings. Overwrite
# default mode to be 'w+'
tmplogfile = tempfile.NamedTemporaryFile(mode="w+")
logfilehandler = logging.StreamHandler(tmplogfile)
logfilehandler.setLevel(logging.DEBUG)
pm_logger.addHandler(logfilehandler)
if args.adc_accum_rate:
# We can't set the accumulator time too long since we will lose the
# samples. Ideally we'd send a shutdown event early and wait for each
# driver to finish but the transactions take a very long time to
# complete and dut-power has no knowledge of the drivers and limits.
# As a work around, we'll artificially reduce the time.
# Benchmarks with ServoMicro and a single PAC1954 show it requires
# 10ms for each rail's accumulator due to Servod and the devices not
# optimizing and batching transactions. The max read time should be
# under 1 second but GPIO-I2C setups may take longer.
max_adc_accum_rate = max(0, args.time - 2)
args.adc_accum_rate = min(args.adc_accum_rate, max_adc_accum_rate)
self.pm_logger = pm_logger
self.tmplogfile = tmplogfile
def _setup_visualization(self, args, pm):
"""Set up visualization.
Args:
args: args parsed from dut-power cmdline.
pm: PowerMeasurement object that measures power and stores measurement data.
"""
server_port = args.visualization_port
self.power_data = dut_power_data.DataSampler(pm)
self.http_server_handler = http_server.HttpRequestHandler(self.power_data)
if self.http_server_handler.is_port_used(server_port):
self.pm_logger.error(
"port: %d is already in use. USE --visualization-port \
argument to change another port.",
server_port,
)
sys.exit(1)
file_path = self.http_server_handler.get_visualization_html_exist()
# If the path does not exist, we would tell
# the user how to build the visualization html
if not file_path:
self.pm_logger.error("The html file does not exist")
self.pm_logger.error(
"If the path above does not exist, "
"please follow these steps to build the visualization html"
)
self.pm_logger.error(
"cd ~/chromiumos/src/platform2/parallax/\n"
"run: sudo emerge net-libs/nodejs\n"
"run: npm install\n"
"run: npm run build -- release\n"
)
sys.exit(1)
try:
self.visualization_server = http_server.ThreadedTCPServer(
("localhost", server_port), self.http_server_handler
)
if server_port == 0:
_unused, server_port = self.visualization_server.server_address
except Exception:
self.pm_logger.error(
"Failed to start http server. You may try to switch to"
"another port by Use --visualization-port"
)
sys.exit(1)
self.pm_logger.info("Try to use port: %d for visualization", server_port)
self.pm_logger.info("Real-time visualization is available on: %s", file_path)
self.pm_logger.info(
"In the html page, switch the localhost number to %d "
"and press toggle stream to start",
server_port,
)
self.pm_logger.info("press ctrl-c to stop the visualization server.")
visualization_server_thread = threading.Thread(
target=self.visualization_server.serve_forever, daemon=True
)
visualization_server_thread.start()
def _measure_power(self, args, pm):
"""Measure power.
Args:
args: args parsed from dut-power cmdline.
pm: PowerMeasurement object that measures power and stores measurement data.
"""
# pylint: disable=undefined-variable
# Event.wait() is used as a preemptible way to sleep and control the
# ProgressPrinters while handling the SIGTERM/SIGINT signals
sleep_waiting = threading.Event()
sleep_sampling = threading.Event()
setup_done = pm.MeasurePower(wait=args.wait, powerstate=args.powerstate)
handler = lambda signal, _unused, pm=pm, sw=sleep_waiting, ss=sleep_sampling: (
sw.set(),
ss.set(),
pm.FinishMeasurement(),
)
if args.visualization:
handler = (
lambda signal, _unused, pm=pm, sw=sleep_waiting, ss=sleep_sampling: (
sw.set(),
ss.set(),
pm.FinishMeasurement(),
self.visualization_server.server_close(),
self.visualization_server.shutdown(),
)
)
# Ensure that SIGTERM and SIGNINT gracefully stop the measurement
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGTERM, handler)
if args.visualization:
# Start to prepare the data which will pass to the visualization UI
threading.Thread(
target=self.power_data.sample_generator, daemon=True
).start()
# Wait until measurement is setup
setup_done.wait()
if not args.no_output:
waiting_printer = ProgressPrinter(
marker=ProgressPrinter.WAIT_MARKER,
stop_signal=sleep_waiting,
max_duration=args.wait,
)
# Start printing progress once power collection has started
waiting_printer.start()
# Sleep for the wait time and stop printing the wait symbol.
sleep_waiting.wait(args.wait)
sleep_waiting.set()
if not args.no_output:
sampling_printer = ProgressPrinter(
stop_signal=sleep_sampling, max_duration=args.time
)
# Start printing progress once power collection has started
sampling_printer.start()
# Sleep for measurement time and wait time. Will wake on SIGINT & SIGTERM
if args.visualization:
sleep_sampling.wait()
else:
sleep_sampling.wait(args.time)
# To ensure the ProgressPrinter also stops printing.
sleep_sampling.set()
def _save_results(self, args, pm):
"""Save power measurement results.
Args:
args: args parsed from dut-power cmdline.
pm: PowerMeasurement object that measures power and stores measurement data.
"""
if args.save_summary:
pm.SaveSummary(args.outdir, args.message)
if args.save_raw_data:
pm.SaveRawData(args.outdir)
if args.save_json:
pm.SaveSummaryJSON(args.outdir)
if args.save_logs:
# pylint: disable=protected-access
outdir = pm._outdir
if args.outdir and os.path.isdir(args.outdir):
outdir = args.outdir
logfile = os.path.join(outdir, "logs.txt")
self.pm_logger.info("Storing logs at:\n%s", logfile)
shutil.move(self.tmplogfile.name, logfile)
if args.visualization:
self.http_server_handler.save_visualization_html(pm._outdir)
# pylint: disable=dangerous-default-value
def main(self, cmdline):
"""Main function for dut-power.
Args:
cmdline: dut-power cmdline.
"""
parser = self._build_parser()
# Add double-dashes in help message to unify docker and standalone versions
parser.prog = parser.prog + " --"
args = self._parse_cmdline(parser, cmdline)
self._setup_logging(args)
try:
pm = measure_power.PowerMeasurement(
host=args.host,
port=args.port,
adc_rate=args.adc_rate,
adc_accum_rate=args.adc_accum_rate,
vbat_rate=args.vbat_rate,
fast=args.fast,
board=args.board,
rgx_to_keep=args.filter,
rgx_to_remove=args.filter_out,
)
except measure_power.NoSourceError as e:
self.pm_logger.info(e)
sys.exit(1)
if args.visualization:
self._setup_visualization(args, pm)
self._measure_power(args, pm)
# Indicate that measurement should stop, as ProcessMeasurement sets
# stop_signal internally as well
pm.ProcessMeasurement()
pm.DisplaySummary()
self._save_results(args, pm)
def main(cmdline=sys.argv[1:]):
if len(cmdline) > 0 and cmdline[0] == "--":
cmdline = cmdline[1:]
dut_power = DutPower()
dut_power.main(cmdline)
if __name__ == "__main__":
main(sys.argv[1:])