Skip to content

Commit

Permalink
Significant perf. improvement but breaks compat.
Browse files Browse the repository at this point in the history
* Modified decorator syntax to add a decorator around state enter/leave
  methods.  These methods must now be decorated with @state.enter and
  @state.leave.
* The @State, @state.enter, @state.leave, and @transition decorators now
  register their methods with the state instance called from the decorator.
  (This state is detected by inspecting the Python call stack).
* Modified the pysm translator to use the new decorators.
* Removed the _getlocals() and _getsomelocals() functions from decorator.py
  since they are no longer needed.
* Modified smTest.py and pysmTest.pysm to use the new enter/leave decorators
  and to no longer test the _getlocals() and _getsomelocals() utilities.

With these changes, the profiler reports SuckNDrop Toolglass instantiation
times decreasing from 5-6 seconds to about a tenth of a second.

git-svn-id: https://www.lri.fr/svn/in-situ/code/PythonStateMachines/trunk@1663 2173f847-0edd-4382-bdcc-49b71dc1bf57
  • Loading branch information
eaganj committed May 5, 2010
1 parent ef8def3 commit 9c837c4
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 160 deletions.
172 changes: 98 additions & 74 deletions StateMachines/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,69 +84,74 @@ def AnotherState(self)

# -------- utilities --------

# Functions to find out the locals of a function.
# Calls the function and uses tracing tools to find out.
# NOTE : this is both dangerous (the function could have side effects)
# and expensive: tracing is costly.
#
# Copied from http://wiki.python.org/moin/PythonDecoratorLibrary
# and modified by [mbl]:
# - use depth of original call to test that we're getting info from the right 'return'
# - use depth to avoid tracing nested calls (more efficient)
# - handle exceptions

def _getsomelocals(function, keys, *args):
"""Execute a function and return its locals whose names are in keys (private).
:Parameters:
- `function`: The function from which to extract the locals.
- `keys`: A tuple of names of locals to extract
- `args`: Arguments to be passed to `function` so as to extract its locals.
:return: The locals in `keys` with their values, as a dictionary.
:note: Extracting the locals requires executing the function and is fairly inefficient.
"""
func_locals = {'doc':function.__doc__}
def probeFunc(frame, event, arg):
if event == 'call' and len(inspect.stack()) > depth:
return None
elif event == 'return' and len(inspect.stack()) == depth:
locals = frame.f_locals
func_locals.update(dict((k,locals.get(k)) for k in keys))
sys.settrace(None)
elif event == 'exception' or event == 'c_exception':
sys.settrace(None)
return probeFunc
depth = len(inspect.stack())+2
sys.settrace(probeFunc)
function(*args)
return func_locals

