##// END OF EJS Templates
branchmap: load branchmap as an iterable...
Martijn Pieters -
r39151:2a4bfbb5 default
parent child Browse files
Show More
@@ -1,549 +1,547 b''
1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import struct
10 import struct
11
11
12 from .node import (
12 from .node import (
13 bin,
13 bin,
14 hex,
14 hex,
15 nullid,
15 nullid,
16 nullrev,
16 nullrev,
17 )
17 )
18 from . import (
18 from . import (
19 encoding,
19 encoding,
20 error,
20 error,
21 pycompat,
21 pycompat,
22 scmutil,
22 scmutil,
23 util,
23 util,
24 )
24 )
25 from .utils import (
25 from .utils import (
26 stringutil,
26 stringutil,
27 )
27 )
28
28
29 calcsize = struct.calcsize
29 calcsize = struct.calcsize
30 pack_into = struct.pack_into
30 pack_into = struct.pack_into
31 unpack_from = struct.unpack_from
31 unpack_from = struct.unpack_from
32
32
33 def _filename(repo):
33 def _filename(repo):
34 """name of a branchcache file for a given repo or repoview"""
34 """name of a branchcache file for a given repo or repoview"""
35 filename = "branch2"
35 filename = "branch2"
36 if repo.filtername:
36 if repo.filtername:
37 filename = '%s-%s' % (filename, repo.filtername)
37 filename = '%s-%s' % (filename, repo.filtername)
38 return filename
38 return filename
39
39
40 def read(repo):
40 def read(repo):
41 try:
41 try:
42 f = repo.cachevfs(_filename(repo))
42 f = repo.cachevfs(_filename(repo))
43 lines = f.read().split('\n')
43 cachekey = next(f).split(" ", 2)
44 f.close()
45 except (IOError, OSError):
46 return None
47
48 try:
49 cachekey = lines.pop(0).split(" ", 2)
50 last, lrev = cachekey[:2]
44 last, lrev = cachekey[:2]
51 last, lrev = bin(last), int(lrev)
45 last, lrev = bin(last), int(lrev)
52 filteredhash = None
46 filteredhash = None
53 if len(cachekey) > 2:
47 if len(cachekey) > 2:
54 filteredhash = bin(cachekey[2])
48 filteredhash = bin(cachekey[2])
55 partial = branchcache(tipnode=last, tiprev=lrev,
49 partial = branchcache(tipnode=last, tiprev=lrev,
56 filteredhash=filteredhash)
50 filteredhash=filteredhash)
57 if not partial.validfor(repo):
51 if not partial.validfor(repo):
58 # invalidate the cache
52 # invalidate the cache
59 raise ValueError(r'tip differs')
53 raise ValueError(r'tip differs')
60 cl = repo.changelog
54 cl = repo.changelog
61 for l in lines:
55 for l in f:
62 if not l:
56 if not l:
63 continue
57 continue
64 node, state, label = l.split(" ", 2)
58 node, state, label = l.split(" ", 2)
65 if state not in 'oc':
59 if state not in 'oc':
66 raise ValueError(r'invalid branch state')
60 raise ValueError(r'invalid branch state')
67 label = encoding.tolocal(label.strip())
61 label = encoding.tolocal(label.strip())
68 node = bin(node)
62 node = bin(node)
69 if not cl.hasnode(node):
63 if not cl.hasnode(node):
70 raise ValueError(
64 raise ValueError(
71 r'node %s does not exist' % pycompat.sysstr(hex(node)))
65 r'node %s does not exist' % pycompat.sysstr(hex(node)))
72 partial.setdefault(label, []).append(node)
66 partial.setdefault(label, []).append(node)
73 if state == 'c':
67 if state == 'c':
74 partial._closednodes.add(node)
68 partial._closednodes.add(node)
69
70 except (IOError, OSError):
71 return None
72
75 except Exception as inst:
73 except Exception as inst:
76 if repo.ui.debugflag:
74 if repo.ui.debugflag:
77 msg = 'invalid branchheads cache'
75 msg = 'invalid branchheads cache'
78 if repo.filtername is not None:
76 if repo.filtername is not None:
79 msg += ' (%s)' % repo.filtername
77 msg += ' (%s)' % repo.filtername
80 msg += ': %s\n'
78 msg += ': %s\n'
81 repo.ui.debug(msg % pycompat.bytestr(inst))
79 repo.ui.debug(msg % pycompat.bytestr(inst))
82 partial = None
80 partial = None
83 return partial
81 return partial
84
82
85 ### Nearest subset relation
83 ### Nearest subset relation
86 # Nearest subset of filter X is a filter Y so that:
84 # Nearest subset of filter X is a filter Y so that:
87 # * Y is included in X,
85 # * Y is included in X,
88 # * X - Y is as small as possible.
86 # * X - Y is as small as possible.
89 # This create and ordering used for branchmap purpose.
87 # This create and ordering used for branchmap purpose.
90 # the ordering may be partial
88 # the ordering may be partial
91 subsettable = {None: 'visible',
89 subsettable = {None: 'visible',
92 'visible-hidden': 'visible',
90 'visible-hidden': 'visible',
93 'visible': 'served',
91 'visible': 'served',
94 'served': 'immutable',
92 'served': 'immutable',
95 'immutable': 'base'}
93 'immutable': 'base'}
96
94
97 def updatecache(repo):
95 def updatecache(repo):
98 cl = repo.changelog
96 cl = repo.changelog
99 filtername = repo.filtername
97 filtername = repo.filtername
100 partial = repo._branchcaches.get(filtername)
98 partial = repo._branchcaches.get(filtername)
101
99
102 revs = []
100 revs = []
103 if partial is None or not partial.validfor(repo):
101 if partial is None or not partial.validfor(repo):
104 partial = read(repo)
102 partial = read(repo)
105 if partial is None:
103 if partial is None:
106 subsetname = subsettable.get(filtername)
104 subsetname = subsettable.get(filtername)
107 if subsetname is None:
105 if subsetname is None:
108 partial = branchcache()
106 partial = branchcache()
109 else:
107 else:
110 subset = repo.filtered(subsetname)
108 subset = repo.filtered(subsetname)
111 partial = subset.branchmap().copy()
109 partial = subset.branchmap().copy()
112 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
110 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
113 revs.extend(r for r in extrarevs if r <= partial.tiprev)
111 revs.extend(r for r in extrarevs if r <= partial.tiprev)
114 revs.extend(cl.revs(start=partial.tiprev + 1))
112 revs.extend(cl.revs(start=partial.tiprev + 1))
115 if revs:
113 if revs:
116 partial.update(repo, revs)
114 partial.update(repo, revs)
117 partial.write(repo)
115 partial.write(repo)
118
116
119 assert partial.validfor(repo), filtername
117 assert partial.validfor(repo), filtername
120 repo._branchcaches[repo.filtername] = partial
118 repo._branchcaches[repo.filtername] = partial
121
119
122 def replacecache(repo, bm):
120 def replacecache(repo, bm):
123 """Replace the branchmap cache for a repo with a branch mapping.
121 """Replace the branchmap cache for a repo with a branch mapping.
124
122
125 This is likely only called during clone with a branch map from a remote.
123 This is likely only called during clone with a branch map from a remote.
126 """
124 """
127 rbheads = []
125 rbheads = []
128 closed = []
126 closed = []
129 for bheads in bm.itervalues():
127 for bheads in bm.itervalues():
130 rbheads.extend(bheads)
128 rbheads.extend(bheads)
131 for h in bheads:
129 for h in bheads:
132 r = repo.changelog.rev(h)
130 r = repo.changelog.rev(h)
133 b, c = repo.changelog.branchinfo(r)
131 b, c = repo.changelog.branchinfo(r)
134 if c:
132 if c:
135 closed.append(h)
133 closed.append(h)
136
134
137 if rbheads:
135 if rbheads:
138 rtiprev = max((int(repo.changelog.rev(node))
136 rtiprev = max((int(repo.changelog.rev(node))
139 for node in rbheads))
137 for node in rbheads))
140 cache = branchcache(bm,
138 cache = branchcache(bm,
141 repo[rtiprev].node(),
139 repo[rtiprev].node(),
142 rtiprev,
140 rtiprev,
143 closednodes=closed)
141 closednodes=closed)
144
142
145 # Try to stick it as low as possible
143 # Try to stick it as low as possible
146 # filter above served are unlikely to be fetch from a clone
144 # filter above served are unlikely to be fetch from a clone
147 for candidate in ('base', 'immutable', 'served'):
145 for candidate in ('base', 'immutable', 'served'):
148 rview = repo.filtered(candidate)
146 rview = repo.filtered(candidate)
149 if cache.validfor(rview):
147 if cache.validfor(rview):
150 repo._branchcaches[candidate] = cache
148 repo._branchcaches[candidate] = cache
151 cache.write(rview)
149 cache.write(rview)
152 break
150 break
153
151
154 class branchcache(dict):
152 class branchcache(dict):
155 """A dict like object that hold branches heads cache.
153 """A dict like object that hold branches heads cache.
156
154
157 This cache is used to avoid costly computations to determine all the
155 This cache is used to avoid costly computations to determine all the
158 branch heads of a repo.
156 branch heads of a repo.
159
157
160 The cache is serialized on disk in the following format:
158 The cache is serialized on disk in the following format:
161
159
162 <tip hex node> <tip rev number> [optional filtered repo hex hash]
160 <tip hex node> <tip rev number> [optional filtered repo hex hash]
163 <branch head hex node> <open/closed state> <branch name>
161 <branch head hex node> <open/closed state> <branch name>
164 <branch head hex node> <open/closed state> <branch name>
162 <branch head hex node> <open/closed state> <branch name>
165 ...
163 ...
166
164
167 The first line is used to check if the cache is still valid. If the
165 The first line is used to check if the cache is still valid. If the
168 branch cache is for a filtered repo view, an optional third hash is
166 branch cache is for a filtered repo view, an optional third hash is
169 included that hashes the hashes of all filtered revisions.
167 included that hashes the hashes of all filtered revisions.
170
168
171 The open/closed state is represented by a single letter 'o' or 'c'.
169 The open/closed state is represented by a single letter 'o' or 'c'.
172 This field can be used to avoid changelog reads when determining if a
170 This field can be used to avoid changelog reads when determining if a
173 branch head closes a branch or not.
171 branch head closes a branch or not.
174 """
172 """
175
173
176 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
174 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
177 filteredhash=None, closednodes=None):
175 filteredhash=None, closednodes=None):
178 super(branchcache, self).__init__(entries)
176 super(branchcache, self).__init__(entries)
179 self.tipnode = tipnode
177 self.tipnode = tipnode
180 self.tiprev = tiprev
178 self.tiprev = tiprev
181 self.filteredhash = filteredhash
179 self.filteredhash = filteredhash
182 # closednodes is a set of nodes that close their branch. If the branch
180 # closednodes is a set of nodes that close their branch. If the branch
183 # cache has been updated, it may contain nodes that are no longer
181 # cache has been updated, it may contain nodes that are no longer
184 # heads.
182 # heads.
185 if closednodes is None:
183 if closednodes is None:
186 self._closednodes = set()
184 self._closednodes = set()
187 else:
185 else:
188 self._closednodes = closednodes
186 self._closednodes = closednodes
189
187
190 def validfor(self, repo):
188 def validfor(self, repo):
191 """Is the cache content valid regarding a repo
189 """Is the cache content valid regarding a repo
192
190
193 - False when cached tipnode is unknown or if we detect a strip.
191 - False when cached tipnode is unknown or if we detect a strip.
194 - True when cache is up to date or a subset of current repo."""
192 - True when cache is up to date or a subset of current repo."""
195 try:
193 try:
196 return ((self.tipnode == repo.changelog.node(self.tiprev))
194 return ((self.tipnode == repo.changelog.node(self.tiprev))
197 and (self.filteredhash == \
195 and (self.filteredhash == \
198 scmutil.filteredhash(repo, self.tiprev)))
196 scmutil.filteredhash(repo, self.tiprev)))
199 except IndexError:
197 except IndexError:
200 return False
198 return False
201
199
202 def _branchtip(self, heads):
200 def _branchtip(self, heads):
203 '''Return tuple with last open head in heads and false,
201 '''Return tuple with last open head in heads and false,
204 otherwise return last closed head and true.'''
202 otherwise return last closed head and true.'''
205 tip = heads[-1]
203 tip = heads[-1]
206 closed = True
204 closed = True
207 for h in reversed(heads):
205 for h in reversed(heads):
208 if h not in self._closednodes:
206 if h not in self._closednodes:
209 tip = h
207 tip = h
210 closed = False
208 closed = False
211 break
209 break
212 return tip, closed
210 return tip, closed
213
211
214 def branchtip(self, branch):
212 def branchtip(self, branch):
215 '''Return the tipmost open head on branch head, otherwise return the
213 '''Return the tipmost open head on branch head, otherwise return the
216 tipmost closed head on branch.
214 tipmost closed head on branch.
217 Raise KeyError for unknown branch.'''
215 Raise KeyError for unknown branch.'''
218 return self._branchtip(self[branch])[0]
216 return self._branchtip(self[branch])[0]
219
217
220 def iteropen(self, nodes):
218 def iteropen(self, nodes):
221 return (n for n in nodes if n not in self._closednodes)
219 return (n for n in nodes if n not in self._closednodes)
222
220
223 def branchheads(self, branch, closed=False):
221 def branchheads(self, branch, closed=False):
224 heads = self[branch]
222 heads = self[branch]
225 if not closed:
223 if not closed:
226 heads = list(self.iteropen(heads))
224 heads = list(self.iteropen(heads))
227 return heads
225 return heads
228
226
229 def iterbranches(self):
227 def iterbranches(self):
230 for bn, heads in self.iteritems():
228 for bn, heads in self.iteritems():
231 yield (bn, heads) + self._branchtip(heads)
229 yield (bn, heads) + self._branchtip(heads)
232
230
233 def copy(self):
231 def copy(self):
234 """return an deep copy of the branchcache object"""
232 """return an deep copy of the branchcache object"""
235 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
233 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
236 self._closednodes)
234 self._closednodes)
237
235
238 def write(self, repo):
236 def write(self, repo):
239 try:
237 try:
240 f = repo.cachevfs(_filename(repo), "w", atomictemp=True)
238 f = repo.cachevfs(_filename(repo), "w", atomictemp=True)
241 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
239 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
242 if self.filteredhash is not None:
240 if self.filteredhash is not None:
243 cachekey.append(hex(self.filteredhash))
241 cachekey.append(hex(self.filteredhash))
244 f.write(" ".join(cachekey) + '\n')
242 f.write(" ".join(cachekey) + '\n')
245 nodecount = 0
243 nodecount = 0
246 for label, nodes in sorted(self.iteritems()):
244 for label, nodes in sorted(self.iteritems()):
247 for node in nodes:
245 for node in nodes:
248 nodecount += 1
246 nodecount += 1
249 if node in self._closednodes:
247 if node in self._closednodes:
250 state = 'c'
248 state = 'c'
251 else:
249 else:
252 state = 'o'
250 state = 'o'
253 f.write("%s %s %s\n" % (hex(node), state,
251 f.write("%s %s %s\n" % (hex(node), state,
254 encoding.fromlocal(label)))
252 encoding.fromlocal(label)))
255 f.close()
253 f.close()
256 repo.ui.log('branchcache',
254 repo.ui.log('branchcache',
257 'wrote %s branch cache with %d labels and %d nodes\n',
255 'wrote %s branch cache with %d labels and %d nodes\n',
258 repo.filtername, len(self), nodecount)
256 repo.filtername, len(self), nodecount)
259 except (IOError, OSError, error.Abort) as inst:
257 except (IOError, OSError, error.Abort) as inst:
260 # Abort may be raised by read only opener, so log and continue
258 # Abort may be raised by read only opener, so log and continue
261 repo.ui.debug("couldn't write branch cache: %s\n" %
259 repo.ui.debug("couldn't write branch cache: %s\n" %
262 stringutil.forcebytestr(inst))
260 stringutil.forcebytestr(inst))
263
261
264 def update(self, repo, revgen):
262 def update(self, repo, revgen):
265 """Given a branchhead cache, self, that may have extra nodes or be
263 """Given a branchhead cache, self, that may have extra nodes or be
266 missing heads, and a generator of nodes that are strictly a superset of
264 missing heads, and a generator of nodes that are strictly a superset of
267 heads missing, this function updates self to be correct.
265 heads missing, this function updates self to be correct.
268 """
266 """
269 starttime = util.timer()
267 starttime = util.timer()
270 cl = repo.changelog
268 cl = repo.changelog
271 # collect new branch entries
269 # collect new branch entries
272 newbranches = {}
270 newbranches = {}
273 getbranchinfo = repo.revbranchcache().branchinfo
271 getbranchinfo = repo.revbranchcache().branchinfo
274 for r in revgen:
272 for r in revgen:
275 branch, closesbranch = getbranchinfo(r)
273 branch, closesbranch = getbranchinfo(r)
276 newbranches.setdefault(branch, []).append(r)
274 newbranches.setdefault(branch, []).append(r)
277 if closesbranch:
275 if closesbranch:
278 self._closednodes.add(cl.node(r))
276 self._closednodes.add(cl.node(r))
279
277
280 # fetch current topological heads to speed up filtering
278 # fetch current topological heads to speed up filtering
281 topoheads = set(cl.headrevs())
279 topoheads = set(cl.headrevs())
282
280
283 # if older branchheads are reachable from new ones, they aren't
281 # if older branchheads are reachable from new ones, they aren't
284 # really branchheads. Note checking parents is insufficient:
282 # really branchheads. Note checking parents is insufficient:
285 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
283 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
286 for branch, newheadrevs in newbranches.iteritems():
284 for branch, newheadrevs in newbranches.iteritems():
287 bheads = self.setdefault(branch, [])
285 bheads = self.setdefault(branch, [])
288 bheadset = set(cl.rev(node) for node in bheads)
286 bheadset = set(cl.rev(node) for node in bheads)
289
287
290 # This have been tested True on all internal usage of this function.
288 # This have been tested True on all internal usage of this function.
291 # run it again in case of doubt
289 # run it again in case of doubt
292 # assert not (set(bheadrevs) & set(newheadrevs))
290 # assert not (set(bheadrevs) & set(newheadrevs))
293 newheadrevs.sort()
291 newheadrevs.sort()
294 bheadset.update(newheadrevs)
292 bheadset.update(newheadrevs)
295
293
296 # This prunes out two kinds of heads - heads that are superseded by
294 # This prunes out two kinds of heads - heads that are superseded by
297 # a head in newheadrevs, and newheadrevs that are not heads because
295 # a head in newheadrevs, and newheadrevs that are not heads because
298 # an existing head is their descendant.
296 # an existing head is their descendant.
299 uncertain = bheadset - topoheads
297 uncertain = bheadset - topoheads
300 if uncertain:
298 if uncertain:
301 floorrev = min(uncertain)
299 floorrev = min(uncertain)
302 ancestors = set(cl.ancestors(newheadrevs, floorrev))
300 ancestors = set(cl.ancestors(newheadrevs, floorrev))
303 bheadset -= ancestors
301 bheadset -= ancestors
304 bheadrevs = sorted(bheadset)
302 bheadrevs = sorted(bheadset)
305 self[branch] = [cl.node(rev) for rev in bheadrevs]
303 self[branch] = [cl.node(rev) for rev in bheadrevs]
306 tiprev = bheadrevs[-1]
304 tiprev = bheadrevs[-1]
307 if tiprev > self.tiprev:
305 if tiprev > self.tiprev:
308 self.tipnode = cl.node(tiprev)
306 self.tipnode = cl.node(tiprev)
309 self.tiprev = tiprev
307 self.tiprev = tiprev
310
308
311 if not self.validfor(repo):
309 if not self.validfor(repo):
312 # cache key are not valid anymore
310 # cache key are not valid anymore
313 self.tipnode = nullid
311 self.tipnode = nullid
314 self.tiprev = nullrev
312 self.tiprev = nullrev
315 for heads in self.values():
313 for heads in self.values():
316 tiprev = max(cl.rev(node) for node in heads)
314 tiprev = max(cl.rev(node) for node in heads)
317 if tiprev > self.tiprev:
315 if tiprev > self.tiprev:
318 self.tipnode = cl.node(tiprev)
316 self.tipnode = cl.node(tiprev)
319 self.tiprev = tiprev
317 self.tiprev = tiprev
320 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
318 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
321
319
322 duration = util.timer() - starttime
320 duration = util.timer() - starttime
323 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
321 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
324 repo.filtername, duration)
322 repo.filtername, duration)
325
323
326 # Revision branch info cache
324 # Revision branch info cache
327
325
328 _rbcversion = '-v1'
326 _rbcversion = '-v1'
329 _rbcnames = 'rbc-names' + _rbcversion
327 _rbcnames = 'rbc-names' + _rbcversion
330 _rbcrevs = 'rbc-revs' + _rbcversion
328 _rbcrevs = 'rbc-revs' + _rbcversion
331 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
329 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
332 _rbcrecfmt = '>4sI'
330 _rbcrecfmt = '>4sI'
333 _rbcrecsize = calcsize(_rbcrecfmt)
331 _rbcrecsize = calcsize(_rbcrecfmt)
334 _rbcnodelen = 4
332 _rbcnodelen = 4
335 _rbcbranchidxmask = 0x7fffffff
333 _rbcbranchidxmask = 0x7fffffff
336 _rbccloseflag = 0x80000000
334 _rbccloseflag = 0x80000000
337
335
338 class revbranchcache(object):
336 class revbranchcache(object):
339 """Persistent cache, mapping from revision number to branch name and close.
337 """Persistent cache, mapping from revision number to branch name and close.
340 This is a low level cache, independent of filtering.
338 This is a low level cache, independent of filtering.
341
339
342 Branch names are stored in rbc-names in internal encoding separated by 0.
340 Branch names are stored in rbc-names in internal encoding separated by 0.
343 rbc-names is append-only, and each branch name is only stored once and will
341 rbc-names is append-only, and each branch name is only stored once and will
344 thus have a unique index.
342 thus have a unique index.
345
343
346 The branch info for each revision is stored in rbc-revs as constant size
344 The branch info for each revision is stored in rbc-revs as constant size
347 records. The whole file is read into memory, but it is only 'parsed' on
345 records. The whole file is read into memory, but it is only 'parsed' on
348 demand. The file is usually append-only but will be truncated if repo
346 demand. The file is usually append-only but will be truncated if repo
349 modification is detected.
347 modification is detected.
350 The record for each revision contains the first 4 bytes of the
348 The record for each revision contains the first 4 bytes of the
351 corresponding node hash, and the record is only used if it still matches.
349 corresponding node hash, and the record is only used if it still matches.
352 Even a completely trashed rbc-revs fill thus still give the right result
350 Even a completely trashed rbc-revs fill thus still give the right result
353 while converging towards full recovery ... assuming no incorrectly matching
351 while converging towards full recovery ... assuming no incorrectly matching
354 node hashes.
352 node hashes.
355 The record also contains 4 bytes where 31 bits contains the index of the
353 The record also contains 4 bytes where 31 bits contains the index of the
356 branch and the last bit indicate that it is a branch close commit.
354 branch and the last bit indicate that it is a branch close commit.
357 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
355 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
358 and will grow with it but be 1/8th of its size.
356 and will grow with it but be 1/8th of its size.
359 """
357 """
360
358
361 def __init__(self, repo, readonly=True):
359 def __init__(self, repo, readonly=True):
362 assert repo.filtername is None
360 assert repo.filtername is None
363 self._repo = repo
361 self._repo = repo
364 self._names = [] # branch names in local encoding with static index
362 self._names = [] # branch names in local encoding with static index
365 self._rbcrevs = bytearray()
363 self._rbcrevs = bytearray()
366 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
364 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
367 try:
365 try:
368 bndata = repo.cachevfs.read(_rbcnames)
366 bndata = repo.cachevfs.read(_rbcnames)
369 self._rbcsnameslen = len(bndata) # for verification before writing
367 self._rbcsnameslen = len(bndata) # for verification before writing
370 if bndata:
368 if bndata:
371 self._names = [encoding.tolocal(bn)
369 self._names = [encoding.tolocal(bn)
372 for bn in bndata.split('\0')]
370 for bn in bndata.split('\0')]
373 except (IOError, OSError):
371 except (IOError, OSError):
374 if readonly:
372 if readonly:
375 # don't try to use cache - fall back to the slow path
373 # don't try to use cache - fall back to the slow path
376 self.branchinfo = self._branchinfo
374 self.branchinfo = self._branchinfo
377
375
378 if self._names:
376 if self._names:
379 try:
377 try:
380 data = repo.cachevfs.read(_rbcrevs)
378 data = repo.cachevfs.read(_rbcrevs)
381 self._rbcrevs[:] = data
379 self._rbcrevs[:] = data
382 except (IOError, OSError) as inst:
380 except (IOError, OSError) as inst:
383 repo.ui.debug("couldn't read revision branch cache: %s\n" %
381 repo.ui.debug("couldn't read revision branch cache: %s\n" %
384 stringutil.forcebytestr(inst))
382 stringutil.forcebytestr(inst))
385 # remember number of good records on disk
383 # remember number of good records on disk
386 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
384 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
387 len(repo.changelog))
385 len(repo.changelog))
388 if self._rbcrevslen == 0:
386 if self._rbcrevslen == 0:
389 self._names = []
387 self._names = []
390 self._rbcnamescount = len(self._names) # number of names read at
388 self._rbcnamescount = len(self._names) # number of names read at
391 # _rbcsnameslen
389 # _rbcsnameslen
392 self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
390 self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
393
391
394 def _clear(self):
392 def _clear(self):
395 self._rbcsnameslen = 0
393 self._rbcsnameslen = 0
396 del self._names[:]
394 del self._names[:]
397 self._rbcnamescount = 0
395 self._rbcnamescount = 0
398 self._namesreverse.clear()
396 self._namesreverse.clear()
399 self._rbcrevslen = len(self._repo.changelog)
397 self._rbcrevslen = len(self._repo.changelog)
400 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
398 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
401
399
402 def branchinfo(self, rev):
400 def branchinfo(self, rev):
403 """Return branch name and close flag for rev, using and updating
401 """Return branch name and close flag for rev, using and updating
404 persistent cache."""
402 persistent cache."""
405 changelog = self._repo.changelog
403 changelog = self._repo.changelog
406 rbcrevidx = rev * _rbcrecsize
404 rbcrevidx = rev * _rbcrecsize
407
405
408 # avoid negative index, changelog.read(nullrev) is fast without cache
406 # avoid negative index, changelog.read(nullrev) is fast without cache
409 if rev == nullrev:
407 if rev == nullrev:
410 return changelog.branchinfo(rev)
408 return changelog.branchinfo(rev)
411
409
412 # if requested rev isn't allocated, grow and cache the rev info
410 # if requested rev isn't allocated, grow and cache the rev info
413 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
411 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
414 return self._branchinfo(rev)
412 return self._branchinfo(rev)
415
413
416 # fast path: extract data from cache, use it if node is matching
414 # fast path: extract data from cache, use it if node is matching
417 reponode = changelog.node(rev)[:_rbcnodelen]
415 reponode = changelog.node(rev)[:_rbcnodelen]
418 cachenode, branchidx = unpack_from(
416 cachenode, branchidx = unpack_from(
419 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx)
417 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx)
420 close = bool(branchidx & _rbccloseflag)
418 close = bool(branchidx & _rbccloseflag)
421 if close:
419 if close:
422 branchidx &= _rbcbranchidxmask
420 branchidx &= _rbcbranchidxmask
423 if cachenode == '\0\0\0\0':
421 if cachenode == '\0\0\0\0':
424 pass
422 pass
425 elif cachenode == reponode:
423 elif cachenode == reponode:
426 try:
424 try:
427 return self._names[branchidx], close
425 return self._names[branchidx], close
428 except IndexError:
426 except IndexError:
429 # recover from invalid reference to unknown branch
427 # recover from invalid reference to unknown branch
430 self._repo.ui.debug("referenced branch names not found"
428 self._repo.ui.debug("referenced branch names not found"
431 " - rebuilding revision branch cache from scratch\n")
429 " - rebuilding revision branch cache from scratch\n")
432 self._clear()
430 self._clear()
433 else:
431 else:
434 # rev/node map has changed, invalidate the cache from here up
432 # rev/node map has changed, invalidate the cache from here up
435 self._repo.ui.debug("history modification detected - truncating "
433 self._repo.ui.debug("history modification detected - truncating "
436 "revision branch cache to revision %d\n" % rev)
434 "revision branch cache to revision %d\n" % rev)
437 truncate = rbcrevidx + _rbcrecsize
435 truncate = rbcrevidx + _rbcrecsize
438 del self._rbcrevs[truncate:]
436 del self._rbcrevs[truncate:]
439 self._rbcrevslen = min(self._rbcrevslen, truncate)
437 self._rbcrevslen = min(self._rbcrevslen, truncate)
440
438
441 # fall back to slow path and make sure it will be written to disk
439 # fall back to slow path and make sure it will be written to disk
442 return self._branchinfo(rev)
440 return self._branchinfo(rev)
443
441
444 def _branchinfo(self, rev):
442 def _branchinfo(self, rev):
445 """Retrieve branch info from changelog and update _rbcrevs"""
443 """Retrieve branch info from changelog and update _rbcrevs"""
446 changelog = self._repo.changelog
444 changelog = self._repo.changelog
447 b, close = changelog.branchinfo(rev)
445 b, close = changelog.branchinfo(rev)
448 if b in self._namesreverse:
446 if b in self._namesreverse:
449 branchidx = self._namesreverse[b]
447 branchidx = self._namesreverse[b]
450 else:
448 else:
451 branchidx = len(self._names)
449 branchidx = len(self._names)
452 self._names.append(b)
450 self._names.append(b)
453 self._namesreverse[b] = branchidx
451 self._namesreverse[b] = branchidx
454 reponode = changelog.node(rev)
452 reponode = changelog.node(rev)
455 if close:
453 if close:
456 branchidx |= _rbccloseflag
454 branchidx |= _rbccloseflag
457 self._setcachedata(rev, reponode, branchidx)
455 self._setcachedata(rev, reponode, branchidx)
458 return b, close
456 return b, close
459
457
460 def setdata(self, branch, rev, node, close):
458 def setdata(self, branch, rev, node, close):
461 """add new data information to the cache"""
459 """add new data information to the cache"""
462 if branch in self._namesreverse:
460 if branch in self._namesreverse:
463 branchidx = self._namesreverse[branch]
461 branchidx = self._namesreverse[branch]
464 else:
462 else:
465 branchidx = len(self._names)
463 branchidx = len(self._names)
466 self._names.append(branch)
464 self._names.append(branch)
467 self._namesreverse[branch] = branchidx
465 self._namesreverse[branch] = branchidx
468 if close:
466 if close:
469 branchidx |= _rbccloseflag
467 branchidx |= _rbccloseflag
470 self._setcachedata(rev, node, branchidx)
468 self._setcachedata(rev, node, branchidx)
471 # If no cache data were readable (non exists, bad permission, etc)
469 # If no cache data were readable (non exists, bad permission, etc)
472 # the cache was bypassing itself by setting:
470 # the cache was bypassing itself by setting:
473 #
471 #
474 # self.branchinfo = self._branchinfo
472 # self.branchinfo = self._branchinfo
475 #
473 #
476 # Since we now have data in the cache, we need to drop this bypassing.
474 # Since we now have data in the cache, we need to drop this bypassing.
477 if 'branchinfo' in vars(self):
475 if 'branchinfo' in vars(self):
478 del self.branchinfo
476 del self.branchinfo
479
477
480 def _setcachedata(self, rev, node, branchidx):
478 def _setcachedata(self, rev, node, branchidx):
481 """Writes the node's branch data to the in-memory cache data."""
479 """Writes the node's branch data to the in-memory cache data."""
482 if rev == nullrev:
480 if rev == nullrev:
483 return
481 return
484 rbcrevidx = rev * _rbcrecsize
482 rbcrevidx = rev * _rbcrecsize
485 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
483 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
486 self._rbcrevs.extend('\0' *
484 self._rbcrevs.extend('\0' *
487 (len(self._repo.changelog) * _rbcrecsize -
485 (len(self._repo.changelog) * _rbcrecsize -
488 len(self._rbcrevs)))
486 len(self._rbcrevs)))
489 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
487 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
490 self._rbcrevslen = min(self._rbcrevslen, rev)
488 self._rbcrevslen = min(self._rbcrevslen, rev)
491
489
492 tr = self._repo.currenttransaction()
490 tr = self._repo.currenttransaction()
493 if tr:
491 if tr:
494 tr.addfinalize('write-revbranchcache', self.write)
492 tr.addfinalize('write-revbranchcache', self.write)
495
493
496 def write(self, tr=None):
494 def write(self, tr=None):
497 """Save branch cache if it is dirty."""
495 """Save branch cache if it is dirty."""
498 repo = self._repo
496 repo = self._repo
499 wlock = None
497 wlock = None
500 step = ''
498 step = ''
501 try:
499 try:
502 if self._rbcnamescount < len(self._names):
500 if self._rbcnamescount < len(self._names):
503 step = ' names'
501 step = ' names'
504 wlock = repo.wlock(wait=False)
502 wlock = repo.wlock(wait=False)
505 if self._rbcnamescount != 0:
503 if self._rbcnamescount != 0:
506 f = repo.cachevfs.open(_rbcnames, 'ab')
504 f = repo.cachevfs.open(_rbcnames, 'ab')
507 if f.tell() == self._rbcsnameslen:
505 if f.tell() == self._rbcsnameslen:
508 f.write('\0')
506 f.write('\0')
509 else:
507 else:
510 f.close()
508 f.close()
511 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
509 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
512 self._rbcnamescount = 0
510 self._rbcnamescount = 0
513 self._rbcrevslen = 0
511 self._rbcrevslen = 0
514 if self._rbcnamescount == 0:
512 if self._rbcnamescount == 0:
515 # before rewriting names, make sure references are removed
513 # before rewriting names, make sure references are removed
516 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
514 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
517 f = repo.cachevfs.open(_rbcnames, 'wb')
515 f = repo.cachevfs.open(_rbcnames, 'wb')
518 f.write('\0'.join(encoding.fromlocal(b)
516 f.write('\0'.join(encoding.fromlocal(b)
519 for b in self._names[self._rbcnamescount:]))
517 for b in self._names[self._rbcnamescount:]))
520 self._rbcsnameslen = f.tell()
518 self._rbcsnameslen = f.tell()
521 f.close()
519 f.close()
522 self._rbcnamescount = len(self._names)
520 self._rbcnamescount = len(self._names)
523
521
524 start = self._rbcrevslen * _rbcrecsize
522 start = self._rbcrevslen * _rbcrecsize
525 if start != len(self._rbcrevs):
523 if start != len(self._rbcrevs):
526 step = ''
524 step = ''
527 if wlock is None:
525 if wlock is None:
528 wlock = repo.wlock(wait=False)
526 wlock = repo.wlock(wait=False)
529 revs = min(len(repo.changelog),
527 revs = min(len(repo.changelog),
530 len(self._rbcrevs) // _rbcrecsize)
528 len(self._rbcrevs) // _rbcrecsize)
531 f = repo.cachevfs.open(_rbcrevs, 'ab')
529 f = repo.cachevfs.open(_rbcrevs, 'ab')
532 if f.tell() != start:
530 if f.tell() != start:
533 repo.ui.debug("truncating cache/%s to %d\n"
531 repo.ui.debug("truncating cache/%s to %d\n"
534 % (_rbcrevs, start))
532 % (_rbcrevs, start))
535 f.seek(start)
533 f.seek(start)
536 if f.tell() != start:
534 if f.tell() != start:
537 start = 0
535 start = 0
538 f.seek(start)
536 f.seek(start)
539 f.truncate()
537 f.truncate()
540 end = revs * _rbcrecsize
538 end = revs * _rbcrecsize
541 f.write(self._rbcrevs[start:end])
539 f.write(self._rbcrevs[start:end])
542 f.close()
540 f.close()
543 self._rbcrevslen = revs
541 self._rbcrevslen = revs
544 except (IOError, OSError, error.Abort, error.LockError) as inst:
542 except (IOError, OSError, error.Abort, error.LockError) as inst:
545 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
543 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
546 % (step, stringutil.forcebytestr(inst)))
544 % (step, stringutil.forcebytestr(inst)))
547 finally:
545 finally:
548 if wlock is not None:
546 if wlock is not None:
549 wlock.release()
547 wlock.release()
General Comments 0
You need to be logged in to leave comments. Login now