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