changeset 37810:856f381ad74b stable

interfaceutil: module to stub out zope.interface 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. Differential Revision: https://phab.mercurial-scm.org/D3419
author Gregory Szorc <gregory.szorc@gmail.com>
date Sun, 22 Apr 2018 11:54:10 -0700
parents 80695628adcb
children 51dee6fad783
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
diffstat 11 files changed, 126 insertions(+), 84 deletions(-) [+]
line wrap: on
line diff
--- a/mercurial/filelog.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/mercurial/filelog.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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,
--- a/mercurial/httppeer.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/mercurial/httppeer.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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):
--- a/mercurial/localrepo.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/mercurial/localrepo.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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:
--- a/mercurial/repository.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/mercurial/repository.py	Sun Apr 22 11:54:10 2018 -0700
@@ -8,14 +8,14 @@
 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.
@@ -24,7 +24,7 @@
     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,13 +608,13 @@
 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
@@ -622,7 +622,7 @@
         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,59 +642,59 @@
         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.
@@ -702,40 +702,40 @@
         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):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/utils/interfaceutil.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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
--- a/mercurial/wireprotoserver.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/mercurial/wireprotoserver.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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):
--- a/mercurial/wireprototypes.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/mercurial/wireprototypes.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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,7 +179,7 @@
     '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
@@ -188,7 +188,7 @@
     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.
--- a/mercurial/wireprotov1peer.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/mercurial/wireprotov1peer.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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.
 
--- a/mercurial/wireprotov2server.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/mercurial/wireprotov2server.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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
--- a/tests/test-check-interfaces.py	Mon Apr 23 21:13:19 2018 +0900
+++ b/tests/test-check-interfaces.py	Sun Apr 22 11:54:10 2018 -0700
@@ -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 (
--- a/tests/test-check-module-imports.t	Mon Apr 23 21:13:19 2018 +0900
+++ b/tests/test-check-module-imports.t	Sun Apr 22 11:54:10 2018 -0700
@@ -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 \