Skip to content

Commit

Permalink
Move the one-base optimization down a level, and enable using pre-cal…
Browse files Browse the repository at this point in the history
…culated __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.
  • Loading branch information
jamadden committed Mar 18, 2020
1 parent 4c4e1c9 commit 9e39907
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 33 deletions.
31 changes: 16 additions & 15 deletions src/zope/interface/interface.py
Expand Up @@ -21,8 +21,8 @@

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
Expand Down Expand Up @@ -322,29 +322,29 @@ def _calculate_sro(self):
#
# 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

Expand Down Expand Up @@ -705,6 +705,7 @@ def __ge__(self, other):
Interface.changed(Interface)
assert Interface.__sro__ == (Interface,)
Specification._ROOT = Interface
ro._ROOT = Interface


class Attribute(Element):
Expand Down
65 changes: 47 additions & 18 deletions src/zope/interface/ro.py
Expand Up @@ -189,19 +189,39 @@ def __get__(self, inst, klass):
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:
factory = _StrictC3
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
Expand Down Expand Up @@ -229,6 +249,9 @@ def __init__(self, C, memo):

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
Expand Down Expand Up @@ -520,10 +543,9 @@ def __str__(self):
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)
Expand All @@ -547,28 +569,26 @@ def __str__(self):
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
Add the *strict*, *log_changed_ro* and *use_legacy_ro*
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
Expand All @@ -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",
Expand All @@ -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
21 changes: 21 additions & 0 deletions src/zope/interface/tests/test_ro.py
Expand Up @@ -375,6 +375,27 @@ def test_non_orderable(self):
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):
Expand Down

0 comments on commit 9e39907

Please sign in to comment.