Merge lp:~therve/storm/cache-compile into lp:storm

Proposed by Thomas Herve
Status: Merged
Merged at revision: 400
Proposed branch: lp:~therve/storm/cache-compile
Merge into: lp:storm
Diff against target: 282 lines (+40/-26)
4 files modified
storm/expr.py (+14/-5)
storm/info.py (+10/-11)
tests/expr.py (+7/-1)
tests/info.py (+9/-9)
To merge this branch: bzr merge lp:~therve/storm/cache-compile
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Jamu Kakar (community) Approve
Free Ekanayaka (community) Approve
Review via email: mp+71473@code.launchpad.net

Description of the change

Here it is. The branch adds a new attribute on Table and Column to cache the compilation. AFAIU, those compilations don't depend on the state, so we can store directly the value. It also changes the ClassInfo to store the table as a Table object instead of a SQLToken, to go through the compile_table logic.

I made a quick benchmark to show the gains:

                        | Trunk | Branch | Gain
10 inserts, 3 colums | 1.68s | 1.55s | 7.8%
100 insersts, 3 columns | 14.15s | 12.79s | 9.6%
20 insersts, 10 columns | 5.38s | 4.71s | 12.4%

I think it's worth it considering the low impact of the changes.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Nice!

Would you mind to repeat the benchmark with 1000 inserts and 10 columns each, to see a bit better the effects? The first number feels a bit suspect (hard to believe 10 inserts would take 1.6 seconds).

review: Needs Information
Revision history for this message
Thomas Herve (therve) wrote :

Gustavo: sorry this was explicit, but it's run 1000 times over.

Revision history for this message
Thomas Herve (therve) wrote :

Here's the script I used, with the last run: http://pastebin.ubuntu.com/666307/

Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Hi Thomas,

that's indeed significantly faster for use cases similar to the one in the attached script.

+1

[1]

- self.table = SQLToken(self.table)
+ self.table = Table(self.table)

I guess this is to make use of the cache? Are SQLToken and Table fully interchangeable in this case?

[2]

It'd be good to add a couple of unit tests for the new behavior.

review: Approve
lp:~therve/storm/cache-compile updated
399. By Thomas Herve

Add tests for compile_cache value

Revision history for this message
Thomas Herve (therve) wrote :

[1] Yes, it's for using the cache. table has been documented as an expression, so I don't see any problem changing it. The compilation passes token=True which results to the same result.

[2] I added some tests.

Thanks!

Revision history for this message
Jamu Kakar (jkakar) wrote :

Looks good to me, +1!

review: Approve
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Thomas, I mentioned 1000 inserts, not running it 1000 times.

Either way, sounds like it's improved things in practice, so +1.

review: Approve
Revision history for this message
Thomas Herve (therve) wrote :

Right, with 1000 inserts

Branch: 0.227118968964
Trunk: 0.256669044495

