mirror of
https://github.com/ARM-software/devlib.git
synced 2025-09-24 04:41:54 +01:00
Add a new Arm Energy Probe instrument
Add new energy instrument that is based on arm-probe tool to manage AEP Main advantages of this tool are: - uses a config file for describing channels and shunt resistors value - manages power topology description in the config file. This topology is then used when computing power figures - can create virtual power channel and aggregate channels - support multiple AEP - support auto-zero of AEP's channel Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org>
This commit is contained in:
committed by
setrofim
parent
9e8f77b8f2
commit
a0fc7202a1
521
devlib/utils/parse_aep.py
Executable file
521
devlib/utils/parse_aep.py
Executable file
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright 2018 Linaro 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 os
|
||||
import sys
|
||||
import getopt
|
||||
import subprocess
|
||||
import logging
|
||||
import signal
|
||||
import serial
|
||||
import time
|
||||
import math
|
||||
|
||||
logger = logging.getLogger('aep-parser')
|
||||
|
||||
class AepParser(object):
|
||||
prepared = False
|
||||
|
||||
@staticmethod
|
||||
def topology_from_data(array, topo):
|
||||
# Extract topology information for the data file
|
||||
# The header of a data file looks like this ('#' included):
|
||||
# configuration: <file path>
|
||||
# config_name: <file name>
|
||||
# trigger: 0.400000V (hyst 0.200000V) 0.000000W (hyst 0.200000W) 400us
|
||||
# date: Fri, 10 Jun 2016 11:25:07 +0200
|
||||
# host: <host name>
|
||||
#
|
||||
# CHN_0 Pretty_name_0 PARENT_0 Color0 Class0
|
||||
# CHN_1 Pretty_name_1 PARENT_1 Color1 Class1
|
||||
# CHN_2 Pretty_name_2 PARENT_2 Color2 Class2
|
||||
# CHN_3 Pretty_name_3 PARENT_3 Color3 Class3
|
||||
# ..
|
||||
# CHN_N Pretty_name_N PARENT_N ColorN ClassN
|
||||
#
|
||||
|
||||
info = {}
|
||||
|
||||
if len(array) == 6:
|
||||
info['name'] = array[1]
|
||||
info['parent'] = array[3]
|
||||
info['pretty'] = array[2]
|
||||
# add an entry for both name and pretty name in order to not parse
|
||||
# the whole dict when looking for a parent and the parent of parent
|
||||
topo[array[1]] = info
|
||||
topo[array[2]] = info
|
||||
return topo
|
||||
|
||||
@staticmethod
|
||||
def create_virtual(topo, label, hide, duplicate):
|
||||
# Create a list of virtual power domain that are the sum of others
|
||||
# A virtual domain is the parent of several channels but is not sampled by a
|
||||
# channel
|
||||
# This can be useful if a power domain is supplied by 2 power rails
|
||||
virtual = {}
|
||||
|
||||
# Create an entry for each virtual parent
|
||||
for supply in topo.iterkeys():
|
||||
index = topo[supply]['index']
|
||||
# Don't care of hidden columns
|
||||
if hide[index]:
|
||||
continue
|
||||
|
||||
# Parent is in the topology
|
||||
parent = topo[supply]['parent']
|
||||
if parent in topo:
|
||||
continue
|
||||
|
||||
if parent not in virtual:
|
||||
virtual[parent] = { supply : index }
|
||||
|
||||
virtual[parent][supply] = index
|
||||
|
||||
# Remove parent with 1 child as they don't give more information than their
|
||||
# child
|
||||
for supply in virtual.keys():
|
||||
if len(virtual[supply]) == 1:
|
||||
del virtual[supply];
|
||||
|
||||
for supply in virtual.keys():
|
||||
# Add label, hide and duplicate columns for virtual domains
|
||||
hide.append(0)
|
||||
duplicate.append(1)
|
||||
label.append(supply)
|
||||
|
||||
return virtual
|
||||
|
||||
@staticmethod
|
||||
def get_label(array):
|
||||
# Get the label of each column
|
||||
# Remove unit '(X)' from the end of the label
|
||||
label = [""]*len(array)
|
||||
unit = [""]*len(array)
|
||||
|
||||
label[0] = array[0]
|
||||
unit[0] = "(S)"
|
||||
for i in range(1,len(array)):
|
||||
label[i] = array[i][:-3]
|
||||
unit[i] = array[i][-3:]
|
||||
|
||||
return label, unit
|
||||
|
||||
@staticmethod
|
||||
def filter_column(label, unit, topo):
|
||||
# Filter columns
|
||||
# We don't parse Volt and Amper columns: put in hide list
|
||||
# We don't add in Total a column that is the child of another one: put in duplicate list
|
||||
|
||||
# By default we hide all columns
|
||||
hide = [1] * len(label)
|
||||
# By default we assume that there is no child
|
||||
duplicate = [0] * len(label)
|
||||
|
||||
for i in range(len(label)):
|
||||
# We only care about time and Watt
|
||||
if label[i] == 'time':
|
||||
hide[i] = 0
|
||||
continue
|
||||
|
||||
if '(W)' not in unit[i]:
|
||||
continue
|
||||
|
||||
hide[i] = 0
|
||||
|
||||
#label is pretty name
|
||||
pretty = label[i]
|
||||
|
||||
# We don't add a power domain that is already accounted by its parent
|
||||
if topo[pretty]['parent'] in topo:
|
||||
duplicate[i] = 1
|
||||
|
||||
# Set index, that will be used by virtual domain
|
||||
topo[topo[pretty]['name']]['index'] = i
|
||||
|
||||
# remove pretty element that is useless now
|
||||
del topo[pretty]
|
||||
|
||||
return hide, duplicate
|
||||
|
||||
@staticmethod
|
||||
def parse_text(array, hide):
|
||||
data = [0]*len(array)
|
||||
for i in range(len(array)):
|
||||
if hide[i]:
|
||||
continue
|
||||
|
||||
try:
|
||||
data[i] = int(float(array[i])*1000000)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def add_virtual_data(data, virtual):
|
||||
# write virtual domain
|
||||
for parent in virtual.iterkeys():
|
||||
power = 0
|
||||
for child in virtual[parent].values():
|
||||
try:
|
||||
power += data[child]
|
||||
except IndexError:
|
||||
continue
|
||||
data.append(power)
|
||||
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def delta_nrj(array, delta, min, max, hide):
|
||||
# Compute the energy consumed in this time slice and add it
|
||||
# delta[0] is used to save the last time stamp
|
||||
|
||||
if (delta[0] < 0):
|
||||
delta[0] = array[0]
|
||||
|
||||
time = array[0] - delta[0]
|
||||
if (time <= 0):
|
||||
return delta
|
||||
|
||||
for i in range(len(array)):
|
||||
if hide[i]:
|
||||
continue
|
||||
|
||||
try:
|
||||
data = array[i]
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if (data < min[i]):
|
||||
min[i] = data
|
||||
if (data > max[i]):
|
||||
max[i] = data
|
||||
delta[i] += time * data
|
||||
|
||||
# save last time stamp
|
||||
delta[0] = array[0]
|
||||
|
||||
return delta
|
||||
|
||||
def output_label(self, label, hide):
|
||||
self.fo.write(label[0]+"(uS)")
|
||||
for i in range(1, len(label)):
|
||||
if hide[i]:
|
||||
continue
|
||||
self.fo.write(" "+label[i]+"(uW)")
|
||||
|
||||
self.fo.write("\n")
|
||||
|
||||
def output_power(self, array, hide):
|
||||
#skip partial line. Most probably the last one
|
||||
if len(array) < len(hide):
|
||||
return
|
||||
|
||||
# write not hidden colums
|
||||
self.fo.write(str(array[0]))
|
||||
for i in range(1, len(array)):
|
||||
if hide[i]:
|
||||
continue
|
||||
|
||||
self.fo.write(" "+str(array[i]))
|
||||
|
||||
self.fo.write("\n")
|
||||
|
||||
def prepare(self, infile, outfile, summaryfile):
|
||||
|
||||
try:
|
||||
self.fi = open(infile, "r")
|
||||
except IOError:
|
||||
logger.warn('Unable to open input file {}'.format(infile))
|
||||
logger.warn('Usage: parse_arp.py -i <inputfile> [-o <outputfile>]')
|
||||
sys.exit(2)
|
||||
|
||||
self.parse = True
|
||||
if len(outfile) > 0:
|
||||
try:
|
||||
self.fo = open(outfile, "w")
|
||||
except IOError:
|
||||
logger.warn('Unable to create {}'.format(outfile))
|
||||
self.parse = False
|
||||
else:
|
||||
self.parse = False
|
||||
|
||||
self.summary = True
|
||||
if len(summaryfile) > 0:
|
||||
try:
|
||||
self.fs = open(summaryfile, "w")
|
||||
except IOError:
|
||||
logger.warn('Unable to create {}'.format(summaryfile))
|
||||
self.fs = sys.stdout
|
||||
else:
|
||||
self.fs = sys.stdout
|
||||
|
||||
self.prepared = True
|
||||
|
||||
def unprepare(self):
|
||||
if not self.prepared:
|
||||
# nothing has been prepared
|
||||
return
|
||||
|
||||
self.fi.close()
|
||||
|
||||
if self.parse:
|
||||
self.fo.close()
|
||||
|
||||
self.prepared = False
|
||||
|
||||
def parse_aep(self, start=0, lenght=-1):
|
||||
# Parse aep data and calculate the energy consumed
|
||||
begin = 0
|
||||
|
||||
label_line = 1
|
||||
|
||||
topo = {}
|
||||
|
||||
lines = self.fi.readlines()
|
||||
|
||||
for myline in lines:
|
||||
array = myline.split()
|
||||
|
||||
if "#" in myline:
|
||||
# update power topology
|
||||
topo = self.topology_from_data(array, topo)
|
||||
continue
|
||||
|
||||
if label_line:
|
||||
label_line = 0
|
||||
# 1st line not starting with # gives label of each column
|
||||
label, unit = self.get_label(array)
|
||||
# hide useless columns and detect channels that are children
|
||||
# of other channels
|
||||
hide, duplicate = self.filter_column(label, unit, topo)
|
||||
|
||||
# Create virtual power domains
|
||||
virtual = self.create_virtual(topo, label, hide, duplicate)
|
||||
if self.parse:
|
||||
self.output_label(label, hide)
|
||||
|
||||
logger.debug('Topology : {}'.format(topo))
|
||||
logger.debug('Virtual power domain : {}'.format(virtual))
|
||||
logger.debug('Duplicated power domain : : {}'.format(duplicate))
|
||||
logger.debug('Name of columns : {}'.format(label))
|
||||
logger.debug('Hidden columns : {}'.format(hide))
|
||||
logger.debug('Unit of columns : {}'.format(unit))
|
||||
|
||||
# Init arrays
|
||||
nrj = [0]*len(label)
|
||||
min = [100000000]*len(label)
|
||||
max = [0]*len(label)
|
||||
offset = [0]*len(label)
|
||||
|
||||
continue
|
||||
|
||||
# convert text to int and unit to micro-unit
|
||||
data = self.parse_text(array, hide)
|
||||
|
||||
# get 1st time stamp
|
||||
if begin <= 0:
|
||||
being = data[0]
|
||||
|
||||
# skip data before start
|
||||
if (data[0]-begin) < start:
|
||||
continue
|
||||
|
||||
# stop after lenght
|
||||
if lenght >= 0 and (data[0]-begin) > (start + lenght):
|
||||
continue
|
||||
|
||||
# add virtual domains
|
||||
data = self.add_virtual_data(data, virtual)
|
||||
|
||||
# extract power figures
|
||||
self.delta_nrj(data, nrj, min, max, hide)
|
||||
|
||||
# write data into new file
|
||||
if self.parse:
|
||||
self.output_power(data, hide)
|
||||
|
||||
# if there is no data just return
|
||||
if label_line or len(nrj) == 1:
|
||||
raise ValueError('No data found in the data file. Please check the Arm Energy Probe')
|
||||
return
|
||||
|
||||
# display energy consumption of each channel and total energy consumption
|
||||
total = 0
|
||||
results_table = {}
|
||||
for i in range(1, len(nrj)):
|
||||
if hide[i]:
|
||||
continue
|
||||
|
||||
nrj[i] -= offset[i] * nrj[0]
|
||||
|
||||
total_nrj = nrj[i]/1000000000000.0
|
||||
duration = (max[0]-min[0])/1000000.0
|
||||
channel_name = label[i]
|
||||
average_power = total_nrj/duration
|
||||
|
||||
self.fs.write("Total nrj: %8.3f J for %s -- duration %8.3f sec -- min %8.3f W -- max %8.3f W\n" % (nrj[i]/1000000000000.0, label[i], (max[0]-min[0])/1000000.0, min[i]/1000000.0, max[i]/1000000.0))
|
||||
|
||||
# store each AEP channel info except Platform in the results table
|
||||
results_table[channel_name] = total_nrj, average_power
|
||||
|
||||
if (min[i] < offset[i]):
|
||||
self.fs.write ("!!! Min below offset\n")
|
||||
|
||||
if duplicate[i]:
|
||||
continue
|
||||
|
||||
total += nrj[i]
|
||||
|
||||
self.fs.write ("Total nrj: %8.3f J for %s -- duration %8.3f sec\n" % (total/1000000000000.0, "Platform ", (max[0]-min[0])/1000000.0))
|
||||
|
||||
total_nrj = total/1000000000000.0
|
||||
duration = (max[0]-min[0])/1000000.0
|
||||
average_power = total_nrj/duration
|
||||
|
||||
# store AEP Platform channel info in the results table
|
||||
results_table["Platform"] = total_nrj, average_power
|
||||
|
||||
return results_table
|
||||
|
||||
def topology_from_config(self, topofile):
|
||||
try:
|
||||
ft = open(topofile, "r")
|
||||
except IOError:
|
||||
logger.warn('Unable to open config file {}'.format(topofile))
|
||||
return
|
||||
lines = ft.readlines()
|
||||
|
||||
topo = {}
|
||||
virtual = {}
|
||||
name = ""
|
||||
offset = 0
|
||||
index = 0
|
||||
#parse config file
|
||||
for myline in lines:
|
||||
if myline.startswith("#"):
|
||||
# skip comment
|
||||
continue
|
||||
|
||||
if myline == "\n":
|
||||
# skip empty line
|
||||
continue
|
||||
|
||||
if name == "":
|
||||
# 1st valid line is the config's name
|
||||
name = myline
|
||||
continue
|
||||
|
||||
if not myline.startswith((' ', '\t')):
|
||||
# new device path
|
||||
offset = index
|
||||
continue
|
||||
|
||||
# Get parameters of channel configuration
|
||||
items = myline.split()
|
||||
|
||||
info = {}
|
||||
info['name'] = items[0]
|
||||
info['parent'] = items[9]
|
||||
info['pretty'] = items[8]
|
||||
info['index'] = int(items[2])+offset
|
||||
|
||||
# Add channel
|
||||
topo[items[0]] = info
|
||||
|
||||
# Increase index
|
||||
index +=1
|
||||
|
||||
|
||||
# Create an entry for each virtual parent
|
||||
for supply in topo.iterkeys():
|
||||
# Parent is in the topology
|
||||
parent = topo[supply]['parent']
|
||||
if parent in topo:
|
||||
continue
|
||||
|
||||
if parent not in virtual:
|
||||
virtual[parent] = { supply : topo[supply]['index'] }
|
||||
|
||||
virtual[parent][supply] = topo[supply]['index']
|
||||
|
||||
|
||||
# Remove parent with 1 child as they don't give more information than their
|
||||
# child
|
||||
for supply in virtual.keys():
|
||||
if len(virtual[supply]) == 1:
|
||||
del virtual[supply];
|
||||
|
||||
topo_list = ['']*(1+len(topo)+len(virtual))
|
||||
topo_list[0] = 'time'
|
||||
for chnl in topo.iterkeys():
|
||||
topo_list[topo[chnl]['index']] = chnl
|
||||
for chnl in virtual.iterkeys():
|
||||
index +=1
|
||||
topo_list[index] = chnl
|
||||
|
||||
ft.close()
|
||||
|
||||
return topo_list
|
||||
|
||||
def __del__(self):
|
||||
self.unprepare()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
def handleSigTERM(signum, frame):
|
||||
sys.exit(2)
|
||||
|
||||
signal.signal(signal.SIGTERM, handleSigTERM)
|
||||
signal.signal(signal.SIGINT, handleSigTERM)
|
||||
|
||||
logger.setLevel(logging.WARN)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(logging.DEBUG)
|
||||
logger.addHandler(ch)
|
||||
|
||||
infile = ""
|
||||
outfile = ""
|
||||
figurefile = ""
|
||||
start = 0
|
||||
lenght = -1
|
||||
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "i:vo:s:l:t:")
|
||||
except getopt.GetoptError as err:
|
||||
print str(err) # will print something like "option -a not recognized"
|
||||
sys.exit(2)
|
||||
|
||||
for o, a in opts:
|
||||
if o == "-i":
|
||||
infile = a
|
||||
if o == "-v":
|
||||
logger.setLevel(logging.DEBUG)
|
||||
if o == "-o":
|
||||
parse = True
|
||||
outfile = a
|
||||
if o == "-s":
|
||||
start = int(float(a)*1000000)
|
||||
if o == "-l":
|
||||
lenght = int(float(a)*1000000)
|
||||
if o == "-t":
|
||||
topofile = a
|
||||
parser = AepParser()
|
||||
print parser.topology_from_config(topofile)
|
||||
exit(0)
|
||||
|
||||
parser = AepParser()
|
||||
parser.prepare(infile, outfile, figurefile)
|
||||
parser.parse_aep(start, lenght)
|
Reference in New Issue
Block a user