aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVincent Guittot <vincent.guittot@linaro.org>2016-06-16 09:51:42 +0200
committerVincent Guittot <vincent.guittot@linaro.org>2017-09-05 18:35:43 +0200
commitdc18239d294396a8c8282d545e17cdebe890b52e (patch)
tree4c9f74ddc256b659a07b141674b20ed668a46d4f
parent82648946febcaf9b0dc2ba828be68f1d0bf0a2e2 (diff)
wa: add arm-probe instrument
Add an instrument that uses arm-probe command line to record dynamic power consumption trace of the use case with AEP. Then, the instrument uses the header of the trace file to evaluate power topology and compute the power consumption of platform and virtual intermediate power domains. As an example, it will not add the 2 channels of the dragonboard when computating the energy consummed by the platform because the VDD_SOC is a child on DCIN and as a result it is already accounted in DCIN It can also append 2 channels to create a new virtual one like for the mtk evb which uses 2 power rails to supply a cluster power domain so we have to add the 2 channels to get the power consumption of the cluster Signed-off-by: Vincent Guittot <vincent.guittot@linaro.org>
-rw-r--r--wlauto/instrumentation/energy_probe_ext/__init__.py93
-rwxr-xr-xwlauto/instrumentation/energy_probe_ext/parse_aep.py376
2 files changed, 469 insertions, 0 deletions
diff --git a/wlauto/instrumentation/energy_probe_ext/__init__.py b/wlauto/instrumentation/energy_probe_ext/__init__.py
new file mode 100644
index 00000000..ba5efbef
--- /dev/null
+++ b/wlauto/instrumentation/energy_probe_ext/__init__.py
@@ -0,0 +1,93 @@
+# Copyright 2013-2015 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,access-member-before-definition,attribute-defined-outside-init
+import os
+import subprocess
+import signal
+import struct
+import csv
+import sys
+try:
+ import pandas
+except ImportError:
+ pandas = None
+
+sys.path.append(os.path.dirname(sys.modules[__name__].__file__))
+from parse_aep import AEP_parser
+
+from wlauto import Instrument, Parameter, Executable
+from wlauto.exceptions import InstrumentError, ConfigError
+from wlauto.utils.types import list_of_numbers
+
+
+class ArmEnergyProbe(Instrument):
+
+ name = 'energy_probe_ext'
+ description = """Collects power traces using the ARM Energy Probe.
+
+ This instrument requires ``arm-probe`` utility to be installed in the workload automation
+ host and be in the PATH. arm-probe is available here ``https://git.linaro.org/tools/arm-probe.git`` .
+ ARM energy probe device can simultaneously collect power from up to 3 power rails and
+ arm-probe utility can record data from several devices simultaneously.
+
+ To connect the energy probe on a rail, connect the white wire to the pin that is closer to the
+ Voltage source and the black wire to the pin that is closer to the load (the SoC or the device
+ you are probing). Between the pins there should be a shunt resistor of known resistance in the
+ range of 5 to 500 mOhm but the voltage on the shunt resistor must stay smaller than 165mV.
+ The resistance of the shunt resistors is a mandatory parameter to be set in the ``config`` file.
+ """
+
+ parameters = [
+ Parameter('config', kind=str, default='./config',
+ description="""config file path"""),
+ ]
+
+ MAX_CHANNELS = 12 # 4 Arm Energy Probes
+
+ def __init__(self, device, **kwargs):
+ super(ArmEnergyProbe, self).__init__(device, **kwargs)
+
+ def validate(self):
+ if subprocess.call('which arm-probe', stdout=subprocess.PIPE, shell=True):
+ raise InstrumentError('arm-probe not in PATH. Cannot enable energy probe ext instrument')
+ if not self.config:
+ raise ConfigError('a valid config file must be set')
+
+ def setup(self, context):
+ self.output_directory = os.path.join(context.output_directory, 'energy_probe')
+ self.output_file_raw = os.path.join(self.output_directory, 'data_raw')
+ self.output_file = os.path.join(self.output_directory, 'data')
+ self.output_file_figure = os.path.join(self.output_directory, 'summary.txt')
+ self.command = 'arm-probe --config {} > {}'.format(self.config, self.output_file_raw)
+ os.makedirs(self.output_directory)
+
+ def fast_start(self, context):
+ self.logger.debug(self.command)
+ self.armprobe = subprocess.Popen(self.command,
+ stderr=subprocess.PIPE,
+ preexec_fn=os.setpgrp,
+ shell=True)
+
+ def fast_stop(self, context):
+ self.logger.debug("kill running arm-probe")
+ os.killpg(self.armprobe.pid, signal.SIGTERM)
+
+ def update_result(self, context): # pylint: disable=too-many-locals
+ self.logger.debug("Parse data and compute consumed energy")
+ self.parser = AEP_parser(self.output_file_raw, self.output_file, self.output_file_figure)
+ self.parser.parse_AEP()
+
diff --git a/wlauto/instrumentation/energy_probe_ext/parse_aep.py b/wlauto/instrumentation/energy_probe_ext/parse_aep.py
new file mode 100755
index 00000000..cc5e1821
--- /dev/null
+++ b/wlauto/instrumentation/energy_probe_ext/parse_aep.py
@@ -0,0 +1,376 @@
+#!/usr/bin/env python
+# Copyright 2013-2015 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 os
+import sys
+import getopt
+import subprocess
+import signal
+import serial
+import time
+import math
+
+class AEP_parser:
+
+ def __init__(self, infile, outfile, summaryfile, verbose = False):
+ self.verbose = verbose
+
+ try:
+ self.fi = open(infile, "r")
+ except IOError:
+ print "WARN: Unable to open input file %s" % (infile)
+ print "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:
+ print "WARN: Unable to create %s" % (outfile)
+ self.parse = False
+ else:
+ self.parse = False
+
+ self.summary = True
+ if len(summaryfile) > 0:
+ try:
+ self.fs = open(summaryfile, "w")
+ except IOError:
+ print "WARN: Unable to create %s" % (summaryfile)
+ self.fs = sys.stdout
+ else:
+ self.fs = sys.stdout
+
+ def __del__(self):
+ self.fi.close()
+
+ if self.parse:
+ self.fo.close()
+
+ @staticmethod
+ def create_topology(array, topo):
+ # Extract topology information for the data file
+ # The header of a data file looks like this:
+# 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]
+ # save 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=[], virtual=[]):
+ # 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):
+ # write not hidden colums
+ for i in range(len(array)):
+ if hide[i]:
+ continue
+
+ self.fo.write(str(array[i]) + " ")
+
+ self.fo.write("\n")
+
+ def parse_AEP(self):
+ # Parse aep data and calculate the energy consumed
+ label_line = 1
+
+ topo = {}
+
+ lines = self.fi.readlines()
+
+ for myline in lines:
+ array = myline.split()
+
+ if "#" in myline:
+ # update power topology
+ topo = self.create_topology(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)
+
+ if self.verbose:
+ print "Topology :"
+ print topo
+ print "Virtual power domain :"
+ print virtual
+ print "Duplicated power domain :"
+ print duplicate
+ print "Name of columns :"
+ print label
+ print "Hidden columns :"
+ print hide
+ print "Unit of columns :"
+ print unit
+ # Init arrays
+ nrj = [0]*len(label)
+ min = [0]*len(label)
+ max = [500]*len(label)
+ offset = [0]*len(label)
+
+ continue
+
+ # convert text to int and unit to micro-unit
+ data = self.parse_text(array, hide)
+
+ # 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)
+
+ # display energy consumption of each channel and total energy consumption
+ total = 0
+ for i in range(1, len(nrj)):
+ if hide[i]:
+ continue
+
+ nrj[i] -= offset[i] * nrj[0]
+
+ 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))
+ 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))
+
+
+if __name__ == '__main__':
+
+ def handleSigTERM(signum, frame):
+ sys.exit(2)
+
+ signal.signal(signal.SIGTERM, handleSigTERM)
+ signal.signal(signal.SIGINT, handleSigTERM)
+
+ infile = ""
+ verbose = False
+ outfile = ""
+ figurefile = ""
+
+ try:
+ opts, args = getopt.getopt(sys.argv[1:], "i:vo:")
+ 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":
+ verbose = True
+ if o == "-o":
+ parse = True
+ outfile = a
+
+ parser = AEP_parser(infile, outfile, figurefile, verbose, )
+ parser.parse_AEP()
+