So around 11% faster.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'storm/expr.py'
--- storm/expr.py 2011-05-16 10:39:36 +0000
+++ storm/expr.py 2011-08-24 11:55:39 +0000
@@ -800,7 +800,8 @@
800 @ivar variable_factory: Factory producing C{Variable} instances typed800 @ivar variable_factory: Factory producing C{Variable} instances typed
801 according to this column.801 according to this column.
802 """802 """
803 __slots__ = ("name", "table", "primary", "variable_factory")803 __slots__ = ("name", "table", "primary", "variable_factory",
804 "compile_cache")
804805
805 def __init__(self, name=Undef, table=Undef, primary=False,806 def __init__(self, name=Undef, table=Undef, primary=False,
806 variable_factory=None):807 variable_factory=None):
@@ -808,6 +809,7 @@
808 self.table = table809 self.table = table
809 self.primary = int(primary)810 self.primary = int(primary)
810 self.variable_factory = variable_factory or Variable811 self.variable_factory = variable_factory or Variable
812 self.compile_cache = None
811813
812@compile.when(Column)814@compile.when(Column)
813def compile_column(compile, column, state):815def compile_column(compile, column, state):
@@ -819,11 +821,15 @@
819 alias = state.aliases.get(column)821 alias = state.aliases.get(column)
820 if alias is not None:822 if alias is not None:
821 return compile(alias.name, state, token=True)823 return compile(alias.name, state, token=True)
822 return compile(column.name, state, token=True)824 if column.compile_cache is None:
825 column.compile_cache = compile(column.name, state, token=True)
826 return column.compile_cache
823 state.push("context", COLUMN_PREFIX)827 state.push("context", COLUMN_PREFIX)
824 table = compile(column.table, state, token=True)828 table = compile(column.table, state, token=True)
825 state.pop()829 state.pop()
826 return "%s.%s" % (table, compile(column.name, state, token=True))830 if column.compile_cache is None:
831 column.compile_cache = compile(column.name, state, token=True)
832 return "%s.%s" % (table, column.compile_cache)
827833
828@compile_python.when(Column)834@compile_python.when(Column)
829def compile_python_column(compile, column, state):835def compile_python_column(compile, column, state):
@@ -870,14 +876,17 @@
870876
871877
872class Table(FromExpr):878class Table(FromExpr):
873 __slots__ = ("name",)879 __slots__ = ("name", "compile_cache")
874880
875 def __init__(self, name):881 def __init__(self, name):
876 self.name = name882 self.name = name
883 self.compile_cache = None
877884
878@compile.when(Table)885@compile.when(Table)
879def compile_table(compile, table, state):886def compile_table(compile, table, state):
880 return compile(table.name, state, token=True)887 if table.compile_cache is None:
888 table.compile_cache = compile(table.name, state, token=True)
889 return table.compile_cache
881890
882891
883class JoinExpr(FromExpr):892class JoinExpr(FromExpr):
884893
=== modified file 'storm/info.py'
--- storm/info.py 2008-06-20 06:50:47 +0000
+++ storm/info.py 2011-08-24 11:55:39 +0000
@@ -18,11 +18,11 @@
18# You should have received a copy of the GNU Lesser General Public License18# You should have received a copy of the GNU Lesser General Public License
19# along with this program. If not, see <http://www.gnu.org/licenses/>.19# along with this program. If not, see <http://www.gnu.org/licenses/>.
20#20#
21from weakref import ref, WeakKeyDictionary21from weakref import ref
2222
23from storm.exceptions import ClassInfoError23from storm.exceptions import ClassInfoError
24from storm.expr import Expr, FromExpr, Column, Desc, TABLE24from storm.expr import Column, Desc, TABLE
25from storm.expr import SQLToken, CompileError, compile25from storm.expr import compile, Table
26from storm.event import EventSystem26from storm.event import EventSystem
27from storm import Undef, has_cextensions27from storm import Undef, has_cextensions
2828
@@ -40,9 +40,11 @@
40 obj_info = ObjectInfo(obj)40 obj_info = ObjectInfo(obj)
41 return obj.__dict__.setdefault("__storm_object_info__", obj_info)41 return obj.__dict__.setdefault("__storm_object_info__", obj_info)
4242
43
43def set_obj_info(obj, obj_info):44def set_obj_info(obj, obj_info):
44 obj.__dict__["__storm_object_info__"] = obj_info45 obj.__dict__["__storm_object_info__"] = obj_info
4546
47
46def get_cls_info(cls):48def get_cls_info(cls):
47 if "__storm_class_info__" in cls.__dict__:49 if "__storm_class_info__" in cls.__dict__:
48 # Can't use attribute access here, otherwise subclassing won't work.50 # Can't use attribute access here, otherwise subclassing won't work.
@@ -51,6 +53,7 @@
51 cls.__storm_class_info__ = ClassInfo(cls)53 cls.__storm_class_info__ = ClassInfo(cls)
52 return cls.__storm_class_info__54 return cls.__storm_class_info__
5355
56
54class ClassInfo(dict):57class ClassInfo(dict):
55 """Persistent storm-related information of a class.58 """Persistent storm-related information of a class.
5659
@@ -71,7 +74,7 @@
71 self.cls = cls74 self.cls = cls
7275
73 if isinstance(self.table, basestring):76 if isinstance(self.table, basestring):
74 self.table = SQLToken(self.table)77 self.table = Table(self.table)
7578
76 pairs = []79 pairs = []
77 for attr in dir(cls):80 for attr in dir(cls):
@@ -79,7 +82,6 @@
79 if isinstance(column, Column):82 if isinstance(column, Column):
80 pairs.append((attr, column))83 pairs.append((attr, column))
8184
82
83 pairs.sort()85 pairs.sort()
8486
85 self.columns = tuple(pair[1] for pair in pairs)87 self.columns = tuple(pair[1] for pair in pairs)
@@ -121,7 +123,6 @@
121 self.primary_key_pos = tuple(id_positions[id(column)]123 self.primary_key_pos = tuple(id_positions[id(column)]
122 for column in self.primary_key)124 for column in self.primary_key)
123125
124
125 __order__ = getattr(cls, "__storm_order__", None)126 __order__ = getattr(cls, "__storm_order__", None)
126 if __order__ is None:127 if __order__ is None:
127 self.default_order = Undef128 self.default_order = Undef
@@ -151,7 +152,7 @@
151 __hash__ = object.__hash__152 __hash__ = object.__hash__
152153
153 # For get_obj_info(), an ObjectInfo is its own obj_info.154 # For get_obj_info(), an ObjectInfo is its own obj_info.
154 __storm_object_info__ = property(lambda self:self)155 __storm_object_info__ = property(lambda self: self)
155156
156 def __init__(self, obj):157 def __init__(self, obj):
157 # FASTPATH This method is part of the fast path. Be careful when158 # FASTPATH This method is part of the fast path. Be careful when
@@ -171,7 +172,7 @@
171 column.variable_factory(column=column,172 column.variable_factory(column=column,
172 event=event,173 event=event,
173 validator_object_factory=self.get_obj)174 validator_object_factory=self.get_obj)
174 175
175 self.primary_vars = tuple(variables[column]176 self.primary_vars = tuple(variables[column]
176 for column in self.cls_info.primary_key)177 for column in self.cls_info.primary_key)
177178
@@ -199,7 +200,6 @@
199 from storm.cextensions import ObjectInfo, get_obj_info200 from storm.cextensions import ObjectInfo, get_obj_info
200201
201202
202
203class ClassAlias(object):203class ClassAlias(object):
204 """Create a named alias for a Storm class for use in queries.204 """Create a named alias for a Storm class for use in queries.
205205
@@ -229,8 +229,7 @@
229 cls._storm_alias_cache = {}229 cls._storm_alias_cache = {}
230 elif name in cache:230 elif name in cache:
231 return cache[name]231 return cache[name]
232 cls_info = get_cls_info(cls)232 alias_cls = type(cls.__name__ + "Alias", (self_cls,),
233 alias_cls = type(cls.__name__+"Alias", (self_cls,),
234 {"__storm_table__": name})233 {"__storm_table__": name})
235 alias_cls.__bases__ = (cls, self_cls)234 alias_cls.__bases__ = (cls, self_cls)
236 alias_cls_info = get_cls_info(alias_cls)235 alias_cls_info = get_cls_info(alias_cls)
237236
=== modified file 'tests/expr.py'
--- tests/expr.py 2011-05-11 05:38:41 +0000
+++ tests/expr.py 2011-08-24 11:55:39 +0000
@@ -135,6 +135,7 @@
135 expr = Column()135 expr = Column()
136 self.assertEquals(expr.name, Undef)136 self.assertEquals(expr.name, Undef)
137 self.assertEquals(expr.table, Undef)137 self.assertEquals(expr.table, Undef)
138 self.assertIdentical(expr.compile_cache, None)
138139
139 # Test for identity. We don't want False there.140 # Test for identity. We don't want False there.
140 self.assertTrue(expr.primary is 0)141 self.assertTrue(expr.primary is 0)
@@ -1007,13 +1008,16 @@
1007 statement = compile(expr, state)1008 statement = compile(expr, state)
1008 self.assertEquals(statement, "column1")1009 self.assertEquals(statement, "column1")
1009 self.assertEquals(state.parameters, [])1010 self.assertEquals(state.parameters, [])
1011 self.assertEquals(expr.compile_cache, "column1")
10101012
1011 def test_column_table(self):1013 def test_column_table(self):
1012 expr = Select(Column(column1, Func1()))1014 column = Column(column1, Func1())
1015 expr = Select(column)
1013 state = State()1016 state = State()
1014 statement = compile(expr, state)1017 statement = compile(expr, state)
1015 self.assertEquals(statement, "SELECT func1().column1 FROM func1()")1018 self.assertEquals(statement, "SELECT func1().column1 FROM func1()")
1016 self.assertEquals(state.parameters, [])1019 self.assertEquals(state.parameters, [])
1020 self.assertEquals(column.compile_cache, "column1")
10171021
1018 def test_column_contexts(self):1022 def test_column_contexts(self):
1019 table, = track_contexts(1)1023 table, = track_contexts(1)
@@ -1579,10 +1583,12 @@
15791583
1580 def test_table(self):1584 def test_table(self):
1581 expr = Table(table1)1585 expr = Table(table1)
1586 self.assertIdentical(expr.compile_cache, None)
1582 state = State()1587 state = State()
1583 statement = compile(expr, state)1588 statement = compile(expr, state)
1584 self.assertEquals(statement, '"table 1"')1589 self.assertEquals(statement, '"table 1"')
1585 self.assertEquals(state.parameters, [])1590 self.assertEquals(state.parameters, [])
1591 self.assertEquals(expr.compile_cache, '"table 1"')
15861592
1587 def test_alias(self):1593 def test_alias(self):
1588 expr = Alias(Table(table1), "name")1594 expr = Alias(Table(table1), "name")
15891595
=== modified file 'tests/info.py'
--- tests/info.py 2008-06-18 17:28:37 +0000
+++ tests/info.py 2011-08-24 11:55:39 +0000
@@ -94,7 +94,7 @@
94 (self.Class.prop1, self.Class.prop2))94 (self.Class.prop1, self.Class.prop2))
9595
96 def test_table(self):96 def test_table(self):
97 self.assertEquals(self.cls_info.table, "table")97 self.assertEquals(self.cls_info.table.name, "table")
9898
99 def test_primary_key(self):99 def test_primary_key(self):
100 # Can't use == for props.100 # Can't use == for props.
@@ -202,7 +202,7 @@
202202
203 def test_variables(self):203 def test_variables(self):
204 self.assertTrue(isinstance(self.obj_info.variables, dict))204 self.assertTrue(isinstance(self.obj_info.variables, dict))
205 205
206 for column in self.cls_info.columns:206 for column in self.cls_info.columns:
207 variable = self.obj_info.variables.get(column)207 variable = self.obj_info.variables.get(column)
208 self.assertTrue(isinstance(variable, Variable))208 self.assertTrue(isinstance(variable, Variable))
@@ -227,7 +227,7 @@
227227
228 def test_primary_vars(self):228 def test_primary_vars(self):
229 self.assertTrue(isinstance(self.obj_info.primary_vars, tuple))229 self.assertTrue(isinstance(self.obj_info.primary_vars, tuple))
230 230
231 for column, variable in zip(self.cls_info.primary_key,231 for column, variable in zip(self.cls_info.primary_key,
232 self.obj_info.primary_vars):232 self.obj_info.primary_vars):
233 self.assertEquals(self.obj_info.variables.get(column),233 self.assertEquals(self.obj_info.variables.get(column),
@@ -316,7 +316,7 @@
316 self.obj_info.checkpoint()316 self.obj_info.checkpoint()
317317
318 obj = object()318 obj = object()
319 319
320 self.obj_info.event.hook("changed", object_changed1, obj)320 self.obj_info.event.hook("changed", object_changed1, obj)
321 self.obj_info.event.hook("changed", object_changed2, obj)321 self.obj_info.event.hook("changed", object_changed2, obj)
322322
@@ -326,7 +326,7 @@
326 self.assertEquals(changes1,326 self.assertEquals(changes1,
327 [(1, self.obj_info, self.variable2, Undef, 10, False, obj),327 [(1, self.obj_info, self.variable2, Undef, 10, False, obj),
328 (1, self.obj_info, self.variable1, Undef, 20, False, obj)])328 (1, self.obj_info, self.variable1, Undef, 20, False, obj)])
329 self.assertEquals(changes2, 329 self.assertEquals(changes2,
330 [(2, self.obj_info, self.variable2, Undef, 10, False, obj),330 [(2, self.obj_info, self.variable2, Undef, 10, False, obj),
331 (2, self.obj_info, self.variable1, Undef, 20, False, obj)])331 (2, self.obj_info, self.variable1, Undef, 20, False, obj)])
332332
@@ -339,7 +339,7 @@
339 self.assertEquals(changes1,339 self.assertEquals(changes1,
340 [(1, self.obj_info, self.variable1, 20, None, False, obj),340 [(1, self.obj_info, self.variable1, 20, None, False, obj),
341 (1, self.obj_info, self.variable2, 10, None, False, obj)])341 (1, self.obj_info, self.variable2, 10, None, False, obj)])
342 self.assertEquals(changes2, 342 self.assertEquals(changes2,
343 [(2, self.obj_info, self.variable1, 20, None, False, obj),343 [(2, self.obj_info, self.variable1, 20, None, False, obj),
344 (2, self.obj_info, self.variable2, 10, None, False, obj)])344 (2, self.obj_info, self.variable2, 10, None, False, obj)])
345345
@@ -352,7 +352,7 @@
352 self.assertEquals(changes1,352 self.assertEquals(changes1,
353 [(1, self.obj_info, self.variable1, None, Undef, False, obj),353 [(1, self.obj_info, self.variable1, None, Undef, False, obj),
354 (1, self.obj_info, self.variable2, None, Undef, False, obj)])354 (1, self.obj_info, self.variable2, None, Undef, False, obj)])
355 self.assertEquals(changes2, 355 self.assertEquals(changes2,
356 [(2, self.obj_info, self.variable1, None, Undef, False, obj),356 [(2, self.obj_info, self.variable1, None, Undef, False, obj),
357 (2, self.obj_info, self.variable2, None, Undef, False, obj)])357 (2, self.obj_info, self.variable2, None, Undef, False, obj)])
358358
@@ -528,11 +528,11 @@
528 prop1 = Property("column1", primary=True)528 prop1 = Property("column1", primary=True)
529 self.Class = Class529 self.Class = Class
530 self.ClassAlias = ClassAlias(self.Class, "alias")530 self.ClassAlias = ClassAlias(self.Class, "alias")
531 531
532 def test_cls_info_cls(self):532 def test_cls_info_cls(self):
533 cls_info = get_cls_info(self.ClassAlias)533 cls_info = get_cls_info(self.ClassAlias)
534 self.assertEquals(cls_info.cls, self.Class)534 self.assertEquals(cls_info.cls, self.Class)
535 self.assertEquals(cls_info.table, "alias")535 self.assertEquals(cls_info.table.name, "alias")
536 self.assertEquals(self.ClassAlias.prop1.name, "column1")536 self.assertEquals(self.ClassAlias.prop1.name, "column1")
537 self.assertEquals(self.ClassAlias.prop1.table, self.ClassAlias)537 self.assertEquals(self.ClassAlias.prop1.table, self.ClassAlias)
538538

Subscribers

People subscribed via source and target branches

to status/vote changes: