# 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 argparse
import fnmatch as fnm
import json
import math
import numpy as np
import os
import re
import sys
import logging
from collections import defaultdict
from colors import TestColors
class Results(object):
def __init__(self, results_dir):
self.results_dir = results_dir
self.results_json = results_dir + '/results.json'
self.results = {}
# Setup logging
self._log = logging.getLogger('Results')
# Do nothing if results have been already parsed
if os.path.isfile(self.results_json):
return
# Parse results
self.base_wls = defaultdict(list)
self.test_wls = defaultdict(list)
self._log.info('Loading energy/perf data...')
for test_idx in sorted(os.listdir(self.results_dir)):
test_dir = self.results_dir + '/' + test_idx
if not os.path.isdir(test_dir):
continue
test = TestFactory.get(test_idx, test_dir, self.results)
test.parse()
results_json = self.results_dir + '/results.json'
self._log.info('Dump perf results on JSON file [%s]...',
results_json)
with open(results_json, 'w') as outfile:
json.dump(self.results, outfile, indent=4, sort_keys=True)
################################################################################
# Tests processing base classes
################################################################################
class Test(object):
def __init__(self, test_idx, test_dir, res):
self.test_idx = test_idx
self.test_dir = test_dir
self.res = res
match = TEST_DIR_RE.search(test_dir)
if not match:
self._log.error('Results folder not matching naming template')
self._log.error('Skip parsing of test results [%s]', test_dir)
return
# Create required JSON entries
wtype = match.group(1)
if wtype not in res.keys():
res[wtype] = {}
wload_idx = match.group(3)
if wload_idx not in res[wtype].keys():
res[wtype][wload_idx] = {}
conf_idx = match.group(2)
if conf_idx not in res[wtype][wload_idx].keys():
res[wtype][wload_idx][conf_idx] = {}
# Set the workload type for this test
self.wtype = wtype
self.wload_idx = wload_idx
self.conf_idx = conf_idx
# Energy metrics collected for all tests
self.little = []
self.total = []
self.big = []
def parse(self):
self._log.info('Processing results from wtype [%s]', self.wtype)
# Parse test's run results
for run_idx in sorted(os.listdir(self.test_dir)):
# Skip all files which are not folders
run_dir = os.path.join(self.test_dir, run_idx)
if not os.path.isdir(run_dir):
continue
run = self.parse_run(run_idx, run_dir)
self.collect_energy(run)
self.collect_performance(run)
# Report energy/performance stats over all runs
self.res[self.wtype][self.wload_idx][self.conf_idx]\
['energy'] = self.energy()
self.res[self.wtype][self.wload_idx][self.conf_idx]\
['performance'] = self.performance()
def collect_energy(self, run):
# Keep track of average energy of each run
self.little.append(run.little_nrg)
self.total.append(run.total_nrg)
self.big.append(run.big_nrg)
def energy(self):
# Compute energy stats over all run
return {
'LITTLE' : Stats(self.little).get(),
'big' : Stats(self.big).get(),
'Total' : Stats(self.total).get()
}
class TestFactory(object):
@staticmethod
def get(test_idx, test_dir, res):
# Retrive workload class from results folder name
match = TEST_DIR_RE.search(test_dir)
if not match:
self._log.error('Results folder not matching naming template')
self._log.error('Skip parsing of test results [%s]', test_dir)
return
# Create workload specifi test class
wtype = match.group(1)
if wtype == 'rtapp':
return RTAppTest(test_idx, test_dir, res)
# Return a generi test parser
return DefaultTest(test_idx, test_dir, res)
class Energy(object):
def __init__(self, nrg_file):
# Set of exposed attributes
self.little = 0.0
self.big = 0.0
self.total = 0.0
self._log.debug('Parse [%s]...', nrg_file)
with open(nrg_file, 'r') as infile:
nrg = json.load(infile)
if 'LITTLE' in nrg:
self.little = float(nrg['LITTLE'])
if 'big' in nrg:
self.big = float(nrg['big'])
self.total = self.little + self.big
self._log.debug('Energy LITTLE [%s], big [%s], Total [%s]',
self.little, self.big, self.total)
class Stats(object):
def __init__(self, data):
self.stats = {}
self.stats['count'] = len(data)
self.stats['min'] = min(data)
self.stats['max'] = max(data)
self.stats['avg'] = sum(data)/len(data)
std = Stats.stdev(data)
c99 = Stats.ci99(data, std)
self.stats['std'] = std
self.stats['c99'] = c99
def get(self):
return self.stats
@staticmethod
def stdev(values):
sum1 = 0
sum2 = 0
for value in values:
sum1 += value
sum2 += math.pow(value, 2)
# print 'sum1: {}, sum2: {}'.format(sum1, sum2)
avg = sum1 / len(values)
var = (sum2 / len(values)) - (avg * avg)
# print 'avg: {} var: {}'.format(avg, var)
std = math.sqrt(var)
return float(std)
@staticmethod
def ci99(values, std):
count = len(values)
ste = std / math.sqrt(count)
c99 = 2.58 * ste
return c99
################################################################################
# Run processing base classes
################################################################################
class Run(object):
def __init__(self, run_idx, run_dir):
self.run_idx = run_idx
self.nrg = None
self._log.debug('Parse [%s]...', 'Run', run_dir)
# Energy stats
self.little_nrg = 0
self.total_nrg = 0
self.big_nrg = 0
nrg_file = run_dir + '/energy.json'
if os.path.isfile(nrg_file):
self.nrg = Energy(nrg_file)
self.little_nrg = self.nrg.little
self.total_nrg = self.nrg.total
self.big_nrg = self.nrg.big
################################################################################
# RTApp workload parsing classes
################################################################################
class RTAppTest(Test):
def __init__(self, test_idx, test_dir, res):
super(RTAppTest, self).__init__(test_idx, test_dir, res)
# RTApp specific performance metric
self.slack_pct = []
self.perf_avg = []
self.edp1 = []
self.edp2 = []
self.edp3 = []
self.rtapp_run = {}
def parse_run(self, run_idx, run_dir):
return RTAppRun(run_idx, run_dir)
def collect_performance(self, run):
# Keep track of average performances of each run
self.slack_pct.extend(run.slack_pct)
self.perf_avg.extend(run.perf_avg)
self.edp1.extend(run.edp1)
self.edp2.extend(run.edp2)
self.edp3.extend(run.edp3)
# Keep track of performance stats for each run
self.rtapp_run[run.run_idx] = {
'slack_pct' : Stats(run.slack_pct).get(),
'perf_avg' : Stats(run.perf_avg).get(),
'edp1' : Stats(run.edp1).get(),
'edp2' : Stats(run.edp2).get(),
'edp3' : Stats(run.edp3).get(),
}
def performance(self):
# Dump per run rtapp stats
prf_file = os.path.join(self.test_dir, 'performance.json')
with open(prf_file, 'w') as ofile:
json.dump(self.rtapp_run, ofile, indent=4, sort_keys=True)
# Return oveall stats
return {
'slack_pct' : Stats(self.slack_pct).get(),
'perf_avg' : Stats(self.perf_avg).get(),
'edp1' : Stats(self.edp1).get(),
'edp2' : Stats(self.edp2).get(),
'edp3' : Stats(self.edp3).get(),
}
class RTAppRun(Run):
def __init__(self, run_idx, run_dir):
# Call base class to parse energy data
super(RTAppRun, self).__init__(run_idx, run_dir)
# RTApp specific performance stats
self.slack_pct = []
self.perf_avg = []
self.edp1 = []
self.edp2 = []
self.edp3 = []
rta = {}
# Load run's performance of each task
for task_idx in sorted(os.listdir(run_dir)):
if not fnm.fnmatch(task_idx, 'rt-app-*.log'):
continue
# Parse run's performance results
prf_file = run_dir + '/' + task_idx
task = RTAppPerf(prf_file, self.nrg)
# Keep track of average performances of each task
self.slack_pct.append(task.prf['slack_pct'])
self.perf_avg.append(task.prf['perf_avg'])
self.edp1.append(task.prf['edp1'])
self.edp2.append(task.prf['edp2'])
self.edp3.append(task.prf['edp3'])
# Keep track of performance stats for each task
rta[task.name] = task.prf
# Dump per task rtapp stats
prf_file = os.path.join(run_dir, 'performance.json')
with open(prf_file, 'w') as ofile:
json.dump(rta, ofile, indent=4, sort_keys=True)
class RTAppPerf(object):
def __init__(self, perf_file, nrg):
# Set of exposed attibutes
self.prf = {
'perf_avg' : 0,
'perf_std' : 0,
'run_sum' : 0,
'slack_sum' : 0,
'slack_pct' : 0,
'edp1' : 0,
'edp2' : 0,
'edp3' : 0
}
self._log.debug('Parse [%s]...', perf_file)
# Load performance data for each RT-App task
self.name = perf_file.split('-')[-2]
self.data = np.loadtxt(perf_file, comments='#', unpack=False)
# Max Slack (i.e. configured/expected slack): period - run
max_slack = np.subtract(
self.data[:,RTAPP_COL_C_PERIOD], self.data[:,RTAPP_COL_C_RUN])
# Performance Index: 100 * slack / max_slack
perf = np.divide(self.data[:,RTAPP_COL_SLACK], max_slack)
perf = np.multiply(perf, 100)
self.prf['perf_avg'] = np.mean(perf)
self.prf['perf_std'] = np.std(perf)
self._log.debug('perf [%s]: %6.2f,%6.2f',
self.name, self.prf['perf_avg'],
self.prf['perf_std'])
# Negative slacks
nslacks = self.data[:,RTAPP_COL_SLACK]
nslacks = nslacks[nslacks < 0]
self._log.debug('Negative slacks: %s', nslacks)
self.prf['slack_sum'] = -nslacks.sum()
self._log.debug('Negative slack [%s] sum: %6.2f',
self.name, self.prf['slack_sum'])
# Slack over run-time
self.prf['run_sum'] = np.sum(self.data[:,RTAPP_COL_RUN])
self.prf['slack_pct'] = 100 * self.prf['slack_sum'] / self.prf['run_sum']
self._log.debug('SlackPct [%s]: %6.2f %%', self.name, self.slack_pct)
if nrg is None:
return
# Computing EDP
self.prf['edp1'] = nrg.total * math.pow(self.prf['run_sum'], 1)
self._log.debug('EDP1 [%s]: {%6.2f}', self.name, self.prf['edp1'])
self.prf['edp2'] = nrg.total * math.pow(self.prf['run_sum'], 2)
self._log.debug('EDP2 [%s]: %6.2f', self.name, self.prf['edp2'])
self.prf['edp3'] = nrg.total * math.pow(self.prf['run_sum'], 3)
self._log.debug('EDP3 [%s]: %6.2f', self.name, self.prf['edp3'])
# Columns of the per-task rt-app log file
RTAPP_COL_IDX = 0
RTAPP_COL_PERF = 1
RTAPP_COL_RUN = 2
RTAPP_COL_PERIOD = 3
RTAPP_COL_START = 4
RTAPP_COL_END = 5
RTAPP_COL_REL_ST = 6
RTAPP_COL_SLACK = 7
RTAPP_COL_C_RUN = 8
RTAPP_COL_C_PERIOD = 9
RTAPP_COL_WU_LAT = 10
################################################################################
# Generic workload performance parsing class
################################################################################
class DefaultTest(Test):
def __init__(self, test_idx, test_dir, res):
super(DefaultTest, self).__init__(test_idx, test_dir, res)
# Default performance metric
self.ctime_avg = []
self.perf_avg = []
self.edp1 = []
self.edp2 = []
self.edp3 = []
def parse_run(self, run_idx, run_dir):
return DefaultRun(run_idx, run_dir)
def collect_performance(self, run):
# Keep track of average performances of each run
self.ctime_avg.append(run.ctime_avg)
self.perf_avg.append(run.perf_avg)
self.edp1.append(run.edp1)
self.edp2.append(run.edp2)
self.edp3.append(run.edp3)
def performance(self):
return {
'ctime_avg' : Stats(self.ctime_avg).get(),
'perf_avg' : Stats(self.perf_avg).get(),
'edp1' : Stats(self.edp1).get(),
'edp2' : Stats(self.edp2).get(),
'edp3' : Stats(self.edp3).get(),
}
class DefaultRun(Run):
def __init__(self, run_idx, run_dir):
# Call base class to parse energy data
super(DefaultRun, self).__init__(run_idx, run_dir)
# Default specific performance stats
self.ctime_avg = 0
self.perf_avg = 0
self.edp1 = 0
self.edp2 = 0
self.edp3 = 0
# Load default performance.json
prf_file = os.path.join(run_dir, 'performance.json')
if not os.path.isfile(prf_file):
self._log.warning('No performance.json found in %s',
run_dir)
return
# Load performance report from JSON
with open(prf_file, 'r') as infile:
prf = json.load(infile)
# Keep track of performance value
self.ctime_avg = prf['ctime']
self.perf_avg = prf['performance']
# Compute EDP indexes if energy measurements are available
if self.nrg is None:
return
# Computing EDP
self.edp1 = self.nrg.total * math.pow(self.ctime_avg, 1)
self.edp2 = self.nrg.total * math.pow(self.ctime_avg, 2)
self.edp3 = self.nrg.total * math.pow(self.ctime_avg, 3)
################################################################################
# Globals
################################################################################
# Regexp to match the format of a result folder
TEST_DIR_RE = re.compile(
r'.*/([^:]*):([^:]*):([^:]*)'
)
#vim :set tabstop=4 shiftwidth=4 expandtab