1
0
mirror of https://github.com/ARM-software/devlib.git synced 2025-09-04 11:01:53 +01:00

Merge pull request #178 from setrofim/master

Various fixes.
This commit is contained in:
setrofim
2017-09-27 14:00:26 +01:00
committed by GitHub
15 changed files with 317 additions and 97 deletions

View File

@@ -19,8 +19,8 @@ from devlib.instrument.monsoon import MonsoonInstrument
from devlib.instrument.netstats import NetstatsInstrument
from devlib.instrument.gem5power import Gem5PowerInstrument
from devlib.derived import DerivedMeasurements
from devlib.derived.derived_measurements import DerivedEnergyMeasurements
from devlib.derived import DerivedMeasurements, DerivedMetric
from devlib.derived.energy import DerivedEnergyMeasurements
from devlib.trace.ftrace import FtraceCollector

View File

@@ -12,8 +12,49 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
from devlib.instrument import MeasurementType, MEASUREMENT_TYPES
class DerivedMetric(object):
__slots__ = ['name', 'value', 'measurement_type']
@property
def units(self):
return self.measurement_type.units
def __init__(self, name, value, measurement_type):
self.name = name
self.value = value
if isinstance(measurement_type, MeasurementType):
self.measurement_type = measurement_type
else:
try:
self.measurement_type = MEASUREMENT_TYPES[measurement_type]
except KeyError:
msg = 'Unknown measurement type: {}'
raise ValueError(msg.format(measurement_type))
def __cmp__(self, other):
if hasattr(other, 'value'):
return cmp(self.value, other.value)
else:
return cmp(self.value, other)
def __str__(self):
if self.units:
return '{}: {} {}'.format(self.name, self.value, self.units)
else:
return '{}: {}'.format(self.name, self.value)
__repr__ = __str__
class DerivedMeasurements(object):
@staticmethod
def process(measurements_csv):
raise NotImplementedError()
def process(self, measurements_csv):
return []
def process_raw(self, *args):
return []

View File

@@ -15,8 +15,8 @@
from __future__ import division
from collections import defaultdict
from devlib import DerivedMeasurements
from devlib.instrument import Measurement, MEASUREMENT_TYPES, InstrumentChannel
from devlib import DerivedMeasurements, DerivedMetric
from devlib.instrument import MEASUREMENT_TYPES, InstrumentChannel
class DerivedEnergyMeasurements(DerivedMeasurements):
@@ -56,7 +56,7 @@ class DerivedEnergyMeasurements(DerivedMeasurements):
power_results = defaultdict(float)
# Process data
for count, row in enumerate(measurements_csv.itermeasurements()):
for count, row in enumerate(measurements_csv.iter_measurements()):
if use_timestamp:
last_ts = row_ts
row_ts = time_measurment.convert(float(row[ts_index].value), 'time')
@@ -86,12 +86,12 @@ class DerivedEnergyMeasurements(DerivedMeasurements):
derived_measurements = []
for site in energy_results:
total_energy = energy_results[site]['end'] - energy_results[site]['start']
instChannel = InstrumentChannel('cum_energy', site, MEASUREMENT_TYPES['energy'])
derived_measurements.append(Measurement(total_energy, instChannel))
name = '{}_total_energy'.format(site)
derived_measurements.append(DerivedMetric(name, total_energy, MEASUREMENT_TYPES['energy']))
for site in power_results:
power = power_results[site] / (count + 1) #pylint: disable=undefined-loop-variable
instChannel = InstrumentChannel('avg_power', site, MEASUREMENT_TYPES['power'])
derived_measurements.append(Measurement(power, instChannel))
name = '{}_average_power'.format(site)
derived_measurements.append(DerivedMetric(name, power, MEASUREMENT_TYPES['power']))
return derived_measurements

View File

