diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rwxr-xr-x | bin/analyze-archive.py | 9 | ||||
-rwxr-xr-x | bin/generate-dfa-benchmark.py | 7 | ||||
-rw-r--r-- | lib/harness.py | 38 | ||||
-rw-r--r-- | lib/loader.py | 312 | ||||
-rw-r--r-- | lib/runner.py | 36 |
6 files changed, 385 insertions, 18 deletions
@@ -2,3 +2,4 @@ *.pyc /htmlcov/ /.coverage* +.idea/ diff --git a/bin/analyze-archive.py b/bin/analyze-archive.py index 5c7c97e..10fe304 100755 --- a/bin/analyze-archive.py +++ b/bin/analyze-archive.py @@ -104,6 +104,9 @@ Options: --export-energymodel=<model.json> Export energy model. Works out of the box for v1 and v2 logfiles. Requires --hwmodel for v0 logfiles. + +--no-cache + Do not load cached measurement results """ import getopt @@ -304,7 +307,7 @@ if __name__ == "__main__": try: optspec = ( - "info " + "info no-cache " "plot-unparam= plot-param= plot-traces= show-models= show-quality= " "ignored-trace-indexes= discard-outliers= function-override= " "export-traces= " @@ -369,7 +372,9 @@ if __name__ == "__main__": sys.exit(2) raw_data = RawData( - args, with_traces=("export-traces" in opt or "plot-traces" in opt) + args, + with_traces=("export-traces" in opt or "plot-traces" in opt), + skip_cache=("no-cache" in opt), ) if "info" in opt: diff --git a/bin/generate-dfa-benchmark.py b/bin/generate-dfa-benchmark.py index 1410c28..6540702 100755 --- a/bin/generate-dfa-benchmark.py +++ b/bin/generate-dfa-benchmark.py @@ -393,7 +393,7 @@ def run_benchmark( os.remove(filename) harness.undo(i) else: - files.extend(monitor.get_files()) + files.append(monitor.get_files()) i += 1 harness.restart() @@ -636,8 +636,10 @@ if __name__ == "__main__": elif "energytrace" in opt: # Use barcode sync by default gpio_mode = "bar" + energytrace_sync = None if "sync" in opt["energytrace"] and opt["energytrace"]["sync"] != "bar": gpio_mode = "around" + energytrace_sync = "led" harness = OnboardTimerHarness( gpio_pin=timer_pin, gpio_mode=gpio_mode, @@ -645,6 +647,7 @@ if __name__ == "__main__": counter_limits=runner.get_counter_limits_us(opt["arch"]), log_return_values=need_return_values, repeat=1, + energytrace_sync=energytrace_sync, ) elif "timing" in opt: harness = OnboardTimerHarness( @@ -677,7 +680,7 @@ if __name__ == "__main__": "files": list(map(lambda x: x[3], results)), "configs": list(map(lambda x: x[2].get_config(), results)), } - extra_files = flatten(json_out["files"]) + extra_files = flatten(map(flatten, json_out["files"])) if "instance" in pta.codegen: output_prefix = ( opt["data"] + time.strftime("/%Y%m%d-%H%M%S-") + pta.codegen["instance"] diff --git a/lib/harness.py b/lib/harness.py index c251ec8..833807e 100644 --- a/lib/harness.py +++ b/lib/harness.py @@ -33,6 +33,7 @@ class TransitionHarness: log_return_values=False, repeat=0, post_transition_delay_us=0, + energytrace_sync=None, ): """ Create a new TransitionHarness @@ -53,6 +54,7 @@ class TransitionHarness: self.log_return_values = log_return_values self.repeat = repeat self.post_transition_delay_us = post_transition_delay_us + self.energytrace_sync = energytrace_sync self.reset() def copy(self): @@ -63,6 +65,7 @@ class TransitionHarness: log_return_values=self.log_return_values, repeat=self.repeat, post_transition_delay_us=self.post_transition_delay_us, + energytrace_sync=self.energytrace_sync, ) new_object.traces = self.traces.copy() new_object.trace_id = self.trace_id @@ -373,6 +376,7 @@ class OnboardTimerHarness(TransitionHarness): pta=self.pta, log_return_values=self.log_return_values, repeat=self.repeat, + energytrace_sync=self.energytrace_sync, ) new_harness.traces = self.traces.copy() new_harness.trace_id = self.trace_id @@ -400,13 +404,45 @@ class OnboardTimerHarness(TransitionHarness): def global_code(self): ret = "#define PTALOG_TIMING\n" ret += super().global_code() + if self.energytrace_sync == "led": + #TODO Make nicer + ret += """\nvoid runLASync(){ + // ======================= LED SYNC ================================ + ptalog.passTransition(0); + ptalog.startTransition(); + gpio.led_toggle(0); + gpio.led_toggle(1); + ptalog.stopTransition(); + + for (unsigned char i = 0; i < 4; i++) { + arch.sleep_ms(250); + } + + ptalog.passTransition(0); + ptalog.startTransition(); + gpio.led_toggle(0); + gpio.led_toggle(1); + ptalog.stopTransition(); + // ======================= LED SYNC ================================ + arch.sleep_ms(250); +}\n\n""" return ret def start_benchmark(self, benchmark_id=0): - ret = "ptalog.passNop();\n" + ret = "" + if self.energytrace_sync == "led": + ret += "runLASync();\n" + ret += "ptalog.passNop();\n" ret += super().start_benchmark(benchmark_id) return ret + def stop_benchmark(self): + ret = "" + if self.energytrace_sync == "led": + ret += "runLASync();\n" + ret += super().stop_benchmark() + return ret + def pass_transition( self, transition_id, transition_code, transition: object = None ): diff --git a/lib/loader.py b/lib/loader.py index c35eb4c..531559f 100644 --- a/lib/loader.py +++ b/lib/loader.py @@ -107,7 +107,14 @@ def _preprocess_mimosa(measurement): def _preprocess_etlog(measurement): setup = measurement["setup"] - etlog = EnergyTraceLog( + + energytrace_class = EnergyTraceWithBarcode + if measurement["sync_mode"] == "la": + energytrace_class = EnergyTraceWithLogicAnalyzer + elif measurement["sync_mode"] == "timer": + energytrace_class = EnergyTraceWithTimer + + etlog = energytrace_class( float(setup["voltage"]), int(setup["state_duration"]), measurement["transition_names"], @@ -242,7 +249,7 @@ class RawData: file system, making subsequent loads near-instant. """ - def __init__(self, filenames, with_traces=False): + def __init__(self, filenames, with_traces=False, skip_cache=False): """ Create a new RawData object. @@ -321,7 +328,7 @@ class RawData: self.pta = self.ptalog["pta"] self.set_cache_file() - if not with_traces: + if not with_traces and not skip_cache: self.load_cache() def set_cache_file(self): @@ -406,7 +413,7 @@ class RawData: processed_data["error"] = "; ".join(processed_data["datasource_errors"]) return False - # Note that the low-level parser (EnergyTraceLog) already checks + # Note that the low-level parser (EnergyTraceWithBarcode) already checks # whether the transition count is correct return True @@ -909,6 +916,10 @@ class RawData: new_filenames = list() with tarfile.open(filename) as tf: ptalog = self.ptalog + if "sync" in ptalog["opt"]["energytrace"]: + sync_mode = ptalog["opt"]["energytrace"]["sync"] + else: + sync_mode = "bar" # Benchmark code may be too large to be executed in a single # run, so benchmarks (a benchmark is basically a list of DFA runs) @@ -963,13 +974,16 @@ class RawData: "state_duration": ptalog["opt"]["sleep"], } ) - for repeat_id, etlog_file in enumerate(ptalog["files"][j]): - member = tf.getmember(etlog_file) + for repeat_id, etlog_files in enumerate(ptalog["files"][j]): + members = list(map(tf.getmember, etlog_files)) offline_data.append( { - "content": tf.extractfile(member).read(), + "content": list( + map(lambda f: tf.extractfile(f).read(), members) + ), + "sync_mode": sync_mode, "fileno": j, - "info": member, + "info": members[0], "setup": self.setup_by_fileno[j], "repeat_id": repeat_id, "expected_trace": ptalog["traces"][j], @@ -1161,7 +1175,7 @@ def pta_trace_to_aggregate(traces, ignore_trace_indexes=[]): return by_name, parameter_names, arg_count -class EnergyTraceLog: +class EnergyTraceWithBarcode: """ EnergyTrace log loader for DFA traces. @@ -1184,7 +1198,7 @@ class EnergyTraceLog: with_traces=False, ): """ - Create a new EnergyTraceLog object. + Create a new EnergyTraceWithBarcode object. :param voltage: supply voltage [V], usually 3.3 V :param state_duration: state duration [ms] @@ -1226,7 +1240,7 @@ class EnergyTraceLog: ) return list() - lines = log_data.decode("ascii").split("\n") + lines = log_data[0].decode("ascii").split("\n") data_count = sum(map(lambda x: len(x) > 0 and x[0] != "#", lines)) data_lines = filter(lambda x: len(x) > 0 and x[0] != "#", lines) @@ -1599,6 +1613,282 @@ class EnergyTraceLog: return None, None, None, None +class EnergyTraceWithLogicAnalyzer: + def __init__( + self, + voltage: float, + state_duration: int, + transition_names: list, + with_traces=False, + ): + + """ + Create a new EnergyTraceWithLogicAnalyzer object. + + :param voltage: supply voltage [V], usually 3.3 V + :param state_duration: state duration [ms] + :param transition_names: list of transition names in PTA transition order. + Needed to map barcode synchronization numbers to transitions. + """ + self.voltage = voltage + self.state_duration = state_duration * 1e-3 + self.transition_names = transition_names + self.with_traces = with_traces + self.errors = list() + + def load_data(self, log_data): + from data.timing.SigrokInterface import SigrokResult + from data.energy.EnergyInterface import EnergyInterface + + # Daten laden + self.sync_data = SigrokResult.fromString(log_data[0]) + self.energy_data = EnergyInterface.getDataFromString(str(log_data[1])) + + pass + + def analyze_states(self, traces, offline_index: int): + u""" + Split log data into states and transitions and return duration, energy, and mean power for each element. + + :param traces: expected traces, needed to synchronize with the measurement. + traces is a list of runs, traces[*]['trace'] is a single run + (i.e. a list of states and transitions, starting with a transition + and ending with a state). + :param offline_index: This function uses traces[*]['trace'][*]['online_aggregates']['duration'][offline_index] to find sync codes + + :param charges: raw charges (each element describes the charge in pJ transferred during 10 µs) + :param trigidx: "charges" indexes corresponding to a trigger edge, see `trigger_edges` + :param ua_func: charge(pJ) -> current(µA) function as returned by `calibration_function` + + :returns: returns list of states and transitions, starting with a transition and ending with astate + Each element is a dict containing: + * `isa`: 'state' or 'transition' + * `W_mean`: Mittelwert der Leistungsaufnahme + * `W_std`: Standardabweichung der Leistungsaufnahme + * `s`: Dauer + if isa == 'transition, it also contains: + * `W_mean_delta_prev`: Differenz zwischen W_mean und W_mean des vorherigen Zustands + * `W_mean_delta_next`: Differenz zwischen W_mean und W_mean des Folgezustands + """ + + # Tatsächlich Daten auswerten + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # remove Dirty Data from previously running program (happens if logic Analyzer Measurement starts earlier than + # the HW Reset from energytrace) + use_data_after_index = 0 + for x in range(1, len(self.sync_data.timestamps)): + if self.sync_data.timestamps[x] - self.sync_data.timestamps[x - 1] > 1.3: + use_data_after_index = x + break + + time_stamp_data = self.sync_data.timestamps[use_data_after_index:] + + start_offset = None + start_timestamp = None + end_offset = None + end_timestamp = None + + last_values = [] # maybe needed to flatten the data + plot_data_x = [] + plot_data_y = [] + last_data = [0, 0, 0, 0] + + power_sync_watt = 0.0162 + + # MAIN ENERGY DATA ITERATION + for energytrace_dataset in self.energy_data: + usedtime = energytrace_dataset[0] - last_data[0] + usedenergy = energytrace_dataset[3] - last_data[3] + power = usedenergy / usedtime * 10 ** -3 # in watts + + if power > 0: + if power > power_sync_watt and start_offset is None: + # automatic START offset calculation + start_offset = energytrace_dataset[0] / 1_000_000 - time_stamp_data[0] + start_timestamp = energytrace_dataset[0] / 1_000_000 + if power > power_sync_watt and start_offset is not None: # TODO BETTER + # automatic END offset calculation (Drift) + end_offset = energytrace_dataset[0] / 1_000_000 - time_stamp_data[-1] - start_offset + end_timestamp = energytrace_dataset[0] / 1_000_000 + if len(last_values) > 10: + last_values.pop(0) + last_values.append(power) + # print("%s \tUsed Energy: %i \tUsed Time: %i \tPower: %f \t l_10: %f" % (now_data[0], usedenergy, usedtime, power, sum(last_values) / len(last_values))) + plot_data_x.append(energytrace_dataset[0] / 1_000_000) + plot_data_y.append(power) + + last_data = energytrace_dataset + + # -------------------- + # add start offset to the data + modified_timestamps_with_offset = [] + for x in time_stamp_data: + if x + start_offset >= 0: + modified_timestamps_with_offset.append(x + start_offset) + + # -------------------- + # Add drift to datapoints + modified_timestamps_with_drift = [] + endFactor = (end_timestamp + end_offset - start_timestamp) / (end_timestamp - start_timestamp) + for x in modified_timestamps_with_offset: + modified_timestamps_with_drift.append(((x - start_timestamp) * endFactor) + start_timestamp) + + self.start_offset = 0 + def getPowerBetween(start, end, base_power=0.001469): + first_index = 0 + all_power = 0 + all_power_count = 0 + for ind in range(self.start_offset, len(plot_data_x)): + first_index = ind + if plot_data_x[ind] > start: + break + + for ind in range(first_index, len(plot_data_x)): + all_power += plot_data_y[ind] + all_power_count += 1 + if plot_data_x[ind] > end: + self.start_offset = ind - 1 + break + return (all_power / all_power_count) - base_power + + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + energy_trace = list() + + expected_transitions = list() + for trace_number, trace in enumerate(traces): + for state_or_transition_number, state_or_transition in enumerate( + trace["trace"] + ): + if state_or_transition["isa"] == "transition": + try: + expected_transitions.append( + ( + state_or_transition["name"], + state_or_transition["online_aggregates"]["duration"][ + offline_index + ] + * 1e-6, + ) + ) + except IndexError: + self.errors.append( + 'Entry #{} ("{}") in trace #{} has no duration entry for offline_index/repeat_id {}'.format( + state_or_transition_number, + state_or_transition["name"], + trace_number, + offline_index, + ) + ) + return energy_trace + + + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + end_transition_ts = None + timestamps_sync_start = 0 + energy_trace_new = list() + + for ts_index in range(0 + timestamps_sync_start, int(len(modified_timestamps_with_drift) / 2)): + start_transition_ts = modified_timestamps_with_drift[ts_index * 2] + if end_transition_ts is not None: + power = getPowerBetween(end_transition_ts, start_transition_ts) + #print("STATE", end_transition_ts * 10 ** 6, start_transition_ts * 10 ** 6, (start_transition_ts - end_transition_ts) * 10 ** 6, power) + if (start_transition_ts - end_transition_ts) * 10 ** 6 > 900_000 and power > power_sync_watt * 0.9 and ts_index > 10: + #remove last transition and stop (upcoming data only sync) + del energy_trace_new[-1] + break + pass + + state = { + "isa": "state", + "W_mean": power, + "W_std": 0.0001, + "s": (start_transition_ts - end_transition_ts), #* 10 ** 6, + } + energy_trace_new.append(state) + + energy_trace_new[-2]["W_mean_delta_next"] = ( + energy_trace_new[-2]["W_mean"] - energy_trace_new[-1]["W_mean"] + ) + + # get energy end_transition_ts + end_transition_ts = modified_timestamps_with_drift[ts_index * 2 + 1] + power = getPowerBetween(start_transition_ts, end_transition_ts) + #print("TRANS", start_transition_ts * 10 ** 6, end_transition_ts * 10 ** 6, (end_transition_ts - start_transition_ts) * 10 ** 6, power) + transition = { + "isa": "transition", + "W_mean": power, + "W_std": 0.0001, + "s": (end_transition_ts - start_transition_ts), #* 10 ** 6, + } + + if (end_transition_ts - start_transition_ts) * 10 ** 6 > 2_000_000: + # TODO Last data set corrupted? HOT FIX!!!!!!!!!!!! REMOVE LATER + #for x in range(4): + # del energy_trace_new[-1] + #break + pass + + energy_trace_new.append(transition) + # print(start_transition_ts, "-", end_transition_ts, "-", end_transition_ts - start_transition_ts) + + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + # +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + energy_trace_new = energy_trace_new[4:] + + + + for number, item in enumerate(expected_transitions): + name, duration = item + transition = { + "isa": "transition", + "W_mean": max(np.random.normal(0.023, 0.002), 0), + "W_std": 0.0001, + "s": duration, + "name": name, + } + + energy_trace.append(transition) + + if len(energy_trace) > 1: + energy_trace[-1]["W_mean_delta_prev"] = ( + energy_trace[-1]["W_mean"] - energy_trace[-2]["W_mean"] + ) + + state = { + "isa": "state", + "W_mean": max(np.random.normal(0.023, 0.002), 0), + "W_std": 0.0001, + "s": self.state_duration, + "name": "state", + } + + energy_trace.append(state) + + energy_trace[-2]["W_mean_delta_next"] = ( + energy_trace[-2]["W_mean"] - energy_trace[-1]["W_mean"] + ) + + st = "" + for i, x in enumerate(energy_trace_new[-10:]): + st += "(%s|%s|%s)" % (energy_trace[i-10]["name"],x['W_mean'],x['s']) + + #print(st) + print(len(self.sync_data.timestamps), " - ", len(energy_trace_new), " - ", len(energy_trace), " - ", ",".join([str(x["s"]) for x in energy_trace_new[-6:]]), " - ", ",".join([str(x["s"]) for x in energy_trace[-6:]])) + if len(energy_trace_new) < len(energy_trace): + return None + return energy_trace_new + + +class EnergyTraceWithTimer(EnergyTraceWithLogicAnalyzer): + pass + + class MIMOSA: """ MIMOSA log loader for DFA traces with auto-calibration. diff --git a/lib/runner.py b/lib/runner.py index aeb8600..3092d5a 100644 --- a/lib/runner.py +++ b/lib/runner.py @@ -9,7 +9,7 @@ Functions: get_monitor -- return Monitor class suitable for the selected multipass arch get_counter_limits -- return arch-specific multipass counter limits (max value, max overflow) """ - +import json import os import re import serial @@ -17,6 +17,7 @@ import serial.threaded import subprocess import sys import time +from data.timing.SigrokCLIInterface import SigrokCLIInterface class SerialReader(serial.threaded.Protocol): @@ -156,6 +157,7 @@ class EnergyTraceMonitor(SerialMonitor): self._start_energytrace() def _start_energytrace(self): + print("EnergyTrace Start") cmd = ["msp430-etv", "--save", self._output, "0"] self._logger = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True @@ -163,6 +165,7 @@ class EnergyTraceMonitor(SerialMonitor): # Benchmark fertig -> externe Hilfsprogramme beenden def close(self): + print("EnergyTrace Close") super().close() self._logger.send_signal(subprocess.signal.SIGINT) stdout, stderr = self._logger.communicate(timeout=15) @@ -170,9 +173,12 @@ class EnergyTraceMonitor(SerialMonitor): # Zusätzliche Dateien, die mit dem Benchmark-Log und -Plan abgespeichert werden sollen # (hier: Die von msp430-etv generierten Logfiles) def get_files(self) -> list: + print("EnergyTrace Get Files") return [self._output] - # + # Benchmark-Konfiguration. Hier: Die (konstante) Spannung. + # MSP430FR5969: 3,6V (wird aktuell nicht unterstützt) + # MSP430FR5994: 3,3V (default) def get_config(self) -> dict: return { "voltage": self._voltage, @@ -185,6 +191,32 @@ class EnergyTraceLogicAnalyzerMonitor(EnergyTraceMonitor): def __init__(self, port: str, baud: int, callback=None, voltage=3.3): super().__init__(port=port, baud=baud, callback=callback, voltage=voltage) + #TODO Max length + options = {'length': 90, 'fake': False, 'sample_rate': 1_000_000} + self.log_file = 'logic_output_log_%s.json' % (time.strftime("%Y%m%d-%H%M%S")) + + # Initialization of Interfaces + self.sig = SigrokCLIInterface(sample_rate=options['sample_rate'], + sample_count=options['length'] * options['sample_rate'], fake=options['fake']) + + # Start Measurements + self.sig.runMeasureAsynchronous() + + def close(self): + super().close() + # Read measured data + #self.sig.waitForAsynchronousMeasure() + self.sig.forceStopMeasure() + time.sleep(0.2) + sync_data = self.sig.getData() + with open(self.log_file, 'w') as fp: + json.dump(sync_data.getDict(), fp) + + def get_files(self) -> list: + files = [self.log_file] + files.extend(super().get_files()) + return files + class MIMOSAMonitor(SerialMonitor): """MIMOSAMonitor captures serial output and MIMOSA energy data for a specific amount of time.""" |