mirror of
				https://github.com/ARM-software/workload-automation.git
				synced 2025-11-04 09:02:12 +00:00 
			
		
		
		
	Merge pull request #144 from ep1cman/servo
servo_power: Added support for chromebook servo boards
This commit is contained in:
		
							
								
								
									
										228
									
								
								wlauto/instrumentation/servo_power_monitors/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								wlauto/instrumentation/servo_power_monitors/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,228 @@
 | 
			
		||||
#    Copyright 2016 ARM Limited
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
# you may not use this file except in compliance with the License.
 | 
			
		||||
# You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
# See the License for the specific language governing permissions and
 | 
			
		||||
# limitations under the License.
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# pylint: disable=W0613,E1101,attribute-defined-outside-init
 | 
			
		||||
from __future__ import division
 | 
			
		||||
import os
 | 
			
		||||
import subprocess
 | 
			
		||||
import signal
 | 
			
		||||
import csv
 | 
			
		||||
import threading
 | 
			
		||||
import time
 | 
			
		||||
import getpass
 | 
			
		||||
import logging
 | 
			
		||||
import xmlrpclib
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
from wlauto import Instrument, Parameter, Executable
 | 
			
		||||
from wlauto.exceptions import InstrumentError, ConfigError
 | 
			
		||||
from wlauto.utils.types import list_of_strings
 | 
			
		||||
from wlauto.utils.misc import check_output
 | 
			
		||||
from wlauto.utils.cros_sdk import CrosSdkSession
 | 
			
		||||
