##// END OF EJS Templates
fastannotate: address pyflakes concerns...
Augie Fackler -
r39248:eaa14dff default
parent child Browse files
Show More
@@ -1,193 +1,192
1 1 # Copyright 2016-present Facebook. All Rights Reserved.
2 2 #
3 3 # fastannotate: faster annotate implementation using linelog
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """yet another annotate implementation that might be faster (EXPERIMENTAL)
8 8
9 9 The fastannotate extension provides a 'fastannotate' command that makes
10 10 use of the linelog data structure as a cache layer and is expected to
11 11 be faster than the vanilla 'annotate' if the cache is present.
12 12
13 13 In most cases, fastannotate requires a setup that mainbranch is some pointer
14 14 that always moves forward, to be most efficient.
15 15
16 16 Using fastannotate together with linkrevcache would speed up building the
17 17 annotate cache greatly. Run "debugbuildlinkrevcache" before
18 18 "debugbuildannotatecache".
19 19
20 20 ::
21 21
22 22 [fastannotate]
23 23 # specify the main branch head. the internal linelog will only contain
24 24 # the linear (ignoring p2) "mainbranch". since linelog cannot move
25 25 # backwards without a rebuild, this should be something that always moves
26 26 # forward, usually it is "master" or "@".
27 27 mainbranch = master
28 28
29 29 # fastannotate supports different modes to expose its feature.
30 30 # a list of combination:
31 31 # - fastannotate: expose the feature via the "fastannotate" command which
32 32 # deals with everything in a most efficient way, and provides extra
33 33 # features like --deleted etc.
34 34 # - fctx: replace fctx.annotate implementation. note:
35 35 # a. it is less efficient than the "fastannotate" command
36 36 # b. it will make it practically impossible to access the old (disk
37 37 # side-effect free) annotate implementation
38 38 # c. it implies "hgweb".
39 39 # - hgweb: replace hgweb's annotate implementation. conflict with "fctx".
40 40 # (default: fastannotate)
41 41 modes = fastannotate
42 42
43 43 # default format when no format flags are used (default: number)
44 44 defaultformat = changeset, user, date
45 45
46 46 # serve the annotate cache via wire protocol (default: False)
47 47 # tip: the .hg/fastannotate directory is portable - can be rsynced
48 48 server = True
49 49
50 50 # build annotate cache on demand for every client request (default: True)
51 51 # disabling it could make server response faster, useful when there is a
52 52 # cronjob building the cache.
53 53 serverbuildondemand = True
54 54
55 55 # update local annotate cache from remote on demand
56 56 client = False
57 57
58 58 # path to use when connecting to the remote server (default: default)
59 59 remotepath = default
60 60
61 61 # minimal length of the history of a file required to fetch linelog from
62 62 # the server. (default: 10)
63 63 clientfetchthreshold = 10
64 64
65 65 # use flock instead of the file existence lock
66 66 # flock may not work well on some network filesystems, but they avoid
67 67 # creating and deleting files frequently, which is faster when updating
68 68 # the annotate cache in batch. if you have issues with this option, set it
69 69 # to False. (default: True if flock is supported, False otherwise)
70 70 useflock = True
71 71
72 72 # for "fctx" mode, always follow renames regardless of command line option.
73 73 # this is a BC with the original command but will reduced the space needed
74 74 # for annotate cache, and is useful for client-server setup since the
75 75 # server will only provide annotate cache with default options (i.e. with
76 76 # follow). do not affect "fastannotate" mode. (default: True)
77 77 forcefollow = True
78 78
79 79 # for "fctx" mode, always treat file as text files, to skip the "isbinary"
80 80 # check. this is consistent with the "fastannotate" command and could help
81 81 # to avoid a file fetch if remotefilelog is used. (default: True)
82 82 forcetext = True
83 83
84 84 # use unfiltered repo for better performance.
85 85 unfilteredrepo = True
86 86
87 87 # sacrifice correctness in some corner cases for performance. it does not
88 88 # affect the correctness of the annotate cache being built. the option
89 89 # is experimental and may disappear in the future (default: False)
90 90 perfhack = True
91 91 """
92 92
93 93 # TODO from import:
94 94 # * `branch` is probably the wrong term, throughout the code.
95 95 #
96 96 # * replace the fastannotate `modes` configuration with a collection
97 97 # of booleans.
98 98 #
99 99 # * Use the templater instead of bespoke formatting
100 100 #
101 101 # * rename the config knob for updating the local cache from a remote server
102 102 #
103 103 # * move `flock` based locking to a common area
104 104 #
105 105 # * revise wireprotocol for sharing annotate files
106 106 #
107 107 # * figure out a sensible default for `mainbranch` (with the caveat
108 108 # that we probably also want to figure out a better term than
109 109 # `branch`, see above)
110 110 #
111 111 # * format changes to the revmap file (maybe use length-encoding
112 112 # instead of null-terminated file paths at least?)
113 113 from __future__ import absolute_import
114 114
115 115 from mercurial.i18n import _
116 116 from mercurial import (
117 117 error as hgerror,
118 118 localrepo,
119 119 registrar,
120 util,
121 120 )
122 121
123 122 from . import (
124 123 commands,
125 124 context,
126 125 protocol,
127 126 )
128 127
129 128 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
130 129 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
131 130 # be specifying the version(s) of Mercurial they are tested with, or
132 131 # leave the attribute unspecified.
133 132 testedwith = 'ships-with-hg-core'
134 133
135 134 cmdtable = commands.cmdtable
136 135
137 136 configtable = {}
138 137 configitem = registrar.configitem(configtable)
139 138
140 139 configitem('fastannotate', 'modes', default=['fastannotate'])
141 140 configitem('fastannotate', 'server', default=False)
142 141 configitem('fastannotate', 'useflock', default=True)
143 142 configitem('fastannotate', 'client', default=False)
144 143 configitem('fastannotate', 'unfilteredrepo', default=True)
145 144 configitem('fastannotate', 'defaultformat', default=['number'])
146 145 configitem('fastannotate', 'perfhack', default=False)
147 146 configitem('fastannotate', 'mainbranch')
148 147 configitem('fastannotate', 'forcetext', default=True)
149 148 configitem('fastannotate', 'forcefollow', default=True)
150 149 configitem('fastannotate', 'clientfetchthreshold', default=10)
151 150 configitem('fastannotate', 'serverbuildondemand', default=True)
152 151 configitem('fastannotate', 'remotepath', default='default')
153 152
154 153 def _flockavailable():
155 154 try:
156 155 import fcntl
157 156 fcntl.flock
158 157 except StandardError:
159 158 return False
160 159 else:
161 160 return True
162 161
163 162 def uisetup(ui):
164 163 modes = set(ui.configlist('fastannotate', 'modes'))
165 164 if 'fctx' in modes:
166 165 modes.discard('hgweb')
167 166 for name in modes:
168 167 if name == 'fastannotate':
169 168 commands.registercommand()
170 169 elif name == 'hgweb':
171 170 from . import support
172 171 support.replacehgwebannotate()
173 172 elif name == 'fctx':
174 173 from . import support
175 174 support.replacefctxannotate()
176 175 commands.wrapdefault()
177 176 else:
178 177 raise hgerror.Abort(_('fastannotate: invalid mode: %s') % name)
179 178
180 179 if ui.configbool('fastannotate', 'server'):
181 180 protocol.serveruisetup(ui)
182 181
183 182 if ui.configbool('fastannotate', 'useflock', _flockavailable()):
184 183 context.pathhelper.lock = context.pathhelper._lockflock
185 184
186 185 def extsetup(ui):
187 186 # fastannotate has its own locking, without depending on repo lock
188 187 # TODO: avoid mutating this unless the specific repo has it enabled
189 188 localrepo.localrepository._wlockfreeprefix.add('fastannotate/')
190 189
191 190 def reposetup(ui, repo):
192 191 if ui.configbool('fastannotate', 'client'):
193 192 protocol.clientreposetup(ui, repo)
@@ -1,229 +1,226
1 1 # Copyright 2016-present Facebook. All Rights Reserved.
2 2 #
3 3 # protocol: logic for a server providing fastannotate support
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 from __future__ import absolute_import
8 8
9 9 import contextlib
10 10 import os
11 11
12 12 from mercurial.i18n import _
13 13 from mercurial import (
14 14 error,
15 15 extensions,
16 16 hg,
17 17 localrepo,
18 scmutil,
19 18 wireprotov1peer,
20 19 wireprotov1server,
21 20 )
22 21 from . import context
23 22
24 23 # common
25 24
26 25 def _getmaster(ui):
27 26 """get the mainbranch, and enforce it is set"""
28 27 master = ui.config('fastannotate', 'mainbranch')
29 28 if not master:
30 29 raise error.Abort(_('fastannotate.mainbranch is required '
31 30 'for both the client and the server'))
32 31 return master
33 32
34 33 # server-side
35 34
36 35 def _capabilities(orig, repo, proto):
37 36 result = orig(repo, proto)
38 37 result.append('getannotate')
39 38 return result
40 39
41 40 def _getannotate(repo, proto, path, lastnode):
42 41 # output:
43 42 # FILE := vfspath + '\0' + str(size) + '\0' + content
44 43 # OUTPUT := '' | FILE + OUTPUT
45 44 result = ''
46 45 buildondemand = repo.ui.configbool('fastannotate', 'serverbuildondemand',
47 46 True)
48 47 with context.annotatecontext(repo, path) as actx:
49 48 if buildondemand:
50 49 # update before responding to the client
51 50 master = _getmaster(repo.ui)
52 51 try:
53 52 if not actx.isuptodate(master):
54 53 actx.annotate(master, master)
55 54 except Exception:
56 55 # non-fast-forward move or corrupted. rebuild automically.
57 56 actx.rebuild()
58 57 try:
59 58 actx.annotate(master, master)
60 59 except Exception:
61 60 actx.rebuild() # delete files
62 61 finally:
63 62 # although the "with" context will also do a close/flush, we
64 63 # need to do it early so we can send the correct respond to
65 64 # client.
66 65 actx.close()
67 66 # send back the full content of revmap and linelog, in the future we
68 67 # may want to do some rsync-like fancy updating.
69 68 # the lastnode check is not necessary if the client and the server
70 69 # agree where the main branch is.
71 70 if actx.lastnode != lastnode:
72 71 for p in [actx.revmappath, actx.linelogpath]:
73 72 if not os.path.exists(p):
74 73 continue
75 74 content = ''
76 75 with open(p, 'rb') as f:
77 76 content = f.read()
78 77 vfsbaselen = len(repo.vfs.base + '/')
79 78 relpath = p[vfsbaselen:]
80 79 result += '%s\0%s\0%s' % (relpath, len(content), content)
81 80 return result
82 81
83 82 def _registerwireprotocommand():
84 83 if 'getannotate' in wireprotov1server.commands:
85 84 return
86 85 wireprotov1server.wireprotocommand(
87 86 'getannotate', 'path lastnode')(_getannotate)
88 87
89 88 def serveruisetup(ui):
90 89 _registerwireprotocommand()
91 90 extensions.wrapfunction(wireprotov1server, '_capabilities', _capabilities)
92 91
93 92 # client-side
94 93
95 94 def _parseresponse(payload):
96 95 result = {}
97 96 i = 0
98 97 l = len(payload) - 1
99 98 state = 0 # 0: vfspath, 1: size
100 99 vfspath = size = ''
101 100 while i < l:
102 101 ch = payload[i]
103 102 if ch == '\0':
104 103 if state == 1:
105 104 result[vfspath] = buffer(payload, i + 1, int(size))
106 105 i += int(size)
107 106 state = 0
108 107 vfspath = size = ''
109 108 elif state == 0:
110 109 state = 1
111 110 else:
112 111 if state == 1:
113 112 size += ch
114 113 elif state == 0:
115 114 vfspath += ch
116 115 i += 1
117 116 return result
118 117
119 118 def peersetup(ui, peer):
120 119 class fastannotatepeer(peer.__class__):
121 120 @wireprotov1peer.batchable
122 121 def getannotate(self, path, lastnode=None):
123 122 if not self.capable('getannotate'):
124 123 ui.warn(_('remote peer cannot provide annotate cache\n'))
125 124 yield None, None
126 125 else:
127 126 args = {'path': path, 'lastnode': lastnode or ''}
128 127 f = wireprotov1peer.future()
129 128 yield args, f
130 129 yield _parseresponse(f.value)
131 130 peer.__class__ = fastannotatepeer
132 131
133 132 @contextlib.contextmanager
134 133 def annotatepeer(repo):
135 134 ui = repo.ui
136 135
137 136 remotepath = ui.expandpath(
138 137 ui.config('fastannotate', 'remotepath', 'default'))
139 138 peer = hg.peer(ui, {}, remotepath)
140 139
141 140 try:
142 141 yield peer
143 142 finally:
144 143 peer.close()
145 144
146 145 def clientfetch(repo, paths, lastnodemap=None, peer=None):
147 146 """download annotate cache from the server for paths"""
148 147 if not paths:
149 148 return
150 149
151 150 if peer is None:
152 151 with annotatepeer(repo) as peer:
153 152 return clientfetch(repo, paths, lastnodemap, peer)
154 153
155 154 if lastnodemap is None:
156 155 lastnodemap = {}
157 156
158 157 ui = repo.ui
159 158 results = []
160 159 with peer.commandexecutor() as batcher:
161 160 ui.debug('fastannotate: requesting %d files\n' % len(paths))
162 161 for p in paths:
163 162 results.append(batcher.callcommand(
164 163 'getannotate',
165 164 {'path': p, 'lastnode':lastnodemap.get(p)}))
166 165
167 166 ui.debug('fastannotate: server returned\n')
168 167 for result in results:
169 168 r = result.result()
170 169 for path in sorted(r):
171 170 # ignore malicious paths
172 171 if not path.startswith('fastannotate/') or '/../' in (path + '/'):
173 172 ui.debug('fastannotate: ignored malicious path %s\n' % path)
174 173 continue
175 174 content = r[path]
176 175 if ui.debugflag:
177 176 ui.debug('fastannotate: writing %d bytes to %s\n'
178 177 % (len(content), path))
179 178 repo.vfs.makedirs(os.path.dirname(path))
180 179 with repo.vfs(path, 'wb') as f:
181 180 f.write(content)
182 181
183 182 def _filterfetchpaths(repo, paths):
184 183 """return a subset of paths whose history is long and need to fetch linelog
185 184 from the server. works with remotefilelog and non-remotefilelog repos.
186 185 """
187 186 threshold = repo.ui.configint('fastannotate', 'clientfetchthreshold', 10)
188 187 if threshold <= 0:
189 188 return paths
190 189
191 master = repo.ui.config('fastannotate', 'mainbranch') or 'default'
192
193 190 result = []
194 191 for path in paths:
195 192 try:
196 193 if len(repo.file(path)) >= threshold:
197 194 result.append(path)
198 195 except Exception: # file not found etc.
199 196 result.append(path)
200 197
201 198 return result
202 199
203 200 def localreposetup(ui, repo):
204 201 class fastannotaterepo(repo.__class__):
205 202 def prefetchfastannotate(self, paths, peer=None):
206 203 master = _getmaster(self.ui)
207 204 needupdatepaths = []
208 205 lastnodemap = {}
209 206 try:
210 207 for path in _filterfetchpaths(self, paths):
211 208 with context.annotatecontext(self, path) as actx:
212 209 if not actx.isuptodate(master, strict=False):
213 210 needupdatepaths.append(path)
214 211 lastnodemap[path] = actx.lastnode
215 212 if needupdatepaths:
216 213 clientfetch(self, needupdatepaths, lastnodemap, peer)
217 214 except Exception as ex:
218 215 # could be directory not writable or so, not fatal
219 216 self.ui.debug('fastannotate: prefetch failed: %r\n' % ex)
220 217 repo.__class__ = fastannotaterepo
221 218
222 219 def clientreposetup(ui, repo):
223 220 _registerwireprotocommand()
224 221 if isinstance(repo, localrepo.localrepository):
225 222 localreposetup(ui, repo)
226 223 # TODO: this mutates global state, but only if at least one repo
227 224 # has the extension enabled. This is probably bad for hgweb.
228 225 if peersetup not in hg.wirepeersetupfuncs:
229 226 hg.wirepeersetupfuncs.append(peersetup)
General Comments 0
You need to be logged in to leave comments. Login now