浏览代码

Fix error in set of enter states for Day & Atlee's example 4. Theoretically faster generation of "effective target states" by generating them *partially* statically. Quite complex and dirty at the moment.

Joeri Exelmans 5 年之前
父节点
当前提交
4a3a9a0507

+ 11 - 0
src/sccd/statechart/dynamic/round.py

@@ -4,6 +4,7 @@ from sccd.util.bitmap import *
 from sccd.statechart.static.tree import *
 from sccd.util.debug import *
 from sccd.action_lang.dynamic.exceptions import *
+from sccd.util import timer
 
 class CandidatesGenerator:
     def __init__(self, reverse: bool):
@@ -68,6 +69,7 @@ class Round(ABC):
         self.callbacks.append(callback)
 
     def run_and_cycle_events(self, forbidden_arenas: Bitmap = Bitmap()) -> RoundResult:
+        timer.start("round: %s" % self.name)
         changed, stable = self._run(forbidden_arenas)
         if changed:
             # notify round observers
@@ -77,6 +79,7 @@ class Round(ABC):
             self.remainder_events = self.next_events
             self.next_events = []
             print_debug("completed "+self.name)
+        timer.stop("round: %s" % self.name)
         return (changed, stable)
 
     @abstractmethod
@@ -184,9 +187,11 @@ class SmallStep(Round):
         enabled_events = None
         def get_candidates(extra_forbidden):
             nonlocal enabled_events
+            timer.start("get enabled events")
             enabled_events = self.enabled_events()
             # The cost of sorting our enabled events is smaller than the benefit gained by having to loop less often over it in our transition execution code:
             enabled_events.sort(key=lambda e: e.id)
+            timer.stop("get enabled events")
 
             candidates = self.generator.generate(self.state, enabled_events, forbidden_arenas |  extra_forbidden)
 
@@ -204,8 +209,10 @@ class SmallStep(Round):
         arenas = Bitmap()
         stable_arenas = Bitmap()
 
+        timer.start("get candidate")
         candidates = get_candidates(0)
         t = next(candidates, None)
+        timer.stop("get candidate")
         while t:
             arena = t.gen.arena_bitmap
             if not (arenas & arena):
@@ -220,9 +227,13 @@ class SmallStep(Round):
 
                 # need to re-generate candidates after firing transition
                 # because possibly the set of current events has changed
+                timer.start("get candidate")
                 candidates = get_candidates(extra_forbidden=arenas)
+            else:
+                timer.start("get candidate")
 
             t = next(candidates, None)
+            timer.stop("get candidate")
 
         return (arenas, stable_arenas)
 

+ 52 - 31
src/sccd/statechart/dynamic/statechart_execution.py

@@ -5,6 +5,7 @@ from sccd.util.debug import print_debug
 from sccd.util.bitmap import *
 from sccd.action_lang.static.scope import *
 from sccd.action_lang.dynamic.exceptions import *
+from sccd.util import timer
 
 # Set of current states etc.
 class StatechartExecution:
@@ -27,7 +28,7 @@ class StatechartExecution:
         # mapping from configuration_bitmap (=int) to configuration (=List[State])
         self.config_mem = {}
         # mapping from history state id to states to enter if history is target of transition
-        self.history_values = {}
+        self.history_values: Dict[int, List[State]] = {}
 
         # For each AfterTrigger in the statechart tree, we keep an expected 'id' that is
         # a parameter to a future 'after' event. This 'id' is incremented each time a timer
@@ -39,15 +40,15 @@ class StatechartExecution:
 
     # enter default states
     def initialize(self):
-        states = self.statechart.tree.root.getEffectiveTargetStates(self)
-        self.configuration.extend(states)
-        self.configuration_bitmap = Bitmap.from_list(s.gen.state_id for s in states)
+        states = self.statechart.tree.root.target_states(self, True)
+        self.configuration.extend(self.statechart.tree.state_list[id] for id in states.items())
+        self.configuration_bitmap = states
 
         ctx = EvalContext(current_state=self, events=[], memory=self.rhs_memory)
         if self.statechart.datamodel is not None:
             self.statechart.datamodel.exec(self.rhs_memory)
 
-        for state in states:
+        for state in self.configuration:
             print_debug(termcolor.colored('  ENTER %s'%state.gen.full_name, 'green'))
             self.eventless_states += state.gen.has_eventless_transitions
             self._perform_actions(ctx, state.enter)
@@ -60,41 +61,55 @@ class StatechartExecution:
     # events: list SORTED by event id
     def fire_transition(self, events: List[Event], t: Transition):
         try:
