##// END OF EJS Templates
fastannotate: move some global state mutation to extsetup()...
Augie Fackler -
r39247:8da20fc9 default
parent child Browse files
Show More
@@ -1,193 +1,193 b''
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 # * move various global-setup bits to extsetup() or reposetup()
104 #
105 103 # * move `flock` based locking to a common area
106 104 #
107 105 # * revise wireprotocol for sharing annotate files
108 106 #
109 107 # * figure out a sensible default for `mainbranch` (with the caveat
110 108 # that we probably also want to figure out a better term than
111 109 # `branch`, see above)
112 110 #
113 111 # * format changes to the revmap file (maybe use length-encoding
114 112 # instead of null-terminated file paths at least?)
115 113 from __future__ import absolute_import
116 114
117 115 from mercurial.i18n import _
118 116 from mercurial import (
119 117 error as hgerror,
120 118 localrepo,
121 119 registrar,
122 120 util,
123 121 )
124 122
125 123 from . import (
126 124 commands,
127 125 context,
128 126 protocol,
129 127 )
130 128
131 129 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
132 130 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
133 131 # be specifying the version(s) of Mercurial they are tested with, or
134 132 # leave the attribute unspecified.
135 133 testedwith = 'ships-with-hg-core'
136 134
137 135 cmdtable = commands.cmdtable
138 136
139 137 configtable = {}
140 138 configitem = registrar.configitem(configtable)
141 139
142 140 configitem('fastannotate', 'modes', default=['fastannotate'])
143 141 configitem('fastannotate', 'server', default=False)
144 142 configitem('fastannotate', 'useflock', default=True)
145 143 configitem('fastannotate', 'client', default=False)
146 144 configitem('fastannotate', 'unfilteredrepo', default=True)
147 145 configitem('fastannotate', 'defaultformat', default=['number'])
148 146 configitem('fastannotate', 'perfhack', default=False)
149 147 configitem('fastannotate', 'mainbranch')
150 148 configitem('fastannotate', 'forcetext', default=True)
151 149 configitem('fastannotate', 'forcefollow', default=True)
152 150 configitem('fastannotate', 'clientfetchthreshold', default=10)
153 151 configitem('fastannotate', 'serverbuildondemand', default=True)
154 152 configitem('fastannotate', 'remotepath', default='default')
155 153
156 154 def _flockavailable():
157 155 try:
158 156 import fcntl
159 157 fcntl.flock
160 158 except StandardError:
161 159 return False
162 160 else:
163 161 return True
164 162
165 163 def uisetup(ui):
166 164 modes = set(ui.configlist('fastannotate', 'modes'))
167 165 if 'fctx' in modes:
168 166 modes.discard('hgweb')
169 167 for name in modes:
170 168 if name == 'fastannotate':
171 169 commands.registercommand()
172 170 elif name == 'hgweb':
173 171 from . import support
174 172 support.replacehgwebannotate()
175 173 elif name == 'fctx':
176 174 from . import support
177 175 support.replacefctxannotate()
178 176 commands.wrapdefault()
179 177 else:
180 178 raise hgerror.Abort(_('fastannotate: invalid mode: %s') % name)
181 179
182 180 if ui.configbool('fastannotate', 'server'):
183 181 protocol.serveruisetup(ui)
184 182
185 183 if ui.configbool('fastannotate', 'useflock', _flockavailable()):
186 184 context.pathhelper.lock = context.pathhelper._lockflock
187 185
186 def extsetup(ui):
188 187 # fastannotate has its own locking, without depending on repo lock
188 # TODO: avoid mutating this unless the specific repo has it enabled
189 189 localrepo.localrepository._wlockfreeprefix.add('fastannotate/')
190 190
191 191 def reposetup(ui, repo):
192 192 if ui.configbool('fastannotate', 'client'):
193 193 protocol.clientreposetup(ui, repo)
@@ -1,227 +1,229 b''
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 18 scmutil,
19 19 wireprotov1peer,
20 20 wireprotov1server,
21 21 )
22 22 from . import context
23 23
24 24 # common
25 25
26 26 def _getmaster(ui):
27 27 """get the mainbranch, and enforce it is set"""
28 28 master = ui.config('fastannotate', 'mainbranch')
29 29 if not master:
30 30 raise error.Abort(_('fastannotate.mainbranch is required '
31 31 'for both the client and the server'))
32 32 return master
33 33
34 34 # server-side
35 35
36 36 def _capabilities(orig, repo, proto):
37 37 result = orig(repo, proto)
38 38 result.append('getannotate')
39 39 return result
40 40
41 41 def _getannotate(repo, proto, path, lastnode):
42 42 # output:
43 43 # FILE := vfspath + '\0' + str(size) + '\0' + content
44 44 # OUTPUT := '' | FILE + OUTPUT
45 45 result = ''
46 46 buildondemand = repo.ui.configbool('fastannotate', 'serverbuildondemand',
47 47 True)
48 48 with context.annotatecontext(repo, path) as actx:
49 49 if buildondemand:
50 50 # update before responding to the client
51 51 master = _getmaster(repo.ui)
52 52 try:
53 53 if not actx.isuptodate(master):
54 54 actx.annotate(master, master)
55 55 except Exception:
56 56 # non-fast-forward move or corrupted. rebuild automically.
57 57 actx.rebuild()
58 58 try:
59 59 actx.annotate(master, master)
60 60 except Exception:
61 61 actx.rebuild() # delete files
62 62 finally:
63 63 # although the "with" context will also do a close/flush, we
64 64 # need to do it early so we can send the correct respond to
65 65 # client.
66 66 actx.close()
67 67 # send back the full content of revmap and linelog, in the future we
68 68 # may want to do some rsync-like fancy updating.
69 69 # the lastnode check is not necessary if the client and the server
70 70 # agree where the main branch is.
71 71 if actx.lastnode != lastnode:
72 72 for p in [actx.revmappath, actx.linelogpath]:
73 73 if not os.path.exists(p):
74 74 continue
75 75 content = ''
76 76 with open(p, 'rb') as f:
77 77 content = f.read()
78 78 vfsbaselen = len(repo.vfs.base + '/')
79 79 relpath = p[vfsbaselen:]
80 80 result += '%s\0%s\0%s' % (relpath, len(content), content)
81 81 return result
82 82
83 83 def _registerwireprotocommand():
84 84 if 'getannotate' in wireprotov1server.commands:
85 85 return
86 86 wireprotov1server.wireprotocommand(
87 87 'getannotate', 'path lastnode')(_getannotate)
88 88
89 89 def serveruisetup(ui):
90 90 _registerwireprotocommand()
91 91 extensions.wrapfunction(wireprotov1server, '_capabilities', _capabilities)
92 92
93 93 # client-side
94 94
95 95 def _parseresponse(payload):
96 96 result = {}
97 97 i = 0
98 98 l = len(payload) - 1
99 99 state = 0 # 0: vfspath, 1: size
100 100 vfspath = size = ''
101 101 while i < l:
102 102 ch = payload[i]
103 103 if ch == '\0':
104 104 if state == 1:
105 105 result[vfspath] = buffer(payload, i + 1, int(size))
106 106 i += int(size)
107 107 state = 0
108 108 vfspath = size = ''
109 109 elif state == 0:
110 110 state = 1
111 111 else:
112 112 if state == 1:
113 113 size += ch
114 114 elif state == 0:
115 115 vfspath += ch
116 116 i += 1
117 117 return result
118 118
119 119 def peersetup(ui, peer):
120 120 class fastannotatepeer(peer.__class__):
121 121 @wireprotov1peer.batchable
122 122 def getannotate(self, path, lastnode=None):
123 123 if not self.capable('getannotate'):
124 124 ui.warn(_('remote peer cannot provide annotate cache\n'))
125 125 yield None, None
126 126 else:
127 127 args = {'path': path, 'lastnode': lastnode or ''}
128 128 f = wireprotov1peer.future()
129 129 yield args, f
130 130 yield _parseresponse(f.value)
131 131 peer.__class__ = fastannotatepeer
132 132
133 133 @contextlib.contextmanager
134 134 def annotatepeer(repo):
135 135 ui = repo.ui
136 136
137 137 remotepath = ui.expandpath(
138 138 ui.config('fastannotate', 'remotepath', 'default'))
139 139 peer = hg.peer(ui, {}, remotepath)
140 140
141 141 try:
142 142 yield peer
143 143 finally:
144 144 peer.close()
145 145
146 146 def clientfetch(repo, paths, lastnodemap=None, peer=None):
147 147 """download annotate cache from the server for paths"""
148 148 if not paths:
149 149 return
150 150
151 151 if peer is None:
152 152 with annotatepeer(repo) as peer:
153 153 return clientfetch(repo, paths, lastnodemap, peer)
154 154
155 155 if lastnodemap is None:
156 156 lastnodemap = {}
157 157
158 158 ui = repo.ui
159 159 results = []
160 160 with peer.commandexecutor() as batcher:
161 161 ui.debug('fastannotate: requesting %d files\n' % len(paths))
162 162 for p in paths:
163 163 results.append(batcher.callcommand(
164 164 'getannotate',
165 165 {'path': p, 'lastnode':lastnodemap.get(p)}))
166 166
167 167 ui.debug('fastannotate: server returned\n')
168 168 for result in results:
169 169 r = result.result()
170 170 for path in sorted(r):
171 171 # ignore malicious paths
172 172 if not path.startswith('fastannotate/') or '/../' in (path + '/'):
173 173 ui.debug('fastannotate: ignored malicious path %s\n' % path)
174 174 continue
175 175 content = r[path]
176 176 if ui.debugflag:
177 177 ui.debug('fastannotate: writing %d bytes to %s\n'
178 178 % (len(content), path))
179 179 repo.vfs.makedirs(os.path.dirname(path))
180 180 with repo.vfs(path, 'wb') as f:
181 181 f.write(content)
182 182
183 183 def _filterfetchpaths(repo, paths):
184 184 """return a subset of paths whose history is long and need to fetch linelog
185 185 from the server. works with remotefilelog and non-remotefilelog repos.
186 186 """
187 187 threshold = repo.ui.configint('fastannotate', 'clientfetchthreshold', 10)
188 188 if threshold <= 0:
189 189 return paths
190 190
191 191 master = repo.ui.config('fastannotate', 'mainbranch') or 'default'
192 192
193 193 result = []
194 194 for path in paths:
195 195 try:
196 196 if len(repo.file(path)) >= threshold:
197 197 result.append(path)
198 198 except Exception: # file not found etc.
199 199 result.append(path)
200 200
201 201 return result
202 202
203 203 def localreposetup(ui, repo):
204 204 class fastannotaterepo(repo.__class__):
205 205 def prefetchfastannotate(self, paths, peer=None):
206 206 master = _getmaster(self.ui)
207 207 needupdatepaths = []
208 208 lastnodemap = {}
209 209 try:
210 210 for path in _filterfetchpaths(self, paths):
211 211 with context.annotatecontext(self, path) as actx:
212 212 if not actx.isuptodate(master, strict=False):
213 213 needupdatepaths.append(path)
214 214 lastnodemap[path] = actx.lastnode
215 215 if needupdatepaths:
216 216 clientfetch(self, needupdatepaths, lastnodemap, peer)
217 217 except Exception as ex:
218 218 # could be directory not writable or so, not fatal
219 219 self.ui.debug('fastannotate: prefetch failed: %r\n' % ex)
220 220 repo.__class__ = fastannotaterepo
221 221
222 222 def clientreposetup(ui, repo):
223 223 _registerwireprotocommand()
224 224 if isinstance(repo, localrepo.localrepository):
225 225 localreposetup(ui, repo)
226 # TODO: this mutates global state, but only if at least one repo
227 # has the extension enabled. This is probably bad for hgweb.
226 228 if peersetup not in hg.wirepeersetupfuncs:
227 229 hg.wirepeersetupfuncs.append(peersetup)
General Comments 0
You need to be logged in to leave comments. Login now