瀏覽代碼

More slotted classes and using Python's 'with' statement for recording timings.

Joeri Exelmans 5 年之前
父節點
當前提交
49bc688471

+ 3 - 0
src/sccd/action_lang/dynamic/memory.py

@@ -8,6 +8,8 @@ from sccd.action_lang.static.expression import *
 
 @dataclass(frozen=True)
 class StackFrame:
+  __slots__ = ["storage", "parent", "context", "scope"]
+
   # Values of variables in the frame.
   storage: List[Any]
 
@@ -31,6 +33,7 @@ class StackFrame:
     return "StackFrame(%s, len=%d, parent=%s, context=%s)" % (self.scope.name, len(self.storage), short_descr(self.parent), "parent" if self.context is self.parent else short_descr(self.context))
 
 class Memory(MemoryInterface):
+  __slots__ = ["_current_frame"]
 
   def __init__(self):
     self._current_frame = None

+ 4 - 0
src/sccd/action_lang/static/scope.py

@@ -16,6 +16,8 @@ class ScopeError(ModelError):
 # Stateless stuff we know about a variable existing within a scope.
 @dataclass(frozen=True)
 class _Variable(ABC):
+  __slots__ = ["_name", "offset", "type", "const"]
+  
   _name: str # only used to print error messages
   offset: int # Offset within variable's scope. Always >= 0.
   type: SCCDType
@@ -31,6 +33,8 @@ class _Variable(ABC):
 
 # Stateless stuff we know about a scope (= set of named values)
 class Scope:
+  __slots__ = ["name", "parent", "parent_offset", "names", "variables"]
+
   def __init__(self, name: str, parent: 'Scope'):
     self.name = name
     self.parent = parent

+ 64 - 55
src/sccd/controller/event_queue.py

@@ -2,12 +2,15 @@ from heapq import heappush, heappop, heapify
 from abc import ABC
 from typing import List, Set, Tuple, Deque, Any, TypeVar, Generic, Generator, Optional
 from collections import deque
+from  sccd.util import timer
 
 Timestamp = TypeVar('Timestamp')
 
 Item = TypeVar('Item')
 
 class EventQueue(Generic[Timestamp, Item]):
+    __slots__ = ["queue", "counters", "removed"]
+
     def __init__(self):
         self.queue: List[Tuple[Timestamp, int, Item]] = []
         self.counters = {} # mapping from timestamp to number of items at timestamp
@@ -29,27 +32,30 @@ class EventQueue(Generic[Timestamp, Item]):
             return None
     
     def add(self, timestamp: Timestamp, item: Item):
-        self.counters[timestamp] = self.counters.setdefault(timestamp, 0) + 1
-        def_event = (timestamp, self.counters[timestamp], item)
-        heappush(self.queue, def_event)
-        return def_event
+        with timer.Context("event_queue"):
+            self.counters[timestamp] = self.counters.setdefault(timestamp, 0) + 1
+            def_event = (timestamp, self.counters[timestamp], item)
+            heappush(self.queue, def_event)
+            return def_event
     
     def remove(self, item: Item):
-        self.removed.add(item)
-        if len(self.removed) > 100:
-            self.queue = [x for x in self.queue if x not in self.removed]
-            self.removed = set()
+        with timer.Context("event_queue"):
+            self.removed.add(item)
+            if len(self.removed) > 100:
+                self.queue = [x for x in self.queue if x not in self.removed]
+                self.removed = set()
     
     # Raises exception if called on empty queue
     def pop(self) -> Tuple[Timestamp, Item]:
-        while 1:
-            item = heappop(self.queue)
-            timestamp = item[0]
-            self.counters[timestamp] -= 1
-            if not self.counters[timestamp]:
-                del self.counters[timestamp]
-            if item[2] not in self.removed:
-                return (timestamp, item[2])
+        with timer.Context("event_queue"):
+            while 1:
+                item = heappop(self.queue)
+                timestamp = item[0]
+                self.counters[timestamp] -= 1
+                if not self.counters[timestamp]:
+                    del self.counters[timestamp]
+                if item[2] not in self.removed:
+                    return (timestamp, item[2])
 
     def is_due(self, timestamp: Optional[Timestamp]) -> bool:
         return len(self.queue) and (timestamp == None or self.queue[0][0] <= timestamp)