@@ -72,38 +72,60 @@ class MeasurementType(object):
return text.format(self.name, self.units)
# Standard measures
# Standard measures. In order to make sure that downstream data processing is not tied
# to particular insturments (e.g. a particular method of mearuing power), instruments
# must, where possible, resport their measurments formatted as on of the standard types
# defined here.
_measurement_types = [
# For whatever reason, the type of measurement could not be established.
MeasurementType('unknown', None),
MeasurementType('time', 'seconds',
# Generic measurements
MeasurementType('count', 'count'),
MeasurementType('percent', 'percent'),
# Time measurement. While there is typically a single "canonical" unit
# used for each type of measurmenent, time may be measured to a wide variety
# of events occuring at a wide range of scales. Forcing everying into a
# single scale will lead to inefficient and awkward to work with result tables.
# Coversion functions between the formats are specified, so that downstream
# processors that expect all times time be at a particular scale can automatically
# covert without being familar with individual instruments.
MeasurementType('time', 'seconds', 'time',
conversions={
'time_us': lambda x: x * 1000000,
'time_ms': lambda x: x * 1000,
}
),
MeasurementType('time_us', 'microseconds',
MeasurementType('time_us', 'microseconds', 'time',
conversions={
'time': lambda x: x / 1000000,
'time_ms': lambda x: x / 1000,
}
),
MeasurementType('time_ms', 'milliseconds',
MeasurementType('time_ms', 'milliseconds', 'time',
conversions={
'time': lambda x: x / 1000,
'time_us': lambda x: x * 1000,
}
),
MeasurementType('temperature', 'degrees'),
# Measurements related to thermals.
MeasurementType('temperature', 'degrees', 'thermal'),
# Measurements related to power end energy consumption.
MeasurementType('power', 'watts', 'power/energy'),
MeasurementType('voltage', 'volts', 'power/energy'),
MeasurementType('current', 'amps', 'power/energy'),
MeasurementType('energy', 'joules', 'power/energy'),
# Measurments realted to data transfer, e.g. neworking,
# memory, or backing storage.
MeasurementType('tx', 'bytes', 'data transfer'),
MeasurementType('rx', 'bytes', 'data transfer'),
MeasurementType('tx/rx', 'bytes', 'data transfer'),
MeasurementType('fps', 'fps', 'ui render'),
MeasurementType('frames', 'frames', 'ui render'),
]
for m in _measurement_types:
@@ -127,7 +149,7 @@ class Measurement(object):
self.channel = channel
def __cmp__(self, other):
if isinstance(other, Measurement):
if hasattr(other, 'value'):
return cmp(self.value, other.value)
else:
return cmp(self.value, other)
@@ -147,26 +169,32 @@ class MeasurementsCsv(object):
self.path = path
self.channels = channels
self.sample_rate_hz = sample_rate_hz
self._fh = open(path, 'rb')
if self.channels is None:
self._load_channels()
headings = [chan.label for chan in self.channels]
self.data_tuple = collections.namedtuple('csv_entry', headings)
def measurements(self):
return list(self.itermeasurements())
return list(self.iter_measurements())
def itermeasurements(self):
self._fh.seek(0)
reader = csv.reader(self._fh)
reader.next() # headings
for row in reader:
def iter_measurements(self):
for row in self._iter_rows():
values = map(numeric, row)
yield [Measurement(v, c) for (v, c) in zip(values, self.channels)]
def values(self):
return list(self.iter_values())
def iter_values(self):
for row in self._iter_rows():
values = map(numeric, row)
yield self.data_tuple(*values)
def _load_channels(self):
self._fh.seek(0)
reader = csv.reader(self._fh)
header = reader.next()
self._fh.seek(0)
header = []
with open(self.path, 'rb') as fh:
reader = csv.reader(fh)
header = reader.next()
self.channels = []
for entry in header:
@@ -175,22 +203,35 @@ class MeasurementsCsv(object):
if entry.endswith(suffix):
site = entry[:-len(suffix)]
measure = mt
name = '{}_{}'.format(site, measure)
break
else:
site = entry
measure = 'unknown'
name = entry
if entry in MEASUREMENT_TYPES:
site = None
measure = entry
else:
site = entry
measure = 'unknown'
chan = InstrumentChannel(name, site, measure)
chan = InstrumentChannel(site, measure)
self.channels.append(chan)
def _iter_rows(self):
with open(self.path, 'rb') as fh:
reader = csv.reader(fh)
reader.next() # headings
for row in reader:
yield row
class InstrumentChannel(object):
@property
def label(self):
return '{}_{}'.format(self.site, self.kind)
if self.site is not None:
return '{}_{}'.format(self.site, self.kind)
return self.kind
name = label
@property
def kind(self):
@@ -200,8 +241,7 @@ class InstrumentChannel(object):
def units(self):
return self.measurement_type.units
def __init__(self, name, site, measurement_type, **attrs):
self.name = name
def __init__(self, site, measurement_type, **attrs):
self.site = site
if isinstance(measurement_type, MeasurementType):
self.measurement_type = measurement_type
@@ -243,10 +283,8 @@ class Instrument(object):
measure = measure.name
return [c for c in self.list_channels() if c.kind == measure]
def add_channel(self, site, measure, name=None, **attrs):
if name is None:
name = '{}_{}'.format(site, measure)
chan = InstrumentChannel(name, site, measure, **attrs)
def add_channel(self, site, measure, **attrs):
chan = InstrumentChannel(site, measure, **attrs)
self.channels[chan.label] = chan
# initialization and teardown
@@ -297,3 +335,6 @@ class Instrument(object):
def get_data(self, outfile):
pass
def get_raw(self):
return []

View File

@@ -121,3 +121,6 @@ class AcmeCapeInstrument(Instrument):
output_row.append(float(row[i])/1000)
writer.writerow(output_row)
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
def get_raw(self):
return [self.raw_data_file]

View File

@@ -33,6 +33,7 @@ class DaqInstrument(Instrument):
# pylint: disable=no-member
super(DaqInstrument, self).__init__(target)
self._need_reset = True
self._raw_files = []
if execute_command is None:
raise HostError('Could not import "daqpower": {}'.format(import_error_mesg))
if labels is None:
@@ -68,6 +69,7 @@ class DaqInstrument(Instrument):
if not result.status == Status.OK: # pylint: disable=no-member
raise HostError(result.message)
self._need_reset = False
self._raw_files = []
def start(self):
if self._need_reset:
@@ -86,6 +88,7 @@ class DaqInstrument(Instrument):
site = os.path.splitext(entry)[0]
path = os.path.join(tempdir, entry)
raw_file_map[site] = path
self._raw_files.append(path)
active_sites = unique([c.site for c in self.active_channels])
file_handles = []
@@ -131,6 +134,9 @@ class DaqInstrument(Instrument):
for fh in file_handles:
fh.close()
def get_raw(self):
return self._raw_files
def teardown(self):
self.execute('close')

View File

@@ -52,6 +52,7 @@ class EnergyProbeInstrument(Instrument):
self.raw_output_directory = None
self.process = None
self.sample_rate_hz = 10000 # Determined empirically
self.raw_data_file = None
for label in self.labels:
for kind in self.attributes:
@@ -64,6 +65,7 @@ class EnergyProbeInstrument(Instrument):
for i, rval in enumerate(self.resistor_values)]
rstring = ''.join(parts)
self.command = '{} -d {} -l {} {}'.format(self.caiman, self.device_entry, rstring, self.raw_output_directory)
self.raw_data_file = None
def start(self):
self.logger.debug(self.command)
@@ -92,10 +94,10 @@ class EnergyProbeInstrument(Instrument):
num_of_ports = len(self.resistor_values)
struct_format = '{}I'.format(num_of_ports * self.attributes_per_sample)
not_a_full_row_seen = False
raw_data_file = os.path.join(self.raw_output_directory, '0000000000')
self.raw_data_file = os.path.join(self.raw_output_directory, '0000000000')
self.logger.debug('Parsing raw data file: {}'.format(raw_data_file))
with open(raw_data_file, 'rb') as bfile:
self.logger.debug('Parsing raw data file: {}'.format(self.raw_data_file))
with open(self.raw_data_file, 'rb') as bfile:
with open(outfile, 'wb') as wfh:
writer = csv.writer(wfh)
writer.writerow(active_channels)
@@ -114,3 +116,6 @@ class EnergyProbeInstrument(Instrument):
else:
not_a_full_row_seen = True
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
def get_raw(self):
return [self.raw_data_file]

View File

@@ -20,6 +20,7 @@ class FramesInstrument(Instrument):
self.collector = None
self.header = None
self._need_reset = True
self._raw_file = None
self._init_channels()
def reset(self, sites=None, kinds=None, channels=None):
@@ -27,6 +28,7 @@ class FramesInstrument(Instrument):
self.collector = self.collector_cls(self.target, self.period,
self.collector_target, self.header)
self._need_reset = False
self._raw_file = None
def start(self):
if self._need_reset:
@@ -38,14 +40,16 @@ class FramesInstrument(Instrument):
self._need_reset = True
def get_data(self, outfile):
raw_outfile = None
if self.keep_raw:
raw_outfile = outfile + '.raw'
self.collector.process_frames(raw_outfile)
self._raw_file = outfile + '.raw'
self.collector.process_frames(self._raw_file)
active_sites = [chan.label for chan in self.active_channels]
self.collector.write_frames(outfile, columns=active_sites)
return MeasurementsCsv(outfile, self.active_channels, self.sample_rate_hz)
def get_raw(self):
return [self._raw_file] if self._raw_file else []
def _init_channels(self):
raise NotImplementedError()

View File

@@ -45,7 +45,7 @@ class HwmonInstrument(Instrument):
measure = self.measure_map.get(ts.kind)[0]
if measure:
self.logger.debug('\tAdding sensor {}'.format(ts.name))
self.add_channel(_guess_site(ts), measure, name=ts.name, sensor=ts)
self.add_channel(_guess_site(ts), measure, sensor=ts)
else:
self.logger.debug('\tSkipping sensor {} (unknown kind "{}")'.format(ts.name, ts.kind))
except ValueError:

View File

@@ -210,22 +210,22 @@ class JunoEnergyInstrument(Instrument):
mode = CONTINUOUS | INSTANTANEOUS
_channels = [
InstrumentChannel('sys_curr', 'sys', 'current'),
InstrumentChannel('a57_curr', 'a57', 'current'),
InstrumentChannel('a53_curr', 'a53', 'current'),
InstrumentChannel('gpu_curr', 'gpu', 'current'),
InstrumentChannel('sys_volt', 'sys', 'voltage'),
InstrumentChannel('a57_volt', 'a57', 'voltage'),
InstrumentChannel('a53_volt', 'a53', 'voltage'),
InstrumentChannel('gpu_volt', 'gpu', 'voltage'),
InstrumentChannel('sys_pow', 'sys', 'power'),
InstrumentChannel('a57_pow', 'a57', 'power'),
InstrumentChannel('a53_pow', 'a53', 'power'),
InstrumentChannel('gpu_pow', 'gpu', 'power'),
InstrumentChannel('sys_cenr', 'sys', 'energy'),
InstrumentChannel('a57_cenr', 'a57', 'energy'),
InstrumentChannel('a53_cenr', 'a53', 'energy'),
InstrumentChannel('gpu_cenr', 'gpu', 'energy'),
InstrumentChannel('sys', 'current'),
InstrumentChannel('a57', 'current'),
InstrumentChannel('a53', 'current'),
InstrumentChannel('gpu', 'current'),
InstrumentChannel('sys', 'voltage'),
InstrumentChannel('a57', 'voltage'),
InstrumentChannel('a53', 'voltage'),
InstrumentChannel('gpu', 'voltage'),
InstrumentChannel('sys', 'power'),
InstrumentChannel('a57', 'power'),
InstrumentChannel('a53', 'power'),
InstrumentChannel('gpu', 'power'),
InstrumentChannel('sys', 'energy'),
InstrumentChannel('a57', 'energy'),
InstrumentChannel('a53', 'energy'),
InstrumentChannel('gpu', 'energy'),
]
def __init__(self, target):

View File

@@ -1011,11 +1011,12 @@ class AndroidTarget(Target):
self.uninstall_executable(name)
def get_pids_of(self, process_name):
result = self.execute('ps {}'.format(process_name[-15:]), check_exit_code=False).strip()
if result and 'not found' not in result:
return [int(x.split()[1]) for x in result.split('\n')[1:]]
else:
return []
result = []
search_term = process_name[-15:]
for entry in self.ps():
if search_term in entry.name:
result.append(entry.pid)
return result
def ps(self, **kwargs):
lines = iter(convert_new_lines(self.execute('ps')).split('\n'))
@@ -1023,8 +1024,12 @@ class AndroidTarget(Target):
result = []
for line in lines:
parts = line.split(None, 8)
if parts:
result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
if not parts:
continue
if len(parts) == 8:
# wchan was blank; insert an empty field where it should be.
parts.insert(5, '')
result.append(PsEntry(*(parts[0:1] + map(int, parts[1:5]) + parts[5:])))
if not kwargs:
return result
else:

View File

@@ -103,6 +103,9 @@ CPU_PART_MAP = {
0x211: {0x1: 'KryoGold'},
0x800: {None: 'Falkor'},
},
0x53: { # Samsung LSI
0x001: {0x1: 'MongooseM1'},
},
0x56: { # Marvell
0x131: {
0x2: 'Feroceon 88F6281',

View File

@@ -83,9 +83,14 @@ class FrameCollector(threading.Thread):
header = self.header
frames = self.frames
else:
header = [c for c in self.header if c in columns]
indexes = [self.header.index(c) for c in header]
indexes = []
for c in columns:
if c not in self.header:
msg = 'Invalid column "{}"; must be in {}'
raise ValueError(msg.format(c, self.header))
indexes.append(self.header.index(c))
frames = [[f[i] for i in indexes] for f in self.frames]
header = columns
with open(outfile, 'w') as wfh:
writer = csv.writer(wfh)
if header:
@@ -122,7 +127,8 @@ class SurfaceFlingerFrameCollector(FrameCollector):
return self.target.execute(cmd.format(activity))
def list(self):
return self.target.execute('dumpsys SurfaceFlinger --list').split('\r\n')
text = self.target.execute('dumpsys SurfaceFlinger --list')
return text.replace('\r\n', '\n').replace('\r', '\n').split('\n')
def _process_raw_file(self, fh):
text = fh.read().replace('\r\n', '\n').replace('\r', '\n')
@@ -203,3 +209,43 @@ class GfxinfoFrameCollector(FrameCollector):
if not found:
logger.warning('Could not find frames data in gfxinfo output')
return
def _file_reverse_iter(fh, buf_size=1024):
fh.seek(0, os.SEEK_END)
offset = 0
file_size = remaining_size = fh.tell()
while remaining_size > 0:
offset = min(file_size, offset + buf_size)
fh.seek(file_size - offset)
buf = fh.read(min(remaining_size, buf_size))
remaining_size -= buf_size
yield buf
def gfxinfo_get_last_dump(filepath):
"""
Return the last gfxinfo dump from the frame collector's raw output.
"""
record = ''
with open(filepath, 'r') as fh:
fh_iter = _file_reverse_iter(fh)
try:
while True:
buf = fh_iter.next()
ix = buf.find('** Graphics')
if ix >= 0:
return buf[ix:] + record
ix = buf.find(' **\n')
if ix >= 0:
buf = fh_iter.next() + buf
ix = buf.find('** Graphics')
if ix < 0:
msg = '"{}" appears to be corrupted'
raise RuntimeError(msg.format(filepath))
return buf[ix:] + record
record = buf + record
except StopIteration:
pass