from wlauto.utils.misc import which
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ServoPowerMonitor(Instrument):
 | 
			
		||||
 | 
			
		||||
    name = 'servo_power'
 | 
			
		||||
    description = """
 | 
			
		||||
    Collects power traces using the Chromium OS Servo Board.
 | 
			
		||||
 | 
			
		||||
    Servo is a debug board used for Chromium OS test and development. Among other uses, it allows
 | 
			
		||||
    access to the built in power monitors (if present) of a Chrome OS device. More information on
 | 
			
		||||
    Servo board can be found in the link bellow:
 | 
			
		||||
 | 
			
		||||
    https://www.chromium.org/chromium-os/servo
 | 
			
		||||
 | 
			
		||||
    In order to use this instrument you need to be a sudoer and you need a chroot environment. More
 | 
			
		||||
    information on the chroot environment can be found on the link bellow:
 | 
			
		||||
 | 
			
		||||
    https://www.chromium.org/chromium-os/developer-guide
 | 
			
		||||
 | 
			
		||||
    If you wish to run servod on a remote machine you will need to allow it to accept external connections
 | 
			
		||||
    using the `--host` command line option, like so:
 | 
			
		||||
    `sudo servod -b some_board -c some_board.xml --host=''`
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    parameters = [
 | 
			
		||||
        Parameter('power_domains', kind=list_of_strings, default=[],
 | 
			
		||||
                  description="""The names of power domains to be monitored by the
 | 
			
		||||
                                 instrument using servod."""),
 | 
			
		||||
        Parameter('labels', kind=list_of_strings, default=[],
 | 
			
		||||
                  description="""Meaningful labels for each of the monitored domains."""),
 | 
			
		||||
        Parameter('chroot_path', kind=str,
 | 
			
		||||
                  description="""Path to chroot direcory on the host."""),
 | 
			
		||||
        Parameter('sampling_rate', kind=int, default=10,
 | 
			
		||||
                  description="""Samples per second."""),
 | 
			
		||||
        Parameter('board_name', kind=str, mandatory=True,
 | 
			
		||||
                  description="""The name of the board under test."""),
 | 
			
		||||
        Parameter('autostart', kind=bool, default=True,
 | 
			
		||||
                  description="""Automatically start `servod`. Set to `False` if you want to
 | 
			
		||||
                                 use an already running `servod` instance or a remote servo"""),
 | 
			
		||||
        Parameter('host', kind=str, default="localhost",
 | 
			
		||||
                  description="""When `autostart` is set to `False` you can specify the host
 | 
			
		||||
                                 on which `servod` is running allowing you to remotelly access
 | 
			
		||||
                                 as servo board.
 | 
			
		||||
 | 
			
		||||
                                 if `autostart` is `True` this parameter is ignored and `localhost`
 | 
			
		||||
                                 is used instead"""),
 | 
			
		||||
        Parameter('port', kind=int, default=9999,
 | 
			
		||||
                  description="""When `autostart` is set to false you must provide the port
 | 
			
		||||
                                 that `servod` is running on
 | 
			
		||||
 | 
			
		||||
                                 If `autostart` is `True` this parameter is ignored and the port
 | 
			
		||||
                                 output during the startup of `servod` will be used."""),
 | 
			
		||||
        Parameter('vid', kind=str,
 | 
			
		||||
                  description="""When more than one servo is plugged in, you must provide
 | 
			
		||||
                                 a vid/pid pair to identify the servio you wish to use."""),
 | 
			
		||||
        Parameter('pid', kind=str,
 | 
			
		||||
                  description="""When more than one servo is plugged in, you must provide
 | 
			
		||||
                                 a vid/pid pair to identify the servio you wish to use."""),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    # When trying to initialize servod, it may take some time until the server is up
 | 
			
		||||
    # Therefore we need to poll to identify when the sever has successfully started
 | 
			
		||||
    # servod_max_tries specifies the maximum number of times we will check to see if the server has started
 | 
			
		||||
    # while servod_delay_between_tries is the sleep time between checks.
 | 
			
		||||
    servod_max_tries = 100
 | 
			
		||||
    servod_delay_between_tries = 0.1
 | 
			
		||||
 | 
			
		||||
    def validate(self):
 | 
			
		||||
        # pylint: disable=access-member-before-definition
 | 
			
		||||
        if self.labels and len(self.power_domains) != len(self.labels):
 | 
			
		||||
            raise ConfigError('There should be exactly one label per power domain')
 | 
			
		||||
        if self.autostart:
 | 
			
		||||
            if self.host != 'localhost':  # pylint: disable=access-member-before-definition
 | 
			
		||||
                self.logger.warning('Ignoring host "%s" since autostart is set to "True"', self.host)
 | 
			
		||||
                self.host = "localhost"
 | 
			
		||||
        if (self.vid is None) != (self.pid is None):
 | 
			
		||||
            raise ConfigError('`vid` and `pid` must both be specified')
 | 
			
		||||
 | 
			
		||||
    def initialize(self, context):
 | 
			
		||||
        # pylint: disable=access-member-before-definition
 | 
			
		||||
        self.poller = None
 | 
			
		||||
        self.data = None
 | 
			
		||||
        self.stopped = True
 | 
			
		||||
        if not self.labels:
 | 
			
		||||
            self.labels = ["PORT_{}".format(channel) for channel, _ in enumerate(self.power_domains)]
 | 
			
		||||
 | 
			
		||||
        self.power_domains = [channel if channel.endswith("_mw") else
 | 
			
		||||
                              "{}_mw".format(channel) for channel in self.power_domains]
 | 
			
		||||
        self.label_map = {pd: l for pd, l in zip(self.power_domains, self.labels)}
 | 
			
		||||
 | 
			
		||||
        if self.autostart:
 | 
			
		||||
            self._start_servod()
 | 
			
		||||
 | 
			
		||||
    def setup(self, context):
 | 
			
		||||
        # pylint: disable=access-member-before-definition
 | 
			
		||||
        self.outfile = os.path.join(context.output_directory, 'servo.csv')
 | 
			
		||||
        self.poller = PowerPoller(self.host, self.port, self.power_domains, self.sampling_rate)
 | 
			
		||||
 | 
			
		||||
    def start(self, context):
 | 
			
		||||
        self.poller.start()
 | 
			
		||||
        self.stopped = False
 | 
			
		||||
 | 
			
		||||
    def stop(self, context):
 | 
			
		||||
        self.data = self.poller.stop()
 | 
			
		||||
        self.poller.join()
 | 
			
		||||
        self.stopped = True
 | 
			
		||||
 | 
			
		||||
        timestamps = self.data.pop("timestamp")
 | 
			
		||||
        for channel, data in self.data.iteritems():
 | 
			
		||||
            label = self.label_map[channel]
 | 
			
		||||
            data = [float(v) / 1000.0 for v in data]
 | 
			
		||||
            sample_sum = sum(data)
 | 
			
		||||
 | 
			
		||||
            metric_name = '{}_power'.format(label)
 | 
			
		||||
            power = sample_sum / len(data)
 | 
			
		||||
            context.result.add_metric(metric_name, round(power, 3), 'Watts')
 | 
			
		||||
 | 
			
		||||
            metric_name = '{}_energy'.format(label)
 | 
			
		||||
            energy = sample_sum * (1.0 / self.sampling_rate)
 | 
			
		||||
            context.result.add_metric(metric_name, round(energy, 3), 'Joules')
 | 
			
		||||
 | 
			
		||||
        with open(self.outfile, 'wb') as f:
 | 
			
		||||
            c = csv.writer(f)
 | 
			
		||||
            headings = ['timestamp'] + ['{}_power'.format(label) for label in self.labels]
 | 
			
		||||
            c.writerow(headings)
 | 
			
		||||
            for row in zip(timestamps, *self.data.itervalues()):
 | 
			
		||||
                c.writerow(row)
 | 
			
		||||
 | 
			
		||||
    def teardown(self, context):
 | 
			
		||||
        if not self.stopped:
 | 
			
		||||
            self.stop(context)
 | 
			
		||||
        if self.autostart:
 | 
			
		||||
            self.server_session.kill_session()
 | 
			
		||||
 | 
			
		||||
    def _start_servod(self):
 | 
			
		||||
        in_chroot = False if which('dut-control') is None else True
 | 
			
		||||
        password = ''
 | 
			
		||||
        if not in_chroot:
 | 
			
		||||
            msg = 'Instrument %s requires sudo access on this machine to start `servod`'
 | 
			
		||||
            self.logger.info(msg, self.name)
 | 
			
		||||
            self.logger.info('You need to be sudoer to use it.')
 | 
			
		||||
            password = getpass.getpass()
 | 
			
		||||
            check = subprocess.call('echo {} | sudo -S ls > /dev/null'.format(password), shell=True)
 | 
			
		||||
            if check:
 | 
			
		||||
                raise InstrumentError('Given password was either wrong or you are not a sudoer')
 | 
			
		||||
        self.server_session = CrosSdkSession(self.chroot_path, password=password)
 | 
			
		||||
        password = ''
 | 
			
		||||
 | 
			
		||||
        command = 'sudo servod -b {b} -c {b}.xml'
 | 
			
		||||
        if self.vid and self.pid:
 | 
			
		||||
            command += " -v " + self.vid
 | 
			
		||||
            command += " -p " + self.pid
 | 
			
		||||
        command += '&'
 | 
			
		||||
        self.server_session.send_command(command.format(b=self.board_name))
 | 
			
		||||
        for _ in xrange(self.servod_max_tries):
 | 
			
		||||
            server_lines = self.server_session.get_lines(timeout=1, from_stderr=True,
 | 
			
		||||
                                                         timeout_only_for_first_line=False)
 | 
			
		||||
            if server_lines:
 | 
			
		||||
                if 'Listening on' in server_lines[-1]:
 | 
			
		||||
                    self.port = int(server_lines[-1].split()[-1])
 | 
			
		||||
                    break
 | 
			
		||||
            time.sleep(self.servod_delay_between_tries)
 | 
			
		||||
        else:
 | 
			
		||||
            raise InstrumentError('Failed to start servod in cros_sdk environment')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PowerPoller(threading.Thread):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, host, port, channels, sampling_rate):
 | 
			
		||||
        super(PowerPoller, self).__init__()
 | 
			
		||||
        self.proxy = xmlrpclib.ServerProxy("http://{}:{}/".format(host, port))
 | 
			
		||||
        self.proxy.get(channels[1])  # Testing connection
 | 
			
		||||
        self.channels = channels
 | 
			
		||||
        self.data = {channel: [] for channel in channels}
 | 
			
		||||
        self.data['timestamp'] = []
 | 
			
		||||
        self.period = 1.0 / sampling_rate
 | 
			
		||||
 | 
			
		||||
        self.term_signal = threading.Event()
 | 
			
		||||
        self.term_signal.set()
 | 
			
		||||
        self.logger = logging.getLogger(self.__class__.__name__)
 | 
			
		||||
 | 
			
		||||
    def run(self):
 | 
			
		||||
        while self.term_signal.is_set():
 | 
			
		||||
            self.data['timestamp'].append(str(datetime.now()))
 | 
			
		||||
            for channel in self.channels:
 | 
			
		||||
                self.data[channel].append(float(self.proxy.get(channel)))
 | 
			
		||||
            time.sleep(self.period)
 | 
			
		||||
 | 
			
		||||
    def stop(self):
 | 
			
		||||
        self.term_signal.clear()
 | 
			
		||||
        self.join()
 | 
			
		||||
        return self.data
 | 
			
		||||
							
								
								
									
										132
									
								
								wlauto/utils/cros_sdk.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								wlauto/utils/cros_sdk.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,132 @@
 | 
			
		||||
#    Copyright 2016 ARM Limited
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
# you may not use this file except in compliance with the License.
 | 
			
		||||
# You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
# See the License for the specific language governing permissions and
 | 
			
		||||
# limitations under the License.
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from Queue import Queue, Empty
 | 
			
		||||
from threading import Thread
 | 
			
		||||
from subprocess import Popen, PIPE
 | 
			
		||||
from wlauto.utils.misc import which
 | 
			
		||||
from wlauto.exceptions import HostError
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OutputPollingThread(Thread):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, out, queue, name):
 | 
			
		||||
        super(OutputPollingThread, self).__init__()
 | 
			
		||||
        self.out = out
 | 
			
		||||
        self.queue = queue
 | 
			
		||||
        self.stop_signal = False
 | 
			
		||||
        self.name = name
 | 
			
		||||
 | 
			
		||||
    def run(self):
 | 
			
		||||
        for line in iter(self.out.readline, ''):
 | 
			
		||||
            if self.stop_signal:
 | 
			
		||||
                break
 | 
			
		||||
            self.queue.put(line)
 | 
			
		||||
 | 
			
		||||
    def set_stop(self):
 | 
			
		||||
        self.stop_signal = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CrosSdkSession(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, cros_path, password=''):
 | 
			
		||||
        self.logger = logging.getLogger(self.__class__.__name__)
 | 
			
		||||
        self.in_chroot = True if which('dut-control') else False
 | 
			
		||||
        ON_POSIX = 'posix' in sys.builtin_module_names
 | 
			
		||||
        if self.in_chroot:
 | 
			
		||||
            self.cros_sdk_session = Popen(['/bin/sh'], bufsize=1, stdin=PIPE, stdout=PIPE, stderr=PIPE,
 | 
			
		||||
                                          cwd=cros_path, close_fds=ON_POSIX, shell=True)
 | 
			
		||||
        else:
 | 
			
		||||
            cros_sdk_bin_path = which('cros_sdk')
 | 
			
		||||
            potential_path = os.path.join("cros_path", "chromium/tools/depot_tools/cros_sdk")
 | 
			
		||||
            if not cros_sdk_bin_path and os.path.isfile(potential_path):
 | 
			
		||||
                cros_sdk_bin_path = potential_path
 | 
			
		||||
            if not cros_sdk_bin_path:
 | 
			
		||||
                raise HostError("Failed to locate 'cros_sdk' make sure it is in your PATH")
 | 
			
		||||
            self.cros_sdk_session = Popen(['sudo -Sk {}'.format(cros_sdk_bin_path)], bufsize=1, stdin=PIPE,
 | 
			
		||||
                                          stdout=PIPE, stderr=PIPE, cwd=cros_path, close_fds=ON_POSIX, shell=True)
 | 
			
		||||
            self.cros_sdk_session.stdin.write(password)
 | 
			
		||||
            self.cros_sdk_session.stdin.write('\n')
 | 
			
		||||
        self.stdout_queue = Queue()
 | 
			
		||||
        self.stdout_thread = OutputPollingThread(self.cros_sdk_session.stdout, self.stdout_queue, 'stdout')
 | 
			
		||||
        self.stdout_thread.daemon = True
 | 
			
		||||
        self.stdout_thread.start()
 | 
			
		||||
        self.stderr_queue = Queue()
 | 
			
		||||
        self.stderr_thread = OutputPollingThread(self.cros_sdk_session.stderr, self.stderr_queue, 'stderr')
 | 
			
		||||
        self.stderr_thread.daemon = True
 | 
			
		||||
        self.stderr_thread.start()
 | 
			
		||||
 | 
			
		||||
    def kill_session(self):
 | 
			
		||||
        self.stdout_thread.set_stop()
 | 
			
		||||
        self.stderr_thread.set_stop()
 | 
			
		||||
        self.send_command('echo TERMINATE >&1')  # send something into stdout to unblock it and close it properly
 | 
			
		||||
        self.send_command('echo TERMINATE 1>&2')  # ditto for stderr
 | 
			
		||||
        self.stdout_thread.join()
 | 
			
		||||
        self.stderr_thread.join()
 | 
			
		||||
        self.cros_sdk_session.kill()
 | 
			
		||||
 | 
			
		||||
    def send_command(self, cmd, flush=True):
 | 
			
		||||
        if not cmd.endswith('\n'):
 | 
			
		||||
            cmd = cmd + '\n'
 | 
			
		||||
        self.logger.debug(cmd.strip())
 | 
			
		||||
        self.cros_sdk_session.stdin.write(cmd)
 | 
			
		||||
        if flush:
 | 
			
		||||
            self.cros_sdk_session.stdin.flush()
 | 
			
		||||
 | 
			
		||||
    def read_line(self, timeout=0):
 | 
			
		||||
        return _read_line_from_queue(self.stdout_queue, timeout=timeout, logger=self.logger)
 | 
			
		||||
 | 
			
		||||
    def read_stderr_line(self, timeout=0):
 | 
			
		||||
        return _read_line_from_queue(self.stderr_queue, timeout=timeout, logger=self.logger)
 | 
			
		||||
 | 
			
		||||
    def get_lines(self, timeout=0, timeout_only_for_first_line=True, from_stderr=False):
 | 
			
		||||
        lines = []
 | 
			
		||||
        line = True
 | 
			
		||||
        while line is not None:
 | 
			
		||||
            if from_stderr:
 | 
			
		||||
                line = self.read_stderr_line(timeout)
 | 
			
		||||
            else:
 | 
			
		||||
                line = self.read_line(timeout)
 | 
			
		||||
            if line:
 | 
			
		||||
                lines.append(line)
 | 
			
		||||
                if timeout and timeout_only_for_first_line:
 | 
			
		||||
                    timeout = 0  # after a line has been read, no further delay is required
 | 
			
		||||
        return lines
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _read_line_from_queue(queue, timeout=0, logger=None):
 | 
			
		||||
    try:
 | 
			
		||||
        line = queue.get_nowait()
 | 
			
		||||
    except Empty:
 | 
			
		||||
        line = None
 | 
			
		||||
    if line is None and timeout:
 | 
			
		||||
        sleep_time = timeout
 | 
			
		||||
        time.sleep(sleep_time)
 | 
			
		||||
        try:
 | 
			
		||||
            line = queue.get_nowait()
 | 
			
		||||
        except Empty:
 | 
			
		||||
            line = None
 | 
			
		||||
    if line is not None:
 | 
			
		||||
        line = line.strip('\n')
 | 
			
		||||
    if logger and line:
 | 
			
		||||
        logger.debug(line)
 | 
			
		||||
    return line
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user