Closed Blindfreddy closed 2 years ago
Hello @Blindfreddy,
some background information concerning this:
The behavior of get_graph
depends on whether you use pygraphviz
or the graphviz
backend. Usually get_graph
will only recreate a graph when the force_new
parameter is set to True
. This is the case when new states and transitions have been added. The graphviz
backend is rather simplistic and will not keep an editable state of the graph which means it basically regenerates a graph whenever get_graph
is called, independently of whether force_new
is set or not.
Caching sounds like a good idea. Changes to the graph are usually done via TransitionGraphSupport
so I'd use a machine that does not depend on it. For instance, I'd use a GraphMachine
for the creation of graphs and a custom Machine
during runtime. There is no need to tinker with GraphMachine
to create the graphs you need. You can access the BaseGraph
instance, edit it according to your liking and save the result:
from transitions import Machine
from transitions.extensions.diagrams import GraphMachine
class CachedGraphsMachine(Machine):
def __init__(self, *args, **kwargs):
self.cached_graphs = {}
super().__init__(*args, **kwargs)
def get_graph(self, model_state):
return self.cached_graphs[model_state]
states = ["A", "B", "C", "D"]
transitions = [
["go", ["A", "D"], "B"],
["do", "B", "C"],
["make", "B", "D"]
]
gm = GraphMachine(states=states, transitions=transitions, initial="A", auto_transitions=False, title="My Machine")
pm = CachedGraphsMachine(states=states, transitions=transitions, initial="A")
base_graph = gm.model_graphs[id(gm)]
for state in gm.states.values():
base_graph.reset_styling()
base_graph.set_node_style(state.name, "active")
# In case you also want to set the last transition
# base_graph.set_previous_transition("A", state.name)
# Note that you cannot just pass `AGraph` or the graphviz pendant to the cache since the object will
# change during looping.
pm.cached_graphs[state.name] = base_graph.get_graph().draw(None, prog='dot', format='png')
pm.go()
# write the buffered byte array to disk to have a look
with open("current_state.png", "wb") as f:
f.write(pm.get_graph(pm.state))
Thanks for your response, I think we can close this topic, now. Note, however that I made it work in a different way - perhaps that may be of interest to other users:
In my application, state_diagrams are accessed infrequently, so I chose a lazy caching approach tied to the machine.after_state_change
event. When it fires, I call the method create_state_diagram
, which adds the newly entered state's diagram to the cache in a separate thread, unless it already exists. That way, state changes are still practically immediate because only a simple check or kicking off the thread to generate the diagram are performed, and the associated state_diagram is then generated in the background. Retrieving a state_diagram is now practically immediate once generated. Only drawback is that the user gets an error if he retrieves the state_diagram whilst it is being generated. In that case, retrieving it again will succeed as soon as generating the diagram has completed.
Here is the code for anyone interested:
def create_state_diagram(self, *args, **kwargs):
"""
Callback on machine.after_state_change hence *args, **kwargs
Create state diagram for current state unless it already exists.
Implement lazy state diagram generation for fast retrieval, because
generating the diagram on demand is usually slow compared to the
expected system response time, whereas retrieving and transmitting it
is fast. Hence generate the diagram for each state when that state is
first entered. Do so in a separate thread, so that the state change is
not slowed down by the diagram generation. Store diagram in private
variable self._diagrams_map, which is accessed by property
state_diagram. On subsequent entries to same state, generating is
omitted.
"""
def gen_diag():
debug_log(self, msg + f"{current_thread()}")
try:
content = self._sm.get_graph().draw(None, format="png",
prog="dot")
d = {'name' : self.name,
'filename' : self.name + '_state_diagram.png',
'mimetype' : 'image/png',
'encoding' : 'base64',
'value' : b64encode(content).decode()}
self._diagrams_map[self.state] = d
debug_log(self, msg + "done.")
except Exception as e:
exc_log(self, msg + f"failed: {e}")
msg = info_log(self, f"create_state_diagram(unused args {args}, "
f"kwargs {kwargs}) in separate thread...")
if self.state in self._diagrams_map:
debug_log(self, msg + "omitted: already exists.")
else:
# Avoid multiple thread instances for the same state in the event
# that state changes to the same state are faster than the creation
# of the state diagram. Subsequent lookup to the state only checks
# if the key is present, not its value. If the state diagram for
# given state is accessed before the thread finishes, the user
# will get a None and hopefully retry. By that time the diagram
# should be finished and the thread replaces None with the actual
# diagram.
self._diagrams_map[self.state] = None
Thread(target=gen_diag,
name="create_state_diagram_" + self.state + "_thread",
daemon=True).start()
debug_log(self, msg + "done (thread started).")
Is your feature request related to a problem? Please describe. Currently, the
get_graph()
method always returns the diagram for the active state, and generating the diagram takes relatively long, making response times relatively large. To my knowledge, it is not possible to generate the diagram for another, inactive state as if that state were active.Describe the solution you'd like I'd like to be able to generate diagrams for any state as if a given state were active, irrespective of the actual active state at the time. This would effectively highlight the given state instead of the active state.
Additional context Background My application relies on very short response times. When the user requests the current state diagram, it is computed on the fly, but generating the state diagram is too slow.
Motivation To improve response times, I would like to pre-compute diagrams for all possible states when the application bootstraps and store the diagrams in a dictionary holding state and the pre-computed diagram:
{state : diagram}
. That way, when the user requests the current state diagram after bootstrapping the application, it is simply fetched from the dict and not computed.To be clear, the only difference between the pre-computed diagrams would be the highlighted active state.
Potential Solution In pseudo code it will be something like this:
then later on during runtime, instead of
diag = m.get_graph().draw()
<-- this takes timesimply do
diag = state_dict[current_state]
<-- this is much fasterSo a potential solution could be to add a parameter
state=None
toget_graph
(assuming we have control over it) which creates the state diagram as ifstate
were currently active. Ifstate
isNone
behavior would be as-is, otherwise create the diagram as ifstate
were active.