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