# HG changeset patch # User Gregory Szorc # Date 2018-04-22 18:54:10 # Node ID 856f381ad74b78f5188a8e12648e9f7d38097c46 # Parent 80695628adcbee8860f19b9b8578fdf26ff4e701 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 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, diff --git a/mercurial/httppeer.py b/mercurial/httppeer.py --- a/mercurial/httppeer.py +++ b/mercurial/httppeer.py @@ -20,9 +20,6 @@ from .i18n import _ from .thirdparty import ( cbor, ) -from .thirdparty.zope import ( - interface as zi, -) from . import ( bundle2, error, @@ -38,6 +35,9 @@ from . import ( wireprotov2peer, wireprotov2server, ) +from .utils import ( + interfaceutil, +) httplib = util.httplib urlerr = util.urlerr @@ -582,7 +582,7 @@ class queuedcommandfuture(pycompat.futur # 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 @@ class httpv2executor(object): 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/localrepo.py b/mercurial/localrepo.py --- a/mercurial/localrepo.py +++ b/mercurial/localrepo.py @@ -21,9 +21,6 @@ from .node import ( nullid, short, ) -from .thirdparty.zope import ( - interface as zi, -) from . import ( bookmarks, branchmap, @@ -68,6 +65,7 @@ from . import ( vfs as vfsmod, ) from .utils import ( + interfaceutil, procutil, stringutil, ) @@ -153,7 +151,7 @@ moderncaps = {'lookup', 'branchmap', 'pu '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 @@ class localcommandexecutor(object): 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 @@ class localpeer(repository.peer): # 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 @@ REVLOGV2_REQUIREMENT = 'exp-revlogv2.0' # 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/repository.py b/mercurial/repository.py --- a/mercurial/repository.py +++ b/mercurial/repository.py @@ -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 @@ class ipeerconnection(zi.Interface): 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 @@ class ipeerconnection(zi.Interface): 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 @@ class ipeercapabilities(zi.Interface): 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 @@ class ipeercommands(zi.Interface): 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 @@ class ipeerlegacycommands(zi.Interface): 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 @@ class ipeercommandexecutor(zi.Interface) 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 @@ class ipeerbase(ipeerconnection, ipeerca 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 @@ class peer(object): _('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 @@ class ifilerevisionssequence(zi.Interfac 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 @@ class ifileindex(zi.Interface): * 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 @@ class ifileindex(zi.Interface): 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 @@ class ifiledata(zi.Interface): 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 ifilemutation(zi.Interface): 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 @@ class ifilestorage(ifileindex, ifiledata 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 @@ class ifilestorage(ifileindex, ifiledata 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 @@ class completelocalrepository(zi.Interfa 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 @@ class completelocalrepository(zi.Interfa 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 @@ class completelocalrepository(zi.Interfa def checkpush(pushop): pass - prepushoutgoinghooks = zi.Attribute( + prepushoutgoinghooks = interfaceutil.Attribute( """util.hooks instance.""") def pushkey(namespace, key, old, new): 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 +# +# 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/wireprotoserver.py b/mercurial/wireprotoserver.py --- a/mercurial/wireprotoserver.py +++ b/mercurial/wireprotoserver.py @@ -15,9 +15,6 @@ from .i18n import _ from .thirdparty import ( cbor, ) -from .thirdparty.zope import ( - interface as zi, -) from . import ( encoding, error, @@ -29,6 +26,7 @@ from . import ( wireprotov2server, ) from .utils import ( + interfaceutil, procutil, ) @@ -62,7 +60,7 @@ def decodevaluefromheaders(req, headerpr 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 @@ def _sshv1respondooberror(fout, ferr, rs 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/wireprototypes.py b/mercurial/wireprototypes.py --- a/mercurial/wireprototypes.py +++ b/mercurial/wireprototypes.py @@ -9,14 +9,14 @@ from .node import ( 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 @@ GETBUNDLE_ARGUMENTS = { '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 @@ class baseprotocolhandler(zi.Interface): 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/wireprotov1peer.py b/mercurial/wireprotov1peer.py --- a/mercurial/wireprotov1peer.py +++ b/mercurial/wireprotov1peer.py @@ -15,9 +15,6 @@ from .i18n import _ from .node import ( bin, ) -from .thirdparty.zope import ( - interface as zi, -) from . import ( bundle2, changegroup as changegroupmod, @@ -29,6 +26,9 @@ from . import ( util, wireprototypes, ) +from .utils import ( + interfaceutil, +) urlreq = util.urlreq @@ -110,7 +110,7 @@ class unsentfuture(pycompat.futures.Futu # 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 @@ class peerexecutor(object): 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/wireprotov2server.py b/mercurial/wireprotov2server.py --- a/mercurial/wireprotov2server.py +++ b/mercurial/wireprotov2server.py @@ -12,9 +12,6 @@ from .i18n import _ from .thirdparty import ( cbor, ) -from .thirdparty.zope import ( - interface as zi, -) from . import ( encoding, error, @@ -24,6 +21,9 @@ from . import ( wireprotoframing, wireprototypes, ) +from .utils import ( + interfaceutil, +) FRAMINGTYPE = b'application/mercurial-exp-framing-0005' @@ -340,7 +340,7 @@ def dispatch(repo, proto, command): 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/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/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 @@ outputs, which should be fixed later. > -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 \