@@ -87,53 +93,56 @@ class EventQueueDeque(Generic[Timestamp, Item]):
             return None
 
     def add(self, timestamp: Timestamp, item: Item):
-        try:
-            # entry at timestamp already exists:
-            self.entries[timestamp].append(item)
-        except KeyError:
-            # need to create entry
-            d = deque([item])
-            self.entries[timestamp] = d
-            heappush(self.queue, (timestamp, d))
+        with timer.Context("event_queue"):
+            try:
+                # entry at timestamp already exists:
+                self.entries[timestamp].append(item)
+            except KeyError:
+                # need to create entry
+                d = deque([item])
+                self.entries[timestamp] = d
+                heappush(self.queue, (timestamp, d))
 
     def remove(self, item: Item):
-        self.removed.add(item)
-        if len(self.removed) > 5:
-            # to remove some elements safely from list while iterating over it and without copying the list,
-            # we iterate backwards:
-            for i in range(len(self.queue)-1, -1, -1):
-                queue_entry = self.queue[i]
-                timestamp, old_deque = queue_entry
-                new_deque: Deque[Item] = deque([])
-                for item in old_deque:
-                    if item not in self.removed:
-                        new_deque.append(item)
-                if not new_deque:
-                    del self.entries[timestamp]
-                    del self.queue[i]
-                else:
-                    self.queue[i] = (timestamp, new_deque)
-            # not sure if the heap invariant maintained here, though
-            # if not, uncomment:
-            # heapify(self.queue)
-            self.removed = set()
+        with timer.Context("event_queue"):
+            self.removed.add(item)
+            if len(self.removed) > 5:
+                # to remove some elements safely from list while iterating over it and without copying the list,
+                # we iterate backwards:
+                for i in range(len(self.queue)-1, -1, -1):
+                    queue_entry = self.queue[i]
+                    timestamp, old_deque = queue_entry
+                    new_deque: Deque[Item] = deque([])
+                    for item in old_deque:
+                        if item not in self.removed:
+                            new_deque.append(item)
+                    if not new_deque:
+                        del self.entries[timestamp]
+                        del self.queue[i]
+                    else:
+                        self.queue[i] = (timestamp, new_deque)
+                # not sure if the heap invariant maintained here, though
+                # if not, uncomment:
+                # heapify(self.queue)
+                self.removed = set()
 
     # Raises exception if called on empty queue
     def pop(self) -> Tuple[Timestamp, Item]:
-        while True:
-            timestamp, d = self.queue[0]
+        with timer.Context("event_queue"):
             while True:
-                item = d.popleft()
-                if not d: # deque empty - get rid of entry
-                    del self.entries[timestamp]
-                    heappop(self.queue)
-                if item not in self.removed:
-                    return (timestamp, item)
-                else:
-                    self.removed.remove(item)
+                timestamp, d = self.queue[0]
+                while True:
+                    item = d.popleft()
+                    if not d: # deque empty - get rid of entry
+                        del self.entries[timestamp]
+                        heappop(self.queue)
+                    if item not in self.removed:
+                        return (timestamp, item)
+                    else:
+                        self.removed.remove(item)
 
     # Safe to call on empty queue
     # Safe to call other methods on the queue while the returned generator exists
     def due(self, timestamp: Timestamp) -> Generator[Tuple[Timestamp, Item], None, None]:
-        while len(self.queue) and self.queue[0][0] <= timestamp:
+        while len(self.queue) and (timestamp == None or self.queue[0][0] <= timestamp):
             yield self.pop()

+ 1 - 0
src/sccd/statechart/dynamic/memory_snapshot.py

@@ -2,6 +2,7 @@ from sccd.action_lang.dynamic.memory import *
 from sccd.util import timer
 
 class MemoryPartialSnapshot(MemoryInterface):
