kivy/kivy — pull request #2368 by matham (2014-07-26 23:27:57+00:00)
Title: [WIP]: Optimize kv binding

The main idea is stop creating partial functions for each binding, and instead add the params that wouldv'e been called with partial to the observers in property. To that end, I created a `_bind_with_args` and `_unbind_with_args` methods to handle this.

This is clearly still in the hackey stage, and is just meant to get feedback. One thing, it doesn't support Observable yet because `Observable` doesn't support `_bind_with_args` and `_unbind_with_args` yet. I'm not exactly sure what to do with this. One way, which is probably what we should do, is to formalize `_bind_with_args` and `_unbind_with_args` into user methods and add them to `Observable`. Alternatively, make `Observable` a superclass of eventdispatcher which will improve performance anyway.

Stats from code below
==================

With master:
-----------------

```
binding - pyobj: False, rebind: True - 5.05774174753
unbinding - pyobj: False, rebind: True - 4.07955124517
binding - pyobj: False, rebind: False - 1.52420031363
unbinding - pyobj: False, rebind: False - 0.286696304983
binding - pyobj: True, rebind: False - 2.16739078929
unbinding - pyobj: True, rebind: False - 0.296439419043

27829 function calls in 0.140 seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)
500 0.040 0.000 0.123 0.000 lang.py:1426(update_intermediates)
3000 0.036 0.000 0.036 0.000 {method 'bind' of 'kivy._event.EventDispatcher' objects}
3500 0.013 0.000 0.031 0.000 lang.py:1414(get_proxy)
3602 0.012 0.000 0.018 0.000 widget.py:312(proxy_ref)
3101 0.007 0.000 0.007 0.000 {isinstance}
3703 0.006 0.000 0.006 0.000 {hasattr}
3503 0.006 0.000 0.006 0.000 {method 'append' of 'list' objects}
2501 0.005 0.000 0.005 0.000 {getattr}
100 0.005 0.000 0.136 0.001 lang.py:1520(create_handler)
2500 0.004 0.000 0.004 0.000 {method 'property' of 'kivy._event.EventDispatcher' objects}
```

With pr:
-----------------

```
binding - pyobj: False, rebind: True - 1.69825377018
unbinding - pyobj: False, rebind: True - 0.682011824941
binding - pyobj: False, rebind: False - 1.3360524374
unbinding - pyobj: False, rebind: False - 0.461435902111
binding - pyobj: True, rebind: False - 2.05720794576
unbinding - pyobj: True, rebind: False - 0.461210062902

20727 function calls in 0.086 seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)
500 0.039 0.000 0.072 0.000 lang.py:1430(update_intermediates)
3602 0.007 0.000 0.007 0.000 widget.py:313(proxy_ref)
3101 0.006 0.000 0.006 0.000 {isinstance}
3503 0.006 0.000 0.006 0.000 {method 'append' of 'list' objects}
100 0.005 0.000 0.081 0.001 lang.py:1524(create_handler)
2500 0.005 0.000 0.005 0.000 {method '_bind_with_args' of 'kivy._event.EventDispatcher' objects}
2501 0.005 0.000 0.005 0.000 {getattr}
2500 0.004 0.000 0.004 0.000 {method 'property' of 'kivy._event.EventDispatcher' objects}
500 0.003 0.000 0.003 0.000 {method 'bind' of 'kivy._event.EventDispatcher' objects}
```

Test code
--------------

```
from kivy.lang import Builder
from kivy.uix.widget import Widget
import pstats
import cProfile
import timeit
from kivy.base import EventLoop
EventLoop.ensure_window()

class TestWidget(Widget):

def __init__(self, pyobj=False, rebind=False, **kwargs):

# create object props pointing to self
if pyobj:
self.obj0 = self.obj1 = self.obj2 = self.obj3 = self.obj4 = self
else:
for i in range(5):
self.create_property('obj{}'.format(i), self, rebind=rebind)

# create bool props to be used in rule source
for i in range(5):
self.create_property('boo{}_in'.format(i), False)

# create bool props to be used for rule target
for i in range(100):
self.create_property('boo{}_out'.format(i), False)
super(TestWidget, self).__init__(**kwargs)

rule = ' and '.join(['self.obj0.obj1.obj2.obj3.obj4.boo{}_in'.format(i)
for i in range(5)])
kv = '\n'.join([':'] +
[' boo{}_out: {}'.format(i, rule) for i in range(100)])
Builder.load_string(kv)

count = 100
rebind = True
pyobj = False
test_bind = True
profile = False

if test_bind:
print 'binding - pyobj: {}, rebind: {} - {}'.format(pyobj, rebind,
timeit.timeit('TestWidget(pyobj=pyobj, rebind=rebind)',
"from __main__ import TestWidget, rebind, pyobj", number=count))
elif not profile:
objs = [TestWidget(pyobj=pyobj, rebind=rebind) for _ in range(count)]
keep_refs = []
print 'unbinding - pyobj: {}, rebind: {} - {}'.format(pyobj, rebind,
timeit.timeit('Builder.unbind_widget(objs[0].uid) or keep_refs.append(objs.pop(0))',
"from __main__ import objs, Builder, keep_refs", number=count))
else:
cProfile.runctx("TestWidget(pyobj=pyobj, rebind=rebind)", globals(), locals(), "Profile.prof")

s = pstats.Stats("Profile.prof")
s.strip_dirs().sort_stats("tottime").print_stats()
```

You need the following diff to run the test code:

```
diff --git a/kivy/_event.pyx b/kivy/_event.pyx
index 2911296..3dcc4c5 100644
--- a/kivy/_event.pyx
+++ b/kivy/_event.pyx
@@ -463,7 +463,7 @@ cdef class EventDispatcher(ObjectWithUid):
ret[x] = p[x]
return ret

- def create_property(self, name, value=None):
+ def create_property(self, name, value=None, *largs, **kwargs):
'''Create a new property at runtime.

.. versionadded:: 1.0.9
@@ -497,17 +497,17 @@ cdef class EventDispatcher(ObjectWithUid):
True
'''
if isinstance(value, bool):
- prop = BooleanProperty(value)
+ prop = BooleanProperty(value, *largs, **kwargs)
elif isinstance(value, (int, float)):
- prop = NumericProperty(value)
+ prop = NumericProperty(value, *largs, **kwargs)
elif isinstance(value, string_types):
- prop = StringProperty(value)
+ prop = StringProperty(value, *largs, **kwargs)
elif isinstance(value, (list, tuple)):
- prop = ListProperty(value)
+ prop = ListProperty(value, *largs, **kwargs)
elif isinstance(value, dict):
- prop = DictProperty(value)
+ prop = DictProperty(value, *largs, **kwargs)
else:
- prop = ObjectProperty(value)
+ prop = ObjectProperty(value, *largs, **kwargs)
prop.link(self, name)
prop.link_deps(self, name)
self.__properties[name] = prop
```