From 5fdd68c1f830eca9d824488adcce59a95a0198e2 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 27 Jan 2017 10:28:33 +0100 Subject: [PATCH 01/97] Add first version of etime.py etime.py is a tool to search for a benchmark with a predefined execution time (in real-world units) --- Commands/etime.py | 260 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) create mode 100644 Commands/etime.py diff --git a/Commands/etime.py b/Commands/etime.py new file mode 100644 index 0000000..ef5fd1b --- /dev/null +++ b/Commands/etime.py @@ -0,0 +1,260 @@ +#!/usr/bin/python3 + +from multiprocessing import Process, Pipe +from tqdm import tqdm +import argparse as ap +import os +import shutil +import pkgutil +import sys +import uuid +import time +import numpy +import Queue +import threading +import multiprocessing +import matplotlib.pyplot as plt + +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Git, XMC4500, Result, EstimateSpill, GenE +from Helper.Frequency import Frequency + +CPU = "xmc4500" +CPU_TYPE = "cortex-m4" + +TQDM_BAR_FORMAT = '{l_bar}{bar} | {n_fmt}/{total_fmt} [{elapsed}<{remaining}]' + +def get_cycles(args, odir, xmcs, budgets): + + work_queue = Queue.Queue() + axf_queue = Queue.Queue() + res_queue = Queue.Queue() + pbar_queue = Queue.Queue() + + for i in xrange(len(budgets)): + work_queue.put(budgets[i]) + + def __generate(work_queue): + while True: + try: + budget = work_queue.get_nowait() + except Queue.Empty: + break + + pml = odir + '/bench_budget' + str(budget) + '.pml' + ll = odir + '/bench_budget' + str(budget) + '.ll' + o = odir + '/bench_budget' + str(budget) + '.o' + axf = odir + '/bench_budget' + str(budget) + '.axf' + + bm_uuid = str(uuid.uuid4()) + + bm = GenE.execute(Pattern.PATTERN[args.suite], CPU_TYPE, budget, args.bits, args.seed, 25, ll, simulate=False, silent=True) + + ll_comb = odir + '/combined_budget' + str(budget) + '.ll' + Tools.llvm_lls_to_ll([ll], ll_comb) + Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, args.opt) + + Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) + + o_uuid = "./xmc4500/" + bm_uuid + ".o" + axf_uuid = "./xmc4500/bin/system_" + bm_uuid + ".axf" + + shutil.copyfile(o, o_uuid) + Execution.run_wait("make -C ./xmc4500 bin/system_" + bm_uuid + ".axf", None, 0) + shutil.copyfile(axf_uuid, axf) + + Log.debug("Deleting " + o_uuid) + os.remove(o_uuid) + Log.debug("Deleting " + axf_uuid) + os.remove(axf_uuid) + + axf_queue.put((budget, axf)) + + + def __flash(axf_queue, xmc): + while True: + budget, bin_axf = axf_queue.get(block = True) + + if None == budget and None == bin_axf: + break + + success, errors = xmc.flash_retry(odir, bin_axf, silent = True) + + if not success: + Log.fail("Failed to flash to device, aborting") + for error in errors: + Log.fail(" * " + error) + return + + xmc.connect() + cy = xmc.benchmark(args.seed) + Log.debug("Got " + str(cy) + "cy for " + str(bin_axf)) + res_queue.put((budget, cy)) + xmc.disconnect() + pbar_queue.put(None) + + + procs_gen = [] + for i in xrange(0, multiprocessing.cpu_count()): + proc = threading.Thread(target=__generate, args=(work_queue,)) + proc.start() + procs_gen.append(proc) + + procs_flash = [] + for xmc in xmcs: + proc = threading.Thread(target=__flash, args=(axf_queue, xmc)) + proc.start() + procs_flash.append(proc) + + + pos = 0 + with tqdm(total=len(budgets), desc="Sampling", bar_format=TQDM_BAR_FORMAT) as pbar: + while pos != len(budgets): + pbar_queue.get() + pbar.update(1) + pos += 1 + + + for proc in procs_gen: + proc.join() + + for i in xrange(0, len(xmcs)): + axf_queue.put((None, None)) + + for proc in procs_flash: + proc.join() + + + result = [] + while not res_queue.empty(): + result.append(res_queue.get()) + + return result + +def get_argparser(): + parser = ap.ArgumentParser(prog=sys.argv[0] + " bench_input", + formatter_class=ap.ArgumentDefaultsHelpFormatter, + description='Subfunction bench_input_arm: ' + desc) + + parser.add_argument('--seed', metavar='seed', type=int, default=1431655765, help='seed passed to GenE') + parser.add_argument('--suite', metavar='suite', type=str, default="all", help='pattern suite used') + parser.add_argument('--entry', metavar='function', type=str, default='gene_main', help='entry used for analysis/simulation') + parser.add_argument('--etime', metavar='etime', type=int, default=100, help='desired execution time in us') + parser.add_argument('--bits', metavar='bits', type=int, default=32, help='Number of input bits') + parser.add_argument('--opt', metavar='opt', type=int, default=0, help='Optimization level') + parser.add_argument('--ofactor', metavar='ofactor', type=int, default=25, help='Overweighting factor') + parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') + + parser.add_argument('--eic', dest='eic', action='store_true', help='Enable instruction cache') + + return parser + +def run(args): + gene_rev, gene_mod = GenE.revision() + + subcmd = os.path.splitext(os.path.basename(__file__))[0] + + odir = './results/' + subcmd \ + + '_etime' + str(args.etime) \ + + '_seed' + str(args.seed) \ + + '_bits' + str(args.bits) \ + + '_opt' + str(args.opt) \ + + '_ofactor' + str(args.ofactor) \ + + '_rev' + gene_rev \ + + '_suite' + str(args.suite) + + # instruction cache enabled? + if args.eic: + odir = odir + '_eic' + + if not os.path.exists(odir): + os.makedirs(odir) + + Log.set_logfile(odir + '/aladdin.log') + + if gene_mod: + Log.warn("Your GenE repo is modified!") + + xmcs = XMC4500.list_devices(odir, args.xmccache) + + xmc = xmcs[0] + xmc.connect() + dev_freq = Frequency(xmc.get_info()["freq"]) + xmc.disconnect() + + Log.info("Device Frequency: " + str(dev_freq)) + target_sec = args.etime / float(1000 * 1000) + target_cy = dev_freq.cycles(target_sec) + Log.info("Target Cycles: " + str(target_cy)) + + samples = range(1000, 6000, 1000) + range(10000, 150000, 10000) + result = get_cycles(args, odir, xmcs, samples) + + samples = [] + cycles = [] + for budget, cy in result: + Log.info("Execution time for budget " + str(budget) + ": " + str(cy)) + samples.append(budget) + cycles.append(cy) + + fit, res = numpy.polyfit(samples, cycles, 1, full = True)[0:2] + fit_fn = numpy.poly1d(fit) + Log.info("Fit-Func: " + str(fit_fn).strip('\r\n ') + ", residual: " + str(res[0])) + + #plt.plot(samples, cycles, 'yo', samples, fit_fn(samples), '--k') + #plt.show() + + m = fit_fn.coeffs[0] + t = fit_fn.coeffs[1] + est_cost = int((target_cy - t) / m) + + Log.info(" -> Estimated budget: " + str(est_cost)) + + if est_cost < 750: + Log.warn("Estimated budget is less than the minimum sample value; resampling!") + + samples = range(100, 1500, 100) + result = get_cycles(args, odir, xmcs, samples) + + samples = [] + cycles = [] + for budget, cy in result: + Log.info("Execution time for budget " + str(budget) + ": " + str(cy)) + samples.append(budget) + cycles.append(cy) + + fit, res = numpy.polyfit(samples, cycles, 1, full = True)[0:2] + fit_fn = numpy.poly1d(fit) + Log.info("Fit-Func: " + str(fit_fn).strip('\r\n ') + ", residual: " + str(res[0])) + + #plt.plot(samples, cycles, 'yo', samples, fit_fn(samples), '--k') + #plt.show() + + m = fit_fn.coeffs[0] + t = fit_fn.coeffs[1] + est_cost = int((target_cy - t) / m) + + Log.info(" -> Estimated budget: " + str(est_cost)) + + + guess = [] + f = 0.85 + while f <= 1.15: + guess.append( int(f * est_cost) ) + f += 0.005 + + result = get_cycles(args, odir, xmcs, guess) + for budget, cy in result: + Log.debug(" * Guess budget " + str(budget) + ": " + str(cy) + "cy (" + str(cy / 120) + "us)") + + best_budget, best_cy = result[0] + best_delta = abs(target_cy - best_cy) + for i in xrange(len(guess)): + delta = abs(target_cy - result[i][1]) + if delta < best_delta: + best_budget, best_cy = result[i] + best_delta = delta + + Log.info("best guess " + str(best_budget) + ": " + str(best_cy / 120) + "us (delta: " + str(best_cy - target_cy) + "cy; " + str(100 * float(best_cy - target_cy) / target_cy) + "%)") + + +desc = "[ARM] Find a benchmark with a predefined execution time (in us)" -- GitLab From 8d82afa7840db7158397160841a39d9b0bb22b58 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 27 Jan 2017 10:31:37 +0100 Subject: [PATCH 02/97] Add .gitignore --- .gitignore | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa88221 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# vim swap files +*.swp + +# temp files created by many text editors +*~ + +# qtcreator files: +*.creator +*.creator.user +*.files +*.txt.user + +# files compiled by python +*.pyc -- GitLab From 4911d6f3bea80f522358a32991fe2eef22360aac Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Feb 2017 11:13:48 +0100 Subject: [PATCH 03/97] dat2pgf: Allow changing font-size --- Commands/dat2pgf.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index abc0160..761ef5d 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -13,6 +13,9 @@ def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2pgf", description='Subfunction dat2pgf: ' + desc) parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file(s) to be converted') + fs_def = matplotlib.rcParams['font.size'] + + parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used (default: ' + str(fs_def) + ')') parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics (default: false)') parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image (default: 6)') parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image (default: 2)') @@ -98,6 +101,8 @@ def hist_fh(args, dat): def run(args): + matplotlib.rcParams.update({'font.size': args.fs}) + for fle in args.input: if os.stat(fle).st_size > 0: dat = Dat.Data.from_file(fle) -- GitLab From 2e15b386018d7f071c8a5b64cae73e2e9b2835d5 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 15 Feb 2017 09:11:45 +0100 Subject: [PATCH 04/97] Include minimum/maximum overestimation factor in output --- Commands/dat2stat.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index e214409..079305a 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -82,8 +82,9 @@ def run(args): geo_valid = geomean( [ c for c in cy if c >= 1.0 ] ) - Log.info(" * Avg wcet: " + "{0:.2f}".format(numpy.average(arr)) + "x max (" + str(len(cy)) + " samples)") - Log.info(" * Geo mean: " + "{0:.2f}".format(geomean(arr)) + "x max (" + str(len(cy)) + " samples) - " + str(geo_valid) + "x (only valid)") + Log.info(" * Min/Max: " + "{0:.2f}".format(mins[ana]) + "/" + "{0:.2f}".format(maxs[ana])) + Log.info(" * Avg wcet: " + "{0:.2f}".format(avgs[ana]) + "x max (" + str(len(cy)) + " samples)") + Log.info(" * Geo mean: " + "{0:.2f}".format(geos[ana]) + "x max (" + str(len(cy)) + " samples) - " + str(geo_valid) + "x (only valid)") else: Log.warn(" >> NO VALID DATA <<") -- GitLab From 673e637005ba60e6b6e42acffcfaea21167fee8b Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 15 Feb 2017 15:34:31 +0100 Subject: [PATCH 05/97] Use appropriate backend for pyplot to allow dat2* without DISPLAY --- Commands/dat2pgf.py | 9 +++++---- Commands/dat2stat.py | 6 ++++-- Commands/display.py | 6 ++++-- Helper/Python.py | 13 +++++++++++++ aladdin | 17 +++++++++++++---- 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 Helper/Python.py diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index abc0160..a3f559a 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -1,13 +1,10 @@ #!/usr/bin/python import argparse import numpy -import matplotlib.pyplot as plt -import matplotlib -from matplotlib.ticker import AutoMinorLocator import sys import os -from Helper import Dat, Execution, Log +from Helper import Dat, Execution, Log, Python def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2pgf", description='Subfunction dat2pgf: ' + desc) @@ -63,6 +60,7 @@ def hist_fh(args, dat): for axis in ['top','bottom','left','right']: axes.spines[axis].set_linewidth(0.5) + from matplotlib.ticker import AutoMinorLocator minorLocator = AutoMinorLocator(5) axes.yaxis.set_minor_locator(minorLocator) @@ -98,6 +96,9 @@ def hist_fh(args, dat): def run(args): + global plt + plt = Python.import_pyplot('pgf') + for fle in args.input: if os.stat(fle).st_size > 0: dat = Dat.Data.from_file(fle) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index 079305a..dc35cde 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -4,9 +4,8 @@ import glob import numpy import sys import os -import matplotlib.pyplot as plt -from Helper import Dat, Log +from Helper import Dat, Log, Python def geomean(a): return numpy.exp( numpy.log(a).mean() ) @@ -32,6 +31,9 @@ def get_argparser(): return parser def run(args): + global plt + plt = Python.import_pyplot('pdf') + ubs = {} wcets = [] mins = {} diff --git a/Commands/display.py b/Commands/display.py index 9fe6d8a..a887af8 100644 --- a/Commands/display.py +++ b/Commands/display.py @@ -1,10 +1,9 @@ #!/usr/bin/python import argparse -import matplotlib.pyplot as plt import sys import os -from Helper import Dat, Log +from Helper import Dat, Log, Python def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " display", description='Subfunction display: ' + desc) @@ -53,6 +52,9 @@ def hist_fh(fobject, bins): plt.title(title) def run(args): + global plt + plt = Python.import_pyplot('GTK3Cairo') + i = 1 for fle in args.input: #if os.fstat(fle.fileno()).st_size > 0: diff --git a/Helper/Python.py b/Helper/Python.py new file mode 100644 index 0000000..986cd4f --- /dev/null +++ b/Helper/Python.py @@ -0,0 +1,13 @@ +from Helper import Log + +# switch backends to allow using pyplot without having DISPLAY set +# the following two lines need to preceed any other includes of matplotlib +# or related packages (pyplots, etc.) +def import_pyplot(backend = None): + Log.debug("Importing pyplot with backend " + str(backend)) + if None != backend: + import matplotlib + matplotlib.use('GTK3Cairo') + + import matplotlib.pyplot as plt + return plt diff --git a/aladdin b/aladdin index f243137..49c6d7e 100755 --- a/aladdin +++ b/aladdin @@ -25,9 +25,17 @@ class ZshCompHelpFormatter(ap.HelpFormatter): res, actions, groups, "") +class Module: + def __init__(self, prefix, name, loader): + self.prefix = prefix + self.name = name + self.loader = loader + self.mod = self.loader.find_module(self.name).load_module(self.name) + self.desc = self.mod.desc + def get_modules(prefix): - return { pkg[1][len(prefix):]: pkg[0].find_module(pkg[1]).load_module(pkg[1]) - for pkg in pkgutil.walk_packages('.') if pkg[1].startswith(prefix) } + return { name[len(prefix):]: Module(prefix, name, loader) + for loader, name, _ in pkgutil.walk_packages('.') if name.startswith(prefix) } # find all available modules in Commands/ oldcwd=os.getcwd() @@ -73,7 +81,8 @@ else: resource.setrlimit(resource.RLIMIT_AS, (mem_limit, mem_limit)) time_start = datetime.now() - subargs = cmd_to_mod[cmd].get_argparser().parse_args(args[1]) - cmd_to_mod[cmd].run(subargs) + mod = cmd_to_mod[cmd].mod + subargs = mod.get_argparser().parse_args(args[1]) + mod.run(subargs) Log.info("Execution of command \"" + cmd + "\" took " + str(datetime.now() - time_start)) -- GitLab From f2e56367de4ab0661efc2624f634871ee6520d89 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 14 Feb 2017 09:35:46 +0100 Subject: [PATCH 06/97] Implement globbing for input files to dat2stat --- Commands/dat2stat.py | 50 ++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index 9615d02..e214409 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -1,5 +1,6 @@ #!/usr/bin/python import argparse +import glob import numpy import sys import os @@ -14,7 +15,7 @@ def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2stat", description='Subfunction dat2stat: ' + desc) parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file(s) to be converted') - parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds (default: 1)') + parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds (default: 1)') def __register_color(name, label, default): parser.add_argument('--color-' + name, @@ -23,7 +24,6 @@ def get_argparser(): default=default, type=str, help='Color for ' + label + ' (default: ' + default + ')') - __register_color('analyzer', 'upper bounds from WCET analyzers', '#19B219') __register_color('wcet', 'Worst Observed Execution Time', '#B41C2E') __register_color('bin', 'Facecolor of Bins', '#00669E') @@ -40,24 +40,35 @@ def run(args): geos = {} wca_errors = {} - for fle in args.input: - if os.stat(fle).st_size > 0: - dat = Dat.Data.from_file(fle) - for ana, cy in dat.wcet.iteritems(): - if ana not in ubs: ubs[ana] = [] - if ana not in wca_errors: wca_errors[ana] = {} + def __accumulate_dat(fle): + dat = Dat.Data.from_file(fle) + for ana, cy in dat.wcet.iteritems(): + if ana not in ubs: ubs[ana] = [] + if ana not in wca_errors: wca_errors[ana] = {} - wcets.append(dat.max) + wcets.append(dat.max) - if cy > 0: ubs[ana].append( float(cy) / dat.max ) - else: - if cy not in wca_errors[ana]: wca_errors[ana][cy] = 0 - wca_errors[ana][cy] += 1 + if cy > 0: ubs[ana].append( float(cy) / dat.max ) + else: + if cy not in wca_errors[ana]: wca_errors[ana][cy] = 0 + wca_errors[ana][cy] += 1 - if not ana.endswith("_noff") and cy == Dat.ERROR_WCET_UNBOUND: - Log.fail(" Unbound: " + fle ) + if not ana.endswith("_noff") and cy == Dat.ERROR_WCET_UNBOUND: + Log.fail(" Unbound: " + fle ) + + for fle in args.input: + have_fle = False + if os.path.isfile(fle): + __accumulate_dat(fle) + have_fle = True else: - Log.warn("File " + fle.name + " is empty") + Log.warn("Using \"" + fle + " for globing\"") + for match in glob.iglob(fle): + __accumulate_dat(match) + have_fle = True + + if not have_fle: + Log.warn("Filename or pattern " + fle + " does not match any valid file") for ana, cy in ubs.iteritems(): Log.info("Statistics for " + ana) @@ -68,8 +79,11 @@ def run(args): maxs[ana] = numpy.max(arr) avgs[ana] = numpy.average(arr) geos[ana] = geomean(arr) + + geo_valid = geomean( [ c for c in cy if c >= 1.0 ] ) + Log.info(" * Avg wcet: " + "{0:.2f}".format(numpy.average(arr)) + "x max (" + str(len(cy)) + " samples)") - Log.info(" * Geo mean: " + "{0:.2f}".format(geomean(arr)) + "x max (" + str(len(cy)) + " samples)") + Log.info(" * Geo mean: " + "{0:.2f}".format(geomean(arr)) + "x max (" + str(len(cy)) + " samples) - " + str(geo_valid) + "x (only valid)") else: Log.warn(" >> NO VALID DATA <<") @@ -100,7 +114,7 @@ def run(args): plt.clf() # dump histogram "WCET" - bins = numpy.linspace(mins[ana], maxs[ana], 500) + bins = numpy.linspace(min(mins.itervalues()), max(maxs.itervalues()), 500) plt.hist(wcets, 500, facecolor=args.color_bin, alpha=0.5) -- GitLab From 8cc0ebcb67008348c1efe881c3d12731ae411c31 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 15 Feb 2017 09:11:45 +0100 Subject: [PATCH 07/97] Include minimum/maximum overestimation factor in output --- Commands/dat2stat.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index e214409..079305a 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -82,8 +82,9 @@ def run(args): geo_valid = geomean( [ c for c in cy if c >= 1.0 ] ) - Log.info(" * Avg wcet: " + "{0:.2f}".format(numpy.average(arr)) + "x max (" + str(len(cy)) + " samples)") - Log.info(" * Geo mean: " + "{0:.2f}".format(geomean(arr)) + "x max (" + str(len(cy)) + " samples) - " + str(geo_valid) + "x (only valid)") + Log.info(" * Min/Max: " + "{0:.2f}".format(mins[ana]) + "/" + "{0:.2f}".format(maxs[ana])) + Log.info(" * Avg wcet: " + "{0:.2f}".format(avgs[ana]) + "x max (" + str(len(cy)) + " samples)") + Log.info(" * Geo mean: " + "{0:.2f}".format(geos[ana]) + "x max (" + str(len(cy)) + " samples) - " + str(geo_valid) + "x (only valid)") else: Log.warn(" >> NO VALID DATA <<") -- GitLab From 740401ef8fb51bbaf7008ad529d7c8ee0f1bf440 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 15 Feb 2017 15:34:31 +0100 Subject: [PATCH 08/97] Use appropriate backend for pyplot to allow dat2* without DISPLAY --- Commands/dat2pgf.py | 9 +++++---- Commands/dat2stat.py | 6 ++++-- Commands/display.py | 6 ++++-- Helper/Python.py | 13 +++++++++++++ aladdin | 17 +++++++++++++---- 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 Helper/Python.py diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index 761ef5d..17f41a7 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -1,13 +1,10 @@ #!/usr/bin/python import argparse import numpy -import matplotlib.pyplot as plt -import matplotlib -from matplotlib.ticker import AutoMinorLocator import sys import os -from Helper import Dat, Execution, Log +from Helper import Dat, Execution, Log, Python def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2pgf", description='Subfunction dat2pgf: ' + desc) @@ -66,6 +63,7 @@ def hist_fh(args, dat): for axis in ['top','bottom','left','right']: axes.spines[axis].set_linewidth(0.5) + from matplotlib.ticker import AutoMinorLocator minorLocator = AutoMinorLocator(5) axes.yaxis.set_minor_locator(minorLocator) @@ -103,6 +101,9 @@ def hist_fh(args, dat): def run(args): matplotlib.rcParams.update({'font.size': args.fs}) + global plt + plt = Python.import_pyplot('pgf') + for fle in args.input: if os.stat(fle).st_size > 0: dat = Dat.Data.from_file(fle) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index 079305a..dc35cde 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -4,9 +4,8 @@ import glob import numpy import sys import os -import matplotlib.pyplot as plt -from Helper import Dat, Log +from Helper import Dat, Log, Python def geomean(a): return numpy.exp( numpy.log(a).mean() ) @@ -32,6 +31,9 @@ def get_argparser(): return parser def run(args): + global plt + plt = Python.import_pyplot('pdf') + ubs = {} wcets = [] mins = {} diff --git a/Commands/display.py b/Commands/display.py index 9fe6d8a..a887af8 100644 --- a/Commands/display.py +++ b/Commands/display.py @@ -1,10 +1,9 @@ #!/usr/bin/python import argparse -import matplotlib.pyplot as plt import sys import os -from Helper import Dat, Log +from Helper import Dat, Log, Python def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " display", description='Subfunction display: ' + desc) @@ -53,6 +52,9 @@ def hist_fh(fobject, bins): plt.title(title) def run(args): + global plt + plt = Python.import_pyplot('GTK3Cairo') + i = 1 for fle in args.input: #if os.fstat(fle.fileno()).st_size > 0: diff --git a/Helper/Python.py b/Helper/Python.py new file mode 100644 index 0000000..986cd4f --- /dev/null +++ b/Helper/Python.py @@ -0,0 +1,13 @@ +from Helper import Log + +# switch backends to allow using pyplot without having DISPLAY set +# the following two lines need to preceed any other includes of matplotlib +# or related packages (pyplots, etc.) +def import_pyplot(backend = None): + Log.debug("Importing pyplot with backend " + str(backend)) + if None != backend: + import matplotlib + matplotlib.use('GTK3Cairo') + + import matplotlib.pyplot as plt + return plt diff --git a/aladdin b/aladdin index f243137..49c6d7e 100755 --- a/aladdin +++ b/aladdin @@ -25,9 +25,17 @@ class ZshCompHelpFormatter(ap.HelpFormatter): res, actions, groups, "") +class Module: + def __init__(self, prefix, name, loader): + self.prefix = prefix + self.name = name + self.loader = loader + self.mod = self.loader.find_module(self.name).load_module(self.name) + self.desc = self.mod.desc + def get_modules(prefix): - return { pkg[1][len(prefix):]: pkg[0].find_module(pkg[1]).load_module(pkg[1]) - for pkg in pkgutil.walk_packages('.') if pkg[1].startswith(prefix) } + return { name[len(prefix):]: Module(prefix, name, loader) + for loader, name, _ in pkgutil.walk_packages('.') if name.startswith(prefix) } # find all available modules in Commands/ oldcwd=os.getcwd() @@ -73,7 +81,8 @@ else: resource.setrlimit(resource.RLIMIT_AS, (mem_limit, mem_limit)) time_start = datetime.now() - subargs = cmd_to_mod[cmd].get_argparser().parse_args(args[1]) - cmd_to_mod[cmd].run(subargs) + mod = cmd_to_mod[cmd].mod + subargs = mod.get_argparser().parse_args(args[1]) + mod.run(subargs) Log.info("Execution of command \"" + cmd + "\" took " + str(datetime.now() - time_start)) -- GitLab From b5b109dddc070b29448aee08c8af4de6307a8441 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 7 Feb 2017 10:38:11 +0100 Subject: [PATCH 09/97] Stop retrying connection after 3 fails --- Helper/XMC4500.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Helper/XMC4500.py b/Helper/XMC4500.py index ff274ec..3bb1406 100644 --- a/Helper/XMC4500.py +++ b/Helper/XMC4500.py @@ -147,7 +147,11 @@ class XMC4500: tty = self.get_tty() assert None != tty, "failed to obtain tty for " + str(self) - while None == self.dev: + retry = 0 + MAX_RETRY = 3 + last_errno = None + + while None == self.dev and retry < MAX_RETRY: try: self.dev = Serial( port = tty, @@ -156,8 +160,14 @@ class XMC4500: # use default values for other options ) except SerialException as e: - Log.debug("Retrying connection to " + str(self) + ": " + str(e)) - time.sleep(1) + retry += 1 + last_errno = e.errno + + if not (retry < MAX_RETRY): + Log.fail("Connection to " + str(self) + " cannot be established: " + str(e)) + else: + Log.debug("Retrying connection to " + str(self) + ": " + str(e)) + time.sleep(1) assert self.is_connected(), "device " + str(tty) + " is not connected after opening" -- GitLab From 7b08c09c20f8b6ada11eb481bd833c590b1ed0a5 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 16 Feb 2017 08:59:10 +0100 Subject: [PATCH 10/97] Move GenE's parameter handling to Helper/GenE.py --- Commands/bench_input.py | 7 +------ Commands/bench_input_arm.py | 5 +---- Helper/GenE.py | 7 +++++++ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Commands/bench_input.py b/Commands/bench_input.py index 82cea1c..a3c1549 100644 --- a/Commands/bench_input.py +++ b/Commands/bench_input.py @@ -16,15 +16,10 @@ CPU = "patmos" def get_argparser(): parser = ap.ArgumentParser(prog=sys.argv[0] + " bench_input", description='Subfunction bench_input: ' + desc) parser.add_argument('--entry', metavar='function', type=str, default='gene_main', help='entry used for analysis/simulation') - - parser.add_argument('--seed', metavar='seed', type=int, default=1431655765, help='seed passed to GenE (default: 1431655765)') - parser.add_argument('--suite', metavar='suite', type=str, default="all", help='pattern suite used (default: all)') parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking (default: 10000)') - parser.add_argument('--cost', metavar='cost', type=int, default=20000, help='cost to be used for generating code (default: 10000)') - parser.add_argument('--bits', metavar='bits', type=int, default=32, help='Number of input bits (default: 32)') parser.add_argument('--opt', metavar='opt', type=int, default=0, help='Optimization level (default: 0)') - parser.add_argument('--ofactor', metavar='ofactor', type=int, default=25, help='Overweighting factor') + GenE.add_arguments(parser) Analyzers.add_arguments(parser) return parser diff --git a/Commands/bench_input_arm.py b/Commands/bench_input_arm.py index 0b3682b..c2d30c5 100644 --- a/Commands/bench_input_arm.py +++ b/Commands/bench_input_arm.py @@ -33,12 +33,8 @@ def get_argparser(): formatter_class=ap.ArgumentDefaultsHelpFormatter, description='Subfunction bench_input_arm: ' + desc) - parser.add_argument('--seed', metavar='seed', type=int, default=1431655765, help='seed passed to GenE') - parser.add_argument('--suite', metavar='suite', type=str, default="all", help='pattern suite used') parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking') parser.add_argument('--entry', metavar='function', type=str, default='gene_main', help='entry used for analysis/simulation') - parser.add_argument('--cost', metavar='cost', type=int, default=20000, help='cost to be used for generating code') - parser.add_argument('--bits', metavar='bits', type=int, default=32, help='Number of input bits') parser.add_argument('--opt', metavar='opt', type=int, default=0, help='Optimization level') parser.add_argument('--ofactor', metavar='ofactor', type=int, default=25, help='Overweighting factor') parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') @@ -48,6 +44,7 @@ def get_argparser(): parser.add_argument('--no-metrics', dest='nometrics', action='store_true', help='Disable computing metrics') parser.add_argument('--no-espill', dest='noespill', action='store_true', help='Disable estimation of spilling instructions') + GenE.add_arguments(parser) Analyzers.add_arguments(parser) return parser diff --git a/Helper/GenE.py b/Helper/GenE.py index b014961..4b8245b 100644 --- a/Helper/GenE.py +++ b/Helper/GenE.py @@ -135,3 +135,10 @@ def revision(): return (repo_rev + "+", True) return (repo_rev, False) + +def add_arguments(parser): + parser.add_argument('--seed', metavar='seed', type=int, default=1431655765, help='seed passed to GenE') + parser.add_argument('--suite', metavar='suite', type=str, default="all", help='pattern suite used') + parser.add_argument('--cost', metavar='cost', type=int, default=20000, help='cost to be used for generating code') + parser.add_argument('--bits', metavar='bits', type=int, default=32, help='Number of input bits') + parser.add_argument('--ofactor', metavar='ofactor', type=int, default=25, help='Overweighting factor') -- GitLab From 8608760c1a01ebf386cff8b010ba5548d6c61668 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 27 Feb 2017 14:23:19 +0100 Subject: [PATCH 11/97] Remove global state for get_input to achieve thread safety --- Helper/Benchmark.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/Helper/Benchmark.py b/Helper/Benchmark.py index a8e4e01..b6119e2 100644 --- a/Helper/Benchmark.py +++ b/Helper/Benchmark.py @@ -3,38 +3,37 @@ import numpy import array -def get_input_add(lst, value): - get_input_add.last_add += 1 - - if get_input_add.last_add >= len(lst): - get_input_add.last_add = 0 - - lst[get_input_add.last_add].append(value) - -get_input_add.last_add = -1 - def get_input(bits, seed, samples, chunks): input_data = [] for i in range(0, chunks): input_data.append( array.array('L') ) # L = unsigned long + vars = {'last': -1, 'arr': input_data} + def __add(value): + vars['last'] += 1 + + if vars['last'] >= len(vars['arr']): + vars['last'] = 0 + + vars['arr'][ vars['last'] ].append(value) + # perform an explicit enumeration of all possible inputs # if 2^(bits) > #samples if 2**bits <= samples: - get_input_add(input_data, seed) + __add(seed) for val in range(0, 2**bits - 1): - get_input_add(input_data, val) + __add(input_data, val) else: - get_input_add(input_data, seed) + __add(seed) for exp in range(0, bits - 1): - get_input_add(input_data, seed ^ (2**exp)) + __add(seed ^ (2**exp)) for exp in range(0, bits - 1): - get_input_add(input_data, 2**exp) + __add(2**exp) for i in xrange(samples): - get_input_add(input_data, numpy.random.randint(2**bits - 1)) + __add(numpy.random.randint(2**bits - 1)) return input_data -- GitLab From b14f304c1a9fec267c7e9816220f9627b842b022 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 27 Feb 2017 15:23:25 +0100 Subject: [PATCH 12/97] Fix ArgParse message for bench_input_arm --- Commands/bench_input_arm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Commands/bench_input_arm.py b/Commands/bench_input_arm.py index c2d30c5..5e32ba2 100644 --- a/Commands/bench_input_arm.py +++ b/Commands/bench_input_arm.py @@ -29,7 +29,7 @@ def benchmark_input_run(pipe, input_array, xmc, eic): pipe.close() def get_argparser(): - parser = ap.ArgumentParser(prog=sys.argv[0] + " bench_input", + parser = ap.ArgumentParser(prog=sys.argv[0] + " bench_input_arm", formatter_class=ap.ArgumentDefaultsHelpFormatter, description='Subfunction bench_input_arm: ' + desc) -- GitLab From bc69693c4baa1856ecb5af84b72aeaf22726ec8a Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 10:48:28 +0100 Subject: [PATCH 13/97] Allow local and global silencing of Log.* --- Helper/Log.py | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/Helper/Log.py b/Helper/Log.py index 3986ff4..29c07bb 100644 --- a/Helper/Log.py +++ b/Helper/Log.py @@ -2,8 +2,9 @@ from __future__ import print_function import datetime, sys -__fpath = None -__fh = None +__silent = False +__fpath = None +__fh = None def red(text): return '\033[1;31m' + text + '\033[1;m' def yellow(text): return '\033[1;33m' + text + '\033[1;m' @@ -11,6 +12,12 @@ def green(text): return '\033[1;32m' + text + '\033[1;m' def blue(text): return '\033[1;36m' + text + '\033[1;m' def pink(text): return '\033[1;35m' + text + '\033[1;m' + +def __print(line, end='\n', silent=False): + global __silent + if not __silent and not silent: + print(line, end=end) + def __write(line): global __fh if __fh != None: @@ -23,45 +30,45 @@ def set_logfile(fpath): __fpath = fpath __fh = open(fpath, 'w') -def fail(msg): - print(red('[fail]') + ' ' + msg) +def fail(msg, silent=False): + __print(red('[fail]') + ' ' + msg, silent=silent) __write(red('[fail]') + ' ' + msg) -def warn(msg): - print(yellow('[warn]') + ' ' + msg) +def warn(msg, silent=False): + __print(yellow('[warn]') + ' ' + msg, silent=silent) __write(yellow('[warn]') + ' ' + msg) -def info(msg): - print(blue('[info]') + ' ' + msg) +def info(msg, silent=False): + __print(blue('[info]') + ' ' + msg, silent=silent) __write(blue('[info]') + ' ' + msg) - def debug(msg): + # debug does not print to std* __write(pink('[debg]') + ' ' + msg) -def operation_start(msg): - print('\r' + yellow('[....]') + ' ' + msg, end='') +def operation_start(msg, silent=False): + __print('\r' + yellow('[....]') + ' ' + msg, end='', silent=silent) sys.stdout.flush() __write(blue('[strt]') + ' ' + msg) -def operation_end(msg, fail = False): +def operation_end(msg, fail=False, silent=False): if fail: - print('\r' + red('[fail]')+ ' ' + msg) + __print('\r' + red('[fail]')+ ' ' + msg, silent=silent) __write(red('[fail]') + ' ' + msg) else: - print('\r' + green('[done]') + ' ' + msg) + __print('\r' + green('[done]') + ' ' + msg, silent=silent) __write(green('[done]') + ' ' + msg) __last_start = None -def start(msg): +def start(msg, silent=False): global __last_start __last_start = msg - print('\r' + yellow('[....]') + ' ' + msg, end='') + __print('\r' + yellow('[....]') + ' ' + msg, end='', silent=silent) sys.stdout.flush() __write(blue('[strt]') + ' ' + msg) -def end(msg = None, fail = False): +def end(msg=None, fail=False, silent=False): global __last_start if None == msg: @@ -71,10 +78,10 @@ def end(msg = None, fail = False): assert None != __last_start if fail: - print('\r' + red('[fail]')+ ' ' + __last_start + suffix) + __print('\r' + red('[fail]')+ ' ' + __last_start + suffix, silent=silent) __write(red('[fail]') + ' ' + __last_start + suffix) else: - print('\r' + green('[done]') + ' ' + __last_start + suffix) + __print('\r' + green('[done]') + ' ' + __last_start + suffix, silent=silent) __write(green('[done]') + ' ' + __last_start + suffix) __last_start = None -- GitLab From 9232f0906ec01e1debdab97c85ceca391947485b Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 10:55:16 +0100 Subject: [PATCH 14/97] Transform Analyzers.run() into a Python generator --- Analyzers/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Analyzers/__init__.py b/Analyzers/__init__.py index 6de99e2..6b857ac 100644 --- a/Analyzers/__init__.py +++ b/Analyzers/__init__.py @@ -34,6 +34,8 @@ def run(args, odir, dat, target, x, func, pml, config, eic): else: Log.info(" -> Result: " + str(result)) + yield (name + ("" if has_ff else "_noff"), result) + def add_arguments(parser): modules = get_modules('Analyzers.') for name, mod in modules.iteritems(): -- GitLab From b6dcd76da840ce6655f310c11a172bf267495695 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 10:56:54 +0100 Subject: [PATCH 15/97] Allow disabling execution of analyzers with(out) flow-facts --- Analyzers/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Analyzers/__init__.py b/Analyzers/__init__.py index 6b857ac..f38dad5 100644 --- a/Analyzers/__init__.py +++ b/Analyzers/__init__.py @@ -13,7 +13,12 @@ def get_modules(prefix): os.chdir(oldcwd) return res -def run(args, odir, dat, target, x, func, pml, config, eic): +def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_noff = True): + + ffs = [] + if run_ff: ffs.append(True) + if run_noff: ffs.append(False) + Log.info("Run Timing Analysis...") modules = get_modules('Analyzers.') for name, mod in modules.iteritems(): @@ -23,7 +28,7 @@ def run(args, odir, dat, target, x, func, pml, config, eic): if getattr(args, 'no' + name.lower()): continue - for has_ff in (True, False): + for has_ff in ffs: Log.info(" * Analyzer " + name + " WITH" + ("" if has_ff else "OUT") + " FFs") rpt = odir + '/' + name + ("" if has_ff else "_noff") + '.log' result = mod.run(odir, has_ff, target, x, func, pml, config, rpt, eic) -- GitLab From 836cbbd469f156637f584c11a3457910048bc087 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 11:00:04 +0100 Subject: [PATCH 16/97] Implement silencing of platin.run() --- Analyzers/platin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Analyzers/platin.py b/Analyzers/platin.py index ae9eb2a..3fb8970 100644 --- a/Analyzers/platin.py +++ b/Analyzers/platin.py @@ -2,10 +2,10 @@ import re -from Helper import Dat, Execution, Log, Tools +from Helper import Dat, Execution, Tools from AnalyzerResult import AnalyzerResult -def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic): +def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silent=False): "Run the platin WCET analyzer on `binary`" re_wcet = re.compile(r'^\[platin\] INFO: best WCET bound: (-?\d*) cycles$', re.MULTILINE) -- GitLab From 7f2fa75856377a9d181553e9a9a6d5279e2fe6e5 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 11:03:52 +0100 Subject: [PATCH 17/97] Add entry and #wait_states as parameters to __prepare_apx --- Analyzers/aiT.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Analyzers/aiT.py b/Analyzers/aiT.py index 5154a5a..f0c5179 100644 --- a/Analyzers/aiT.py +++ b/Analyzers/aiT.py @@ -13,15 +13,15 @@ __USER = os.environ.get('CIP_USER') __HOST = "i4jenkins.informatik.uni-erlangen.de" __A3BIN = "/proj/i4ezs/tools/a3_arm/bin/a3arm" -def __prepare_apx(x, apx, ais, rpt, eic): +def __prepare_apx(x, apx, ais, rpt, entry, wait_states, eic): with open('aladdin/data/xmc4500_time.skel.apx') as skel_file: content = skel_file.read() # insert actual values into .apx skeleton - content = content.replace('NAME_OF_BIN_FILE', x) - content = content.replace('DATA_WAIT_STATES', '3') - content = content.replace('NAME_OF_ENTRY', 'gene_main') - content = content.replace('NAME_OF_AIS_FILE', ais) + content = content.replace('NAME_OF_BIN_FILE', x) + content = content.replace('DATA_WAIT_STATES', str(wait_states)) + content = content.replace('NAME_OF_ENTRY', str(entry)) + content = content.replace('NAME_OF_AIS_FILE', ais) content = content.replace('NAME_OF_REPORT_FILE', rpt) content = content.replace('BYPASS_INSTRUCTION_BUFFER', "false" if eic else "true") @@ -78,7 +78,7 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic): else: __pml_to_ais(pml, binary, pml + ".symb.pml", ais_uuid) - __prepare_apx(os.path.basename(binary), apx_uuid, os.path.basename(ais_uuid), rpt_uuid, eic) + __prepare_apx(os.path.basename(binary), apx_uuid, os.path.basename(ais_uuid), rpt_uuid, 'gene_main', 3, eic) # copy required files to target host and start aiT Log.start(" -> Copy binary, .apx and .ais to remote node") -- GitLab From 837c0b76493f0558eb091abf775643b0036b2afc Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 11:18:15 +0100 Subject: [PATCH 18/97] Rename binary to .axf to allow parallel aiT analysis --- Analyzers/aiT.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Analyzers/aiT.py b/Analyzers/aiT.py index f0c5179..f54cbec 100644 --- a/Analyzers/aiT.py +++ b/Analyzers/aiT.py @@ -50,16 +50,16 @@ def __parse_rpt(rpt): assert False, "Did not find cycles in aiT output:\n" + content -def __pml_to_ais(pml, x, out_pml_symb, out_ais): - Log.operation_start(" -> Extract Symbols for .pml") +def __pml_to_ais(pml, x, out_pml_symb, out_ais, silent=False): + Log.operation_start(" -> Extract Symbols for .pml", silent=silent) Execution.run_wait("platin extract-symbols -i \"" + pml + "\" -o \"" + out_pml_symb + "\" \"" + x + "\"", None, 0) - Log.operation_end(" -> Extract Symbols for .pml") + Log.operation_end(" -> Extract Symbols for .pml", silent=silent) - Log.operation_start(" -> Export .pml to .ais") + Log.operation_start(" -> Export .pml to .ais", silent=silent) Execution.run_wait("platin pml2ais -i \"" + out_pml_symb + "\" --ais \"" + out_ais + "\"", None, 0) - Log.operation_end(" -> Export .pml to .ais") + Log.operation_end(" -> Export .pml to .ais", silent=silent) -def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic): +def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silent=False): assert cpu in supported_backends host = __HOST @@ -70,19 +70,22 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic): ais_uuid = odir + '/' + ait_uuid + ".ais" apx_uuid = odir + '/' + ait_uuid + ".apx" + axf_uuid = odir + '/' + ait_uuid + ".axf" rpt_uuid = ait_uuid + ".rpt" + shutil.copyfile(binary, axf_uuid) + if not has_ff: Log.info(" -> Using empty ais file") shutil.copyfile("aladdin/data/no_ff.ais", ais_uuid) else: - __pml_to_ais(pml, binary, pml + ".symb.pml", ais_uuid) + __pml_to_ais(pml, axf_uuid, pml + ".symb.pml", ais_uuid) - __prepare_apx(os.path.basename(binary), apx_uuid, os.path.basename(ais_uuid), rpt_uuid, 'gene_main', 3, eic) + __prepare_apx(os.path.basename(axf_uuid), apx_uuid, os.path.basename(ais_uuid), rpt_uuid, 'gene_main', 3, eic) # copy required files to target host and start aiT Log.start(" -> Copy binary, .apx and .ais to remote node") - proc = Execution.run_wait("scp -q \"" + binary + "\" \"" + apx_uuid + "\" \"" + ais_uuid + "\" " + host + ":") + proc = Execution.run_wait("scp -q \"" + axf_uuid + "\" \"" + apx_uuid + "\" \"" + ais_uuid + "\" " + host + ":") if 0 != proc.retcode: stderr = proc.stderr.strip(' \t\r\n') errmsg = "scp failed: " + stderr @@ -113,7 +116,7 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic): Log.start(" -> Delete files from remote node") - proc = Execution.run_wait("ssh " + host + " rm \"" + rpt_uuid + "\" \"" + os.path.basename(ais_uuid) + "\" \"" + os.path.basename(apx_uuid) + "\" \"" + os.path.basename(binary) + "\"") + proc = Execution.run_wait("ssh " + host + " rm \"" + rpt_uuid + "\" \"" + os.path.basename(ais_uuid) + "\" \"" + os.path.basename(apx_uuid) + "\" \"" + os.path.basename(axf_uuid) + "\"") if 0 != proc.retcode: stderr = proc.stderr.strip(' \t\r\n') errmsg = "ssh failed: " + stderr -- GitLab From 56fdcfe9bfc7894386ba4dfce65b8eed7f9ad152 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 11:20:19 +0100 Subject: [PATCH 19/97] Implement silencing of aiT.run() --- Analyzers/aiT.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Analyzers/aiT.py b/Analyzers/aiT.py index f54cbec..3e60924 100644 --- a/Analyzers/aiT.py +++ b/Analyzers/aiT.py @@ -76,7 +76,7 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silen shutil.copyfile(binary, axf_uuid) if not has_ff: - Log.info(" -> Using empty ais file") + Log.info(" -> Using empty ais file", silent=silent) shutil.copyfile("aladdin/data/no_ff.ais", ais_uuid) else: __pml_to_ais(pml, axf_uuid, pml + ".symb.pml", ais_uuid) @@ -84,55 +84,55 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silen __prepare_apx(os.path.basename(axf_uuid), apx_uuid, os.path.basename(ais_uuid), rpt_uuid, 'gene_main', 3, eic) # copy required files to target host and start aiT - Log.start(" -> Copy binary, .apx and .ais to remote node") + Log.start(" -> Copy binary, .apx and .ais to remote node", silent=silent) proc = Execution.run_wait("scp -q \"" + axf_uuid + "\" \"" + apx_uuid + "\" \"" + ais_uuid + "\" " + host + ":") if 0 != proc.retcode: stderr = proc.stderr.strip(' \t\r\n') errmsg = "scp failed: " + stderr - Log.end(errmsg, True) + Log.end(errmsg, True, silent=silent) return AnalyzerResult(has_ff, Dat.ERROR_WCET_PREPARE, True, errmsg, None) else: - Log.end() + Log.end(silent=silent) - Log.start(" -> Run aiT") + Log.start(" -> Run aiT", silent=silent) proc = Execution.run_wait("ssh " + host + " \"" + __A3BIN + " -b " + os.path.basename(apx_uuid) + "\"") if 0 != proc.retcode: stderr = proc.stderr.strip(' \t\r\n') errmsg = "Execution failed: " + stderr - Log.end(errmsg, True) + Log.end(errmsg, True, silent=silent) return AnalyzerResult(has_ff, Dat.ERROR_WCET_EXEC, True, errmsg, None) else: - Log.end() + Log.end(silent=silent) - Log.start(" -> Copy results from remote node") + Log.start(" -> Copy results from remote node", silent=silent) proc = Execution.run_wait("scp " + host + ":" + rpt_uuid + " \"" + out_report + "\"") if 0 != proc.retcode: stderr = proc.stderr.strip(' \t\r\n') errmsg = "scp failed: " + stderr - Log.end(errmsg, True) + Log.end(errmsg, True, silent=silent) return AnalyzerResult(has_ff, Dat.ERROR_WCET_PREPARE, True, errmsg, None) else: - Log.end() + Log.end(silent=silent) - Log.start(" -> Delete files from remote node") + Log.start(" -> Delete files from remote node", silent=silent) proc = Execution.run_wait("ssh " + host + " rm \"" + rpt_uuid + "\" \"" + os.path.basename(ais_uuid) + "\" \"" + os.path.basename(apx_uuid) + "\" \"" + os.path.basename(axf_uuid) + "\"") if 0 != proc.retcode: stderr = proc.stderr.strip(' \t\r\n') errmsg = "ssh failed: " + stderr - Log.end(errmsg, True) + Log.end(errmsg, True, silent=silent) return AnalyzerResult(has_ff, Dat.ERROR_WCET_PREPARE, True, errmsg, None) else: - Log.end() + Log.end(silent=silent) - Log.operation_start(" -> Analyzing result from aiT") + Log.operation_start(" -> Analyzing result from aiT", silent=silent) result, unb_loops = __parse_rpt(out_report) if len(unb_loops) > 0: ubloops = ', '.join(unb_loops) - Log.operation_end(" -> Run aiT: " + str(result) + "cy; " + str(len(unb_loops)) + " loops with unknown bound: " + ubloops, True) + Log.operation_end(" -> Run aiT: " + str(result) + "cy; " + str(len(unb_loops)) + " loops with unknown bound: " + ubloops, True, silent=silent) else: - Log.operation_end(" -> Run aiT: " + str(result) + "cy") + Log.operation_end(" -> Run aiT: " + str(result) + "cy", silent=silent) return AnalyzerResult(has_ff, result, len(unb_loops) > 0 or result < 0, "", unb_loops) -- GitLab From 2ac02e3c4699f36dc991d22721f001473b48716c Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 12:04:32 +0100 Subject: [PATCH 20/97] Bugfix: Use backend provided by parameter instead 'GTK3Cairo' --- Helper/Python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Helper/Python.py b/Helper/Python.py index 986cd4f..a97efad 100644 --- a/Helper/Python.py +++ b/Helper/Python.py @@ -7,7 +7,7 @@ def import_pyplot(backend = None): Log.debug("Importing pyplot with backend " + str(backend)) if None != backend: import matplotlib - matplotlib.use('GTK3Cairo') + matplotlib.use(backend) import matplotlib.pyplot as plt return plt -- GitLab From 2a3acf6ab34bd79421fda68dded3c4ec13298976 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 12:24:42 +0100 Subject: [PATCH 21/97] Cleanup command etime, use proper loading mechanism for pyplot --- Commands/etime.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Commands/etime.py b/Commands/etime.py index ef5fd1b..53f93d7 100644 --- a/Commands/etime.py +++ b/Commands/etime.py @@ -1,21 +1,17 @@ -#!/usr/bin/python3 +#!/usr/bin/python -from multiprocessing import Process, Pipe from tqdm import tqdm import argparse as ap import os import shutil -import pkgutil import sys import uuid -import time import numpy import Queue import threading import multiprocessing -import matplotlib.pyplot as plt -from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Git, XMC4500, Result, EstimateSpill, GenE +from Helper import Log, Tools, Pattern, Execution, XMC4500, GenE, Python from Helper.Frequency import Frequency CPU = "xmc4500" @@ -149,6 +145,9 @@ def get_argparser(): return parser def run(args): + global plt + plt = Python.import_pyplot('Qt5Agg') + gene_rev, gene_mod = GenE.revision() subcmd = os.path.splitext(os.path.basename(__file__))[0] -- GitLab From 4c7c5dd68469ed2361d47073c25b1794c7cc9551 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 14:44:36 +0100 Subject: [PATCH 22/97] Implement dynamic number of columns in Dat --- Helper/Dat.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Helper/Dat.py b/Helper/Dat.py index ec73e44..73fcb01 100644 --- a/Helper/Dat.py +++ b/Helper/Dat.py @@ -47,6 +47,8 @@ class Data: self.inputs = None self.cycles = None + self.__columns = ['inputs', 'cycles'] + self.pattern = None self.seed = None self.cy_seed = None @@ -160,6 +162,9 @@ class Data: else: return errno_to_string(wcet) + def register_column(self, colname): + self.__columns.append(colname) + def __write_header(self, fh): fh.write("# seed: " + str(self.seed) + "\n") fh.write("# cy(seed): " + str(self.cy_seed) + "\n") @@ -170,7 +175,8 @@ class Data: fh.write("# min: " + str(self.min) + "\n") fh.write("# max: " + str(self.max) + "\n") fh.write("# pattern: " + self.pattern + "\n") - fh.write("# input cycles\n") + + fh.write("# " + " ".join(self.__columns) + "\n") def write(self, filepath): self.validate() @@ -178,7 +184,7 @@ class Data: fh = open(filepath, 'w') self.__write_header(fh) for i in xrange(len(self.cycles)): - fh.write(str(self.inputs[i]) + " " + str(self.cycles[i]) + "\n") + fh.write(" ".join([str(getattr(self, col)[i]) for col in self.__columns]) + "\n") fh.close() def write_dref(self, dref_file): -- GitLab From 5d518cc5f689156e7ccb850250b554e36690c2d9 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 14:46:53 +0100 Subject: [PATCH 23/97] Implement searching and using of Red Pitaya(s) --- External/__init__.py | 0 External/redpitaya_scpi.py | 75 +++++++++++++++++++++++++ Helper/RedPitaya.py | 112 +++++++++++++++++++++++++++++++++++++ Helper/Tools.py | 23 ++++++++ 4 files changed, 210 insertions(+) create mode 100644 External/__init__.py create mode 100644 External/redpitaya_scpi.py create mode 100644 Helper/RedPitaya.py diff --git a/External/__init__.py b/External/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/External/redpitaya_scpi.py b/External/redpitaya_scpi.py new file mode 100644 index 0000000..5a46afa --- /dev/null +++ b/External/redpitaya_scpi.py @@ -0,0 +1,75 @@ +"""SCPI access to Red Pitaya.""" + +import socket + +__author__ = "Luka Golinar, Iztok Jeras" +__copyright__ = "Copyright 2015, Red Pitaya" + +class scpi (object): + """SCPI class used to access Red Pitaya over an IP network.""" + delimiter = '\r\n' + + def __init__(self, host, timeout=None, port=5000): + """Initialize object and open IP connection. + Host IP should be a string in parentheses, like '192.168.1.100'. + """ + self.host = host + self.port = port + self.timeout = timeout + + try: + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + if timeout is not None: + self._socket.settimeout(timeout) + + self._socket.connect((host, port)) + + except socket.error as e: + print('SCPI >> connect({:s}:{:d}) failed: {:s}'.format(host, port, e)) + + def rx_txt(self, chunksize = 4096): + """Receive text string and return it after removing the delimiter.""" + msg = '' + while 1: + chunk = self._socket.recv(chunksize + len(self.delimiter)).decode('utf-8') # Receive chunk size of 2^n preferably + msg += chunk + if (len(chunk) and chunk[-2:] == self.delimiter): + break + return msg[:-2] + + def rx_arb(self): + numOfBytes = 0 + """ Recieve binary data from scpi server""" + str='' + while (len(str) != 1): + str = (self._socket.recv(1)) + if not (str == '#'): + return False + str='' + while (len(str) != 1): + str = (self._socket.recv(1)) + numOfNumBytes = int(str) + if not (numOfNumBytes > 0): + return False + str='' + while (len(str) != numOfNumBytes): + str += (self._socket.recv(1)) + numOfBytes = int(str) + str='' + while (len(str) != numOfBytes): + str += (self._socket.recv(1)) + return str + + def tx_txt(self, msg): + """Send text string ending and append delimiter.""" + return self._socket.send((msg + self.delimiter).encode('utf-8')) + + def close(self): + """Close IP connection.""" + self.__del__() + + def __del__(self): + if self._socket is not None: + self._socket.close() + self._socket = None diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py new file mode 100644 index 0000000..681fdcb --- /dev/null +++ b/Helper/RedPitaya.py @@ -0,0 +1,112 @@ +from External import redpitaya_scpi as scpi + +from Helper import Tools, Log + +class RedPitaya: + def __init__(self, ip): + self.ip = ip + self.def_input = 1 + self.max_sampling_rate = 125 * 1000 * 1000 # 125 MS/s + self.rp = scpi.scpi(ip) + self.send('ACQ:DATA:UNITS VOLTS') + + def send(self, string): + assert None != self.rp + self.rp.tx_txt(string) + + def recv(self): + assert None != self.rp + return self.rp.rx_txt() + + def send_recv(self, string): + self.send(string) + return self.recv() + + def set_default_input(self, def_input): + self.def_input = def_input + + def set_trigger(self, source, level, delay_samples=0): + assert source in ['DISABLED', 'NOW', 'CH1_PE', 'CH1_NE', 'CH2_PE', 'CH2_NE', 'EXT_PE', 'EXT_NE', 'AWG_PE', 'AWG_NE'] + + self.send('ACQ:TRIG ' + source) + self.send('ACQ:TRIG:LEV ' + str(level) + ' mV') + self.send('ACQ:TRIG:DLY ' + str(delay_samples)) + + return (self.send_recv('ACQ:TRIG:LEV?'), self.send_recv('ACQ:TRIG:DLY?')) + + def await_trigger(self, inp=None): + if inp == None: + inp = self.def_input + + while True: + if not self.get_trigger_status(): + return (self.get_data(inp), self.get_tpos(), self.get_wpos()) + + def get_trigger_status(self): + return self.send_recv('ACQ:TRIG:STAT?') == 'WAIT' + + def set_gain(self, gain, inp=None): + assert gain in ['LV', 'HV'] + if inp == None: + inp = self.def_input + self.send('ACQ:SOUR' + str(inp) + ':GAIN ' + gain) + + + def get_decimation(self): + return int(self.send_recv('ACQ:DEC?')) + + def set_decimation(self, dec): + assert dec in [1, 8, 64, 1024, 8192, 65536] + self.send('ACQ:DEC ' + str(dec)) + return self.get_decimation() + + def get_data(self, inp=None): + if inp == None: + inp = self.def_input + data_string = self.send_recv('ACQ:SOUR' + str(inp) + ':DATA?') + data_string = data_string.strip('{}\n\r').replace(" ", "").split(',') + return list(map(float, data_string)) + + def get_tpos(self): + return int(self.send_recv('ACQ:TPOS?')) + + def get_wpos(self): + return int(self.send_recv('ACQ:WPOS?')) + + def get_buffer_size(self): + return int(self.send_recv('ACQ:BUF:SIZE?')) + + def start_acquire(self): + self.send('ACQ:START') + + def stop_acquire(self): + self.send('ACQ:STOP') + + def samples_to_time(self, samples, dec=None): + if dec == None: + dec = self.get_decimation() + + return float(samples * dec) / float(125)# * 1000 * 1000) / float(self.max_sampling_rate) + + +def list_devices(): + Log.start("Searching for Red Pitayas") + rps = [] + for mac, ip, host in Tools.udhcpd_get_leases(): + Log.debug("Candidate: " + host + " at " + ip + " with MAC " + mac) + + exp_host = "rp-" + mac.lower().replace(":","")[-6:] + if exp_host != host: + Log.debug(" -> Hostname does not match expected hostname `" + str(exp_host) + "`") + continue + + reachable = Tools.host_is_reachable(ip) + if None != reachable: + Log.debug(" -> Not reachable: " + str(reachable)) + continue + + Log.debug(" -> Hostname matches & is reachable; assuming Red Pitaya") + rps.append(ip) + Log.end(",".join(rps), len(rps) < 1) + + return rps diff --git a/Helper/Tools.py b/Helper/Tools.py index 980f8a5..99977d4 100644 --- a/Helper/Tools.py +++ b/Helper/Tools.py @@ -7,6 +7,7 @@ import uuid import shutil import filecmp import pexpect +import socket import Dat, Execution, Log, Metrics @@ -132,3 +133,25 @@ get_metrics.prog_idep = re.compile(r'^Input dependencies: (\d+)$', get_metrics.prog_loops = re.compile(r'^Total Loops: (\d+)$', re.MULTILINE) get_metrics.prog_nested_loops = re.compile(r'^Nested Loops: (\d+)$', re.MULTILINE) get_metrics.prog_call_chain = re.compile(r'^Longest call chain: (\d+)$', re.MULTILINE) + + +def udhcpd_get_leases(): + output = pexpect.run("dumpleases") + + prog = re.compile(r'^(\S+) +(\S+) +(\S+).+$') + + res = [] + for line in output.split('\r\n')[1:]: + search = prog.search(line) + if search: + res.append( (search.group(1), search.group(2), search.group(3)) ) + + return res + +def host_is_reachable(ip, port = 22): + try: + sock = socket.create_connection( (ip, port) ) + sock.close() + return None + except socket.error as msg: + return str(msg) -- GitLab From 6a1436a0ee5c862cc90c529362d225c0121facd5 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 14:52:56 +0100 Subject: [PATCH 24/97] Remove duplicate --ofactor in bench_input_arm.py --- Commands/bench_input_arm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Commands/bench_input_arm.py b/Commands/bench_input_arm.py index 5e32ba2..9919f43 100644 --- a/Commands/bench_input_arm.py +++ b/Commands/bench_input_arm.py @@ -36,7 +36,6 @@ def get_argparser(): parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking') parser.add_argument('--entry', metavar='function', type=str, default='gene_main', help='entry used for analysis/simulation') parser.add_argument('--opt', metavar='opt', type=int, default=0, help='Optimization level') - parser.add_argument('--ofactor', metavar='ofactor', type=int, default=25, help='Overweighting factor') parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') parser.add_argument('--eic', dest='eic', action='store_true', help='Enable instruction cache') -- GitLab From 9d26b665f601f6a922607c2949db7ed21a2b3a97 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 15:00:37 +0100 Subject: [PATCH 25/97] Improve error message for aiT --- Analyzers/aiT.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Analyzers/aiT.py b/Analyzers/aiT.py index 3e60924..1d40fdc 100644 --- a/Analyzers/aiT.py +++ b/Analyzers/aiT.py @@ -1,12 +1,11 @@ #!/usr/bin/python -from __future__ import print_function import re import os import uuid import shutil -from Helper import Dat, Execution, Log, Metrics +from Helper import Dat, Execution, Log from AnalyzerResult import AnalyzerResult __USER = os.environ.get('CIP_USER') @@ -97,8 +96,10 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silen Log.start(" -> Run aiT", silent=silent) proc = Execution.run_wait("ssh " + host + " \"" + __A3BIN + " -b " + os.path.basename(apx_uuid) + "\"") if 0 != proc.retcode: + Log.debug(str(proc)) stderr = proc.stderr.strip(' \t\r\n') - errmsg = "Execution failed: " + stderr + errmsg = "Execution failed (retcode: " + str(proc.retcode) + ")\n" + stderr + if "" == stderr: errmsg += proc.stdout Log.end(errmsg, True, silent=silent) return AnalyzerResult(has_ff, Dat.ERROR_WCET_EXEC, True, errmsg, None) else: -- GitLab From 0e2189e9be868871b0b5fb84674676f0b09a91d2 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 15:01:06 +0100 Subject: [PATCH 26/97] Implement customizations of plots for dat2stat --- Commands/dat2stat.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index dc35cde..bf25b7c 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -15,6 +15,11 @@ def get_argparser(): parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file(s) to be converted') parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds (default: 1)') + fs_def = matplotlib.rcParams['font.size'] + + parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics (default: false)') + parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used (default: ' + str(fs_def) + ')') + parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds (default: 1)') def __register_color(name, label, default): parser.add_argument('--color-' + name, @@ -34,6 +39,8 @@ def run(args): global plt plt = Python.import_pyplot('pdf') + matplotlib.rcParams.update({'font.size': args.fs}) + ubs = {} wcets = [] mins = {} @@ -105,6 +112,10 @@ def run(args): plt.hist(valid, bins, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) plt.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0.1, alpha=0.5) + if args.no_tic: + plt.gca().axes.get_xaxis().set_ticks([]) + plt.gca().axes.get_yaxis().set_ticks([]) + plt.xlabel('upper bound(' + ana + ')/WCET[%]') plt.ylabel('Occurrences') -- GitLab From b97183531210073b4d969c3f5447a779d38e20cc Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 15:03:46 +0100 Subject: [PATCH 27/97] Rename __jlink_error_check to jlink_error_check --- Helper/XMC4500.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Helper/XMC4500.py b/Helper/XMC4500.py index 3bb1406..bce1df3 100644 --- a/Helper/XMC4500.py +++ b/Helper/XMC4500.py @@ -11,29 +11,29 @@ import Log, Execution USB_DEV='/sys/bus/usb/devices' -def __jlink_error_check(output): - search = __jlink_error_check.prog_con.search(output) +def jlink_error_check(output): + search = jlink_error_check.prog_con.search(output) if search: return "cannot connect to device" - search = __jlink_error_check.prog_fle.search(output) + search = jlink_error_check.prog_fle.search(output) if search: return "failed to open file" - search = __jlink_error_check.prog_fle2.search(output) + search = jlink_error_check.prog_fle2.search(output) if search: return "unknown file format" - search = __jlink_error_check.prog_per.search(output) + search = jlink_error_check.prog_per.search(output) if search: return search.group(1) - search = __jlink_error_check.prog_err.search(output) + search = jlink_error_check.prog_err.search(output) if search: return search.group(1) return None -__jlink_error_check.prog_fle = re.compile(r'^ *Failed to open file.*$', re.MULTILINE) -__jlink_error_check.prog_fle2 = re.compile(r'^ *File is of unknown \/ supported format.*$', re.MULTILINE) -__jlink_error_check.prog_con = re.compile(r'^ *Connecting to J-Link via USB\.\.\.FAILED *$', re.MULTILINE) -__jlink_error_check.prog_err = re.compile(r'^.*\*\*\*\*\*\* Error: (.*) *$', re.MULTILINE) -__jlink_error_check.prog_per = re.compile(r'^.*Error while programming flash: (.*) *$', re.MULTILINE) +jlink_error_check.prog_fle = re.compile(r'^ *Failed to open file.*$', re.MULTILINE) +jlink_error_check.prog_fle2 = re.compile(r'^ *File is of unknown \/ supported format.*$', re.MULTILINE) +jlink_error_check.prog_con = re.compile(r'^ *Connecting to J-Link via USB\.\.\.FAILED *$', re.MULTILINE) +jlink_error_check.prog_err = re.compile(r'^.*\*\*\*\*\*\* Error: (.*) *$', re.MULTILINE) +jlink_error_check.prog_per = re.compile(r'^.*Error while programming flash: (.*) *$', re.MULTILINE) def flash(odir, serial, bin_axf): @@ -71,7 +71,7 @@ def flash(odir, serial, bin_axf): except pexpect.TIMEOUT: return "JLinkExe did not terminate in time" - return __jlink_error_check(output) + return jlink_error_check(output) class XMC4500: def __init__(self, port, serial = None): @@ -240,8 +240,8 @@ class XMC4500: def reset(self): proc = Execution.run_wait("make -C ./xmc4500 reset_" + self.serial) - __jlink_error_check(proc.stdout) - Log.debug(str(proc)) + error = jlink_error_check(proc.stdout) + Log.debug(str(proc) + " had error " + str(error)) time.sleep(2) def try_read_file(path): -- GitLab From d6ad13f0efb97fa53bbcc274012ff9484015ff83 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 15:13:56 +0100 Subject: [PATCH 28/97] Add first version of demo application (TODO) --- Commands/bench_input_arm_demo.py | 286 +++++++++++++++++++++++++++++++ Gui/DemoWindow.py | 40 +++++ Gui/SamplingWidget.py | 179 +++++++++++++++++++ Gui/__init__.py | 0 data/img/checkmark.png | Bin 0 -> 2772 bytes data/img/disabled.png | Bin 0 -> 5273 bytes data/img/error.png | Bin 0 -> 2344 bytes data/img/working.png | Bin 0 -> 12637 bytes 8 files changed, 505 insertions(+) create mode 100644 Commands/bench_input_arm_demo.py create mode 100644 Gui/DemoWindow.py create mode 100644 Gui/SamplingWidget.py create mode 100644 Gui/__init__.py create mode 100644 data/img/checkmark.png create mode 100644 data/img/disabled.png create mode 100644 data/img/error.png create mode 100644 data/img/working.png diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py new file mode 100644 index 0000000..0b398b0 --- /dev/null +++ b/Commands/bench_input_arm_demo.py @@ -0,0 +1,286 @@ +#!/usr/bin/python + +from multiprocessing import Process, Pipe +import argparse as ap +import os +import shutil +import sys +import time +import numpy +import threading +import tempfile +from thread import start_new_thread + +from PyQt5 import QtWidgets +from Gui.DemoWindow import DemoWindow + +from Helper.Frequency import Frequency +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, XMC4500, Result, GenE, Python +import Analyzers + +COUNT_BMS = 10 + +CPU = "xmc4500" +CPU_TYPE = "cortex-m4" + +CHUNK_SIZE = 1000 + +def hist_to_file(cycles, idx, width, height, analyzers): + fle = tempfile.mktemp(suffix='.png') + + import matplotlib + matplotlib.use('Agg') + fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) + fig.set_alpha(0.0) + axes = fig.add_subplot(111) + + fig.subplots_adjust(hspace=0, wspace=0) + + fig.tight_layout(pad=0, h_pad=0, w_pad=0) + fig.set_tight_layout(True) + axes.yaxis.grid(True) + fig.patch.set_visible(False) + + # uncomment this line to remove the plot's background + #axes.patch.set_visible(False) + + n, bins, patches = axes.hist(cycles[0:idx], bins=500, facecolor='green', alpha=0.5) + + ana_y = axes.get_ylim()[0] + 0.8*(axes.get_ylim()[1] - axes.get_ylim()[0]) + + minval = numpy.amin(cycles[0:idx]) + axes.axvline(x=minval, ymin=0, ymax=1, linewidth=2, color='m') + axes.text(minval, ana_y, ' BOET', fontsize='xx-large', ha='left', va='bottom', color='m') + + maxval = numpy.amax(cycles[0:idx]) + axes.axvline(x=maxval, ymin=0, ymax=1, linewidth=2, color='g') + axes.text(maxval, ana_y, 'WCET', fontsize='xx-large', ha='right', va='bottom', color='g') + + for name, result in analyzers.iteritems(): + if result > 0: + axes.axvline(x=result, ymin=0, ymax=1, linewidth=2, color='m') + axes.text(result, ana_y, ' ' + name, fontsize='xx-large', ha='left', va='bottom', color='m') + + fig.tight_layout(pad=0, h_pad=0, w_pad=0) + fig.set_tight_layout(True) + fig.savefig(fle, transparent=True, dpi=100) + + return fle + +def benchmark_worker(args, sw, xmc, no, seed): + odir = "results/aladdin_rtas17_demo/benchmark" + str(no) + + if not os.path.exists(odir): + os.makedirs(odir) + + ll = odir + '/bench.ll' + ll_comb = odir + '/combined.ll' + o = odir + '/bench.o' + pml = odir + '/bench.pml' + axf = odir + '/bench.axf' + + dat = Dat.Data() + dat.seed = seed + dat.pattern = Pattern.assemble_string(args.suite) + + ## generate the benchmark + sw.update_message.emit("Generating Benchmark...") + bm = GenE.get_benchmark(Pattern.PATTERN[args.suite], CPU_TYPE, args.cost, args.bits, seed, ll, opt=0, of=args.ofactor) + Tools.llvm_lls_to_ll([ll], ll_comb) + Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) + + ## compile the benchmark OS + sw.update_message.emit("Compile Benchmark...") + o_xmc = "./xmc4500/gene_seed" + str(seed) + ".o" + shutil.copyfile(o, o_xmc) + Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(seed) + ".axf", None, 0) + shutil.move("./xmc4500/bin/system_gene_seed" + str(seed) + ".axf", axf) + os.remove(o_xmc) + + ## flash to devices + sw.update_message.emit("Flash Benchmark...") + success, errors = xmc.flash_retry(odir, axf) + if not success: + Log.fail("Flashing to " + str(xmc) + " failed: " + ", ".join(errors)) + return + else: Log.debug("Successfully flashed to " + str(xmc)) + + time.sleep(2) + + inp = Benchmark.get_input(args.bits, seed, args.samples, 1)[0] + # get the total number of samples, which does not + # equal args.samples due to special input values + sample_count = len(inp) + + procs = [] + pipe_parent, pipe_child = Pipe() + p = Process(target=benchmark_input_run, args=(pipe_child, args, inp, xmc, args.eic, sw)) + p.start() + procs.append({'proc':p, 'pos':0, 'pipe':pipe_parent, 'input':inp}) + + Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) + + pipe_ana, pipe_child = Pipe() + proc_ana = Process(target=analyzers_run, args=(pipe_child, args, odir, dat, axf, pml)) + proc_ana.start() + + + analyzers = ['aiT', 'platin'] + analyzer_disabled = ['platin'] + + sw.update_message.emit("Sampling...") + + dat.inputs = numpy.zeros(sample_count, numpy.uint32) + dat.cycles = numpy.zeros(sample_count, numpy.uint32) + + sw.samples = dat.cycles + sw.samples_lock = threading.Lock() + + idx = 0 + finished = 0 + analyzer_results = {} + while True: + samples_changed = False + + for proc in procs: + if None == proc['proc']: continue + + fle, pos = proc['pipe'].recv() + if None == fle: + proc['proc'].join() + proc['proc'] = None + continue + + sw.update_sampling.emit(fle) + sw.update_message.emit(str(pos) + " samples") + sw.update_progress.emit(0, args.samples, pos) + + ana_changed = False + proc = procs[0] + while pipe_ana.poll(): + ana_changed = True + name, result = pipe_ana.recv() + analyzer_results[name] = result + proc['pipe'].send( (name, result.cy) ) + + if ana_changed: + for name in analyzers: + if name in analyzer_results: + result = analyzer_results[name] + if result.cy > 0: + #maxval = numpy.amax(dat.cycles[0:idx]) # TODO + sw.update_analyzer.emit(name, 'checkmark.png', name + ': ' + str(result.cy) + 'cy') + else: + msg = name + ': ' + if None != result.error_msg and "" != result.error_msg: + msg += result.error_msg + else: + msg += Dat.errno_to_string(result.cy) + sw.update_analyzer.emit(name, 'error.png', msg) + elif name in analyzer_disabled: + sw.update_analyzer.emit(name, 'disabled.png', name) + else: + sw.update_analyzer.emit(name, 'working.png', name) + + time.sleep(0.01) + Log.operation_end("Wait for Sampling") + + # get rid of unused arrays + del inp + del procs + + dat.write(odir + '/result.dat') + + # check that worst-observed execution time is triggered by worst-case input + assert dat.max == dat.cy_seed, "Maximum execution cycles exceeds cycles used for seed" + + +def worker(args, dw): + odir = "results/aladdin_rtas17_demo" + + if not os.path.exists(odir): + os.makedirs(odir) + + Log.set_logfile(odir + '/aladdin.log') + + xmcs = XMC4500.list_devices(odir, args.xmccache) + if None == xmcs: + return + + for i in xrange(0, min(len(xmcs), COUNT_BMS)): + start_new_thread(benchmark_worker, (args, dw.sws[i], xmcs[i], i, 1431655765 + i)) + + while True: continue + + +def benchmark_input_run(pipe, args, input_array, xmc, eic, sw): + xmc.connect() + xmc.set_ic(eic) + + analyzers = {} + + cycles = numpy.zeros(len(input_array), numpy.uint32) + + for i in xrange(0, len(input_array)): + cycles[i] = xmc.benchmark(input_array[i]) + assert cycles[i], "Invalid value from benchmark()" + + if 0 == i % CHUNK_SIZE and 0 < i: + while pipe.poll(): + name, res = pipe.recv() + if None != name: + analyzers[name] = res + + fle = hist_to_file(cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers) + pipe.send( (fle, i) ) + + while True: + name, res = pipe.recv() + if None != name: + analyzers[name] = res + + fle = hist_to_file(cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers) + pipe.send( (fle, len(input_array)) ) + + pipe.send( (None, None) ) + pipe.close() + +def analyzers_run(pipe, args, odir, dat, axf, pml): + for name, res in Analyzers.run(args, odir, dat, CPU, axf, "gene_main", pml, None, args.eic, run_ff = True, run_noff = False): + pipe.send( (name, res) ) + + pipe.close() + + +def get_argparser(): + parser = ap.ArgumentParser(prog=sys.argv[0] + " bench_input", + formatter_class=ap.ArgumentDefaultsHelpFormatter, + description='Subfunction bench_input_arm_demo: ' + desc) + + parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking') + parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') + + parser.add_argument('--eic', dest='eic', action='store_true', help='Enable instruction cache') + parser.add_argument('--no-sampling', dest='nosampling', action='store_true', help='Disable sampling') + + GenE.add_arguments(parser) + Analyzers.add_arguments(parser) + + return parser + +def run(args): + global plt + plt = Python.import_pyplot('Agg') + + qApp = QtWidgets.QApplication(sys.argv) + + dw = DemoWindow(COUNT_BMS, 100) + dw.setWindowTitle("Aladdin: RTAS Demo '17") + dw.show() + + start_new_thread(worker, (args, dw)) + + sys.exit(qApp.exec_()) + + +desc = "[ARM] Benchmark GenE using varying values as input to the generated executable" diff --git a/Gui/DemoWindow.py b/Gui/DemoWindow.py new file mode 100644 index 0000000..630baf2 --- /dev/null +++ b/Gui/DemoWindow.py @@ -0,0 +1,40 @@ +from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import QObject, pyqtSignal + +from SamplingWidget import SamplingWidget + +class DemoWindow(QtWidgets.QMainWindow): + def __init__(self, sw_count, initial_ylim): + QtWidgets.QMainWindow.__init__(self) + + self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + + self.setStyleSheet("QMainWindow {background: 'white';}") + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setWindowTitle("RTAS Demo '17") + + self.main_widget = QtWidgets.QWidget(self) + self.main_layout = QtWidgets.QHBoxLayout(self.main_widget) + + + self.bm_layouts = [] + self.sws = [] + layout = -1 + for i in xrange(0, sw_count): + self.sws.append( SamplingWidget(self, initial_ylim) ) + if 0 == i % 5: + self.bm_layouts.append( QtWidgets.QVBoxLayout() ) + self.main_layout.addLayout(self.bm_layouts[-1]) + self.main_layout.setStretchFactor(self.bm_layouts[-1], 1) + + self.bm_layouts[-1].addWidget(self.sws[-1]) + self.bm_layouts[-1].setStretchFactor(self.sws[-1], 1) + + + test = SamplingWidget(self, initial_ylim) + self.main_layout.addWidget(test) + + self.main_layout.setStretchFactor(test, 3) + + self.setCentralWidget(self.main_widget) + self.main_widget.setFocus() diff --git a/Gui/SamplingWidget.py b/Gui/SamplingWidget.py new file mode 100644 index 0000000..73c6fb5 --- /dev/null +++ b/Gui/SamplingWidget.py @@ -0,0 +1,179 @@ +from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import QObject, pyqtSignal + +from Helper import Log + +import multiprocessing + +class ResizeLabel(QtWidgets.QLabel): + def __init__(self, mpWidth, mpHeight): + QtWidgets.QLabel.__init__(self) + + self.setMinimumSize(1, 1) + #self.setScaledContents(True) + + self.mpWidth = mpWidth + self.mpHeight = mpHeight + + self.mpWidth.value = self.size().width() + self.mpHeight.value = self.size().height() + + def resizeEvent(self, evt): + self.mpWidth.value = evt.size().width() + self.mpHeight.value = evt.size().height() + +class SamplingWidget(QtWidgets.QFrame): + update_message = pyqtSignal('QString') + def handle_update_message(self, message): + self.message.setText(message) + + update_sampling = pyqtSignal('QString') + def handle_update_sampling(self, fle):#, analyzers): + pixmap = QtGui.QPixmap(fle) + self.sampling.setPixmap(pixmap)#.scaledToWidth(self.sampling.width())) + + update_progress = pyqtSignal(int, int, int) + def handle_update_progress(self, mi, ma, pr): + self.progress.setMinimum(mi) + self.progress.setMaximum(ma) + self.progress.setValue(pr) + + update_analyzer = pyqtSignal('QString', 'QString', 'QString') + def handle_update_analyzer(self, analyzer, img, message): + if not analyzer in self.analyzers: + icon = QtWidgets.QLabel() + label = QtWidgets.QLabel() + self.analyzers[analyzer] = (icon, label, img, 42) + + label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + label.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + + icon.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum) + + layout = QtWidgets.QHBoxLayout() + layout.addWidget(icon) + layout.addWidget(label) + + self.analyzer_layout.addLayout(layout) + + icon, label, _, height = self.analyzers[analyzer] + if None != icon.pixmap(): + height = icon.pixmap().height() + self.analyzers[analyzer] = (icon, label, img, height) + icon.setPixmap(QtGui.QPixmap("aladdin/data/img/" + img).scaledToWidth(height)) + label.setText(message) + + def enterEvent(self, evt): + if not self.maximized: + self.setStyleSheet(".SamplingWidget { background: '#FFFDDA'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + + def leaveEvent(self, evt): + self.setStyleSheet(".SamplingWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + + def mousePressEvent(self, evt): + #Log.info("mousePressEvent for " + str(self)) + #Log.info("parent: " + str(self.parent())) + #Log.info("win: " + str(self.win)) + + if self.maximized: + self.maximized = False + for sw in self.win.findChildren(SamplingWidget): + sw.show() + else: + self.maximized = True + for sw in self.win.findChildren(SamplingWidget): + if self != sw: sw.hide() + + + def resizeEvent(self, evt): + self.__set_font(evt.size().height() / 25) + self.__set_icon(evt.size().height() / 25) + + def __set_font(self, size): + newfont = QtGui.QFont("sans-serif", weight=QtGui.QFont.Bold) + newfont.setPixelSize(size) + + import matplotlib + matplotlib.rcParams.update({'font.size': size}) + + self.message.setFont(newfont) + self.progress.setFont(newfont) + for _, label, _, _ in self.analyzers.itervalues(): + label.setFont(newfont) + + def __set_icon(self, size): + for icon, _, img, _ in self.analyzers.itervalues(): + icon.setPixmap(QtGui.QPixmap("aladdin/data/img/" + img).scaledToWidth(size)) + + def __init__(self, parent, initial_ylim): + QtWidgets.QWidget.__init__(self) + self.win = parent + + self.mpWidth = multiprocessing.Value('i', 0) + self.mpHeight = multiprocessing.Value('i', 0) + + self.maximized = False + + self.setStyleSheet(".SamplingWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + + self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.updateGeometry() + + self.update_message.connect(self.handle_update_message) + self.update_sampling.connect(self.handle_update_sampling) + self.update_progress.connect(self.handle_update_progress) + self.update_analyzer.connect(self.handle_update_analyzer) + + self.main_widget = QtWidgets.QWidget(self) + self.main_layout = QtWidgets.QVBoxLayout(self.main_widget) + self.main_layout.setContentsMargins(0, 0, 0, 0) + + self.main_widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + + hlayout = QtWidgets.QHBoxLayout() + + vlayout_topleft = QtWidgets.QVBoxLayout() + margin = vlayout_topleft.contentsMargins() + margin.setLeft(50) + vlayout_topleft.setContentsMargins(margin) + hlayout.addLayout(vlayout_topleft) + + self.analyzer_layout = QtWidgets.QVBoxLayout() + hlayout.addLayout(self.analyzer_layout) + + self.main_layout.addLayout(hlayout) + + self.message = QtWidgets.QLabel() + self.message.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + vlayout_topleft.addWidget(self.message) + + self.progress = QtWidgets.QProgressBar() + self.progress.setFormat("%v/%m samples") + + vlayout_topleft.addWidget(self.progress) + + self.sampling = ResizeLabel(self.mpWidth, self.mpHeight) + self.sampling.setMargin(0) + self.sampling.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + #self.sampling.setScaledContents(True) # no + #self.sampling = SamplingPlot(initial_ylim, self.main_widget, width=5, height=4, dpi=100) + self.main_layout.addWidget(self.sampling) + + self.main_widget.setFocus() + #self.setCentralWidget(self.main_widget) + + self.analyzers = {} + self.samples = None + self.samples_lock = None + + for name in ['aiT', 'platin']: + self.handle_update_analyzer(name, 'working.png', name) + + QtWidgets.QWidget.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + QtWidgets.QWidget.updateGeometry(self) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(self.main_widget) + + self.__set_font(self.height() / 50) + self.__set_icon(self.height() / 25) diff --git a/Gui/__init__.py b/Gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/img/checkmark.png b/data/img/checkmark.png new file mode 100644 index 0000000000000000000000000000000000000000..c7dae54880608e8e40512d66e53d05bb949b16cb GIT binary patch literal 2772 zcmV;_3M=)AP)6BN=VSmg`E zG8p{Z?D@J#6dgigF9Ly(B)o=MU+S{JXi6PB+~e?~M-`0Y!wHdcGnT3}W%1%L+%m0# z@whi(hx^wIhNhGe?YSBcjH+M|2z#7!uxLgP2166JRs2Wf_pgQVvn?UacQgx(CX}&y z@_byesD*LQn6PEyS1eU&Vrm=V?Ts9ZvKknb0z%5@41`Z&sfu?X;giPW^so|89Y1eP z_(z~K3liTJ>gS4dyVvz#+%_R7q~EgO@IRXxf;9UF^Q)b*?wJcR_ZYmHQ zI0zQM{R~lF|9YS0SiHf_-_K`3;!_#xy-l&t`9?iu5D1062MO1csDx@56H@o5WY>tL%m2PNI8fjTY?evJ)s8@A@)OBtbl+Su)yjF);1VLUY; zZ1h;gVDM=%(gS$hyCIBnZNe_m4Hh6il%bFUI0D2`3wdhFe1G~IA-583WM+gZR#e$}880)?J;d`fYR0^7gQD97n6E9+^O5HG41e)W>$h)WzG@q(c9M9s< z^RyTYb;1CE17mJBTA!-xnM2F5X0ni}LoF~os*Zo1Wr!r<6}-}G9>#;dgb1&2219-7 z5C8!7x*Tm0MyU?rhmSJ=0t2E>OK{)QXtD5p&#`!oFuzd@gu2F!Ase)?ef+j2i-mRl zQ6Iu450Xi=+y>>qm#U(GULLF8H;p?)?< zb9f9=!jBIptn;yE>fky}g-oq_=oaqrt4onh@se$^yx$9Tp(b?J%AgC;mDFE2vz8;VJD5s37ZuN4k2pGqL$)!bj z#iPJjc^C!8gqU}=DrPDwqqehf5$>2M52H+naA3@p_B6%q*v}ZwO$DOl^C9|m6)Xys z|Nd38ViXw9Vplg;<3@z{pKxJ_^2#6(ih2?vykb}|xq`7tCPC)VH*JDZszXS2$z#Ff z%GUZg;h00)ltCa|v@Ao6YzQllaFV9H9Prae#chUB)PoQ!34>L1JBQJpPp)>%Fu_kF z9x7EVtmgv4?s4}ZS=x>l#9Jhnv2A=1BD@U|9H7-;O4(jueD_ITBngX}ZGdjcPYok% z^ssE-7vN4&F!BZx&Rai2q9__6{v+FsYO4SL$^ZlW=$MT%owmbxYCzcG{%!joH^L_w zAy6r0nd!LGOh;e2R@Dw^Cx+)>ozD_1_|%ZoSy}Dd zWhg|+^lJ~ia*0uGu2_- z?@QP$S=vAxd`wxr=C+&)mC|Ze?{vWmpO6|SyImotD9Y%!~A!T%C z$1jc4?&;yD)XkpCcbj%)TfTrodI+n0`?VomdLq~SErv3ukmk zvhcXlT^rSv)O1@yvP&X1$VcMqPrJ3Knh0+pPTCPCFbaDUBD@uyuW-10swIx9P*HuQ zOSs;z1pA!R5#s0D07~^SBw!rQS-<22MyU?rpi@?dsL!==w)0t&+Yu?_`2z?EZZ{Dp zcE>uO?lu1>M%E9<>{Y8z)%9HSH(2ZA(0R+)C!_oUtLS_37eU>+NxV#@S%3idWqMT-4dZtEIF` zo*T$<)vQwEXP#l3hg|cRuHD9?kODLRB0&6vgjQ9A)#d4Sgz!lt!R(zanQnp<#}^ub z@z98{ZNjpyJ#I8wDwj*^y@iY~PjS46mHzDIT8%>4=e!-o-T8x&ZCQyhUoivF6qKN; zmN+rIh!+_5`w}*JboxzTRBP{9k$el62Q5VY0QH__xi;|2;7kZ;F;hZoQrB+RRJ_(z zpE`2QOA+QL>8&i9^D7p`*yu5etvT9Wd^E*bn+i@~ywW2?c`6tTRiO0T1;mM$s$3C% zt4&B2-iCEFdlRY3Hsx>KuzTDCm4I9rLQqKA)_R`AWgauq8yw?O3W(mxcKG;L_mM_f;jh z1u+=vwgk6m6t-f)c)%$iA^yD>40Wfj;6Oc`7=F8180Uu)LjA|HfbliJM|jP^jebqP zv1Z%&#Vl3v4o8;W;AU2ZfN*3)7DD~>7!1Bu5-B&tf9wnEJ#&&}MF{g_sY+u|qCKYL zkx^w$vqFq4f(1vDP`s9CBTDo literal 0 HcmV?d00001 diff --git a/data/img/disabled.png b/data/img/disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..b61bc3d82895592d70ef89e333aa7b59b97b5cbf GIT binary patch literal 5273 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 G(EkH5nSY7^ literal 0 HcmV?d00001 diff --git a/data/img/error.png b/data/img/error.png new file mode 100644 index 0000000000000000000000000000000000000000..b44d9e31f1e3f6cc6fa179a00d3fc71052b1eeff GIT binary patch literal 2344 zcma*oeNYo;9tZHX_6k<0#gnKA=w-y}tZN7gBIm*Jk|Id)1$c;0I5Y`?WJ3}Ni3q3I zT180uBDBdB+rkJ2Pm@K!Y@$$6Pa-c;6p+vh)$2vMn}dN4T2wfYdlvt{X=XmZXY$QH z`#iJz%rE78ljFVL_In${Fz$n3BUP!f+&qc0*b8tbQT{t7k5x;A8ktJDE>)(GU?IWW z5F$972V`1Qt^)zD(%g^eDTh*b)ON?EY5VBW%`zX? zmc6@ND3W&F?VWq&xlcu5n8${Mxaia)uJMOU)x%*cP6>nP-wvIVw^C;5{2I#tv)Yww zSKd4}yPlK&;q?1&B)l7*V^3)fte&40d$hJ4ou5>Ad>2^LI`JP|o^-ILWn#KQ#v7Qi zvdLrcdRwD4lFH01g3IE~FXoOP*9PP~nt3`Wo%;FS@s`g*Ra4;f;smoj!e0vC!z-O# zx_Yh6E&Z0_!@qzP)NT~fzOiF2U7c3WyWc|BN*@Xw@`nPhwbW8ems&jGZP0{FLNTa7 z`ru=bk2Jv)un(z#1AvR4QpGB=4eT ztxrH33<8-*HI#rzqy(BkAd(43z;h%Lh5{!N2(`e5Jcl$eB2G934kI>50Du^w0)!!l zAq7|nfa72W34?yXg|J`*XhCKm1ZNQ!EC$6$3p@)Tau&9L2&5RgfFAZfuAPZ24 z9}EKuWE=uOAQU_drVs@*0vaKp4QP=m=men%4WEM%L<<9f2?>RfK!S|GOb~>aU8AJIw)GZ#~QF?Mb3cFnYeMJxch>yWqsV_|w*IkC#V7 zdw8caYP%daGC;C<_Uack-p@YPkRx}_{HXqN_{tCP+6U@6aP2@9WjOtoO=O5K{3|Rv zsT6>sGlmNWk8Zov(DcrF>XY=pH>8#NbFCq{xnp4U^h(Om_q9xN&X9kLsXqT^D(%J8 zv=F9Fr8CuVjH#h@OdafGs&+z6Q38Y1WXQm&Cr}nNl!#vgu#4_bY%l;dWv!I||DTpt zRZV}iW^c_;<<{b&XmY%|bwY7(&8iRN^G@x(){Z*of2*^31FjousldW;>R{B7AD-kj zyT$|e!nsdRYxS?D3y0=jI-{f&*5vK7N~g&;LI3h)p~BI}ZsY$5_gTv>TV4P!!Y%4N z;~TBN+7p)RdzqJUGvJEO$u99*W!DX|OEhPjbr0Ajz2De$1MHHGHCJ@~Y~`)5?7BX- za-xFIlls^{?Xt|$Wowq3z$4gBHK_4}gnPs2a(oZrem)z5M-lE@pYO*xgu9|vjjtu# zLfQ{sM!3115%`}8_o88l%L#Yicrh*@L>vF8H<(ylW$pLf$@*1U{iMXi`=nE5Z|-B4 za%B7iQXlbkjpY{_9aN^vv~P_01O6vM)M=_W)U&veX+w#7^s9Qz3KN=k)M@WW)5-;W z8Jae#u(;^5-*__3rJeNty*Z2!$@Hc)bY9=-bY9BT^S4P9X=a`!_=^3~0NePuh(9Rx zk*_>v>88=iW_n8dB|g(qL`#}bM`1NRa-oL8+noFY^^+o)K+jTHunmMzo_M*@ZiqwY zJoD~9$V|MOOve9)^pX%|5bq}wQI?21$qbZb;4U%=Wl8uWl8-XJ{!r+7Uq$G7M`O#7 z&fYS_^b;S8k`4z$&}*Tmm{^X{{d$Sk>E?e+OTl{!Z6;?hXMfvsZqrv|=~sUq+vHJM x??8Q}3|wd3y)?vz$9e_dOpoqV$$Hc07vEgrnHsaICW-lDPT(cSHN+e`@joV#7nuM6 literal 0 HcmV?d00001 diff --git a/data/img/working.png b/data/img/working.png new file mode 100644 index 0000000000000000000000000000000000000000..bf836768fc9bfca89c3f782386cdc262d04feed8 GIT binary patch literal 12637 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 PKb?_*nf^OnhuHrEATvXk literal 0 HcmV?d00001 -- GitLab From beafe6d953997f1a0f18f45c81c4ead61a9faea9 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 2 Mar 2017 15:15:40 +0100 Subject: [PATCH 29/97] Add first version of measurement via Red Pitaya (TODO) --- Commands/bench_input_arm_redpitaya.py | 317 ++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 Commands/bench_input_arm_redpitaya.py diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py new file mode 100644 index 0000000..42c0dee --- /dev/null +++ b/Commands/bench_input_arm_redpitaya.py @@ -0,0 +1,317 @@ +#!/usr/bin/python + +from multiprocessing import Process, Pipe +import multiprocessing +import argparse as ap +import os +import shutil +import sys +import time +import numpy +import Queue + +from Helper.Frequency import Frequency +from Helper import RedPitaya, Benchmark, Dat, Log, Tools, Pattern, Execution, Git, XMC4500, Result, EstimateSpill, GenE, Python +import Analyzers + +CPU = "xmc4500" +CPU_TYPE = "cortex-m4" + +__tlevel_low_high = 0.2 +__tlevel_high_low = 2.5 + +def benchmark_input_run(args, osci_queue, pipe, input_array, xmc, rp, eic): + Log.debug("Trying to connect to XMC4500...") + + xmc.connect() + xmc.set_ic(eic) + + Log.debug("Connection to XMC4500 established!") + + cycles = numpy.zeros(len(input_array), numpy.uint32) + + buf_size = rp.get_buffer_size() + offset = - ( int(buf_size) / 2 - 1) + Log.debug("Buffer Size: " + str(buf_size) + " => Offset: " + str(offset)) + + for i in xrange(len(input_array)): + Log.debug("set_trigger: " + str( rp.set_trigger('CH' + str(args.pitaya_in) + '_NE', 1000*__tlevel_high_low, offset) )) + + while True: + rp.set_gain('HV') + rp.set_decimation(64) + + rp.start_acquire() + cycles[i] = xmc.benchmark(input_array[i]) + + if not rp.get_trigger_status(): + buff = rp.get_data() + rp.stop_acquire() + break + + Log.warn("Repeating measurement for input " + str(input_array[i])) + + osci_queue.put( (i, buff) ) + + Log.debug("Sending poison pill to " + str(multiprocessing.cpu_count()) + " threads...") + for i in xrange(0, multiprocessing.cpu_count()): + osci_queue.put( (None, None) ) + + pipe.send(cycles) + pipe.close() + + +def __osci_analyze(osci_queue, pipe): + results = {} + + while True: + i, buff = osci_queue.get() + + if None == buff: + Log.debug("__osci_analyze: Exiting processing loop...") + break + + j = len(buff) - 1 + is_high = False + start = -1 + end = -1 + + while j >= 0: + val = buff[j] + + if val > __tlevel_high_low and not is_high: + Log.debug("__osci_analyze: Found falling edge at position " + str(j)) + end = j + is_high = True + + if val < __tlevel_low_high and is_high: + Log.debug("__osci_analyze: Found rising edge at position " + str(j)) + start = j + break + + j -= 1 + + #plot.plot(buff) + #plot.ylabel('Voltage') + #plot.show() + + results[i] = (end - start, buff) + #results[i] = rp.samples_to_time(end - start) * float(120) + + Log.debug("Sending results...") + pipe.send(results) + pipe.close() + +def get_argparser(): + parser = ap.ArgumentParser(prog=sys.argv[0] + " bench_input", + formatter_class=ap.ArgumentDefaultsHelpFormatter, + description='Subfunction bench_input_arm: ' + desc) + + parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking') + parser.add_argument('--entry', metavar='function', type=str, default='gene_main', help='entry used for analysis/simulation') + parser.add_argument('--opt', metavar='opt', type=int, default=0, help='Optimization level') + parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') + parser.add_argument('--pitaya', metavar='pitaya', type=str, default=None, help='IP of the RedPitaya') + parser.add_argument('--pitaya-in', metavar='pitaya_in', type=int, default=1, help='RedPitaya: Input used') + + parser.add_argument('--eic', dest='eic', action='store_true', help='Enable instruction cache') + parser.add_argument('--no-metrics', dest='nometrics', action='store_true', help='Disable computing metrics') + parser.add_argument('--no-espill', dest='noespill', action='store_true', help='Disable estimation of spilling instructions') + + GenE.add_arguments(parser) + Analyzers.add_arguments(parser) + + return parser + +def run(args): + global plt + plt = Python.import_pyplot('GTK3Cairo') + + gene_rev, gene_mod = GenE.revision() + + subcmd = os.path.splitext(os.path.basename(__file__))[0] + odir = Result.get_dirname(subcmd, args.cost, args.seed, args.bits, args.samples, gene_rev, args.suite, args.opt, args.eic, args.ofactor) + + if not os.path.exists(odir): + os.makedirs(odir) + + Log.set_logfile(odir + '/aladdin.log') + + if gene_mod: + Log.warn("Your GenE repo is modified!") + + dat = odir + '/bench.dat' + ll = odir + '/bench.ll' + ll_comb = odir + '/combined.ll' + o = odir + '/bench.o' + pml = odir + '/bench.pml' + axf = odir + '/bench.axf' + + xmcs = XMC4500.list_devices(odir, args.xmccache) + if None == xmcs: + return + + if args.pitaya == None: + rps = RedPitaya.list_devices() + args.pitaya = rps[0] + + dat = Dat.Data() + dat.seed = args.seed + dat.pattern = Pattern.assemble_string(args.suite) + + bm = GenE.get_benchmark(Pattern.PATTERN[args.suite], CPU_TYPE, args.cost, args.bits, args.seed, ll, args.opt, args.ofactor) + Log.operation_start(" * Compile Benchmark") + Tools.llvm_lls_to_ll([ll], ll_comb) + Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, args.opt) + Log.operation_end(" * Compile Benchmark") + + + bm.print_patterninfo() + bm.print_loopinfo() + + if not args.nometrics: + Log.operation_start("Calculate metrics") + metrics = bm.get_metrics() + Log.operation_end("Calculate metrics") + if len(metrics) == 0: + Log.fail("Found no metrics") + + funcs = metrics.keys() + funcs.sort() + for func in funcs: + Log.info( metrics[func].get_summary() ) + + Log.operation_start("Build system for XMC4500") + shutil.copyfile(o, "./xmc4500/gene.o") + Execution.run_wait("make -C ./xmc4500 clean bin/system_gene.axf", None, 0) + shutil.copyfile("./xmc4500/bin/system_gene.axf", axf) + Log.operation_end("Build system for XMC4500") + + Log.info("Flash system to XMC4500s") + for xmc in xmcs: + success = False + for retry in xrange(1, 4): + Log.start(" * Flash system to " + xmc.serial + " (try " + str(retry) + ")") + error = xmc.flash(odir, "./xmc4500/bin/system_gene.axf") + Log.end(error, error != None) + + if error == None: + success = True + break + + if not success: + Log.fail("Failed to flash to device, aborting") + return + + time.sleep(2) + + Log.start("Start " + str(len(xmcs)) + " Sampling Threads") + input_chunks = Benchmark.get_input(args.bits, args.seed, args.samples, len(xmcs)) + + # get the total number of samples, which does not + # equal args.samples due to special input values + sample_count = sum(len(chunk) for chunk in input_chunks) + + rp = RedPitaya.RedPitaya(args.pitaya) + rp.set_default_input(args.pitaya_in) + rp.set_gain('HV') + rp.set_decimation(8) + + buf_size = rp.get_buffer_size() + offset = - ( int(buf_size) / 2 - 1) + Log.debug("Buffer Size: " + str(buf_size) + " => Offset: " + str(offset)) + rp.set_trigger('CH' + str(args.pitaya_in) + '_NE', __tlevel_high_low, offset) + + manager = multiprocessing.Manager() + osci_queue = manager.Queue() + + pipe_xmc, pipe_child = Pipe() + proc_xmc = Process(target=benchmark_input_run, args=(args, osci_queue, pipe_child, input_chunks[0], xmcs[0], rp, args.eic)) + proc_xmc.start() + + procs = [] + for i in xrange(0, multiprocessing.cpu_count()): + pipe_parent, pipe_child = Pipe() + proc = Process(target=__osci_analyze, args=(osci_queue, pipe_child)) + proc.start() + procs.append({'proc':proc, 'pipe':pipe_parent}) + + Log.end() + + Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) + + Analyzers.run(args, odir, dat, CPU, axf, args.entry, pml, None, args.eic) + + if not args.noespill: + spills = EstimateSpill.run(pml) + for func, spill in spills.iteritems(): + Log.info(str(spill)) + + Log.operation_start("Run CFRG 1:1 Check") + oo = Tools.oocheck_execute(pml, args.entry) + + nonoo = ', '.join( [func for func, is_oo in oo.items() if not is_oo] ) + + if nonoo != "": + Log.operation_end("Run CFRG 1:1 Check: " + nonoo, True) + else: + Log.operation_end("Run CFRG 1:1 Check") + + + Log.operation_start("Wait for Sampling") + + dat.register_column('samples') + dat.samples = numpy.zeros(sample_count, numpy.uint32) + + dat.register_column('osci_raw') + dat.osci_raw = [None] * sample_count + + dat.inputs = input_chunks[0] + dat.cycles = pipe_xmc.recv() + + sum_delta = 0 + + idx = 0 + for proc in procs: + samples = proc['pipe'].recv() + proc['proc'].join() + + for i, val in samples.iteritems(): + sample_delta, osci_raw = val + dat.samples[i] = sample_delta + dat.osci_raw[i] = osci_raw + #Log.info("dat.cycles[i] = " + str(dat.cycles[i]) + ", sample_delta = " + str(sample_delta) + " ^= " + str(sample_delta * 64 * 120 / 125) + "cy") + sum_delta += dat.cycles[i] - (sample_delta * 64 * 120 / 125) + + del samples + Log.operation_end("Wait for Sampling") + + avg_delta = sum_delta / float(sample_count) + Log.info("Average Delta RP <-> XMC: " + str(avg_delta) + "cy") + + # get rid of unused arrays + del input_chunks + del procs + + xmcs[0].connect() + dev_freq = Frequency(xmcs[0].get_info()["freq"]) + + Log.operation_start("Postprocessing") + dat.write(odir + '/result.dat') + Log.operation_end("Postprocessing: " + odir + '/result.dat') + + Log.info("Seed used: " + str(dat.seed)) + Log.info("Min execution time: " + str(dat.min) + "cy") + Log.info("Mean execution time: " + "{0:.2f}".format(dat.mean) + "cy") + + Log.info("Max execution time: " + str(dat.max) + "cy" + ' (' + str(dev_freq.usec(dat.max)) + 'us @' + str(dev_freq) + ')') + Log.info("Seed execution time: " + str(dat.cy_seed) + "cy") + + # check that worst-observed execution time is triggered by worst-case input + assert dat.max == dat.cy_seed, "Maximum execution cycles exceeds cycles used for seed" + + Dat.print_wcet("platin", dat.get_wca_result('platin'), dat.max) + Dat.print_wcet("aiT", dat.get_wca_result('ait'), dat.max) + + +desc = "[ARM] Benchmark GenE using varying values as input to the generated executable" -- GitLab From 939985bb7135877c1634022951dcc265dcc31096 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 3 Mar 2017 10:18:59 +0100 Subject: [PATCH 30/97] Move raw value analysis to RedPitaya.py --- Commands/bench_input_arm_redpitaya.py | 31 ++++----------------------- Helper/RedPitaya.py | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 42c0dee..b47c42b 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -65,37 +65,14 @@ def __osci_analyze(osci_queue, pipe): results = {} while True: - i, buff = osci_queue.get() + i, raw = osci_queue.get() - if None == buff: + if None == raw: Log.debug("__osci_analyze: Exiting processing loop...") break - j = len(buff) - 1 - is_high = False - start = -1 - end = -1 - - while j >= 0: - val = buff[j] - - if val > __tlevel_high_low and not is_high: - Log.debug("__osci_analyze: Found falling edge at position " + str(j)) - end = j - is_high = True - - if val < __tlevel_low_high and is_high: - Log.debug("__osci_analyze: Found rising edge at position " + str(j)) - start = j - break - - j -= 1 - - #plot.plot(buff) - #plot.ylabel('Voltage') - #plot.show() - - results[i] = (end - start, buff) + delta = RedPitaya.samples_high_to_low(raw, __tlevel_low_high, __tlevel_high_low) + results[i] = (delta, raw) #results[i] = rp.samples_to_time(end - start) * float(120) Log.debug("Sending results...") diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index 681fdcb..ba3d239 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -88,6 +88,28 @@ class RedPitaya: return float(samples * dec) / float(125)# * 1000 * 1000) / float(self.max_sampling_rate) +def samples_high_to_low(raw, level_low, level_high): + i = len(raw) - 1 + is_high = False + start = -1 + end = -1 + + while i >= 0: + val = raw[i] + + if val > level_low and not is_high: + end = i + is_high = True + + if val < level_high and is_high: + start = i + break + + i -= 1 + + Log.debug("samples_high_to_low: high from " + str(start) + " to " + str(end) + ": " + str(end - start) + " samples") + return end - start + def list_devices(): Log.start("Searching for Red Pitayas") -- GitLab From d4456643d3643594985583c12d9275bbbaced37b Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 3 Mar 2017 10:53:55 +0100 Subject: [PATCH 31/97] Bugfix: Fix incorrect usage of limits --- Helper/RedPitaya.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index ba3d239..3a61e01 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -97,11 +97,11 @@ def samples_high_to_low(raw, level_low, level_high): while i >= 0: val = raw[i] - if val > level_low and not is_high: + if val > level_high and not is_high: end = i is_high = True - if val < level_high and is_high: + if val < level_low and is_high: start = i break -- GitLab From 70c5ffa0417c53c03088739864f585ba273437ea Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 15 Mar 2017 08:25:08 +0100 Subject: [PATCH 32/97] Fix several issues found by pylint --- Analyzers/__init__.py | 1 - Analyzers/platin.py | 2 +- Helper/Benchmark.py | 2 +- Helper/XMC4500.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Analyzers/__init__.py b/Analyzers/__init__.py index f38dad5..b232d9f 100644 --- a/Analyzers/__init__.py +++ b/Analyzers/__init__.py @@ -14,7 +14,6 @@ def get_modules(prefix): return res def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_noff = True): - ffs = [] if run_ff: ffs.append(True) if run_noff: ffs.append(False) diff --git a/Analyzers/platin.py b/Analyzers/platin.py index 3fb8970..03630ac 100644 --- a/Analyzers/platin.py +++ b/Analyzers/platin.py @@ -43,7 +43,7 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silen result = int(search.group(1)) else: proc.print_details() - result = ERROR_WCET_FIND + result = Dat.ERROR_WCET_FIND error = True return AnalyzerResult(has_ff, result, error, "", None) diff --git a/Helper/Benchmark.py b/Helper/Benchmark.py index b6119e2..5402584 100644 --- a/Helper/Benchmark.py +++ b/Helper/Benchmark.py @@ -22,7 +22,7 @@ def get_input(bits, seed, samples, chunks): if 2**bits <= samples: __add(seed) for val in range(0, 2**bits - 1): - __add(input_data, val) + __add(val) else: __add(seed) diff --git a/Helper/XMC4500.py b/Helper/XMC4500.py index bce1df3..d81cd80 100644 --- a/Helper/XMC4500.py +++ b/Helper/XMC4500.py @@ -115,7 +115,7 @@ class XMC4500: return (True, errors) if not silent: - Log.warn("Retrying flashing to " + xmc.serial + ": " + error) + Log.warn("Retrying flashing to " + self.serial + ": " + error) if None == errors: errors = [] errors.append(error) -- GitLab From bcfe07e4021b0906c440dccf7150f3000e1303e3 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 15 Mar 2017 09:57:26 +0100 Subject: [PATCH 33/97] Implement variable number and types of columns for .dat --- Commands/bench_input.py | 11 +- Commands/bench_input_arm.py | 11 +- Commands/bench_input_arm_redpitaya.py | 31 ++-- Commands/dat2pgf.py | 3 +- Commands/dat2stat.py | 3 +- Commands/display.py | 27 ++-- Helper/Dat.py | 214 ++++++++++++++++++++++---- 7 files changed, 227 insertions(+), 73 deletions(-) diff --git a/Commands/bench_input.py b/Commands/bench_input.py index a3c1549..b88a7b4 100644 --- a/Commands/bench_input.py +++ b/Commands/bench_input.py @@ -109,10 +109,11 @@ def run(args): p.start() procs.append({'proc':p, 'pipe':pipe_parent, 'input':input_chunks[i]}) - Analyzers.run(args, odir, dat, CPU, x, args.entry, pml, config_pml, eic) + for res in Analyzers.run(args, odir, dat, CPU, x, args.entry, pml, config_pml, eic): + pass - dat.inputs = numpy.zeros(sample_count, numpy.uint32) - dat.cycles = numpy.zeros(sample_count, numpy.uint32) + dat.register_column('inputs$u', sample_count) + dat.register_column('cycles$u', sample_count) idx = 0 for proc in procs: @@ -122,8 +123,8 @@ def run(args): input_chunk = proc['input'] for i in xrange(len(cycles)): - dat.inputs[idx] = input_chunk[i] - dat.cycles[idx] = cycles[i] + dat.inputs()[idx] = input_chunk[i] + dat.cycles()[idx] = cycles[i] idx += 1 del input_chunk diff --git a/Commands/bench_input_arm.py b/Commands/bench_input_arm.py index 9919f43..f4e475f 100644 --- a/Commands/bench_input_arm.py +++ b/Commands/bench_input_arm.py @@ -144,7 +144,8 @@ def run(args): Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) - Analyzers.run(args, odir, dat, CPU, axf, args.entry, pml, None, args.eic) + for res in Analyzers.run(args, odir, dat, CPU, axf, args.entry, pml, None, args.eic): + pass if not args.noespill: spills = EstimateSpill.run(pml) @@ -165,8 +166,8 @@ def run(args): if not args.nosampling: Log.operation_start("Wait for Sampling") - dat.inputs = numpy.zeros(sample_count, numpy.uint32) - dat.cycles = numpy.zeros(sample_count, numpy.uint32) + dat.register_column('inputs$u', sample_count) + dat.register_column('cycles$u', sample_count) idx = 0 for proc in procs: @@ -176,8 +177,8 @@ def run(args): input_chunk = proc['input'] for i in xrange(len(cycles)): - dat.inputs[idx] = input_chunk[i] - dat.cycles[idx] = cycles[i] + dat.inputs()[idx] = input_chunk[i] + dat.cycles()[idx] = cycles[i] idx += 1 del input_chunk diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index b47c42b..374e31c 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -19,6 +19,7 @@ CPU_TYPE = "cortex-m4" __tlevel_low_high = 0.2 __tlevel_high_low = 2.5 +__deci = 64 def benchmark_input_run(args, osci_queue, pipe, input_array, xmc, rp, eic): Log.debug("Trying to connect to XMC4500...") @@ -39,7 +40,7 @@ def benchmark_input_run(args, osci_queue, pipe, input_array, xmc, rp, eic): while True: rp.set_gain('HV') - rp.set_decimation(64) + rp.set_decimation(__deci) rp.start_acquire() cycles[i] = xmc.benchmark(input_array[i]) @@ -130,6 +131,7 @@ def run(args): if args.pitaya == None: rps = RedPitaya.list_devices() + if 0 == len(rps): return args.pitaya = rps[0] dat = Dat.Data() @@ -192,7 +194,7 @@ def run(args): rp = RedPitaya.RedPitaya(args.pitaya) rp.set_default_input(args.pitaya_in) rp.set_gain('HV') - rp.set_decimation(8) + rp.set_decimation(__deci) buf_size = rp.get_buffer_size() offset = - ( int(buf_size) / 2 - 1) @@ -237,34 +239,33 @@ def run(args): Log.operation_start("Wait for Sampling") - dat.register_column('samples') - dat.samples = numpy.zeros(sample_count, numpy.uint32) + dat.register_column('inputs$u') + dat.register_column('cycles$u') + dat.register_column('samples$u', sample_count) + dat.register_column('osci_raw@f', sample_count) - dat.register_column('osci_raw') - dat.osci_raw = [None] * sample_count - - dat.inputs = input_chunks[0] - dat.cycles = pipe_xmc.recv() + dat.set_column('inputs', input_chunks[0]) + dat.set_column('cycles', pipe_xmc.recv()) sum_delta = 0 idx = 0 + deltas = numpy.zeros(sample_count, numpy.int32) for proc in procs: samples = proc['pipe'].recv() proc['proc'].join() for i, val in samples.iteritems(): sample_delta, osci_raw = val - dat.samples[i] = sample_delta - dat.osci_raw[i] = osci_raw - #Log.info("dat.cycles[i] = " + str(dat.cycles[i]) + ", sample_delta = " + str(sample_delta) + " ^= " + str(sample_delta * 64 * 120 / 125) + "cy") - sum_delta += dat.cycles[i] - (sample_delta * 64 * 120 / 125) + dat.samples()[i] = sample_delta + dat.osci_raw()[i] = osci_raw + deltas[i] = dat.cycles()[i] - (sample_delta * __deci * 120 / 125) + Log.info("deltas[" + str(i) + "] = " + str(deltas[i]) + " = " + str(dat.cycles()[i]) + " - " + str(sample_delta * __deci * 120 / 125)) del samples Log.operation_end("Wait for Sampling") - avg_delta = sum_delta / float(sample_count) - Log.info("Average Delta RP <-> XMC: " + str(avg_delta) + "cy") + Log.info("Average Delta RP <-> XMC: " + str(numpy.mean(deltas)) + "cy (min/max/stddev: " + str(numpy.amin(deltas)) + "/" + str(numpy.amax(deltas)) + "/" + str(numpy.std(deltas)) + ")") # get rid of unused arrays del input_chunks diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index 17f41a7..e6bbc9e 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -3,6 +3,7 @@ import argparse import numpy import sys import os +import matplotlib from Helper import Dat, Execution, Log, Python @@ -42,7 +43,7 @@ def get_argparser(): return parser def hist_fh(args, dat): - plt.hist(dat.cycles, args.bins, normed=False, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) + plt.hist(dat.cycles(), args.bins, normed=False, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) if args.no_tic: plt.gca().axes.get_xaxis().set_ticks([]) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index bf25b7c..1e00d19 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -4,6 +4,7 @@ import glob import numpy import sys import os +import matplotlib from Helper import Dat, Log, Python @@ -19,7 +20,6 @@ def get_argparser(): parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics (default: false)') parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used (default: ' + str(fs_def) + ')') - parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds (default: 1)') def __register_color(name, label, default): parser.add_argument('--color-' + name, @@ -107,7 +107,6 @@ def run(args): invalid = [100*f for f in ubs[ana] if f < 1.0] bins = range(int(100 * mins[ana]), int(100 * maxs[ana]), 1) - #numpy.linspace(mins[ana], maxs[ana], 500) plt.hist(valid, bins, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) plt.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0.1, alpha=0.5) diff --git a/Commands/display.py b/Commands/display.py index a887af8..e18d639 100644 --- a/Commands/display.py +++ b/Commands/display.py @@ -7,8 +7,9 @@ from Helper import Dat, Log, Python def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " display", description='Subfunction display: ' + desc) - parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file to be displayed') - parser.add_argument('--bins', metavar='bins', type=int, default=200, help='Number of bins used for plotting histogram (default: 200)') + parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file to be displayed') + parser.add_argument('--column', metavar='column', type=str, default='cycles', help='TODO') + parser.add_argument('--bins', metavar='bins', type=int, default=200, help='Number of bins used for plotting histogram (default: 200)') return parser def draw_wcet(plt, tool, wcet): @@ -20,26 +21,26 @@ def draw_wcet(plt, tool, wcet): plt.text(wcet, 1.2 * center, ' ' + tool, ha='left', va='bottom', color='m', horizontalalignment='left') -def hist_fh(fobject, bins): +def hist_fh(args,fobject, bins): dat = Dat.Data.from_file(fobject) - n, bins, patches = plt.hist(dat.cycles, bins, normed=False, facecolor='green', alpha=0.5) + n, bins, patches = plt.hist(dat.column(args.column), bins, normed=False, facecolor='green', alpha=0.5) plt.xlabel('Cycles') plt.ylabel('Occurrences') - xmax = 1.1 * dat.get_x_maximum() - plt.xlim([0, xmax]) + #xmax = 1.1 * dat.get_x_maximum() + #plt.xlim([0, xmax]) - plt.axvline(x=dat.min, ymin=0, ymax=1, linewidth=2, color='g') - plt.axvline(x=dat.max, ymin=0, ymax=1, linewidth=2, color='r') + #plt.axvline(x=dat.min, ymin=0, ymax=1, linewidth=2, color='g') + #plt.axvline(x=dat.max, ymin=0, ymax=1, linewidth=2, color='r') plt.subplots_adjust(left=0.04, right=0.96, bottom=0.04, top=0.9) title_wcet = '' - for ana in dat.wcet: - draw_wcet(plt, ana, dat.wcet[ana]) - title_wcet = title_wcet + 'WCET (' + ana + ') = ' + dat.get_analyzer_result(ana) + '\n' + #for ana in dat.wcet: + # draw_wcet(plt, ana, dat.wcet[ana]) + # title_wcet = title_wcet + 'WCET (' + ana + ') = ' + dat.get_analyzer_result(ana) + '\n' title = r'mean=' + "{0:.2f}".format(dat.mean) + r'cy, ' \ + 'stddev=' + "{0:.2f}".format(dat.std) + r'cy, ' \ @@ -60,7 +61,7 @@ def run(args): #if os.fstat(fle.fileno()).st_size > 0: Log.info("Processing file " + fle) plt.figure(i) - hist_fh(fle, args.bins) + hist_fh(args, fle, args.bins) i = i + 1 #else: # Log.warn("File " + fle.name + " empty") @@ -68,4 +69,4 @@ def run(args): plt.show() -desc = "Use pyplots to display a GenE .dat file" +desc = "Use pyplots to display an Aladdin .dat file" diff --git a/Helper/Dat.py b/Helper/Dat.py index 73fcb01..18fc4b3 100644 --- a/Helper/Dat.py +++ b/Helper/Dat.py @@ -15,6 +15,15 @@ ERROR_WCET_FIND = -15 ERROR_WCET_RETCODE = -16 ERROR_WCET_UNKNOWN = -100 +_type_to_char = { + numpy.uint32: 'u', + numpy.float32: 'f' +} + +# also provide mapping character -> datatype +_char_to_type = {v: k for k, v in _type_to_char.iteritems()} + + def errno_to_string(errno): if ERROR_WCET_UNBOUND == errno: return "UNBOUND" @@ -42,13 +51,90 @@ def print_wcet(tool, wcet, woet): else: Log.info("WCET analysis (" + tool + "): " + str(wcet) + "cy") + +### (start) Column*Type classes +class ColumnType(object): + def __init__(self, dtype): + self.dtype = dtype + self.char = _type_to_char[dtype] + + def numpy_storable(self): + return 'numpy' == self.dtype.__module__ + + def get_char(self): + return self.char + + def __str__(self): + return self.char + + @classmethod + def from_string(cls, string): + dtype = _char_to_type[ string[-1] ] + if '$' == string[-2]: return ColumnPrimitiveType(dtype) + elif '@' == string[-2]: return ColumnArrayType(dtype) + + +class ColumnArrayType(ColumnType): + def __init__(self, dtype): + ColumnType.__init__(self, dtype) + + def parse(self, string): + val, rem = string.split(']', 1) + return (numpy.fromstring(val[1:], dtype=self.dtype, sep=','), rem.strip()) + + def prepare(self, count): + return numpy.zeros(count, self.dtype) + + def prepare_pc(self, count): + return [0] * count + + def __str__(self): + return '@' + ColumnType.__str__(self) + + +class ColumnPrimitiveType(ColumnType): + def __init__(self, dtype): + ColumnType.__init__(self, dtype) + + def parse(self, string): + if ' ' in string: + val, rem = string.split(' ', 1) + return (self.dtype(val), rem) + else: + return (self.dtype(string), None) + + + def prepare(self, count): + return self.dtype(0) + + def prepare_pc(self, count): + return numpy.zeros(count, self.dtype) + + def __str__(self): + return '$' + ColumnType.__str__(self) +### (end) Column*Type classes + + + +class DataColumn: + def __init__(self, string, index): + self.name = string[:-2] + self.index = index + self.type = ColumnType.from_string(string) + + def parse(self, string): + return self.type.parse(string) + + def __str__(self): + return self.name + str(self.type) + + class Data: def __init__(self): self.inputs = None self.cycles = None - - self.__columns = ['inputs', 'cycles'] - + self.dcs = [] + self.count = 0 self.pattern = None self.seed = None self.cy_seed = None @@ -57,7 +143,39 @@ class Data: self.std = None self.max = None self.wcet = dict() + self.file = None + + def _lazy_loader(self, ldc): + int_name = "_data_" + ldc.name + assert not hasattr(self, int_name) + + # TODO: load data from file + Log.warn("loading column " + ldc.name + " from file " + self.file) + + setattr(self, int_name, ldc.type.prepare_pc(self.count)) + + with open(self.file, 'r') as fobject: + i = 0 + for line in fobject: + # ignore file header + if line.startswith('#'): continue + rem = line + for dc in self.dcs: + val, rem = dc.parse(rem) + if dc == ldc: + getattr(self, int_name)[i] = val + i += 1 + break + + # patch accessor + setattr(self, ldc.name, lambda: getattr(self, int_name)) + setattr(self, "loaded_" + ldc.name, True) + + return getattr(self, int_name) + + def _access_error(self): + assert False, "Missing call to prepare!" @classmethod def from_file(cls, filepath): @@ -66,7 +184,7 @@ class Data: prog_cyc = re.compile(r'^\s*(\d*)\s+(\d*)\s*$') cycles = None - cycles_pos = 0; + cycles_pos = 0 dat = Data() dat.file = filepath @@ -99,52 +217,54 @@ class Data: if name == 'pattern': setattr(dat, name, search.group(2)) - num_info_lines += 1 - else: - if 0 == cycles_pos: - cycles = numpy.zeros(num_lines - num_info_lines, numpy.uint32) - inputs = numpy.zeros(num_lines - num_info_lines, numpy.uint32) + if line.startswith("###"): + cols = line.split(" ")[1:] - search = prog_cyc.search(line) - assert search, 'Line `' + line + r'` does not match `^\s*(\d*)\s+(\d*)\s*$`' + for col in cols: + dat.register_column(col) - inputs[cycles_pos] = int(search.group(1)) - cycles[cycles_pos] = int(search.group(2)) - cycles_pos += 1 + num_info_lines += 1 + else: + # remain backwards compatible: + # older .dat files do not have the columns description + # -> use default "inputs cycles + if 0 == len(dat.dcs): + dat.register_column('inputs$u') + dat.register_column('cycles$u') - assert cycles_pos == num_lines - num_info_lines - dat.cycles = cycles - dat.inputs = inputs + dat.count = num_lines - num_info_lines + break dat.validate() + Log.warn("Reading done") return dat def validate(self): assert None != self.seed, "seed unknown" - if None == self.min: self.min = numpy.amin(self.cycles) - if None == self.mean: self.mean = numpy.mean(self.cycles, dtype=numpy.float64) - if None == self.std: self.std = numpy.std(self.cycles, dtype=numpy.float64) - if None == self.max: self.max = numpy.amax(self.cycles) + if None == self.min: self.min = numpy.amin(self.cycles()) + if None == self.mean: self.mean = numpy.mean(self.cycles(), dtype=numpy.float64) + if None == self.std: self.std = numpy.std(self.cycles(), dtype=numpy.float64) + if None == self.max: self.max = numpy.amax(self.cycles()) if None == self.cy_seed: - for i in xrange(len(self.cycles)): - if self.seed == self.inputs[i]: - self.cy_seed = self.cycles[i] + for i in xrange(len(self.cycles())): + if self.seed == self.inputs()[i]: + self.cy_seed = self.cycles()[i] assert None != self.cy_seed, "Unable to find cycles required for input = seed" if self.max != self.cy_seed: Log.fail("self.max != self.cy_seed") def sample_count(self): - return self.cycles.size + return self.cycles().size def ets_count(self): - return numpy.unique(self.cycles).size + return numpy.unique(self.cycles()).size def trigger_wcp_count(self): - return sum(1 for cy in self.cycles if cy == self.max) + return sum(1 for cy in self.cycles() if cy == self.max) def get_x_maximum(self): if 0 == len(self.wcet): return self.max @@ -162,8 +282,38 @@ class Data: else: return errno_to_string(wcet) - def register_column(self, colname): - self.__columns.append(colname) + register_column_index = 0 + def register_column(self, name, count=0): + name = name.strip(' \t\n\r') + Log.debug("Registering new DataColumn " + name + " at index " + str(self.register_column_index)) + dc = DataColumn(name, self.register_column_index) + self.register_column_index += 1 + + self.dcs.append(dc) + + int_name = "_data_" + dc.name + + Log.debug("Installing accessor " + str(dc.name) + " to read from " + str(name)) + if None == self.file and 0 == count: + setattr(self, dc.name, lambda: self._access_error()) + setattr(self, "loaded_" + dc.name, False) + elif None == self.file: + setattr(self, int_name, dc.type.prepare_pc(count)) + setattr(self, dc.name, lambda: getattr(self, int_name)) + setattr(self, "loaded_" + dc.name, True) + else: + setattr(self, dc.name, lambda: self._lazy_loader(dc)) + setattr(self, "loaded_" + dc.name, False) + + def set_column(self, name, value): + int_name = "_data_" + name + + setattr(self, int_name, value) + setattr(self, name, lambda: getattr(self, int_name)) + setattr(self, "loaded_" + name, True) + + def column(self, name): + return getattr(self, name)() def __write_header(self, fh): fh.write("# seed: " + str(self.seed) + "\n") @@ -176,15 +326,15 @@ class Data: fh.write("# max: " + str(self.max) + "\n") fh.write("# pattern: " + self.pattern + "\n") - fh.write("# " + " ".join(self.__columns) + "\n") + fh.write("### " + " ".join(str(dc) for dc in self.dcs) + "\n") def write(self, filepath): self.validate() fh = open(filepath, 'w') self.__write_header(fh) - for i in xrange(len(self.cycles)): - fh.write(" ".join([str(getattr(self, col)[i]) for col in self.__columns]) + "\n") + for i in xrange(len(self.cycles())): + fh.write(" ".join([str(getattr(self, dc.name)()[i]) for dc in self.dcs]) + "\n") fh.close() def write_dref(self, dref_file): -- GitLab From 9a580d000ecda23b0d5d90fcfa6041f058f6be6c Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 15 Mar 2017 10:05:43 +0100 Subject: [PATCH 34/97] Replace old-style classes with new-style classes --- AnalyzerResult.py | 2 +- Helper/Dat.py | 4 ++-- Helper/EstimateSpill.py | 2 +- Helper/Execution.py | 2 +- Helper/Frequency.py | 2 +- Helper/GenE.py | 2 +- Helper/Metrics.py | 2 +- Helper/RedPitaya.py | 2 +- Helper/XMC4500.py | 2 +- aladdin | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/AnalyzerResult.py b/AnalyzerResult.py index c317679..11e8b38 100644 --- a/AnalyzerResult.py +++ b/AnalyzerResult.py @@ -2,7 +2,7 @@ from Helper import Dat -class AnalyzerResult: +class AnalyzerResult(object): def __init__(self, has_ff, cy, error, error_msg, unb_loops): self.has_ff = has_ff self.cy = cy diff --git a/Helper/Dat.py b/Helper/Dat.py index 18fc4b3..0590a3e 100644 --- a/Helper/Dat.py +++ b/Helper/Dat.py @@ -116,7 +116,7 @@ class ColumnPrimitiveType(ColumnType): -class DataColumn: +class DataColumn(object): def __init__(self, string, index): self.name = string[:-2] self.index = index @@ -129,7 +129,7 @@ class DataColumn: return self.name + str(self.type) -class Data: +class Data(object): def __init__(self): self.inputs = None self.cycles = None diff --git a/Helper/EstimateSpill.py b/Helper/EstimateSpill.py index f55c605..c19dc84 100644 --- a/Helper/EstimateSpill.py +++ b/Helper/EstimateSpill.py @@ -3,7 +3,7 @@ import Log, Execution import re -class EstimateSpillResult: +class EstimateSpillResult(object): def __init__(self, func): self.func = func self.spills = None diff --git a/Helper/Execution.py b/Helper/Execution.py index 81c81bf..ccfa2de 100644 --- a/Helper/Execution.py +++ b/Helper/Execution.py @@ -10,7 +10,7 @@ import Log import Log -class Execution: +class Execution(object): def __init__(self, command, proc): self.command = command self.proc = proc diff --git a/Helper/Frequency.py b/Helper/Frequency.py index dffa3e1..4cc6c39 100644 --- a/Helper/Frequency.py +++ b/Helper/Frequency.py @@ -1,6 +1,6 @@ #!/usr/bin/python -class Frequency: +class Frequency(object): def __init__(self, hz): self.hz = hz diff --git a/Helper/GenE.py b/Helper/GenE.py index 4b8245b..785e0d4 100644 --- a/Helper/GenE.py +++ b/Helper/GenE.py @@ -7,7 +7,7 @@ import pexpect import Log, Tools, Git -class Benchmark: +class Benchmark(object): def __init__(self, ll, pattern, cost, bits, seed): self.cost = cost self.bits = bits diff --git a/Helper/Metrics.py b/Helper/Metrics.py index 75dd226..a816aef 100644 --- a/Helper/Metrics.py +++ b/Helper/Metrics.py @@ -2,7 +2,7 @@ import Log -class MetricsResult: +class MetricsResult(object): def __init__(self): self.func = None diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index 3a61e01..f480c21 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -2,7 +2,7 @@ from External import redpitaya_scpi as scpi from Helper import Tools, Log -class RedPitaya: +class RedPitaya(object): def __init__(self, ip): self.ip = ip self.def_input = 1 diff --git a/Helper/XMC4500.py b/Helper/XMC4500.py index d81cd80..724c86a 100644 --- a/Helper/XMC4500.py +++ b/Helper/XMC4500.py @@ -73,7 +73,7 @@ def flash(odir, serial, bin_axf): return jlink_error_check(output) -class XMC4500: +class XMC4500(object): def __init__(self, port, serial = None): self.serial = serial self.port = port diff --git a/aladdin b/aladdin index 49c6d7e..33e84f7 100755 --- a/aladdin +++ b/aladdin @@ -25,7 +25,7 @@ class ZshCompHelpFormatter(ap.HelpFormatter): res, actions, groups, "") -class Module: +class Module(object): def __init__(self, prefix, name, loader): self.prefix = prefix self.name = name -- GitLab From 8187263ce66030996c6495a88a25f87bc12df263 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 15 Mar 2017 12:11:55 +0100 Subject: [PATCH 35/97] Various small improvements --- Analyzers/__init__.py | 2 +- Commands/bench_input.py | 1 - Commands/bench_input_arm.py | 2 +- Commands/bench_input_arm_redpitaya.py | 6 +---- Commands/dat2pgf.py | 1 - Commands/display.py | 12 +++------ Commands/resilience.py | 11 ++++---- Commands/test.py | 36 --------------------------- Helper/Dat.py | 2 +- Helper/EstimateSpill.py | 2 +- Helper/Execution.py | 23 ++--------------- Helper/Tools.py | 5 +--- Helper/XMC4500.py | 1 - 13 files changed, 18 insertions(+), 86 deletions(-) delete mode 100644 Commands/test.py diff --git a/Analyzers/__init__.py b/Analyzers/__init__.py index b232d9f..8b49654 100644 --- a/Analyzers/__init__.py +++ b/Analyzers/__init__.py @@ -42,5 +42,5 @@ def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_n def add_arguments(parser): modules = get_modules('Analyzers.') - for name, mod in modules.iteritems(): + for name, _ in modules.iteritems(): parser.add_argument('--no-' + name.lower(), dest='no' + name.lower(), action='store_true', help='Disable execution of ' + name) diff --git a/Commands/bench_input.py b/Commands/bench_input.py index b88a7b4..a097b89 100644 --- a/Commands/bench_input.py +++ b/Commands/bench_input.py @@ -6,7 +6,6 @@ import argparse as ap import os import numpy import sys -import pkgutil from Helper import Benchmark, Dat, Log, Tools, Pattern, Git, Result, Execution, GenE import Analyzers diff --git a/Commands/bench_input_arm.py b/Commands/bench_input_arm.py index f4e475f..b650904 100644 --- a/Commands/bench_input_arm.py +++ b/Commands/bench_input_arm.py @@ -9,7 +9,7 @@ import time import numpy from Helper.Frequency import Frequency -from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Git, XMC4500, Result, EstimateSpill, GenE +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, XMC4500, Result, EstimateSpill, GenE import Analyzers CPU = "xmc4500" diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 374e31c..1e8b1a0 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -8,10 +8,9 @@ import shutil import sys import time import numpy -import Queue from Helper.Frequency import Frequency -from Helper import RedPitaya, Benchmark, Dat, Log, Tools, Pattern, Execution, Git, XMC4500, Result, EstimateSpill, GenE, Python +from Helper import RedPitaya, Benchmark, Dat, Log, Tools, Pattern, Execution, XMC4500, Result, EstimateSpill, GenE, Python import Analyzers CPU = "xmc4500" @@ -247,9 +246,6 @@ def run(args): dat.set_column('inputs', input_chunks[0]) dat.set_column('cycles', pipe_xmc.recv()) - sum_delta = 0 - - idx = 0 deltas = numpy.zeros(sample_count, numpy.int32) for proc in procs: samples = proc['pipe'].recv() diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index e6bbc9e..88f6186 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -1,6 +1,5 @@ #!/usr/bin/python import argparse -import numpy import sys import os import matplotlib diff --git a/Commands/display.py b/Commands/display.py index e18d639..371a03a 100644 --- a/Commands/display.py +++ b/Commands/display.py @@ -1,7 +1,6 @@ #!/usr/bin/python import argparse import sys -import os from Helper import Dat, Log, Python @@ -58,13 +57,10 @@ def run(args): i = 1 for fle in args.input: - #if os.fstat(fle.fileno()).st_size > 0: - Log.info("Processing file " + fle) - plt.figure(i) - hist_fh(args, fle, args.bins) - i = i + 1 - #else: - # Log.warn("File " + fle.name + " empty") + Log.info("Processing file " + fle) + plt.figure(i) + hist_fh(args, fle, args.bins) + i = i + 1 plt.show() diff --git a/Commands/resilience.py b/Commands/resilience.py index 21c28ef..2674425 100644 --- a/Commands/resilience.py +++ b/Commands/resilience.py @@ -2,8 +2,9 @@ import argparse import sys import re +import pexpect -from Helper import Execution, Metrics, Tools, Log +from Helper import Execution, Metrics, Tools, Log, GenE def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " resilience", description='Subfunction resilience: ' + desc) @@ -18,11 +19,11 @@ def get_argparser(): def check_args(args): if None == args.opt: - args.opt = Execution.which("opt-3.4") + args.opt = pexpect.which("opt-3.4") assert None != args.opt, "Failed to find opt-3.4" if None == args.llvm_dis: - args.llvm_dis = Execution.which("llvm-dis-3.4") + args.llvm_dis = pexpect.which("llvm-dis-3.4") assert None != args.llvm_dis, "Failed to find llvm-dis-3.4" return args @@ -35,7 +36,7 @@ def run(args): for inpt in args.input: Log.info("Processing file: " + inpt) - bm = Tools.Benchmark(inpt, pattern=None, cost=None, bits=None, seed=None) + bm = GenE.Benchmark(inpt, pattern=None, cost=None, bits=None, seed=None) bm.add_func(args.entry) Log.operation_start("Calculating metrics...") metrics_unopt = bm.get_metrics() @@ -53,7 +54,7 @@ def run(args): Execution.run_wait(args.opt + ' -O3 ' + inpt + ' -o ' + opted_bc, None, 0) Execution.run_wait(args.llvm_dis + ' ' + opted_bc + ' -o ' + opted_ll, None, 0) - bm_opt = Tools.Benchmark(opted_ll, pattern=None, cost=None, bits=None, seed=None) + bm_opt = GenE.Benchmark(opted_ll, pattern=None, cost=None, bits=None, seed=None) bm_opt.add_func(args.entry) Log.operation_start("Calculating metrics on optimized code...") diff --git a/Commands/test.py b/Commands/test.py deleted file mode 100644 index 8163d6b..0000000 --- a/Commands/test.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/python3 - -from multiprocessing import Process, Queue -import multiprocessing -import argparse as ap -import os -import re -import subprocess -import time -import shlex -import numpy -import sys - -from Helper import Log, Tools, Pattern, Execution - -def get_argparser(): - parser = ap.ArgumentParser(prog=sys.argv[0] + " bench_input", description='Subfunction bench_input_arm: ' + desc) - - parser.add_argument('--seed', metavar='seed', type=int, default=1431655765, help='seed passed to GenE (default: 1431655765)') - parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking (default: 10000)') - parser.add_argument('--cost', metavar='cost', type=int, default=10000, help='cost to be used for generating code (default: 10000)') - - return parser - -def run(args): - for xmc in list_xmc4500(): - Log.info(" * Found device with id " + xmc.serial + " at port " + xmc.port + " (current tty: " + xmc.get_tty() + ")") - - -def xmccomm_execute(dev, cmds, retcode = 0): - proc = Execution.run_wait("./xmc4500/xmccomm " + dev + " " + cmds) - Execution.assert_retcode(proc, retcode) - - return proc - -desc = "Test Command: Internally Used" diff --git a/Helper/Dat.py b/Helper/Dat.py index 0590a3e..3ec142e 100644 --- a/Helper/Dat.py +++ b/Helper/Dat.py @@ -3,7 +3,7 @@ import numpy import re -import Pattern, Log +import Log ERROR_WCET_UNBOUND = -1 ERROR_WCET_NOTEXEC = -2 diff --git a/Helper/EstimateSpill.py b/Helper/EstimateSpill.py index c19dc84..d165c6b 100644 --- a/Helper/EstimateSpill.py +++ b/Helper/EstimateSpill.py @@ -1,6 +1,6 @@ #!/usr/bin/python -import Log, Execution +import Execution import re class EstimateSpillResult(object): diff --git a/Helper/Execution.py b/Helper/Execution.py index ccfa2de..9795d15 100644 --- a/Helper/Execution.py +++ b/Helper/Execution.py @@ -6,8 +6,7 @@ import os import signal import subprocess import threading -import Log - +import pexpect import Log class Execution(object): @@ -43,28 +42,10 @@ class Execution(object): def kill(self): self.proc.kill() -def which(program): - "python implementation of which(1)" - import os - def is_exe(fpath): - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) - - fpath, fname = os.path.split(program) - if fpath: - if is_exe(program): - return program - else: - for path in os.environ["PATH"].split(os.pathsep): - path = path.strip('"') - exe_file = os.path.join(path, program) - if is_exe(exe_file): - return exe_file - - return None def __prepare_argv(cmd): argv = shlex.split(cmd) - if which(argv[0]) is None: + if pexpect.which(argv[0]) is None: sys.exit("unable to find executable: %s" % argv[0]) return argv diff --git a/Helper/Tools.py b/Helper/Tools.py index 99977d4..2d2e46e 100644 --- a/Helper/Tools.py +++ b/Helper/Tools.py @@ -3,13 +3,10 @@ from __future__ import print_function import re import os -import uuid -import shutil -import filecmp import pexpect import socket -import Dat, Execution, Log, Metrics +import Execution, Metrics def get_target(cpu): if "patmos" == cpu: diff --git a/Helper/XMC4500.py b/Helper/XMC4500.py index 724c86a..6bb080e 100644 --- a/Helper/XMC4500.py +++ b/Helper/XMC4500.py @@ -3,7 +3,6 @@ import os import re import time -import select import pexpect from serial import Serial, SerialException -- GitLab From ee5e170e51581bed2e604573b5ca4ce442b4c696 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 17 Mar 2017 10:20:30 +0100 Subject: [PATCH 36/97] Implement RP <-> XMC4500 searching --- Commands/bench_input_arm_redpitaya.py | 74 +++++++++++++++++---------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 1e8b1a0..713c19c 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -20,6 +20,30 @@ __tlevel_low_high = 0.2 __tlevel_high_low = 2.5 __deci = 64 +def __get_sample(xmc, rp, inp, rp_in, rep_limit = 5): + buf_size = rp.get_buffer_size() + offset = - ( int(buf_size) / 2 - 1) + rp.set_trigger('CH' + str(rp_in) + '_NE', 1000*__tlevel_high_low, offset) + + rep = 0 + raw = None + while rep < rep_limit: + rp.set_gain('HV') + rp.set_decimation(__deci) + + rp.start_acquire() + cy = xmc.benchmark(inp) + + if not rp.get_trigger_status(): + raw = rp.get_data() + rp.stop_acquire() + break + + rep += 1 + Log.warn("Repeating measurement for input " + str(inp)) + + return (cy, raw) + def benchmark_input_run(args, osci_queue, pipe, input_array, xmc, rp, eic): Log.debug("Trying to connect to XMC4500...") @@ -30,27 +54,8 @@ def benchmark_input_run(args, osci_queue, pipe, input_array, xmc, rp, eic): cycles = numpy.zeros(len(input_array), numpy.uint32) - buf_size = rp.get_buffer_size() - offset = - ( int(buf_size) / 2 - 1) - Log.debug("Buffer Size: " + str(buf_size) + " => Offset: " + str(offset)) - for i in xrange(len(input_array)): - Log.debug("set_trigger: " + str( rp.set_trigger('CH' + str(args.pitaya_in) + '_NE', 1000*__tlevel_high_low, offset) )) - - while True: - rp.set_gain('HV') - rp.set_decimation(__deci) - - rp.start_acquire() - cycles[i] = xmc.benchmark(input_array[i]) - - if not rp.get_trigger_status(): - buff = rp.get_data() - rp.stop_acquire() - break - - Log.warn("Repeating measurement for input " + str(input_array[i])) - + cycles[i], buff = __get_sample(xmc, rp, input_array[i], args.pitaya_in) osci_queue.put( (i, buff) ) Log.debug("Sending poison pill to " + str(multiprocessing.cpu_count()) + " threads...") @@ -88,6 +93,7 @@ def get_argparser(): parser.add_argument('--entry', metavar='function', type=str, default='gene_main', help='entry used for analysis/simulation') parser.add_argument('--opt', metavar='opt', type=int, default=0, help='Optimization level') parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') + parser.add_argument('--xmc', metavar='devid', type=str, default=None, help='Device ID of the XMC board') parser.add_argument('--pitaya', metavar='pitaya', type=str, default=None, help='IP of the RedPitaya') parser.add_argument('--pitaya-in', metavar='pitaya_in', type=int, default=1, help='RedPitaya: Input used') @@ -183,13 +189,6 @@ def run(args): time.sleep(2) - Log.start("Start " + str(len(xmcs)) + " Sampling Threads") - input_chunks = Benchmark.get_input(args.bits, args.seed, args.samples, len(xmcs)) - - # get the total number of samples, which does not - # equal args.samples due to special input values - sample_count = sum(len(chunk) for chunk in input_chunks) - rp = RedPitaya.RedPitaya(args.pitaya) rp.set_default_input(args.pitaya_in) rp.set_gain('HV') @@ -200,6 +199,27 @@ def run(args): Log.debug("Buffer Size: " + str(buf_size) + " => Offset: " + str(offset)) rp.set_trigger('CH' + str(args.pitaya_in) + '_NE', __tlevel_high_low, offset) + # search for osci <-> XMC4500 + rp_xmc = None + for xmc in xmcs: + xmc.connect() + cy, raw = __get_sample(xmc, rp, 0, args.pitaya_in) + xmc.disconnect() + if None != raw: + Log.info("RP connected to " + str(xmc)) + rp_xmc = xmc + break + + assert rp_xmc != None + xmcs = [rp_xmc] + + Log.start("Start " + str(len(xmcs)) + " Sampling Threads") + input_chunks = Benchmark.get_input(args.bits, args.seed, args.samples, len(xmcs)) + + # get the total number of samples, which does not + # equal args.samples due to special input values + sample_count = sum(len(chunk) for chunk in input_chunks) + manager = multiprocessing.Manager() osci_queue = manager.Queue() -- GitLab From ee36ba99071091d6a4c0573e80ab161f4504a885 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 17 Mar 2017 10:28:26 +0100 Subject: [PATCH 37/97] Only read RP buffer size once --- Commands/bench_input_arm_redpitaya.py | 8 +++----- Helper/RedPitaya.py | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 713c19c..d3d6e5b 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -21,8 +21,7 @@ __tlevel_high_low = 2.5 __deci = 64 def __get_sample(xmc, rp, inp, rp_in, rep_limit = 5): - buf_size = rp.get_buffer_size() - offset = - ( int(buf_size) / 2 - 1) + offset = - (rp.buffer_size / 2 - 1) rp.set_trigger('CH' + str(rp_in) + '_NE', 1000*__tlevel_high_low, offset) rep = 0 @@ -194,9 +193,8 @@ def run(args): rp.set_gain('HV') rp.set_decimation(__deci) - buf_size = rp.get_buffer_size() - offset = - ( int(buf_size) / 2 - 1) - Log.debug("Buffer Size: " + str(buf_size) + " => Offset: " + str(offset)) + offset = - ( int(rp.buffer_size) / 2 - 1) + Log.debug("Buffer Size: " + str(rp.buffer_size) + " => Offset: " + str(offset)) rp.set_trigger('CH' + str(args.pitaya_in) + '_NE', __tlevel_high_low, offset) # search for osci <-> XMC4500 diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index f480c21..2b4bc81 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -10,6 +10,9 @@ class RedPitaya(object): self.rp = scpi.scpi(ip) self.send('ACQ:DATA:UNITS VOLTS') + # (constant) device information + self.buffer_size = int(self.send_recv('ACQ:BUF:SIZE?')) + def send(self, string): assert None != self.rp self.rp.tx_txt(string) @@ -73,9 +76,6 @@ class RedPitaya(object): def get_wpos(self): return int(self.send_recv('ACQ:WPOS?')) - def get_buffer_size(self): - return int(self.send_recv('ACQ:BUF:SIZE?')) - def start_acquire(self): self.send('ACQ:START') -- GitLab From 03f6bc44f22524f08372feec471f89a5fd2186b8 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 17 Mar 2017 10:29:28 +0100 Subject: [PATCH 38/97] Replace old-style `` with str() --- Commands/bench_seed.py | 2 +- Commands/test_pattern.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/bench_seed.py b/Commands/bench_seed.py index 6b75153..672c4f5 100644 --- a/Commands/bench_seed.py +++ b/Commands/bench_seed.py @@ -28,7 +28,7 @@ def generate_and_run(cost, seed, entry): Tools.llvm_c_to_ll("patmos-unknown-unknown-elf", "data/main.c", ll_main) Log.operation_end(" * Compile main.c") - return run_("cost: % 6d," % cost, "GenE.x", entry, "GenE.pml", `seed` + "\n") + return run_("cost: % 6d," % cost, "GenE.x", entry, "GenE.pml", str(seed) + "\n") def run_(suffix, binary, function, pml, stdin_data): diff --git a/Commands/test_pattern.py b/Commands/test_pattern.py index 4fe27b5..e4ff7cc 100644 --- a/Commands/test_pattern.py +++ b/Commands/test_pattern.py @@ -33,7 +33,7 @@ def run(args): wcet = Tools.platin_execute("GenE.x", args.entry, "GenE.pml") assert 1 == len(pc), "More than one different pattern generated" - assert 1 == pc[pc.keys()[0]], pc.keys()[0] + " was generated " + `pc[pc.keys()[0]]` + "x" + assert 1 == pc[pc.keys()[0]], pc.keys()[0] + " was generated " + str(pc[pc.keys()[0]]) + "x" print(pattern.ljust(max_len) + " %dcy" % wcet) -- GitLab From 3daea0eabac6d34f93bfa46fe8ef03494163fd9d Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 17 Mar 2017 10:36:22 +0100 Subject: [PATCH 39/97] Move XMC4500.py to Backends/ --- {Helper => Backends}/XMC4500.py | 2 +- Backends/__init__.py | 0 Commands/bench_input_arm.py | 3 ++- Commands/bench_input_arm_demo.py | 3 ++- Commands/bench_input_arm_redpitaya.py | 3 ++- Commands/etime.py | 3 ++- 6 files changed, 9 insertions(+), 5 deletions(-) rename {Helper => Backends}/XMC4500.py (99%) create mode 100644 Backends/__init__.py diff --git a/Helper/XMC4500.py b/Backends/XMC4500.py similarity index 99% rename from Helper/XMC4500.py rename to Backends/XMC4500.py index 6bb080e..d2110fb 100644 --- a/Helper/XMC4500.py +++ b/Backends/XMC4500.py @@ -6,7 +6,7 @@ import time import pexpect from serial import Serial, SerialException -import Log, Execution +from Helper import Log, Execution USB_DEV='/sys/bus/usb/devices' diff --git a/Backends/__init__.py b/Backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Commands/bench_input_arm.py b/Commands/bench_input_arm.py index b650904..21f1673 100644 --- a/Commands/bench_input_arm.py +++ b/Commands/bench_input_arm.py @@ -9,7 +9,8 @@ import time import numpy from Helper.Frequency import Frequency -from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, XMC4500, Result, EstimateSpill, GenE +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, EstimateSpill, GenE +from Backends import XMC4500 import Analyzers CPU = "xmc4500" diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 0b398b0..dc0388a 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -15,7 +15,8 @@ from PyQt5 import QtWidgets from Gui.DemoWindow import DemoWindow from Helper.Frequency import Frequency -from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, XMC4500, Result, GenE, Python +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python +from Backends import XMC4500 import Analyzers COUNT_BMS = 10 diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index d3d6e5b..3d06d3e 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -10,7 +10,8 @@ import time import numpy from Helper.Frequency import Frequency -from Helper import RedPitaya, Benchmark, Dat, Log, Tools, Pattern, Execution, XMC4500, Result, EstimateSpill, GenE, Python +from Helper import RedPitaya, Benchmark, Dat, Log, Tools, Pattern, Execution, Result, EstimateSpill, GenE, Python +from Backends import XMC4500 import Analyzers CPU = "xmc4500" diff --git a/Commands/etime.py b/Commands/etime.py index 53f93d7..3685997 100644 --- a/Commands/etime.py +++ b/Commands/etime.py @@ -11,7 +11,8 @@ import Queue import threading import multiprocessing -from Helper import Log, Tools, Pattern, Execution, XMC4500, GenE, Python +from Helper import Log, Tools, Pattern, Execution, GenE, Python +from Backends import XMC4500 from Helper.Frequency import Frequency CPU = "xmc4500" -- GitLab From 3f70bc2bc0db41ffa0512928a8c97a2b283568b4 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 17 Mar 2017 10:47:57 +0100 Subject: [PATCH 40/97] Toggle RP Leds when benchmarking --- Commands/bench_input_arm_redpitaya.py | 4 ++++ Helper/RedPitaya.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 3d06d3e..925889c 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -54,10 +54,14 @@ def benchmark_input_run(args, osci_queue, pipe, input_array, xmc, rp, eic): cycles = numpy.zeros(len(input_array), numpy.uint32) + rp.led_on() + for i in xrange(len(input_array)): cycles[i], buff = __get_sample(xmc, rp, input_array[i], args.pitaya_in) osci_queue.put( (i, buff) ) + rp.led_off() + Log.debug("Sending poison pill to " + str(multiprocessing.cpu_count()) + " threads...") for i in xrange(0, multiprocessing.cpu_count()): osci_queue.put( (None, None) ) diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index 2b4bc81..e69fa2e 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -9,6 +9,7 @@ class RedPitaya(object): self.max_sampling_rate = 125 * 1000 * 1000 # 125 MS/s self.rp = scpi.scpi(ip) self.send('ACQ:DATA:UNITS VOLTS') + self.led_off() # (constant) device information self.buffer_size = int(self.send_recv('ACQ:BUF:SIZE?')) @@ -88,6 +89,14 @@ class RedPitaya(object): return float(samples * dec) / float(125)# * 1000 * 1000) / float(self.max_sampling_rate) + def led_on(self, leds=range(0, 8)): + for led in leds: + self.send("DIG:PIN LED" + str(led) + ",1") + + def led_off(self, leds=range(0, 8)): + for led in leds: + self.send("DIG:PIN LED" + str(led) + ",0") + def samples_high_to_low(raw, level_low, level_high): i = len(raw) - 1 is_high = False -- GitLab From 68c2860ac81702c21d95045a172b3f6a328a4520 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 17 Mar 2017 13:04:33 +0100 Subject: [PATCH 41/97] Implement command switch --no-analyzer --- Analyzers/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Analyzers/__init__.py b/Analyzers/__init__.py index 8b49654..408ff6f 100644 --- a/Analyzers/__init__.py +++ b/Analyzers/__init__.py @@ -14,6 +14,10 @@ def get_modules(prefix): return res def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_noff = True): + if args.noanalyzer: + Log.info("Timing Analysis disabled by user!") + return + ffs = [] if run_ff: ffs.append(True) if run_noff: ffs.append(False) @@ -41,6 +45,7 @@ def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_n yield (name + ("" if has_ff else "_noff"), result) def add_arguments(parser): + parser.add_argument('--no-analyzer', dest='noanalyzer', action='store_true', help='Disable execution of all WCET analyzers (overwrites --no-*)') modules = get_modules('Analyzers.') for name, _ in modules.iteritems(): parser.add_argument('--no-' + name.lower(), dest='no' + name.lower(), action='store_true', help='Disable execution of ' + name) -- GitLab From ff849137a14b944dfc44a4af461757e2e50453ea Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 17 Mar 2017 14:38:46 +0100 Subject: [PATCH 42/97] Move RP measurement wrapper to Helper/RedPitaya.py --- Commands/bench_input_arm_redpitaya.py | 25 +++------------------- Helper/RedPitaya.py | 30 ++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 925889c..342244f 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -22,27 +22,7 @@ __tlevel_high_low = 2.5 __deci = 64 def __get_sample(xmc, rp, inp, rp_in, rep_limit = 5): - offset = - (rp.buffer_size / 2 - 1) - rp.set_trigger('CH' + str(rp_in) + '_NE', 1000*__tlevel_high_low, offset) - - rep = 0 - raw = None - while rep < rep_limit: - rp.set_gain('HV') - rp.set_decimation(__deci) - - rp.start_acquire() - cy = xmc.benchmark(inp) - - if not rp.get_trigger_status(): - raw = rp.get_data() - rp.stop_acquire() - break - - rep += 1 - Log.warn("Repeating measurement for input " + str(inp)) - - return (cy, raw) + return rp.measure(xmc, inp, __tlevel_high_low, channel=rp_in) def benchmark_input_run(args, osci_queue, pipe, input_array, xmc, rp, eic): Log.debug("Trying to connect to XMC4500...") @@ -55,6 +35,8 @@ def benchmark_input_run(args, osci_queue, pipe, input_array, xmc, rp, eic): cycles = numpy.zeros(len(input_array), numpy.uint32) rp.led_on() + rp.set_gain('HV') + rp.set_decimation(__deci) for i in xrange(len(input_array)): cycles[i], buff = __get_sample(xmc, rp, input_array[i], args.pitaya_in) @@ -279,7 +261,6 @@ def run(args): dat.samples()[i] = sample_delta dat.osci_raw()[i] = osci_raw deltas[i] = dat.cycles()[i] - (sample_delta * __deci * 120 / 125) - Log.info("deltas[" + str(i) + "] = " + str(deltas[i]) + " = " + str(dat.cycles()[i]) + " - " + str(sample_delta * __deci * 120 / 125)) del samples Log.operation_end("Wait for Sampling") diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index e69fa2e..8804ff3 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -16,11 +16,14 @@ class RedPitaya(object): def send(self, string): assert None != self.rp + Log.debug("send: " + string) self.rp.tx_txt(string) def recv(self): assert None != self.rp - return self.rp.rx_txt() + res = self.rp.rx_txt() + Log.debug("read: " + res) + return res def send_recv(self, string): self.send(string) @@ -97,6 +100,31 @@ class RedPitaya(object): for led in leds: self.send("DIG:PIN LED" + str(led) + ",0") + def measure(self, backend, inp, tlevel_high_low, rep_limit = 5, channel=None): + if channel == None: + channel = self.def_input + + offset = - (self.buffer_size / 2 - 1) + self.set_trigger('CH' + str(channel) + '_NE', 1000*tlevel_high_low, offset) + + rep = 0 + raw = None + while rep < rep_limit: + self.start_acquire() + cy = backend.benchmark(inp) + + if not self.get_trigger_status(): + raw = self.get_data() + self.stop_acquire() + break + + rep += 1 + Log.warn("Repeating measurement for input " + str(inp)) + + return (cy, raw) + + + def samples_high_to_low(raw, level_low, level_high): i = len(raw) - 1 is_high = False -- GitLab From c3c020554a794f28239f52d090d56c8d3c83d27f Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 17 Mar 2017 20:58:31 +0100 Subject: [PATCH 43/97] Increase number of connection retries to 5 --- Backends/XMC4500.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backends/XMC4500.py b/Backends/XMC4500.py index d2110fb..8454eb4 100644 --- a/Backends/XMC4500.py +++ b/Backends/XMC4500.py @@ -147,7 +147,7 @@ class XMC4500(object): assert None != tty, "failed to obtain tty for " + str(self) retry = 0 - MAX_RETRY = 3 + MAX_RETRY = 5 last_errno = None while None == self.dev and retry < MAX_RETRY: -- GitLab From c4a0384278293e492c41b3c11197f995404d566b Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Sat, 18 Mar 2017 11:55:29 +0100 Subject: [PATCH 44/97] Implement commandline options for color (demo) --- Commands/bench_input_arm_demo.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index dc0388a..2625d26 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -26,7 +26,7 @@ CPU_TYPE = "cortex-m4" CHUNK_SIZE = 1000 -def hist_to_file(cycles, idx, width, height, analyzers): +def hist_to_file(args, cycles, idx, width, height, analyzers): fle = tempfile.mktemp(suffix='.png') import matplotlib @@ -45,22 +45,22 @@ def hist_to_file(cycles, idx, width, height, analyzers): # uncomment this line to remove the plot's background #axes.patch.set_visible(False) - n, bins, patches = axes.hist(cycles[0:idx], bins=500, facecolor='green', alpha=0.5) + n, bins, patches = axes.hist(cycles[0:idx], bins=500, facecolor=args.color_bin, alpha=0.5) ana_y = axes.get_ylim()[0] + 0.8*(axes.get_ylim()[1] - axes.get_ylim()[0]) minval = numpy.amin(cycles[0:idx]) axes.axvline(x=minval, ymin=0, ymax=1, linewidth=2, color='m') - axes.text(minval, ana_y, ' BOET', fontsize='xx-large', ha='left', va='bottom', color='m') + axes.text(minval, ana_y, ' BOET', ha='left', va='bottom', color='m') maxval = numpy.amax(cycles[0:idx]) - axes.axvline(x=maxval, ymin=0, ymax=1, linewidth=2, color='g') - axes.text(maxval, ana_y, 'WCET', fontsize='xx-large', ha='right', va='bottom', color='g') + axes.axvline(x=maxval, ymin=0, ymax=1, linewidth=2, color=args.color_wcet) + axes.text(maxval, ana_y, 'WCET', ha='right', va='bottom', color=args.color_wcet) for name, result in analyzers.iteritems(): if result > 0: - axes.axvline(x=result, ymin=0, ymax=1, linewidth=2, color='m') - axes.text(result, ana_y, ' ' + name, fontsize='xx-large', ha='left', va='bottom', color='m') + axes.axvline(x=result, ymin=0, ymax=1, linewidth=2, color=args.color_analyzer) + axes.text(result, ana_y, ' ' + name, ha='left', va='bottom', color=args.color_analyzer) fig.tight_layout(pad=0, h_pad=0, w_pad=0) fig.set_tight_layout(True) @@ -232,7 +232,7 @@ def benchmark_input_run(pipe, args, input_array, xmc, eic, sw): if None != name: analyzers[name] = res - fle = hist_to_file(cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers) + fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers) pipe.send( (fle, i) ) while True: @@ -240,7 +240,7 @@ def benchmark_input_run(pipe, args, input_array, xmc, eic, sw): if None != name: analyzers[name] = res - fle = hist_to_file(cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers) + fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers) pipe.send( (fle, len(input_array)) ) pipe.send( (None, None) ) @@ -267,6 +267,17 @@ def get_argparser(): GenE.add_arguments(parser) Analyzers.add_arguments(parser) + def __register_color(name, label, default): + parser.add_argument('--color-' + name, + dest='color_' + name, + metavar='color', + default=default, + type=str, + help='Color for ' + label + ' (default: ' + default + ')') + __register_color('analyzer', 'upper bounds from WCET analyzers', '#19B219') + __register_color('wcet', 'Worst Observed Execution Time', '#B41C2E') + __register_color('bin', 'Facecolor of Bins', '#00669E') + return parser def run(args): -- GitLab From 89cb544cd4abc0da5726f78d196280628db4868f Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Sat, 18 Mar 2017 15:15:34 +0100 Subject: [PATCH 45/97] Automatically maximize DemoWindow --- Gui/DemoWindow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gui/DemoWindow.py b/Gui/DemoWindow.py index 630baf2..b4e4354 100644 --- a/Gui/DemoWindow.py +++ b/Gui/DemoWindow.py @@ -38,3 +38,5 @@ class DemoWindow(QtWidgets.QMainWindow): self.setCentralWidget(self.main_widget) self.main_widget.setFocus() + + QtCore.QTimer.singleShot(100, self.showFullScreen); -- GitLab From 60a751aac26163a8cdfcadb7fd23c4c535dc85e5 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Sun, 19 Mar 2017 22:52:46 +0100 Subject: [PATCH 46/97] Add command demo_dryrun that prepares the demo configuration --- Commands/bench_input_arm_demo.py | 57 +++++++--- Commands/demo_dryrun.py | 173 +++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 Commands/demo_dryrun.py diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 2625d26..ade919d 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -14,6 +14,8 @@ from thread import start_new_thread from PyQt5 import QtWidgets from Gui.DemoWindow import DemoWindow +import xml.etree.ElementTree as ET + from Helper.Frequency import Frequency from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python from Backends import XMC4500 @@ -26,9 +28,11 @@ CPU_TYPE = "cortex-m4" CHUNK_SIZE = 1000 -def hist_to_file(args, cycles, idx, width, height, analyzers): +def hist_to_file(args, cycles, idx, width, height, analyzers, xmax, ymax): fle = tempfile.mktemp(suffix='.png') + linewidth = 1 + import matplotlib matplotlib.use('Agg') fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) @@ -45,21 +49,24 @@ def hist_to_file(args, cycles, idx, width, height, analyzers): # uncomment this line to remove the plot's background #axes.patch.set_visible(False) + axes.set_xlim((0, xmax)) + axes.set_ylim((0, ymax)) + n, bins, patches = axes.hist(cycles[0:idx], bins=500, facecolor=args.color_bin, alpha=0.5) ana_y = axes.get_ylim()[0] + 0.8*(axes.get_ylim()[1] - axes.get_ylim()[0]) minval = numpy.amin(cycles[0:idx]) - axes.axvline(x=minval, ymin=0, ymax=1, linewidth=2, color='m') + axes.axvline(x=minval, ymin=0, ymax=1, linewidth=linewidth, color='m') axes.text(minval, ana_y, ' BOET', ha='left', va='bottom', color='m') maxval = numpy.amax(cycles[0:idx]) - axes.axvline(x=maxval, ymin=0, ymax=1, linewidth=2, color=args.color_wcet) + axes.axvline(x=maxval, ymin=0, ymax=1, linewidth=linewidth, color=args.color_wcet) axes.text(maxval, ana_y, 'WCET', ha='right', va='bottom', color=args.color_wcet) for name, result in analyzers.iteritems(): if result > 0: - axes.axvline(x=result, ymin=0, ymax=1, linewidth=2, color=args.color_analyzer) + axes.axvline(x=result, ymin=0, ymax=1, linewidth=linewidth, color=args.color_analyzer) axes.text(result, ana_y, ' ' + name, ha='left', va='bottom', color=args.color_analyzer) fig.tight_layout(pad=0, h_pad=0, w_pad=0) @@ -68,7 +75,7 @@ def hist_to_file(args, cycles, idx, width, height, analyzers): return fle -def benchmark_worker(args, sw, xmc, no, seed): +def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): odir = "results/aladdin_rtas17_demo/benchmark" + str(no) if not os.path.exists(odir): @@ -82,11 +89,11 @@ def benchmark_worker(args, sw, xmc, no, seed): dat = Dat.Data() dat.seed = seed - dat.pattern = Pattern.assemble_string(args.suite) + dat.pattern = Pattern.assemble_string(suite) ## generate the benchmark sw.update_message.emit("Generating Benchmark...") - bm = GenE.get_benchmark(Pattern.PATTERN[args.suite], CPU_TYPE, args.cost, args.bits, seed, ll, opt=0, of=args.ofactor) + bm = GenE.get_benchmark(Pattern.PATTERN[suite], CPU_TYPE, args.cost, args.bits, seed, ll, opt=0, of=args.ofactor) Tools.llvm_lls_to_ll([ll], ll_comb) Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) @@ -115,7 +122,7 @@ def benchmark_worker(args, sw, xmc, no, seed): procs = [] pipe_parent, pipe_child = Pipe() - p = Process(target=benchmark_input_run, args=(pipe_child, args, inp, xmc, args.eic, sw)) + p = Process(target=benchmark_input_run, args=(pipe_child, args, inp, xmc, args.eic, sw, xmax, ymax)) p.start() procs.append({'proc':p, 'pos':0, 'pipe':pipe_parent, 'input':inp}) @@ -127,7 +134,7 @@ def benchmark_worker(args, sw, xmc, no, seed): analyzers = ['aiT', 'platin'] - analyzer_disabled = ['platin'] + analyzer_disabled = [] sw.update_message.emit("Sampling...") @@ -208,13 +215,32 @@ def worker(args, dw): if None == xmcs: return + bms = [] + tree = ET.parse(args.config) + + for conf in tree.getroot(): + seed = int(conf.attrib['seed']) + budget = int(conf.attrib['budget']) + bits = int(conf.attrib['bits']) + ofactor = int(conf.attrib['ofactor']) + samples = int(conf.attrib['samples']) + xmax = int(conf.attrib['xmax']) + ymax = int(conf.attrib['ymax']) + eic = bool(conf.attrib['eic']) + suite = conf.attrib['suite'] + opt = 0 + entry = 'gene_main' + + bms.append( (seed, xmax, ymax) ) + for i in xrange(0, min(len(xmcs), COUNT_BMS)): - start_new_thread(benchmark_worker, (args, dw.sws[i], xmcs[i], i, 1431655765 + i)) + seed, xmax, ymax = bms[i] + start_new_thread(benchmark_worker, (args, dw.sws[i], xmcs[i], i, seed, suite, xmax, ymax)) while True: continue -def benchmark_input_run(pipe, args, input_array, xmc, eic, sw): +def benchmark_input_run(pipe, args, input_array, xmc, eic, sw, xmax, ymax): xmc.connect() xmc.set_ic(eic) @@ -232,7 +258,7 @@ def benchmark_input_run(pipe, args, input_array, xmc, eic, sw): if None != name: analyzers[name] = res - fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers) + fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers, xmax, ymax) pipe.send( (fle, i) ) while True: @@ -240,7 +266,7 @@ def benchmark_input_run(pipe, args, input_array, xmc, eic, sw): if None != name: analyzers[name] = res - fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers) + fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers, xmax, ymax) pipe.send( (fle, len(input_array)) ) pipe.send( (None, None) ) @@ -264,6 +290,8 @@ def get_argparser(): parser.add_argument('--eic', dest='eic', action='store_true', help='Enable instruction cache') parser.add_argument('--no-sampling', dest='nosampling', action='store_true', help='Disable sampling') + parser.add_argument('config', metavar='config', type=str, help='The demo configuration file') + GenE.add_arguments(parser) Analyzers.add_arguments(parser) @@ -284,6 +312,9 @@ def run(args): global plt plt = Python.import_pyplot('Agg') + Analyzers.prepare(args) + return + qApp = QtWidgets.QApplication(sys.argv) dw = DemoWindow(COUNT_BMS, 100) diff --git a/Commands/demo_dryrun.py b/Commands/demo_dryrun.py new file mode 100644 index 0000000..cc2c499 --- /dev/null +++ b/Commands/demo_dryrun.py @@ -0,0 +1,173 @@ +#!/usr/bin/python + +from multiprocessing import Process, Pipe +import argparse as ap +import os +import shutil +import sys +import time +import numpy +import threading +import tempfile +from thread import start_new_thread + +import xml.etree.ElementTree as ET + +from Helper.Frequency import Frequency +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python +from Backends import XMC4500 +import Analyzers + +COUNT_BMS = 10 + +CPU = "xmc4500" +CPU_TYPE = "cortex-m4" + +def benchmark_input_run(pipe, input_array, xmc, eic): + xmc.connect() + xmc.set_ic(eic) + + cycles = numpy.zeros(len(input_array), numpy.uint32) + + for i in xrange(len(input_array)): + cycles[i] = xmc.benchmark(input_array[i]) + assert cycles[i], "Invalid value from benchmark()" + + pipe.send(cycles) + pipe.close() + +def get_argparser(): + parser = ap.ArgumentParser(prog=sys.argv[0] + " bench_input", + formatter_class=ap.ArgumentDefaultsHelpFormatter, + description='Subfunction demo_dryrun: ' + desc) + + parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking') + parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') + parser.add_argument('--eic', dest='eic', action='store_true', help='Enable instruction cache') + + parser.add_argument('config', metavar='config', type=str, help='The demo configuration file') + + GenE.add_arguments(parser) + Analyzers.add_arguments(parser) + + return parser + +def run(args): + tree = ET.parse(args.config) + + odir = './results/demo_dryrun' + + if not os.path.exists(odir): + os.makedirs(odir) + + xmcs = XMC4500.list_devices(odir, args.xmccache) + assert len(xmcs) > 0 + xmc = xmcs[0] + + for conf in tree.getroot(): + seed = int(conf.attrib['seed']) + budget = int(conf.attrib['budget']) + bits = int(conf.attrib['bits']) + ofactor = int(conf.attrib['ofactor']) + samples = int(conf.attrib['samples']) + eic = bool(conf.attrib['eic']) + suite = conf.attrib['suite'] + opt = 0 + entry = 'gene_main' + + dat = odir + '/bench.dat' + ll = odir + '/bench.ll' + ll_comb = odir + '/combined.ll' + o = odir + '/bench.o' + pml = odir + '/bench.pml' + axf = odir + '/bench.axf' + + + dat = Dat.Data() + dat.seed = seed + dat.pattern = Pattern.assemble_string(suite) + + bm = GenE.get_benchmark(Pattern.PATTERN[suite], CPU_TYPE, budget, 32, seed, ll, opt, ofactor) + Log.operation_start(" * Compile Benchmark") + Tools.llvm_lls_to_ll([ll], ll_comb) + Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt) + Log.operation_end(" * Compile Benchmark") + + Log.operation_start("Build system for XMC4500") + shutil.copyfile(o, "./xmc4500/gene.o") + Execution.run_wait("make -C ./xmc4500 clean bin/system_gene.axf", None, 0) + shutil.copyfile("./xmc4500/bin/system_gene.axf", axf) + Log.operation_end("Build system for XMC4500") + + + Log.info("Flash system to XMC4500s") + success = False + for retry in xrange(1, 4): + Log.start(" * Flash system to " + xmc.serial + " (try " + str(retry) + ")") + error = xmc.flash(odir, "./xmc4500/bin/system_gene.axf") + Log.end(error, error != None) + + if error == None: + success = True + break + + if not success: + Log.fail("Failed to flash to device, aborting") + return + + time.sleep(2) + + Log.start("Start 1 Sampling Threads") + input_chunks = Benchmark.get_input(bits, seed, samples, 1) + + # get the total number of samples, which does not + # equal args.samples due to special input values + sample_count = sum(len(chunk) for chunk in input_chunks) + + procs = [] + pipe_parent, pipe_child = Pipe() + p = Process(target=benchmark_input_run, args=(pipe_child, input_chunks[0], xmc, eic)) + p.start() + procs.append({'proc':p, 'pipe':pipe_parent, 'input':input_chunks[0]}) + + Log.end() + + + Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) + + for res in Analyzers.run(args, odir, dat, CPU, axf, entry, pml, None, eic, run_noff = False): + pass + + dat.register_column('inputs$u', sample_count) + dat.register_column('cycles$u', sample_count) + + idx = 0 + for proc in procs: + cycles = proc['pipe'].recv() + proc['proc'].join() + + input_chunk = proc['input'] + + for i in xrange(len(cycles)): + dat.inputs()[idx] = input_chunk[i] + dat.cycles()[idx] = cycles[i] + idx += 1 + + del input_chunk + del cycles + Log.operation_end("Wait for Sampling") + + # get rid of unused arrays + del input_chunks + del procs + + xmax = max(dat.max, dat.wcet['ait'], dat.wcet['platin']) + hist, _ = numpy.histogram(dat.cycles(), bins=500) + ymax = max(hist) + + conf.attrib['xmax'] = str(xmax) + conf.attrib['ymax'] = str(ymax) + + tree.write(args.config) + +desc = "[ARM] Benchmark GenE using varying values as input to the generated executable" -- GitLab From 76ec79d68ffa5df0e26881c62860e3faa6e51e44 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Sun, 19 Mar 2017 22:53:14 +0100 Subject: [PATCH 47/97] Implement Analyzers.prepare() [TODO: aiT.prepare() not yet working] --- Analyzers/__init__.py | 15 +++++++++++++++ Analyzers/aiT.py | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/Analyzers/__init__.py b/Analyzers/__init__.py index 408ff6f..2cec1c9 100644 --- a/Analyzers/__init__.py +++ b/Analyzers/__init__.py @@ -44,6 +44,21 @@ def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_n yield (name + ("" if has_ff else "_noff"), result) +def prepare(args): + if args.noanalyzer: + Log.info("Timing Analysis disabled by user!") + return + + Log.info("Preparing WCET analyzers") + modules = get_modules('Analyzers.') + for name, mod in modules.iteritems(): + if getattr(args, 'no' + name.lower()): + continue + + if hasattr(mod, 'prepare'): + Log.debug("Executing " + name + ".prepare()") + mod.prepare(args) + def add_arguments(parser): parser.add_argument('--no-analyzer', dest='noanalyzer', action='store_true', help='Disable execution of all WCET analyzers (overwrites --no-*)') modules = get_modules('Analyzers.') diff --git a/Analyzers/aiT.py b/Analyzers/aiT.py index 1d40fdc..7f73397 100644 --- a/Analyzers/aiT.py +++ b/Analyzers/aiT.py @@ -137,4 +137,10 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silen return AnalyzerResult(has_ff, result, len(unb_loops) > 0 or result < 0, "", unb_loops) +def prepare(args): + proc = Execution.run_wait("ssh-add -l") + if 0 != proc.retcode: + #TODO: do not use run_wait, does not wait for user input... + Execution.run_wait("ssh-add") + supported_backends = ["xmc4500"] -- GitLab From 329074f52146fd3f8e13208e1161e29fef81e1d6 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 20 Mar 2017 13:09:56 +0100 Subject: [PATCH 48/97] Use XMC4500.flash_retry() instead of loop around XMC4500.flash() --- Commands/bench_input_arm.py | 14 ++++---------- Commands/bench_input_arm_redpitaya.py | 14 ++++---------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/Commands/bench_input_arm.py b/Commands/bench_input_arm.py index 21f1673..54297c5 100644 --- a/Commands/bench_input_arm.py +++ b/Commands/bench_input_arm.py @@ -111,18 +111,12 @@ def run(args): if not args.nosampling: Log.info("Flash system to XMC4500s") for xmc in xmcs: - success = False - for retry in xrange(1, 4): - Log.start(" * Flash system to " + xmc.serial + " (try " + str(retry) + ")") - error = xmc.flash(odir, "./xmc4500/bin/system_gene.axf") - Log.end(error, error != None) - - if error == None: - success = True - break - + Log.info(" * Flash system to " + xmc.serial) + success, errors = xmc.flash_retry(odir, "./xmc4500/bin/system_gene.axf") if not success: Log.fail("Failed to flash to device, aborting") + for error in errors: + Log.fail(" * " + error) return time.sleep(2) diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 342244f..0dcfa7c 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -159,18 +159,12 @@ def run(args): Log.info("Flash system to XMC4500s") for xmc in xmcs: - success = False - for retry in xrange(1, 4): - Log.start(" * Flash system to " + xmc.serial + " (try " + str(retry) + ")") - error = xmc.flash(odir, "./xmc4500/bin/system_gene.axf") - Log.end(error, error != None) - - if error == None: - success = True - break - + Log.info(" * Flash system to " + xmc.serial) + success, errors = xmc.flash_retry(odir, "./xmc4500/bin/system_gene.axf") if not success: Log.fail("Failed to flash to device, aborting") + for error in errors: + Log.fail(" * " + error) return time.sleep(2) -- GitLab From 495f1728bcff1570209a963d41b650db3ccf1859 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 20 Mar 2017 13:59:13 +0100 Subject: [PATCH 49/97] Improve various aspects of RTAS'17 demo --- Commands/bench_input_arm_demo.py | 202 ++++++++++++++++++------------- Commands/demo_dryrun.py | 64 +++------- Gui/DemoWindow.py | 13 +- Gui/ResizeLabel.py | 19 +++ Gui/SamplingWidget.py | 24 +--- Gui/SummaryWidget.py | 78 ++++++++++++ Helper/Config.py | 65 ++++++++++ 7 files changed, 311 insertions(+), 154 deletions(-) create mode 100644 Gui/ResizeLabel.py create mode 100644 Gui/SummaryWidget.py create mode 100644 Helper/Config.py diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index ade919d..fab22ab 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -9,65 +9,97 @@ import time import numpy import threading import tempfile +import matplotlib from thread import start_new_thread from PyQt5 import QtWidgets from Gui.DemoWindow import DemoWindow -import xml.etree.ElementTree as ET - from Helper.Frequency import Frequency -from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python, Config from Backends import XMC4500 import Analyzers -COUNT_BMS = 10 - CPU = "xmc4500" CPU_TYPE = "cortex-m4" CHUNK_SIZE = 1000 -def hist_to_file(args, cycles, idx, width, height, analyzers, xmax, ymax): +def geomean(a): + return numpy.exp( numpy.log(a).mean() ) + +def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, xmax, ymax): + fle = tempfile.mktemp(suffix='.png') linewidth = 1 - import matplotlib - matplotlib.use('Agg') fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) fig.set_alpha(0.0) axes = fig.add_subplot(111) fig.subplots_adjust(hspace=0, wspace=0) + # uncomment the following line to remove the plot's background + #axes.patch.set_visible(False) + axes.set_xlim((0, xmax)) + axes.set_ylim((0, ymax)) + axes.yaxis.grid(True) + + if None != cycles: + _, bins, _ = axes.hist(cycles[0:idx], bins=500, facecolor=args.color_bin, alpha=0.5) + + ana_y = axes.get_ylim()[0] + 0.8*(axes.get_ylim()[1] - axes.get_ylim()[0]) + + minval = numpy.amin(cycles[0:idx]) + axes.axvline(x=minval, ymin=0, ymax=1, linewidth=linewidth, color='m') + axes.text(minval, ana_y, ' BOET', ha='left', va='bottom', color='m') + + maxval = numpy.amax(cycles[0:idx]) + axes.axvline(x=maxval, ymin=0, ymax=1, linewidth=linewidth, color=args.color_wcet) + axes.text(maxval, ana_y, 'WCET', ha='right', va='bottom', color=args.color_wcet) + + for name, result in analyzers.iteritems(): + if result > 0: + axes.axvline(x=result, ymin=0, ymax=1, linewidth=linewidth, color=args.color_analyzer) + axes.text(result, ana_y, ' ' + name, ha='left', va='bottom', color=args.color_analyzer) + fig.tight_layout(pad=0, h_pad=0, w_pad=0) fig.set_tight_layout(True) - axes.yaxis.grid(True) - fig.patch.set_visible(False) + fig.savefig(fle, transparent=True, dpi=100) - # uncomment this line to remove the plot's background - #axes.patch.set_visible(False) + return fle - axes.set_xlim((0, xmax)) - axes.set_ylim((0, ymax)) +def anahist_to_file(args, factors, width, height, xmax, ymax): + fle = tempfile.mktemp(suffix='.png') + + fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) + fig.set_alpha(0.0) + axes = fig.add_subplot(111) + + from matplotlib.ticker import MaxNLocator + axes.yaxis.set_major_locator(MaxNLocator(integer=True)) + + arr = numpy.array(factors) - 1.0 + + mins = numpy.min(arr) + maxs = numpy.max(arr) + geos = geomean(arr) - n, bins, patches = axes.hist(cycles[0:idx], bins=500, facecolor=args.color_bin, alpha=0.5) + geo_valid = geomean( [ f for f in arr if f >= 0.0 ] ) - ana_y = axes.get_ylim()[0] + 0.8*(axes.get_ylim()[1] - axes.get_ylim()[0]) + # dump histograms + #if len(ubs[ana]) == 0: continue + valid = [100*f for f in arr if f >= 0.0] + invalid = [100*f for f in arr if f < 0.0] - minval = numpy.amin(cycles[0:idx]) - axes.axvline(x=minval, ymin=0, ymax=1, linewidth=linewidth, color='m') - axes.text(minval, ana_y, ' BOET', ha='left', va='bottom', color='m') + bins = range(int(100 * mins), int(100 * maxs), 1) - maxval = numpy.amax(cycles[0:idx]) - axes.axvline(x=maxval, ymin=0, ymax=1, linewidth=linewidth, color=args.color_wcet) - axes.text(maxval, ana_y, 'WCET', ha='right', va='bottom', color=args.color_wcet) + axes.hist(valid, bins, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) + axes.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0.1, alpha=0.5) - for name, result in analyzers.iteritems(): - if result > 0: - axes.axvline(x=result, ymin=0, ymax=1, linewidth=linewidth, color=args.color_analyzer) - axes.text(result, ana_y, ' ' + name, ha='left', va='bottom', color=args.color_analyzer) + axes.axvline(x=100 * geos, ymin=0, ymax=1, linewidth=1, color='g') + axes.axvline(x=0, ymin=0, ymax=1, linewidth=2, color=args.color_wcet) fig.tight_layout(pad=0, h_pad=0, w_pad=0) fig.set_tight_layout(True) @@ -75,7 +107,7 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, xmax, ymax): return fle -def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): +def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): odir = "results/aladdin_rtas17_demo/benchmark" + str(no) if not os.path.exists(odir): @@ -87,22 +119,29 @@ def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): pml = odir + '/bench.pml' axf = odir + '/bench.axf' + time.sleep(2) + + # initialize the widgets with empty plots + sw.update_sampling.emit( + hist_to_file(args, None, 0, sw.mpWidth.value, sw.mpHeight.value, {}, None, None, conf.xmax, conf.ymax) + ) + dat = Dat.Data() - dat.seed = seed - dat.pattern = Pattern.assemble_string(suite) + dat.seed = conf.seed + dat.pattern = Pattern.assemble_string(conf.suite) ## generate the benchmark sw.update_message.emit("Generating Benchmark...") - bm = GenE.get_benchmark(Pattern.PATTERN[suite], CPU_TYPE, args.cost, args.bits, seed, ll, opt=0, of=args.ofactor) + bm = GenE.get_benchmark(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, ll, opt=conf.opt, of=conf.ofactor) Tools.llvm_lls_to_ll([ll], ll_comb) Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) ## compile the benchmark OS sw.update_message.emit("Compile Benchmark...") - o_xmc = "./xmc4500/gene_seed" + str(seed) + ".o" + o_xmc = "./xmc4500/gene_seed" + str(conf.seed) + ".o" shutil.copyfile(o, o_xmc) - Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(seed) + ".axf", None, 0) - shutil.move("./xmc4500/bin/system_gene_seed" + str(seed) + ".axf", axf) + Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(conf.seed) + ".axf", None, 0) + shutil.move("./xmc4500/bin/system_gene_seed" + str(conf.seed) + ".axf", axf) os.remove(o_xmc) ## flash to devices @@ -115,27 +154,23 @@ def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): time.sleep(2) - inp = Benchmark.get_input(args.bits, seed, args.samples, 1)[0] + inp = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1)[0] # get the total number of samples, which does not - # equal args.samples due to special input values + # equal conf.samples due to special input values sample_count = len(inp) procs = [] pipe_parent, pipe_child = Pipe() - p = Process(target=benchmark_input_run, args=(pipe_child, args, inp, xmc, args.eic, sw, xmax, ymax)) + p = Process(target=benchmark_input_run, args=(pipe_child, args, inp, xmc, conf.eic, sw, conf.xmax, conf.ymax)) p.start() procs.append({'proc':p, 'pos':0, 'pipe':pipe_parent, 'input':inp}) Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) pipe_ana, pipe_child = Pipe() - proc_ana = Process(target=analyzers_run, args=(pipe_child, args, odir, dat, axf, pml)) + proc_ana = Process(target=analyzers_run, args=(pipe_child, args, conf, odir, dat, axf, pml)) proc_ana.start() - - analyzers = ['aiT', 'platin'] - analyzer_disabled = [] - sw.update_message.emit("Sampling...") dat.inputs = numpy.zeros(sample_count, numpy.uint32) @@ -147,13 +182,15 @@ def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): idx = 0 finished = 0 analyzer_results = {} + analyzers = ['aiT', 'platin'] while True: samples_changed = False for proc in procs: + # TODO: this? if None == proc['proc']: continue - fle, pos = proc['pipe'].recv() + wcet, fle, pos = proc['pipe'].recv() if None == fle: proc['proc'].join() proc['proc'] = None @@ -161,7 +198,7 @@ def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): sw.update_sampling.emit(fle) sw.update_message.emit(str(pos) + " samples") - sw.update_progress.emit(0, args.samples, pos) + sw.update_progress.emit(0, conf.samples, pos) ana_changed = False proc = procs[0] @@ -170,6 +207,7 @@ def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): name, result = pipe_ana.recv() analyzer_results[name] = result proc['pipe'].send( (name, result.cy) ) + sum_pipe.send( (name, float(result.cy)/wcet) ) if ana_changed: for name in analyzers: @@ -185,8 +223,6 @@ def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): else: msg += Dat.errno_to_string(result.cy) sw.update_analyzer.emit(name, 'error.png', msg) - elif name in analyzer_disabled: - sw.update_analyzer.emit(name, 'disabled.png', name) else: sw.update_analyzer.emit(name, 'working.png', name) @@ -202,8 +238,7 @@ def benchmark_worker(args, sw, xmc, no, seed, suite, xmax, ymax): # check that worst-observed execution time is triggered by worst-case input assert dat.max == dat.cy_seed, "Maximum execution cycles exceeds cycles used for seed" - -def worker(args, dw): +def worker(args, configs, dw): odir = "results/aladdin_rtas17_demo" if not os.path.exists(odir): @@ -215,29 +250,29 @@ def worker(args, dw): if None == xmcs: return - bms = [] - tree = ET.parse(args.config) + bm_count = min(len(xmcs), len(configs) + 1) - for conf in tree.getroot(): - seed = int(conf.attrib['seed']) - budget = int(conf.attrib['budget']) - bits = int(conf.attrib['bits']) - ofactor = int(conf.attrib['ofactor']) - samples = int(conf.attrib['samples']) - xmax = int(conf.attrib['xmax']) - ymax = int(conf.attrib['ymax']) - eic = bool(conf.attrib['eic']) - suite = conf.attrib['suite'] - opt = 0 - entry = 'gene_main' + pipe, pipe_child = Pipe() - bms.append( (seed, xmax, ymax) ) + for i in xrange(0, bm_count): + start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[i], xmcs[i], i, configs[i])) + + + factors = { + 'ait': [], + 'platin': [] + } + while True: + ana, factor = pipe.recv() + ana = ana.lower() - for i in xrange(0, min(len(xmcs), COUNT_BMS)): - seed, xmax, ymax = bms[i] - start_new_thread(benchmark_worker, (args, dw.sws[i], xmcs[i], i, seed, suite, xmax, ymax)) + factors[ana].append(factor) - while True: continue + if(len(factors[ana]) > 1): + fle = anahist_to_file(args, factors[ana], dw.sum[ana].mpWidth.value, dw.sum[ana].mpHeight.value, None, None) + dw.sum[ana].update_sampling.emit(fle) + + assert False, "Should never end up here" def benchmark_input_run(pipe, args, input_array, xmc, eic, sw, xmax, ymax): @@ -248,6 +283,9 @@ def benchmark_input_run(pipe, args, input_array, xmc, eic, sw, xmax, ymax): cycles = numpy.zeros(len(input_array), numpy.uint32) + boet = None + wcet = None + for i in xrange(0, len(input_array)): cycles[i] = xmc.benchmark(input_array[i]) assert cycles[i], "Invalid value from benchmark()" @@ -258,22 +296,25 @@ def benchmark_input_run(pipe, args, input_array, xmc, eic, sw, xmax, ymax): if None != name: analyzers[name] = res - fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers, xmax, ymax) - pipe.send( (fle, i) ) + boet = numpy.amin(cycles[0:i]) + wcet = numpy.amax(cycles[0:i]) + + fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers, boet, wcet, xmax, ymax) + pipe.send( (wcet, fle, i) ) while True: name, res = pipe.recv() if None != name: analyzers[name] = res - fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers, xmax, ymax) - pipe.send( (fle, len(input_array)) ) + fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers, boet, wcet, xmax, ymax) + pipe.send( (wcet, fle, len(input_array)) ) - pipe.send( (None, None) ) + pipe.send( (None, None, None) ) pipe.close() -def analyzers_run(pipe, args, odir, dat, axf, pml): - for name, res in Analyzers.run(args, odir, dat, CPU, axf, "gene_main", pml, None, args.eic, run_ff = True, run_noff = False): +def analyzers_run(pipe, args, conf, odir, dat, axf, pml): + for name, res in Analyzers.run(args, odir, dat, CPU, axf, "gene_main", pml, None, conf.eic, run_ff = True, run_noff = False): pipe.send( (name, res) ) pipe.close() @@ -284,13 +325,9 @@ def get_argparser(): formatter_class=ap.ArgumentDefaultsHelpFormatter, description='Subfunction bench_input_arm_demo: ' + desc) - parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking') parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') - - parser.add_argument('--eic', dest='eic', action='store_true', help='Enable instruction cache') - parser.add_argument('--no-sampling', dest='nosampling', action='store_true', help='Disable sampling') - parser.add_argument('config', metavar='config', type=str, help='The demo configuration file') + parser.add_argument('--title', metavar='title', type=str, default="Aladdin: Demo", help='The DemoWindow title') GenE.add_arguments(parser) Analyzers.add_arguments(parser) @@ -305,6 +342,7 @@ def get_argparser(): __register_color('analyzer', 'upper bounds from WCET analyzers', '#19B219') __register_color('wcet', 'Worst Observed Execution Time', '#B41C2E') __register_color('bin', 'Facecolor of Bins', '#00669E') + __register_color('binfail', 'Facecolor of Bins with ub/WCET < 1', 'red') return parser @@ -313,15 +351,15 @@ def run(args): plt = Python.import_pyplot('Agg') Analyzers.prepare(args) - return + configs = Config.from_xml(args.config) qApp = QtWidgets.QApplication(sys.argv) - dw = DemoWindow(COUNT_BMS, 100) - dw.setWindowTitle("Aladdin: RTAS Demo '17") + dw = DemoWindow(len(configs) + 1, 100) + dw.setWindowTitle(args.title) dw.show() - start_new_thread(worker, (args, dw)) + start_new_thread(worker, (args, configs, dw)) sys.exit(qApp.exec_()) diff --git a/Commands/demo_dryrun.py b/Commands/demo_dryrun.py index cc2c499..b8f095b 100644 --- a/Commands/demo_dryrun.py +++ b/Commands/demo_dryrun.py @@ -11,15 +11,10 @@ import threading import tempfile from thread import start_new_thread -import xml.etree.ElementTree as ET - -from Helper.Frequency import Frequency -from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python, Config from Backends import XMC4500 import Analyzers -COUNT_BMS = 10 - CPU = "xmc4500" CPU_TYPE = "cortex-m4" @@ -41,10 +36,7 @@ def get_argparser(): formatter_class=ap.ArgumentDefaultsHelpFormatter, description='Subfunction demo_dryrun: ' + desc) - parser.add_argument('--samples', metavar='count', type=int, default=10000, help='number of samples used for benchmarking') parser.add_argument('--xmccache', metavar='xmccache', type=str, default=None, help='XMC Cache file to be used') - parser.add_argument('--eic', dest='eic', action='store_true', help='Enable instruction cache') - parser.add_argument('config', metavar='config', type=str, help='The demo configuration file') GenE.add_arguments(parser) @@ -53,8 +45,6 @@ def get_argparser(): return parser def run(args): - tree = ET.parse(args.config) - odir = './results/demo_dryrun' if not os.path.exists(odir): @@ -64,33 +54,23 @@ def run(args): assert len(xmcs) > 0 xmc = xmcs[0] - for conf in tree.getroot(): - seed = int(conf.attrib['seed']) - budget = int(conf.attrib['budget']) - bits = int(conf.attrib['bits']) - ofactor = int(conf.attrib['ofactor']) - samples = int(conf.attrib['samples']) - eic = bool(conf.attrib['eic']) - suite = conf.attrib['suite'] - opt = 0 - entry = 'gene_main' - - dat = odir + '/bench.dat' + configs = Config.from_xml(args.config) + + for conf in configs: ll = odir + '/bench.ll' ll_comb = odir + '/combined.ll' o = odir + '/bench.o' pml = odir + '/bench.pml' axf = odir + '/bench.axf' - dat = Dat.Data() - dat.seed = seed - dat.pattern = Pattern.assemble_string(suite) + dat.seed = conf.seed + dat.pattern = Pattern.assemble_string(conf.suite) - bm = GenE.get_benchmark(Pattern.PATTERN[suite], CPU_TYPE, budget, 32, seed, ll, opt, ofactor) + bm = GenE.get_benchmark(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, ll, conf.opt, conf.ofactor) Log.operation_start(" * Compile Benchmark") Tools.llvm_lls_to_ll([ll], ll_comb) - Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt) + Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, conf.opt) Log.operation_end(" * Compile Benchmark") Log.operation_start("Build system for XMC4500") @@ -99,34 +79,26 @@ def run(args): shutil.copyfile("./xmc4500/bin/system_gene.axf", axf) Log.operation_end("Build system for XMC4500") - Log.info("Flash system to XMC4500s") - success = False - for retry in xrange(1, 4): - Log.start(" * Flash system to " + xmc.serial + " (try " + str(retry) + ")") - error = xmc.flash(odir, "./xmc4500/bin/system_gene.axf") - Log.end(error, error != None) - - if error == None: - success = True - break - + success, errors = xmc.flash_retry(odir, "./xmc4500/bin/system_gene.axf") if not success: Log.fail("Failed to flash to device, aborting") + for error in errors: + Log.fail(" * " + error) return time.sleep(2) Log.start("Start 1 Sampling Threads") - input_chunks = Benchmark.get_input(bits, seed, samples, 1) + input_chunks = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1) # get the total number of samples, which does not - # equal args.samples due to special input values + # equal conf.samples due to special input values sample_count = sum(len(chunk) for chunk in input_chunks) procs = [] pipe_parent, pipe_child = Pipe() - p = Process(target=benchmark_input_run, args=(pipe_child, input_chunks[0], xmc, eic)) + p = Process(target=benchmark_input_run, args=(pipe_child, input_chunks[0], xmc, conf.eic)) p.start() procs.append({'proc':p, 'pipe':pipe_parent, 'input':input_chunks[0]}) @@ -135,7 +107,7 @@ def run(args): Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) - for res in Analyzers.run(args, odir, dat, CPU, axf, entry, pml, None, eic, run_noff = False): + for res in Analyzers.run(args, odir, dat, CPU, axf, conf.entry, pml, None, conf.eic, run_noff = False): pass dat.register_column('inputs$u', sample_count) @@ -165,9 +137,9 @@ def run(args): hist, _ = numpy.histogram(dat.cycles(), bins=500) ymax = max(hist) - conf.attrib['xmax'] = str(xmax) - conf.attrib['ymax'] = str(ymax) + conf.xmax = xmax + conf.ymax = ymax - tree.write(args.config) + Config.to_xml(configs, args.config) desc = "[ARM] Benchmark GenE using varying values as input to the generated executable" diff --git a/Gui/DemoWindow.py b/Gui/DemoWindow.py index b4e4354..a3f7c37 100644 --- a/Gui/DemoWindow.py +++ b/Gui/DemoWindow.py @@ -2,6 +2,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5.QtCore import QObject, pyqtSignal from SamplingWidget import SamplingWidget +from SummaryWidget import SummaryWidget class DemoWindow(QtWidgets.QMainWindow): def __init__(self, sw_count, initial_ylim): @@ -30,11 +31,17 @@ class DemoWindow(QtWidgets.QMainWindow): self.bm_layouts[-1].addWidget(self.sws[-1]) self.bm_layouts[-1].setStretchFactor(self.sws[-1], 1) + self.sum_layout = QtWidgets.QVBoxLayout() + self.main_layout.addLayout(self.sum_layout) - test = SamplingWidget(self, initial_ylim) - self.main_layout.addWidget(test) + self.sum = { + 'ait': SummaryWidget(self, 'aiT', initial_ylim), + 'platin': SummaryWidget(self, 'platin', initial_ylim) + } + for _, sw in self.sum.iteritems(): + self.sum_layout.addWidget(sw) - self.main_layout.setStretchFactor(test, 3) + self.main_layout.setStretchFactor(self.sum_layout, 2) self.setCentralWidget(self.main_widget) self.main_widget.setFocus() diff --git a/Gui/ResizeLabel.py b/Gui/ResizeLabel.py new file mode 100644 index 0000000..f94291c --- /dev/null +++ b/Gui/ResizeLabel.py @@ -0,0 +1,19 @@ +from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import QObject, pyqtSignal + +class ResizeLabel(QtWidgets.QLabel): + def __init__(self, mpWidth, mpHeight): + QtWidgets.QLabel.__init__(self) + + self.setMinimumSize(1, 1) + #self.setScaledContents(True) + + self.mpWidth = mpWidth + self.mpHeight = mpHeight + + self.mpWidth.value = self.size().width() + self.mpHeight.value = self.size().height() + + def resizeEvent(self, evt): + self.mpWidth.value = evt.size().width() + self.mpHeight.value = evt.size().height() diff --git a/Gui/SamplingWidget.py b/Gui/SamplingWidget.py index 73c6fb5..53d2d62 100644 --- a/Gui/SamplingWidget.py +++ b/Gui/SamplingWidget.py @@ -1,26 +1,11 @@ from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5.QtCore import QObject, pyqtSignal +from Gui.ResizeLabel import ResizeLabel from Helper import Log import multiprocessing -class ResizeLabel(QtWidgets.QLabel): - def __init__(self, mpWidth, mpHeight): - QtWidgets.QLabel.__init__(self) - - self.setMinimumSize(1, 1) - #self.setScaledContents(True) - - self.mpWidth = mpWidth - self.mpHeight = mpHeight - - self.mpWidth.value = self.size().width() - self.mpHeight.value = self.size().height() - - def resizeEvent(self, evt): - self.mpWidth.value = evt.size().width() - self.mpHeight.value = evt.size().height() class SamplingWidget(QtWidgets.QFrame): update_message = pyqtSignal('QString') @@ -71,10 +56,6 @@ class SamplingWidget(QtWidgets.QFrame): self.setStyleSheet(".SamplingWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") def mousePressEvent(self, evt): - #Log.info("mousePressEvent for " + str(self)) - #Log.info("parent: " + str(self.parent())) - #Log.info("win: " + str(self.win)) - if self.maximized: self.maximized = False for sw in self.win.findChildren(SamplingWidget): @@ -155,12 +136,9 @@ class SamplingWidget(QtWidgets.QFrame): self.sampling = ResizeLabel(self.mpWidth, self.mpHeight) self.sampling.setMargin(0) self.sampling.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - #self.sampling.setScaledContents(True) # no - #self.sampling = SamplingPlot(initial_ylim, self.main_widget, width=5, height=4, dpi=100) self.main_layout.addWidget(self.sampling) self.main_widget.setFocus() - #self.setCentralWidget(self.main_widget) self.analyzers = {} self.samples = None diff --git a/Gui/SummaryWidget.py b/Gui/SummaryWidget.py new file mode 100644 index 0000000..ba11d12 --- /dev/null +++ b/Gui/SummaryWidget.py @@ -0,0 +1,78 @@ +from PyQt5 import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import QObject, pyqtSignal + +from Gui.ResizeLabel import ResizeLabel +from Helper import Log +from matplotlib.ticker import MaxNLocator + +import multiprocessing + + +class SummaryWidget(QtWidgets.QFrame): + update_sampling = pyqtSignal('QString') + def handle_update_sampling(self, fle): + pixmap = QtGui.QPixmap(fle) + self.sampling.setPixmap(pixmap) + + def enterEvent(self, evt): + if not self.maximized: + self.setStyleSheet(".SummaryWidget { background: '#FFFDDA'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + + def leaveEvent(self, evt): + self.setStyleSheet(".SummaryWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + + def resizeEvent(self, evt): + self.__set_font(evt.size().height() / 25) + + def __set_font(self, size): + newfont = QtGui.QFont("sans-serif", weight=QtGui.QFont.Bold) + newfont.setPixelSize(size) + + #import matplotlib + #matplotlib.rcParams.update({'font.size': size}) + + #self.message.setFont(newfont) + + def __init__(self, parent, title, initial_ylim): + QtWidgets.QWidget.__init__(self) + self.win = parent + + self.mpWidth = multiprocessing.Value('i', 0) + self.mpHeight = multiprocessing.Value('i', 0) + + self.maximized = False + + self.setStyleSheet(".SummaryWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + + self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.updateGeometry() + + self.update_sampling.connect(self.handle_update_sampling) + + self.main_widget = QtWidgets.QWidget(self) + self.main_layout = QtWidgets.QVBoxLayout(self.main_widget) + self.main_layout.setContentsMargins(0, 0, 0, 0) + + self.main_widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + + self.title = QtWidgets.QLabel() + self.title.setText(title) + self.title.setAlignment(QtCore.Qt.AlignCenter) + self.title.setStyleSheet("font-weight:bold; font-size:20pt;") + self.title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.main_layout.addWidget(self.title) + + self.sampling = ResizeLabel(self.mpWidth, self.mpHeight) + self.sampling.setMargin(0) + self.sampling.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.main_layout.addWidget(self.sampling) + + self.main_widget.setFocus() + + QtWidgets.QWidget.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + QtWidgets.QWidget.updateGeometry(self) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(self.main_widget) + + self.__set_font(self.height() / 50) diff --git a/Helper/Config.py b/Helper/Config.py new file mode 100644 index 0000000..53524b0 --- /dev/null +++ b/Helper/Config.py @@ -0,0 +1,65 @@ +#!/usr/bin/python + +from Helper import Log +import xml.etree.ElementTree as ET +import xml.dom.minidom as minidom + +class Config(object): + def __init__(self): + self.seed = 1431655765 + self.budget = 20000 + self.bits = 32 + self.ofactor = 25 + self.samples = 10000 + self.eic = False + self.suite = "nofloat" + self.opt = 0 + self.entry = "gene_main" + + def to_xml(self): + node = ET.Element("config") + + for attrib in dir(self): + if attrib.startswith('__'): continue + val = getattr(self, attrib) + + if callable(val): continue + + node.set(attrib, str(val)) + + return node + +def from_xml(filename): + configs = [] + + tree = ET.parse(filename) + root = tree.getroot() + for node in root: + conf = Config() + + for attrib in node.attrib: + val = node.attrib[attrib] + try: val = int(val) + except ValueError: + try: val = float(val) + except ValueError: + if "false" == val.lower(): val = False + elif "true" == val.lower(): val = True + + setattr(conf, attrib, val) + + configs.append(conf) + + return configs + +def to_xml(configs, filename): + root = ET.Element("configs") + #tree = ET.ElementTree(root) + + for config in configs: + root.append( config.to_xml() ) + + #tree.write(filename) + xmlstr = minidom.parseString(ET.tostring(root)).toprettyxml(indent="\t") + with open(filename, "w") as f: + f.write(xmlstr) -- GitLab From 2607ccbc680cd1cba633734cf2ed04cea7bf825d Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 20 Mar 2017 14:06:19 +0100 Subject: [PATCH 50/97] Remove unused code from SummaryWidget.py --- Gui/SummaryWidget.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/Gui/SummaryWidget.py b/Gui/SummaryWidget.py index ba11d12..9408779 100644 --- a/Gui/SummaryWidget.py +++ b/Gui/SummaryWidget.py @@ -7,7 +7,6 @@ from matplotlib.ticker import MaxNLocator import multiprocessing - class SummaryWidget(QtWidgets.QFrame): update_sampling = pyqtSignal('QString') def handle_update_sampling(self, fle): @@ -21,18 +20,6 @@ class SummaryWidget(QtWidgets.QFrame): def leaveEvent(self, evt): self.setStyleSheet(".SummaryWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") - def resizeEvent(self, evt): - self.__set_font(evt.size().height() / 25) - - def __set_font(self, size): - newfont = QtGui.QFont("sans-serif", weight=QtGui.QFont.Bold) - newfont.setPixelSize(size) - - #import matplotlib - #matplotlib.rcParams.update({'font.size': size}) - - #self.message.setFont(newfont) - def __init__(self, parent, title, initial_ylim): QtWidgets.QWidget.__init__(self) self.win = parent @@ -74,5 +61,3 @@ class SummaryWidget(QtWidgets.QFrame): layout = QtWidgets.QVBoxLayout(self) layout.addWidget(self.main_widget) - - self.__set_font(self.height() / 50) -- GitLab From a933a782ca95a79cc2e91386fc9631e2d3ee76e3 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 20 Mar 2017 15:39:49 +0100 Subject: [PATCH 51/97] Improve appearance of RTAS'17 Demo GUI --- Commands/bench_input_arm_demo.py | 13 ++++++++---- Gui/DemoWindow.py | 16 ++++++++++++--- Gui/SamplingWidget.py | 23 +++++++++++++-------- Gui/SummaryWidget.py | 33 +++++++++++++++++++++---------- data/img/bg_ait.png | Bin 0 -> 13835 bytes data/img/bg_cortex.png | Bin 0 -> 29934 bytes data/img/bg_platin.png | Bin 0 -> 86820 bytes data/img/logo/ait.png | Bin 0 -> 35132 bytes data/img/logo/arm_cortex.png | Bin 0 -> 51674 bytes data/img/logo/platin.jpg | Bin 0 -> 17155 bytes data/img/logo/redpitaya.png | Bin 0 -> 1036 bytes 11 files changed, 60 insertions(+), 25 deletions(-) create mode 100644 data/img/bg_ait.png create mode 100644 data/img/bg_cortex.png create mode 100644 data/img/bg_platin.png create mode 100644 data/img/logo/ait.png create mode 100644 data/img/logo/arm_cortex.png create mode 100644 data/img/logo/platin.jpg create mode 100644 data/img/logo/redpitaya.png diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index fab22ab..e5e8a4e 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -35,7 +35,6 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, xmax, linewidth = 1 fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) - fig.set_alpha(0.0) axes = fig.add_subplot(111) fig.subplots_adjust(hspace=0, wspace=0) @@ -64,9 +63,12 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, xmax, axes.axvline(x=result, ymin=0, ymax=1, linewidth=linewidth, color=args.color_analyzer) axes.text(result, ana_y, ' ' + name, ha='left', va='bottom', color=args.color_analyzer) + fig.patch.set_alpha(0) + axes.patch.set_alpha(.7) + fig.tight_layout(pad=0, h_pad=0, w_pad=0) fig.set_tight_layout(True) - fig.savefig(fle, transparent=True, dpi=100) + fig.savefig(fle, dpi=100) return fle @@ -74,7 +76,7 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): fle = tempfile.mktemp(suffix='.png') fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) - fig.set_alpha(0.0) + fig.set_alpha(0.5) axes = fig.add_subplot(111) from matplotlib.ticker import MaxNLocator @@ -101,9 +103,12 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): axes.axvline(x=100 * geos, ymin=0, ymax=1, linewidth=1, color='g') axes.axvline(x=0, ymin=0, ymax=1, linewidth=2, color=args.color_wcet) + fig.patch.set_alpha(0) + axes.patch.set_alpha(.7) + fig.tight_layout(pad=0, h_pad=0, w_pad=0) fig.set_tight_layout(True) - fig.savefig(fle, transparent=True, dpi=100) + fig.savefig(fle, dpi=100) return fle diff --git a/Gui/DemoWindow.py b/Gui/DemoWindow.py index a3f7c37..13c96c2 100644 --- a/Gui/DemoWindow.py +++ b/Gui/DemoWindow.py @@ -4,6 +4,8 @@ from PyQt5.QtCore import QObject, pyqtSignal from SamplingWidget import SamplingWidget from SummaryWidget import SummaryWidget +from Helper import Log + class DemoWindow(QtWidgets.QMainWindow): def __init__(self, sw_count, initial_ylim): QtWidgets.QMainWindow.__init__(self) @@ -22,7 +24,7 @@ class DemoWindow(QtWidgets.QMainWindow): self.sws = [] layout = -1 for i in xrange(0, sw_count): - self.sws.append( SamplingWidget(self, initial_ylim) ) + self.sws.append( SamplingWidget(self, 'bg_cortex.png', initial_ylim) ) if 0 == i % 5: self.bm_layouts.append( QtWidgets.QVBoxLayout() ) self.main_layout.addLayout(self.bm_layouts[-1]) @@ -35,8 +37,8 @@ class DemoWindow(QtWidgets.QMainWindow): self.main_layout.addLayout(self.sum_layout) self.sum = { - 'ait': SummaryWidget(self, 'aiT', initial_ylim), - 'platin': SummaryWidget(self, 'platin', initial_ylim) + 'ait': SummaryWidget(self, 'aiT', 'logo/ait.png', 'bg_ait.png', initial_ylim), + 'platin': SummaryWidget(self, 'platin', 'logo/platin.jpg', 'bg_platin.png', initial_ylim) } for _, sw in self.sum.iteritems(): self.sum_layout.addWidget(sw) @@ -47,3 +49,11 @@ class DemoWindow(QtWidgets.QMainWindow): self.main_widget.setFocus() QtCore.QTimer.singleShot(100, self.showFullScreen); + + def keyPressEvent(self, event): + key = event.key() + Log.warn("key: " + str(event) + ": " + str(key)) + if QtCore.Qt.Key_Q == key: + Log.warn("Q!") + elif QtCore.Qt.Key_Return == key: + Log.warn("Enter!") diff --git a/Gui/SamplingWidget.py b/Gui/SamplingWidget.py index 53d2d62..cc551cd 100644 --- a/Gui/SamplingWidget.py +++ b/Gui/SamplingWidget.py @@ -48,12 +48,12 @@ class SamplingWidget(QtWidgets.QFrame): icon.setPixmap(QtGui.QPixmap("aladdin/data/img/" + img).scaledToWidth(height)) label.setText(message) - def enterEvent(self, evt): - if not self.maximized: - self.setStyleSheet(".SamplingWidget { background: '#FFFDDA'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") - - def leaveEvent(self, evt): - self.setStyleSheet(".SamplingWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") +# def enterEvent(self, evt): +# if not self.maximized: +# self.setStyleSheet(".SamplingWidget { background: '#FFFDDA'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") +# +# def leaveEvent(self, evt): +# self.setStyleSheet(".SamplingWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") def mousePressEvent(self, evt): if self.maximized: @@ -70,6 +70,10 @@ class SamplingWidget(QtWidgets.QFrame): self.__set_font(evt.size().height() / 25) self.__set_icon(evt.size().height() / 25) + pixmap = QtGui.QPixmap("aladdin/data/img/" + self.bgimg) + pixmap = pixmap.scaled(0.75*self.width(), 0.75*self.height(), QtCore.Qt.KeepAspectRatio) + self.main_widget.setPixmap(pixmap) + def __set_font(self, size): newfont = QtGui.QFont("sans-serif", weight=QtGui.QFont.Bold) newfont.setPixelSize(size) @@ -86,7 +90,8 @@ class SamplingWidget(QtWidgets.QFrame): for icon, _, img, _ in self.analyzers.itervalues(): icon.setPixmap(QtGui.QPixmap("aladdin/data/img/" + img).scaledToWidth(size)) - def __init__(self, parent, initial_ylim): + def __init__(self, parent, bgimg, initial_ylim): + self.bgimg = bgimg QtWidgets.QWidget.__init__(self) self.win = parent @@ -105,7 +110,9 @@ class SamplingWidget(QtWidgets.QFrame): self.update_progress.connect(self.handle_update_progress) self.update_analyzer.connect(self.handle_update_analyzer) - self.main_widget = QtWidgets.QWidget(self) + self.main_widget = QtWidgets.QLabel(self) + self.main_widget.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) + self.main_layout = QtWidgets.QVBoxLayout(self.main_widget) self.main_layout.setContentsMargins(0, 0, 0, 0) diff --git a/Gui/SummaryWidget.py b/Gui/SummaryWidget.py index 9408779..62b7e0f 100644 --- a/Gui/SummaryWidget.py +++ b/Gui/SummaryWidget.py @@ -13,14 +13,14 @@ class SummaryWidget(QtWidgets.QFrame): pixmap = QtGui.QPixmap(fle) self.sampling.setPixmap(pixmap) - def enterEvent(self, evt): - if not self.maximized: - self.setStyleSheet(".SummaryWidget { background: '#FFFDDA'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + def resizeEvent(self, evt): + pixmap = QtGui.QPixmap("aladdin/data/img/" + self.bgimg) + pixmap = pixmap.scaled(0.75*self.width(), 0.75*self.height(), QtCore.Qt.KeepAspectRatio) + self.main_widget.setPixmap(pixmap) - def leaveEvent(self, evt): - self.setStyleSheet(".SummaryWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + def __init__(self, parent, title, logo, bgimg, initial_ylim): + self.bgimg = bgimg - def __init__(self, parent, title, initial_ylim): QtWidgets.QWidget.__init__(self) self.win = parent @@ -29,25 +29,38 @@ class SummaryWidget(QtWidgets.QFrame): self.maximized = False - self.setStyleSheet(".SummaryWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + self.setStyleSheet(".SummaryWidget { background-color: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") + #" background-image: url(aladdin/data/img/" + bgimg + "); background-repeat: no-repeat; background-position: left bottom; }") self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self.updateGeometry() self.update_sampling.connect(self.handle_update_sampling) - self.main_widget = QtWidgets.QWidget(self) + self.main_widget = QtWidgets.QLabel(self) + self.main_widget.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom) + self.main_layout = QtWidgets.QVBoxLayout(self.main_widget) self.main_layout.setContentsMargins(0, 0, 0, 0) self.main_widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + self.title_layout = QtWidgets.QHBoxLayout() + self.main_layout.addLayout(self.title_layout) + + self.logo = QtWidgets.QLabel() + self.logo.setPixmap(QtGui.QPixmap("aladdin/data/img/" + logo).scaledToHeight(32)) + self.logo.setAlignment(QtCore.Qt.AlignRight) + self.logo.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.title = QtWidgets.QLabel() self.title.setText(title) - self.title.setAlignment(QtCore.Qt.AlignCenter) + self.title.setAlignment(QtCore.Qt.AlignLeft) self.title.setStyleSheet("font-weight:bold; font-size:20pt;") self.title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.main_layout.addWidget(self.title) + + self.title_layout.addWidget(self.logo) + self.title_layout.addWidget(self.title) self.sampling = ResizeLabel(self.mpWidth, self.mpHeight) self.sampling.setMargin(0) diff --git a/data/img/bg_ait.png b/data/img/bg_ait.png new file mode 100644 index 0000000000000000000000000000000000000000..9c09368c4c321e9ba9f8f32ed8baab3105e1622e GIT binary patch literal 13835 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 literal 0 HcmV?d00001 diff --git a/data/img/bg_platin.png b/data/img/bg_platin.png new file mode 100644 index 0000000000000000000000000000000000000000..05d1d6b5c5c7dfaa04652ded499957fe47f47089 GIT binary patch literal 86820 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°yZ4HLkd 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 literal 0 HcmV?d00001 diff --git a/data/img/logo/ait.png b/data/img/logo/ait.png new file mode 100644 index 0000000000000000000000000000000000000000..fb91bd2a7a1d73b8e469032798cef4e3750204d9 GIT binary patch literal 35132 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≈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 literal 0 HcmV?d00001 diff --git a/data/img/logo/arm_cortex.png b/data/img/logo/arm_cortex.png new file mode 100644 index 0000000000000000000000000000000000000000..19ba7f9474aa2e9d36da3e3a62adca132647f96a GIT binary patch literal 51674 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ΪLYr>t=X}Of7of@e&{y*GJcM%jq`t1 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

TeguxZ~|~ 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≥4jlF5{tMbIKc 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 literal 0 HcmV?d00001 diff --git a/data/img/logo/platin.jpg b/data/img/logo/platin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..471e9af6cf4ea91d2344b9695bae869cee4df449 GIT binary patch literal 17155 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 Vf0|JLx5c6V?vni9oCCR?{colD;@AKH literal 0 HcmV?d00001 diff --git a/data/img/logo/redpitaya.png b/data/img/logo/redpitaya.png new file mode 100644 index 0000000000000000000000000000000000000000..41533c121c06110cc262e6f42ba05238d34fc0a4 GIT binary patch literal 1036 zcmV+n1oQieP)zC3^yDh~|C{G;6he`E1IXv$lFD`TxVk4Q`b@VOc9 zVJ%ci3w2#vFz_6(+1TeI{)bY44@uuUY3xmedthHnuoh|~jc2f8{F@1%z@&;!c)*m~ zwNN)$!N5y2=6+BMwH5d<1r*>hEtE9&I#c#wu3Q`V_E>!ba0uv63BC$AtcBVH+!66_ z0sF3YQv%xQX5j0XZ#~#o-13NZNf(>ITBreg+pg&{<_XPdp*Cuvc9}rdtKBDk?{xr2 z3v~e44%|(6)0}MAb0w+;0lkiMqx8L>bHKgE{}ympR#1Rm$Khc0@1G!CanR{N^&p@e z)Km4D59+Mx=`G|5Z>D0J&I>`Ez4Pbzpn9H*bTsd39{7$hzlrrs`8SSSDmVCshlg?9 zw8q$Fxw-1F!sFhp{XA z=$rquX*ZWjB?bnTFTQfQF&&eWlS}dG>1mv3Pz!aK1dsg}m&!bO;Vjk8b~Hc}_-Wv^ zBpuIOuJOsjKb)@DQbYImttrI8>j*bmcM@q)X|c-I^S?Ci6zO}Xflsr70^FJ}>;(JD zUL<_JvXBS7Z3Ro;`vKUS5%?+Tdo?TA=mpF-aTm2m`rfhsDps)cy+znpMv1|eiChh=CH#oE0GyG&*XrM7ll}$d Date: Wed, 22 Mar 2017 13:10:44 +0100 Subject: [PATCH 52/97] Move device search for RP to RedPitaya.py --- Commands/bench_input_arm_redpitaya.py | 15 +++------------ Helper/RedPitaya.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 0dcfa7c..081c70c 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -179,18 +179,9 @@ def run(args): rp.set_trigger('CH' + str(args.pitaya_in) + '_NE', __tlevel_high_low, offset) # search for osci <-> XMC4500 - rp_xmc = None - for xmc in xmcs: - xmc.connect() - cy, raw = __get_sample(xmc, rp, 0, args.pitaya_in) - xmc.disconnect() - if None != raw: - Log.info("RP connected to " + str(xmc)) - rp_xmc = xmc - break - - assert rp_xmc != None - xmcs = [rp_xmc] + channel, rp_xmc = rp.select_device(xmcs) + if None == rp_xmc: return + xmcs = [ rp_xmc ] Log.start("Start " + str(len(xmcs)) + " Sampling Threads") input_chunks = Benchmark.get_input(args.bits, args.seed, args.samples, len(xmcs)) diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index 8804ff3..9165d49 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -123,6 +123,20 @@ class RedPitaya(object): return (cy, raw) + def select_device(self, devices, rep_limit = 5): + for dev in devices: + for channel in [1, 2]: + dev.connect() + _, raw = self.measure(dev, inp=0, tlevel_high_low=2.5, channel=channel) + dev.disconnect() + + if None != raw: + Log.info("RP CH" + str(channel) + " connected to " + str(dev)) + return (channel, dev) + + Log.fail("Failed to find device connected to " + str(self)) + return (None, None) + def samples_high_to_low(raw, level_low, level_high): -- GitLab From 994c2f94807c03db15e47cb3e5e78dfda62e5765 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 22 Mar 2017 13:18:49 +0100 Subject: [PATCH 53/97] Implement handling of Config.xml errors --- Commands/demo_dryrun.py | 5 +++-- Helper/Config.py | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Commands/demo_dryrun.py b/Commands/demo_dryrun.py index b8f095b..01ed769 100644 --- a/Commands/demo_dryrun.py +++ b/Commands/demo_dryrun.py @@ -50,12 +50,13 @@ def run(args): if not os.path.exists(odir): os.makedirs(odir) + configs = Config.from_xml(args.config) + if None == configs: return + xmcs = XMC4500.list_devices(odir, args.xmccache) assert len(xmcs) > 0 xmc = xmcs[0] - configs = Config.from_xml(args.config) - for conf in configs: ll = odir + '/bench.ll' ll_comb = odir + '/combined.ll' diff --git a/Helper/Config.py b/Helper/Config.py index 53524b0..a660cc0 100644 --- a/Helper/Config.py +++ b/Helper/Config.py @@ -32,7 +32,12 @@ class Config(object): def from_xml(filename): configs = [] - tree = ET.parse(filename) + try: + tree = ET.parse(filename) + except ET.ParseError as e: + Log.fail("Failed to parse " + filename + ": " + str(e)) + return None + root = tree.getroot() for node in root: conf = Config() -- GitLab From 9969c1bf7e4a9dc4fdc7cd050b995fa09fbca627 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 22 Mar 2017 13:39:53 +0100 Subject: [PATCH 54/97] dat2pgf: Fix transparency for plot --- Commands/dat2pgf.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index 88f6186..439c3f4 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -12,17 +12,19 @@ def get_argparser(): fs_def = matplotlib.rcParams['font.size'] - parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used (default: ' + str(fs_def) + ')') - parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics (default: false)') - parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image (default: 6)') - parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image (default: 2)') - parser.add_argument('--bins', metavar='bins', type=int, default=200, help='Number of bins used for plotting histogram (default: 200)') - parser.add_argument('--xlabel', metavar='xlabel', type=str, default='Cycles', help='x label (default: "Cycles")') - parser.add_argument('--ylabel', metavar='ylabel', type=str, default='Occurrences', help='y label (default: "Occurrences")') - parser.add_argument('--no-ait', dest='no_ait', action='store_true', help='Hide aiT\'s WCET') - parser.add_argument('--no-platin', dest='no_platin', action='store_true', help='Hide platin\'s WCET') - parser.add_argument('--no-dref', dest='no_dref', action='store_true', help='Disable writing .dref result file') - parser.add_argument('--xmax', dest='xmax', type=int, default=-1, help='Overwrite maximum displayed x Value') + parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used (default: ' + str(fs_def) + ')') + parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics (default: false)') + parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image (default: 6)') + parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image (default: 2)') + parser.add_argument('--bins', metavar='bins', type=int, default=200, help='Number of bins used for plotting histogram (default: 200)') + parser.add_argument('--xlabel', metavar='xlabel', type=str, default='Cycles', help='x label (default: "Cycles")') + parser.add_argument('--ylabel', metavar='ylabel', type=str, default='Occurrences', help='y label (default: "Occurrences")') + parser.add_argument('--label-wcet', metavar='label_wcet', type=str, default='WCET', help='WCET label (default: "WCET")') + parser.add_argument('--xmax', dest='xmax', type=int, default=-1, help='Overwrite maximum displayed x Value') + + parser.add_argument('--no-ait', dest='no_ait', action='store_true', help='Hide aiT\'s WCET') + parser.add_argument('--no-platin', dest='no_platin', action='store_true', help='Hide platin\'s WCET') + parser.add_argument('--no-dref', dest='no_dref', action='store_true', help='Disable writing .dref result file') parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds (default: 1)') @@ -72,8 +74,11 @@ def hist_fh(args, dat): # plt.axvline(x=info['min'], ymin=0, ymax=1, linewidth=2, color='g') # plt.text(info['min'] + 0.065*xmax, 1.1 * center, 'BOET', ha='right', va='bottom', color='g') + plt.gcf().patch.set_alpha(0) + axes.patch.set_alpha(1) + plt.axvline(x=dat.max, ymin=0, ymax=1, linewidth=args.linewidth, color=args.color_wcet) - plt.text(0.98*dat.max, 1.6 * center, 'WCET', ha='right', va='bottom', color=args.color_wcet) + plt.text(0.98*dat.max, 1.6 * center, args.label_wcet.replace('\\n', '\n'), ha='right', va='top', color=args.color_wcet) plt.tight_layout() @@ -115,7 +120,7 @@ def run(args): pgf_file = fle + ".pgf" Log.operation_start("Save generated image") - plt.savefig(pgf_file, transparent=True) + plt.savefig(pgf_file) plt.close(0) Log.operation_end("Save generated image: " + pgf_file) -- GitLab From c052d6611089e98a3e629d67d332a36e85e8100c Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 24 Mar 2017 08:17:44 +0100 Subject: [PATCH 55/97] Remove validation of Dat to prevent loading whole dataset --- Helper/Dat.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Helper/Dat.py b/Helper/Dat.py index 3ec142e..ff36234 100644 --- a/Helper/Dat.py +++ b/Helper/Dat.py @@ -235,8 +235,7 @@ class Data(object): dat.count = num_lines - num_info_lines break - dat.validate() - Log.warn("Reading done") + #dat.validate() return dat -- GitLab From 8b327677959e88113af5b380a1962d72cce47d72 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 24 Mar 2017 08:19:25 +0100 Subject: [PATCH 56/97] Improve plotting of dat2stat (required for RTAS '17 Poster) --- Commands/dat2stat.py | 55 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index 1e00d19..be7cb98 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -33,6 +33,13 @@ def get_argparser(): __register_color('bin', 'Facecolor of Bins', '#00669E') __register_color('binfail', 'Facecolor of Bins with ub/WCET < 1', 'red') + parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image (default: 6)') + parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image (default: 2)') + parser.add_argument('--xmin', dest='xmin', type=int, default=None, help='Overwrite minimum displayed x Value') + parser.add_argument('--xmax', dest='xmax', type=int, default=None, help='Overwrite maximum displayed x Value') + parser.add_argument('--no-xtic', dest='no_xtic', action='store_true', help='Hide x tics (default: false)') + parser.add_argument('--no-ytic', dest='no_ytic', action='store_true', help='Hide y tics (default: false)') + return parser def run(args): @@ -57,7 +64,7 @@ def run(args): wcets.append(dat.max) - if cy > 0: ubs[ana].append( float(cy) / dat.max ) + if cy > 0: ubs[ana].append( (float(cy) / dat.max) ) else: if cy not in wca_errors[ana]: wca_errors[ana][cy] = 0 wca_errors[ana][cy] += 1 @@ -84,12 +91,16 @@ def run(args): if len(cy) > 0: arr = numpy.array(cy) + geos[ana] = geomean(arr) - 1.0 + + arr = arr - 1.0 + mins[ana] = numpy.min(arr) maxs[ana] = numpy.max(arr) avgs[ana] = numpy.average(arr) - geos[ana] = geomean(arr) - geo_valid = geomean( [ c for c in cy if c >= 1.0 ] ) + + geo_valid = geomean( [ c for c in cy if c >= 0.0 ] ) Log.info(" * Min/Max: " + "{0:.2f}".format(mins[ana]) + "/" + "{0:.2f}".format(maxs[ana])) Log.info(" * Avg wcet: " + "{0:.2f}".format(avgs[ana]) + "x max (" + str(len(cy)) + " samples)") @@ -103,25 +114,49 @@ def run(args): # dump histograms if len(ubs[ana]) == 0: continue - valid = [100*f for f in ubs[ana] if f >= 1.0] - invalid = [100*f for f in ubs[ana] if f < 1.0] + plt.figure(0)#, figsize=(args.width, args.height)) + + if args.no_xtic: plt.gca().axes.get_xaxis().set_ticks([]) + if args.no_ytic: plt.gca().axes.get_yaxis().set_ticks([]) + + axes = plt.gca() + for axis in ['top','bottom','left','right']: + axes.spines[axis].set_linewidth(0.5) + + valid = [100*f for f in arr if f >= 0.0] + invalid = [100*f for f in arr if f < 0.0] bins = range(int(100 * mins[ana]), int(100 * maxs[ana]), 1) - plt.hist(valid, bins, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) - plt.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0.1, alpha=0.5) + plt.gcf().patch.set_alpha(0) + axes.patch.set_alpha(1) + + plt.hist(valid, bins, facecolor=args.color_bin, linewidth=0, alpha=0.5) + plt.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0, alpha=0.5) if args.no_tic: plt.gca().axes.get_xaxis().set_ticks([]) plt.gca().axes.get_yaxis().set_ticks([]) - plt.xlabel('upper bound(' + ana + ')/WCET[%]') + plt.xlabel('Overestimation [%]') plt.ylabel('Occurrences') - plt.title(str(len(ubs[ana])) + " samples, " + str(len(invalid)) + " underestimations") + plt.xlim([args.xmin, args.xmax]) + center = plt.ylim()[1] / 2 + + #plt.text(0, ana_y, ' BOET', ha='left', va='bottom', color='m') + #plt.title(str(len(ubs[ana])) + " samples, " + str(len(invalid)) + " underestimations") plt.axvline(x=100 * geos[ana], ymin=0, ymax=1, linewidth=1, color='g') - plt.axvline(x=100, ymin=0, ymax=1, linewidth=1, color=args.color_wcet) + + plt.axvline(x=0, ymin=0, ymax=1, linewidth=1, color=args.color_wcet) + #plt.text(0, center, 'Optimum', rotation=90, va='center', ha='right', xytext=(-15, 25), color=args.color_wcet) + + plt.gca().annotate('Optimum', + xy=(0, center), xycoords='data', xytext=(-3, 0), textcoords='offset points', va='center', ha='right', rotation=90, color=args.color_wcet) + + plt.gcf().tight_layout(pad=0, h_pad=0, w_pad=0) + plt.gcf().set_tight_layout(True) plt.savefig(ana + ".pdf", format="pdf") plt.clf() -- GitLab From ebce4faecdf9d0fbdc3f8101d0a87cca74cba33a Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 22 Mar 2017 14:29:16 +0100 Subject: [PATCH 57/97] dat2pgf: Use ArgumentDefaultsHelpFormatter --- Commands/dat2pgf.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index 439c3f4..5221d77 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -7,26 +7,28 @@ import matplotlib from Helper import Dat, Execution, Log, Python def get_argparser(): - parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2pgf", description='Subfunction dat2pgf: ' + desc) + parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2pgf", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + description='Subfunction dat2pgf: ' + desc) parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file(s) to be converted') fs_def = matplotlib.rcParams['font.size'] - parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used (default: ' + str(fs_def) + ')') - parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics (default: false)') - parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image (default: 6)') - parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image (default: 2)') - parser.add_argument('--bins', metavar='bins', type=int, default=200, help='Number of bins used for plotting histogram (default: 200)') - parser.add_argument('--xlabel', metavar='xlabel', type=str, default='Cycles', help='x label (default: "Cycles")') - parser.add_argument('--ylabel', metavar='ylabel', type=str, default='Occurrences', help='y label (default: "Occurrences")') - parser.add_argument('--label-wcet', metavar='label_wcet', type=str, default='WCET', help='WCET label (default: "WCET")') + parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used') + parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics') + parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image') + parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image') + parser.add_argument('--bins', metavar='bins', type=int, default=200, help='Number of bins used for plotting histogram') + parser.add_argument('--xlabel', metavar='xlabel', type=str, default='Cycles', help='x label') + parser.add_argument('--ylabel', metavar='ylabel', type=str, default='Occurrences', help='y label') + parser.add_argument('--label-wcet', metavar='label_wcet', type=str, default='WCET', help='WCET label') parser.add_argument('--xmax', dest='xmax', type=int, default=-1, help='Overwrite maximum displayed x Value') parser.add_argument('--no-ait', dest='no_ait', action='store_true', help='Hide aiT\'s WCET') parser.add_argument('--no-platin', dest='no_platin', action='store_true', help='Hide platin\'s WCET') parser.add_argument('--no-dref', dest='no_dref', action='store_true', help='Disable writing .dref result file') - parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds (default: 1)') + parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds') def __register_color(name, label, default): parser.add_argument('--color-' + name, @@ -34,7 +36,7 @@ def get_argparser(): metavar='color', default=default, type=str, - help='Color for ' + label + ' (default: ' + default + ')') + help='Color for ' + label) __register_color('analyzer', 'upper bounds from WCET analyzers', '#19B219') __register_color('boet', 'Best Observed Execution Time', 'green') -- GitLab From 7967df6be8c3a229c37ff76c6ad30c3cf5c95d57 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 28 Mar 2017 08:29:11 +0200 Subject: [PATCH 58/97] Move plotting code to Helper/Plot.py (TODO!) --- Commands/dat2pgf.py | 69 ++++++++++------------------------------- Commands/dat2stat.py | 51 +++---------------------------- Commands/display.py | 2 +- Helper/Plot.py | 73 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 100 deletions(-) create mode 100644 Helper/Plot.py diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index 5221d77..21581bb 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -4,7 +4,7 @@ import sys import os import matplotlib -from Helper import Dat, Execution, Log, Python +from Helper import Dat, Execution, Log, Python, Plot def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2pgf", @@ -12,60 +12,28 @@ def get_argparser(): description='Subfunction dat2pgf: ' + desc) parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file(s) to be converted') - fs_def = matplotlib.rcParams['font.size'] - - parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used') - parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics') - parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image') - parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image') - parser.add_argument('--bins', metavar='bins', type=int, default=200, help='Number of bins used for plotting histogram') - parser.add_argument('--xlabel', metavar='xlabel', type=str, default='Cycles', help='x label') - parser.add_argument('--ylabel', metavar='ylabel', type=str, default='Occurrences', help='y label') - parser.add_argument('--label-wcet', metavar='label_wcet', type=str, default='WCET', help='WCET label') - parser.add_argument('--xmax', dest='xmax', type=int, default=-1, help='Overwrite maximum displayed x Value') - - parser.add_argument('--no-ait', dest='no_ait', action='store_true', help='Hide aiT\'s WCET') - parser.add_argument('--no-platin', dest='no_platin', action='store_true', help='Hide platin\'s WCET') - parser.add_argument('--no-dref', dest='no_dref', action='store_true', help='Disable writing .dref result file') - - parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds') - - def __register_color(name, label, default): - parser.add_argument('--color-' + name, - dest='color_' + name, - metavar='color', - default=default, - type=str, - help='Color for ' + label) - - __register_color('analyzer', 'upper bounds from WCET analyzers', '#19B219') - __register_color('boet', 'Best Observed Execution Time', 'green') - __register_color('wcet', 'Worst Observed Execution Time', '#B41C2E') - __register_color('bin', 'Facecolor of Bins', '#00669E') + parser.add_argument('--bins', metavar='bins', type=int, default=200, help='Number of bins used for plotting histogram') + parser.add_argument('--label-wcet', metavar='label_wcet', type=str, default='WCET', help='WCET label') - return parser + parser.add_argument('--no-ait', dest='no_ait', action='store_true', help='Hide aiT\'s WCET') + parser.add_argument('--no-platin', dest='no_platin', action='store_true', help='Hide platin\'s WCET') + parser.add_argument('--no-dref', dest='no_dref', action='store_true', help='Disable writing .dref result file') -def hist_fh(args, dat): - plt.hist(dat.cycles(), args.bins, normed=False, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) + Plot.register_color(parser, 'analyzer', 'upper bounds from WCET analyzers', '#19B219') + Plot.add_arguments(parser) - if args.no_tic: - plt.gca().axes.get_xaxis().set_ticks([]) - plt.gca().axes.get_yaxis().set_ticks([]) + return parser - plt.xlabel(args.xlabel) - plt.ylabel(args.ylabel) +def hist_fh(args, dat): + plt.hist(dat.cycles(), args.bins, normed=False, facecolor=args.color_face, linewidth=0.1, alpha=0.5) - _xmax = dat.get_x_maximum() * 1.05 + Plot.configure(args, plt.gcf()) # overwrite xmax if passed as argument - if args.xmax != -1: - _xmax = args.xmax - - plt.xlim([0, _xmax]) + if args.xmax == None: + plt.xlim([0, dat.get_x_maximum() * 1.05]) axes = plt.gca() - for axis in ['top','bottom','left','right']: - axes.spines[axis].set_linewidth(0.5) from matplotlib.ticker import AutoMinorLocator minorLocator = AutoMinorLocator(5) @@ -76,13 +44,8 @@ def hist_fh(args, dat): # plt.axvline(x=info['min'], ymin=0, ymax=1, linewidth=2, color='g') # plt.text(info['min'] + 0.065*xmax, 1.1 * center, 'BOET', ha='right', va='bottom', color='g') - plt.gcf().patch.set_alpha(0) - axes.patch.set_alpha(1) - - plt.axvline(x=dat.max, ymin=0, ymax=1, linewidth=args.linewidth, color=args.color_wcet) - plt.text(0.98*dat.max, 1.6 * center, args.label_wcet.replace('\\n', '\n'), ha='right', va='top', color=args.color_wcet) - - plt.tight_layout() + plt.axvline(x=dat.max, ymin=0, ymax=1, linewidth=args.linewidth, color=args.color_max) + plt.text(0.98*dat.max, 1.6 * center, args.label_wcet.replace('\\n', '\n'), ha='right', va='top', color=args.color_max) plt.locator_params(nbins=8, axis='x') plt.locator_params(nbins=5, axis='y') diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index be7cb98..88db75d 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -6,7 +6,7 @@ import sys import os import matplotlib -from Helper import Dat, Log, Python +from Helper import Dat, Log, Python, Plot def geomean(a): return numpy.exp( numpy.log(a).mean() ) @@ -15,30 +15,9 @@ def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2stat", description='Subfunction dat2stat: ' + desc) parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file(s) to be converted') - parser.add_argument('--linewidth', dest='linewidth', type=int, default=1, help='Line width used for WCET/upper bounds (default: 1)') - fs_def = matplotlib.rcParams['font.size'] - - parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics (default: false)') - parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used (default: ' + str(fs_def) + ')') - - def __register_color(name, label, default): - parser.add_argument('--color-' + name, - dest='color_' + name, - metavar='color', - default=default, - type=str, - help='Color for ' + label + ' (default: ' + default + ')') - __register_color('analyzer', 'upper bounds from WCET analyzers', '#19B219') - __register_color('wcet', 'Worst Observed Execution Time', '#B41C2E') - __register_color('bin', 'Facecolor of Bins', '#00669E') - __register_color('binfail', 'Facecolor of Bins with ub/WCET < 1', 'red') - - parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image (default: 6)') - parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image (default: 2)') - parser.add_argument('--xmin', dest='xmin', type=int, default=None, help='Overwrite minimum displayed x Value') - parser.add_argument('--xmax', dest='xmax', type=int, default=None, help='Overwrite maximum displayed x Value') - parser.add_argument('--no-xtic', dest='no_xtic', action='store_true', help='Hide x tics (default: false)') - parser.add_argument('--no-ytic', dest='no_ytic', action='store_true', help='Hide y tics (default: false)') + Plot.register_color('analyzer', 'upper bounds from WCET analyzers', '#19B219') + Plot.register_color('binfail', 'Facecolor of Bins with ub/WCET < 1', 'red') + Plot.add_arguments(parser) return parser @@ -99,7 +78,6 @@ def run(args): maxs[ana] = numpy.max(arr) avgs[ana] = numpy.average(arr) - geo_valid = geomean( [ c for c in cy if c >= 0.0 ] ) Log.info(" * Min/Max: " + "{0:.2f}".format(mins[ana]) + "/" + "{0:.2f}".format(maxs[ana])) @@ -114,34 +92,19 @@ def run(args): # dump histograms if len(ubs[ana]) == 0: continue - plt.figure(0)#, figsize=(args.width, args.height)) - - if args.no_xtic: plt.gca().axes.get_xaxis().set_ticks([]) - if args.no_ytic: plt.gca().axes.get_yaxis().set_ticks([]) - - axes = plt.gca() - for axis in ['top','bottom','left','right']: - axes.spines[axis].set_linewidth(0.5) + fig = Plot.get_plot(args) valid = [100*f for f in arr if f >= 0.0] invalid = [100*f for f in arr if f < 0.0] bins = range(int(100 * mins[ana]), int(100 * maxs[ana]), 1) - plt.gcf().patch.set_alpha(0) - axes.patch.set_alpha(1) - plt.hist(valid, bins, facecolor=args.color_bin, linewidth=0, alpha=0.5) plt.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0, alpha=0.5) - if args.no_tic: - plt.gca().axes.get_xaxis().set_ticks([]) - plt.gca().axes.get_yaxis().set_ticks([]) - plt.xlabel('Overestimation [%]') plt.ylabel('Occurrences') - plt.xlim([args.xmin, args.xmax]) center = plt.ylim()[1] / 2 #plt.text(0, ana_y, ' BOET', ha='left', va='bottom', color='m') @@ -150,14 +113,10 @@ def run(args): plt.axvline(x=100 * geos[ana], ymin=0, ymax=1, linewidth=1, color='g') plt.axvline(x=0, ymin=0, ymax=1, linewidth=1, color=args.color_wcet) - #plt.text(0, center, 'Optimum', rotation=90, va='center', ha='right', xytext=(-15, 25), color=args.color_wcet) plt.gca().annotate('Optimum', xy=(0, center), xycoords='data', xytext=(-3, 0), textcoords='offset points', va='center', ha='right', rotation=90, color=args.color_wcet) - plt.gcf().tight_layout(pad=0, h_pad=0, w_pad=0) - plt.gcf().set_tight_layout(True) - plt.savefig(ana + ".pdf", format="pdf") plt.clf() diff --git a/Commands/display.py b/Commands/display.py index 371a03a..1b43318 100644 --- a/Commands/display.py +++ b/Commands/display.py @@ -20,7 +20,7 @@ def draw_wcet(plt, tool, wcet): plt.text(wcet, 1.2 * center, ' ' + tool, ha='left', va='bottom', color='m', horizontalalignment='left') -def hist_fh(args,fobject, bins): +def hist_fh(args, fobject, bins): dat = Dat.Data.from_file(fobject) n, bins, patches = plt.hist(dat.column(args.column), bins, normed=False, facecolor='green', alpha=0.5) diff --git a/Helper/Plot.py b/Helper/Plot.py new file mode 100644 index 0000000..ccdf003 --- /dev/null +++ b/Helper/Plot.py @@ -0,0 +1,73 @@ +#!/usr/bin/python + +import matplotlib + +def add_arguments(parser): + fs_def = matplotlib.rcParams['font.size'] + + parser.add_argument('--font-size', dest='fs', type=float, default=fs_def, help='Size of the font used') + parser.add_argument('--frame-width', dest='frame_width', type=float, default=0.5, help='Size of the frame around the plot') + + parser.add_argument('--width', metavar='width', type=float, default=6, help='Width of the generated image') + parser.add_argument('--height', metavar='height', type=float, default=2, help='Height of the generated image') + + parser.add_argument('--xlabel', metavar='xlabel', type=str, default='Cycles', help='x label') + parser.add_argument('--ylabel', metavar='ylabel', type=str, default='Occurrences', help='y label') + + parser.add_argument('--no-tic', dest='no_tic', action='store_true', help='Hide tics') + parser.add_argument('--no-xtic', dest='no_xtic', action='store_true', help='Hide x tics') + parser.add_argument('--no-ytic', dest='no_ytic', action='store_true', help='Hide y tics') + + parser.add_argument('--int-tic', dest='int_tic', action='store_true', help='Use integer tics') + parser.add_argument('--int-xtic', dest='int_xtic', action='store_true', help='Use integer x tics') + parser.add_argument('--int-ytic', dest='int_ytic', action='store_true', help='Use integer y tics') + + parser.add_argument('--xmin', dest='xmin', type=int, default=None, help='Overwrite minimum displayed x value') + parser.add_argument('--xmax', dest='xmax', type=int, default=None, help='Overwrite maximum displayed x value') + parser.add_argument('--ymin', dest='ymin', type=int, default=None, help='Overwrite minimum displayed y value') + parser.add_argument('--ymax', dest='ymax', type=int, default=None, help='Overwrite maximum displayed y value') + + parser.add_argument('--line-width', dest='linewidth', type=int, default=1, help='Line width used for min/max') + + register_color(parser, 'min', 'Best Observed Execution Time', 'green') + register_color(parser, 'max', 'Worst Observed Execution Time', '#B41C2E') + register_color(parser, 'face', 'Facecolor of Bins', '#00669E') + + +def register_color(parser, name, label, default): + parser.add_argument('--color-' + name, + dest='color_' + name, + metavar='color', + default=default, + type=str, + help='Color for ' + label) + + +def get_plot(args): + fig = plt.figure(0, figsize=(args.width, args.height)) + configure(args, fig) + return fig + + +def configure(args, fig): + ax = fig.gca() + + if args.no_tic or args.no_xtic: ax.axes.get_xaxis().set_ticks([]) + if args.no_tic or args.no_ytic: ax.axes.get_yaxis().set_ticks([]) + + ax.set_xlabel(args.xlabel) + ax.set_ylabel(args.ylabel) + + ax.set_xlim(left=args.xmin, right=args.xmax) + ax.set_xlim(left=args.xmin, right=args.xmax) + + for pos in ['top','bottom','left','right']: + ax.spines[pos].set_linewidth(args.frame_width) + + # set figure patch transparent, + # set axis patch opaque + fig.patch.set_alpha(0) + ax.patch.set_alpha(1) + + fig.tight_layout(pad=0, h_pad=0, w_pad=0) + fig.set_tight_layout(True) -- GitLab From 477522be971c45f419404b026c10a8dec89190cd Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 29 Mar 2017 19:17:11 +0200 Subject: [PATCH 59/97] Disable verbose logging for RedPitaya --- Helper/RedPitaya.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index 9165d49..e31631f 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -16,13 +16,13 @@ class RedPitaya(object): def send(self, string): assert None != self.rp - Log.debug("send: " + string) + #Log.debug("send: " + string) self.rp.tx_txt(string) def recv(self): assert None != self.rp res = self.rp.rx_txt() - Log.debug("read: " + res) + #Log.debug("read: " + res) return res def send_recv(self, string): -- GitLab From 8e8925ec812ce0a958d0f4cd14d363eefd33fbcd Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 29 Mar 2017 19:17:37 +0200 Subject: [PATCH 60/97] Move Tools.get_metrics to Metrics.py --- Helper/GenE.py | 8 +++----- Helper/Metrics.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- Helper/Tools.py | 43 ------------------------------------------- 3 files changed, 48 insertions(+), 49 deletions(-) diff --git a/Helper/GenE.py b/Helper/GenE.py index 785e0d4..f307d7f 100644 --- a/Helper/GenE.py +++ b/Helper/GenE.py @@ -5,7 +5,7 @@ import re import os import pexpect -import Log, Tools, Git +import Log, Tools, Git, Metrics class Benchmark(object): def __init__(self, ll, pattern, cost, bits, seed): @@ -35,7 +35,7 @@ class Benchmark(object): metrics = {} for func in self.funcs: - metrics[func] = Tools.get_metrics(self.ll, func) + metrics[func] = Metrics.execute(self.ll, func) return metrics @@ -60,9 +60,7 @@ class Benchmark(object): Log.info(" -> Pattern " + p + ": " + str(self.pattern[p]) + "x") def execute(pattern, cpu, cost, bits, seed, of, out_ll, simulate, silent=False): - pattern_str = "" - for p in pattern: - pattern_str += " --pattern " + p + " " + pattern_str = " ".join( map('--pattern {0}'.format, pattern) ) proc = pexpect.spawn("patmos-gene -mcpu=" + cpu + " " + pattern_str + " -gene-cost=" + str(cost) + " -gene-input-bits=" + str(bits) + " -gene-seed=" + str(seed) + " -ll=\"" + out_ll + "\" -gene-overweight-factor=" + str(of) + (" -gene-execute" if simulate else "")) diff --git a/Helper/Metrics.py b/Helper/Metrics.py index a816aef..13a52e7 100644 --- a/Helper/Metrics.py +++ b/Helper/Metrics.py @@ -1,6 +1,7 @@ #!/usr/bin/python -import Log +import re +import Log, Execution class MetricsResult(object): def __init__(self): @@ -125,3 +126,46 @@ def get_metrics_table(mO0, mO3): result += r'\def\notLoopResilient{' + str(not_loop_resilient) + '}\n' result += r'\def\percentageNotLoopResilient{' + "{0:.0f}".format(round(float(not_loop_resilient)/ float(len(mO0)) * 100, 0)) + r'\,\%\xspace}' +'\n' return result + +def execute(ll, entry): + + def __find_int_group1(prog, inp): + search = prog.search(inp) + return int(search.group(1)) if search else 0 + + def __find_int_group2(prog, inp): + search = prog.search(inp) + if search: + assert entry == search.group(1) + return int(search.group(2)) if search else 0 + else: + return 0 + + proc = Execution.run_wait("patmos-metrics -all -entry \"" + entry + "\" \"" + ll + "\"") + + metric = MetricsResult() + + metric.func = entry + metric.mcc = __find_int_group2(execute.prog_mcc, proc.stdout) + metric.uses_func_ptr = __find_int_group2(execute.prog_fptr, proc.stdout) > 0 + metric.fpu = __find_int_group2(execute.prog_float, proc.stdout) + metric.float_cond = __find_int_group2(execute.prog_fcond, proc.stdout) > 0 + + # __find_int_group2(get_metrics.prog_cond, proc.stdout) + + metric.input = __find_int_group1(execute.prog_idep, proc.stdout) + metric.loop_count = __find_int_group1(execute.prog_loops, proc.stdout) + metric.nested_loop_count = __find_int_group1(execute.prog_nested_loops, proc.stdout) + metric.call_chain = __find_int_group1(execute.prog_call_chain, proc.stdout) + + return metric + +execute.prog_mcc = re.compile(r'^McCabe\'s Cyclomatic Complexity of function \'(\w+)\': (\d+)$', re.MULTILINE) +execute.prog_fptr = re.compile(r'^Function \'(\w+)\' calls function via pointers: (\d+)$', re.MULTILINE) +execute.prog_float = re.compile(r'^Function \'(\w+)\' uses floating point values: (\d+)$', re.MULTILINE) +execute.prog_fcond = re.compile(r'^Conditionals using floating point values in function \'(\w+)\': (\d+)$', re.MULTILINE) +execute.prog_cond = re.compile(r'^Total number of conditionals in funtion \'(\w+)\': (\d+)$', re.MULTILINE) +execute.prog_idep = re.compile(r'^Input dependencies: (\d+)$', re.MULTILINE) +execute.prog_loops = re.compile(r'^Total Loops: (\d+)$', re.MULTILINE) +execute.prog_nested_loops = re.compile(r'^Nested Loops: (\d+)$', re.MULTILINE) +execute.prog_call_chain = re.compile(r'^Longest call chain: (\d+)$', re.MULTILINE) diff --git a/Helper/Tools.py b/Helper/Tools.py index 2d2e46e..44819a5 100644 --- a/Helper/Tools.py +++ b/Helper/Tools.py @@ -88,49 +88,6 @@ clang_execute.prog = re.compile(r'^\[OneOneCheck\] (\w*): (true|false)$', re.MUL def pml_strip_user_ff(pml, out_pml): Execution.run_wait("platin pmlstrip -i \"" + pml + "\" -o \"" + out_pml + "\"", None, 0) -def get_metrics(ll, entry): - - def __find_int_group1(prog, inp): - search = prog.search(inp) - return int(search.group(1)) if search else 0 - - def __find_int_group2(prog, inp): - search = prog.search(inp) - if search: - assert entry == search.group(1) - return int(search.group(2)) if search else 0 - else: - return 0 - - proc = Execution.run_wait("patmos-metrics -all -entry \"" + entry + "\" \"" + ll + "\"") - - metric = Metrics.MetricsResult() - - metric.func = entry - metric.mcc = __find_int_group2(get_metrics.prog_mcc, proc.stdout) - metric.uses_func_ptr = __find_int_group2(get_metrics.prog_fptr, proc.stdout) > 0 - metric.fpu = __find_int_group2(get_metrics.prog_float, proc.stdout) - metric.float_cond = __find_int_group2(get_metrics.prog_fcond, proc.stdout) > 0 - - # __find_int_group2(get_metrics.prog_cond, proc.stdout) - - metric.input = __find_int_group1(get_metrics.prog_idep, proc.stdout) - metric.loop_count = __find_int_group1(get_metrics.prog_loops, proc.stdout) - metric.nested_loop_count = __find_int_group1(get_metrics.prog_nested_loops, proc.stdout) - metric.call_chain = __find_int_group1(get_metrics.prog_call_chain, proc.stdout) - - return metric - -get_metrics.prog_mcc = re.compile(r'^McCabe\'s Cyclomatic Complexity of function \'(\w+)\': (\d+)$', re.MULTILINE) -get_metrics.prog_fptr = re.compile(r'^Function \'(\w+)\' calls function via pointers: (\d+)$', re.MULTILINE) -get_metrics.prog_float = re.compile(r'^Function \'(\w+)\' uses floating point values: (\d+)$', re.MULTILINE) -get_metrics.prog_fcond = re.compile(r'^Conditionals using floating point values in function \'(\w+)\': (\d+)$', re.MULTILINE) -get_metrics.prog_cond = re.compile(r'^Total number of conditionals in funtion \'(\w+)\': (\d+)$', re.MULTILINE) -get_metrics.prog_idep = re.compile(r'^Input dependencies: (\d+)$', re.MULTILINE) -get_metrics.prog_loops = re.compile(r'^Total Loops: (\d+)$', re.MULTILINE) -get_metrics.prog_nested_loops = re.compile(r'^Nested Loops: (\d+)$', re.MULTILINE) -get_metrics.prog_call_chain = re.compile(r'^Longest call chain: (\d+)$', re.MULTILINE) - def udhcpd_get_leases(): output = pexpect.run("dumpleases") -- GitLab From 8d4a7a1f3cad33cb012d54b5164ff52c264180b6 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 08:38:06 +0200 Subject: [PATCH 61/97] Use correct background image for RP tracing --- Gui/DemoWindow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Gui/DemoWindow.py b/Gui/DemoWindow.py index 13c96c2..5fbf9f0 100644 --- a/Gui/DemoWindow.py +++ b/Gui/DemoWindow.py @@ -24,7 +24,9 @@ class DemoWindow(QtWidgets.QMainWindow): self.sws = [] layout = -1 for i in xrange(0, sw_count): - self.sws.append( SamplingWidget(self, 'bg_cortex.png', initial_ylim) ) + bg_img = 'bg_cortex.png' + if i == sw_count - 1: bg_img = 'bg_rp.png' + self.sws.append( SamplingWidget(self, bg_img, initial_ylim) ) if 0 == i % 5: self.bm_layouts.append( QtWidgets.QVBoxLayout() ) self.main_layout.addLayout(self.bm_layouts[-1]) -- GitLab From ec95ab857636deadf64a3e6703936853a62d47c6 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 08:38:18 +0200 Subject: [PATCH 62/97] Make qq terminate aladdin demo --- Gui/DemoWindow.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Gui/DemoWindow.py b/Gui/DemoWindow.py index 5fbf9f0..d118914 100644 --- a/Gui/DemoWindow.py +++ b/Gui/DemoWindow.py @@ -4,6 +4,8 @@ from PyQt5.QtCore import QObject, pyqtSignal from SamplingWidget import SamplingWidget from SummaryWidget import SummaryWidget +import sys + from Helper import Log class DemoWindow(QtWidgets.QMainWindow): @@ -19,6 +21,7 @@ class DemoWindow(QtWidgets.QMainWindow): self.main_widget = QtWidgets.QWidget(self) self.main_layout = QtWidgets.QHBoxLayout(self.main_widget) + self.had_Q = False self.bm_layouts = [] self.sws = [] @@ -54,8 +57,14 @@ class DemoWindow(QtWidgets.QMainWindow): def keyPressEvent(self, event): key = event.key() - Log.warn("key: " + str(event) + ": " + str(key)) + if QtCore.Qt.Key_Q == key: - Log.warn("Q!") - elif QtCore.Qt.Key_Return == key: + if self.had_Q: + Log.warn("Q pressed twice, terminating!") + sys.exit(0) + self.had_Q = True + else: + self.had_Q = False + + if QtCore.Qt.Key_Return == key: Log.warn("Enter!") -- GitLab From 2a02a5d06839f4cfe6a8932ff6cc8a94c127f9b8 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 10:22:02 +0200 Subject: [PATCH 63/97] Add missing image bg_rp.png --- data/img/bg_rp.png | Bin 0 -> 30153 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 data/img/bg_rp.png diff --git a/data/img/bg_rp.png b/data/img/bg_rp.png new file mode 100644 index 0000000000000000000000000000000000000000..93ef3d1f9a544b4dde997cfa09dbd825aa3813e7 GIT binary patch literal 30153 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 literal 0 HcmV?d00001 -- GitLab From bd042099453a055afee142016fcb9bc66ad018db Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 10:23:54 +0200 Subject: [PATCH 64/97] Cleanup: Remove unused code from bench_input_arm_demo --- Commands/bench_input_arm_demo.py | 105 ++++++++++++------------------- Helper/Plot.py | 30 ++++++--- 2 files changed, 59 insertions(+), 76 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index e5e8a4e..e7bcb97 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -16,18 +16,20 @@ from PyQt5 import QtWidgets from Gui.DemoWindow import DemoWindow from Helper.Frequency import Frequency -from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python, Config +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python, Config, Plot from Backends import XMC4500 import Analyzers CPU = "xmc4500" CPU_TYPE = "cortex-m4" +ANALYZERS_TO_SHOW = ['aiT', 'platin'] CHUNK_SIZE = 1000 def geomean(a): return numpy.exp( numpy.log(a).mean() ) + def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, xmax, ymax): fle = tempfile.mktemp(suffix='.png') @@ -39,13 +41,11 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, xmax, fig.subplots_adjust(hspace=0, wspace=0) - # uncomment the following line to remove the plot's background - #axes.patch.set_visible(False) axes.set_xlim((0, xmax)) axes.set_ylim((0, ymax)) axes.yaxis.grid(True) - if None != cycles: + if not cycles is None: _, bins, _ = axes.hist(cycles[0:idx], bins=500, facecolor=args.color_bin, alpha=0.5) ana_y = axes.get_ylim()[0] + 0.8*(axes.get_ylim()[1] - axes.get_ylim()[0]) @@ -63,11 +63,9 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, xmax, axes.axvline(x=result, ymin=0, ymax=1, linewidth=linewidth, color=args.color_analyzer) axes.text(result, ana_y, ' ' + name, ha='left', va='bottom', color=args.color_analyzer) - fig.patch.set_alpha(0) - axes.patch.set_alpha(.7) + Plot.set_transparency(fig, 0, 0.7) + Plot.set_tight_layout(fig) - fig.tight_layout(pad=0, h_pad=0, w_pad=0) - fig.set_tight_layout(True) fig.savefig(fle, dpi=100) return fle @@ -76,22 +74,19 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): fle = tempfile.mktemp(suffix='.png') fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) - fig.set_alpha(0.5) axes = fig.add_subplot(111) from matplotlib.ticker import MaxNLocator axes.yaxis.set_major_locator(MaxNLocator(integer=True)) - arr = numpy.array(factors) - 1.0 + arr = numpy.array(factors) + geos = geomean(arr) - 1.0 + arr = arr - 1.0 mins = numpy.min(arr) maxs = numpy.max(arr) - geos = geomean(arr) - - geo_valid = geomean( [ f for f in arr if f >= 0.0 ] ) # dump histograms - #if len(ubs[ana]) == 0: continue valid = [100*f for f in arr if f >= 0.0] invalid = [100*f for f in arr if f < 0.0] @@ -103,11 +98,9 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): axes.axvline(x=100 * geos, ymin=0, ymax=1, linewidth=1, color='g') axes.axvline(x=0, ymin=0, ymax=1, linewidth=2, color=args.color_wcet) - fig.patch.set_alpha(0) - axes.patch.set_alpha(.7) + Plot.set_transparency(fig, 0, 0.7) + Plot.set_tight_layout(fig) - fig.tight_layout(pad=0, h_pad=0, w_pad=0) - fig.set_tight_layout(True) fig.savefig(fle, dpi=100) return fle @@ -164,11 +157,9 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): # equal conf.samples due to special input values sample_count = len(inp) - procs = [] - pipe_parent, pipe_child = Pipe() - p = Process(target=benchmark_input_run, args=(pipe_child, args, inp, xmc, conf.eic, sw, conf.xmax, conf.ymax)) - p.start() - procs.append({'proc':p, 'pos':0, 'pipe':pipe_parent, 'input':inp}) + pipe_bm, pipe_child = Pipe() + proc_bm = Process(target=bench_and_plot, args=(pipe_child, args, inp, xmc, conf.eic, sw, conf.xmax, conf.ymax)) + proc_bm.start() Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) @@ -187,35 +178,25 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): idx = 0 finished = 0 analyzer_results = {} - analyzers = ['aiT', 'platin'] + while True: samples_changed = False - for proc in procs: - # TODO: this? - if None == proc['proc']: continue - - wcet, fle, pos = proc['pipe'].recv() - if None == fle: - proc['proc'].join() - proc['proc'] = None - continue - - sw.update_sampling.emit(fle) - sw.update_message.emit(str(pos) + " samples") - sw.update_progress.emit(0, conf.samples, pos) + wcet, fle, pos = pipe_bm.recv() + sw.update_sampling.emit(fle) + sw.update_message.emit(str(pos) + " samples") + sw.update_progress.emit(0, conf.samples, pos) ana_changed = False - proc = procs[0] - while pipe_ana.poll(): + while None != wcet and pipe_ana.poll(): ana_changed = True name, result = pipe_ana.recv() analyzer_results[name] = result - proc['pipe'].send( (name, result.cy) ) + pipe_bm.send( (name, result.cy) ) sum_pipe.send( (name, float(result.cy)/wcet) ) if ana_changed: - for name in analyzers: + for name in ANALYZERS_TO_SHOW: if name in analyzer_results: result = analyzer_results[name] if result.cy > 0: @@ -232,16 +213,9 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): sw.update_analyzer.emit(name, 'working.png', name) time.sleep(0.01) - Log.operation_end("Wait for Sampling") - # get rid of unused arrays - del inp - del procs + assert False, "Never reached" - dat.write(odir + '/result.dat') - - # check that worst-observed execution time is triggered by worst-case input - assert dat.max == dat.cy_seed, "Maximum execution cycles exceeds cycles used for seed" def worker(args, configs, dw): odir = "results/aladdin_rtas17_demo" @@ -277,29 +251,30 @@ def worker(args, configs, dw): fle = anahist_to_file(args, factors[ana], dw.sum[ana].mpWidth.value, dw.sum[ana].mpHeight.value, None, None) dw.sum[ana].update_sampling.emit(fle) - assert False, "Should never end up here" + assert False, "Never reached" -def benchmark_input_run(pipe, args, input_array, xmc, eic, sw, xmax, ymax): +def bench_and_plot(pipe, args, input_array, xmc, eic, sw, xmax, ymax): xmc.connect() xmc.set_ic(eic) analyzers = {} + cycles = numpy.zeros(len(input_array), numpy.uint32) + boet = None + wcet = None - cycles = numpy.zeros(len(input_array), numpy.uint32) - - boet = None - wcet = None + def __recv_wca_result(): + while pipe.poll(): + name, res = pipe.recv() + if None != name: + analyzers[name] = res for i in xrange(0, len(input_array)): cycles[i] = xmc.benchmark(input_array[i]) - assert cycles[i], "Invalid value from benchmark()" + if not cycles[i]: Log.fail("Invalid value from benchmark(): cycles[" + str(i) + "] = " + str(cycles[i])) if 0 == i % CHUNK_SIZE and 0 < i: - while pipe.poll(): - name, res = pipe.recv() - if None != name: - analyzers[name] = res + __recv_wca_result() boet = numpy.amin(cycles[0:i]) wcet = numpy.amax(cycles[0:i]) @@ -308,15 +283,13 @@ def benchmark_input_run(pipe, args, input_array, xmc, eic, sw, xmax, ymax): pipe.send( (wcet, fle, i) ) while True: - name, res = pipe.recv() - if None != name: - analyzers[name] = res + __recv_wca_result() - fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers, boet, wcet, xmax, ymax) + fle = hist_to_file(args, cycles, len(input_array), sw.mpWidth.value, sw.mpHeight.value, analyzers, boet, wcet, xmax, ymax) pipe.send( (wcet, fle, len(input_array)) ) - pipe.send( (None, None, None) ) - pipe.close() + assert False, "Never reached" + def analyzers_run(pipe, args, conf, odir, dat, axf, pml): for name, res in Analyzers.run(args, odir, dat, CPU, axf, "gene_main", pml, None, conf.eic, run_ff = True, run_noff = False): diff --git a/Helper/Plot.py b/Helper/Plot.py index ccdf003..2949439 100644 --- a/Helper/Plot.py +++ b/Helper/Plot.py @@ -49,6 +49,23 @@ def get_plot(args): return fig +def set_tight_layout(fig): + fig.tight_layout(pad=0, h_pad=0, w_pad=0) + fig.set_tight_layout(True) + + +def set_transparency(fig, fig_bg=0.0, plot_bg=1.0): + ax = fig.gca() + fig.patch.set_alpha(fig_bg) # whole figure's background + ax.patch.set_alpha(plot_bg) # plot area's background + + +def set_framewidth(fig, width): + ax = fig.gca() + for pos in ['top','bottom','left','right']: + ax.spines[pos].set_linewidth(width) + + def configure(args, fig): ax = fig.gca() @@ -61,13 +78,6 @@ def configure(args, fig): ax.set_xlim(left=args.xmin, right=args.xmax) ax.set_xlim(left=args.xmin, right=args.xmax) - for pos in ['top','bottom','left','right']: - ax.spines[pos].set_linewidth(args.frame_width) - - # set figure patch transparent, - # set axis patch opaque - fig.patch.set_alpha(0) - ax.patch.set_alpha(1) - - fig.tight_layout(pad=0, h_pad=0, w_pad=0) - fig.set_tight_layout(True) + set_framewidth(fig, args.frame_width) + set_transparency(fig) + set_tight_layout(fig) -- GitLab From 6e996389933f5200ac53e937b20a5857f2c4e786 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 13:08:59 +0200 Subject: [PATCH 65/97] Cleanup: Remove unused code from bench_input_arm_redpitaya.py --- Commands/bench_input_arm_redpitaya.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Commands/bench_input_arm_redpitaya.py b/Commands/bench_input_arm_redpitaya.py index 081c70c..8dbb37f 100644 --- a/Commands/bench_input_arm_redpitaya.py +++ b/Commands/bench_input_arm_redpitaya.py @@ -174,10 +174,6 @@ def run(args): rp.set_gain('HV') rp.set_decimation(__deci) - offset = - ( int(rp.buffer_size) / 2 - 1) - Log.debug("Buffer Size: " + str(rp.buffer_size) + " => Offset: " + str(offset)) - rp.set_trigger('CH' + str(args.pitaya_in) + '_NE', __tlevel_high_low, offset) - # search for osci <-> XMC4500 channel, rp_xmc = rp.select_device(xmcs) if None == rp_xmc: return -- GitLab From 84c8a0710f5351f87d66065b2eff1721e41833e0 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 15:03:30 +0200 Subject: [PATCH 66/97] Reduce intensity of bg_rp.png --- data/img/bg_rp.png | Bin 30153 -> 24457 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/img/bg_rp.png b/data/img/bg_rp.png index 93ef3d1f9a544b4dde997cfa09dbd825aa3813e7..35677171198a36c7e13a0f2b6a4df1dfc5f50cf1 100644 GIT binary patch literal 24457 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

    V``e$B$K3>Yt) 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 literal 30153 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 -- GitLab From 0b86540efe488b87a38ed312266dc2f8571bd6fa Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 15:05:02 +0200 Subject: [PATCH 67/97] Integrate RedPitaya into Demo --- Commands/bench_input_arm_demo.py | 142 +++++++++++++++++++++++-------- Helper/Benchmark.py | 15 ++-- Helper/RedPitaya.py | 1 + 3 files changed, 113 insertions(+), 45 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index e7bcb97..88cfc68 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -16,7 +16,7 @@ from PyQt5 import QtWidgets from Gui.DemoWindow import DemoWindow from Helper.Frequency import Frequency -from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python, Config, Plot +from Helper import Benchmark, Dat, Log, Tools, Pattern, Execution, Result, GenE, Python, Config, Plot, RedPitaya from Backends import XMC4500 import Analyzers @@ -24,15 +24,18 @@ CPU = "xmc4500" CPU_TYPE = "cortex-m4" ANALYZERS_TO_SHOW = ['aiT', 'platin'] -CHUNK_SIZE = 1000 + +__tlevel_low_high = 0.2 +__tlevel_high_low = 2.5 +__deci = 64 def geomean(a): return numpy.exp( numpy.log(a).mean() ) -def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, xmax, ymax): +def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): - fle = tempfile.mktemp(suffix='.png') + fle = tempfile.mktemp(suffix='.png', dir='/dev/shm/aladdin/') linewidth = 1 @@ -41,12 +44,14 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, xmax, fig.subplots_adjust(hspace=0, wspace=0) - axes.set_xlim((0, xmax)) - axes.set_ylim((0, ymax)) + axes.set_xlim((0, conf.xmax)) + axes.set_ylim((0, conf.ymax)) axes.yaxis.grid(True) + lines = [] + if not cycles is None: - _, bins, _ = axes.hist(cycles[0:idx], bins=500, facecolor=args.color_bin, alpha=0.5) + _, bins, _ = axes.hist(cycles[0:idx], bins=conf.bins, facecolor=args.color_bin, alpha=0.5) ana_y = axes.get_ylim()[0] + 0.8*(axes.get_ylim()[1] - axes.get_ylim()[0]) @@ -92,12 +97,19 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): bins = range(int(100 * mins), int(100 * maxs), 1) + axes.set_xlim((-25, int(100 * maxs) + 25)) + axes.hist(valid, bins, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) axes.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0.1, alpha=0.5) axes.axvline(x=100 * geos, ymin=0, ymax=1, linewidth=1, color='g') axes.axvline(x=0, ymin=0, ymax=1, linewidth=2, color=args.color_wcet) + center = axes.get_ylim()[1] / 2 + axes.annotate('Optimum', xy=(0, center), xycoords='data', + xytext=(-3, 0), textcoords='offset points', + va='center', ha='right', rotation=90, color=args.color_wcet, fontsize='large') + Plot.set_transparency(fig, 0, 0.7) Plot.set_tight_layout(fig) @@ -105,7 +117,7 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): return fle -def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): +def benchmark_worker(args, sum_pipe, swx, swr, xmc, rp, no, conf): odir = "results/aladdin_rtas17_demo/benchmark" + str(no) if not os.path.exists(odir): @@ -120,22 +132,28 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): time.sleep(2) # initialize the widgets with empty plots - sw.update_sampling.emit( - hist_to_file(args, None, 0, sw.mpWidth.value, sw.mpHeight.value, {}, None, None, conf.xmax, conf.ymax) + swx.update_sampling.emit( + hist_to_file(args, None, 0, swx.mpWidth.value, swx.mpHeight.value, {}, None, None, conf) ) + if not swr is None: + swr.update_message.emit("Waiting...") + swr.update_sampling.emit( + hist_to_file(args, None, 0, swr.mpWidth.value, swr.mpHeight.value, {}, None, None, conf) + ) + dat = Dat.Data() dat.seed = conf.seed dat.pattern = Pattern.assemble_string(conf.suite) ## generate the benchmark - sw.update_message.emit("Generating Benchmark...") + swx.update_message.emit("Generating Benchmark...") bm = GenE.get_benchmark(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, ll, opt=conf.opt, of=conf.ofactor) Tools.llvm_lls_to_ll([ll], ll_comb) Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) ## compile the benchmark OS - sw.update_message.emit("Compile Benchmark...") + swx.update_message.emit("Compile Benchmark...") o_xmc = "./xmc4500/gene_seed" + str(conf.seed) + ".o" shutil.copyfile(o, o_xmc) Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(conf.seed) + ".axf", None, 0) @@ -143,7 +161,7 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): os.remove(o_xmc) ## flash to devices - sw.update_message.emit("Flash Benchmark...") + swx.update_message.emit("Flash Benchmark...") success, errors = xmc.flash_retry(odir, axf) if not success: Log.fail("Flashing to " + str(xmc) + " failed: " + ", ".join(errors)) @@ -152,13 +170,13 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): time.sleep(2) - inp = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1)[0] + inp = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1, crafted=conf.crafted)[0] # get the total number of samples, which does not # equal conf.samples due to special input values sample_count = len(inp) pipe_bm, pipe_child = Pipe() - proc_bm = Process(target=bench_and_plot, args=(pipe_child, args, inp, xmc, conf.eic, sw, conf.xmax, conf.ymax)) + proc_bm = Process(target=bench_and_plot, args=(pipe_child, args, inp, xmc, rp, conf.eic, swx, swr, conf)) proc_bm.start() Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) @@ -167,13 +185,13 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): proc_ana = Process(target=analyzers_run, args=(pipe_child, args, conf, odir, dat, axf, pml)) proc_ana.start() - sw.update_message.emit("Sampling...") + swx.update_message.emit("Sampling...") dat.inputs = numpy.zeros(sample_count, numpy.uint32) dat.cycles = numpy.zeros(sample_count, numpy.uint32) - sw.samples = dat.cycles - sw.samples_lock = threading.Lock() + swx.samples = dat.cycles + swx.samples_lock = threading.Lock() idx = 0 finished = 0 @@ -182,10 +200,16 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): while True: samples_changed = False - wcet, fle, pos = pipe_bm.recv() - sw.update_sampling.emit(fle) - sw.update_message.emit(str(pos) + " samples") - sw.update_progress.emit(0, conf.samples, pos) + wcet, fle, rp_fle, pos = pipe_bm.recv() + swx.update_sampling.emit(fle) + swx.update_message.emit(str(pos) + " samples") + swx.update_progress.emit(0, conf.samples, pos) + + if not rp_fle is None: + assert not swr is None + swr.update_sampling.emit(rp_fle) + swr.update_message.emit(str(pos) + " samples") + swr.update_progress.emit(0, conf.samples, pos) ana_changed = False while None != wcet and pipe_ana.poll(): @@ -201,16 +225,16 @@ def benchmark_worker(args, sum_pipe, sw, xmc, no, conf): result = analyzer_results[name] if result.cy > 0: #maxval = numpy.amax(dat.cycles[0:idx]) # TODO - sw.update_analyzer.emit(name, 'checkmark.png', name + ': ' + str(result.cy) + 'cy') + swx.update_analyzer.emit(name, 'checkmark.png', name + ': ' + str(result.cy) + 'cy') else: msg = name + ': ' if None != result.error_msg and "" != result.error_msg: msg += result.error_msg else: msg += Dat.errno_to_string(result.cy) - sw.update_analyzer.emit(name, 'error.png', msg) + swx.update_analyzer.emit(name, 'error.png', msg) else: - sw.update_analyzer.emit(name, 'working.png', name) + swx.update_analyzer.emit(name, 'working.png', name) time.sleep(0.01) @@ -226,16 +250,37 @@ def worker(args, configs, dw): Log.set_logfile(odir + '/aladdin.log') xmcs = XMC4500.list_devices(odir, args.xmccache) - if None == xmcs: - return + if None == xmcs: return + + rps = RedPitaya.list_devices() + if 0 == len(rps): return + + Log.info("Found RP at " + rps[0]) + rp = RedPitaya.RedPitaya(rps[0]) + rp.set_default_input(1) + rp.set_gain('HV') + rp.set_decimation(__deci) + + # search for osci <-> XMC4500 + channel, rp_xmc = rp.select_device(xmcs) + if None == rp_xmc: return + rp.set_default_input(channel) + + rp_config = next((c for c in configs if c.haspitaya), None) + assert None != rp_config, "Failed to find configuration for RP" + + configs.remove(rp_config) + xmcs.remove(rp_xmc) bm_count = min(len(xmcs), len(configs) + 1) - pipe, pipe_child = Pipe() + Log.info("Starting " + str(bm_count) + " worker threads") + pipe, pipe_child = Pipe() for i in xrange(0, bm_count): - start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[i], xmcs[i], i, configs[i])) + start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[i], None, xmcs[i], None, i, configs[i])) + start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[-2], dw.sws[-1], rp_xmc, rp, i, rp_config)) factors = { 'ait': [], @@ -254,39 +299,62 @@ def worker(args, configs, dw): assert False, "Never reached" -def bench_and_plot(pipe, args, input_array, xmc, eic, sw, xmax, ymax): +def bench_and_plot(pipe, args, input_array, xmc, rp, eic, swx, swr, conf): xmc.connect() xmc.set_ic(eic) analyzers = {} cycles = numpy.zeros(len(input_array), numpy.uint32) + samples = numpy.zeros(len(input_array), numpy.uint32) boet = None wcet = None def __recv_wca_result(): + changed = False while pipe.poll(): name, res = pipe.recv() if None != name: analyzers[name] = res + changed = True + + return changed for i in xrange(0, len(input_array)): - cycles[i] = xmc.benchmark(input_array[i]) + if rp is None: + cycles[i] = xmc.benchmark(input_array[i]) + else: + (cycles[i], raw) = rp.measure(xmc, input_array[i], 2.5) + samples[i] = int( 120 * rp.samples_to_time( RedPitaya.samples_high_to_low(raw, 0.2, 2.5) ) ) + if not cycles[i]: Log.fail("Invalid value from benchmark(): cycles[" + str(i) + "] = " + str(cycles[i])) - if 0 == i % CHUNK_SIZE and 0 < i: + if 0 == i % conf.chunksize and 0 < i: __recv_wca_result() boet = numpy.amin(cycles[0:i]) wcet = numpy.amax(cycles[0:i]) - fle = hist_to_file(args, cycles, i, sw.mpWidth.value, sw.mpHeight.value, analyzers, boet, wcet, xmax, ymax) - pipe.send( (wcet, fle, i) ) + rp_fle = None + if not rp is None: + rp_fle = hist_to_file(args, samples, i, swr.mpWidth.value, swr.mpHeight.value, {}, boet, wcet, conf) + + fle = hist_to_file(args, cycles, i, swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) + pipe.send( (wcet, fle, rp_fle, i) ) while True: - __recv_wca_result() + while True: + name, res = pipe.recv() + if None != name: + analyzers[name] = res + + if not pipe.poll(): break + + rp_fle = None + if not rp is None: + rp_fle = hist_to_file(args, samples, i, swr.mpWidth.value, swr.mpHeight.value, {}, boet, wcet, conf) - fle = hist_to_file(args, cycles, len(input_array), sw.mpWidth.value, sw.mpHeight.value, analyzers, boet, wcet, xmax, ymax) - pipe.send( (wcet, fle, len(input_array)) ) + fle = hist_to_file(args, cycles, len(input_array), swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) + pipe.send( (wcet, fle, rp_fle, len(input_array)) ) assert False, "Never reached" diff --git a/Helper/Benchmark.py b/Helper/Benchmark.py index 5402584..9af8bf9 100644 --- a/Helper/Benchmark.py +++ b/Helper/Benchmark.py @@ -3,7 +3,7 @@ import numpy import array -def get_input(bits, seed, samples, chunks): +def get_input(bits, seed, samples, chunks, crafted = True): input_data = [] for i in range(0, chunks): input_data.append( array.array('L') ) # L = unsigned long @@ -19,18 +19,17 @@ def get_input(bits, seed, samples, chunks): # perform an explicit enumeration of all possible inputs # if 2^(bits) > #samples + __add(seed) if 2**bits <= samples: - __add(seed) for val in range(0, 2**bits - 1): __add(val) else: - __add(seed) + if crafted: + for exp in range(0, bits - 1): + __add(seed ^ (2**exp)) - for exp in range(0, bits - 1): - __add(seed ^ (2**exp)) - - for exp in range(0, bits - 1): - __add(2**exp) + for exp in range(0, bits - 1): + __add(2**exp) for i in xrange(samples): __add(numpy.random.randint(2**bits - 1)) diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index e31631f..7122900 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -126,6 +126,7 @@ class RedPitaya(object): def select_device(self, devices, rep_limit = 5): for dev in devices: for channel in [1, 2]: + Log.debug("Trying " + str(dev) + " CH" + str(channel)) dev.connect() _, raw = self.measure(dev, inp=0, tlevel_high_low=2.5, channel=channel) dev.disconnect() -- GitLab From f264e53ee9ed9418b104b50f4af5b685ca3dcd3b Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 16:22:00 +0200 Subject: [PATCH 68/97] Do not include errors from WCET analyzers in geomean --- Commands/bench_input_arm_demo.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 88cfc68..ce2c9b5 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -217,7 +217,9 @@ def benchmark_worker(args, sum_pipe, swx, swr, xmc, rp, no, conf): name, result = pipe_ana.recv() analyzer_results[name] = result pipe_bm.send( (name, result.cy) ) - sum_pipe.send( (name, float(result.cy)/wcet) ) + + factor = float(result.cy)/wcet if result.cy > 0 else result.cy + sum_pipe.send( (name, factor) ) if ana_changed: for name in ANALYZERS_TO_SHOW: @@ -286,11 +288,20 @@ def worker(args, configs, dw): 'ait': [], 'platin': [] } + + errors = { + 'ait': [], + 'platin': [] + } + while True: ana, factor = pipe.recv() ana = ana.lower() - factors[ana].append(factor) + if factor > 0: + factors[ana].append(factor) + else: + errors[ana].append(factor) if(len(factors[ana]) > 1): fle = anahist_to_file(args, factors[ana], dw.sum[ana].mpWidth.value, dw.sum[ana].mpHeight.value, None, None) -- GitLab From 0faa39b7913c3848e5b056b4e746210b1ea9d483 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 16:22:34 +0200 Subject: [PATCH 69/97] Implement heuristic for label positioning --- Commands/bench_input_arm_demo.py | 57 ++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index ce2c9b5..8a49650 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -37,8 +37,6 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): fle = tempfile.mktemp(suffix='.png', dir='/dev/shm/aladdin/') - linewidth = 1 - fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) axes = fig.add_subplot(111) @@ -53,20 +51,20 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): if not cycles is None: _, bins, _ = axes.hist(cycles[0:idx], bins=conf.bins, facecolor=args.color_bin, alpha=0.5) - ana_y = axes.get_ylim()[0] + 0.8*(axes.get_ylim()[1] - axes.get_ylim()[0]) + ana_y = axes.get_ylim()[0] + 0.9*(axes.get_ylim()[1] - axes.get_ylim()[0]) minval = numpy.amin(cycles[0:idx]) - axes.axvline(x=minval, ymin=0, ymax=1, linewidth=linewidth, color='m') - axes.text(minval, ana_y, ' BOET', ha='left', va='bottom', color='m') - maxval = numpy.amax(cycles[0:idx]) - axes.axvline(x=maxval, ymin=0, ymax=1, linewidth=linewidth, color=args.color_wcet) - axes.text(maxval, ana_y, 'WCET', ha='right', va='bottom', color=args.color_wcet) + + lines = [] + lines.append( (minval, ana_y, 'BOET', 'm') ) + lines.append( (maxval, ana_y, 'WCET', args.color_wcet) ) for name, result in analyzers.iteritems(): if result > 0: - axes.axvline(x=result, ymin=0, ymax=1, linewidth=linewidth, color=args.color_analyzer) - axes.text(result, ana_y, ' ' + name, ha='left', va='bottom', color=args.color_analyzer) + lines.append( (result, ana_y, name, args.color_analyzer) ) + + add_lines(axes, 0, conf.xmax + 1, lines) Plot.set_transparency(fig, 0, 0.7) Plot.set_tight_layout(fig) @@ -75,6 +73,45 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): return fle +def add_lines(axes, xmin, xmax, lines): + # add 0 and xmax + 1 to ensure that the outermost labels are directed towards the inside + anchors = find_anchors( [xmin, xmax] + [x for x, _, _, _ in lines] ) + + for x, y, text, color in lines: + anchor = anchors[x] + + off = 3 if 'left' == anchors[x] else -3 + + axes.axvline(x=x, ymin=0, ymax=1, linewidth=1, color=color) + axes.annotate(text, xy=(x, y), xycoords='data', + xytext=(off, 0), textcoords='offset points', + va='center', ha=anchors[x], color=color, fontsize='large') + + + +def find_anchors(values): + deltas = { value: (numpy.inf, numpy.inf) for value in values } + + for value in values: + for cmp_val in values: + delta = abs( int(value) - int(cmp_val) ) + delta_left, delta_right = deltas[value] + if cmp_val < value and delta < delta_left: + deltas[value] = (delta, delta_right) + if cmp_val > value and delta < delta_right: + deltas[value] = (delta_left, delta) + + anchors = {} + for value in deltas.iterkeys(): + delta_right, delta_left = deltas[value] + if delta_right < delta_left: + anchors[value] = 'left' + else: + anchors[value] = 'right' + + return anchors + + def anahist_to_file(args, factors, width, height, xmax, ymax): fle = tempfile.mktemp(suffix='.png') -- GitLab From 0b42588a0739d5477e8b6af8cfba7708f9f542ea Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 3 Apr 2017 16:23:16 +0200 Subject: [PATCH 70/97] Ensure benchmark IDs are IDs --- Commands/bench_input_arm_demo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 8a49650..00889ce 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -136,8 +136,8 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): axes.set_xlim((-25, int(100 * maxs) + 25)) - axes.hist(valid, bins, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) - axes.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0.1, alpha=0.5) + if(len(valid) > 0): axes.hist(valid, bins, facecolor=args.color_bin, linewidth=0.1, alpha=0.5) + if(len(invalid) > 0): axes.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0.1, alpha=0.5) axes.axvline(x=100 * geos, ymin=0, ymax=1, linewidth=1, color='g') axes.axvline(x=0, ymin=0, ymax=1, linewidth=2, color=args.color_wcet) @@ -319,7 +319,7 @@ def worker(args, configs, dw): for i in xrange(0, bm_count): start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[i], None, xmcs[i], None, i, configs[i])) - start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[-2], dw.sws[-1], rp_xmc, rp, i, rp_config)) + start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[-2], dw.sws[-1], rp_xmc, rp, bm_count, rp_config)) factors = { 'ait': [], -- GitLab From f279826e340877f6556d20d0fd5132ecd5bd297a Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 4 Apr 2017 08:26:52 +0200 Subject: [PATCH 71/97] Silence output of benchmark generation --- Commands/bench_input_arm_demo.py | 2 +- Helper/GenE.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 00889ce..ee8f416 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -185,7 +185,7 @@ def benchmark_worker(args, sum_pipe, swx, swr, xmc, rp, no, conf): ## generate the benchmark swx.update_message.emit("Generating Benchmark...") - bm = GenE.get_benchmark(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, ll, opt=conf.opt, of=conf.ofactor) + bm = GenE.execute(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, conf.ofactor, ll, silent=True) Tools.llvm_lls_to_ll([ll], ll_comb) Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) diff --git a/Helper/GenE.py b/Helper/GenE.py index f307d7f..2d5fedb 100644 --- a/Helper/GenE.py +++ b/Helper/GenE.py @@ -59,7 +59,7 @@ class Benchmark(object): for p in pattern: Log.info(" -> Pattern " + p + ": " + str(self.pattern[p]) + "x") -def execute(pattern, cpu, cost, bits, seed, of, out_ll, simulate, silent=False): +def execute(pattern, cpu, cost, bits, seed, of, out_ll, simulate=True, silent=False): pattern_str = " ".join( map('--pattern {0}'.format, pattern) ) proc = pexpect.spawn("patmos-gene -mcpu=" + cpu + " " + pattern_str + " -gene-cost=" + str(cost) + " -gene-input-bits=" + str(bits) + " -gene-seed=" + str(seed) + " -ll=\"" + out_ll + "\" -gene-overweight-factor=" + str(of) + (" -gene-execute" if simulate else "")) -- GitLab From a5305feab83f26cf9e99525828e4cb946bea85e2 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 4 Apr 2017 09:28:00 +0200 Subject: [PATCH 72/97] Fix incorrect caluclation of bins --- Commands/bench_input_arm_demo.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index ee8f416..8196722 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -132,7 +132,7 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): valid = [100*f for f in arr if f >= 0.0] invalid = [100*f for f in arr if f < 0.0] - bins = range(int(100 * mins), int(100 * maxs), 1) + bins = range(-25, int(100 * maxs) + 25, 1) axes.set_xlim((-25, int(100 * maxs) + 25)) @@ -263,7 +263,6 @@ def benchmark_worker(args, sum_pipe, swx, swr, xmc, rp, no, conf): if name in analyzer_results: result = analyzer_results[name] if result.cy > 0: - #maxval = numpy.amax(dat.cycles[0:idx]) # TODO swx.update_analyzer.emit(name, 'checkmark.png', name + ': ' + str(result.cy) + 'cy') else: msg = name + ': ' -- GitLab From c4a7a2c84d3352a0f0fff663b535a563f07c8cc2 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 4 Apr 2017 09:35:22 +0200 Subject: [PATCH 73/97] Move plotting helpers to Plot.py --- Commands/bench_input_arm_demo.py | 49 +++----------------------------- Helper/Plot.py | 40 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 45 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 8196722..3396d4a 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -56,15 +56,12 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): minval = numpy.amin(cycles[0:idx]) maxval = numpy.amax(cycles[0:idx]) - lines = [] - lines.append( (minval, ana_y, 'BOET', 'm') ) - lines.append( (maxval, ana_y, 'WCET', args.color_wcet) ) + lines = [ (minval, ana_y, 'BOET', 'm'), + (maxval, ana_y, 'WCET', args.color_wcet) ] - for name, result in analyzers.iteritems(): - if result > 0: - lines.append( (result, ana_y, name, args.color_analyzer) ) + lines += [ (result, ana_y, name, args.color_analyzer) for name, result in analyzers.iteritems() if result > 0 ] - add_lines(axes, 0, conf.xmax + 1, lines) + Plot.add_lines(axes, 0, conf.xmax + 1, lines) Plot.set_transparency(fig, 0, 0.7) Plot.set_tight_layout(fig) @@ -73,44 +70,6 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): return fle -def add_lines(axes, xmin, xmax, lines): - # add 0 and xmax + 1 to ensure that the outermost labels are directed towards the inside - anchors = find_anchors( [xmin, xmax] + [x for x, _, _, _ in lines] ) - - for x, y, text, color in lines: - anchor = anchors[x] - - off = 3 if 'left' == anchors[x] else -3 - - axes.axvline(x=x, ymin=0, ymax=1, linewidth=1, color=color) - axes.annotate(text, xy=(x, y), xycoords='data', - xytext=(off, 0), textcoords='offset points', - va='center', ha=anchors[x], color=color, fontsize='large') - - - -def find_anchors(values): - deltas = { value: (numpy.inf, numpy.inf) for value in values } - - for value in values: - for cmp_val in values: - delta = abs( int(value) - int(cmp_val) ) - delta_left, delta_right = deltas[value] - if cmp_val < value and delta < delta_left: - deltas[value] = (delta, delta_right) - if cmp_val > value and delta < delta_right: - deltas[value] = (delta_left, delta) - - anchors = {} - for value in deltas.iterkeys(): - delta_right, delta_left = deltas[value] - if delta_right < delta_left: - anchors[value] = 'left' - else: - anchors[value] = 'right' - - return anchors - def anahist_to_file(args, factors, width, height, xmax, ymax): fle = tempfile.mktemp(suffix='.png') diff --git a/Helper/Plot.py b/Helper/Plot.py index 2949439..a10af2f 100644 --- a/Helper/Plot.py +++ b/Helper/Plot.py @@ -1,6 +1,7 @@ #!/usr/bin/python import matplotlib +import numpy def add_arguments(parser): fs_def = matplotlib.rcParams['font.size'] @@ -81,3 +82,42 @@ def configure(args, fig): set_framewidth(fig, args.frame_width) set_transparency(fig) set_tight_layout(fig) + + +def add_lines(axes, xmin, xmax, lines): + # add 0 and xmax + 1 to ensure that the outermost labels are directed towards the inside + anchors = find_anchors( [xmin, xmax] + [x for x, _, _, _ in lines] ) + + for x, y, text, color in lines: + anchor = anchors[x] + + off = 3 if 'left' == anchors[x] else -3 + + axes.axvline(x=x, ymin=0, ymax=1, linewidth=1, color=color) + axes.annotate(text, xy=(x, y), xycoords='data', + xytext=(off, 0), textcoords='offset points', + va='center', ha=anchors[x], color=color, fontsize='large') + + + +def find_anchors(values): + deltas = { value: (numpy.inf, numpy.inf) for value in values } + + for value in values: + for cmp_val in values: + delta = abs( int(value) - int(cmp_val) ) + delta_left, delta_right = deltas[value] + if cmp_val < value and delta < delta_left: + deltas[value] = (delta, delta_right) + if cmp_val > value and delta < delta_right: + deltas[value] = (delta_left, delta) + + anchors = {} + for value in deltas.iterkeys(): + delta_right, delta_left = deltas[value] + if delta_right < delta_left: + anchors[value] = 'left' + else: + anchors[value] = 'right' + + return anchors -- GitLab From cc97fdf12fc9281ded11167166079026839ff9d2 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 4 Apr 2017 12:14:18 +0200 Subject: [PATCH 74/97] Remove non-required code from bench_input_arm_demo.py --- Commands/bench_input_arm_demo.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 3396d4a..94c7af0 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -40,24 +40,17 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) axes = fig.add_subplot(111) - fig.subplots_adjust(hspace=0, wspace=0) - axes.set_xlim((0, conf.xmax)) axes.set_ylim((0, conf.ymax)) axes.yaxis.grid(True) - lines = [] - if not cycles is None: _, bins, _ = axes.hist(cycles[0:idx], bins=conf.bins, facecolor=args.color_bin, alpha=0.5) ana_y = axes.get_ylim()[0] + 0.9*(axes.get_ylim()[1] - axes.get_ylim()[0]) - minval = numpy.amin(cycles[0:idx]) - maxval = numpy.amax(cycles[0:idx]) - - lines = [ (minval, ana_y, 'BOET', 'm'), - (maxval, ana_y, 'WCET', args.color_wcet) ] + lines = [ (numpy.amin(cycles[0:idx]), ana_y, 'BOET', 'm'), + (numpy.amax(cycles[0:idx]), ana_y, 'WCET', args.color_wcet) ] lines += [ (result, ana_y, name, args.color_analyzer) for name, result in analyzers.iteritems() if result > 0 ] -- GitLab From 976f11a2c1a8226e3598f7e0c01c90b6e267400c Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 4 Apr 2017 12:14:44 +0200 Subject: [PATCH 75/97] Implement killing parentless instances of aladdin --- aladdin | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/aladdin b/aladdin index 33e84f7..f744096 100755 --- a/aladdin +++ b/aladdin @@ -1,5 +1,6 @@ #!/usr/bin/python from __future__ import print_function +from __future__ import unicode_literals from datetime import datetime import argparse as ap @@ -7,6 +8,7 @@ import pkgutil import os import resource import psutil +from prompt_toolkit.shortcuts import confirm from Helper import Log @@ -60,6 +62,14 @@ parser = ap.ArgumentParser( parser.add_argument(dest="command", metavar='', choices=['comp'] + cmd_to_mod.keys(), help='Function to be executed') args = parser.parse_known_args() +dead_aladdins = [ proc for proc in psutil.process_iter() if 'aladdin' == proc.name() and 1 == proc.ppid() ] +if len(dead_aladdins) > 0: + Log.warn("Found " + str(len(dead_aladdins)) + " dead aladdins: " + ", ".join(str(proc.pid) for proc in dead_aladdins)) + if confirm("Shall I terminate these processes? (y/n) "): + for proc in dead_aladdins: + proc.kill() + + cmd = args[0].command if 'comp' == cmd: -- GitLab From eddd8d8b3e61b30ebddec5b2d9025e14420e61c6 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 10 Apr 2017 08:13:34 +0200 Subject: [PATCH 76/97] Fix python errors due to changed interfaces in Plot.py --- Commands/dat2stat.py | 26 ++++++++++++++++---------- Helper/Plot.py | 1 + 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/Commands/dat2stat.py b/Commands/dat2stat.py index 88db75d..ca7f64a 100644 --- a/Commands/dat2stat.py +++ b/Commands/dat2stat.py @@ -15,8 +15,8 @@ def get_argparser(): parser = argparse.ArgumentParser(prog=sys.argv[0] + " dat2stat", description='Subfunction dat2stat: ' + desc) parser.add_argument('input', metavar='input', nargs='+', type=str, help='The file(s) to be converted') - Plot.register_color('analyzer', 'upper bounds from WCET analyzers', '#19B219') - Plot.register_color('binfail', 'Facecolor of Bins with ub/WCET < 1', 'red') + Plot.register_color(parser, 'analyzer', 'upper bounds from WCET analyzers', '#19B219') + Plot.register_color(parser, 'binfail', 'Facecolor of Bins with ub/WCET < 1', 'red') Plot.add_arguments(parser) return parser @@ -43,13 +43,17 @@ def run(args): wcets.append(dat.max) - if cy > 0: ubs[ana].append( (float(cy) / dat.max) ) + if cy > 0: + factor = (float(cy) / dat.max) + ubs[ana].append(factor) + #if factor < 1.0: + # Log.warn("Found underestimation (" + str(factor) + ") for " + fle) else: if cy not in wca_errors[ana]: wca_errors[ana][cy] = 0 wca_errors[ana][cy] += 1 - if not ana.endswith("_noff") and cy == Dat.ERROR_WCET_UNBOUND: - Log.fail(" Unbound: " + fle ) + #if not ana.endswith("_noff") and cy == Dat.ERROR_WCET_UNBOUND: + # Log.fail(" Unbound: " + fle ) for fle in args.input: have_fle = False @@ -99,7 +103,7 @@ def run(args): bins = range(int(100 * mins[ana]), int(100 * maxs[ana]), 1) - plt.hist(valid, bins, facecolor=args.color_bin, linewidth=0, alpha=0.5) + plt.hist(valid, bins, facecolor=args.color_face, linewidth=0, alpha=0.5) plt.hist(invalid, bins, facecolor=args.color_binfail, linewidth=0, alpha=0.5) plt.xlabel('Overestimation [%]') @@ -110,12 +114,14 @@ def run(args): #plt.text(0, ana_y, ' BOET', ha='left', va='bottom', color='m') #plt.title(str(len(ubs[ana])) + " samples, " + str(len(invalid)) + " underestimations") - plt.axvline(x=100 * geos[ana], ymin=0, ymax=1, linewidth=1, color='g') + plt.axvline(x=100 * geos[ana], ymin=0, ymax=1, linewidth=1, color='#000080') + plt.gca().annotate('Geo. Mean', + xy=(100 * geos[ana], 1.5*center), xycoords='data', xytext=(3, 0), textcoords='offset points', va='center', ha='left', rotation=90, color='#000080') - plt.axvline(x=0, ymin=0, ymax=1, linewidth=1, color=args.color_wcet) + plt.axvline(x=0, ymin=0, ymax=1, linewidth=1, color=args.color_max) plt.gca().annotate('Optimum', - xy=(0, center), xycoords='data', xytext=(-3, 0), textcoords='offset points', va='center', ha='right', rotation=90, color=args.color_wcet) + xy=(0, center), xycoords='data', xytext=(-3, 0), textcoords='offset points', va='center', ha='right', rotation=90, color=args.color_max) plt.savefig(ana + ".pdf", format="pdf") plt.clf() @@ -123,7 +129,7 @@ def run(args): # dump histogram "WCET" bins = numpy.linspace(min(mins.itervalues()), max(maxs.itervalues()), 500) - plt.hist(wcets, 500, facecolor=args.color_bin, alpha=0.5) + plt.hist(wcets, 500, facecolor=args.color_face, alpha=0.5) plt.xlabel('WCET[cy]') plt.ylabel('Occurrences') diff --git a/Helper/Plot.py b/Helper/Plot.py index a10af2f..46e9a28 100644 --- a/Helper/Plot.py +++ b/Helper/Plot.py @@ -45,6 +45,7 @@ def register_color(parser, name, label, default): def get_plot(args): + import matplotlib.pyplot as plt fig = plt.figure(0, figsize=(args.width, args.height)) configure(args, fig) return fig -- GitLab From 5368d37fb976dae7394df7803b7289369031ba9c Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 10 Apr 2017 08:14:41 +0200 Subject: [PATCH 77/97] Remove (non-working) "maximize" from SamplingWidget --- Gui/SamplingWidget.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Gui/SamplingWidget.py b/Gui/SamplingWidget.py index cc551cd..6869314 100644 --- a/Gui/SamplingWidget.py +++ b/Gui/SamplingWidget.py @@ -48,24 +48,6 @@ class SamplingWidget(QtWidgets.QFrame): icon.setPixmap(QtGui.QPixmap("aladdin/data/img/" + img).scaledToWidth(height)) label.setText(message) -# def enterEvent(self, evt): -# if not self.maximized: -# self.setStyleSheet(".SamplingWidget { background: '#FFFDDA'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") -# -# def leaveEvent(self, evt): -# self.setStyleSheet(".SamplingWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") - - def mousePressEvent(self, evt): - if self.maximized: - self.maximized = False - for sw in self.win.findChildren(SamplingWidget): - sw.show() - else: - self.maximized = True - for sw in self.win.findChildren(SamplingWidget): - if self != sw: sw.hide() - - def resizeEvent(self, evt): self.__set_font(evt.size().height() / 25) self.__set_icon(evt.size().height() / 25) @@ -98,8 +80,6 @@ class SamplingWidget(QtWidgets.QFrame): self.mpWidth = multiprocessing.Value('i', 0) self.mpHeight = multiprocessing.Value('i', 0) - self.maximized = False - self.setStyleSheet(".SamplingWidget { background: 'white'; border: 5px outset grey; padding: -5px; border-radius: 10px; }") self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) -- GitLab From 1931e077c7a20d9f1d569aa2686cff1e56d69a1b Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 10 Apr 2017 08:15:14 +0200 Subject: [PATCH 78/97] Implement dumping stracktraces to file on SIGUSR1 --- aladdin | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/aladdin b/aladdin index f744096..6c9eff2 100755 --- a/aladdin +++ b/aladdin @@ -9,9 +9,23 @@ import os import resource import psutil from prompt_toolkit.shortcuts import confirm +import code, traceback, signal from Helper import Log + +def debug(sig, frame): + print("Signal received: SIGUSR1; Dumping stacktraces to file(s)") + + import os, sys + with open("aladdin_calltraces_pid" + str(os.getpid()), 'w') as log: + for tid, frame in sys._current_frames().items(): + log.write("=============== " + str(tid) + " ===============\n") + traceback.print_stack(frame, file=log) + log.write("\n\n") + + sys.exit(1) + BASE=os.path.dirname(os.path.realpath(__file__)) class ZshCompHelpFormatter(ap.HelpFormatter): @@ -39,6 +53,10 @@ def get_modules(prefix): return { name[len(prefix):]: Module(prefix, name, loader) for loader, name, _ in pkgutil.walk_packages('.') if name.startswith(prefix) } + +# Register signal handler for SIGUSR1 +signal.signal(signal.SIGUSR1, debug) + # find all available modules in Commands/ oldcwd=os.getcwd() os.chdir( os.path.dirname(os.path.realpath(__file__)) ) -- GitLab From 39145d5f0e4dfe79c75a7c2cc86e4306c02250db Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 10 Apr 2017 08:17:32 +0200 Subject: [PATCH 79/97] Demo: Change default temp path to /dev/shm/aladdin/ --- Commands/bench_input_arm_demo.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 94c7af0..428efd8 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -23,6 +23,9 @@ import Analyzers CPU = "xmc4500" CPU_TYPE = "cortex-m4" +TEMP_BASE = "/dev/shm/" +TEMP = TEMP_BASE + "aladdin/" + ANALYZERS_TO_SHOW = ['aiT', 'platin'] __tlevel_low_high = 0.2 @@ -392,6 +395,13 @@ def get_argparser(): return parser def run(args): + assert os.path.exists(TEMP_BASE) + + if os.path.exists(TEMP): + shutil.rmtree(TEMP) + + os.makedirs(TEMP) + global plt plt = Python.import_pyplot('Agg') -- GitLab From a550d7ab6d5815e61ef38e65585f6d52d514e0c2 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 10 Apr 2017 08:18:08 +0200 Subject: [PATCH 80/97] Fix incorrect calculation of worker threads --- Commands/bench_input_arm_demo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index 428efd8..f12bd6e 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -265,9 +265,9 @@ def worker(args, configs, dw): configs.remove(rp_config) xmcs.remove(rp_xmc) - bm_count = min(len(xmcs), len(configs) + 1) + bm_count = min(len(xmcs), len(configs)) - Log.info("Starting " + str(bm_count) + " worker threads") + Log.info("Starting " + str(bm_count + 1) + " worker threads") pipe, pipe_child = Pipe() for i in xrange(0, bm_count): -- GitLab From 0c171b7f8cf34135f671e8a0eeab4b843e475c94 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 10 Apr 2017 08:19:52 +0200 Subject: [PATCH 81/97] dat2pgf: Use Plot.add_lines() instead of manual positioning --- Commands/dat2pgf.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Commands/dat2pgf.py b/Commands/dat2pgf.py index 21581bb..c5cc094 100644 --- a/Commands/dat2pgf.py +++ b/Commands/dat2pgf.py @@ -2,6 +2,7 @@ import argparse import sys import os +import numpy import matplotlib from Helper import Dat, Execution, Log, Python, Plot @@ -39,13 +40,15 @@ def hist_fh(args, dat): minorLocator = AutoMinorLocator(5) axes.yaxis.set_minor_locator(minorLocator) - center = plt.ylim()[1] / 2 + ana_y = axes.get_ylim()[0] + 0.9*(axes.get_ylim()[1] - axes.get_ylim()[0]) -# plt.axvline(x=info['min'], ymin=0, ymax=1, linewidth=2, color='g') -# plt.text(info['min'] + 0.065*xmax, 1.1 * center, 'BOET', ha='right', va='bottom', color='g') + lines = [ (numpy.amin(dat.cycles()), ana_y, 'BOET', 'm'), + (numpy.amax(dat.cycles()), ana_y, 'WCET', args.color_max) ] - plt.axvline(x=dat.max, ymin=0, ymax=1, linewidth=args.linewidth, color=args.color_max) - plt.text(0.98*dat.max, 1.6 * center, args.label_wcet.replace('\\n', '\n'), ha='right', va='top', color=args.color_max) + lines += [ (dat.wcet['ait'], ana_y, 'aiT', args.color_analyzer) ] + lines += [ (dat.wcet['platin'], ana_y, 'platin', args.color_analyzer) ] + + Plot.add_lines(axes, 0, dat.get_x_maximum() + 1, lines) plt.locator_params(nbins=8, axis='x') plt.locator_params(nbins=5, axis='y') -- GitLab From 88d100f84914537219dc9de1ba10a6f08e74dbe1 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 10 Apr 2017 08:21:03 +0200 Subject: [PATCH 82/97] Demo: Fix command description --- Commands/bench_input_arm_demo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Commands/bench_input_arm_demo.py b/Commands/bench_input_arm_demo.py index f12bd6e..7fb9ebe 100644 --- a/Commands/bench_input_arm_demo.py +++ b/Commands/bench_input_arm_demo.py @@ -419,4 +419,4 @@ def run(args): sys.exit(qApp.exec_()) -desc = "[ARM] Benchmark GenE using varying values as input to the generated executable" +desc = "GenE Benchmark Generation Demo: 9x XMC4500 & 1x Red Pitaya" -- GitLab From b76f382482404c7f7bc372c5f418a97c9f4c8e74 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Mon, 10 Apr 2017 08:22:14 +0200 Subject: [PATCH 83/97] Demo: Rename command to "demo" --- Commands/{bench_input_arm_demo.py => demo.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Commands/{bench_input_arm_demo.py => demo.py} (100%) diff --git a/Commands/bench_input_arm_demo.py b/Commands/demo.py similarity index 100% rename from Commands/bench_input_arm_demo.py rename to Commands/demo.py -- GitLab From 4cac87affffc7cf12ef3cf5816b1dc22e536c70e Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 11 Apr 2017 10:01:32 +0200 Subject: [PATCH 84/97] Rewrite multithreading/multiprocessing in demo.py --- Commands/demo.py | 365 ++++++++++++++++++++++-------------------- Gui/SamplingWidget.py | 14 +- 2 files changed, 199 insertions(+), 180 deletions(-) diff --git a/Commands/demo.py b/Commands/demo.py index 7fb9ebe..8f70184 100644 --- a/Commands/demo.py +++ b/Commands/demo.py @@ -10,6 +10,7 @@ import numpy import threading import tempfile import matplotlib +import select from thread import start_new_thread from PyQt5 import QtWidgets @@ -28,6 +29,10 @@ TEMP = TEMP_BASE + "aladdin/" ANALYZERS_TO_SHOW = ['aiT', 'platin'] +EVENT_ANALYZER = 0 # (no, ana, factor) +EVENT_PLOT = 1 # (no, pos, fle_xmc, fle_rp) +EVENT_MESSAGE = 2 # (no, msg) + __tlevel_low_high = 0.2 __tlevel_high_low = 2.5 __deci = 64 @@ -40,6 +45,8 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): fle = tempfile.mktemp(suffix='.png', dir='/dev/shm/aladdin/') + matplotlib.rcParams.update({'font.size': height/25}) + fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) axes = fig.add_subplot(111) @@ -109,8 +116,9 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): return fle -def benchmark_worker(args, sum_pipe, swx, swr, xmc, rp, no, conf): - odir = "results/aladdin_rtas17_demo/benchmark" + str(no) + +def measurement_process(args, pipe, conf, xmc, rp, no, swx, swr): + odir = "results/aladdin_rtas17_demo/benchmark" + str(no) + "_seed" + str(conf.seed) if not os.path.exists(odir): os.makedirs(odir) @@ -124,28 +132,27 @@ def benchmark_worker(args, sum_pipe, swx, swr, xmc, rp, no, conf): time.sleep(2) # initialize the widgets with empty plots - swx.update_sampling.emit( - hist_to_file(args, None, 0, swx.mpWidth.value, swx.mpHeight.value, {}, None, None, conf) - ) + plot_xmc = hist_to_file(args, None, 0, swx.mpWidth.value, swx.mpHeight.value, {}, None, None, conf) + plot_rp = None if not swr is None: - swr.update_message.emit("Waiting...") - swr.update_sampling.emit( - hist_to_file(args, None, 0, swr.mpWidth.value, swr.mpHeight.value, {}, None, None, conf) - ) + pipe.send( (EVENT_MESSAGE, no, "Waiting...") ) + plot_rp = hist_to_file(args, None, 0, swr.mpWidth.value, swr.mpHeight.value, {}, None, None, conf) + + pipe.send( (EVENT_PLOT, no, 0, None, plot_xmc, plot_rp) ) dat = Dat.Data() dat.seed = conf.seed dat.pattern = Pattern.assemble_string(conf.suite) ## generate the benchmark - swx.update_message.emit("Generating Benchmark...") + pipe.send( (EVENT_MESSAGE, no, "Generating Benchmark...") ) bm = GenE.execute(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, conf.ofactor, ll, silent=True) Tools.llvm_lls_to_ll([ll], ll_comb) Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) ## compile the benchmark OS - swx.update_message.emit("Compile Benchmark...") + pipe.send( (EVENT_MESSAGE, no, "Compile Benchmark...") ) o_xmc = "./xmc4500/gene_seed" + str(conf.seed) + ".o" shutil.copyfile(o, o_xmc) Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(conf.seed) + ".axf", None, 0) @@ -153,218 +160,174 @@ def benchmark_worker(args, sum_pipe, swx, swr, xmc, rp, no, conf): os.remove(o_xmc) ## flash to devices - swx.update_message.emit("Flash Benchmark...") + pipe.send( (EVENT_MESSAGE, no, "Flash Benchmark...") ) success, errors = xmc.flash_retry(odir, axf) if not success: Log.fail("Flashing to " + str(xmc) + " failed: " + ", ".join(errors)) return else: Log.debug("Successfully flashed to " + str(xmc)) - time.sleep(2) - - inp = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1, crafted=conf.crafted)[0] - # get the total number of samples, which does not - # equal conf.samples due to special input values - sample_count = len(inp) - - pipe_bm, pipe_child = Pipe() - proc_bm = Process(target=bench_and_plot, args=(pipe_child, args, inp, xmc, rp, conf.eic, swx, swr, conf)) - proc_bm.start() - Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) pipe_ana, pipe_child = Pipe() - proc_ana = Process(target=analyzers_run, args=(pipe_child, args, conf, odir, dat, axf, pml)) + proc_ana = Process(target=analyzers_process, args=(pipe_child, args, conf, odir, dat, axf, pml)) proc_ana.start() - swx.update_message.emit("Sampling...") - - dat.inputs = numpy.zeros(sample_count, numpy.uint32) - dat.cycles = numpy.zeros(sample_count, numpy.uint32) - - swx.samples = dat.cycles - swx.samples_lock = threading.Lock() - - idx = 0 - finished = 0 - analyzer_results = {} - - while True: - samples_changed = False - - wcet, fle, rp_fle, pos = pipe_bm.recv() - swx.update_sampling.emit(fle) - swx.update_message.emit(str(pos) + " samples") - swx.update_progress.emit(0, conf.samples, pos) - - if not rp_fle is None: - assert not swr is None - swr.update_sampling.emit(rp_fle) - swr.update_message.emit(str(pos) + " samples") - swr.update_progress.emit(0, conf.samples, pos) - - ana_changed = False - while None != wcet and pipe_ana.poll(): - ana_changed = True - name, result = pipe_ana.recv() - analyzer_results[name] = result - pipe_bm.send( (name, result.cy) ) - - factor = float(result.cy)/wcet if result.cy > 0 else result.cy - sum_pipe.send( (name, factor) ) - - if ana_changed: - for name in ANALYZERS_TO_SHOW: - if name in analyzer_results: - result = analyzer_results[name] - if result.cy > 0: - swx.update_analyzer.emit(name, 'checkmark.png', name + ': ' + str(result.cy) + 'cy') - else: - msg = name + ': ' - if None != result.error_msg and "" != result.error_msg: - msg += result.error_msg - else: - msg += Dat.errno_to_string(result.cy) - swx.update_analyzer.emit(name, 'error.png', msg) - else: - swx.update_analyzer.emit(name, 'working.png', name) - - time.sleep(0.01) - - assert False, "Never reached" - + time.sleep(2) -def worker(args, configs, dw): - odir = "results/aladdin_rtas17_demo" + pipe.send( (EVENT_MESSAGE, no, "Sampling...") ) - if not os.path.exists(odir): - os.makedirs(odir) + #### + analyzers = {} + input_array = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1, crafted=conf.crafted)[0] + cycles = numpy.zeros(len(input_array), numpy.uint32) + samples = numpy.zeros(len(input_array), numpy.uint32) + boet = None + wcet = None - Log.set_logfile(odir + '/aladdin.log') + def update_plots(pos): + rp_fle = None + if not rp is None: + rp_fle = hist_to_file(args, samples, pos, swr.mpWidth.value, swr.mpHeight.value, {}, boet, wcet, conf) - xmcs = XMC4500.list_devices(odir, args.xmccache) - if None == xmcs: return + fle = hist_to_file(args, cycles, pos, swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) + pipe.send( (EVENT_PLOT, no, pos, wcet, fle, rp_fle) ) + pipe.send( (EVENT_MESSAGE, no, str(pos) + " samples") ) - rps = RedPitaya.list_devices() - if 0 == len(rps): return - Log.info("Found RP at " + rps[0]) - rp = RedPitaya.RedPitaya(rps[0]) - rp.set_default_input(1) - rp.set_gain('HV') - rp.set_decimation(__deci) + def handle_analyzer_result(name, res): + assert not name is None + assert not wcet is None - # search for osci <-> XMC4500 - channel, rp_xmc = rp.select_device(xmcs) - if None == rp_xmc: return - rp.set_default_input(channel) + pipe.send( (EVENT_ANALYZER, no, name, res.cy, wcet) ) + if res.cy > 0: + analyzers[name] = res.cy - rp_config = next((c for c in configs if c.haspitaya), None) - assert None != rp_config, "Failed to find configuration for RP" - configs.remove(rp_config) - xmcs.remove(rp_xmc) + wca_done = False - bm_count = min(len(xmcs), len(configs)) + xmc.connect() + xmc.set_ic(conf.eic) + for i in xrange(0, len(input_array)): + if rp is None: + cycles[i] = xmc.benchmark(input_array[i]) + else: + cycles[i], raw = rp.measure(xmc, input_array[i], 2.5) + samples[i] = int( 120 * rp.samples_to_time( RedPitaya.samples_high_to_low(raw, 0.2, 2.5) ) ) - Log.info("Starting " + str(bm_count + 1) + " worker threads") + if not cycles[i]: Log.fail("Invalid value from benchmark(): cycles[" + str(i) + "] = " + str(cycles[i])) - pipe, pipe_child = Pipe() - for i in xrange(0, bm_count): - start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[i], None, xmcs[i], None, i, configs[i])) + if 0 == i % conf.chunksize and 0 < i: + boet = numpy.amin(cycles[0:i]) + wcet = numpy.amax(cycles[0:i]) - start_new_thread(benchmark_worker, (args, pipe_child, dw.sws[-2], dw.sws[-1], rp_xmc, rp, bm_count, rp_config)) + while pipe_ana.poll(): + name, res = pipe_ana.recv() + if None != name: + handle_analyzer_result(name, res) + else: wca_done = True - factors = { - 'ait': [], - 'platin': [] - } + update_plots(i) - errors = { - 'ait': [], - 'platin': [] - } + # wait for pending WCET analyzer results + while not wca_done: + name, res = pipe_ana.recv() + if not name is None: + handle_analyzer_result(name, res) + update_plots(len(input_array)) + else: + # (None, None) is received after the last analyzer + break - while True: - ana, factor = pipe.recv() - ana = ana.lower() + Log.info("Measurement & Analysis for seed " + str(conf.seed) + " finished") - if factor > 0: - factors[ana].append(factor) - else: - errors[ana].append(factor) - if(len(factors[ana]) > 1): - fle = anahist_to_file(args, factors[ana], dw.sum[ana].mpWidth.value, dw.sum[ana].mpHeight.value, None, None) - dw.sum[ana].update_sampling.emit(fle) +def event_handle_analyzer(args, dw, configs, no, ana, bound, wcet, factors = {}, errors = {}): + assert not wcet is None - assert False, "Never reached" + swx = dw.sws[no] + # update label + if ana in ANALYZERS_TO_SHOW: + if bound > 0: + swx.update_analyzer.emit(ana, 'checkmark.png', ana + ': ' + str(bound) + 'cy') + else: #TODO this doesn't work + msg = ana + ': Error' + #if None != result.error_msg and "" != result.error_msg: + # msg += result.error_msg + #else: + # msg += Dat.errno_to_string(bound) + swx.update_analyzer.emit(ana, 'error.png', msg) + #else: + # swx.update_analyzer.emit(name, 'working.png', name) -def bench_and_plot(pipe, args, input_array, xmc, rp, eic, swx, swr, conf): - xmc.connect() - xmc.set_ic(eic) + # update summary histograms + ana = ana.lower() + if not ana in factors: factors[ana] = [] + if not ana in errors: errors[ana] = [] - analyzers = {} - cycles = numpy.zeros(len(input_array), numpy.uint32) - samples = numpy.zeros(len(input_array), numpy.uint32) - boet = None - wcet = None + if bound > 0: + factor = float(bound) / wcet + factors[ana].append(factor) + else: + errors[ana].append(bound) - def __recv_wca_result(): - changed = False - while pipe.poll(): - name, res = pipe.recv() - if None != name: - analyzers[name] = res - changed = True + if(len(factors[ana]) > 1): + fle = anahist_to_file(args, factors[ana], dw.sum[ana].mpWidth.value, dw.sum[ana].mpHeight.value, None, None) + dw.sum[ana].update_sampling.emit(fle) - return changed - for i in xrange(0, len(input_array)): - if rp is None: - cycles[i] = xmc.benchmark(input_array[i]) - else: - (cycles[i], raw) = rp.measure(xmc, input_array[i], 2.5) - samples[i] = int( 120 * rp.samples_to_time( RedPitaya.samples_high_to_low(raw, 0.2, 2.5) ) ) +def event_handle_plot(args, dw, conf, no, pos, fle_xmc, fle_rp): + # update images + dw.sws[no].update_sampling.emit(fle_xmc) + if not fle_rp is None: + dw.sws[no + 1].update_sampling.emit(fle_rp) - if not cycles[i]: Log.fail("Invalid value from benchmark(): cycles[" + str(i) + "] = " + str(cycles[i])) + # update progress bar + dw.sws[no].update_progress.emit(0, conf.samples, pos) + if not fle_rp is None: + dw.sws[no + 1].update_progress.emit(0, conf.samples, pos) - if 0 == i % conf.chunksize and 0 < i: - __recv_wca_result() - boet = numpy.amin(cycles[0:i]) - wcet = numpy.amax(cycles[0:i]) +def event_handle_message(args, dw, configs, no, msg): + dw.sws[no].update_message.emit(msg) - rp_fle = None - if not rp is None: - rp_fle = hist_to_file(args, samples, i, swr.mpWidth.value, swr.mpHeight.value, {}, boet, wcet, conf) - fle = hist_to_file(args, cycles, i, swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) - pipe.send( (wcet, fle, rp_fle, i) ) +def event_recver(args, pipes, dw, configs): + """Receive events from other processes and forward them to the GUI""" while True: - while True: - name, res = pipe.recv() - if None != name: - analyzers[name] = res + ready, _, _ = select.select(pipes, [], []) - if not pipe.poll(): break + for pipe in ready: + res = pipe.recv() - rp_fle = None - if not rp is None: - rp_fle = hist_to_file(args, samples, i, swr.mpWidth.value, swr.mpHeight.value, {}, boet, wcet, conf) + event, no = res[0:2] + if not no < len(configs): + Log.fail("Have event for no " + str(no) + ", len(configs) = " + str(len(configs))) + assert False + + conf = configs[no] - fle = hist_to_file(args, cycles, len(input_array), swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) - pipe.send( (wcet, fle, rp_fle, len(input_array)) ) + if EVENT_ANALYZER == event: + ana, bound, wcet = res[2:] + event_handle_analyzer(args, dw, conf, no, ana, bound, wcet) + elif EVENT_PLOT == event: + pos, wcet, fle_xmc, fle_rp = res[2:] #TODO: wcet? + event_handle_plot(args, dw, conf, no, pos, fle_xmc, fle_rp) + elif EVENT_MESSAGE == event: + msg = res[2] + # TODO: handle msg_rp + event_handle_message(args, dw, conf, no, msg) assert False, "Never reached" -def analyzers_run(pipe, args, conf, odir, dat, axf, pml): +def analyzers_process(pipe, args, conf, odir, dat, axf, pml): for name, res in Analyzers.run(args, odir, dat, CPU, axf, "gene_main", pml, None, conf.eic, run_ff = True, run_noff = False): pipe.send( (name, res) ) + pipe.send( (None, None) ) pipe.close() @@ -394,9 +357,40 @@ def get_argparser(): return parser + +def find_hardware(args, configs, odir): + xmcs = XMC4500.list_devices(odir, args.xmccache) + if None == xmcs: return + + rps = RedPitaya.list_devices() + if 0 == len(rps): return + + Log.info("Found RP at " + rps[0]) + rp = RedPitaya.RedPitaya(rps[0]) + rp.set_default_input(1) + rp.set_gain('HV') + rp.set_decimation(__deci) + + # search for osci <-> XMC4500 + channel, rp_xmc = rp.select_device(xmcs) + if None == rp_xmc: return # TODO: handle theses cases properly! + rp.set_default_input(channel) + + rp_config = next((c for c in configs if c.haspitaya), None) + assert None != rp_config, "Failed to find configuration for RP" + + xmcs.remove(rp_xmc) + + return (xmcs, rp, rp_xmc, rp_config) + def run(args): - assert os.path.exists(TEMP_BASE) + odir = "results/aladdin_rtas17_demo" + if not os.path.exists(odir): + os.makedirs(odir) + + Log.set_logfile(odir + '/aladdin.log') + assert os.path.exists(TEMP_BASE) if os.path.exists(TEMP): shutil.rmtree(TEMP) @@ -408,13 +402,40 @@ def run(args): Analyzers.prepare(args) configs = Config.from_xml(args.config) + configs = sorted(configs, key=lambda conf: 1 if conf.haspitaya else 0) + + # search for hardware + xmcs, rp, rp_xmc, rp_config = find_hardware(args, configs, odir) + + # fork prior to any other threading + bm_count = min(len(xmcs), len(configs)) + + # setup QT qApp = QtWidgets.QApplication(sys.argv) dw = DemoWindow(len(configs) + 1, 100) dw.setWindowTitle(args.title) dw.show() - start_new_thread(worker, (args, configs, dw)) + Log.info("Starting " + str(bm_count + 1) + " worker processes") + pipes = [] + def start_process(args, xmc, rp, conf, no): + pipe, pipe_child = Pipe() + swr = dw.sws[no + 1] if conf.haspitaya else None + proc = Process(target=measurement_process, args=(args, pipe_child, conf, xmc, rp, no, dw.sws[no], swr)) + pipes.append(pipe) + proc.start() + + i = 0 + for conf in configs: + if not conf.haspitaya: + start_process(args, xmcs[i], None, conf, i) + i += 1 + + # the Red Pitaya/XMC4500 combination is handled manually + start_process(args, rp_xmc, rp, rp_config, len(configs) - 1) + + start_new_thread(event_recver, (args, pipes, dw, configs)) sys.exit(qApp.exec_()) diff --git a/Gui/SamplingWidget.py b/Gui/SamplingWidget.py index 6869314..810eaa6 100644 --- a/Gui/SamplingWidget.py +++ b/Gui/SamplingWidget.py @@ -1,3 +1,4 @@ +import os.path from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5.QtCore import QObject, pyqtSignal @@ -13,9 +14,10 @@ class SamplingWidget(QtWidgets.QFrame): self.message.setText(message) update_sampling = pyqtSignal('QString') - def handle_update_sampling(self, fle):#, analyzers): + def handle_update_sampling(self, fle): + assert os.path.exists(fle) pixmap = QtGui.QPixmap(fle) - self.sampling.setPixmap(pixmap)#.scaledToWidth(self.sampling.width())) + self.sampling.setPixmap(pixmap) update_progress = pyqtSignal(int, int, int) def handle_update_progress(self, mi, ma, pr): @@ -49,6 +51,7 @@ class SamplingWidget(QtWidgets.QFrame): label.setText(message) def resizeEvent(self, evt): + Log.warn("Resizeevent to: " + str(evt.size())) self.__set_font(evt.size().height() / 25) self.__set_icon(evt.size().height() / 25) @@ -60,9 +63,6 @@ class SamplingWidget(QtWidgets.QFrame): newfont = QtGui.QFont("sans-serif", weight=QtGui.QFont.Bold) newfont.setPixelSize(size) - import matplotlib - matplotlib.rcParams.update({'font.size': size}) - self.message.setFont(newfont) self.progress.setFont(newfont) for _, label, _, _ in self.analyzers.itervalues(): @@ -102,7 +102,7 @@ class SamplingWidget(QtWidgets.QFrame): vlayout_topleft = QtWidgets.QVBoxLayout() margin = vlayout_topleft.contentsMargins() - margin.setLeft(50) + margin.setLeft(20) vlayout_topleft.setContentsMargins(margin) hlayout.addLayout(vlayout_topleft) @@ -128,8 +128,6 @@ class SamplingWidget(QtWidgets.QFrame): self.main_widget.setFocus() self.analyzers = {} - self.samples = None - self.samples_lock = None for name in ['aiT', 'platin']: self.handle_update_analyzer(name, 'working.png', name) -- GitLab From 204dac9c6559e27f703798f999b3186d388b9236 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Tue, 11 Apr 2017 17:27:19 +0200 Subject: [PATCH 85/97] Implement multiple iterations for demo --- Commands/demo.py | 263 +++++++++++++++++++++------------------- Commands/demo_dryrun.py | 129 ++++++++++---------- Helper/Config.py | 32 +++-- 3 files changed, 229 insertions(+), 195 deletions(-) diff --git a/Commands/demo.py b/Commands/demo.py index 8f70184..07b160f 100644 --- a/Commands/demo.py +++ b/Commands/demo.py @@ -32,6 +32,7 @@ ANALYZERS_TO_SHOW = ['aiT', 'platin'] EVENT_ANALYZER = 0 # (no, ana, factor) EVENT_PLOT = 1 # (no, pos, fle_xmc, fle_rp) EVENT_MESSAGE = 2 # (no, msg) +EVENT_FINISHED = 4 __tlevel_low_high = 0.2 __tlevel_high_low = 2.5 @@ -117,129 +118,137 @@ def anahist_to_file(args, factors, width, height, xmax, ymax): return fle -def measurement_process(args, pipe, conf, xmc, rp, no, swx, swr): - odir = "results/aladdin_rtas17_demo/benchmark" + str(no) + "_seed" + str(conf.seed) - - if not os.path.exists(odir): - os.makedirs(odir) - - ll = odir + '/bench.ll' - ll_comb = odir + '/combined.ll' - o = odir + '/bench.o' - pml = odir + '/bench.pml' - axf = odir + '/bench.axf' - - time.sleep(2) +def measurement_process(args, pipe, xmc, rp, no, swx, swr): + while True: + conf = pipe.recv() + if conf is None: + Log.fail("Measurement finished, no further configurations") + return - # initialize the widgets with empty plots - plot_xmc = hist_to_file(args, None, 0, swx.mpWidth.value, swx.mpHeight.value, {}, None, None, conf) - - plot_rp = None - if not swr is None: - pipe.send( (EVENT_MESSAGE, no, "Waiting...") ) - plot_rp = hist_to_file(args, None, 0, swr.mpWidth.value, swr.mpHeight.value, {}, None, None, conf) - - pipe.send( (EVENT_PLOT, no, 0, None, plot_xmc, plot_rp) ) + odir = "results/aladdin_rtas17_demo/benchmark" + str(no) + "_seed" + str(conf.seed) - dat = Dat.Data() - dat.seed = conf.seed - dat.pattern = Pattern.assemble_string(conf.suite) + if not os.path.exists(odir): + os.makedirs(odir) - ## generate the benchmark - pipe.send( (EVENT_MESSAGE, no, "Generating Benchmark...") ) - bm = GenE.execute(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, conf.ofactor, ll, silent=True) - Tools.llvm_lls_to_ll([ll], ll_comb) - Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) + ll = odir + '/bench.ll' + ll_comb = odir + '/combined.ll' + o = odir + '/bench.o' + pml = odir + '/bench.pml' + axf = odir + '/bench.axf' + + time.sleep(2) - ## compile the benchmark OS - pipe.send( (EVENT_MESSAGE, no, "Compile Benchmark...") ) - o_xmc = "./xmc4500/gene_seed" + str(conf.seed) + ".o" - shutil.copyfile(o, o_xmc) - Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(conf.seed) + ".axf", None, 0) - shutil.move("./xmc4500/bin/system_gene_seed" + str(conf.seed) + ".axf", axf) - os.remove(o_xmc) + # initialize the widgets with empty plots + plot_xmc = hist_to_file(args, None, 0, swx.mpWidth.value, swx.mpHeight.value, {}, None, None, conf) + + plot_rp = None + if not swr is None: + pipe.send( (EVENT_MESSAGE, no, "Waiting...") ) + plot_rp = hist_to_file(args, None, 0, swr.mpWidth.value, swr.mpHeight.value, {}, None, None, conf) + + pipe.send( (EVENT_PLOT, no, 0, None, plot_xmc, plot_rp) ) + + dat = Dat.Data() + dat.seed = conf.seed + dat.pattern = Pattern.assemble_string(conf.suite) - ## flash to devices - pipe.send( (EVENT_MESSAGE, no, "Flash Benchmark...") ) - success, errors = xmc.flash_retry(odir, axf) - if not success: - Log.fail("Flashing to " + str(xmc) + " failed: " + ", ".join(errors)) - return - else: Log.debug("Successfully flashed to " + str(xmc)) + ## generate the benchmark + pipe.send( (EVENT_MESSAGE, no, "Generating Benchmark...") ) + bm = GenE.execute(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, conf.ofactor, ll, silent=True) + Tools.llvm_lls_to_ll([ll], ll_comb) + Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) - Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) + ## compile the benchmark OS + pipe.send( (EVENT_MESSAGE, no, "Compile Benchmark...") ) + o_xmc = "./xmc4500/gene_seed" + str(conf.seed) + ".o" + shutil.copyfile(o, o_xmc) + Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(conf.seed) + ".axf", None, 0) + shutil.move("./xmc4500/bin/system_gene_seed" + str(conf.seed) + ".axf", axf) + os.remove(o_xmc) - pipe_ana, pipe_child = Pipe() - proc_ana = Process(target=analyzers_process, args=(pipe_child, args, conf, odir, dat, axf, pml)) - proc_ana.start() + ## flash to devices + pipe.send( (EVENT_MESSAGE, no, "Flash Benchmark...") ) + success, errors = xmc.flash_retry(odir, axf) + if not success: + Log.fail("Flashing to " + str(xmc) + " failed: " + ", ".join(errors)) + return + else: Log.debug("Successfully flashed to " + str(xmc)) - time.sleep(2) + Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) - pipe.send( (EVENT_MESSAGE, no, "Sampling...") ) + pipe_ana, pipe_child = Pipe() + proc_ana = Process(target=analyzers_process, args=(pipe_child, args, conf, odir, dat, axf, pml)) + proc_ana.start() - #### - analyzers = {} - input_array = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1, crafted=conf.crafted)[0] - cycles = numpy.zeros(len(input_array), numpy.uint32) - samples = numpy.zeros(len(input_array), numpy.uint32) - boet = None - wcet = None + time.sleep(2) - def update_plots(pos): - rp_fle = None - if not rp is None: - rp_fle = hist_to_file(args, samples, pos, swr.mpWidth.value, swr.mpHeight.value, {}, boet, wcet, conf) + pipe.send( (EVENT_MESSAGE, no, "Sampling...") ) - fle = hist_to_file(args, cycles, pos, swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) - pipe.send( (EVENT_PLOT, no, pos, wcet, fle, rp_fle) ) - pipe.send( (EVENT_MESSAGE, no, str(pos) + " samples") ) + #### + analyzers = {} + input_array = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1, crafted=conf.crafted)[0] + cycles = numpy.zeros(len(input_array), numpy.uint32) + samples = numpy.zeros(len(input_array), numpy.uint32) + boet = None + wcet = None + def update_plots(pos): + rp_fle = None + if not rp is None: + rp_fle = hist_to_file(args, samples, pos, swr.mpWidth.value, swr.mpHeight.value, {}, boet, wcet, conf) - def handle_analyzer_result(name, res): - assert not name is None - assert not wcet is None + fle = hist_to_file(args, cycles, pos, swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) + pipe.send( (EVENT_PLOT, no, pos, wcet, fle, rp_fle) ) + pipe.send( (EVENT_MESSAGE, no, str(pos) + " samples") ) - pipe.send( (EVENT_ANALYZER, no, name, res.cy, wcet) ) - if res.cy > 0: - analyzers[name] = res.cy + def handle_analyzer_result(name, res): + assert not name is None + assert not wcet is None - wca_done = False + pipe.send( (EVENT_ANALYZER, no, name, res.cy, wcet) ) + if res.cy > 0: + analyzers[name] = res.cy - xmc.connect() - xmc.set_ic(conf.eic) - for i in xrange(0, len(input_array)): - if rp is None: - cycles[i] = xmc.benchmark(input_array[i]) - else: - cycles[i], raw = rp.measure(xmc, input_array[i], 2.5) - samples[i] = int( 120 * rp.samples_to_time( RedPitaya.samples_high_to_low(raw, 0.2, 2.5) ) ) - if not cycles[i]: Log.fail("Invalid value from benchmark(): cycles[" + str(i) + "] = " + str(cycles[i])) + wca_done = False - if 0 == i % conf.chunksize and 0 < i: - boet = numpy.amin(cycles[0:i]) - wcet = numpy.amax(cycles[0:i]) + xmc.connect() + xmc.set_ic(conf.eic) + for i in xrange(0, len(input_array)): + if rp is None: + cycles[i] = xmc.benchmark(input_array[i]) + else: + cycles[i], raw = rp.measure(xmc, input_array[i], 2.5) + samples[i] = int( 120 * rp.samples_to_time( RedPitaya.samples_high_to_low(raw, 0.2, 2.5) ) ) - while pipe_ana.poll(): - name, res = pipe_ana.recv() - if None != name: - handle_analyzer_result(name, res) - else: wca_done = True + if not cycles[i]: Log.fail("Invalid value from benchmark(): cycles[" + str(i) + "] = " + str(cycles[i])) - update_plots(i) + if 0 == i % conf.chunksize and 0 < i: + boet = numpy.amin(cycles[0:i]) + wcet = numpy.amax(cycles[0:i]) + + while pipe_ana.poll(): + name, res = pipe_ana.recv() + if None != name: + handle_analyzer_result(name, res) + else: wca_done = True - # wait for pending WCET analyzer results - while not wca_done: - name, res = pipe_ana.recv() - if not name is None: - handle_analyzer_result(name, res) - update_plots(len(input_array)) - else: - # (None, None) is received after the last analyzer - break + update_plots(i) + xmc.disconnect() - Log.info("Measurement & Analysis for seed " + str(conf.seed) + " finished") + # wait for pending WCET analyzer results + while not wca_done: + name, res = pipe_ana.recv() + if not name is None: + handle_analyzer_result(name, res) + update_plots(len(input_array)) + else: + # (None, None) is received after the last analyzer + break + + pipe.send( (EVENT_FINISHED, no) ) + Log.info("Measurement & Analysis for seed " + str(conf.seed) + " finished") def event_handle_analyzer(args, dw, configs, no, ana, bound, wcet, factors = {}, errors = {}): @@ -296,7 +305,8 @@ def event_handle_message(args, dw, configs, no, msg): def event_recver(args, pipes, dw, configs): """Receive events from other processes and forward them to the GUI""" - while True: + finished = 0 + while len(pipes) > finished: ready, _, _ = select.select(pipes, [], []) for pipe in ready: @@ -319,9 +329,22 @@ def event_recver(args, pipes, dw, configs): msg = res[2] # TODO: handle msg_rp event_handle_message(args, dw, conf, no, msg) + elif EVENT_FINISHED == event: + finished += 1 + + +def config_manager(args, pipes, dw, configs): + for iteration in configs: + Log.fail("==== Starting new Iteration ====") + iteration = sorted(iteration, key=lambda conf: 1 if conf.haspitaya else 0) - assert False, "Never reached" + for i in xrange(0, len(pipes)): + pipes[i].send( iteration[i] ) + event_recver(args, pipes, dw, iteration) + + for i in xrange(0, len(pipes)): + pipes[i].send(None) def analyzers_process(pipe, args, conf, odir, dat, axf, pml): for name, res in Analyzers.run(args, odir, dat, CPU, axf, "gene_main", pml, None, conf.eic, run_ff = True, run_noff = False): @@ -358,7 +381,7 @@ def get_argparser(): return parser -def find_hardware(args, configs, odir): +def find_hardware(args, odir): xmcs = XMC4500.list_devices(odir, args.xmccache) if None == xmcs: return @@ -376,12 +399,9 @@ def find_hardware(args, configs, odir): if None == rp_xmc: return # TODO: handle theses cases properly! rp.set_default_input(channel) - rp_config = next((c for c in configs if c.haspitaya), None) - assert None != rp_config, "Failed to find configuration for RP" - xmcs.remove(rp_xmc) - return (xmcs, rp, rp_xmc, rp_config) + return (xmcs, rp, rp_xmc) def run(args): odir = "results/aladdin_rtas17_demo" @@ -402,40 +422,35 @@ def run(args): Analyzers.prepare(args) configs = Config.from_xml(args.config) - configs = sorted(configs, key=lambda conf: 1 if conf.haspitaya else 0) # search for hardware - xmcs, rp, rp_xmc, rp_config = find_hardware(args, configs, odir) - - # fork prior to any other threading - bm_count = min(len(xmcs), len(configs)) + xmcs, rp, rp_xmc = find_hardware(args, odir) + proc_count = len(xmcs) + 1 # total number of XMC4500s # setup QT qApp = QtWidgets.QApplication(sys.argv) - dw = DemoWindow(len(configs) + 1, 100) + # #XMC4500s + 1x RP + dw = DemoWindow(proc_count + 1, 100) dw.setWindowTitle(args.title) dw.show() - Log.info("Starting " + str(bm_count + 1) + " worker processes") + Log.info("Starting " + str(proc_count) + " worker processes") pipes = [] - def start_process(args, xmc, rp, conf, no): + def start_process(args, xmc, rp, no): pipe, pipe_child = Pipe() - swr = dw.sws[no + 1] if conf.haspitaya else None - proc = Process(target=measurement_process, args=(args, pipe_child, conf, xmc, rp, no, dw.sws[no], swr)) - pipes.append(pipe) + swr = None if rp is None else dw.sws[no + 1] + proc = Process(target=measurement_process, args=(args, pipe_child, xmc, rp, no, dw.sws[no], swr)) proc.start() + return pipe - i = 0 - for conf in configs: - if not conf.haspitaya: - start_process(args, xmcs[i], None, conf, i) - i += 1 + for i in xrange(0, proc_count - 1): + pipes.append( start_process(args, xmcs[i], None, i) ) # the Red Pitaya/XMC4500 combination is handled manually - start_process(args, rp_xmc, rp, rp_config, len(configs) - 1) + pipes.append( start_process(args, rp_xmc, rp, proc_count - 1) ) - start_new_thread(event_recver, (args, pipes, dw, configs)) + start_new_thread(config_manager, (args, pipes, dw, configs)) sys.exit(qApp.exec_()) diff --git a/Commands/demo_dryrun.py b/Commands/demo_dryrun.py index 01ed769..3360ba0 100644 --- a/Commands/demo_dryrun.py +++ b/Commands/demo_dryrun.py @@ -57,90 +57,91 @@ def run(args): assert len(xmcs) > 0 xmc = xmcs[0] - for conf in configs: - ll = odir + '/bench.ll' - ll_comb = odir + '/combined.ll' - o = odir + '/bench.o' - pml = odir + '/bench.pml' - axf = odir + '/bench.axf' + for iteration in configs: + for conf in iteration: + ll = odir + '/bench.ll' + ll_comb = odir + '/combined.ll' + o = odir + '/bench.o' + pml = odir + '/bench.pml' + axf = odir + '/bench.axf' - dat = Dat.Data() - dat.seed = conf.seed - dat.pattern = Pattern.assemble_string(conf.suite) + dat = Dat.Data() + dat.seed = conf.seed + dat.pattern = Pattern.assemble_string(conf.suite) - bm = GenE.get_benchmark(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, ll, conf.opt, conf.ofactor) - Log.operation_start(" * Compile Benchmark") - Tools.llvm_lls_to_ll([ll], ll_comb) - Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, conf.opt) - Log.operation_end(" * Compile Benchmark") + bm = GenE.get_benchmark(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, ll, conf.opt, conf.ofactor) + Log.operation_start(" * Compile Benchmark") + Tools.llvm_lls_to_ll([ll], ll_comb) + Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, conf.opt) + Log.operation_end(" * Compile Benchmark") - Log.operation_start("Build system for XMC4500") - shutil.copyfile(o, "./xmc4500/gene.o") - Execution.run_wait("make -C ./xmc4500 clean bin/system_gene.axf", None, 0) - shutil.copyfile("./xmc4500/bin/system_gene.axf", axf) - Log.operation_end("Build system for XMC4500") + Log.operation_start("Build system for XMC4500") + shutil.copyfile(o, "./xmc4500/gene.o") + Execution.run_wait("make -C ./xmc4500 clean bin/system_gene.axf", None, 0) + shutil.copyfile("./xmc4500/bin/system_gene.axf", axf) + Log.operation_end("Build system for XMC4500") - Log.info("Flash system to XMC4500s") - success, errors = xmc.flash_retry(odir, "./xmc4500/bin/system_gene.axf") - if not success: - Log.fail("Failed to flash to device, aborting") - for error in errors: - Log.fail(" * " + error) - return + Log.info("Flash system to XMC4500s") + success, errors = xmc.flash_retry(odir, "./xmc4500/bin/system_gene.axf") + if not success: + Log.fail("Failed to flash to device, aborting") + for error in errors: + Log.fail(" * " + error) + return - time.sleep(2) + time.sleep(2) - Log.start("Start 1 Sampling Threads") - input_chunks = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1) + Log.start("Start 1 Sampling Threads") + input_chunks = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1) - # get the total number of samples, which does not - # equal conf.samples due to special input values - sample_count = sum(len(chunk) for chunk in input_chunks) + # get the total number of samples, which does not + # equal conf.samples due to special input values + sample_count = sum(len(chunk) for chunk in input_chunks) - procs = [] - pipe_parent, pipe_child = Pipe() - p = Process(target=benchmark_input_run, args=(pipe_child, input_chunks[0], xmc, conf.eic)) - p.start() - procs.append({'proc':p, 'pipe':pipe_parent, 'input':input_chunks[0]}) + procs = [] + pipe_parent, pipe_child = Pipe() + p = Process(target=benchmark_input_run, args=(pipe_child, input_chunks[0], xmc, conf.eic)) + p.start() + procs.append({'proc':p, 'pipe':pipe_parent, 'input':input_chunks[0]}) - Log.end() + Log.end() - Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) + Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) - for res in Analyzers.run(args, odir, dat, CPU, axf, conf.entry, pml, None, conf.eic, run_noff = False): - pass + for res in Analyzers.run(args, odir, dat, CPU, axf, conf.entry, pml, None, conf.eic, run_noff = False): + pass - dat.register_column('inputs$u', sample_count) - dat.register_column('cycles$u', sample_count) + dat.register_column('inputs$u', sample_count) + dat.register_column('cycles$u', sample_count) - idx = 0 - for proc in procs: - cycles = proc['pipe'].recv() - proc['proc'].join() + idx = 0 + for proc in procs: + cycles = proc['pipe'].recv() + proc['proc'].join() - input_chunk = proc['input'] + input_chunk = proc['input'] - for i in xrange(len(cycles)): - dat.inputs()[idx] = input_chunk[i] - dat.cycles()[idx] = cycles[i] - idx += 1 + for i in xrange(len(cycles)): + dat.inputs()[idx] = input_chunk[i] + dat.cycles()[idx] = cycles[i] + idx += 1 - del input_chunk - del cycles - Log.operation_end("Wait for Sampling") + del input_chunk + del cycles + Log.operation_end("Wait for Sampling") - # get rid of unused arrays - del input_chunks - del procs + # get rid of unused arrays + del input_chunks + del procs - xmax = max(dat.max, dat.wcet['ait'], dat.wcet['platin']) - hist, _ = numpy.histogram(dat.cycles(), bins=500) - ymax = max(hist) + xmax = max(dat.max, dat.wcet['ait'], dat.wcet['platin']) + hist, _ = numpy.histogram(dat.cycles(), bins=conf.bins) + ymax = max(hist) - conf.xmax = xmax - conf.ymax = ymax + conf.xmax = xmax + conf.ymax = ymax Config.to_xml(configs, args.config) -desc = "[ARM] Benchmark GenE using varying values as input to the generated executable" +desc = "GenE Benchmark Generation Demo: Preparation Run" diff --git a/Helper/Config.py b/Helper/Config.py index a660cc0..147f0ca 100644 --- a/Helper/Config.py +++ b/Helper/Config.py @@ -38,8 +38,8 @@ def from_xml(filename): Log.fail("Failed to parse " + filename + ": " + str(e)) return None - root = tree.getroot() - for node in root: + + def node_get_config(node): conf = Config() for attrib in node.attrib: @@ -53,18 +53,36 @@ def from_xml(filename): setattr(conf, attrib, val) - configs.append(conf) + return conf + + root = tree.getroot() + for node in root: + if "iteration" == node.tag: + iteration = [] + for subnode in node: + iteration.append( node_get_config(subnode) ) + configs.append(iteration) + else: + configs.append( node_get_config(subnode) ) return configs def to_xml(configs, filename): + + def append_configs(node, configs): + for config in configs: + node.append( config.to_xml() ) + root = ET.Element("configs") - #tree = ET.ElementTree(root) - for config in configs: - root.append( config.to_xml() ) + if isinstance(configs[0], list): + for iteration in configs: + node = ET.Element("iteration") + append_configs(node, iteration) + root.append(node) + else: + append_configs(root, configs) - #tree.write(filename) xmlstr = minidom.parseString(ET.tostring(root)).toprettyxml(indent="\t") with open(filename, "w") as f: f.write(xmlstr) -- GitLab From ff57449f1a45c7abc38f178bd1d22d606433b260 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 12 Apr 2017 15:04:20 +0200 Subject: [PATCH 86/97] Make analyzers run silently for demo --- Analyzers/__init__.py | 14 +++++++------- Commands/demo.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Analyzers/__init__.py b/Analyzers/__init__.py index 2cec1c9..0d81d9f 100644 --- a/Analyzers/__init__.py +++ b/Analyzers/__init__.py @@ -13,16 +13,16 @@ def get_modules(prefix): os.chdir(oldcwd) return res -def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_noff = True): +def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_noff = True, silent = False): if args.noanalyzer: - Log.info("Timing Analysis disabled by user!") + Log.info("Timing Analysis disabled by user!", silent = silent) return ffs = [] if run_ff: ffs.append(True) if run_noff: ffs.append(False) - Log.info("Run Timing Analysis...") + Log.info("Run Timing Analysis...", silent = silent) modules = get_modules('Analyzers.') for name, mod in modules.iteritems(): if not (target in mod.supported_backends): @@ -32,15 +32,15 @@ def run(args, odir, dat, target, x, func, pml, config, eic, run_ff = True, run_n continue for has_ff in ffs: - Log.info(" * Analyzer " + name + " WITH" + ("" if has_ff else "OUT") + " FFs") + Log.info(" * Analyzer " + name + " WITH" + ("" if has_ff else "OUT") + " FFs", silent = silent) rpt = odir + '/' + name + ("" if has_ff else "_noff") + '.log' - result = mod.run(odir, has_ff, target, x, func, pml, config, rpt, eic) + result = mod.run(odir, has_ff, target, x, func, pml, config, rpt, eic, silent = silent) dat.wcet[name.lower() + ("" if has_ff else "_noff")] = result.cy if result.error: - Log.fail(" -> Result: " + str(result)) + Log.fail(" -> Result: " + str(result), silent = silent) else: - Log.info(" -> Result: " + str(result)) + Log.info(" -> Result: " + str(result), silent = silent) yield (name + ("" if has_ff else "_noff"), result) diff --git a/Commands/demo.py b/Commands/demo.py index 07b160f..edc6d74 100644 --- a/Commands/demo.py +++ b/Commands/demo.py @@ -347,7 +347,7 @@ def config_manager(args, pipes, dw, configs): pipes[i].send(None) def analyzers_process(pipe, args, conf, odir, dat, axf, pml): - for name, res in Analyzers.run(args, odir, dat, CPU, axf, "gene_main", pml, None, conf.eic, run_ff = True, run_noff = False): + for name, res in Analyzers.run(args, odir, dat, CPU, axf, "gene_main", pml, None, conf.eic, run_ff = True, run_noff = False, silent = True): pipe.send( (name, res) ) pipe.send( (None, None) ) -- GitLab From 2eb32bdc3c5e982a4c157ce2e56f59d1872f02b2 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 12 Apr 2017 15:05:30 +0200 Subject: [PATCH 87/97] Handle missing responses from RedPitaya via timeout --- Helper/RedPitaya.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Helper/RedPitaya.py b/Helper/RedPitaya.py index 7122900..8a9abd2 100644 --- a/Helper/RedPitaya.py +++ b/Helper/RedPitaya.py @@ -1,13 +1,14 @@ from External import redpitaya_scpi as scpi from Helper import Tools, Log +import socket class RedPitaya(object): def __init__(self, ip): self.ip = ip self.def_input = 1 self.max_sampling_rate = 125 * 1000 * 1000 # 125 MS/s - self.rp = scpi.scpi(ip) + self.rp = scpi.scpi(ip, timeout=10.0) self.send('ACQ:DATA:UNITS VOLTS') self.led_off() @@ -21,9 +22,11 @@ class RedPitaya(object): def recv(self): assert None != self.rp - res = self.rp.rx_txt() - #Log.debug("read: " + res) - return res + try: + return self.rp.rx_txt() + except socket.timeout: + Log.warn("Timeout in rp.rx_txt()") + return None def send_recv(self, string): self.send(string) @@ -71,6 +74,7 @@ class RedPitaya(object): if inp == None: inp = self.def_input data_string = self.send_recv('ACQ:SOUR' + str(inp) + ':DATA?') + if data_string is None: return None data_string = data_string.strip('{}\n\r').replace(" ", "").split(',') return list(map(float, data_string)) @@ -105,18 +109,20 @@ class RedPitaya(object): channel = self.def_input offset = - (self.buffer_size / 2 - 1) - self.set_trigger('CH' + str(channel) + '_NE', 1000*tlevel_high_low, offset) rep = 0 raw = None while rep < rep_limit: + self.set_trigger('CH' + str(channel) + '_NE', 1000*tlevel_high_low, offset) + self.start_acquire() cy = backend.benchmark(inp) if not self.get_trigger_status(): raw = self.get_data() - self.stop_acquire() - break + if not raw is None: + self.stop_acquire() + break rep += 1 Log.warn("Repeating measurement for input " + str(inp)) -- GitLab From 29cd7feec60495d131054eacf937fe9cc8a54ddc Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 12 Apr 2017 15:05:55 +0200 Subject: [PATCH 88/97] Reduce timeout for JLinkExe to 60s --- Backends/XMC4500.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backends/XMC4500.py b/Backends/XMC4500.py index 8454eb4..2e5bf11 100644 --- a/Backends/XMC4500.py +++ b/Backends/XMC4500.py @@ -62,7 +62,7 @@ def flash(odir, serial, bin_axf): loghandle = open(logfile, "wb") Log.debug("Logfile for " + JLINK + ": " + logfile) try: - proc = pexpect.spawn(JLINK + " " + JLINK_FLAGS + " -SelectEmuBySN " + serial, logfile=loghandle, timeout=120) + proc = pexpect.spawn(JLINK + " " + JLINK_FLAGS + " -SelectEmuBySN " + serial, logfile=loghandle, timeout=60) proc.send(jlink_commands) output = proc.read() -- GitLab From 0f5e0bb994ba1ad34871997f6292409611ad6208 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 12 Apr 2017 15:06:12 +0200 Subject: [PATCH 89/97] Use pexpect instead of custom class Execution in platin.py --- Analyzers/platin.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/Analyzers/platin.py b/Analyzers/platin.py index 03630ac..9f8f75a 100644 --- a/Analyzers/platin.py +++ b/Analyzers/platin.py @@ -1,14 +1,15 @@ #!/usr/bin/python import re +import pexpect -from Helper import Dat, Execution, Tools +from Helper import Dat, Execution, Tools, Log from AnalyzerResult import AnalyzerResult def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silent=False): "Run the platin WCET analyzer on `binary`" - re_wcet = re.compile(r'^\[platin\] INFO: best WCET bound: (-?\d*) cycles$', re.MULTILINE) + re_wcet = re.compile(r'^.*\[platin\] INFO: best WCET bound: (-?\d*) cycles.*$', re.MULTILINE) re_af = re.compile(r'Assertion failed') pml_noff = odir + '/bench_noff.pml' @@ -16,33 +17,31 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silen Tools.pml_strip_user_ff(pml, pml_noff) pml = pml_noff - if None != config: - proc = Execution.run_wait_timeout("platin wcet --report \"" + out_report + "\" -i \"" + config + "\" --analysis-entry " + function + " --disable-ait -i \"" + pml + "\" -b \"" + binary + "\"", None, 30) - else: - proc = Execution.run_wait_timeout("platin wcet --report \"" + out_report + "\" --analysis-entry " + function + " --disable-ait -i \"" + pml + "\" -b \"" + binary + "\"", None, 30) - error = False - # check if platin was killed due to timeout - if None == proc.retcode: + try: + cmdline = "platin wcet --report \"" + out_report + "\" --analysis-entry " + function + " --disable-ait -i \"" + pml + "\" -b \"" + binary + "\"" + if None != config: cmdline += " -i \"" + config + "\"" + + output, retcode = pexpect.run(cmdline, timeout=30, withexitstatus=True) + except pexpect.TIMEOUT: result = Dat.ERROR_WCET_TIMEOUT error = True - - if 0 != proc.retcode: - result = Dat.ERROR_WCET_RETCODE - error = True + else: + if 0 != retcode: + result = Dat.ERROR_WCET_RETCODE + error = True if not error: - if re_af.search(proc.stderr): + if re_af.search(output): result = Dat.ERROR_WCET_ASSERT error = True else: # search for WCET in the output of platin - search = re_wcet.search(proc.stderr) + search = re_wcet.search(output) if search: result = int(search.group(1)) else: - proc.print_details() result = Dat.ERROR_WCET_FIND error = True -- GitLab From 59b4b14006fbacf08de6b0323eb67037a16943a9 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Wed, 12 Apr 2017 15:06:40 +0200 Subject: [PATCH 90/97] Remove logging on ResizeEvent --- Gui/SamplingWidget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Gui/SamplingWidget.py b/Gui/SamplingWidget.py index 810eaa6..bcb6461 100644 --- a/Gui/SamplingWidget.py +++ b/Gui/SamplingWidget.py @@ -51,7 +51,6 @@ class SamplingWidget(QtWidgets.QFrame): label.setText(message) def resizeEvent(self, evt): - Log.warn("Resizeevent to: " + str(evt.size())) self.__set_font(evt.size().height() / 25) self.__set_icon(evt.size().height() / 25) -- GitLab From bf369af4a7687b7ecb919e3340bd79f9a16544c9 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 13 Apr 2017 18:43:15 +0200 Subject: [PATCH 91/97] Properly pass the "silent" parameter to __pml_to_ais --- Analyzers/aiT.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Analyzers/aiT.py b/Analyzers/aiT.py index 7f73397..967d3a0 100644 --- a/Analyzers/aiT.py +++ b/Analyzers/aiT.py @@ -49,16 +49,16 @@ def __parse_rpt(rpt): assert False, "Did not find cycles in aiT output:\n" + content -def __pml_to_ais(pml, x, out_pml_symb, out_ais, silent=False): - Log.operation_start(" -> Extract Symbols for .pml", silent=silent) +def __pml_to_ais(pml, x, out_pml_symb, out_ais, silent = False): + Log.operation_start(" -> Extract Symbols for .pml", silent = silent) Execution.run_wait("platin extract-symbols -i \"" + pml + "\" -o \"" + out_pml_symb + "\" \"" + x + "\"", None, 0) - Log.operation_end(" -> Extract Symbols for .pml", silent=silent) + Log.operation_end(" -> Extract Symbols for .pml", silent = silent) - Log.operation_start(" -> Export .pml to .ais", silent=silent) + Log.operation_start(" -> Export .pml to .ais", silent = silent) Execution.run_wait("platin pml2ais -i \"" + out_pml_symb + "\" --ais \"" + out_ais + "\"", None, 0) - Log.operation_end(" -> Export .pml to .ais", silent=silent) + Log.operation_end(" -> Export .pml to .ais", silent = silent) -def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silent=False): +def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silent = False): assert cpu in supported_backends host = __HOST @@ -75,10 +75,10 @@ def run(odir, has_ff, cpu, binary, function, pml, config, out_report, eic, silen shutil.copyfile(binary, axf_uuid) if not has_ff: - Log.info(" -> Using empty ais file", silent=silent) + Log.info(" -> Using empty ais file", silent = silent) shutil.copyfile("aladdin/data/no_ff.ais", ais_uuid) else: - __pml_to_ais(pml, axf_uuid, pml + ".symb.pml", ais_uuid) + __pml_to_ais(pml, axf_uuid, pml + ".symb.pml", ais_uuid, silent = silent) __prepare_apx(os.path.basename(axf_uuid), apx_uuid, os.path.basename(ais_uuid), rpt_uuid, 'gene_main', 3, eic) -- GitLab From 59a31a0d2e4af758d03300c1847e9e6e15fb82f0 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 13 Apr 2017 18:43:50 +0200 Subject: [PATCH 92/97] Final fixes for the RTAS'17 demo --- Commands/demo.py | 23 +++++++++-------------- Gui/DemoWindow.py | 6 ++++-- Gui/SamplingWidget.py | 28 ++++++++++++++++------------ 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/Commands/demo.py b/Commands/demo.py index edc6d74..596f738 100644 --- a/Commands/demo.py +++ b/Commands/demo.py @@ -51,7 +51,7 @@ def hist_to_file(args, cycles, idx, width, height, analyzers, boet, wcet, conf): fig = matplotlib.figure.Figure(figsize=(float(width) / 100, float(height) / 100), dpi=100) axes = fig.add_subplot(111) - axes.set_xlim((0, conf.xmax)) + axes.set_xlim((0, 1.1*conf.xmax)) axes.set_ylim((0, conf.ymax)) axes.yaxis.grid(True) @@ -195,7 +195,7 @@ def measurement_process(args, pipe, xmc, rp, no, swx, swr): def update_plots(pos): rp_fle = None if not rp is None: - rp_fle = hist_to_file(args, samples, pos, swr.mpWidth.value, swr.mpHeight.value, {}, boet, wcet, conf) + rp_fle = hist_to_file(args, samples, pos, swr.mpWidth.value, swr.mpHeight.value, analyzers, boet, wcet, conf) fle = hist_to_file(args, cycles, pos, swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) pipe.send( (EVENT_PLOT, no, pos, wcet, fle, rp_fle) ) @@ -260,15 +260,8 @@ def event_handle_analyzer(args, dw, configs, no, ana, bound, wcet, factors = {}, if ana in ANALYZERS_TO_SHOW: if bound > 0: swx.update_analyzer.emit(ana, 'checkmark.png', ana + ': ' + str(bound) + 'cy') - else: #TODO this doesn't work - msg = ana + ': Error' - #if None != result.error_msg and "" != result.error_msg: - # msg += result.error_msg - #else: - # msg += Dat.errno_to_string(bound) - swx.update_analyzer.emit(ana, 'error.png', msg) - #else: - # swx.update_analyzer.emit(name, 'working.png', name) + else: + swx.update_analyzer.emit(ana, 'error.png', ana + ': Error') # update summary histograms ana = ana.lower() @@ -294,8 +287,6 @@ def event_handle_plot(args, dw, conf, no, pos, fle_xmc, fle_rp): # update progress bar dw.sws[no].update_progress.emit(0, conf.samples, pos) - if not fle_rp is None: - dw.sws[no + 1].update_progress.emit(0, conf.samples, pos) def event_handle_message(args, dw, configs, no, msg): @@ -327,7 +318,6 @@ def event_recver(args, pipes, dw, configs): event_handle_plot(args, dw, conf, no, pos, fle_xmc, fle_rp) elif EVENT_MESSAGE == event: msg = res[2] - # TODO: handle msg_rp event_handle_message(args, dw, conf, no, msg) elif EVENT_FINISHED == event: finished += 1 @@ -338,6 +328,11 @@ def config_manager(args, pipes, dw, configs): Log.fail("==== Starting new Iteration ====") iteration = sorted(iteration, key=lambda conf: 1 if conf.haspitaya else 0) + # Reset analyzer information + for sw in dw.sws[:-1]: + for ana in ANALYZERS_TO_SHOW: + sw.update_analyzer.emit(ana, 'working.png', ana) + for i in xrange(0, len(pipes)): pipes[i].send( iteration[i] ) diff --git a/Gui/DemoWindow.py b/Gui/DemoWindow.py index d118914..94c29d1 100644 --- a/Gui/DemoWindow.py +++ b/Gui/DemoWindow.py @@ -28,8 +28,10 @@ class DemoWindow(QtWidgets.QMainWindow): layout = -1 for i in xrange(0, sw_count): bg_img = 'bg_cortex.png' - if i == sw_count - 1: bg_img = 'bg_rp.png' - self.sws.append( SamplingWidget(self, bg_img, initial_ylim) ) + if i == sw_count - 1: + bg_img = 'bg_rp.png' + + self.sws.append( SamplingWidget(self, bg_img, initial_ylim, i == sw_count - 1) ) if 0 == i % 5: self.bm_layouts.append( QtWidgets.QVBoxLayout() ) self.main_layout.addLayout(self.bm_layouts[-1]) diff --git a/Gui/SamplingWidget.py b/Gui/SamplingWidget.py index bcb6461..719f72b 100644 --- a/Gui/SamplingWidget.py +++ b/Gui/SamplingWidget.py @@ -62,8 +62,8 @@ class SamplingWidget(QtWidgets.QFrame): newfont = QtGui.QFont("sans-serif", weight=QtGui.QFont.Bold) newfont.setPixelSize(size) - self.message.setFont(newfont) - self.progress.setFont(newfont) + if not self.message is None: self.message.setFont(newfont) + if not self.progress is None: self.progress.setFont(newfont) for _, label, _, _ in self.analyzers.itervalues(): label.setFont(newfont) @@ -71,7 +71,7 @@ class SamplingWidget(QtWidgets.QFrame): for icon, _, img, _ in self.analyzers.itervalues(): icon.setPixmap(QtGui.QPixmap("aladdin/data/img/" + img).scaledToWidth(size)) - def __init__(self, parent, bgimg, initial_ylim): + def __init__(self, parent, bgimg, initial_ylim, only_plot = False): self.bgimg = bgimg QtWidgets.QWidget.__init__(self) self.win = parent @@ -84,6 +84,7 @@ class SamplingWidget(QtWidgets.QFrame): self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self.updateGeometry() + # connect signals & slots self.update_message.connect(self.handle_update_message) self.update_sampling.connect(self.handle_update_sampling) self.update_progress.connect(self.handle_update_progress) @@ -110,14 +111,17 @@ class SamplingWidget(QtWidgets.QFrame): self.main_layout.addLayout(hlayout) - self.message = QtWidgets.QLabel() - self.message.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - vlayout_topleft.addWidget(self.message) + self.message = None + self.progress = None + if not only_plot: + self.message = QtWidgets.QLabel() + self.message.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + vlayout_topleft.addWidget(self.message) - self.progress = QtWidgets.QProgressBar() - self.progress.setFormat("%v/%m samples") + self.progress = QtWidgets.QProgressBar() + self.progress.setFormat("%v/%m samples") - vlayout_topleft.addWidget(self.progress) + vlayout_topleft.addWidget(self.progress) self.sampling = ResizeLabel(self.mpWidth, self.mpHeight) self.sampling.setMargin(0) @@ -127,9 +131,9 @@ class SamplingWidget(QtWidgets.QFrame): self.main_widget.setFocus() self.analyzers = {} - - for name in ['aiT', 'platin']: - self.handle_update_analyzer(name, 'working.png', name) + if not only_plot: + for name in ['aiT', 'platin']: + self.handle_update_analyzer(name, 'working.png', name) QtWidgets.QWidget.setSizePolicy(self, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) QtWidgets.QWidget.updateGeometry(self) -- GitLab From 0b88f1ab33a629a00c1a3ce9db7cf251fa9cc741 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 14 Apr 2017 08:11:25 +0200 Subject: [PATCH 93/97] Properly seed the RNG to obtain reproducible results --- Helper/Benchmark.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Helper/Benchmark.py b/Helper/Benchmark.py index 9af8bf9..2d575e0 100644 --- a/Helper/Benchmark.py +++ b/Helper/Benchmark.py @@ -3,7 +3,10 @@ import numpy import array -def get_input(bits, seed, samples, chunks, crafted = True): +from Helper import Log + +def get_input(bits, seed, samples, chunks, crafted = True, rng_seed = 42): + numpy.random.seed(rng_seed) input_data = [] for i in range(0, chunks): input_data.append( array.array('L') ) # L = unsigned long -- GitLab From 09fa3d24c02217cc1f5cd90c6039f30872c2d39a Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Fri, 14 Apr 2017 10:12:43 +0200 Subject: [PATCH 94/97] Silence demo_dryrun --- Commands/demo_dryrun.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/Commands/demo_dryrun.py b/Commands/demo_dryrun.py index 3360ba0..b99bb12 100644 --- a/Commands/demo_dryrun.py +++ b/Commands/demo_dryrun.py @@ -57,8 +57,17 @@ def run(args): assert len(xmcs) > 0 xmc = xmcs[0] + iteration_number = 0 for iteration in configs: + iteration_number += 1 + Log.info("Iteration " + str(iteration_number) + "/" + str(len(configs))) + + conf_number = 0 for conf in iteration: + conf_number += 1 + Log.info(" * Config " + str(conf_number) + "/" + str(len(iteration))) + + ll = odir + '/bench.ll' ll_comb = odir + '/combined.ll' o = odir + '/bench.o' @@ -69,19 +78,16 @@ def run(args): dat.seed = conf.seed dat.pattern = Pattern.assemble_string(conf.suite) - bm = GenE.get_benchmark(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, ll, conf.opt, conf.ofactor) - Log.operation_start(" * Compile Benchmark") + bm = GenE.execute(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, conf.ofactor, ll, silent = True) + + Tools.llvm_lls_to_ll([ll], ll_comb) Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, conf.opt) - Log.operation_end(" * Compile Benchmark") - Log.operation_start("Build system for XMC4500") shutil.copyfile(o, "./xmc4500/gene.o") Execution.run_wait("make -C ./xmc4500 clean bin/system_gene.axf", None, 0) shutil.copyfile("./xmc4500/bin/system_gene.axf", axf) - Log.operation_end("Build system for XMC4500") - Log.info("Flash system to XMC4500s") success, errors = xmc.flash_retry(odir, "./xmc4500/bin/system_gene.axf") if not success: Log.fail("Failed to flash to device, aborting") @@ -91,7 +97,6 @@ def run(args): time.sleep(2) - Log.start("Start 1 Sampling Threads") input_chunks = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1) # get the total number of samples, which does not @@ -104,13 +109,12 @@ def run(args): p.start() procs.append({'proc':p, 'pipe':pipe_parent, 'input':input_chunks[0]}) - Log.end() - Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) - for res in Analyzers.run(args, odir, dat, CPU, axf, conf.entry, pml, None, conf.eic, run_noff = False): - pass + for name, res in Analyzers.run(args, odir, dat, CPU, axf, conf.entry, pml, None, conf.eic, run_noff = False, silent = True): + # write WCA results to config + setattr(conf, "wca_" + name, res.cy) dat.register_column('inputs$u', sample_count) dat.register_column('cycles$u', sample_count) @@ -129,7 +133,6 @@ def run(args): del input_chunk del cycles - Log.operation_end("Wait for Sampling") # get rid of unused arrays del input_chunks -- GitLab From df57a64f550246350967a71406c3e708a3f2813a Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Sun, 16 Apr 2017 15:35:18 +0200 Subject: [PATCH 95/97] demo_dryrun: Store actual WCET --- Commands/demo_dryrun.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Commands/demo_dryrun.py b/Commands/demo_dryrun.py index b99bb12..fdb09ea 100644 --- a/Commands/demo_dryrun.py +++ b/Commands/demo_dryrun.py @@ -138,6 +138,7 @@ def run(args): del input_chunks del procs + conf.wcet = dat.max xmax = max(dat.max, dat.wcet['ait'], dat.wcet['platin']) hist, _ = numpy.histogram(dat.cycles(), bins=conf.bins) ymax = max(hist) -- GitLab From daf95f08742701487d144efac5ab228fbbe6b77c Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Sun, 16 Apr 2017 15:49:52 +0200 Subject: [PATCH 96/97] Demo: Handle unexpected exceptions --- Commands/demo.py | 249 ++++++++++++++++++++++++----------------------- 1 file changed, 127 insertions(+), 122 deletions(-) diff --git a/Commands/demo.py b/Commands/demo.py index 596f738..314028d 100644 --- a/Commands/demo.py +++ b/Commands/demo.py @@ -125,131 +125,133 @@ def measurement_process(args, pipe, xmc, rp, no, swx, swr): Log.fail("Measurement finished, no further configurations") return - odir = "results/aladdin_rtas17_demo/benchmark" + str(no) + "_seed" + str(conf.seed) - - if not os.path.exists(odir): - os.makedirs(odir) - - ll = odir + '/bench.ll' - ll_comb = odir + '/combined.ll' - o = odir + '/bench.o' - pml = odir + '/bench.pml' - axf = odir + '/bench.axf' - - time.sleep(2) - - # initialize the widgets with empty plots - plot_xmc = hist_to_file(args, None, 0, swx.mpWidth.value, swx.mpHeight.value, {}, None, None, conf) - - plot_rp = None - if not swr is None: - pipe.send( (EVENT_MESSAGE, no, "Waiting...") ) - plot_rp = hist_to_file(args, None, 0, swr.mpWidth.value, swr.mpHeight.value, {}, None, None, conf) - - pipe.send( (EVENT_PLOT, no, 0, None, plot_xmc, plot_rp) ) - - dat = Dat.Data() - dat.seed = conf.seed - dat.pattern = Pattern.assemble_string(conf.suite) - - ## generate the benchmark - pipe.send( (EVENT_MESSAGE, no, "Generating Benchmark...") ) - bm = GenE.execute(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, conf.ofactor, ll, silent=True) - Tools.llvm_lls_to_ll([ll], ll_comb) - Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) - - ## compile the benchmark OS - pipe.send( (EVENT_MESSAGE, no, "Compile Benchmark...") ) - o_xmc = "./xmc4500/gene_seed" + str(conf.seed) + ".o" - shutil.copyfile(o, o_xmc) - Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(conf.seed) + ".axf", None, 0) - shutil.move("./xmc4500/bin/system_gene_seed" + str(conf.seed) + ".axf", axf) - os.remove(o_xmc) - - ## flash to devices - pipe.send( (EVENT_MESSAGE, no, "Flash Benchmark...") ) - success, errors = xmc.flash_retry(odir, axf) - if not success: - Log.fail("Flashing to " + str(xmc) + " failed: " + ", ".join(errors)) - return - else: Log.debug("Successfully flashed to " + str(xmc)) - - Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) - - pipe_ana, pipe_child = Pipe() - proc_ana = Process(target=analyzers_process, args=(pipe_child, args, conf, odir, dat, axf, pml)) - proc_ana.start() - - time.sleep(2) - - pipe.send( (EVENT_MESSAGE, no, "Sampling...") ) - - #### - analyzers = {} - input_array = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1, crafted=conf.crafted)[0] - cycles = numpy.zeros(len(input_array), numpy.uint32) - samples = numpy.zeros(len(input_array), numpy.uint32) - boet = None - wcet = None - - def update_plots(pos): - rp_fle = None - if not rp is None: - rp_fle = hist_to_file(args, samples, pos, swr.mpWidth.value, swr.mpHeight.value, analyzers, boet, wcet, conf) - - fle = hist_to_file(args, cycles, pos, swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) - pipe.send( (EVENT_PLOT, no, pos, wcet, fle, rp_fle) ) - pipe.send( (EVENT_MESSAGE, no, str(pos) + " samples") ) - - - def handle_analyzer_result(name, res): - assert not name is None - assert not wcet is None + try: + odir = "results/aladdin_rtas17_demo/benchmark" + str(no) + "_seed" + str(conf.seed) - pipe.send( (EVENT_ANALYZER, no, name, res.cy, wcet) ) - if res.cy > 0: - analyzers[name] = res.cy + if not os.path.exists(odir): + os.makedirs(odir) + ll = odir + '/bench.ll' + ll_comb = odir + '/combined.ll' + o = odir + '/bench.o' + pml = odir + '/bench.pml' + axf = odir + '/bench.axf' + + time.sleep(2) - wca_done = False - - xmc.connect() - xmc.set_ic(conf.eic) - for i in xrange(0, len(input_array)): - if rp is None: - cycles[i] = xmc.benchmark(input_array[i]) - else: - cycles[i], raw = rp.measure(xmc, input_array[i], 2.5) - samples[i] = int( 120 * rp.samples_to_time( RedPitaya.samples_high_to_low(raw, 0.2, 2.5) ) ) - - if not cycles[i]: Log.fail("Invalid value from benchmark(): cycles[" + str(i) + "] = " + str(cycles[i])) - - if 0 == i % conf.chunksize and 0 < i: - boet = numpy.amin(cycles[0:i]) - wcet = numpy.amax(cycles[0:i]) - - while pipe_ana.poll(): - name, res = pipe_ana.recv() - if None != name: - handle_analyzer_result(name, res) - else: wca_done = True - - update_plots(i) - xmc.disconnect() - - # wait for pending WCET analyzer results - while not wca_done: - name, res = pipe_ana.recv() - if not name is None: - handle_analyzer_result(name, res) - update_plots(len(input_array)) - else: - # (None, None) is received after the last analyzer - break - - pipe.send( (EVENT_FINISHED, no) ) - Log.info("Measurement & Analysis for seed " + str(conf.seed) + " finished") - + # initialize the widgets with empty plots + plot_xmc = hist_to_file(args, None, 0, swx.mpWidth.value, swx.mpHeight.value, {}, None, None, conf) + + plot_rp = None + if not swr is None: + pipe.send( (EVENT_MESSAGE, no, "Waiting...") ) + plot_rp = hist_to_file(args, None, 0, swr.mpWidth.value, swr.mpHeight.value, {}, None, None, conf) + + pipe.send( (EVENT_PLOT, no, 0, None, plot_xmc, plot_rp) ) + + dat = Dat.Data() + dat.seed = conf.seed + dat.pattern = Pattern.assemble_string(conf.suite) + + ## generate the benchmark + pipe.send( (EVENT_MESSAGE, no, "Generating Benchmark...") ) + bm = GenE.execute(Pattern.PATTERN[conf.suite], CPU_TYPE, conf.budget, conf.bits, conf.seed, conf.ofactor, ll, silent=True) + Tools.llvm_lls_to_ll([ll], ll_comb) + Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, opt=0) + + ## compile the benchmark OS + pipe.send( (EVENT_MESSAGE, no, "Compile Benchmark...") ) + o_xmc = "./xmc4500/gene_seed" + str(conf.seed) + ".o" + shutil.copyfile(o, o_xmc) + Execution.run_wait("make -C ./xmc4500 bin/system_gene_seed" + str(conf.seed) + ".axf", None, 0) + shutil.move("./xmc4500/bin/system_gene_seed" + str(conf.seed) + ".axf", axf) + os.remove(o_xmc) + + ## flash to devices + pipe.send( (EVENT_MESSAGE, no, "Flash Benchmark...") ) + success, errors = xmc.flash_retry(odir, axf) + if not success: + Log.fail("Flashing to " + str(xmc) + " failed: " + ", ".join(errors)) + return + else: Log.debug("Successfully flashed to " + str(xmc)) + + Execution.run_wait("sed -i 's/thumb--none-eabi/armv7m--none-eabi/g' \"" + pml + "\"", None, 0) + + pipe_ana, pipe_child = Pipe() + proc_ana = Process(target=analyzers_process, args=(pipe_child, args, conf, odir, dat, axf, pml)) + proc_ana.start() + + time.sleep(2) + + pipe.send( (EVENT_MESSAGE, no, "Sampling...") ) + + #### + analyzers = {} + input_array = Benchmark.get_input(conf.bits, conf.seed, conf.samples, 1, crafted=conf.crafted)[0] + cycles = numpy.zeros(len(input_array), numpy.uint32) + samples = numpy.zeros(len(input_array), numpy.uint32) + boet = None + wcet = None + + def update_plots(pos): + rp_fle = None + if not rp is None: + rp_fle = hist_to_file(args, samples, pos, swr.mpWidth.value, swr.mpHeight.value, analyzers, boet, wcet, conf) + + fle = hist_to_file(args, cycles, pos, swx.mpWidth.value, swx.mpHeight.value, analyzers, boet, wcet, conf) + pipe.send( (EVENT_PLOT, no, pos, wcet, fle, rp_fle) ) + pipe.send( (EVENT_MESSAGE, no, str(pos) + " samples") ) + + + def handle_analyzer_result(name, res): + assert not name is None + assert not wcet is None + + pipe.send( (EVENT_ANALYZER, no, name, res.cy, wcet) ) + if res.cy > 0: + analyzers[name] = res.cy + + + wca_done = False + + xmc.connect() + xmc.set_ic(conf.eic) + for i in xrange(0, len(input_array)): + if rp is None: + cycles[i] = xmc.benchmark(input_array[i]) + else: + cycles[i], raw = rp.measure(xmc, input_array[i], 2.5) + samples[i] = int( 120 * rp.samples_to_time( RedPitaya.samples_high_to_low(raw, 0.2, 2.5) ) ) + + if not cycles[i]: Log.fail("Invalid value from benchmark(): cycles[" + str(i) + "] = " + str(cycles[i])) + + if 0 == i % conf.chunksize and 0 < i: + boet = numpy.amin(cycles[0:i]) + wcet = numpy.amax(cycles[0:i]) + + while pipe_ana.poll(): + name, res = pipe_ana.recv() + if None != name: + handle_analyzer_result(name, res) + else: wca_done = True + + update_plots(i) + xmc.disconnect() + + # wait for pending WCET analyzer results + while not wca_done: + name, res = pipe_ana.recv() + if not name is None: + handle_analyzer_result(name, res) + update_plots(len(input_array)) + else: + # (None, None) is received after the last analyzer + break + + pipe.send( (EVENT_FINISHED, no) ) + Log.info("Measurement & Analysis for seed " + str(conf.seed) + " finished") + except Exception as e: + Log.fail("Unhandled exception: " + str(e)) def event_handle_analyzer(args, dw, configs, no, ana, bound, wcet, factors = {}, errors = {}): assert not wcet is None @@ -338,6 +340,9 @@ def config_manager(args, pipes, dw, configs): event_recver(args, pipes, dw, iteration) + # wait 10s for demo purposes + time.sleep(10) + for i in xrange(0, len(pipes)): pipes[i].send(None) -- GitLab From d0a96b0675156c60739b1626e9b079d112165c05 Mon Sep 17 00:00:00 2001 From: Christian Eichler Date: Thu, 20 Apr 2017 17:11:08 +0200 Subject: [PATCH 97/97] Improve error handling in case of GenE error --- Commands/bench_input_arm.py | 4 +++- Helper/GenE.py | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Commands/bench_input_arm.py b/Commands/bench_input_arm.py index 54297c5..02c0188 100644 --- a/Commands/bench_input_arm.py +++ b/Commands/bench_input_arm.py @@ -81,12 +81,14 @@ def run(args): dat.pattern = Pattern.assemble_string(args.suite) bm = GenE.get_benchmark(Pattern.PATTERN[args.suite], CPU_TYPE, args.cost, args.bits, args.seed, ll, args.opt, args.ofactor) + if bm is None: + return + Log.operation_start(" * Compile Benchmark") Tools.llvm_lls_to_ll([ll], ll_comb) Tools.llvm_ll_to_o( Tools.get_target(CPU) , None, ll_comb, o, pml, args.opt) Log.operation_end(" * Compile Benchmark") - bm.print_patterninfo() bm.print_loopinfo() diff --git a/Helper/GenE.py b/Helper/GenE.py index 2d5fedb..e44dc5d 100644 --- a/Helper/GenE.py +++ b/Helper/GenE.py @@ -5,7 +5,7 @@ import re import os import pexpect -import Log, Tools, Git, Metrics +import Log, Git, Metrics class Benchmark(object): def __init__(self, ll, pattern, cost, bits, seed): @@ -62,9 +62,12 @@ class Benchmark(object): def execute(pattern, cpu, cost, bits, seed, of, out_ll, simulate=True, silent=False): pattern_str = " ".join( map('--pattern {0}'.format, pattern) ) - proc = pexpect.spawn("patmos-gene -mcpu=" + cpu + " " + pattern_str + " -gene-cost=" + str(cost) + " -gene-input-bits=" + str(bits) + " -gene-seed=" + str(seed) + " -ll=\"" + out_ll + "\" -gene-overweight-factor=" + str(of) + (" -gene-execute" if simulate else "")) + cmdline = "patmos-gene -mcpu=" + cpu + " " + pattern_str + " -gene-cost=" + str(cost) + " -gene-input-bits=" + str(bits) + " -gene-seed=" + str(seed) + " -ll=\"" + out_ll + "\" -gene-overweight-factor=" + str(of) + (" -gene-execute" if simulate else "") + Log.debug("Executing GenE: " + cmdline) + proc = pexpect.spawn(cmdline) bm = Benchmark(out_ll, pattern, cost, bits, seed) + output = "" if not silent: Log.operation_start(" * Generate Benchmark") @@ -74,6 +77,8 @@ def execute(pattern, cpu, cost, bits, seed, of, out_ll, simulate=True, silent=Fa line = proc.readline().strip("\r\n") if '' == line: break # EOF + output += line + "\n" + line = execute.prog_esc.sub('', line) search = execute.prog_gen.search(line) @@ -108,6 +113,15 @@ def execute(pattern, cpu, cost, bits, seed, of, out_ll, simulate=True, silent=Fa assert not proc.isalive() + if None != proc.signalstatus: + Log.fail("GenE failed with signalstatus " + str(proc.signalstatus) + " for seed " + str(seed)) + Log.fail("Output: " + output) + return None + elif 0 != proc.exitstatus: + Log.fail("GenE failed with exitstatus " + str(proc.exitstatus) + " for seed " + str(seed)) + Log.fail("Output: " + output) + return None + return bm execute.prog_esc = re.compile(r'\x1b[^m]*m') -- GitLab