+            timer.start("transition")
+
+            timer.start("exit states")
             # Exit set is the intersection between self.configuration and t.gen.arena.descendants.
 
-            # The following was found to be more efficient than reverse-iterating and filtering self.configuration or t.arena.gen.descendants lists, despite the fact that Bitmap.reverse_items() isn't very efficient.
-            exit_ids = self.configuration_bitmap & t.gen.arena.gen.descendant_bitmap
+            # The following was found to have better performance than reverse-iterating and filtering self.configuration or t.arena.gen.descendants lists, despite the fact that Bitmap.reverse_items() isn't very efficient.
+            exit_ids = self.configuration_bitmap & t.gen.arena.gen.descendants_bitmap
             exit_set = (self.statechart.tree.state_list[id] for id in exit_ids.reverse_items())
 
-            def __enterSet(targets):
-                target = targets[0]
-                for a in reversed(target.gen.ancestors):
-                    if a in t.source.gen.ancestors:
-                        continue
-                    else:
-                        yield a
-                for target in targets:
-                    yield target
-
-            def __getEffectiveTargetStates():
-                targets = []
-                for target in t.targets:
-                    for e_t in target.getEffectiveTargetStates(self):
-                        if not e_t in targets:
-                            targets.append(e_t)
-                return targets
+            # Alternative implementation:
+            # if len(self.configuration) < len(t.gen.arena.gen.descendants):
+            #     exit_set = (s for s in self.configuration if s.gen.state_id_bitmap & t.gen.arena.gen.descendants_bitmap)
+            # else:
+            #     exit_set = (s for s in t.gen.arena.gen.descendants if s.gen.state_id_bitmap & self.configuration_bitmap)
+            timer.stop("exit states")
+
+
+            timer.start("enter states")
+            # Enter path is the intersection between:
+            #   1) the transitions target + the target's ancestors and
+            #   2) the arena's descendants
+            enter_path = (t.targets[0].gen.ancestors_bitmap | t.targets[0].gen.state_id_bitmap) & t.gen.arena.gen.descendants_bitmap
+            # Now, along the enter path, there may be AND-states whose children we don't explicitly enter, but should enter.
+            # That's why we call 'target_states' on every state on the path and join the results.
+            items = enter_path.items()
+            shifted = itertools.chain(enter_path.items(), [-1])
+            next(shifted) # throw away first value
+            pairwise = zip(items, shifted)
+
+            enter_ids = Bitmap()
+            for state_id, next_state_id in pairwise:
+                enter_ids |= self.statechart.tree.state_list[state_id].target_states(self, next_state_id == -1)
+            enter_set = (self.statechart.tree.state_list[id] for id in enter_ids.items())
+            timer.stop("enter states")
+
 
             ctx = EvalContext(current_state=self, events=events, memory=self.rhs_memory)
 
             print_debug("fire " + str(t))
 
+            timer.start("exit states")
             # exit states...
             for s in exit_set:
                 # remember which state(s) we were in if a history state is present
                 for h in s.gen.history:
-                    f = lambda s0: s0.gen.ancestors and s0.parent == s
                     if isinstance(h, DeepHistoryState):
-                        f = lambda s0: not s0.gen.descendants and s0 in s.gen.descendants
+                        f = lambda s0: not s0.gen.descendants_bitmap and s0.gen.state_id_bitmap & s.gen.descendants_bitmap
+                    else:
+                        f = lambda s0: s0.gen.ancestors_bitmap and s0.parent == s
                     self.history_values[h.gen.state_id] = list(filter(f, self.configuration))
 
                 print_debug(termcolor.colored('  EXIT %s' % s.gen.full_name, 'green'))
@@ -103,31 +118,37 @@ class StatechartExecution:
                 self._perform_actions(ctx, s.exit)
                 # self.rhs_memory.pop_local_scope(s.scope)
                 self.configuration_bitmap &= ~s.gen.state_id_bitmap
+            timer.stop("exit states")
 
-            # print("arena was: ", t.gen.arena.gen.full_name)
-                    
             # execute transition action(s)
+            timer.start("actions")
             self.rhs_memory.push_frame(t.scope) # make room for event parameters on stack
             if t.trigger:
                 t.trigger.copy_params_to_stack(ctx)
             self._perform_actions(ctx, t.actions)
             self.rhs_memory.pop_frame()
-                
+            timer.stop("actions")
+
+            timer.start("enter states")
             # enter states...
