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