From: Mike Bayer Date: Sat, 14 Dec 2019 16:39:06 +0000 (-0500) Subject: Traversal and clause generation performance improvements X-Git-Tag: rel_1_4_0b1~600 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=89bf6d80a999eb31ee4a69b229b887fbfb2ed12a;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Traversal and clause generation performance improvements Added one traversal test, callcounts have been brought from 29754 to 5173 so far. Change-Id: I164e9831600709ee214c1379bb215fdad73b39aa --- diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 548eca58db..aa350c7baa 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -2209,8 +2209,12 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr): for table, columns in self._cols_by_table.items() ) + # temporarily commented out until we fix an issue in the serializer + # @_memoized_configured_property.method def __clause_element__(self): - return self.selectable + return self.selectable # ._annotate( + # {"parententity": self, "parentmapper": self} + # ) @property def selectable(self): diff --git a/lib/sqlalchemy/sql/annotation.py b/lib/sqlalchemy/sql/annotation.py index 0d995ec8a2..9853cef2ac 100644 --- a/lib/sqlalchemy/sql/annotation.py +++ b/lib/sqlalchemy/sql/annotation.py @@ -19,23 +19,26 @@ from .. import util class SupportsAnnotations(object): @util.memoized_property - def _annotation_traversals(self): - return [ - ( - key, - InternalTraversal.dp_has_cache_key - if isinstance(value, HasCacheKey) - else InternalTraversal.dp_plain_obj, - ) - for key, value in self._annotations.items() - ] + def _annotations_cache_key(self): + return ( + "_annotations", + tuple( + ( + key, + value._gen_cache_key(None, []) + if isinstance(value, HasCacheKey) + else value, + ) + for key, value in self._annotations.items() + ), + ) class SupportsCloneAnnotations(SupportsAnnotations): _annotations = util.immutabledict() _traverse_internals = [ - ("_annotations", InternalTraversal.dp_annotations_state) + ("_annotations_cache_key", InternalTraversal.dp_plain_obj) ] def _annotate(self, values): @@ -45,7 +48,7 @@ class SupportsCloneAnnotations(SupportsAnnotations): """ new = self._clone() new._annotations = new._annotations.union(values) - new.__dict__.pop("_annotation_traversals", None) + new.__dict__.pop("_annotations_cache_key", None) return new def _with_annotations(self, values): @@ -55,7 +58,7 @@ class SupportsCloneAnnotations(SupportsAnnotations): """ new = self._clone() new._annotations = util.immutabledict(values) - new.__dict__.pop("_annotation_traversals", None) + new.__dict__.pop("_annotations_cache_key", None) return new def _deannotate(self, values=None, clone=False): @@ -71,7 +74,7 @@ class SupportsCloneAnnotations(SupportsAnnotations): # the expression for a deep deannotation new = self._clone() new._annotations = {} - new.__dict__.pop("_annotation_traversals", None) + new.__dict__.pop("_annotations_cache_key", None) return new else: return self @@ -146,7 +149,7 @@ class Annotated(object): def __init__(self, element, values): self.__dict__ = element.__dict__.copy() - self.__dict__.pop("_annotation_traversals", None) + self.__dict__.pop("_annotations_cache_key", None) self.__element = element self._annotations = values self._hash = hash(element) @@ -159,7 +162,7 @@ class Annotated(object): def _with_annotations(self, values): clone = self.__class__.__new__(self.__class__) clone.__dict__ = self.__dict__.copy() - clone.__dict__.pop("_annotation_traversals", None) + clone.__dict__.pop("_annotations_cache_key", None) clone._annotations = values return clone @@ -305,7 +308,7 @@ def _new_annotation_type(cls, base_cls): if "_traverse_internals" in cls.__dict__: anno_cls._traverse_internals = list(cls._traverse_internals) + [ - ("_annotations", InternalTraversal.dp_annotations_state) + ("_annotations_cache_key", InternalTraversal.dp_plain_obj) ] return anno_cls diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index eda31dc619..da75683305 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -198,12 +198,7 @@ class ClauseElement( _order_by_label_element = None - @property - def _cache_key_traversal(self): - try: - return self._traverse_internals - except AttributeError: - return NO_CACHE + _cache_key_traversal = None def _clone(self): """Create a shallow copy of this ClauseElement. @@ -1344,16 +1339,21 @@ class BindParameter(roles.InElementRole, ColumnElement): return c def _gen_cache_key(self, anon_map, bindparams): - if self in anon_map: - return (anon_map[self], self.__class__) + idself = id(self) + if idself in anon_map: + return (anon_map[idself], self.__class__) + else: + # inline of + # id_ = anon_map[idself] + anon_map[idself] = id_ = str(anon_map.index) + anon_map.index += 1 - id_ = anon_map[self] bindparams.append(self) return ( id_, self.__class__, - self.type._gen_cache_key, + self.type._static_cache_key, traversals._resolve_name_for_compare(self, self.key, anon_map), ) @@ -3239,6 +3239,33 @@ class BinaryExpression(ColumnElement): """ + def _gen_cache_key(self, anon_map, bindparams): + # inlined for performance + + idself = id(self) + + if idself in anon_map: + return (anon_map[idself], self.__class__) + else: + # inline of + # id_ = anon_map[idself] + anon_map[idself] = id_ = str(anon_map.index) + anon_map.index += 1 + + if self._cache_key_traversal is NO_CACHE: + anon_map[NO_CACHE] = True + return None + + result = (id_, self.__class__) + + return result + ( + ("left", self.left._gen_cache_key(anon_map, bindparams)), + ("right", self.right._gen_cache_key(anon_map, bindparams)), + ("operator", self.operator), + ("negate", self.negate), + ("modifiers", self.modifiers), + ) + def __init__( self, left, right, operator, type_=None, negate=None, modifiers=None ): diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index 96e64dc284..ac409ee0b2 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -404,6 +404,9 @@ class FunctionAsBinary(BinaryExpression): ("modifiers", InternalTraversal.dp_plain_dict), ] + def _gen_cache_key(self, anon_map, bindparams): + return ColumnElement._gen_cache_key(self, anon_map, bindparams) + def __init__(self, fn, left_index, right_index): self.sql_function = fn self.left_index = left_index diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 0eadab6107..5f609f8fdc 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -3148,8 +3148,14 @@ class Select( ("_raw_columns", InternalTraversal.dp_clauseelement_list), ("_whereclause", InternalTraversal.dp_clauseelement), ("_having", InternalTraversal.dp_clauseelement), - ("_order_by_clause", InternalTraversal.dp_clauseelement_list), - ("_group_by_clause", InternalTraversal.dp_clauseelement_list), + ( + "_order_by_clause.clauses", + InternalTraversal.dp_clauseelement_list, + ), + ( + "_group_by_clause.clauses", + InternalTraversal.dp_clauseelement_list, + ), ("_correlate", InternalTraversal.dp_clauseelement_unordered_set), ( "_correlate_except", diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py index c0782ce486..588bbc3dcf 100644 --- a/lib/sqlalchemy/sql/traversals.py +++ b/lib/sqlalchemy/sql/traversals.py @@ -1,5 +1,6 @@ from collections import deque from collections import namedtuple +import operator from . import operators from .visitors import ExtendedInternalTraversal @@ -11,6 +12,9 @@ SKIP_TRAVERSE = util.symbol("skip_traverse") COMPARE_FAILED = False COMPARE_SUCCEEDED = True NO_CACHE = util.symbol("no_cache") +CACHE_IN_PLACE = util.symbol("cache_in_place") +CALL_GEN_CACHE_KEY = util.symbol("call_gen_cache_key") +STATIC_CACHE_KEY = util.symbol("static_cache_key") def compare(obj1, obj2, **kw): @@ -46,22 +50,82 @@ class HasCacheKey(object): """ - if self in anon_map: - return (anon_map[self], self.__class__) + idself = id(self) - id_ = anon_map[self] - - if self._cache_key_traversal is NO_CACHE: - anon_map[NO_CACHE] = True + if anon_map is not None: + if idself in anon_map: + return (anon_map[idself], self.__class__) + else: + # inline of + # id_ = anon_map[idself] + anon_map[idself] = id_ = str(anon_map.index) + anon_map.index += 1 + else: + id_ = None + + _cache_key_traversal = self._cache_key_traversal + if _cache_key_traversal is None: + try: + _cache_key_traversal = self._traverse_internals + except AttributeError: + _cache_key_traversal = NO_CACHE + + if _cache_key_traversal is NO_CACHE: + if anon_map is not None: + anon_map[NO_CACHE] = True return None result = (id_, self.__class__) - for attrname, obj, meth in _cache_key_traversal.run_generated_dispatch( - self, self._cache_key_traversal, "_generated_cache_key_traversal" + # inline of _cache_key_traversal_visitor.run_generated_dispatch() + try: + dispatcher = self.__class__.__dict__[ + "_generated_cache_key_traversal" + ] + except KeyError: + dispatcher = _cache_key_traversal_visitor.generate_dispatch( + self, _cache_key_traversal, "_generated_cache_key_traversal" + ) + + for attrname, obj, meth in dispatcher( + self, _cache_key_traversal_visitor ): if obj is not None: - result += meth(attrname, obj, self, anon_map, bindparams) + if meth is CACHE_IN_PLACE: + # cache in place is always going to be a Python + # tuple, dict, list, etc. so we can do a boolean check + if obj: + result += (attrname, obj) + elif meth is STATIC_CACHE_KEY: + result += (attrname, obj._static_cache_key) + elif meth is CALL_GEN_CACHE_KEY: + result += ( + attrname, + obj._gen_cache_key(anon_map, bindparams), + ) + elif meth is InternalTraversal.dp_clauseelement_list: + if obj: + result += ( + attrname, + tuple( + [ + elem._gen_cache_key(anon_map, bindparams) + for elem in obj + ] + ), + ) + else: + # note that all the "ClauseElement" standalone cases + # here have been handled by inlines above; so we can + # safely assume the object is a standard list/tuple/dict + # which we can skip if it evaluates to false. + # improvement would be to have this as a flag delivered + # up front in the dispatcher list + if obj: + result += meth( + attrname, obj, self, anon_map, bindparams + ) + return result def _generate_cache_key(self): @@ -118,17 +182,22 @@ def _clone(element, **kw): class _CacheKey(ExtendedInternalTraversal): - def visit_has_cache_key(self, attrname, obj, parent, anon_map, bindparams): - return (attrname, obj._gen_cache_key(anon_map, bindparams)) + # very common elements are inlined into the main _get_cache_key() method + # to produce a dramatic savings in Python function call overhead + + visit_has_cache_key = visit_clauseelement = CALL_GEN_CACHE_KEY + visit_clauseelement_list = InternalTraversal.dp_clauseelement_list + visit_string = ( + visit_boolean + ) = visit_operator = visit_plain_obj = CACHE_IN_PLACE + visit_statement_hint_list = CACHE_IN_PLACE + visit_type = STATIC_CACHE_KEY def visit_inspectable(self, attrname, obj, parent, anon_map, bindparams): return self.visit_has_cache_key( attrname, inspect(obj), parent, anon_map, bindparams ) - def visit_clauseelement(self, attrname, obj, parent, anon_map, bindparams): - return (attrname, obj._gen_cache_key(anon_map, bindparams)) - def visit_multi(self, attrname, obj, parent, anon_map, bindparams): return ( attrname, @@ -151,6 +220,8 @@ class _CacheKey(ExtendedInternalTraversal): def visit_has_cache_key_tuples( self, attrname, obj, parent, anon_map, bindparams ): + if not obj: + return () return ( attrname, tuple( @@ -165,6 +236,8 @@ class _CacheKey(ExtendedInternalTraversal): def visit_has_cache_key_list( self, attrname, obj, parent, anon_map, bindparams ): + if not obj: + return () return ( attrname, tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj), @@ -177,14 +250,6 @@ class _CacheKey(ExtendedInternalTraversal): attrname, [inspect(o) for o in obj], parent, anon_map, bindparams ) - def visit_clauseelement_list( - self, attrname, obj, parent, anon_map, bindparams - ): - return ( - attrname, - tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj), - ) - def visit_clauseelement_tuples( self, attrname, obj, parent, anon_map, bindparams ): @@ -204,14 +269,18 @@ class _CacheKey(ExtendedInternalTraversal): def visit_fromclause_ordered_set( self, attrname, obj, parent, anon_map, bindparams ): + if not obj: + return () return ( attrname, - tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj), + tuple([elem._gen_cache_key(anon_map, bindparams) for elem in obj]), ) def visit_clauseelement_unordered_set( self, attrname, obj, parent, anon_map, bindparams ): + if not obj: + return () cache_keys = [ elem._gen_cache_key(anon_map, bindparams) for elem in obj ] @@ -230,39 +299,40 @@ class _CacheKey(ExtendedInternalTraversal): def visit_prefix_sequence( self, attrname, obj, parent, anon_map, bindparams ): + if not obj: + return () return ( attrname, tuple( - (clause._gen_cache_key(anon_map, bindparams), strval) - for clause, strval in obj + [ + (clause._gen_cache_key(anon_map, bindparams), strval) + for clause, strval in obj + ] ), ) - def visit_statement_hint_list( - self, attrname, obj, parent, anon_map, bindparams - ): - return (attrname, obj) - def visit_table_hint_list( self, attrname, obj, parent, anon_map, bindparams ): + if not obj: + return () + return ( attrname, tuple( - ( - clause._gen_cache_key(anon_map, bindparams), - dialect_name, - text, - ) - for (clause, dialect_name), text in obj.items() + [ + ( + clause._gen_cache_key(anon_map, bindparams), + dialect_name, + text, + ) + for (clause, dialect_name), text in obj.items() + ] ), ) - def visit_type(self, attrname, obj, parent, anon_map, bindparams): - return (attrname, obj._gen_cache_key) - def visit_plain_dict(self, attrname, obj, parent, anon_map, bindparams): - return (attrname, tuple((key, obj[key]) for key in sorted(obj))) + return (attrname, tuple([(key, obj[key]) for key in sorted(obj)])) def visit_string_clauseelement_dict( self, attrname, obj, parent, anon_map, bindparams @@ -291,18 +361,6 @@ class _CacheKey(ExtendedInternalTraversal): ), ) - def visit_string(self, attrname, obj, parent, anon_map, bindparams): - return (attrname, obj) - - def visit_boolean(self, attrname, obj, parent, anon_map, bindparams): - return (attrname, obj) - - def visit_operator(self, attrname, obj, parent, anon_map, bindparams): - return (attrname, obj) - - def visit_plain_obj(self, attrname, obj, parent, anon_map, bindparams): - return (attrname, obj) - def visit_fromclause_canonical_column_collection( self, attrname, obj, parent, anon_map, bindparams ): @@ -311,22 +369,6 @@ class _CacheKey(ExtendedInternalTraversal): tuple(col._gen_cache_key(anon_map, bindparams) for col in obj), ) - def visit_annotations_state( - self, attrname, obj, parent, anon_map, bindparams - ): - return ( - attrname, - tuple( - ( - key, - self.dispatch(sym)( - key, obj[key], obj, anon_map, bindparams - ), - ) - for key, sym in parent._annotation_traversals - ), - ) - def visit_unknown_structure( self, attrname, obj, parent, anon_map, bindparams ): @@ -334,7 +376,7 @@ class _CacheKey(ExtendedInternalTraversal): return () -_cache_key_traversal = _CacheKey() +_cache_key_traversal_visitor = _CacheKey() class _CopyInternals(InternalTraversal): @@ -489,29 +531,23 @@ class TraversalComparatorStrategy(InternalTraversal, util.MemoizedSlots): right._traverse_internals, fillvalue=(None, None), ): + if not compare_annotations and ( + (left_attrname == "_annotations_cache_key") + or (right_attrname == "_annotations_cache_key") + ): + continue + if ( left_attrname != right_attrname or left_visit_sym is not right_visit_sym ): - if not compare_annotations and ( - ( - left_visit_sym - is InternalTraversal.dp_annotations_state, - ) - or ( - right_visit_sym - is InternalTraversal.dp_annotations_state, - ) - ): - continue - return False elif left_attrname in attributes_compared: continue dispatch = self.dispatch(left_visit_sym) - left_child = getattr(left, left_attrname) - right_child = getattr(right, right_attrname) + left_child = operator.attrgetter(left_attrname)(left) + right_child = operator.attrgetter(right_attrname)(right) if left_child is None: if right_child is not None: return False @@ -564,33 +600,6 @@ class TraversalComparatorStrategy(InternalTraversal, util.MemoizedSlots): return COMPARE_FAILED self.stack.append((left[lstr], right[rstr])) - def visit_annotations_state( - self, left_parent, left, right_parent, right, **kw - ): - if not kw.get("compare_annotations", False): - return - - for (lstr, lmeth), (rstr, rmeth) in util.zip_longest( - left_parent._annotation_traversals, - right_parent._annotation_traversals, - fillvalue=(None, None), - ): - if lstr != rstr or (lmeth is not rmeth): - return COMPARE_FAILED - - dispatch = self.dispatch(lmeth) - left_child = left[lstr] - right_child = right[rstr] - if left_child is None: - if right_child is not None: - return False - else: - continue - - comparison = dispatch(None, left_child, None, right_child, **kw) - if comparison is COMPARE_FAILED: - return comparison - def visit_clauseelement_tuples( self, left_parent, left, right_parent, right, **kw ): diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py index d09bb28bbe..f4b873ecfa 100644 --- a/lib/sqlalchemy/sql/type_api.py +++ b/lib/sqlalchemy/sql/type_api.py @@ -535,7 +535,7 @@ class TypeEngine(Traversible): return dialect.type_descriptor(self) @util.memoized_property - def _gen_cache_key(self): + def _static_cache_key(self): names = util.get_cls_kwargs(self.__class__) return (self.__class__,) + tuple( (k, self.__dict__[k]) diff --git a/lib/sqlalchemy/sql/visitors.py b/lib/sqlalchemy/sql/visitors.py index 8c06eb8afd..dcded3484f 100644 --- a/lib/sqlalchemy/sql/visitors.py +++ b/lib/sqlalchemy/sql/visitors.py @@ -216,12 +216,20 @@ class InternalTraversal(util.with_metaclass(_InternalTraversalType, object)): try: dispatcher = target.__class__.__dict__[generate_dispatcher_name] except KeyError: - dispatcher = _generate_dispatcher( - self, internal_dispatch, generate_dispatcher_name + dispatcher = self.generate_dispatch( + target, internal_dispatch, generate_dispatcher_name ) - setattr(target.__class__, generate_dispatcher_name, dispatcher) return dispatcher(target, self) + def generate_dispatch( + self, target, internal_dispatch, generate_dispatcher_name + ): + dispatcher = _generate_dispatcher( + self, internal_dispatch, generate_dispatcher_name + ) + setattr(target.__class__, generate_dispatcher_name, dispatcher) + return dispatcher + dp_has_cache_key = symbol("HC") """Visit a :class:`.HasCacheKey` object.""" @@ -331,11 +339,6 @@ class InternalTraversal(util.with_metaclass(_InternalTraversalType, object)): """ - dp_annotations_state = symbol("A") - """Visit the state of the :class:`.Annotatated` version of an object. - - """ - dp_named_ddl_element = symbol("DD") """Visit a simple named DDL element. diff --git a/test/aaa_profiling/test_misc.py b/test/aaa_profiling/test_misc.py index c2b6f3d08e..fdb40c2e62 100644 --- a/test/aaa_profiling/test_misc.py +++ b/test/aaa_profiling/test_misc.py @@ -1,4 +1,16 @@ +from sqlalchemy import Column from sqlalchemy import Enum +from sqlalchemy import ForeignKey +from sqlalchemy import Integer +from sqlalchemy import MetaData +from sqlalchemy import select +from sqlalchemy import String +from sqlalchemy import Table +from sqlalchemy import testing +from sqlalchemy.orm import join as ormjoin +from sqlalchemy.orm import mapper +from sqlalchemy.orm import relationship +from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures from sqlalchemy.testing import profiling from sqlalchemy.util import classproperty @@ -35,3 +47,70 @@ class EnumTest(fixtures.TestBase): @profiling.function_call_count() def test_create_enum_from_pep_435_w_expensive_members(self): Enum(self.SomeEnum) + + +class CacheKeyTest(fixtures.TestBase): + __requires__ = ("cpython",) + + @testing.fixture(scope="class") + def mapping_fixture(self): + # note in order to work nicely with "fixture" we are emerging + # a whole new model of setup/teardown, since pytest "fixture" + # sort of purposely works badly with setup/teardown + + metadata = MetaData() + parent = Table( + "parent", + metadata, + Column("id", Integer, primary_key=True), + Column("data", String(20)), + ) + child = Table( + "child", + metadata, + Column("id", Integer, primary_key=True), + Column("data", String(20)), + Column( + "parent_id", Integer, ForeignKey("parent.id"), nullable=False + ), + ) + + class Parent(testing.entities.BasicEntity): + pass + + class Child(testing.entities.BasicEntity): + pass + + mapper( + Parent, + parent, + properties={"children": relationship(Child, backref="parent")}, + ) + mapper(Child, child) + + return Parent, Child + + @testing.fixture(scope="function") + def stmt_fixture_one(self, mapping_fixture): + # note that by using ORM elements we will have annotations in these + # items also which is part of the performance hit + Parent, Child = mapping_fixture + + return [ + ( + select([Parent.id, Child.id]) + .select_from(ormjoin(Parent, Child, Parent.children)) + .where(Child.id == 5) + ) + for i in range(100) + ] + + @profiling.function_call_count() + def test_statement_one(self, stmt_fixture_one): + current_key = None + for stmt in stmt_fixture_one: + key = stmt._generate_cache_key() + if current_key: + eq_(key, current_key) + else: + current_key = key diff --git a/test/profiles.txt b/test/profiles.txt index 721ce9677c..493f276983 100644 --- a/test/profiles.txt +++ b/test/profiles.txt @@ -1,15 +1,15 @@ # /home/classic/dev/sqlalchemy/test/profiles.txt # This file is written out on a per-environment basis. -# For each test in aaa_profiling, the corresponding function and +# For each test in aaa_profiling, the corresponding function and # environment is located within this file. If it doesn't exist, # the test is skipped. -# If a callcount does exist, it is compared to what we received. +# If a callcount does exist, it is compared to what we received. # assertions are raised if the counts do not match. -# -# To add a new callcount test, apply the function_call_count -# decorator and re-run the tests using the --write-profiles +# +# To add a new callcount test, apply the function_call_count +# decorator and re-run the tests using the --write-profiles # option - this file will be rewritten including the new count. -# +# # TEST: test.aaa_profiling.test_compiler.CompileTest.test_insert @@ -136,6 +136,10 @@ test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.7_postgre test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.7_sqlite_pysqlite_dbapiunicode_cextensions 162 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 162 +# TEST: test.aaa_profiling.test_misc.CacheKeyTest.test_statement_one + +test.aaa_profiling.test_misc.CacheKeyTest.test_statement_one 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 5173 + # TEST: test.aaa_profiling.test_misc.EnumTest.test_create_enum_from_pep_435_w_expensive_members test.aaa_profiling.test_misc.EnumTest.test_create_enum_from_pep_435_w_expensive_members 2.7_mssql_pyodbc_dbapiunicode_nocextensions 1325