-            enter_set = (t.gen.arena.gen.descendants)
-            for s in __enterSet(__getEffectiveTargetStates()):
+            for s in enter_set:
                 print_debug(termcolor.colored('  ENTER %s' % s.gen.full_name, 'green'))
                 self.eventless_states += s.gen.has_eventless_transitions
                 self.configuration_bitmap |= s.gen.state_id_bitmap
                 # execute enter action(s)
                 self._perform_actions(ctx, s.enter)
                 self._start_timers(s.gen.after_triggers)
+            timer.stop("enter states")
+            
             try:
                 self.configuration = self.config_mem[self.configuration_bitmap]
             except KeyError:
                 self.configuration = self.config_mem[self.configuration_bitmap] = [s for s in self.statechart.tree.state_list if self.configuration_bitmap & s.gen.state_id_bitmap]
 
             self.rhs_memory.flush_transition()
+
+            timer.stop("transition")
+
         except SCCDRuntimeException as e:
             e.args = ("During execution of transition %s:\n" % str(t) +str(e),)
             raise

+ 4 - 5
src/sccd/statechart/parser/xml.py

@@ -77,8 +77,7 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
       root = State("", parent=None)
       children_dict = {}
       transitions = [] # All of the statechart's transitions accumulate here, cause we still need to find their targets, which we can't do before the entire state tree has been built. We find their targets when encoutering the </root> closing tag.
-      after_triggers = [] # After triggers accumulate here
-      # next_after_id = 0 # Counter for 'after' transitions within the statechart.
+      after_id = 0 # After triggers need unique IDs within the scope of the statechart model
 
       def create_actions_parser(scope):
 
@@ -215,15 +214,15 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
               transition.trigger = NegatedTrigger(positive_events, negative_events)
 
           def parse_attr_after(after):
+            nonlocal after_id
             if have_event_attr:
               raise XmlError("Cannot specify 'after' and 'event' at the same time.")
             after_expr = parse_expression(globals, after)
             after_type = after_expr.init_expr(scope)
             check_duration_type(after_type)
-            after_id = len(after_triggers)
             event_name = "_after%d" % after_id # transition gets unique event name
             transition.trigger = AfterTrigger(globals.events.assign_id(event_name), event_name, after_id, after_expr)
-            after_triggers.append(transition.trigger)
+            after_id += 1
 
           def parse_attr_cond(cond):
             expr = parse_expression(globals, cond)
