Patchwork D3419: interfaceutil: module to stub out zope.interface

login
register
mail settings
Submitter phabricator
Date April 22, 2018, 8 p.m.
Message ID <differential-rev-PHID-DREV-hsv6372ij3zpxezny5t3-req@phab.mercurial-scm.org>
Download mbox | patch
Permalink /patch/31208/
State Superseded
Headers show

Comments

phabricator - April 22, 2018, 8 p.m.
indygreg created this revision.
Herald added a subscriber: mercurial-devel.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  The startup time of `hg` increased during the 4.6 development cycle. A
  cause of that was importing more modules and doing more work at module
  import time.
  
  The import of zope.interface and the declaring of various interfaces
  is partially responsible for the startup time regression.
  
  Our current usage of zope.interface doesn't do much at run time: we are
  merely declaring interfaces and stating that certain types implement
  various interfaces. Core Mercurial is not (yet) using of any of
  zope.interface features that actually require that interface plumbing be
  defined. The only place we actually need the interface metadata is in
  test-check-interfaces.py.
  
  This commit establishes a new interfaceutil module. It exposes the subset
  of the zope.interface API that we currently use. By default, the APIs
  no-op. But if an environment variable is set, we export the real
  zope.interface APIs.
  
  Existing importers of zope.interface have been converted to use the new
  module. test-check-interfaces.py has been updated to define the
  environment variable so the real zope.interface is used.
  
  The net effect of this change is we stop importing 9 zope.interface.*
  modules and we no longer perform interface bookkeeping when registering
  interfaces.
  
  On my i7-6700K on Linux, a shell loop that runs `hg log -r .` 300 times
  on a repo with 1 commit shows a significant CPU time improvement
  (average of 4 runs):
  
  4.5:    14.814s
  before: 19.028s
  after:  16.945s
  
  And with `run-tests.py -j10` (single run):
  
  4.5:    ~3100s (~51.7m)
  before: ~4450s (~74.2m)
  after:  ~3980s (~66.3m)
  
  So this claws back about half of the regressions in 4.6.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D3419

AFFECTED FILES
  mercurial/filelog.py
  mercurial/httppeer.py
  mercurial/localrepo.py
  mercurial/repository.py
  mercurial/utils/interfaceutil.py
  mercurial/wireprotoserver.py
  mercurial/wireprototypes.py
  mercurial/wireprotov1peer.py
  mercurial/wireprotov2server.py
  tests/test-check-interfaces.py
  tests/test-check-module-imports.t

CHANGE DETAILS




