| # SPDX-License-Identifier: Apache-2.0 |
| # |
| # Copyright (C) 2015, ARM Limited and contributors. |
| # |
| # 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 devlib |
| import json |
| import os |
| import psutil |
| import time |
| import logging |
| |
| from collections import namedtuple |
| from subprocess import Popen, PIPE, STDOUT |
| from time import sleep |
| |
| |
| # Default energy measurements for each board |
| DEFAULT_ENERGY_METER = { |
| |
| # ARM TC2: by default use HWMON |
| 'tc2' : { |
| 'instrument' : 'hwmon', |
| 'conf' : { |
| 'sites' : [ 'A7 Jcore', 'A15 Jcore' ], |
| 'kinds' : [ 'energy'] |
| }, |
| 'channel_map' : { |
| 'LITTLE' : 'A7 Jcore', |
| 'big' : 'A15 Jcore', |
| } |
| }, |
| |
| # ARM Juno: by default use HWMON |
| 'juno' : { |
| 'instrument' : 'hwmon', |
| 'conf' : { |
| 'sites' : [ 'BOARDLITTLE', 'BOARDBIG' ], |
| 'kinds' : [ 'energy' ] |
| }, |
| # if the channels do not contain a core name we can match to the |
| # little/big cores on the board, use a channel_map section to |
| # indicate which channel is which |
| 'channel_map' : { |
| 'LITTLE' : 'BOARDLITTLE', |
| 'big' : 'BOARDBIG', |
| } |
| }, |
| |
| } |
| |
| EnergyCounter = namedtuple('EnergyCounter', ['site', 'pwr_total' , 'pwr_samples', 'pwr_avg', 'time', 'nrg']) |
| EnergyReport = namedtuple('EnergyReport', ['channels', 'report_file']) |
| |
| class EnergyMeter(object): |
| |
| _meter = None |
| |
| def __init__(self, target, res_dir=None): |
| self._target = target |
| self._res_dir = res_dir |
| if not self._res_dir: |
| self._res_dir = '/tmp' |
| |
| # Setup logging |
| self._log = logging.getLogger('EnergyMeter') |
| |
| @staticmethod |
| def getInstance(target, conf, force=False, res_dir=None): |
| |
| if not force and EnergyMeter._meter: |
| return EnergyMeter._meter |
| |
| log = logging.getLogger('EnergyMeter') |
| |
| # Initialize energy meter based on configuration |
| if 'emeter' in conf: |
| emeter = conf['emeter'] |
| log.debug('using user-defined configuration') |
| |
| # Initialize energy probe to board default |
| elif 'board' in conf and \ |
| conf['board'] in DEFAULT_ENERGY_METER: |
| emeter = DEFAULT_ENERGY_METER[conf['board']] |
| log.debug('using default energy meter for [%s]', |
| conf['board']) |
| else: |
| return None |
| |
| if emeter['instrument'] == 'hwmon': |
| EnergyMeter._meter = HWMon(target, emeter, res_dir) |
| elif emeter['instrument'] == 'aep': |
| EnergyMeter._meter = AEP(target, emeter, res_dir) |
| elif emeter['instrument'] == 'acme': |
| EnergyMeter._meter = ACME(target, emeter, res_dir) |
| |
| log.debug('Results dir: %s', res_dir) |
| return EnergyMeter._meter |
| |
| def sample(self): |
| raise NotImplementedError('Missing implementation') |
| |
| def reset(self): |
| raise NotImplementedError('Missing implementation') |
| |
| def report(self, out_dir): |
| raise NotImplementedError('Missing implementation') |
| |
| class HWMon(EnergyMeter): |
| |
| def __init__(self, target, conf=None, res_dir=None): |
| super(HWMon, self).__init__(target, res_dir) |
| |
| # The HWMon energy meter |
| self._hwmon = None |
| |
| # Energy readings |
| self.readings = {} |
| |
| if 'hwmon' not in self._target.modules: |
| self._log.info('HWMON module not enabled') |
| self._log.warning('Energy sampling disabled by configuration') |
| return |
| |
| # Initialize HWMON instrument |
| self._log.info('Scanning for HWMON channels, may take some time...') |
| self._hwmon = devlib.HwmonInstrument(self._target) |
| |
| # Configure channels for energy measurements |
| self._log.debug('Enabling channels %s', conf['conf']) |
| self._hwmon.reset(**conf['conf']) |
| |
| # Logging enabled channels |
| self._log.info('Channels selected for energy sampling:') |
| for channel in self._hwmon.active_channels: |
| self._log.info(' %s', channel.label) |
| |
| # record the HWMon channels |
| self._channels = conf.get('channel_map', { |
| 'LITTLE': self._target.little_core.upper(), |
| 'big': self._target.big_core.upper() |
| }) |
| |
| def sample(self): |
| if self._hwmon is None: |
| return None |
| samples = self._hwmon.take_measurement() |
| for s in samples: |
| label = s.channel.label\ |
| .replace('_energy', '')\ |
| .replace(" ", "_") |
| value = s.value |
| |
| if label not in self.readings: |
| self.readings[label] = { |
| 'last' : value, |
| 'delta' : 0, |
| 'total' : 0 |
| } |
| continue |
| |
| self.readings[label]['delta'] = value - self.readings[label]['last'] |
| self.readings[label]['last'] = value |
| self.readings[label]['total'] += self.readings[label]['delta'] |
| |
| self._log.debug('SAMPLE: %s', self.readings) |
| return self.readings |
| |
| def reset(self): |
| if self._hwmon is None: |
| return |
| self.sample() |
| for label in self.readings: |
| self.readings[label]['delta'] = 0 |
| self.readings[label]['total'] = 0 |
| self._log.debug('RESET: %s', self.readings) |
| |
| def report(self, out_dir, out_file='energy.json'): |
| if self._hwmon is None: |
| return (None, None) |
| # Retrive energy consumption data |
| nrg = self.sample() |
| # Reformat data for output generation |
| clusters_nrg = {} |
| for channel in self._channels: |
| if channel not in nrg: |
| raise RuntimeError('hwmon channel "{}" not available. ' |
| 'Selected channels: {}'.format( |
| channel, nrg.keys())) |
| label = self._channels[channel] |
| nrg_total = nrg[label]['total'] |
| self._log.debug('Energy [%16s]: %.6f', label, nrg_total) |
| clusters_nrg[channel] = nrg_total |
| |
| # Dump data as JSON file |
| nrg_file = '{}/{}'.format(out_dir, out_file) |
| with open(nrg_file, 'w') as ofile: |
| json.dump(clusters_nrg, ofile, sort_keys=True, indent=4) |
| |
| return EnergyReport(clusters_nrg, nrg_file) |
| |
| class AEP(EnergyMeter): |
| |
| def __init__(self, target, conf, res_dir): |
| super(AEP, self).__init__(target, res_dir) |
| |
| # Energy channels |
| self.channels = [] |
| |
| # Time (start and diff) for power measurment |
| self.time = {} |
| |
| # Configure channels for energy measurements |
| self._log.info('AEP configuration') |
| self._log.info(' %s', conf) |
| self._aep = devlib.EnergyProbeInstrument( |
| self._target, labels=conf['channel_map'], **conf['conf']) |
| |
| # Configure channels for energy measurements |
| self._log.debug('Enabling channels') |
| self._aep.reset() |
| |
| # Logging enabled channels |
| self._log.info('Channels selected for energy sampling:') |
| self._log.info(' %s', str(self._aep.active_channels)) |
| self._log.debug('Results dir: %s', self._res_dir) |
| |
| def _get_energy(self, samples, time, idx, site): |
| pwr_total = 0 |
| pwr_samples = 0 |
| for sample in samples: |
| pwr_total += sample[idx].value |
| pwr_samples += 1 |
| |
| pwr_avg = pwr_total / pwr_samples |
| nrg = pwr_avg * time |
| |
| ec = EnergyCounter(site, pwr_total, pwr_samples, pwr_avg, time, nrg) |
| return ec |
| |
| def _sample(self, csv_file): |
| if self._aep is None: |
| return |
| |
| self.time['diff'] = time.time() - self.time['start'] |
| self._aep.stop() |
| |
| csv_data = self._aep.get_data(csv_file) |
| samples = csv_data.measurements() |
| |
| # Calculate energy for each channel |
| for idx,channel in enumerate(csv_data.channels): |
| if channel.kind is not 'power': |
| continue |
| ec = self._get_energy(samples, self.time['diff'], idx, channel.site) |
| self.channels.append(ec) |
| |
| def reset(self): |
| if self._aep is None: |
| return |
| |
| self.channels = [] |
| self._log.debug('RESET: %s', self.channels) |
| |
| self._aep.start() |
| self.time['start'] = time.time() |
| |
| def report(self, out_dir, out_energy='energy.json', out_samples='samples.csv'): |
| if self._aep is None: |
| return |
| |
| # Retrieve energy consumption data |
| csv_file = '{}/{}'.format(out_dir, out_samples) |
| self._sample(csv_file) |
| |
| # Reformat data for output generation |
| channels_nrg = {} |
| for channel in self.channels: |
| self._log.debug('Energy [%16s]: %.6f', |
| channel.site, channel.nrg) |
| channels_nrg[channel.site] = channel.nrg |
| |
| # Dump data as JSON file |
| nrg_file = '{}/{}'.format(out_dir, out_energy) |
| with open(nrg_file, 'w') as ofile: |
| json.dump(channels_nrg, ofile, sort_keys=True, indent=4) |
| |
| return EnergyReport(channels_nrg, nrg_file) |
| |
| |
| _acme_install_instructions = ''' |
| |
| If you need to measure energy using an ACME EnergyProbe, |
| please do follow installation instructions available here: |
| https://github.com/ARM-software/lisa/wiki/Energy-Meters-Requirements#iiocapture---baylibre-acme-cape |
| |
| Othwerwise, please select a different energy meter in your |
| configuration file. |
| |
| ''' |
| |
| class ACME(EnergyMeter): |
| """ |
| BayLibre's ACME board based EnergyMeter |
| """ |
| |
| def __init__(self, target, conf, res_dir): |
| super(ACME, self).__init__(target, res_dir) |
| |
| # Assume iio-capture is available in PATH |
| iioc = conf.get('conf', { |
| 'iio-capture' : 'iio-capture', |
| 'ip_address' : 'baylibre-acme.local', |
| }) |
| self._iiocapturebin = iioc.get('iio-capture', 'iio-capture') |
| self._hostname = iioc.get('ip_address', 'baylibre-acme.local') |
| |
| self._channels = conf.get('channel_map', { |
| 'CH0': '0' |
| }) |
| self._iio = {} |
| |
| self._log.info('ACME configuration:') |
| self._log.info(' binary: %s', self._iiocapturebin) |
| self._log.info(' device: %s', self._hostname) |
| self._log.info(' channels:') |
| for channel in self._channels: |
| self._log.info(' %s', self._str(channel)) |
| |
| # Check if iio-capture binary is available |
| try: |
| p = Popen([self._iiocapturebin, '-h'], stdout=PIPE, stderr=STDOUT) |
| except: |
| self._log.error('iio-capture binary [%s] not available', |
| self._iiocapturebin) |
| self._log.warning(_acme_install_instructions) |
| raise RuntimeError('Missing iio-capture binary') |
| |
| def sample(self): |
| raise NotImplementedError('Not available for ACME') |
| |
| def _iio_device(self, channel): |
| return 'iio:device{}'.format(self._channels[channel]) |
| |
| def _str(self, channel): |
| return '{} ({})'.format(channel, self._iio_device(channel)) |
| |
| def reset(self): |
| """ |
| Reset energy meter and start sampling from channels specified in the |
| target configuration. |
| """ |
| # Terminate already running iio-capture instance (if any) |
| wait_for_termination = 0 |
| for proc in psutil.process_iter(): |
| if self._iiocapturebin not in proc.cmdline(): |
| continue |
| for channel in self._channels: |
| if self._iio_device(channel) in proc.cmdline(): |
| self._log.debug('Killing previous iio-capture for [%s]', |
| self._iio_device(channel)) |
| self._log.debug(proc.cmdline()) |
| proc.kill() |
| wait_for_termination = 2 |
| |
| # Wait for previous instances to be killed |
| sleep(wait_for_termination) |
| |
| # Start iio-capture for all channels required |
| for channel in self._channels: |
| ch_id = self._channels[channel] |
| |
| # Setup CSV file to collect samples for this channel |
| csv_file = '{}/{}'.format( |
| self._res_dir, |
| 'samples_{}.csv'.format(channel) |
| ) |
| |
| # Start a dedicated iio-capture instance for this channel |
| self._iio[ch_id] = Popen([self._iiocapturebin, '-n', |
| self._hostname, '-o', |
| '-c', '-f', |
| csv_file, |
| self._iio_device(channel)], |
| stdout=PIPE, stderr=STDOUT) |
| |
| # Wait few milliseconds before to check if there is any output |
| sleep(1) |
| |
| # Check that all required channels have been started |
| for channel in self._channels: |
| ch_id = self._channels[channel] |
| |
| self._iio[ch_id].poll() |
| if self._iio[ch_id].returncode: |
| self._log.error('Failed to run %s for %s', |
| self._iiocapturebin, self._str(channel)) |
| self._log.warning('\n\n'\ |
| ' Make sure there are no iio-capture processes\n'\ |
| ' connected to %s and device %s\n', |
| self._hostname, self._str(channel)) |
| out, _ = self._iio[ch_id].communicate() |
| self._log.error('Output: [%s]', out.strip()) |
| self._iio[ch_id] = None |
| raise RuntimeError('iio-capture connection error') |
| |
| self._log.debug('Started %s on %s...', |
| self._iiocapturebin, self._str(channel)) |
| |
| def report(self, out_dir, out_energy='energy.json'): |
| """ |
| Stop iio-capture and collect sampled data. |
| |
| :param out_dir: Output directory where to store results |
| :type out_dir: str |
| |
| :param out_file: File name where to save energy data |
| :type out_file: str |
| """ |
| channels_nrg = {} |
| channels_stats = {} |
| for channel in self._channels: |
| ch_id = self._channels[channel] |
| |
| if self._iio[ch_id] is None: |
| continue |
| |
| self._iio[ch_id].poll() |
| if self._iio[ch_id].returncode: |
| # returncode not None means that iio-capture has terminated |
| # already, so there must have been an error |
| self._log.error('%s terminated for %s', |
| self._iiocapturebin, self._str(channel)) |
| out, _ = self._iio[ch_id].communicate() |
| self._log.error('[%s]', out) |
| self._iio[ch_id] = None |
| continue |
| |
| # kill process and get return |
| self._iio[ch_id].terminate() |
| out, _ = self._iio[ch_id].communicate() |
| self._iio[ch_id].wait() |
| self._iio[ch_id] = None |
| |
| self._log.debug('Completed IIOCapture for %s...', |
| self._str(channel)) |
| |
| # iio-capture return "energy=value", add a simple format check |
| if '=' not in out: |
| self._log.error('Bad output format for %s:', |
| self._str(channel)) |
| self._log.error('[%s]', out) |
| continue |
| |
| # Build energy counter object |
| nrg = {} |
| for kv_pair in out.split(): |
| key, val = kv_pair.partition('=')[::2] |
| nrg[key] = float(val) |
| channels_stats[channel] = nrg |
| |
| self._log.debug(self._str(channel)) |
| self._log.debug(nrg) |
| |
| # Save CSV samples file to out_dir |
| os.system('mv {}/samples_{}.csv {}' |
| .format(self._res_dir, channel, out_dir)) |
| |
| # Add channel's energy to return results |
| channels_nrg['{}'.format(channel)] = nrg['energy'] |
| |
| # Dump energy data |
| nrg_file = '{}/{}'.format(out_dir, out_energy) |
| with open(nrg_file, 'w') as ofile: |
| json.dump(channels_nrg, ofile, sort_keys=True, indent=4) |
| |
| # Dump energy stats |
| nrg_stats_file = os.path.splitext(out_energy)[0] + \ |
| '_stats' + os.path.splitext(out_energy)[1] |
| nrg_stats_file = '{}/{}'.format(out_dir, nrg_stats_file) |
| with open(nrg_stats_file, 'w') as ofile: |
| json.dump(channels_stats, ofile, sort_keys=True, indent=4) |
| |
| return EnergyReport(channels_nrg, nrg_file) |
| |
| # vim :set tabstop=4 shiftwidth=4 expandtab |