@@ -271,7 +270,7 @@ def create_statechart_parser(globals, src_file, load_external = True, parse = pa
           except Exception as e:
             raise XmlErrorElement(t_el, "Could not find target '%s'." % (transition.target_string)) from e
 
-        statechart.tree = StateTree(root, after_triggers)
+        statechart.tree = StateTree(root)
 
       return (create_state_parser(root, sibling_dict=children_dict), finish_root)
 

+ 87 - 38
src/sccd/statechart/static/tree.py

@@ -2,7 +2,7 @@ import termcolor
 from typing import *
 from sccd.statechart.static.action import *
 from sccd.util.bitmap import *
-
+from sccd.util import timer
 
 @dataclass
 class State:
@@ -13,7 +13,7 @@ class State:
     stable: bool = False # whether this is a stable stabe. this field is ignored if maximality semantics is not set to SYNTACTIC
 
     children: List['State'] = field(default_factory=list)
-    default_state = None # child state pointed to by 'initial' attribute
+    default_state: 'State' = None # child state pointed to by 'initial' attribute
 
     transitions: List['Transition'] = field(default_factory=list)
 
@@ -26,12 +26,27 @@ class State:
         if self.parent is not None:
             self.parent.children.append(self)
 
-    def getEffectiveTargetStates(self, instance):
-        targets = [self]
+
+    def target_states(self, instance, end_of_path) -> Bitmap:
+        if end_of_path:
+            return self.gen.static_ts_bitmap | functools.reduce(lambda x,y: x|y, (s._target_states(instance) for s in self.gen.dynamic_ts), Bitmap())
+        else:
+            return self.gen.state_id_bitmap
+
+    def _target_states(self, instance) -> Bitmap:
+        # targets = [self]
+        targets = self.gen.state_id_bitmap
         if self.default_state:
-            targets.extend(self.default_state.getEffectiveTargetStates(instance))
+            targets |= self.default_state._target_states(instance)
         return targets
-                    
+
+    def _static_ts(self) -> Tuple[List['State'], List['State']]:
+        if self.default_state:
+            static, dynamic = self.default_state._static_ts()
+            return ([self] + static, dynamic)
+        else:
+            return ([self], [])
+
     def __repr__(self):
         return "State(\"%s\")" % (self.gen.full_name)
 
@@ -42,48 +57,73 @@ class StateOptimization:
     state_id_bitmap: Bitmap
     full_name: str
     ancestors: List[State] # order: close to far away, i.e. first element is parent
+    ancestors_bitmap: Bitmap
     descendants: List[State]  # order: breadth-first
-    descendant_bitmap: Bitmap
+    descendants_bitmap: Bitmap
     history: List[State] # subset of children
+
+    static_ts_bitmap: Bitmap # Bitmap of all descendants that are always part of the 'effective targets states'
+    dynamic_ts: List[State] # Subset of descendants containing possible target-states
+
     has_eventless_transitions: bool
     after_triggers: List['AfterTrigger']
 
 
 class HistoryState(State):
+
+    def target_states(self, instance, end_of_path) -> Bitmap:
+        return Bitmap.from_list(s.gen.state_id_bitmap for s in self._target_states(instance))
+
     @abstractmethod
-    def getEffectiveTargetStates(self, instance):
+    def _target_states(self, instance) -> Bitmap:
         pass
-        
+
+    def _static_ts(self) -> Tuple[List[State], Bitmap]:
+        return ([], [self])
+
 class ShallowHistoryState(HistoryState):
-        
-    def getEffectiveTargetStates(self, instance):
-        if self.state_id in instance.history_values:
-            targets = []
+
+    def _target_states(self, instance) -> Bitmap:
+        try:
+            targets = Bitmap()
             for hv in instance.history_values[self.state_id]:
-                targets.extend(hv.getEffectiveTargetStates(instance))
+                targets |= hv.target_states(instance, True)
             return targets
-        else:
-            # TODO: is it correct that in this case, the parent itself is also entered?
-            return self.parent.getEffectiveTargetStates(instance)
-        
+        except KeyError:
+            # TODO: is it correct that in this case, the parent itself is also entered? -> Joeri: Nope!
+            return self.parent._target_states(instance)
+
 class DeepHistoryState(HistoryState):
         
-    def getEffectiveTargetStates(self, instance):
-        if self.state_id in instance.history_values:
-            return instance.history_values[self.state_id]
-        else:
+    def _target_states(self, instance) -> Bitmap:
+        try:
+            return Bitmap.from_list(s.state_id for s in instance.history_values[self.state_id])
+        except KeyError:
             # TODO: is it correct that in this case, the parent itself is also entered?
-            return self.parent.getEffectiveTargetStates(instance)
+            return self.parent._target_states(instance)
         
 class ParallelState(State):
-        
-    def getEffectiveTargetStates(self, instance):
+
+    def target_states(self, instance, end_of_path) -> Bitmap:
+        return self.gen.static_ts_bitmap | Bitmap.from_list(s._target_states(instance) for s in self.gen.dynamic_ts)
+
+    def _target_states(self, instance) -> Bitmap:
         targets = [self]
         for c in self.children:
             if not isinstance(c, HistoryState):
-                targets.extend(c.getEffectiveTargetStates(instance))
+                targets.extend(c._target_states(instance))
         return targets
 
+    def _static_ts(self) -> Tuple[List[State], Bitmap]:
+        static = [self]
+        dynamic = []
+        for c in self.children:
+            if not isinstance(c, HistoryState):
+                c_static, c_dynamic = c._static_ts()
+                static.extend(c_static)
+                dynamic.extend(c_dynamic)
+        return static, dynamic
+
 @dataclass
 class EventDecl:
     id: int
@@ -193,20 +233,22 @@ class StateTree:
 
     # root: The root state of a state,transition tree structure with with all fields filled in,
     #       except the 'gen' fields. This function will fill in the 'gen' fields.
-    def __init__(self, root: State, after_triggers: List[AfterTrigger]):
+    def __init__(self, root: State):
+        timer.start("optimize tree")
         self.state_dict = {} # mapping from 'full name' to State
         self.state_list = [] # depth-first list of states
         self.transition_list = [] # all transitions in the tree, sorted by source state, depth-first
-        self.after_triggers = after_triggers
+        self.after_triggers = []
         self.stable_bitmap = Bitmap() # bitmap of state IDs of states that are stable. Only used for SYNTACTIC-maximality semantics.
 
         next_id = 0
 
-        def init_state(state: State, parent_full_name: str, ancestors: List[State]):
+        def init_state(state: State, parent_full_name: str, ancestors: List[State], ancestors_bitmap):
             nonlocal next_id
 
             state_id = next_id
             next_id += 1
+            state_id_bitmap = bit(state_id)
 
             if state is root:
                 full_name = '/'
@@ -222,6 +264,7 @@ class StateTree:
             history = []
             has_eventless_transitions = False
             after_triggers = []
+            static_ts, dynamic_ts = state._static_ts()
 
             for t in state.transitions:
                 self.transition_list.append(t)
@@ -229,10 +272,10 @@ class StateTree:
                     has_eventless_transitions = True
                 elif isinstance(t.trigger, AfterTrigger):
                     after_triggers.append(t.trigger)
-                    # self.after_triggers.append(t.trigger)
+                    self.after_triggers.append(t.trigger)
 
             for c in state.children:
-                init_state(c, full_name, [state] + ancestors)
+                init_state(c, full_name, [state] + ancestors, state_id_bitmap | ancestors_bitmap)
                 if isinstance(c, HistoryState):
                     history.append(c)
 
@@ -240,24 +283,28 @@ class StateTree:
             for c in state.children:
                 descendants.extend(c.gen.descendants)
 
+            descendants_bitmap = Bitmap.from_list(s.gen.state_id for s in descendants)
+            static_ts_bitmap = Bitmap.from_list(s.gen.state_id for s in static_ts if s.gen) | state_id_bitmap
+
             state.gen = StateOptimization(
                 state_id=state_id,
-                state_id_bitmap=bit(state_id),
+                state_id_bitmap=state_id_bitmap,
                 full_name=full_name,
                 ancestors=ancestors,
+                ancestors_bitmap=ancestors_bitmap,
                 descendants=descendants,
-                descendant_bitmap=reduce(lambda x,y: x | bit(y.gen.state_id), descendants, Bitmap(0)),
+                descendants_bitmap=descendants_bitmap,
                 history=history,
+                static_ts_bitmap=static_ts_bitmap,
+                dynamic_ts=dynamic_ts,
                 has_eventless_transitions=has_eventless_transitions,
                 after_triggers=after_triggers)
 
             if state.stable:
                 self.stable_bitmap |= bit(state_id)
 
-            # print("state:", full_name)
-            # print("ancestors:", len(ancestors))
 
-        init_state(root, "", [])
+        init_state(root, "", [], Bitmap())
         self.root = root
 
         for t in self.transition_list:
@@ -279,6 +326,8 @@ class StateTree:
 
             t.gen = TransitionOptimization(
                 lca=lca,
-                lca_bitmap=lca.gen.descendant_bitmap | lca.gen.state_id_bitmap,
+                lca_bitmap=lca.gen.descendants_bitmap | lca.gen.state_id_bitmap,
                 arena=arena,
-                arena_bitmap=arena.gen.descendant_bitmap | arena.gen.state_id_bitmap)
+                arena_bitmap=arena.gen.descendants_bitmap | arena.gen.state_id_bitmap)
+
+        timer.stop("optimize tree")