To: indygreg, #hg-reviewers
Cc: mercurial-devel
phabricator - April 30, 2018, 3:52 p.m.
lothiraldan added a comment.


  We also detected the regression in 4.6rc0 (http://perf.octobus.net/#basic_commands.DiscoveryTimeSuite.time_debugdiscovery?branch=stable) and can confirm locally that half the regression is fixed on stable (https://phab.mercurial-scm.org/rHGe82b137a8b4ea4f28928d6312b84d95aa66e0519).
  
  Do you have any idea where the second half of regressions come from?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D3419

To: indygreg, #hg-reviewers, durin42
Cc: lothiraldan, mercurial-devel
phabricator - April 30, 2018, 4:10 p.m.
indygreg added a comment.


  I'm not sure where the 2nd half of the start-up time regressions came from. I had a hacky test where I printed `sys.modules` keys after command execution. It resulted in the following:
  
    46,68d45
    < email
    < email.Charset
    < email.Encoders
    < email.Errors
    < email.FeedParser
    < email.Generator
    < email.Header
    < email.Iterators
    < email.MIMEAudio
    < email.MIMEBase
    < email.MIMEImage
    < email.MIMEMessage
    < email.MIMEMultipart
    < email.MIMENonMultipart
    < email.MIMEText
    < email.Message
    < email.Parser
    < email.Utils
    < email.base64MIME
    < email.email
    < email.mime
    < email.quopriMIME
    < email.sys
    89a67
    > inspect
    101d78
    < mercurial.cext.diffhelpers
    124a102
    > mercurial.logcmdutil
    127a106
    > mercurial.narrowspec
    152a132
    > mercurial.templatefuncs
    153a134,135
    > mercurial.templater
    > mercurial.templateutil
    160a143,152
    > mercurial.thirdparty.concurrent
    > mercurial.thirdparty.zope
    > mercurial.thirdparty.zope.interface
    > mercurial.thirdparty.zope.interface._compat
    > mercurial.thirdparty.zope.interface._zope_interface_coptimizations
    > mercurial.thirdparty.zope.interface.declarations
    > mercurial.thirdparty.zope.interface.exceptions
    > mercurial.thirdparty.zope.interface.interface
    > mercurial.thirdparty.zope.interface.interfaces
    > mercurial.thirdparty.zope.interface.ro
    164a157,160
    > mercurial.utils
    > mercurial.utils.dateutil
    > mercurial.utils.procutil
    > mercurial.utils.stringutil
  
  I'm not convinced the total number of modules is responsible: I think it has more to do with the volume of code being imported. But I could be wrong. These things are a bit challenging to profile.
  
  And for reference, all modules that were imported in 4.6rc after running (I think it was `hg log -r .`):
  
    Queue
    UserDict
    __builtin__
    __future__
    __main__
    _abcoll
    _codecs
    _collections
    _curses
    _functools
    _hashlib
    _heapq
    _locale
    _socket
    _sre
    _ssl
    _struct
    _sysconfigdata
    _warnings
    _weakref
    _weakrefset
    abc
    bdb
    binascii
    cStringIO
    cmd
    codecs
    collections
    contextlib
    copy
    copy_reg
    curses
    curses._curses
    curses.curses
    curses.wrapper
    datetime
    encodings
    encodings.__builtin__
    encodings.aliases
    encodings.ascii
    encodings.codecs
    encodings.encodings
    encodings.utf_8
    errno
    exceptions
    fcntl
    functools
    gc
    genericpath
    getopt
    gettext
    grp
    hashlib
    heapq
    hgdemandimport
    hgdemandimport.demandimportpy2
    imp
    inspect
    itertools
    keyword
    linecache
    locale
    mercurial
    mercurial.__modulepolicy__
    mercurial.bookmarks
    mercurial.branchmap
    mercurial.cext
    mercurial.cext.base85
    mercurial.cext.bdiff
    mercurial.cext.mpatch
    mercurial.cext.osutil
    mercurial.cext.parsers
    mercurial.changelog
    mercurial.cmdutil
    mercurial.color
    mercurial.commands
    mercurial.config
    mercurial.configitems
    mercurial.context
    mercurial.debugcommands
    mercurial.dirstate
    mercurial.dispatch
    mercurial.encoding
    mercurial.error
    mercurial.extensions
    mercurial.fancyopts
    mercurial.formatter
    mercurial.hg
    mercurial.hook
    mercurial.i18n
    mercurial.localrepo
    mercurial.lock
    mercurial.logcmdutil
    mercurial.match
    mercurial.mdiff
    mercurial.namespaces
    mercurial.narrowspec
    mercurial.node
    mercurial.obsolete
    mercurial.obsutil
    mercurial.parser
    mercurial.patch
    mercurial.pathutil
    mercurial.phases
    mercurial.policy
    mercurial.posix
    mercurial.profiling
    mercurial.pure
    mercurial.pycompat
    mercurial.rcutil
    mercurial.registrar
    mercurial.repository
    mercurial.repoview
    mercurial.revlog
    mercurial.revset
    mercurial.revsetlang
    mercurial.scmposix
    mercurial.scmutil
    mercurial.smartset
    mercurial.store
    mercurial.tags
    mercurial.templatefilters
    mercurial.templatefuncs
    mercurial.templatekw
    mercurial.templater
    mercurial.templateutil
    mercurial.thirdparty
    mercurial.thirdparty.attr
    mercurial.thirdparty.attr._compat
    mercurial.thirdparty.attr._config
    mercurial.thirdparty.attr._funcs
    mercurial.thirdparty.attr._make
    mercurial.thirdparty.attr.exceptions
    mercurial.thirdparty.concurrent
    mercurial.thirdparty.zope
    mercurial.thirdparty.zope.interface
    mercurial.thirdparty.zope.interface._compat
    mercurial.thirdparty.zope.interface._zope_interface_coptimizations
    mercurial.thirdparty.zope.interface.declarations
    mercurial.thirdparty.zope.interface.exceptions
    mercurial.thirdparty.zope.interface.interface
    mercurial.thirdparty.zope.interface.interfaces
    mercurial.thirdparty.zope.interface.ro
    mercurial.txnutil
    mercurial.ui
    mercurial.urllibcompat
    mercurial.util
    mercurial.utils
    mercurial.utils.dateutil
    mercurial.utils.procutil
    mercurial.utils.stringutil
    mercurial.vfs
    mercurial.zstd
    operator
    os
    os.path
    pdb
    platform
    posix
    posixpath
    pwd
    re
    repr
    shlex
    signal
    site
    socket
    sre_compile
    sre_constants
    sre_parse
    stat
    string
    strop
    struct
    sys
    sysconfig
    thread
    time
    traceback
    types
    urllib
    urlparse
    warnings
    weakref
    zipimport
    zlib
  
  I didn't record whether these are actually imported or our module proxy stub used by the custom module importer. Sorry.
  
  I do have some patches to improve efficiency of the importer. I don't feel comfortable sending them to stable unless we commit to doing another 4.6 RC since they are API breaking. And since release is delayed by a week, maybe that is justified. @durin42?

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D3419

To: indygreg, #hg-reviewers, durin42
Cc: lothiraldan, mercurial-devel
phabricator - April 30, 2018, 4:39 p.m.
durin42 added a comment.


  In https://phab.mercurial-scm.org/D3419#54662, @indygreg wrote:
  
  > I do have some patches to improve efficiency of the importer. I don't feel comfortable sending them to stable unless we commit to doing another 4.6 RC since they are API breaking. And since release is delayed by a week, maybe that is justified. @durin42?
  
  
  I'd rather we reopen default and start doing normal development work there, and keep the 4.6 release as just rc1+security patches.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D3419

To: indygreg, #hg-reviewers, durin42
Cc: lothiraldan, mercurial-devel
phabricator - May 2, 2018, 1:53 p.m.
lothiraldan added a comment.


  We saw a ~15ms regression hit on our performance benchmark for the incoming test between 4.5.3 and 4.6rc0. This regression is ~30ms when the test is over ssh.
  
  We are still running tests against 4.6rc1 because it was available late on https://www.mercurial-scm.org/repo/hg/

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D3419

To: indygreg, #hg-reviewers, durin42
Cc: lothiraldan, mercurial-devel

Patch

diff --git a/tests/test-check-module-imports.t b/tests/test-check-module-imports.t
--- a/tests/test-check-module-imports.t
+++ b/tests/test-check-module-imports.t
@@ -27,6 +27,7 @@ 
   > -X i18n/posplit \
   > -X mercurial/thirdparty \
   > -X tests/hypothesishelpers.py \
+  > -X tests/test-check-interfaces.py \
   > -X tests/test-commit-interactive.t \
   > -X tests/test-contrib-check-code.t \
   > -X tests/test-demandimport.py \
diff --git a/tests/test-check-interfaces.py b/tests/test-check-interfaces.py
--- a/tests/test-check-interfaces.py
+++ b/tests/test-check-interfaces.py
@@ -2,6 +2,9 @@ 
 
 from __future__ import absolute_import, print_function
 
+from mercurial import encoding
+encoding.environ[b'HGREALINTERFACES'] = b'1'
+
 import os
 
 from mercurial.thirdparty.zope import (
diff --git a/mercurial/wireprotov2server.py b/mercurial/wireprotov2server.py
--- a/mercurial/wireprotov2server.py
+++ b/mercurial/wireprotov2server.py
@@ -12,9 +12,6 @@ 
 from .thirdparty import (
     cbor,
 )
-from .thirdparty.zope import (
-    interface as zi,
-)
 from . import (
     encoding,
     error,
@@ -24,6 +21,9 @@ 
     wireprotoframing,
     wireprototypes,
 )
+from .utils import (
+    interfaceutil,
+)
 
 FRAMINGTYPE = b'application/mercurial-exp-framing-0005'
 
@@ -340,7 +340,7 @@ 
 
     return func(repo, proto, **args)
 
-@zi.implementer(wireprototypes.baseprotocolhandler)
+@interfaceutil.implementer(wireprototypes.baseprotocolhandler)
 class httpv2protocolhandler(object):
     def __init__(self, req, ui, args=None):
         self._req = req
diff --git a/mercurial/wireprotov1peer.py b/mercurial/wireprotov1peer.py
--- a/mercurial/wireprotov1peer.py
+++ b/mercurial/wireprotov1peer.py
@@ -15,9 +15,6 @@ 
 from .node import (
     bin,
 )
-from .thirdparty.zope import (
-    interface as zi,
-)
 from . import (
     bundle2,
     changegroup as changegroupmod,
@@ -29,6 +26,9 @@ 
     util,
     wireprototypes,
 )
+from .utils import (
+    interfaceutil,
+)
 
 urlreq = util.urlreq
 
@@ -110,7 +110,7 @@ 
         # on that.
         return self.result(timeout)
 
-@zi.implementer(repository.ipeercommandexecutor)
+@interfaceutil.implementer(repository.ipeercommandexecutor)
 class peerexecutor(object):
     def __init__(self, peer):
         self._peer = peer
@@ -308,7 +308,8 @@ 
             else:
                 f.set_result(result)
 
-@zi.implementer(repository.ipeercommands, repository.ipeerlegacycommands)
+@interfaceutil.implementer(repository.ipeercommands,
+                           repository.ipeerlegacycommands)
 class wirepeer(repository.peer):
     """Client-side interface for communicating with a peer repository.
 
diff --git a/mercurial/wireprototypes.py b/mercurial/wireprototypes.py
--- a/mercurial/wireprototypes.py
+++ b/mercurial/wireprototypes.py
@@ -9,14 +9,14 @@ 
     bin,
     hex,
 )
-from .thirdparty.zope import (
-    interface as zi,
-)
 from .i18n import _
 from . import (
     error,
     util,
 )
+from .utils import (
+    interfaceutil,
+)
 
 # Names of the SSH protocol implementations.
 SSHV1 = 'ssh-v1'
@@ -179,16 +179,16 @@ 
     'stream': 'boolean',
 }
 
-class baseprotocolhandler(zi.Interface):
+class baseprotocolhandler(interfaceutil.Interface):
     """Abstract base class for wire protocol handlers.
 
     A wire protocol handler serves as an interface between protocol command
     handlers and the wire protocol transport layer. Protocol handlers provide
     methods to read command arguments, redirect stdio for the duration of
     the request, handle response types, etc.
     """
 
-    name = zi.Attribute(
+    name = interfaceutil.Attribute(
         """The name of the protocol implementation.
 
         Used for uniquely identifying the transport type.
diff --git a/mercurial/wireprotoserver.py b/mercurial/wireprotoserver.py
--- a/mercurial/wireprotoserver.py
+++ b/mercurial/wireprotoserver.py
@@ -15,9 +15,6 @@ 
 from .thirdparty import (
     cbor,
 )
-from .thirdparty.zope import (
-    interface as zi,
-)
 from . import (
     encoding,
     error,
@@ -29,6 +26,7 @@ 
     wireprotov2server,
 )
 from .utils import (
+    interfaceutil,
     procutil,
 )
 
@@ -62,7 +60,7 @@ 
 
     return ''.join(chunks)
 
-@zi.implementer(wireprototypes.baseprotocolhandler)
+@interfaceutil.implementer(wireprototypes.baseprotocolhandler)
 class httpv1protocolhandler(object):
     def __init__(self, req, ui, checkperm):
         self._req = req
@@ -489,7 +487,7 @@ 
     fout.write(b'\n')
     fout.flush()
 
-@zi.implementer(wireprototypes.baseprotocolhandler)
+@interfaceutil.implementer(wireprototypes.baseprotocolhandler)
 class sshv1protocolhandler(object):
     """Handler for requests services via version 1 of SSH protocol."""
     def __init__(self, ui, fin, fout):
diff --git a/mercurial/utils/interfaceutil.py b/mercurial/utils/interfaceutil.py
new file mode 100644
--- /dev/null
+++ b/mercurial/utils/interfaceutil.py
@@ -0,0 +1,40 @@ 
+# interfaceutil.py - Utilities for declaring interfaces.
+#
+# Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+# zope.interface imposes a run-time cost due to module import overhead and
+# bookkeeping for declaring interfaces. So, we use stubs for various
+# zope.interface primitives unless instructed otherwise.
+
+from __future__ import absolute_import
+
+from .. import (
+    encoding,
+)
+
+if encoding.environ.get('HGREALINTERFACES'):
+    from ..thirdparty.zope import (
+        interface as zi,
+    )
+
+    Attribute = zi.Attribute
+    Interface = zi.Interface
+    implementer = zi.implementer
+else:
+    class Attribute(object):
+        def __init__(self, __name__, __doc__=''):
+            pass
+
+    class Interface(object):
+        def __init__(self, name, bases=(), attrs=None, __doc__=None,
+                 __module__=None):
+            pass
+
+    def implementer(*ifaces):
+        def wrapper(cls):
+            return cls
+
+        return wrapper
diff --git a/mercurial/repository.py b/mercurial/repository.py
--- a/mercurial/repository.py
+++ b/mercurial/repository.py
@@ -8,23 +8,23 @@ 
 from __future__ import absolute_import
 
 from .i18n import _
-from .thirdparty.zope import (
-    interface as zi,
-)
 from . import (
     error,
 )
+from .utils import (
+    interfaceutil,
+)
 
-class ipeerconnection(zi.Interface):
+class ipeerconnection(interfaceutil.Interface):
     """Represents a "connection" to a repository.
 
     This is the base interface for representing a connection to a repository.
     It holds basic properties and methods applicable to all peer types.
 
     This is not a complete interface definition and should not be used
     outside of this module.
     """
-    ui = zi.Attribute("""ui.ui instance""")
+    ui = interfaceutil.Attribute("""ui.ui instance""")
 
     def url():
         """Returns a URL string representing this peer.
@@ -61,7 +61,7 @@ 
         associated with the peer should be cleaned up.
         """
 
-class ipeercapabilities(zi.Interface):
+class ipeercapabilities(interfaceutil.Interface):
     """Peer sub-interface related to capabilities."""
 
     def capable(name):
@@ -81,7 +81,7 @@ 
         Raises a ``CapabilityError`` if the capability isn't present.
         """
 
-class ipeercommands(zi.Interface):
+class ipeercommands(interfaceutil.Interface):
     """Client-side interface for communicating over the wire protocol.
 
     This interface is used as a gateway to the Mercurial wire protocol.
@@ -170,7 +170,7 @@ 
         Returns the integer number of heads added to the peer.
         """
 
-class ipeerlegacycommands(zi.Interface):
+class ipeerlegacycommands(interfaceutil.Interface):
     """Interface for implementing support for legacy wire protocol commands.
 
     Wire protocol commands transition to legacy status when they are no longer
@@ -202,7 +202,7 @@ 
     def changegroupsubset(bases, heads, source):
         pass
 
-class ipeercommandexecutor(zi.Interface):
+class ipeercommandexecutor(interfaceutil.Interface):
     """Represents a mechanism to execute remote commands.
 
     This is the primary interface for requesting that wire protocol commands
@@ -259,7 +259,7 @@ 
         This method may call ``sendcommands()`` if there are buffered commands.
         """
 
-class ipeerrequests(zi.Interface):
+class ipeerrequests(interfaceutil.Interface):
     """Interface for executing commands on a peer."""
 
     def commandexecutor():
@@ -290,7 +290,7 @@ 
     All peer instances must conform to this interface.
     """
 
-@zi.implementer(ipeerbase)
+@interfaceutil.implementer(ipeerbase)
 class peer(object):
     """Base class for peer repositories."""
 
@@ -314,7 +314,7 @@ 
             _('cannot %s; remote repository does not support the %r '
               'capability') % (purpose, name))
 
-class ifilerevisionssequence(zi.Interface):
+class ifilerevisionssequence(interfaceutil.Interface):
     """Contains index data for all revisions of a file.
 
     Types implementing this behave like lists of tuples. The index
@@ -365,7 +365,7 @@ 
     def insert(self, i, entry):
         """Add an item to the index at specific revision."""
 
-class ifileindex(zi.Interface):
+class ifileindex(interfaceutil.Interface):
     """Storage interface for index data of a single file.
 
     File storage data is divided into index metadata and data storage.
@@ -377,7 +377,7 @@ 
     * DAG data (storing and querying the relationship between nodes).
     * Metadata to facilitate storage.
     """
-    index = zi.Attribute(
+    index = interfaceutil.Attribute(
         """An ``ifilerevisionssequence`` instance.""")
 
     def __len__():
@@ -470,7 +470,7 @@ 
     def candelta(baserev, rev):
         """"Whether a delta can be generated between two revisions."""
 
-class ifiledata(zi.Interface):
+class ifiledata(interfaceutil.Interface):
     """Storage interface for data storage of a specific file.
 
     This complements ``ifileindex`` and provides an interface for accessing
@@ -536,7 +536,7 @@ 
         revision data.
         """
 
-class ifilemutation(zi.Interface):
+class ifilemutation(interfaceutil.Interface):
     """Storage interface for mutation events of a tracked file."""
 
     def add(filedata, meta, transaction, linkrev, p1, p2):
@@ -608,21 +608,21 @@ 
 class ifilestorage(ifileindex, ifiledata, ifilemutation):
     """Complete storage interface for a single tracked file."""
 
-    version = zi.Attribute(
+    version = interfaceutil.Attribute(
         """Version number of storage.
 
         TODO this feels revlog centric and could likely be removed.
         """)
 
-    storedeltachains = zi.Attribute(
+    storedeltachains = interfaceutil.Attribute(
         """Whether the store stores deltas.
 
         TODO deltachains are revlog centric. This can probably removed
         once there are better abstractions for obtaining/writing
         data.
         """)
 
-    _generaldelta = zi.Attribute(
+    _generaldelta = interfaceutil.Attribute(
         """Whether deltas can be against any parent revision.
 
         TODO this is used by changegroup code and it could probably be
@@ -642,100 +642,100 @@ 
         TODO this is used by verify and it should not be part of the interface.
         """
 
-class completelocalrepository(zi.Interface):
+class completelocalrepository(interfaceutil.Interface):
     """Monolithic interface for local repositories.
 
     This currently captures the reality of things - not how things should be.
     """
 
-    supportedformats = zi.Attribute(
+    supportedformats = interfaceutil.Attribute(
         """Set of requirements that apply to stream clone.
 
         This is actually a class attribute and is shared among all instances.
         """)
 
-    openerreqs = zi.Attribute(
+    openerreqs = interfaceutil.Attribute(
         """Set of requirements that are passed to the opener.
 
         This is actually a class attribute and is shared among all instances.
         """)
 
-    supported = zi.Attribute(
+    supported = interfaceutil.Attribute(
         """Set of requirements that this repo is capable of opening.""")
 
-    requirements = zi.Attribute(
+    requirements = interfaceutil.Attribute(
         """Set of requirements this repo uses.""")
 
-    filtername = zi.Attribute(
+    filtername = interfaceutil.Attribute(
         """Name of the repoview that is active on this repo.""")
 
-    wvfs = zi.Attribute(
+    wvfs = interfaceutil.Attribute(
         """VFS used to access the working directory.""")
 
-    vfs = zi.Attribute(
+    vfs = interfaceutil.Attribute(
         """VFS rooted at the .hg directory.
 
         Used to access repository data not in the store.
         """)
 
-    svfs = zi.Attribute(
+    svfs = interfaceutil.Attribute(
         """VFS rooted at the store.
 
         Used to access repository data in the store. Typically .hg/store.
         But can point elsewhere if the store is shared.
         """)
 
-    root = zi.Attribute(
+    root = interfaceutil.Attribute(
         """Path to the root of the working directory.""")
 
-    path = zi.Attribute(
+    path = interfaceutil.Attribute(
         """Path to the .hg directory.""")
 
-    origroot = zi.Attribute(
+    origroot = interfaceutil.Attribute(
         """The filesystem path that was used to construct the repo.""")
 
-    auditor = zi.Attribute(
+    auditor = interfaceutil.Attribute(
         """A pathauditor for the working directory.
 
         This checks if a path refers to a nested repository.
 
         Operates on the filesystem.
         """)
 
-    nofsauditor = zi.Attribute(
+    nofsauditor = interfaceutil.Attribute(
         """A pathauditor for the working directory.
 
         This is like ``auditor`` except it doesn't do filesystem checks.
         """)
 
-    baseui = zi.Attribute(
+    baseui = interfaceutil.Attribute(
         """Original ui instance passed into constructor.""")
 
-    ui = zi.Attribute(
+    ui = interfaceutil.Attribute(
         """Main ui instance for this instance.""")
 
-    sharedpath = zi.Attribute(
+    sharedpath = interfaceutil.Attribute(
         """Path to the .hg directory of the repo this repo was shared from.""")
 
-    store = zi.Attribute(
+    store = interfaceutil.Attribute(
         """A store instance.""")
 
-    spath = zi.Attribute(
+    spath = interfaceutil.Attribute(
         """Path to the store.""")
 
-    sjoin = zi.Attribute(
+    sjoin = interfaceutil.Attribute(
         """Alias to self.store.join.""")
 
-    cachevfs = zi.Attribute(
+    cachevfs = interfaceutil.Attribute(
         """A VFS used to access the cache directory.
 
         Typically .hg/cache.
         """)
 
-    filteredrevcache = zi.Attribute(
+    filteredrevcache = interfaceutil.Attribute(
         """Holds sets of revisions to be filtered.""")
 
-    names = zi.Attribute(
+    names = interfaceutil.Attribute(
         """A ``namespaces`` instance.""")
 
     def close():
@@ -750,19 +750,19 @@ 
     def filtered(name, visibilityexceptions=None):
         """Obtain a named view of this repository."""
 
-    obsstore = zi.Attribute(
+    obsstore = interfaceutil.Attribute(
         """A store of obsolescence data.""")
 
-    changelog = zi.Attribute(
+    changelog = interfaceutil.Attribute(
         """A handle on the changelog revlog.""")
 
-    manifestlog = zi.Attribute(
+    manifestlog = interfaceutil.Attribute(
         """A handle on the root manifest revlog.""")
 
-    dirstate = zi.Attribute(
+    dirstate = interfaceutil.Attribute(
         """Working directory state.""")
 
-    narrowpats = zi.Attribute(
+    narrowpats = interfaceutil.Attribute(
         """Matcher patterns for this repository's narrowspec.""")
 
     def narrowmatch():
@@ -978,7 +978,7 @@ 
     def checkpush(pushop):
         pass
 
-    prepushoutgoinghooks = zi.Attribute(
+    prepushoutgoinghooks = interfaceutil.Attribute(
         """util.hooks instance.""")
 
     def pushkey(namespace, key, old, new):
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -21,9 +21,6 @@ 
     nullid,
     short,
 )
-from .thirdparty.zope import (
-    interface as zi,
-)
 from . import (
     bookmarks,
     branchmap,
@@ -68,6 +65,7 @@ 
     vfs as vfsmod,
 )
 from .utils import (
+    interfaceutil,
     procutil,
     stringutil,
 )
@@ -153,7 +151,7 @@ 
               'unbundle'}
 legacycaps = moderncaps.union({'changegroupsubset'})
 
-@zi.implementer(repository.ipeercommandexecutor)
+@interfaceutil.implementer(repository.ipeercommandexecutor)
 class localcommandexecutor(object):
     def __init__(self, peer):
         self._peer = peer
@@ -196,7 +194,7 @@ 
     def close(self):
         self._closed = True
 
-@zi.implementer(repository.ipeercommands)
+@interfaceutil.implementer(repository.ipeercommands)
 class localpeer(repository.peer):
     '''peer for a local repo; reflects only the most recent API'''
 
@@ -324,7 +322,7 @@ 
 
     # End of peer interface.
 
-@zi.implementer(repository.ipeerlegacycommands)
+@interfaceutil.implementer(repository.ipeerlegacycommands)
 class locallegacypeer(localpeer):
     '''peer extension which implements legacy methods too; used for tests with
     restricted capabilities'''
@@ -365,7 +363,7 @@ 
 # set to reflect that the extension knows how to handle that requirements.
 featuresetupfuncs = set()
 
-@zi.implementer(repository.completelocalrepository)
+@interfaceutil.implementer(repository.completelocalrepository)
 class localrepository(object):
 
     # obsolete experimental requirements:
diff --git a/mercurial/httppeer.py b/mercurial/httppeer.py
--- a/mercurial/httppeer.py
+++ b/mercurial/httppeer.py
@@ -20,9 +20,6 @@ 
 from .thirdparty import (
     cbor,
 )
-from .thirdparty.zope import (
-    interface as zi,
-)
 from . import (
     bundle2,
     error,
@@ -38,6 +35,9 @@ 
     wireprotov2peer,
     wireprotov2server,
 )
+from .utils import (
+    interfaceutil,
+)
 
 httplib = util.httplib
 urlerr = util.urlerr
@@ -582,7 +582,7 @@ 
         # will resolve to Future.result.
         return self.result(timeout)
 
-@zi.implementer(repository.ipeercommandexecutor)
+@interfaceutil.implementer(repository.ipeercommandexecutor)
 class httpv2executor(object):
     def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
         self._ui = ui
@@ -731,8 +731,9 @@ 
             pass
 
 # TODO implement interface for version 2 peers
-@zi.implementer(repository.ipeerconnection, repository.ipeercapabilities,
-                repository.ipeerrequests)
+@interfaceutil.implementer(repository.ipeerconnection,
+                           repository.ipeercapabilities,
+                           repository.ipeerrequests)
 class httpv2peer(object):
     def __init__(self, ui, repourl, apipath, opener, requestbuilder,
                  apidescriptor):
diff --git a/mercurial/filelog.py b/mercurial/filelog.py
--- a/mercurial/filelog.py
+++ b/mercurial/filelog.py
@@ -7,16 +7,16 @@ 
 
 from __future__ import absolute_import
 
-from .thirdparty.zope import (
-    interface as zi,
-)
 from . import (
     error,
     repository,
     revlog,
 )
+from .utils import (
+    interfaceutil,
+)
 
-@zi.implementer(repository.ifilestorage)
+@interfaceutil.implementer(repository.ifilestorage)
 class filelog(object):
     def __init__(self, opener, path):
         self._revlog = revlog.revlog(opener,