+  __slots__ = ["description", "memory", "read_only", "frame", "actual", "snapshot", "trans_dirty", "round_dirty"]
 
   def __init__(self, description: str, memory: Memory, read_only: bool = False):
     self.description = description

+ 26 - 30
src/sccd/statechart/dynamic/round.py

@@ -105,18 +105,17 @@ 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
-            for callback in self.callbacks:
-                callback()
-            # rotate enabled events
-            self.remainder_events = self.next_events
-            self.next_events = []
-            print_debug("completed "+self.name)
-        timer.stop("round: %s" % self.name)
-        return (changed, stable)
+        with timer.Context("round: %s" % self.name):
+            changed, stable = self._run(forbidden_arenas)
+            if changed:
+                # notify round observers
+                for callback in self.callbacks:
+                    callback()
+                # rotate enabled events
+                self.remainder_events = self.next_events
+                self.next_events = []
+                print_debug("completed "+self.name)
+            return (changed, stable)
 
     @abstractmethod
     def _run(self, forbidden_arenas: Bitmap) -> RoundResult:
@@ -223,21 +222,20 @@ 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")
+            with timer.Context("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)
 
             candidates = self.generator.generate(self.state, enabled_events, forbidden_arenas |  extra_forbidden)
 
             if DEBUG:
                 candidates = list(candidates) # convert generator to list (gotta do this, otherwise the generator will be all used up by our debug printing
                 if candidates:
-                    print_debug("")
+                    print()
                     if enabled_events:
-                        print_debug("events: " + str(enabled_events))
-                    print_debug("candidates: " + ",  ".join(str(t) for t in candidates))
+                        print("events: " + str(enabled_events))
+                    print("candidates: " + ",  ".join(str(t) for t in candidates))
                 candidates = iter(candidates)
 
             return candidates
@@ -245,10 +243,9 @@ class SmallStep(Round):
         arenas = Bitmap()
         stable_arenas = Bitmap()
 
-        timer.start("candidate generation")
-        candidates = get_candidates(0)
-        t = next(candidates, None)
-        timer.stop("candidate generation")
+        with timer.Context("candidate generation"):
+            candidates = get_candidates(0)
+            t = next(candidates, None)
         while t:
             arena = t.opt.arena_bitmap
             if not (arenas & arena):
@@ -263,13 +260,12 @@ class SmallStep(Round):
 
                 # need to re-generate candidates after firing transition
                 # because possibly the set of current events has changed
-                timer.start("candidate generation")
-                candidates = get_candidates(extra_forbidden=arenas)
+                with timer.Context("candidate generation"):
+                    candidates = get_candidates(extra_forbidden=arenas)
+                    t = next(candidates, None)
             else:
-                timer.start("candidate generation")
-
-            t = next(candidates, None)
-            timer.stop("candidate generation")
+                with timer.Context("candidate generation"):
+                    t = next(candidates, None)
 
         return (arenas, stable_arenas)
 

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

@@ -57,58 +57,49 @@ class StatechartExecution:
     # events: list SORTED by event id
     def fire_transition(self, events: List[Event], t: Transition):
         try:
-            # print("arena is:", t.opt.arena)
-            timer.start("transition")
-
-            # Sequence of exit states is the intersection between set of current states and the arena's descendants.
-            timer.start("exit set")
-            exit_ids = self.configuration & t.opt.arena.opt.descendants
-            exit_set = self._ids_to_states(bm_reverse_items(exit_ids))
-            timer.stop("exit set")
-
-
-            timer.start("enter set")
-            # Sequence of enter states is more complex but has for a large part already been computed statically.
-            enter_ids = t.opt.enter_states_static | reduce(lambda x,y: x|y, (self.history_values[s.history_id] for s in t.opt.enter_states_dynamic), Bitmap())
-            enter_set = self._ids_to_states(bm_items(enter_ids))
-            timer.stop("enter set")
-
-            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:
-                print_debug(termcolor.colored('  EXIT %s' % s.opt.full_name, 'green'))
-                # remember which state(s) we were in if a history state is present
-                for h, mask in s.opt.history:
-                    self.history_values[h.history_id] = exit_ids & mask
-                self._perform_actions(ctx, s.exit)
-                self.configuration &= ~s.opt.state_id_bitmap
-            timer.stop("exit states")
-
-            # 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...
-            for s in enter_set:
-                print_debug(termcolor.colored('  ENTER %s' % s.opt.full_name, 'green'))
-                self.configuration |= s.opt.state_id_bitmap
-                self._perform_actions(ctx, s.enter)
-                self._start_timers(s.opt.after_triggers)
-            timer.stop("enter states")
-
-            self.rhs_memory.flush_transition()
-
-            timer.stop("transition")
+            with timer.Context("transition"):
+                # Sequence of exit states is the intersection between set of current states and the arena's descendants.
+                with timer.Context("exit set"):
+                    exit_ids = self.configuration & t.opt.arena.opt.descendants
+                    exit_set = self._ids_to_states(bm_reverse_items(exit_ids))
+
+
+                with timer.Context("enter set"):
+                    # Sequence of enter states is more complex but has for a large part already been computed statically.
+                    enter_ids = t.opt.enter_states_static | reduce(lambda x,y: x|y, (self.history_values[s.history_id] for s in t.opt.enter_states_dynamic), Bitmap())
+                    enter_set = self._ids_to_states(bm_items(enter_ids))
+
+                ctx = EvalContext(current_state=self, events=events, memory=self.rhs_memory)
+
+                print_debug("fire " + str(t))
+
+                with timer.Context("exit states"):
+                    # exit states...
+                    for s in exit_set:
+                        print_debug(termcolor.colored('  EXIT %s' % s.opt.full_name, 'green'))
+                        # remember which state(s) we were in if a history state is present
+                        for h, mask in s.opt.history:
+                            self.history_values[h.history_id] = exit_ids & mask
+                        self._perform_actions(ctx, s.exit)
+                        self.configuration &= ~s.opt.state_id_bitmap
+
+                # execute transition action(s)
+                with timer.Context("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()
+
+                with timer.Context("enter states"):
+                    # enter states...
+                    for s in enter_set:
+                        print_debug(termcolor.colored('  ENTER %s' % s.opt.full_name, 'green'))
+                        self.configuration |= s.opt.state_id_bitmap
+                        self._perform_actions(ctx, s.enter)
+                        self._start_timers(s.opt.after_triggers)
+
+                self.rhs_memory.flush_transition()
 
             # input(">")
 

+ 2 - 0
src/sccd/statechart/static/statechart.py

@@ -84,6 +84,8 @@ class SemanticConfiguration:
 
 @dataclass
 class Statechart:
+  __slots__ = ["semantics", "scope", "datamodel", "events", "internal_events", "inport_events", "event_outport", "tree"]
+  
   semantics: SemanticConfiguration
 
   scope: Scope

+ 139 - 142
src/sccd/statechart/static/tree.py

@@ -246,149 +246,146 @@ def states_to_bitmap(state_list: List[State]) -> Bitmap:
     return reduce(lambda x,y: x|y, (s.opt.state_id_bitmap for s in state_list), Bitmap())
 
 def optimize_tree(root: State) -> StateTree:
-    timer.start("optimize tree")
-
-    transition_list = []
-    after_triggers = []
-    history_states = []
-    def init_opt():
-        next_id = 0
-        def f(state: State, _=None):
-            state.opt = StateOptimization()
-
-            nonlocal next_id
-            state.opt.state_id = next_id
-            state.opt.state_id_bitmap = bit(next_id)
-            next_id += 1
-
-            for t in state.transitions:
-                transition_list.append(t)
-                if t.trigger and isinstance(t.trigger, AfterTrigger):
-                    state.opt.after_triggers.append(t.trigger)
-                    after_triggers.append(t.trigger)
-
-            if isinstance(state, HistoryState):
-                state.history_id = len(history_states)
-                history_states.append(state)
-
-        return f
-
-    def assign_full_name(state: State, parent_full_name: str = ""):
-        if state is root:
-            full_name = '/'
-        elif state.parent is root:
-            full_name = '/' + state.short_name
-        else:
-            full_name = parent_full_name + '/' + state.short_name
-        state.opt.full_name = full_name
-        return full_name
-
-    state_dict = {}
-    state_list = []
-    stable_bitmap = Bitmap()
-    def add_to_list(state: State ,_=None):
-        nonlocal stable_bitmap
-        state_dict[state.opt.full_name] = state
-        state_list.append(state)
-        if state.stable:
-            stable_bitmap |= state.opt.state_id_bitmap
-
-    def set_ancestors(state: State, ancestors=[]):
-        state.opt.ancestors = states_to_bitmap(ancestors)
-        return ancestors + [state]
-
-    def set_descendants(state: State, children_descendants):
-        descendants = reduce(lambda x,y: x|y, children_descendants, Bitmap())
-        state.opt.descendants = descendants
-        return state.opt.state_id_bitmap | descendants
-
-    def set_static_target_states(state: State, _):
-        if isinstance(state, ParallelState):
-            state.opt.ts_static = reduce(lambda x,y: x|y, (s.opt.ts_static for s in state.children), state.opt.state_id_bitmap)
-            state.opt.ts_dynamic = list(itertools.chain.from_iterable(c.opt.ts_dynamic for c in state.children if not isinstance(c, HistoryState)))
-        elif isinstance(state, HistoryState):
-            state.opt.ts_static = Bitmap()
-            state.opt.ts_dynamic = [state]
-        else: # "regular" state:
-            if state.default_state:
-                state.opt.ts_static = state.opt.state_id_bitmap | state.default_state.opt.ts_static
-                state.opt.ts_dynamic = state.default_state.opt.ts_dynamic
-            else:
-                state.opt.ts_static = state.opt.state_id_bitmap
-                state.opt.ts_dynamic = []
-
-    def add_history(state: State, _= None):
-        for c in state.children:
-            if isinstance(c, HistoryState):
-                state.opt.history.append((c, c.history_mask()))
-
-    def freeze(state: State, _=None):
-        state.freeze()
-        state.opt.freeze()
-
-    visit_tree(root, lambda s: s.children,
-        before_children=[
-            init_opt(),
-            assign_full_name,
-            add_to_list,
-            set_ancestors,
-        ],
-        after_children=[
-            set_descendants,
-            add_history,
-            set_static_target_states,
-            freeze,
-        ])
-
-
-    for t in transition_list:
-        # Arena can be computed statically. First computer Lowest-common ancestor:
-        # Intersection between source & target ancestors, last member in depth-first sorted state list.
-        lca_id = bm_highest_bit(t.source.opt.ancestors & t.targets[0].opt.ancestors)
-        lca = state_list[lca_id]
-        arena = lca
-        # Arena must be an Or-state:
-        while isinstance(arena, (ParallelState, HistoryState)):
-            arena = arena.parent
-
-        # Exit states can be efficiently computed at runtime based on the set of current states.
-        # Enter states are more complex but luckily, can be computed *partially* statically:
-
-        # As a start, we calculate the enter path:
-        # The enter path is the path from arena to the target state (not including the arena state itself).
-        # Enter path is the intersection between:
-        #   1) the transition's target and its ancestors, and
-        #   2) the arena's descendants
-        enter_path = (t.targets[0].opt.state_id_bitmap | t.targets[0].opt.ancestors) & arena.opt.descendants
-        # All states on the enter path will be entered, but on the enter path, there may also be AND-states whose children are not on the enter path, but should also be entered.
-        enter_path_iter = bm_items(enter_path)
-        state_id = next(enter_path_iter, None)
-        enter_states_static = Bitmap()
-        enter_states_dynamic = []
-        while state_id is not None:
-            state = state_list[state_id]
-            next_state_id = next(enter_path_iter, None)
-            if next_state_id:
-                # an intermediate state on the path from arena to target
-                next_state = state_list[next_state_id]
-                static, dynamic = state._static_additional_target_states(next_state)
-                enter_states_static |= static
-                enter_states_dynamic += dynamic
+    with timer.Context("optimize tree"):
+
+        transition_list = []
+        after_triggers = []
+        history_states = []
+        def init_opt():
+            next_id = 0
+            def f(state: State, _=None):
+                state.opt = StateOptimization()
+
+                nonlocal next_id
+                state.opt.state_id = next_id
+                state.opt.state_id_bitmap = bit(next_id)
+                next_id += 1
+
+                for t in state.transitions:
+                    transition_list.append(t)
+                    if t.trigger and isinstance(t.trigger, AfterTrigger):
+                        state.opt.after_triggers.append(t.trigger)
+                        after_triggers.append(t.trigger)
+
+                if isinstance(state, HistoryState):
+                    state.history_id = len(history_states)
+                    history_states.append(state)
+
+            return f
+
+        def assign_full_name(state: State, parent_full_name: str = ""):
+            if state is root:
+                full_name = '/'
+            elif state.parent is root:
+                full_name = '/' + state.short_name
             else:
-                # the actual target of the transition
-                enter_states_static |= state.opt.ts_static
-                enter_states_dynamic += state.opt.ts_dynamic
-            state_id = next_state_id
-
-        t.opt = TransitionOptimization(
-            arena=arena,
-            arena_bitmap=arena.opt.descendants | arena.opt.state_id_bitmap,
-            enter_states_static=enter_states_static,
-            enter_states_dynamic=enter_states_dynamic)
-
-        t.freeze()
+                full_name = parent_full_name + '/' + state.short_name
+            state.opt.full_name = full_name
+            return full_name
+
+        state_dict = {}
+        state_list = []
+        stable_bitmap = Bitmap()
+        def add_to_list(state: State ,_=None):
+            nonlocal stable_bitmap
+            state_dict[state.opt.full_name] = state
+            state_list.append(state)
+            if state.stable:
+                stable_bitmap |= state.opt.state_id_bitmap
+
+        def set_ancestors(state: State, ancestors=[]):
+            state.opt.ancestors = states_to_bitmap(ancestors)
+            return ancestors + [state]
+
+        def set_descendants(state: State, children_descendants):
+            descendants = reduce(lambda x,y: x|y, children_descendants, Bitmap())
+            state.opt.descendants = descendants
+            return state.opt.state_id_bitmap | descendants
+
+        def set_static_target_states(state: State, _):
+            if isinstance(state, ParallelState):
+                state.opt.ts_static = reduce(lambda x,y: x|y, (s.opt.ts_static for s in state.children), state.opt.state_id_bitmap)
+                state.opt.ts_dynamic = list(itertools.chain.from_iterable(c.opt.ts_dynamic for c in state.children if not isinstance(c, HistoryState)))
+            elif isinstance(state, HistoryState):
+                state.opt.ts_static = Bitmap()
+                state.opt.ts_dynamic = [state]
+            else: # "regular" state:
+                if state.default_state:
+                    state.opt.ts_static = state.opt.state_id_bitmap | state.default_state.opt.ts_static
+                    state.opt.ts_dynamic = state.default_state.opt.ts_dynamic
+                else:
+                    state.opt.ts_static = state.opt.state_id_bitmap
+                    state.opt.ts_dynamic = []
+
+        def add_history(state: State, _= None):
+            for c in state.children:
+                if isinstance(c, HistoryState):
+                    state.opt.history.append((c, c.history_mask()))
+
+        def freeze(state: State, _=None):
+            state.freeze()
+            state.opt.freeze()
+
+        visit_tree(root, lambda s: s.children,
+            before_children=[
+                init_opt(),
+                assign_full_name,
+                add_to_list,
+                set_ancestors,
+            ],
+            after_children=[
+                set_descendants,
+                add_history,
+                set_static_target_states,
+                freeze,
+            ])
+
+
+        for t in transition_list:
+            # Arena can be computed statically. First computer Lowest-common ancestor:
+            # Intersection between source & target ancestors, last member in depth-first sorted state list.
+            lca_id = bm_highest_bit(t.source.opt.ancestors & t.targets[0].opt.ancestors)
+            lca = state_list[lca_id]
+            arena = lca
+            # Arena must be an Or-state:
+            while isinstance(arena, (ParallelState, HistoryState)):
+                arena = arena.parent
+
+            # Exit states can be efficiently computed at runtime based on the set of current states.
+            # Enter states are more complex but luckily, can be computed *partially* statically:
+
+            # As a start, we calculate the enter path:
+            # The enter path is the path from arena to the target state (not including the arena state itself).
+            # Enter path is the intersection between:
+            #   1) the transition's target and its ancestors, and
+            #   2) the arena's descendants
+            enter_path = (t.targets[0].opt.state_id_bitmap | t.targets[0].opt.ancestors) & arena.opt.descendants
+            # All states on the enter path will be entered, but on the enter path, there may also be AND-states whose children are not on the enter path, but should also be entered.
+            enter_path_iter = bm_items(enter_path)
+            state_id = next(enter_path_iter, None)
+            enter_states_static = Bitmap()
+            enter_states_dynamic = []
+            while state_id is not None:
+                state = state_list[state_id]
+                next_state_id = next(enter_path_iter, None)
+                if next_state_id:
+                    # an intermediate state on the path from arena to target
+                    next_state = state_list[next_state_id]
+                    static, dynamic = state._static_additional_target_states(next_state)
+                    enter_states_static |= static
+                    enter_states_dynamic += dynamic
+                else:
+                    # the actual target of the transition
+                    enter_states_static |= state.opt.ts_static
+                    enter_states_dynamic += state.opt.ts_dynamic
+                state_id = next_state_id
 
+            t.opt = TransitionOptimization(
+                arena=arena,
+                arena_bitmap=arena.opt.descendants | arena.opt.state_id_bitmap,
+                enter_states_static=enter_states_static,
+                enter_states_dynamic=enter_states_dynamic)
 
-    timer.stop("optimize tree")
+            t.freeze()
 
-    return StateTree(root, transition_list, state_list, state_dict, after_triggers, stable_bitmap, history_states)
+        return StateTree(root, transition_list, state_list, state_dict, after_triggers, stable_bitmap, history_states)

+ 2 - 3
src/sccd/test/run.py

@@ -32,9 +32,8 @@ class Test(unittest.TestCase):
     sc_rules = functools.partial(statechart_parser_rules, path=path)
     test_rules = test_parser_rules(sc_rules)
     try:
-      timer.start("parse test")
-      test_variants = parse_f(self.src, test_rules)
-      timer.stop("parse test")
+      with timer.Context("parse test"):
+        test_variants = parse_f(self.src, test_rules)
     except Exception as e:
       print_debug(e)
       raise e

+ 19 - 16
src/sccd/util/timer.py

@@ -10,20 +10,20 @@ if TIMINGS:
   import atexit
   import collections
 
-  timers = {}
   timings = {}
   counts = collections.Counter()
-    
-  def start(what):
-    timers[what] = time.perf_counter()
 
-  def stop(what):
-    end = time.perf_counter()
-    begin = timers[what]
-    duration = end - begin
-    old_val = timings.setdefault(what, 0)
-    timings[what] = old_val + duration
-    counts[what] += 1
+  class Context:
+    __slots__ = ["what", "started"]
+    def __init__(self, what):
+      self.what = what
+    def __enter__(self):
+      self.started = time.perf_counter()
+    def __exit__(self, type, value, traceback):
+      duration = time.perf_counter() - self.started
+      old_val = timings.setdefault(self.what, 0)
+      timings[self.what] = old_val + duration
+      counts[self.what] += 1
 
   def _print_stats():
       print("\ntimings:")
@@ -33,8 +33,11 @@ if TIMINGS:
   atexit.register(_print_stats)
 
 else:
-  def start(what):
-    pass
-
-  def stop(what):
-    pass
+  class Context:
+    __slots__ = []
+    def __init__(self, what):
+      pass
+    def __enter__(self):
+      pass
+    def __exit__(self, type, value, traceback):
+      pass