diff options
-rwxr-xr-x | bin/generate-dfa-benchmark.py | 92 | ||||
-rwxr-xr-x | lib/automata.py | 86 | ||||
-rwxr-xr-x | lib/dfatool.py | 47 | ||||
-rw-r--r-- | lib/harness.py | 87 | ||||
-rw-r--r-- | lib/runner.py | 2 |
5 files changed, 233 insertions, 81 deletions
diff --git a/bin/generate-dfa-benchmark.py b/bin/generate-dfa-benchmark.py index 30f2d20..9c783b7 100755 --- a/bin/generate-dfa-benchmark.py +++ b/bin/generate-dfa-benchmark.py @@ -28,6 +28,13 @@ Options: --instance=<name> Override the name of the class instance used for benchmarking +--mimosa=[k=v,k=v,...] + Perform energy measurements with MIMOSA. Takes precedence over --timing. + mimosa options are key-value pairs. Possible settings with defaults: + offset = 130 (mysterious 0V offset) + shunt = 330 (measurement shunt in ohms) + voltage = 3.3 (VCC provided to DUT) + --sleep=<ms> (default: 0) How long to sleep between function calls. @@ -35,6 +42,10 @@ Options: Decrease amount of parameter values used in state space exploration (only use minimum and maximum for numeric values) +--timing + Perform timing measurements using on-chip counters (no external hardware + required) + --trace-filter=<transition,transition,transition,...>[ <transition,transition,transition,...> ...] Only consider traces whose beginning matches one of the provided transition sequences. E.g. --trace-filter='init,foo init,bar' will only consider traces with init as first and foo or bar as second transition, @@ -147,7 +158,10 @@ def benchmark_from_runs(pta: PTA, runs: list, harness: OnboardTimerHarness, benc return outbuf def run_benchmark(application_file: str, pta: PTA, runs: list, arch: str, app: str, run_args: list, harness: object, sleep: int = 0, repeat: int = 0, run_offset: int = 0, runs_total: int = 0, dummy = False): - outbuf = benchmark_from_runs(pta, runs, harness, dummy = dummy, repeat = repeat) + if 'mimosa' in opt: + outbuf = benchmark_from_runs(pta, runs, harness, dummy = dummy, repeat = 1) + else: + outbuf = benchmark_from_runs(pta, runs, harness, dummy = dummy, repeat = repeat) with open(application_file, 'w') as f: f.write(outbuf.getvalue()) print('[MAKE] building benchmark with {:d} runs'.format(len(runs))) @@ -180,25 +194,46 @@ def run_benchmark(application_file: str, pta: PTA, runs: list, arch: str, app: s results.extend(run_benchmark(application_file, pta, runs[mid:], arch, app, run_args, harness.copy(), sleep, repeat, run_offset = run_offset + mid, runs_total = runs_total, dummy = dummy)) return results - runner.flash(arch, app, run_args) - monitor = runner.get_monitor(arch, callback = harness.parser_cb, mimosa = {'shunt': 82}) - if arch == 'posix': - print('[RUN] Will run benchmark for {:.0f} seconds'.format(run_timeout)) - lines = monitor.run(int(run_timeout)) - return [(runs, harness, lines)] + if 'mimosa' in opt: + files = list() + for i in range(opt['repeat']): + runner.flash(arch, app, run_args) + monitor = runner.get_monitor(arch, callback = harness.parser_cb, mimosa = opt['mimosa']) + + try: + slept = 0 + while not harness.done: + time.sleep(5) + slept += 5 + print('[RUN] {:d}/{:d} ({:.0f}%), current benchmark at {:.0f}%'.format(run_offset, runs_total, run_offset * 100 / runs_total, slept * 100 / run_timeout)) + except KeyboardInterrupt: + pass + monitor.close() + files.extend(monitor.get_files()) + harness.restart() + + return [(runs, harness, monitor, files)] + else: + runner.flash(arch, app, run_args) + monitor = runner.get_monitor(arch, callback = harness.parser_cb) + + if arch == 'posix': + print('[RUN] Will run benchmark for {:.0f} seconds'.format(run_timeout)) + lines = monitor.run(int(run_timeout)) + return [(runs, harness, lines, list())] - try: - slept = 0 - while repeat == 0 or not harness.done: - time.sleep(5) - slept += 5 - print('[RUN] {:d}/{:d} ({:.0f}%), current benchmark at {:.0f}%'.format(run_offset, runs_total, run_offset * 100 / runs_total, slept * 100 / run_timeout)) - except KeyboardInterrupt: - pass - monitor.close() + try: + slept = 0 + while not harness.done: + time.sleep(5) + slept += 5 + print('[RUN] {:d}/{:d} ({:.0f}%), current benchmark at {:.0f}%'.format(run_offset, runs_total, run_offset * 100 / runs_total, slept * 100 / run_timeout)) + except KeyboardInterrupt: + pass + monitor.close() - return [(runs, harness, monitor)] + return [(runs, harness, monitor, list())] if __name__ == '__main__': @@ -211,10 +246,12 @@ if __name__ == '__main__': 'depth= ' 'dummy= ' 'instance= ' + 'mimosa= ' 'repeat= ' 'run= ' 'sleep= ' 'shrink ' + 'timing ' 'timer-pin= ' 'trace-filter= ' ) @@ -240,13 +277,22 @@ if __name__ == '__main__': opt['sleep'] = 0 if 'trace-filter' in opt: - trace_filter = [] + trace_filter = list() for trace in opt['trace-filter'].split(): trace_filter.append(trace.split(',')) opt['trace-filter'] = trace_filter else: opt['trace-filter'] = None + if 'mimosa' in opt: + if opt['mimosa'] == '': + opt['mimosa'] = dict() + else: + opt['mimosa'] = dict(map(lambda x: x.split('='), opt['mimosa'].split(','))) + opt.pop('timing', None) + if opt['repeat'] == 0: + opt['repeat'] = 1 + except getopt.GetoptError as err: print(err) sys.exit(2) @@ -302,8 +348,10 @@ if __name__ == '__main__': if next(filter(lambda x: len(x.return_value_handlers), pta.transitions), None): need_return_values = True - harness = OnboardTimerHarness(gpio_pin = timer_pin, pta = pta, counter_limits = runner.get_counter_limits_us(opt['arch']), log_return_values = need_return_values, repeat = opt['repeat']) - harness = TransitionHarness(gpio_pin = timer_pin, pta = pta, log_return_values = need_return_values, repeat = opt['repeat'], post_transition_delay_us = 20) + if 'mimosa' in opt: + harness = TransitionHarness(gpio_pin = timer_pin, pta = pta, log_return_values = need_return_values, repeat = 1, post_transition_delay_us = 20) + elif 'timing' in opt: + harness = OnboardTimerHarness(gpio_pin = timer_pin, pta = pta, counter_limits = runner.get_counter_limits_us(opt['arch']), log_return_values = need_return_values, repeat = opt['repeat']) if len(args) > 1: results = run_benchmark(args[1], pta, runs, opt['arch'], opt['app'], opt['run'].split(), harness, opt['sleep'], opt['repeat'], runs_total = len(runs), dummy = 'dummy' in opt) @@ -312,7 +360,7 @@ if __name__ == '__main__': 'pta' : pta.to_json(), 'traces' : list(map(lambda x: x[1].traces, results)), 'raw_output' : list(map(lambda x: x[2].get_lines(), results)), - 'files' : list(map(lambda x: x[2].get_files(), results)), + 'files' : list(map(lambda x: x[3], results)), 'configs' : list(map(lambda x: x[2].get_config(), results)), } extra_files = flatten(json_out['files']) @@ -336,7 +384,7 @@ if __name__ == '__main__': json.dump(json_out, f) print(' --> {}.json'.format(output_prefix)) else: - outbuf = benchmark_from_runs(pta, runs, harness, repeat = repeat) + outbuf = benchmark_from_runs(pta, runs, harness, repeat = opt['repeat']) print(outbuf.getvalue()) sys.exit(0) diff --git a/lib/automata.py b/lib/automata.py index 0cc7143..2387734 100755 --- a/lib/automata.py +++ b/lib/automata.py @@ -27,10 +27,9 @@ class State: u""" Create a new PTA state. - arguments: - name -- state name - power -- static state power in µW - power_function -- parameterized state power in µW + :param name: state name + :param power: static state power in µW + :param power_function: parameterized state power in µW """ self.name = name self.power = power @@ -45,19 +44,25 @@ class State: u""" Return energy spent in state in pJ. - arguments: - duration -- duration in µs - param_dict -- current parameters + :param duration: duration in µs + :param param_dict: current parameters + :returns: energy spent in pJ """ if self.power_function: return self.power_function.eval(_dict_to_list(param_dict)) * duration return self.power * duration def set_random_energy_model(self, static_model = True): + """Set a random static energy value.""" self.power = int(np.random.sample() * 50000) def get_transition(self, transition_name: str) -> object: - """Return Transition object for outgoing transtion transition_name.""" + """ + Return Transition object for outgoing transtion transition_name. + + :param transition_name: transition name + :returns: `Transition` object + """ return self.outgoing_transitions[transition_name] def has_interrupt_transitions(self) -> bool: @@ -73,8 +78,8 @@ class State: Must only be called if has_interrupt_transitions returned true. - arguments: - parameters -- current parameter values + :param parameters: current parameter values + :returns: Transition object """ interrupts = filter(lambda x: x.is_interrupt, self.outgoing_transitions.values()) interrupts = sorted(interrupts, key = lambda x: x.get_timeout(parameters)) @@ -84,14 +89,15 @@ class State: """ Return a generator object for depth-first search over all outgoing transitions. - arguments: - depth -- search depth - with_arguments -- perform dfs with function+argument transitions instead of just function transitions. - trace_filter -- list of lists. Each sub-list is a trace. Only traces matching one of the provided sub-lists are returned. + :param depth: search depth + :param with_arguments: perform dfs with function+argument transitions instead of just function transitions. + :param trace_filter: list of lists. Each sub-list is a trace. Only traces matching one of the provided sub-lists are returned. E.g. trace_filter = [['init', 'foo'], ['init', 'bar']] will only return traces with init as first and foo or bar as second element. trace_filter = [['init', 'foo', '$'], ['init', 'bar', '$']] will only return the traces ['init', 'foo'] and ['init', 'bar']. - sleep -- if set and non-zero: include sleep pseudo-states with <sleep> us duration + Note that `trace_filter` takes precedence over `depth`: traces matching `trace_filter` are generated even if their length exceeds `depth` + :param sleep: if set and non-zero: include sleep pseudo-states with <sleep> us duration For the [['init', 'foo', '$'], ['init', 'bar', '$']] example above, sleep=10 results in [(None, 10), 'init', (None, 10), 'foo'] and [(None, 10), 'init', (None, 10), 'bar'] + :returns: Generator object for depth-first search. Each access yields a list of (Transition, (arguments)) elements describing a single run through the PTA. """ # A '$' entry in trace_filter indicates that the trace should (successfully) terminate here regardless of `depth`. @@ -170,7 +176,28 @@ class State: return ret class Transition: - """A single PTA transition with one origin and one destination state.""" + u""" + A single PTA transition with one origin and one destination state. + + :param name: transition name, corresponds to driver function name + :param origin: origin `State` + :param destination: destination `State` + :param energy: static energy needed to execute this transition, in pJ + :param energy_function: parameterized transition energy `AnalyticFunction`, returning pJ + :param duration: transition duration, in µs + :param duration_function: parameterized duration `AnalyticFunction`, returning µs + :param timeout: transition timeout, in µs. Only set for interrupt transitions. + :param timeout_function: parameterized transition timeout `AnalyticFunction`, in µs. Only set for interrupt transitions. + :param is_interrupt: Is this an interrupt transition? + :param arguments: list of function argument names + :param argument_values: list of argument values used for benchmark generation. Each entry is a list of values for the corresponding argument + :param argument_combination: During benchmark generation, should arguments be combined via `cartesian` or `zip`? + :param param_update_function: Setter for parameters after a transition. Gets current parameter dict and function argument values as arguments, must return the new parameter dict + :param arg_to_param_map: dict mapping argument index to the name of the parameter affected by its value + :param set_param: dict mapping parameter name to their value (set as side-effect of executing the transition, not parameter-dependent) + :param return_value_handlers: todo + :param codegen: todo + """ def __init__(self, orig_state: State, dest_state: State, name: str, energy: float = 0, energy_function: AnalyticFunction = None, @@ -183,14 +210,14 @@ class Transition: param_update_function = None, arg_to_param_map: dict = None, set_param = None, - return_value_handlers: list = []): + return_value_handlers: list = [], + codegen = dict()): """ Create a new transition between two PTA states. - arguments: - orig_state -- origin state - dest_state -- destination state - name -- transition name, typically the same as a driver/library function name + :param orig_state: origin `State` + :param dest_state: destination `State` + :param name: transition name, typically the same as a driver/library function name """ self.name = name self.origin = orig_state @@ -209,18 +236,21 @@ class Transition: self.arg_to_param_map = arg_to_param_map self.set_param = set_param self.return_value_handlers = return_value_handlers + self.codegen = codegen for handler in self.return_value_handlers: if 'formula' in handler: handler['formula'] = NormalizationFunction(handler['formula']) + def get_duration(self, param_dict: dict = {}, args: list = []) -> float: u""" Return transition duration in µs. - arguments: - param_dict -- current parameter values - args -- function arguments + :param param_dict: current parameter values + :param args: function arguments + + :returns: transition duration in µs """ if self.duration_function: return self.duration_function.eval(_dict_to_list(param_dict), args) @@ -307,6 +337,14 @@ class PTA: A parameterized priced timed automaton. All states are accepting. Suitable for simulation, model storage, and (soon) benchmark generation. + + :param state: dict mapping state name to `State` object + :param accepting_states: list of accepting state names + :param parameters: current parameters + :param parameter_normalization: TODO + :param codegen: TODO + :param initial_param_values: TODO + :param transitions: list of `Transition` objects """ def __init__(self, state_names: list = [], diff --git a/lib/dfatool.py b/lib/dfatool.py index 68146ae..ade685c 100755 --- a/lib/dfatool.py +++ b/lib/dfatool.py @@ -648,7 +648,7 @@ class RawData: # state_duration is stored as ms, not us return offline['us'] > state_duration * 1500 - def _measurement_is_valid_01(self, processed_data, repeat = 0): + def _measurement_is_valid_01(self, processed_data): """ Check if a dfatool v0 or v1 measurement is valid. @@ -693,8 +693,6 @@ class RawData: sched_trigger_count = 0 for run in traces: sched_trigger_count += len(run['trace']) - if repeat: - sched_trigger_count *= repeat if sched_trigger_count != processed_data['triggers']: processed_data['error'] = 'got {got:d} trigger edges, expected {exp:d}'.format( got = processed_data['triggers'], @@ -931,20 +929,8 @@ class RawData: elif version == 1: - traces_by_file = list() - mim_files_by_file = list() with tarfile.open(filename) as tf: - # Relies on generate-dfa-benchmark placing the .mim files - # in the order they were created (i.e., lexically sorted) - for member in tf.getmembers(): - _, extension = os.path.splitext(member.name) - if extension == '.mim': - mim_files_by_file.append({ - 'content' : tf.extractfile(member).read(), - 'info' : member, - }) - elif extension == '.json': - ptalog = json.load(tf.extractfile(member)) + ptalog = json.load(tf.extractfile(tf.getmember('ptalog.json'))) # ptalog['traces'] is a list of lists. # The first level corresponds to the individual .mim files: @@ -954,17 +940,22 @@ class RawData: # sub-benchmark, so ptalog['traces'][0][0] is the first # run, ptalog['traces'][0][1] the second, and so on - traces_by_file.extend(ptalog['traces']) - self.setup_by_fileno.append({ - 'mimosa_voltage' : ptalog['configs'][0]['voltage'], - 'mimosa_shunt' : ptalog['configs'][0]['shunt'], - 'state_duration' : ptalog['opt']['sleep'] - }) - for j, mim_file in enumerate(mim_files_by_file): - mim_file['setup'] = self.setup_by_fileno[i] - mim_file['expected_trace'] = ptalog['traces'][j] - mim_file['fileno'] = i - mim_files.extend(mim_files_by_file) + for j, traces in enumerate(ptalog['traces']): + self.traces_by_fileno.append(traces) + self.setup_by_fileno.append({ + 'mimosa_voltage' : ptalog['configs'][j]['voltage'], + 'mimosa_shunt' : ptalog['configs'][j]['shunt'], + 'state_duration' : ptalog['opt']['sleep'], + }) + for mim_file in ptalog['files'][j]: + member = tf.getmember(mim_file) + mim_files.append({ + 'content' : tf.extractfile(member).read(), + 'fileno' : i, + 'info' : member, + 'setup' : self.setup_by_fileno[j], + 'expected_trace' : ptalog['traces'][j], + }) with Pool() as pool: measurements = pool.map(_preprocess_measurement, mim_files) @@ -983,7 +974,7 @@ class RawData: measurement['energy_trace'].pop(0) repeat = ptalog['opt']['repeat'] - if self._measurement_is_valid_01(measurement, repeat): + if self._measurement_is_valid_01(measurement): self._merge_online_and_offline(measurement) num_valid += 1 else: diff --git a/lib/harness.py b/lib/harness.py index d3ea481..f39b28c 100644 --- a/lib/harness.py +++ b/lib/harness.py @@ -11,15 +11,35 @@ import re # generated otherwise and it should also work with AnalyticModel (which does # not have states) class TransitionHarness: - """Foo.""" + """ + TODO + + :param done: True if the specified amount of iterations have been logged. + :param synced: True if `parser_cb` has synchronized with UART output, i.e., the benchmark has successfully started. + :param traces: List of annotated PTA traces from benchmark execution. This list is updated during UART logging and should only be read back when `done` is True. + Uses the standard dfatool trace format: `traces` is a list of `{'id': ..., 'trace': ...}` dictionaries, each of which represents a single PTA trace (AKA + run). Each `trace` is in turn a list of state or transition dictionaries with the + following attributes: + * `isa`: 'state' or 'transition' + * `name`: state or transition name + * `parameter`: currently valid parameter values. If normalization is used, they are already normalized. Each parameter value is either a primitive + int/float/str value (-> constant for each iteration) or a list of + primitive values (-> set by the return value of the current run, not necessarily constan) + * `args`: function arguments, if isa == 'transition' + """ def __init__(self, gpio_pin = None, pta = None, log_return_values = False, repeat = 0, post_transition_delay_us = 0): """ Create a new TransitionHarness - :param gpio_pin: multipass GPIO Pin used for transition synchronization, e.g. `GPIO::p1_0`. Optional. + :param gpio_pin: multipass GPIO Pin used for transition synchronization with an external measurement device, e.g. `GPIO::p1_0`. Optional. The GPIO output is high iff a transition is executing - :param pta: PTA object + :param pta: PTA object. Needed to map UART output IDs to states and transitions :param log_return_values: Log return values of transition function calls? + :param repeat: How many times to run the benchmark until setting `one`, default 0. + When 0, `done` is never set. + :param post_transition_delay_us: If set, inject `arch.delay_us` after each transition, before logging the transition as completed (and releasing + `gpio_pin`). This artificially increases transition duration by the specified time and is useful if an external measurement device's resolution is + lower than the expected minimum transition duration. """ self.gpio_pin = gpio_pin self.pta = pta @@ -29,19 +49,35 @@ class TransitionHarness: self.reset() def copy(self): - new_object = __class__(gpio_pin = self.gpio_pin, pta = self.pta, log_return_values = self.log_return_values, repeat = self.repeat) + new_object = __class__(gpio_pin = self.gpio_pin, pta = self.pta, log_return_values = self.log_return_values, repeat = self.repeat, post_transition_delay_us = self.post_transition_delay_us) new_object.traces = self.traces.copy() new_object.trace_id = self.trace_id return new_object def reset(self): + """ + Reset harness for a new benchmark. + + Truncates `traces`, `trace_id`, `done`, and `synced`. + """ self.traces = [] self.trace_id = 0 self.repetitions = 0 self.done = False self.synced = False + def restart(self): + """ + Reset harness for a new execution of the current benchmark. + + Resets `done` and `synced`. + """ + self.repetitions = 0 + self.done = False + self.synced = False + def global_code(self): + """Return global (pre-`main()`) C++ code needed for tracing.""" ret = '' if self.gpio_pin != None: ret += '#define PTALOG_GPIO {}\n'.format(self.gpio_pin) @@ -56,9 +92,11 @@ class TransitionHarness: return ret def start_benchmark(self, benchmark_id = 0): + """Return C++ code to signal benchmark start to harness.""" return 'ptalog.startBenchmark({:d});\n'.format(benchmark_id) def start_trace(self): + """Prepare a new trace/run in the internal `.traces` structure.""" self.traces.append({ 'id' : self.trace_id, 'trace' : list(), @@ -66,6 +104,12 @@ class TransitionHarness: self.trace_id += 1 def append_state(self, state_name, param): + """ + Append a state to the current run in the internal `.traces` structure. + + :param state_name: state name + :param param: parameter dict + """ self.traces[-1]['trace'].append({ 'name': state_name, 'isa': 'state', @@ -73,6 +117,13 @@ class TransitionHarness: }) def append_transition(self, transition_name, param, args = []): + """ + Append a transition to the current run in the internal `.traces` structure. + + :param transition_name: transition name + :param param: parameter dict + :param args: function arguments (optional) + """ self.traces[-1]['trace'].append({ 'name': transition_name, 'isa': 'transition', @@ -81,9 +132,16 @@ class TransitionHarness: }) def start_run(self): + """Return C++ code used to start a new run/trace.""" return 'ptalog.reset();\n' def pass_transition(self, transition_id, transition_code, transition: object = None): + """ + Return C++ code used to pass a transition, including the corresponding function call. + + Tracks which transition has been executed and optionally its return value. May also inject a delay, if + `post_transition_delay_us` is set. + """ ret = 'ptalog.passTransition({:d});\n'.format(transition_id) ret += 'ptalog.startTransition();\n' if self.log_return_values and transition and len(transition.return_value_handlers): @@ -170,14 +228,22 @@ class TransitionHarness: self.current_transition_in_trace += 1 class OnboardTimerHarness(TransitionHarness): - """Bar.""" + """TODO + + Additional parameters / changes from TransitionHarness: + + :param traces: Each trace element (`.traces[*]['trace'][*]`) additionally contains + the dict `offline_aggregates` with the member `duration`. It contains a list of durations (in us) of the corresponding state/transition for each + benchmark iteration. + I.e. `.traces[*]['trace'][*]['offline_aggregates']['duration'] = [..., ...]` + """ def __init__(self, counter_limits, **kwargs): super().__init__(**kwargs) self.trace_length = 0 self.one_cycle_in_us, self.one_overflow_in_us, self.counter_max_overflow = counter_limits def copy(self): - new_harness = __class__((self.one_cycle_in_us, self.one_overflow_in_us, self.counter_max_overflow), gpio_pin = self.gpio_pin, pta = self.pta, log_return_values = self.log_return_values) + new_harness = __class__((self.one_cycle_in_us, self.one_overflow_in_us, self.counter_max_overflow), gpio_pin = self.gpio_pin, pta = self.pta, log_return_values = self.log_return_values, repeat = self.repeat) new_harness.traces = self.traces.copy() new_harness.trace_id = self.trace_id return new_harness @@ -217,8 +283,14 @@ class OnboardTimerHarness(TransitionHarness): def parser_cb(self, line): #print('[HARNESS] got line {}'.format(line)) if re.match(r'\[PTA\] benchmark start, id=(\S+)', line): + if self.repeat > 0 and self.repetitions == self.repeat: + self.done = True + self.synced = False + print('[HARNESS] done') + return self.synced = True - print('[HARNESS] synced') + self.repetitions += 1 + print('[HARNESS] synced, {}/{}'.format(self.repetitions, self.repeat)) if self.synced: res = re.match(r'\[PTA\] trace=(\S+) count=(\S+)', line) if res: @@ -237,6 +309,7 @@ class OnboardTimerHarness(TransitionHarness): if overflow >= self.counter_max_overflow: raise RuntimeError('Counter overflow ({:d}/{:d}) in benchmark id={:d} trace={:d}: transition #{:d} (ID {:d})'.format(cycles, overflow, 0, self.trace_id, self.current_transition_in_trace, transition_id)) duration_us = cycles * self.one_cycle_in_us + overflow * self.one_overflow_in_us + # TODO subtract 'nop' cycles # self.traces contains transitions and states, UART output only contains transitions -> use index * 2 try: log_data_target = self.traces[self.trace_id]['trace'][self.current_transition_in_trace * 2] diff --git a/lib/runner.py b/lib/runner.py index 588bd4d..25e3519 100644 --- a/lib/runner.py +++ b/lib/runner.py @@ -167,6 +167,8 @@ class MIMOSAMonitor(SerialMonitor): self._mimosactl('connect') def _stop_mimosa(self): + # Make sure the MIMOSA daemon has gathered all needed data + time.sleep(2) self._mimosacmd(['--mimosa-stop']) mtime_changed = True mim_file = None |