+ 1 - 0
src/sccd/util/debug.py

@@ -4,6 +4,7 @@ try:
   DEBUG = bool(os.environ['SCCDDEBUG']=="1")
 except KeyError:
   DEBUG = False
+  
 def print_debug(msg):
     if DEBUG:
         print(msg)

+ 38 - 0
src/sccd/util/timer.py

@@ -0,0 +1,38 @@
+import os
+
+try:
+  TIMINGS = bool(os.environ['SCCDTIMINGS']=="1")
+except KeyError:
+  TIMINGS = False
+
+if TIMINGS:
+  import time
+  import atexit
+
+  timings = {}
+
+  timers = {}
+
+  def start(what):
+    timers[what] = time.time()
+
+  def stop(what):
+    end = time.time()
+    begin = timers[what]
+    del timers[what]
+    duration = end - begin
+    old_val = timings.setdefault(what, 0)
+    timings[what] = old_val + duration
+
+  def _print_stats():
+      print("timings:")
+      for key,val in timings.items():
+        print("  %s: %f ms" % (key,val*1000))
+
+  atexit.register(_print_stats)
+else:
+  def start(what):
+    pass
+
+  def stop(what):
+    pass

+ 3 - 0
test/lib/test.py

@@ -4,6 +4,7 @@ from dataclasses import *
 from sccd.model.model import *
 from sccd.controller.controller import *
 from lib.test_parser import *
+from sccd.util import timer
 
 import threading
 import queue
@@ -20,7 +21,9 @@ class Test(unittest.TestCase):
     statechart_parser = functools.partial(create_statechart_parser, src_file=self.src)
     test_parser = create_test_parser(statechart_parser)
     try:
+      timer.start("parse test")
       test_variants = parse_f(self.src, test_parser)
+      timer.stop("parse test")
     except Exception as e:
       print_debug(e)
       raise e