Reflection#
Thoughts On Reflection#
The miros framework has several different mechanisms to view how your design behaves. This is extremely important, since with the compacting of complexity into behavioral maps it is very easy to lose track of what your system actually does. In fact, it is inevitable that for any moderately ambitious design, you will not be able to track the entirety of its behavior in your head. With any addition of state or event management there is an exponential increase in the number of possible stories your chart can tell. This is the power and the curse of a statechart.
The state of your chart might change as it responds to an event, so you might want to reflect upon the current state.
You might want to view what events caused what state transitions from a high level.
You may need to view the state dynamics in fine detail..
The tools needed to view these dynamics are embedded into the miros framework.
Another issue that is faced is how to explain your design to someone else. A statechart diagram is not helpful when you are talking to a customer or someone within your team who is not versed in Harel formalism. For this reason, it is good to talk about a specific behavior, not all of the behaviors at once. A statechart diagram might make sense to you but it can psychologically shutdown a teammate. miros provides a way to translate how your chart responds to a particular event into a different type of diagram, a sequence diagram. Anyone can understand a sequence diagram. By referencing something that is easy to understand you can reduce the transaction costs in your organization; everyone should be able to participate in the conversation.
With the smallest adjustment of a statechart, all of your sequence diagrams can become moot. For this reason the sequence diagrams can be generated programatically from the trace output. This means that you don’t have to waste expensive engineering time on documentation that will be thrown away as you build out your system. This is also important because your engineers won’t become reluctant to change a design to avoid hours and hours of grinding work.
I would recommend that if you use statecharts, you avoid using non-text based documentation systems. If you use Word to make pretty diagrams, the steps to manually change your documents will amplify cost through your organization. Whereas if you use markdown, LaTeX, HTML, anything that can quickly be constructed without a lot of user intervention, it is easy and cheap to change your design descriptions. If you want to buy a Microsoft product, buy Visio, since you can make beautiful diagrams with links. It makes sense to put a lot of time and attention into the map. To save money, you can use umlet instead.
Suppose you are a documentation genius. This doesn’t mean that anyone wants to read your work. With the miros reflection features an engineer can run an experiment to see how the system actually behaves, rather than digging into dreary specifications. The chart will quickly become the specification, this is the point of Harel formalism. The specification documents can be thought of as temporary work orders that adjust the global specification, the chart itself.
As your system gets bigger and more complicated, it is very important to lock it down with tests. But the more specific the test, the more tightly coupled it is to your design. Any change will break it. To fix things an engineer could isolate the test, reflect upon the behavior, determine if it is correct by carefully thinking about it, then overwrite the test with a copy of the reflection. It has to be very easy and cheap to isolate and update your tests, otherwise your organization will lose this discipline and you will lose control of your design.
What State Am I In?#
To see what state your chart is in:
# state name as a string
chart.state_name
# state as a function
chart.state_fn
Note
This will only work if you have wrapped your statemethod with a @spy_on
decorator or if you have constructed your statechart with the Factory class.
What Signals Do I Have?#
A signal is the name that can be given to an event. To get access to your signals:
from miros import signals
The signals object is provided by a singleton of the SignalSource class, which
is just an OrderedDictionary with a __getattr__
method to make the syntax
easier to use.
This basically means that you can think of the signals object as being a dict that is shared across your whole program.
To see your signals, you just reflect upon it like you would with any other Python dictionary:
# To see your signal names:
signal_names = signals.keys()
# To see your signal numbers:
signal_numbers = signal.values()
# To output your names and number:
for signal_name, signal_number in signals.items():
print(signal_name, signal_number)
# same output with some formatting
max_name_len = len(max(signals, key=len))
max_number_len = len(str(max(signals.values(), key=int)))
for signal_name, signal_number in signals.items():
print("{1: <{0}} {2:{3}}".format(max_name_len,
signal_name,
signal_number,
max_number_len)) # output below ->
# ENTRY_SIGNAL 1
# EXIT_SIGNAL 2
# INIT_SIGNAL 3
# REFLECTION_SIGNAL 4
# SEARCH_FOR_SUPER_SIGNAL 5
# ..
To compare a received event against a signal, compare the signal numbers:
def some_example_state(chart, e):
status = return_status.UNHANLDED
if(e.signal == signals.ENTRY_SIGNAL):
# do something
It you wanted to read an event’s signals name as a string, you would call the
signal_name
method of the Event class:
def some_example_state(chart, e):
status = return_status.UNHANLDED
print(e.signal_name) # "ENTRY_SIGNAL"
If you have a signal number and you want to determine its name:
signal_name = signals.name_for_signal(1) # ENTRY_SIGNAL
signal_name = signals.name_for_signal(signals.ENTRY_SIGNAL) # ENTRY_SIGNAL
A High Level Description of The Behavior#
If you would like to see what your active object was doing from a very high level, you can look at its trace instrumentation. The trace will only create a log item if a state transition has occurred. Each line in the trace will contain:
A datetime stamp between square brackets
The active object name, between square brackets
The event that caused the transition, its signal number and its payload
The starting state
The ending state
If you haven’t named your active object, a unique identifier is given to it, and the first 5 characters of this unique identifier will be used in the trace. The reason that an identifier is given to it is so that the trace outputs, from multiple active objects, can be distinguished from one another.
Suppose you have built the tazor active object described in the second example. Suppose you named this active object tazor: To see the trace you would type:
tazor.trace()
This would output the following trace:
[2017-11-06 08:34:28.268873] [75c8c] e->start_at() top->arming
[2017-11-06 08:34:26.312241] [75c8c] e->BATTERY_CHARGE() arming->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BATTERY_CHARGE() armed->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BATTERY_CHARGE() armed->armed
Notice that on line one, the signal is called start_at. There is no signal called start_at, here the trace is actually using the method name start_at to indicate how the chart was started.
A trace can be used with the sequence project to generate a sequence diagram, more about that is described here.
An Extremely Detailed View of the Behavior#
If you need a very detailed description of your system’s behavior you will want to use the spy instrumentation. The spy outputs:
All of the internal activity that is run by the event processor
How your chart reacts to the events it has received
Information about what happened between each of the rtc activities.
If a signal was hooked by a state
If and when a signal was deferred
If and when a signal was recalled
The number of events awaiting immediate processing after a specific rtc activity.
The number of events which have had their processing deferred.
If you don’t understand what these terms mean, read the simple posting example, since it introduces all of these ideas.
To access the full spy:
# import a pretty printer (like print)
from miros import pp
# .. state charts and other code here
# Assuming your active object is called `ao`
# we use the pretty printer to write the
# full spy log to the terminal
pp(ao.spy())
This might output:
['START',
'SEARCH_FOR_SUPER_SIGNAL:middle',
'SEARCH_FOR_SUPER_SIGNAL:outer',
'ENTRY_SIGNAL:outer',
'ENTRY_SIGNAL:middle',
'INIT_SIGNAL:middle',
'<- Queued:(0) Deferred:(0)',
'A:middle',
'SEARCH_FOR_SUPER_SIGNAL:inner',
'ENTRY_SIGNAL:inner',
'POST_DEFERRED:B',
'INIT_SIGNAL:inner',
'<- Queued:(0) Deferred:(1)',
'A:inner',
'A:middle',
'EXIT_SIGNAL:inner',
'SEARCH_FOR_SUPER_SIGNAL:inner',
'ENTRY_SIGNAL:inner',
'POST_DEFERRED:B',
'INIT_SIGNAL:inner',
'<- Queued:(0) Deferred:(2)',
'A:inner',
'A:middle',
'EXIT_SIGNAL:inner',
'SEARCH_FOR_SUPER_SIGNAL:inner',
'ENTRY_SIGNAL:inner',
'POST_DEFERRED:B',
'INIT_SIGNAL:inner',
'<- Queued:(0) Deferred:(3)',
'D:inner',
'D:middle',
'D:outer',
'POST_FIFO:B',
'D:outer:HOOK',
'<- Queued:(1) Deferred:(2)',
'B:inner',
'B:middle',
'B:outer',
'EXIT_SIGNAL:inner',
'EXIT_SIGNAL:middle',
'EXIT_SIGNAL:outer',
'ENTRY_SIGNAL:outer',
'POST_FIFO:B',
'RECALL:B',
'INIT_SIGNAL:outer',
'<- Queued:(1) Deferred:(1)',
'B:outer',
'EXIT_SIGNAL:outer',
'ENTRY_SIGNAL:outer',
'POST_FIFO:B',
'RECALL:B',
'INIT_SIGNAL:outer',
'<- Queued:(1) Deferred:(0)',
'B:outer',
'EXIT_SIGNAL:outer',
'ENTRY_SIGNAL:outer',
'INIT_SIGNAL:outer',
'<- Queued:(0) Deferred:(0)']
If you would like to understand in detail how to read this log, and how it might have occurred, reference the example from which it came.
The spy log is a ring buffer that contains 500 spots. This is so your system can run forever without using an infinite amount of memory. Once the internal spy log has run out of room, it will shift out the old data and write the new data at the tail end of its log.
How to Test Your Design Using Reflection#
Testing with the Trace Output#
If you would like to sketch out the high level behavior of your statechart using a trace output as the target for a regression test, you would:
Run your program and print your trace to the output.
Copy this trace as the target behavior of your test.
Run this trace target and the future output of the statechart trace through the stripped context manager to remove the date timestamp information.
Compare your target with the results.
This should become clear with an example.
Consider a statechart that outputted the following at the point when you were working on it.
[2017-11-05 21:31:56.098526] [tazor] e->start_at() top->arming
[2017-11-05 21:31:56.200047] [tazor] e->BATTERY_CHARGE() arming->armed
[2017-11-05 21:31:56.300974] [tazor] e->BATTERY_CHARGE() armed->armed
[2017-11-05 21:31:56.401682] [tazor] e->BATTERY_CHARGE() armed->armed
You might have gotten this output with the following code:
print(tazor.trace())
It does a decent job describing what we want, but it has timestamps. With the stripped context manager we can turn the above into something that would look like this:
[tazor] e->start_at() top->arming
[tazor] e->BATTERY_CHARGE() arming->armed
[tazor] e->BATTERY_CHARGE() armed->armed
[tazor] e->BATTERY_CHARGE() armed->armed
That is something that shouldn’t change over time, it looks like something we could use as a test specification. The only problem is that when we run the code in the future and generate a new trace we get a trace with a pre-pended date timestamp. We can get around this issue like this:
1from miros import stripped
2
3target = \
4'''[2017-11-05 21:31:56.098526] [tazor] e->start_at() top->arming
5 [2017-11-05 21:31:56.200047] [tazor] e->BATTERY_CHARGE() arming->armed
6 [2017-11-05 21:31:56.300974] [tazor] e->BATTERY_CHARGE() armed->armed
7 [2017-11-05 21:31:56.401682] [tazor] e->BATTERY_CHARGE() armed->armed
8'''
9with stripped(target) as stripped_target, \
10 stripped(tazor.trace()) as stripped_trace_result:
11
12 for target, result in zip(stripped_target, stripped_trace_result):
13 assert(target == result)
On line 1 we import the stripped context manager.
On lines 3-7 we define the target as just being the trace that we copied when we were first designing our statechart.
On line 9, we map this target to the stripped_target
which contains the
same string with the date timestamps removed.
On line 10, we use the same stripped context manager to strip our tazor active
object’s trace output of its date timestamp information and place the result
into the stripped_trace_result
variable.
Lines 12-13 are for testing each line of our output against our target.
If our design changes, it is easy to update the test, we just copy the new trace of of new design into the target string and everything should be peachy.
Testing with the Spy Output#
A trace does not tell the full story about what your system is doing. For instance it is blind to hooks, deferred events and many other things that might happen in the dynamics of your active object. If you need to look at the exact behavior of your system, you can:
Run your program and print your spy to the output.
Copy the spy as your target behavior.
Compare the target with the results.
Here is an example:
1# pp(tazor.spy())
2# import pdb.set_trace()
3assert(tazor.spy() ==
4 ['START',
5 'SEARCH_FOR_SUPER_SIGNAL:arming',
6 'SEARCH_FOR_SUPER_SIGNAL:tazor_operating',
7 'ENTRY_SIGNAL:tazor_operating',
8 'ENTRY_SIGNAL:arming',
9 'INIT_SIGNAL:arming',
10 '<- Queued:(0) Deferred:(0)',
11 'BATTERY_CHARGE:arming',
12 'SEARCH_FOR_SUPER_SIGNAL:armed',
13 'ENTRY_SIGNAL:armed',
14 'POST_DEFERRED:CAPACITOR_CHARGE',
15 'INIT_SIGNAL:armed',
16 '<- Queued:(0) Deferred:(1)'])
On line 1 we have a commented pretty print command ready to go for when we need to rebuild our test specification. When the test fails in the future, which it will because this is a tightly coupled test, we will uncomment lines 1-2 then re-run the test.
This will drop us into a debugging session just after our next spy output has been printed to the screen. At this point we would carefully determine if it actually describes the new behavior we are looking for. If it wasn’t, we would fix the issue, otherwise we over-write lines 4-16 with this new specification.
We would re-comment lines 1-2 and re-run our tests.
Live Output From Your Chart#
ao1 = ActiveObject()
ao1.live_trace = True # ao1 will output its trace live to the terminal
ao1.live_spy = True # ao1 will output its spy live to the terminal
How to Explain your Design to Others#
To create effective, yet inexpensive documentation, you can first obtain a trace of your system, then use it to generate a sequence diagram, with sequence.
Without a lot of effort, you can configure your text editor to write these pictures for you. When I select this in my editor:
[2017-11-06 08:34:28.268873] [75c8c] e->start_at() top->arming
[2017-11-06 08:34:26.312241] [75c8c] e->BC() arming->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BC() armed->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BC() armed->armed
Then press <ctrl-T>, it becomes this:
[ Chart: 75c8c ] (?)
top arming armed
+-tart_at()->| |
| (?) | |
| +---BC()---->|
| | (?) |
| | +
| | \ (?)
| | BC()
| | /
| | <
Then I would manually replace the question marks with numbers, so that I could explained each event by referencing its number. Since my diagram is in ASCII, I could place it in my code comments.
sequence also works with interleaving trace outputs that would come from two different interacting active objects:
Suppose you got this from your terminal while testing two different statecharts:
[2017-11-06 08:34:28.268873] [75c8c] e->start_at() top->arming
[2017-11-06 08:34:28.268873] [95a8c] e->start_at() top->arming
[2017-11-06 08:34:26.312241] [75c8c] e->BC() arming->armed
[2017-11-06 08:34:26.312241] [95a8c] e->OTHER() arming->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BC() armed->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BC() armed->armed
[2017-11-06 08:34:26.312241] [95a8c] e->BC() armed->armed
[2017-11-06 08:34:26.312241] [95a8c] e->BC() armed->armed
By running it through sequence we would see:
[ Chart: 75c8c ] (?)
top arming armed
+-tart_at()->| |
| (?) | |
| +---BC()---->|
| | (?) |
| | +
| | \ (?)
| | BC()
| | /
| | <
[ Chart: 95a8c ] (?)
top arming armed
+-tart_at()->| |
| (?) | |
| +--OTHER()-->|
| | (?) |
| | +
| | \ (?)
| | BC()
| | /
| | <
Now I’ll write some fake documentation to make a point, notice how I update the numbers in the diagram:
[ Chart: Unit 1 ]
top arming armed
+start_at()->| |
| (1) | |
| +---BC()---->|
| | (3) |
| | +
| | \ (5)
| | BC()
| | /
| | <
[ Chart: Unit 2 ]
top arming armed
+start_at()->| |
| (2) | |
| +--OTHER()-->|
| | (4) |
| | +
| | \ (6)
| | BC()
| | /
| | <
You can gang two tazors together to act as one tazor. The first arming event in your tazor network will also arm all of the other tazors, consider the diagram above to see this interaction.
Tazor labeled ‘Unit 1’ turns on in the arming state.
Tazor labeled ‘Unit 2’ turns on in the arming state.
Unit 1 begins a battery charge (BC) which will send a broadcast message to all other tazors in the network.
Unit 2 detects another tazor is beginning a battery charge, so it too begins its battery charge (OTHER)
…. and so on
If I changed the above design, it would be simple to adjust these diagrams and their description. Sequence diagrams are great for explaining small things, but they do break the DRY principle. You are effectively replicating your data by having these descriptions in your documentation. The source image is your state chart diagram. Give it a lot of attention, since it is actually your specification. The sequence diagrams are little throw away things, that can be used to assist in telling a very specific story about how your system behaves.
I’m probably not following the UML standard and I don’t care. The utility of the sequence diagram picture is in its simplicity.
I know that you can represent loops and object destructor’s using these diagrams, but why bother? It is easier to write a loop in the code than it is in a picture, so why not copy and paste the code onto the sequence diagram if you need to explain a loop?
If you would like to create sequence diagrams that are UML compliant, the umlet program supports these features.