diff options
author | Jason Madden <jamadden@gmail.com> | 2020-03-17 08:28:12 -0500 |
---|---|---|
committer | Jason Madden <jamadden@gmail.com> | 2020-03-17 15:14:37 -0500 |
commit | 7e532de0bc9475294d2dcfd7ebea50a80cb8c7f7 (patch) | |
tree | 8204bf08f3af554c58a5dd28baefc8909faa954c | |
parent | 11a1d01945fd486ba63209f3358fd3313ff1e25f (diff) | |
download | zope-interface-issue8.tar.gz |
Move the one-base optimization down a level, and enable using pre-calculated __sro__ for caching.issue8
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.py | 31 | ||||
-rw-r--r-- | src/zope/interface/ro.py | 65 | ||||
-rw-r--r-- | src/zope/interface/tests/test_ro.py | 21 |
3 files changed, 84 insertions, 33 deletions
diff --git a/src/zope/interface/interface.py b/src/zope/interface/interface.py index be36d35..3a2e6af 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 @@ -311,29 +311,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 @@ -694,6 +694,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): |