def _getlocals(function, *args):
"""Execute a function and return its locals (private).
:Parameters:
- `function`: The function from which to extract the locals.
- `args`: Arguments to be passed to `function` so as to extract its locals.
:return: All the locals of `function` together with their values, as a dictionary.
:note: Extracting the locals requires executing the function and is fairly inefficient.
"""
func_locals = {'doc':function.__doc__}
def probeFunc(frame, event, arg):
if event == 'return' and len(inspect.stack()) == depth:
func_locals.update(frame.f_locals)
sys.settrace(None)
return probeFunc
depth = len(inspect.stack())+2
sys.settrace(probeFunc)
function(*args)
return func_locals
# # Functions to find out the locals of a function.
# # Calls the function and uses tracing tools to find out.
# # NOTE : this is both dangerous (the function could have side effects)
# # and expensive: tracing is costly.
# #
# # Copied from http://wiki.python.org/moin/PythonDecoratorLibrary
# # and modified by [mbl]:
# # - use depth of original call to test that we're getting info from the right 'return'
# # - use depth to avoid tracing nested calls (more efficient)
# # - handle exceptions
#
# def _getsomelocals(function, keys, *args):
# """Execute a function and return its locals whose names are in keys (private).
#
# :Parameters:
# - `function`: The function from which to extract the locals.
# - `keys`: A tuple of names of locals to extract
# - `args`: Arguments to be passed to `function` so as to extract its locals.
#
# :return: The locals in `keys` with their values, as a dictionary.
#
# :note: Extracting the locals requires executing the function and is fairly inefficient.
# """
# func_locals = {'doc':function.__doc__}
# def probeFunc(frame, event, arg):
# if event == 'call' and len(inspect.stack()) > depth:
# return None
# elif event == 'return' and len(inspect.stack()) == depth:
# locals = frame.f_locals
# func_locals.update(dict((k,locals.get(k)) for k in keys))
# sys.settrace(None)
# elif event == 'exception' or event == 'c_exception':
# sys.settrace(None)
# return probeFunc
# depth = len(inspect.stack())+2
# sys.settrace(probeFunc)
# function(*args)
# return func_locals
#
# def _getlocals(function, *args):
# """Execute a function and return its locals (private).
#
# :Parameters:
# - `function`: The function from which to extract the locals.
# - `args`: Arguments to be passed to `function` so as to extract its locals.
#
# :return: All the locals of `function` together with their values, as a dictionary.
#
# :note: Extracting the locals requires executing the function and is fairly inefficient.
# """
# func_locals = {'doc':function.__doc__}
# def probeFunc(frame, event, arg):
# if event == 'return' and len(inspect.stack()) == depth:
# func_locals.update(frame.f_locals)
# sys.settrace(None)
# return probeFunc
# depth = len(inspect.stack())+2
# sys.settrace(probeFunc)
# function(*args)
# return func_locals

# -------- Error classes --------

class EnterLeaveDeclarationError(Exception):
""" Signals that the ``@state.enter`` or ``@state.leave`` decorator
must be used with a state declaration.
"""

class TransitionDeclarationError(Exception):
"""Signals that the ``@transition`` decorator must be used within a state declaration."""

Expand Down Expand Up @@ -206,17 +211,36 @@ def __call__(self, state_machine):
if __DEBUG__:
print "Initializing", self

# call the state function that was decorated and extract the enter/leave functions that it declares, if any.
func_locals = _getsomelocals(self.func, ('enter', 'leave'), state_machine)

self.enter = func_locals.get('enter')
if self.enter and not callable(self.enter):
self.enter = None

self.leave = func_locals.get('leave')
if self.leave and not callable(self.leave):
self.leave = None

# Call the state function that was decorated. This will execute its body, including any
# enter/leave/transition decorators contained therein. These decorators will register their
# functions with this calling state instance (self).
self.func(state_machine)


@classmethod
def _add_enter_or_leave(cls, f, kind):
# Inspect the call stack to find our embedding state.
# We're supposed to be called by the state decorator function, which is a lambda around us.
# This is why we go up 3 levels in the call stack to find the state we belong to.
locals = sys._getframe(3).f_locals
my_state = None
if locals:
my_state = locals.get('self')
if not locals or not my_state or not isinstance(my_state, state):
raise EnterLeaveDeclarationError("state.%s must be used within a state" % (kind))

# import jre.debug
# jre.debug.interact()
if not callable(f):
setattr(my_state, kind, None)
else:
setattr(my_state, kind, f)

return f

enter = classmethod(lambda cls, f: cls._add_enter_or_leave(f, 'enter'))
leave = classmethod(lambda cls, f: cls._add_enter_or_leave(f, 'leave'))

