summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2020-03-17 08:28:12 -0500
committerJason Madden <jamadden@gmail.com>2020-03-18 12:06:17 -0500
commit9e399071f0d033e36dd92cf2cc35d7c09505f9af (patch)
tree9ea0bc31b3c324cb18cd4da72aa4f1aab890ee2f
parent4c4e1c985ffa710482cfeebaf1cb28b470bab47e (diff)
downloadzope-interface-9e399071f0d033e36dd92cf2cc35d7c09505f9af.tar.gz
Move the one-base optimization down a level, and enable using pre-calculated __sro__ for caching.
In my local 'load the world' test, this went from ~7800 full C3 merges to about ~1100. Also take steps to avoid triggering false positive warnings about changed ROs when it's *just* Interface that moved.
-rw-r--r--src/zope/interface/interface.py31
-rw-r--r--src/zope/interface/ro.py65
-rw-r--r--src/zope/interface/tests/test_ro.py21
3 files changed, 84 insertions, 33 deletions
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py
index 34f54ba..a8eac01 100644
--- a/src/zope/interface/interface.py
+++ b/src/zope/interface/interface.py
@@ -21,8 +21,8 @@ import weakref
from zope.interface._compat import _use_c_impl
from zope.interface.exceptions import Invalid
-from zope.interface.ro import ro
-from zope.interface.ro import C3
+from zope.interface.ro import ro as calculate_ro
+from zope.interface import ro
__all__ = [
# Most of the public API from this module is directly exported
@@ -322,29 +322,29 @@ class Specification(SpecificationBase):
#
# So we force the issue by mutating the resolution order.
- # TODO: Caching. Perhaps make ro.C3 able to re-use the computed ``__sro__``
- # instead of re-doing it for the entire tree.
- base_count = len(self._bases)
+ # Note that we let C3 use pre-computed __sro__ for our bases.
+ # This requires that by the time this method is invoked, our bases
+ # have settled their SROs. Thus, ``changed()`` must first
+ # update itself before telling its descendents of changes.
+ sro = calculate_ro(self, base_mros={
+ b: b.__sro__
+ for b in self.__bases__
+ })
+ root = self._ROOT
+ if root is not None and sro and sro[-1] is not root:
+ # In one dataset of 1823 Interface objects, 1117 ClassProvides objects,
+ # sro[-1] was root 4496 times, and only not root 118 times. So it's
+ # probably worth checking.
- if base_count == 1:
- # Fast path: One base makes it trivial to calculate
- # the MRO.
- sro = [self]
- sro.extend(self.__bases__[0].__sro__)
- else:
- sro = ro(self)
- if self._ROOT is not None:
# Once we don't have to deal with old-style classes,
# we can add a check and only do this if base_count > 1,
# if we tweak the bootstrapping for ``<implementedBy object>``
- root = self._ROOT
sro = [
x
for x in sro
if x is not root
]
sro.append(root)
- assert sro[-1] is root, sro
return sro
@@ -705,6 +705,7 @@ Interface._calculate_sro = lambda: (Interface,)
Interface.changed(Interface)
assert Interface.__sro__ == (Interface,)
Specification._ROOT = Interface
+ro._ROOT = Interface
class Attribute(Element):
diff --git a/src/zope/interface/ro.py b/src/zope/interface/ro.py
index dbffb53..7cb4f55 100644
--- a/src/zope/interface/ro.py
+++ b/src/zope/interface/ro.py
@@ -189,11 +189,25 @@ class _ClassBoolFromEnv(object):
return val
+class _StaticMRO(object):
+ # A previously resolved MRO, supplied by the caller.
+ # Used in place of calculating it.
+
+ had_inconsistency = None # We don't know...
+
+ def __init__(self, C, mro):
+ self.leaf = C
+ self.__mro = tuple(mro)
+
+ def mro(self):
+ return list(self.__mro)
+
+
class C3(object):
# Holds the shared state during computation of an MRO.
@staticmethod
- def resolver(C, strict):
+ def resolver(C, strict, base_mros):
strict = strict if strict is not None else C3.STRICT_RO
factory = C3
if strict:
@@ -201,7 +215,13 @@ class C3(object):
elif C3.TRACK_BAD_IRO:
factory = _TrackingC3
- return factory(C, {})
+ memo = {}
+ base_mros = base_mros or {}
+ for base, mro in base_mros.items():
+ assert base in C.__bases__
+ memo[base] = _StaticMRO(base, mro)
+
+ return factory(C, memo)
__mro = None
__legacy_ro = None
@@ -229,6 +249,9 @@ class C3(object):
self.bases_had_inconsistency = any(base.had_inconsistency for base in base_resolvers)
+ if len(C.__bases__) == 1:
+ self.__mro = [C] + memo[C.__bases__[0]].mro()
+
@property
def had_inconsistency(self):
return self.direct_inconsistency or self.bases_had_inconsistency
@@ -520,10 +543,9 @@ class _ROComparison(object):
left_lines = [str(x) for x in legacy_report]
right_lines = [str(x) for x in c3_report]
- # We have the same number of non-empty lines as we do items
- # in the resolution order.
- assert len(list(filter(None, left_lines))) == len(self.c3_ro)
- assert len(list(filter(None, right_lines))) == len(self.c3_ro)
+ # We have the same number of lines in the report; this is not
+ # necessarily the same as the number of items in either RO.
+ assert len(left_lines) == len(right_lines)
padding = ' ' * 2
max_left = max(len(x) for x in left_lines)
@@ -547,20 +569,16 @@ class _ROComparison(object):
return '\n'.join(lines)
-def ro(C, strict=None, log_changed_ro=None, use_legacy_ro=None):
+# Set to `Interface` once it is defined. This is used to
+# avoid logging false positives about changed ROs.
+_ROOT = None
+
+def ro(C, strict=None, base_mros=None, log_changed_ro=None, use_legacy_ro=None):
"""
ro(C) -> list
Compute the precedence list (mro) according to C3.
- As an implementation note, this always calculates the full MRO by
- examining all the bases recursively. If there are special cases
- that can reuse pre-calculated partial MROs, such as a
- ``__bases__`` of length one, the caller is responsible for
- optimizing that. (This is because this function doesn't know how
- to get the complete MRO of a base; it only knows how to get their
- ``__bases__``.)
-
:return: A fresh `list` object.
.. versionchanged:: 5.0.0
@@ -568,7 +586,9 @@ def ro(C, strict=None, log_changed_ro=None, use_legacy_ro=None):
keyword arguments. These are provisional and likely to be
removed in the future. They are most useful for testing.
"""
- resolver = C3.resolver(C, strict)
+ # The ``base_mros`` argument is for internal optimization and
+ # not documented.
+ resolver = C3.resolver(C, strict, base_mros)
mro = resolver.mro()
log_changed = log_changed_ro if log_changed_ro is not None else resolver.LOG_CHANGED_IRO
@@ -578,7 +598,16 @@ def ro(C, strict=None, log_changed_ro=None, use_legacy_ro=None):
legacy_ro = resolver.legacy_ro
assert isinstance(legacy_ro, list)
assert isinstance(mro, list)
- if legacy_ro != mro:
+ changed = legacy_ro != mro
+ if changed:
+ # Did only Interface move? The fix for issue #8 made that
+ # somewhat common. It's almost certainly not a problem, though,
+ # so allow ignoring it.
+ legacy_without_root = [x for x in legacy_ro if x is not _ROOT]
+ mro_without_root = [x for x in mro if x is not _ROOT]
+ changed = legacy_without_root != mro_without_root
+
+ if changed:
comparison = _ROComparison(resolver, mro, legacy_ro)
_logger().warning(
"Object %r has different legacy and C3 MROs:\n%s",
@@ -602,4 +631,4 @@ def is_consistent(C):
Check if the resolution order for *C*, as computed by :func:`ro`, is consistent
according to C3.
"""
- return not C3.resolver(C, False).had_inconsistency
+ return not C3.resolver(C, False, None).had_inconsistency
diff --git a/src/zope/interface/tests/test_ro.py b/src/zope/interface/tests/test_ro.py
index 3a516b5..ce0a6f4 100644
--- a/src/zope/interface/tests/test_ro.py
+++ b/src/zope/interface/tests/test_ro.py
@@ -375,6 +375,27 @@ Object <InterfaceClass zope.interface.tests.test_ro.A> has different legacy and
self.assertEqual(iro, legacy_iro)
+class TestC3(unittest.TestCase):
+ def _makeOne(self, C, strict=False, base_mros=None):
+ from zope.interface.ro import C3
+ return C3.resolver(C, strict, base_mros)
+
+ def test_base_mros_given(self):
+ c3 = self._makeOne(type(self), base_mros={unittest.TestCase: unittest.TestCase.__mro__})
+ memo = c3.memo
+ self.assertIn(unittest.TestCase, memo)
+ # We used the StaticMRO class
+ self.assertIsNone(memo[unittest.TestCase].had_inconsistency)
+
+ def test_one_base_optimization(self):
+ c3 = self._makeOne(type(self))
+ # Even though we didn't call .mro() yet, the MRO has been
+ # computed.
+ self.assertIsNotNone(c3._C3__mro) # pylint:disable=no-member
+ c3._merge = None
+ self.assertEqual(c3.mro(), list(type(self).__mro__))
+
+
class Test_ROComparison(unittest.TestCase):
class MockC3(object):