class transition(Transition):
"""This decorator class is used to declare the transitions of a state machine.
Expand Down Expand Up @@ -274,9 +298,9 @@ def __call__(self, func):

self.action = func

# we're supposed to be called by the state decorator function, which calls '_getsomelocals'
# This is why we go up 3 levels in the call stack to find the state we belong to.
locals = sys._getframe(3).f_locals
# we're supposed to be called by the state decorator function.
# This is why we go up 2 levels in the call stack to find the state we belong to.
locals = sys._getframe(2).f_locals
my_state = None
if locals:
my_state = locals.get('self')
Expand Down
6 changes: 5 additions & 1 deletion StateMachines/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ def _translateDefStatement(self, m, indent):
args = m.group("args")

# return indent + "def %s(state, %s):\n" % (name, args)
return indent + "def %s(%s):\n" % (name, args)
# return indent + "def %s(%s):\n" % (name, args)
decorator = ''
if name in ('enter', 'leave'):
decorator = "%s@state.%s\n" % (indent, name)
return "%s%sdef %s(%s):\n" % (decorator, indent, name, args)

class PySMMetaImporter(object):
def find_module(self, fullname, path=None):
Expand Down
42 changes: 0 additions & 42 deletions pysmTest.pysm
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,6 @@ import unittest
import StateMachines
from StateMachines import *

# ----- testing the _getlocals/_getsomelocals functions --------

def f():
b = 1
c = 2
def g():
return 2
g()
return b

def f2():
b = 1
c = 2
def g():
raise Exception
g()
return b

class LocalsTest(unittest.TestCase):
def testLocals(self):
res = StateMachines.decorator._getlocals(f)
# print res
self.assertEqual(res.get('b'), 1)
self.assertEqual(res.get('c'), 2)
self.assert_(isinstance(res.get('g'), f.__class__))

def testSomeLocals(self):
res = StateMachines.decorator._getsomelocals(f, ('b', 'g'))
# print res
self.assertEqual(res.get('b'), 1)
self.assertEqual(res.get('c'), None)
self.assert_(isinstance(res.get('g'), f.__class__))

def testException(self):
res = None
try:
res = StateMachines.decorator._getsomelocals(f2, ('b', 'g'))
except Exception:
pass
self.assertEqual(res, None)


# -------- utilities for the state machine examples --------

class Point(object):
Expand Down
2 changes: 1 addition & 1 deletion pysmTestRunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
import sys

#subprocess.call(("%s pysmTest.py" % (sys.executable)).split())
subprocess.Popen("python pysmTest.py", shell=True)
subprocess.Popen("python pysmTest.pyc", shell=True)
46 changes: 4 additions & 42 deletions smTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,6 @@
import StateMachines
from StateMachines import *

# ----- testing the _getlocals/_getsomelocals functions --------

def f():
b = 1
c = 2
def g():
return 2
g()
return b

def f2():
b = 1
c = 2
def g():
raise Exception
g()
return b

class LocalsTest(unittest.TestCase):
def testLocals(self):
res = StateMachines.decorator._getlocals(f)
# print res
self.assertEqual(res.get('b'), 1)
self.assertEqual(res.get('c'), 2)
self.assert_(isinstance(res.get('g'), f.__class__))

def testSomeLocals(self):
res = StateMachines.decorator._getsomelocals(f, ('b', 'g'))
# print res
self.assertEqual(res.get('b'), 1)
self.assertEqual(res.get('c'), None)
self.assert_(isinstance(res.get('g'), f.__class__))

def testException(self):
res = None
try:
res = StateMachines.decorator._getsomelocals(f2, ('b', 'g'))
except Exception:
pass
self.assertEqual(res, None)


# -------- utilities for the state machine examples --------

class Point(object):
Expand Down Expand Up @@ -127,9 +85,11 @@ def hysteresis(self, p1, p2, delta=5):
def start(self):
"""Start state"""

@state.enter
def enter():
print "entering Start"

@state.leave
def leave():
print "leaving Start"

Expand All @@ -151,9 +111,11 @@ def action(event):
def drag(self):
"""Drag state"""

@state.enter
def enter():
print "entering Drag"

@state.leave
def leave():
print "leaving Drag"

Expand Down

0 comments on commit 9c837c4

Please sign in to comment.