##// END OF EJS Templates
branchcache: make entries a private attribute...
Pulkit Goyal -
r42172:b137a679 default
parent child Browse files
Show More
@@ -1,620 +1,620
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 iteritems(self):
175 def iteritems(self):
176 return self.entries.iteritems()
176 return self._entries.iteritems()
177
177
178 def hasbranch(self, label):
178 def hasbranch(self, label):
179 """ checks whether a branch of this name exists or not """
179 """ checks whether a branch of this name exists or not """
180 return label in self.entries
180 return label in self._entries
181
181
182 @classmethod
182 @classmethod
183 def fromfile(cls, repo):
183 def fromfile(cls, repo):
184 f = None
184 f = None
185 try:
185 try:
186 f = repo.cachevfs(cls._filename(repo))
186 f = repo.cachevfs(cls._filename(repo))
187 lineiter = iter(f)
187 lineiter = iter(f)
188 cachekey = next(lineiter).rstrip('\n').split(" ", 2)
188 cachekey = next(lineiter).rstrip('\n').split(" ", 2)
189 last, lrev = cachekey[:2]
189 last, lrev = cachekey[:2]
190 last, lrev = bin(last), int(lrev)
190 last, lrev = bin(last), int(lrev)
191 filteredhash = None
191 filteredhash = None
192 if len(cachekey) > 2:
192 if len(cachekey) > 2:
193 filteredhash = bin(cachekey[2])
193 filteredhash = bin(cachekey[2])
194 bcache = cls(tipnode=last, tiprev=lrev, filteredhash=filteredhash)
194 bcache = cls(tipnode=last, tiprev=lrev, filteredhash=filteredhash)
195 if not bcache.validfor(repo):
195 if not bcache.validfor(repo):
196 # invalidate the cache
196 # invalidate the cache
197 raise ValueError(r'tip differs')
197 raise ValueError(r'tip differs')
198 bcache.load(repo, lineiter)
198 bcache.load(repo, lineiter)
199 except (IOError, OSError):
199 except (IOError, OSError):
200 return None
200 return None
201
201
202 except Exception as inst:
202 except Exception as inst:
203 if repo.ui.debugflag:
203 if repo.ui.debugflag:
204 msg = 'invalid branchheads cache'
204 msg = 'invalid branchheads cache'
205 if repo.filtername is not None:
205 if repo.filtername is not None:
206 msg += ' (%s)' % repo.filtername
206 msg += ' (%s)' % repo.filtername
207 msg += ': %s\n'
207 msg += ': %s\n'
208 repo.ui.debug(msg % pycompat.bytestr(inst))
208 repo.ui.debug(msg % pycompat.bytestr(inst))
209 bcache = None
209 bcache = None
210
210
211 finally:
211 finally:
212 if f:
212 if f:
213 f.close()
213 f.close()
214
214
215 return bcache
215 return bcache
216
216
217 def load(self, repo, lineiter):
217 def load(self, repo, lineiter):
218 """ fully loads the branchcache by reading from the file using the line
218 """ fully loads the branchcache by reading from the file using the line
219 iterator passed"""
219 iterator passed"""
220 cl = repo.changelog
220 cl = repo.changelog
221 for line in lineiter:
221 for line in lineiter:
222 line = line.rstrip('\n')
222 line = line.rstrip('\n')
223 if not line:
223 if not line:
224 continue
224 continue
225 node, state, label = line.split(" ", 2)
225 node, state, label = line.split(" ", 2)
226 if state not in 'oc':
226 if state not in 'oc':
227 raise ValueError(r'invalid branch state')
227 raise ValueError(r'invalid branch state')
228 label = encoding.tolocal(label.strip())
228 label = encoding.tolocal(label.strip())
229 node = bin(node)
229 node = bin(node)
230 if not cl.hasnode(node):
230 if not cl.hasnode(node):
231 raise ValueError(
231 raise ValueError(
232 r'node %s does not exist' % pycompat.sysstr(hex(node)))
232 r'node %s does not exist' % pycompat.sysstr(hex(node)))
233 self.entries.setdefault(label, []).append(node)
233 self._entries.setdefault(label, []).append(node)
234 if state == 'c':
234 if state == 'c':
235 self._closednodes.add(node)
235 self._closednodes.add(node)
236
236
237 @staticmethod
237 @staticmethod
238 def _filename(repo):
238 def _filename(repo):
239 """name of a branchcache file for a given repo or repoview"""
239 """name of a branchcache file for a given repo or repoview"""
240 filename = "branch2"
240 filename = "branch2"
241 if repo.filtername:
241 if repo.filtername:
242 filename = '%s-%s' % (filename, repo.filtername)
242 filename = '%s-%s' % (filename, repo.filtername)
243 return filename
243 return filename
244
244
245 def validfor(self, repo):
245 def validfor(self, repo):
246 """Is the cache content valid regarding a repo
246 """Is the cache content valid regarding a repo
247
247
248 - False when cached tipnode is unknown or if we detect a strip.
248 - False when cached tipnode is unknown or if we detect a strip.
249 - True when cache is up to date or a subset of current repo."""
249 - True when cache is up to date or a subset of current repo."""
250 try:
250 try:
251 return ((self.tipnode == repo.changelog.node(self.tiprev))
251 return ((self.tipnode == repo.changelog.node(self.tiprev))
252 and (self.filteredhash ==
252 and (self.filteredhash ==
253 scmutil.filteredhash(repo, self.tiprev)))
253 scmutil.filteredhash(repo, self.tiprev)))
254 except IndexError:
254 except IndexError:
255 return False
255 return False
256
256
257 def _branchtip(self, heads):
257 def _branchtip(self, heads):
258 '''Return tuple with last open head in heads and false,
258 '''Return tuple with last open head in heads and false,
259 otherwise return last closed head and true.'''
259 otherwise return last closed head and true.'''
260 tip = heads[-1]
260 tip = heads[-1]
261 closed = True
261 closed = True
262 for h in reversed(heads):
262 for h in reversed(heads):
263 if h not in self._closednodes:
263 if h not in self._closednodes:
264 tip = h
264 tip = h
265 closed = False
265 closed = False
266 break
266 break
267 return tip, closed
267 return tip, closed
268
268
269 def branchtip(self, branch):
269 def branchtip(self, branch):
270 '''Return the tipmost open head on branch head, otherwise return the
270 '''Return the tipmost open head on branch head, otherwise return the
271 tipmost closed head on branch.
271 tipmost closed head on branch.
272 Raise KeyError for unknown branch.'''
272 Raise KeyError for unknown branch.'''
273 return self._branchtip(self[branch])[0]
273 return self._branchtip(self[branch])[0]
274
274
275 def iteropen(self, nodes):
275 def iteropen(self, nodes):
276 return (n for n in nodes if n not in self._closednodes)
276 return (n for n in nodes if n not in self._closednodes)
277
277
278 def branchheads(self, branch, closed=False):
278 def branchheads(self, branch, closed=False):
279 heads = self[branch]
279 heads = self[branch]
280 if not closed:
280 if not closed:
281 heads = list(self.iteropen(heads))
281 heads = list(self.iteropen(heads))
282 return heads
282 return heads
283
283
284 def iterbranches(self):
284 def iterbranches(self):
285 for bn, heads in self.iteritems():
285 for bn, heads in self.iteritems():
286 yield (bn, heads) + self._branchtip(heads)
286 yield (bn, heads) + self._branchtip(heads)
287
287
288 def iterheads(self):
288 def iterheads(self):
289 """ returns all the heads """
289 """ returns all the heads """
290 return self.entries.itervalues()
290 return self._entries.itervalues()
291
291
292 def copy(self):
292 def copy(self):
293 """return an deep copy of the branchcache object"""
293 """return an deep copy of the branchcache object"""
294 return branchcache(
294 return branchcache(
295 self.entries, self.tipnode, self.tiprev, self.filteredhash,
295 self._entries, self.tipnode, self.tiprev, self.filteredhash,
296 self._closednodes)
296 self._closednodes)
297
297
298 def write(self, repo):
298 def write(self, repo):
299 try:
299 try:
300 f = repo.cachevfs(self._filename(repo), "w", atomictemp=True)
300 f = repo.cachevfs(self._filename(repo), "w", atomictemp=True)
301 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
301 cachekey = [hex(self.tipnode), '%d' % self.tiprev]
302 if self.filteredhash is not None:
302 if self.filteredhash is not None:
303 cachekey.append(hex(self.filteredhash))
303 cachekey.append(hex(self.filteredhash))
304 f.write(" ".join(cachekey) + '\n')
304 f.write(" ".join(cachekey) + '\n')
305 nodecount = 0
305 nodecount = 0
306 for label, nodes in sorted(self.iteritems()):
306 for label, nodes in sorted(self.iteritems()):
307 label = encoding.fromlocal(label)
307 label = encoding.fromlocal(label)
308 for node in nodes:
308 for node in nodes:
309 nodecount += 1
309 nodecount += 1
310 if node in self._closednodes:
310 if node in self._closednodes:
311 state = 'c'
311 state = 'c'
312 else:
312 else:
313 state = 'o'
313 state = 'o'
314 f.write("%s %s %s\n" % (hex(node), state, label))
314 f.write("%s %s %s\n" % (hex(node), state, label))
315 f.close()
315 f.close()
316 repo.ui.log('branchcache',
316 repo.ui.log('branchcache',
317 'wrote %s branch cache with %d labels and %d nodes\n',
317 'wrote %s branch cache with %d labels and %d nodes\n',
318 repo.filtername, len(self.entries), nodecount)
318 repo.filtername, len(self._entries), nodecount)
319 except (IOError, OSError, error.Abort) as inst:
319 except (IOError, OSError, error.Abort) as inst:
320 # Abort may be raised by read only opener, so log and continue
320 # Abort may be raised by read only opener, so log and continue
321 repo.ui.debug("couldn't write branch cache: %s\n" %
321 repo.ui.debug("couldn't write branch cache: %s\n" %
322 stringutil.forcebytestr(inst))
322 stringutil.forcebytestr(inst))
323
323
324 def update(self, repo, revgen):
324 def update(self, repo, revgen):
325 """Given a branchhead cache, self, that may have extra nodes or be
325 """Given a branchhead cache, self, that may have extra nodes or be
326 missing heads, and a generator of nodes that are strictly a superset of
326 missing heads, and a generator of nodes that are strictly a superset of
327 heads missing, this function updates self to be correct.
327 heads missing, this function updates self to be correct.
328 """
328 """
329 starttime = util.timer()
329 starttime = util.timer()
330 cl = repo.changelog
330 cl = repo.changelog
331 # collect new branch entries
331 # collect new branch entries
332 newbranches = {}
332 newbranches = {}
333 getbranchinfo = repo.revbranchcache().branchinfo
333 getbranchinfo = repo.revbranchcache().branchinfo
334 for r in revgen:
334 for r in revgen:
335 branch, closesbranch = getbranchinfo(r)
335 branch, closesbranch = getbranchinfo(r)
336 newbranches.setdefault(branch, []).append(r)
336 newbranches.setdefault(branch, []).append(r)
337 if closesbranch:
337 if closesbranch:
338 self._closednodes.add(cl.node(r))
338 self._closednodes.add(cl.node(r))
339
339
340 # fetch current topological heads to speed up filtering
340 # fetch current topological heads to speed up filtering
341 topoheads = set(cl.headrevs())
341 topoheads = set(cl.headrevs())
342
342
343 # if older branchheads are reachable from new ones, they aren't
343 # if older branchheads are reachable from new ones, they aren't
344 # really branchheads. Note checking parents is insufficient:
344 # really branchheads. Note checking parents is insufficient:
345 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
345 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
346 for branch, newheadrevs in newbranches.iteritems():
346 for branch, newheadrevs in newbranches.iteritems():
347 bheads = self.entries.setdefault(branch, [])
347 bheads = self._entries.setdefault(branch, [])
348 bheadset = set(cl.rev(node) for node in bheads)
348 bheadset = set(cl.rev(node) for node in bheads)
349
349
350 # This have been tested True on all internal usage of this function.
350 # This have been tested True on all internal usage of this function.
351 # run it again in case of doubt
351 # run it again in case of doubt
352 # assert not (set(bheadrevs) & set(newheadrevs))
352 # assert not (set(bheadrevs) & set(newheadrevs))
353 bheadset.update(newheadrevs)
353 bheadset.update(newheadrevs)
354
354
355 # This prunes out two kinds of heads - heads that are superseded by
355 # This prunes out two kinds of heads - heads that are superseded by
356 # a head in newheadrevs, and newheadrevs that are not heads because
356 # a head in newheadrevs, and newheadrevs that are not heads because
357 # an existing head is their descendant.
357 # an existing head is their descendant.
358 uncertain = bheadset - topoheads
358 uncertain = bheadset - topoheads
359 if uncertain:
359 if uncertain:
360 floorrev = min(uncertain)
360 floorrev = min(uncertain)
361 ancestors = set(cl.ancestors(newheadrevs, floorrev))
361 ancestors = set(cl.ancestors(newheadrevs, floorrev))
362 bheadset -= ancestors
362 bheadset -= ancestors
363 bheadrevs = sorted(bheadset)
363 bheadrevs = sorted(bheadset)
364 self[branch] = [cl.node(rev) for rev in bheadrevs]
364 self[branch] = [cl.node(rev) for rev in bheadrevs]
365 tiprev = bheadrevs[-1]
365 tiprev = bheadrevs[-1]
366 if tiprev > self.tiprev:
366 if tiprev > self.tiprev:
367 self.tipnode = cl.node(tiprev)
367 self.tipnode = cl.node(tiprev)
368 self.tiprev = tiprev
368 self.tiprev = tiprev
369
369
370 if not self.validfor(repo):
370 if not self.validfor(repo):
371 # cache key are not valid anymore
371 # cache key are not valid anymore
372 self.tipnode = nullid
372 self.tipnode = nullid
373 self.tiprev = nullrev
373 self.tiprev = nullrev
374 for heads in self.iterheads():
374 for heads in self.iterheads():
375 tiprev = max(cl.rev(node) for node in heads)
375 tiprev = max(cl.rev(node) for node in heads)
376 if tiprev > self.tiprev:
376 if tiprev > self.tiprev:
377 self.tipnode = cl.node(tiprev)
377 self.tipnode = cl.node(tiprev)
378 self.tiprev = tiprev
378 self.tiprev = tiprev
379 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
379 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
380
380
381 duration = util.timer() - starttime
381 duration = util.timer() - starttime
382 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
382 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
383 repo.filtername or b'None', duration)
383 repo.filtername or b'None', duration)
384
384
385 self.write(repo)
385 self.write(repo)
386
386
387
387
388 class remotebranchcache(branchcache):
388 class remotebranchcache(branchcache):
389 """Branchmap info for a remote connection, should not write locally"""
389 """Branchmap info for a remote connection, should not write locally"""
390 def write(self, repo):
390 def write(self, repo):
391 pass
391 pass
392
392
393
393
394 # Revision branch info cache
394 # Revision branch info cache
395
395
396 _rbcversion = '-v1'
396 _rbcversion = '-v1'
397 _rbcnames = 'rbc-names' + _rbcversion
397 _rbcnames = 'rbc-names' + _rbcversion
398 _rbcrevs = 'rbc-revs' + _rbcversion
398 _rbcrevs = 'rbc-revs' + _rbcversion
399 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
399 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
400 _rbcrecfmt = '>4sI'
400 _rbcrecfmt = '>4sI'
401 _rbcrecsize = calcsize(_rbcrecfmt)
401 _rbcrecsize = calcsize(_rbcrecfmt)
402 _rbcnodelen = 4
402 _rbcnodelen = 4
403 _rbcbranchidxmask = 0x7fffffff
403 _rbcbranchidxmask = 0x7fffffff
404 _rbccloseflag = 0x80000000
404 _rbccloseflag = 0x80000000
405
405
406 class revbranchcache(object):
406 class revbranchcache(object):
407 """Persistent cache, mapping from revision number to branch name and close.
407 """Persistent cache, mapping from revision number to branch name and close.
408 This is a low level cache, independent of filtering.
408 This is a low level cache, independent of filtering.
409
409
410 Branch names are stored in rbc-names in internal encoding separated by 0.
410 Branch names are stored in rbc-names in internal encoding separated by 0.
411 rbc-names is append-only, and each branch name is only stored once and will
411 rbc-names is append-only, and each branch name is only stored once and will
412 thus have a unique index.
412 thus have a unique index.
413
413
414 The branch info for each revision is stored in rbc-revs as constant size
414 The branch info for each revision is stored in rbc-revs as constant size
415 records. The whole file is read into memory, but it is only 'parsed' on
415 records. The whole file is read into memory, but it is only 'parsed' on
416 demand. The file is usually append-only but will be truncated if repo
416 demand. The file is usually append-only but will be truncated if repo
417 modification is detected.
417 modification is detected.
418 The record for each revision contains the first 4 bytes of the
418 The record for each revision contains the first 4 bytes of the
419 corresponding node hash, and the record is only used if it still matches.
419 corresponding node hash, and the record is only used if it still matches.
420 Even a completely trashed rbc-revs fill thus still give the right result
420 Even a completely trashed rbc-revs fill thus still give the right result
421 while converging towards full recovery ... assuming no incorrectly matching
421 while converging towards full recovery ... assuming no incorrectly matching
422 node hashes.
422 node hashes.
423 The record also contains 4 bytes where 31 bits contains the index of the
423 The record also contains 4 bytes where 31 bits contains the index of the
424 branch and the last bit indicate that it is a branch close commit.
424 branch and the last bit indicate that it is a branch close commit.
425 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
425 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
426 and will grow with it but be 1/8th of its size.
426 and will grow with it but be 1/8th of its size.
427 """
427 """
428
428
429 def __init__(self, repo, readonly=True):
429 def __init__(self, repo, readonly=True):
430 assert repo.filtername is None
430 assert repo.filtername is None
431 self._repo = repo
431 self._repo = repo
432 self._names = [] # branch names in local encoding with static index
432 self._names = [] # branch names in local encoding with static index
433 self._rbcrevs = bytearray()
433 self._rbcrevs = bytearray()
434 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
434 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
435 try:
435 try:
436 bndata = repo.cachevfs.read(_rbcnames)
436 bndata = repo.cachevfs.read(_rbcnames)
437 self._rbcsnameslen = len(bndata) # for verification before writing
437 self._rbcsnameslen = len(bndata) # for verification before writing
438 if bndata:
438 if bndata:
439 self._names = [encoding.tolocal(bn)
439 self._names = [encoding.tolocal(bn)
440 for bn in bndata.split('\0')]
440 for bn in bndata.split('\0')]
441 except (IOError, OSError):
441 except (IOError, OSError):
442 if readonly:
442 if readonly:
443 # don't try to use cache - fall back to the slow path
443 # don't try to use cache - fall back to the slow path
444 self.branchinfo = self._branchinfo
444 self.branchinfo = self._branchinfo
445
445
446 if self._names:
446 if self._names:
447 try:
447 try:
448 data = repo.cachevfs.read(_rbcrevs)
448 data = repo.cachevfs.read(_rbcrevs)
449 self._rbcrevs[:] = data
449 self._rbcrevs[:] = data
450 except (IOError, OSError) as inst:
450 except (IOError, OSError) as inst:
451 repo.ui.debug("couldn't read revision branch cache: %s\n" %
451 repo.ui.debug("couldn't read revision branch cache: %s\n" %
452 stringutil.forcebytestr(inst))
452 stringutil.forcebytestr(inst))
453 # remember number of good records on disk
453 # remember number of good records on disk
454 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
454 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
455 len(repo.changelog))
455 len(repo.changelog))
456 if self._rbcrevslen == 0:
456 if self._rbcrevslen == 0:
457 self._names = []
457 self._names = []
458 self._rbcnamescount = len(self._names) # number of names read at
458 self._rbcnamescount = len(self._names) # number of names read at
459 # _rbcsnameslen
459 # _rbcsnameslen
460
460
461 def _clear(self):
461 def _clear(self):
462 self._rbcsnameslen = 0
462 self._rbcsnameslen = 0
463 del self._names[:]
463 del self._names[:]
464 self._rbcnamescount = 0
464 self._rbcnamescount = 0
465 self._rbcrevslen = len(self._repo.changelog)
465 self._rbcrevslen = len(self._repo.changelog)
466 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
466 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
467 util.clearcachedproperty(self, '_namesreverse')
467 util.clearcachedproperty(self, '_namesreverse')
468
468
469 @util.propertycache
469 @util.propertycache
470 def _namesreverse(self):
470 def _namesreverse(self):
471 return dict((b, r) for r, b in enumerate(self._names))
471 return dict((b, r) for r, b in enumerate(self._names))
472
472
473 def branchinfo(self, rev):
473 def branchinfo(self, rev):
474 """Return branch name and close flag for rev, using and updating
474 """Return branch name and close flag for rev, using and updating
475 persistent cache."""
475 persistent cache."""
476 changelog = self._repo.changelog
476 changelog = self._repo.changelog
477 rbcrevidx = rev * _rbcrecsize
477 rbcrevidx = rev * _rbcrecsize
478
478
479 # avoid negative index, changelog.read(nullrev) is fast without cache
479 # avoid negative index, changelog.read(nullrev) is fast without cache
480 if rev == nullrev:
480 if rev == nullrev:
481 return changelog.branchinfo(rev)
481 return changelog.branchinfo(rev)
482
482
483 # if requested rev isn't allocated, grow and cache the rev info
483 # if requested rev isn't allocated, grow and cache the rev info
484 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
484 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
485 return self._branchinfo(rev)
485 return self._branchinfo(rev)
486
486
487 # fast path: extract data from cache, use it if node is matching
487 # fast path: extract data from cache, use it if node is matching
488 reponode = changelog.node(rev)[:_rbcnodelen]
488 reponode = changelog.node(rev)[:_rbcnodelen]
489 cachenode, branchidx = unpack_from(
489 cachenode, branchidx = unpack_from(
490 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx)
490 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx)
491 close = bool(branchidx & _rbccloseflag)
491 close = bool(branchidx & _rbccloseflag)
492 if close:
492 if close:
493 branchidx &= _rbcbranchidxmask
493 branchidx &= _rbcbranchidxmask
494 if cachenode == '\0\0\0\0':
494 if cachenode == '\0\0\0\0':
495 pass
495 pass
496 elif cachenode == reponode:
496 elif cachenode == reponode:
497 try:
497 try:
498 return self._names[branchidx], close
498 return self._names[branchidx], close
499 except IndexError:
499 except IndexError:
500 # recover from invalid reference to unknown branch
500 # recover from invalid reference to unknown branch
501 self._repo.ui.debug("referenced branch names not found"
501 self._repo.ui.debug("referenced branch names not found"
502 " - rebuilding revision branch cache from scratch\n")
502 " - rebuilding revision branch cache from scratch\n")
503 self._clear()
503 self._clear()
504 else:
504 else:
505 # rev/node map has changed, invalidate the cache from here up
505 # rev/node map has changed, invalidate the cache from here up
506 self._repo.ui.debug("history modification detected - truncating "
506 self._repo.ui.debug("history modification detected - truncating "
507 "revision branch cache to revision %d\n" % rev)
507 "revision branch cache to revision %d\n" % rev)
508 truncate = rbcrevidx + _rbcrecsize
508 truncate = rbcrevidx + _rbcrecsize
509 del self._rbcrevs[truncate:]
509 del self._rbcrevs[truncate:]
510 self._rbcrevslen = min(self._rbcrevslen, truncate)
510 self._rbcrevslen = min(self._rbcrevslen, truncate)
511
511
512 # fall back to slow path and make sure it will be written to disk
512 # fall back to slow path and make sure it will be written to disk
513 return self._branchinfo(rev)
513 return self._branchinfo(rev)
514
514
515 def _branchinfo(self, rev):
515 def _branchinfo(self, rev):
516 """Retrieve branch info from changelog and update _rbcrevs"""
516 """Retrieve branch info from changelog and update _rbcrevs"""
517 changelog = self._repo.changelog
517 changelog = self._repo.changelog
518 b, close = changelog.branchinfo(rev)
518 b, close = changelog.branchinfo(rev)
519 if b in self._namesreverse:
519 if b in self._namesreverse:
520 branchidx = self._namesreverse[b]
520 branchidx = self._namesreverse[b]
521 else:
521 else:
522 branchidx = len(self._names)
522 branchidx = len(self._names)
523 self._names.append(b)
523 self._names.append(b)
524 self._namesreverse[b] = branchidx
524 self._namesreverse[b] = branchidx
525 reponode = changelog.node(rev)
525 reponode = changelog.node(rev)
526 if close:
526 if close:
527 branchidx |= _rbccloseflag
527 branchidx |= _rbccloseflag
528 self._setcachedata(rev, reponode, branchidx)
528 self._setcachedata(rev, reponode, branchidx)
529 return b, close
529 return b, close
530
530
531 def setdata(self, branch, rev, node, close):
531 def setdata(self, branch, rev, node, close):
532 """add new data information to the cache"""
532 """add new data information to the cache"""
533 if branch in self._namesreverse:
533 if branch in self._namesreverse:
534 branchidx = self._namesreverse[branch]
534 branchidx = self._namesreverse[branch]
535 else:
535 else:
536 branchidx = len(self._names)
536 branchidx = len(self._names)
537 self._names.append(branch)
537 self._names.append(branch)
538 self._namesreverse[branch] = branchidx
538 self._namesreverse[branch] = branchidx
539 if close:
539 if close:
540 branchidx |= _rbccloseflag
540 branchidx |= _rbccloseflag
541 self._setcachedata(rev, node, branchidx)
541 self._setcachedata(rev, node, branchidx)
542 # If no cache data were readable (non exists, bad permission, etc)
542 # If no cache data were readable (non exists, bad permission, etc)
543 # the cache was bypassing itself by setting:
543 # the cache was bypassing itself by setting:
544 #
544 #
545 # self.branchinfo = self._branchinfo
545 # self.branchinfo = self._branchinfo
546 #
546 #
547 # Since we now have data in the cache, we need to drop this bypassing.
547 # Since we now have data in the cache, we need to drop this bypassing.
548 if r'branchinfo' in vars(self):
548 if r'branchinfo' in vars(self):
549 del self.branchinfo
549 del self.branchinfo
550
550
551 def _setcachedata(self, rev, node, branchidx):
551 def _setcachedata(self, rev, node, branchidx):
552 """Writes the node's branch data to the in-memory cache data."""
552 """Writes the node's branch data to the in-memory cache data."""
553 if rev == nullrev:
553 if rev == nullrev:
554 return
554 return
555 rbcrevidx = rev * _rbcrecsize
555 rbcrevidx = rev * _rbcrecsize
556 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
556 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
557 self._rbcrevs.extend('\0' *
557 self._rbcrevs.extend('\0' *
558 (len(self._repo.changelog) * _rbcrecsize -
558 (len(self._repo.changelog) * _rbcrecsize -
559 len(self._rbcrevs)))
559 len(self._rbcrevs)))
560 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
560 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
561 self._rbcrevslen = min(self._rbcrevslen, rev)
561 self._rbcrevslen = min(self._rbcrevslen, rev)
562
562
563 tr = self._repo.currenttransaction()
563 tr = self._repo.currenttransaction()
564 if tr:
564 if tr:
565 tr.addfinalize('write-revbranchcache', self.write)
565 tr.addfinalize('write-revbranchcache', self.write)
566
566
567 def write(self, tr=None):
567 def write(self, tr=None):
568 """Save branch cache if it is dirty."""
568 """Save branch cache if it is dirty."""
569 repo = self._repo
569 repo = self._repo
570 wlock = None
570 wlock = None
571 step = ''
571 step = ''
572 try:
572 try:
573 if self._rbcnamescount < len(self._names):
573 if self._rbcnamescount < len(self._names):
574 step = ' names'
574 step = ' names'
575 wlock = repo.wlock(wait=False)
575 wlock = repo.wlock(wait=False)
576 if self._rbcnamescount != 0:
576 if self._rbcnamescount != 0:
577 f = repo.cachevfs.open(_rbcnames, 'ab')
577 f = repo.cachevfs.open(_rbcnames, 'ab')
578 if f.tell() == self._rbcsnameslen:
578 if f.tell() == self._rbcsnameslen:
579 f.write('\0')
579 f.write('\0')
580 else:
580 else:
581 f.close()
581 f.close()
582 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
582 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
583 self._rbcnamescount = 0
583 self._rbcnamescount = 0
584 self._rbcrevslen = 0
584 self._rbcrevslen = 0
585 if self._rbcnamescount == 0:
585 if self._rbcnamescount == 0:
586 # before rewriting names, make sure references are removed
586 # before rewriting names, make sure references are removed
587 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
587 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
588 f = repo.cachevfs.open(_rbcnames, 'wb')
588 f = repo.cachevfs.open(_rbcnames, 'wb')
589 f.write('\0'.join(encoding.fromlocal(b)
589 f.write('\0'.join(encoding.fromlocal(b)
590 for b in self._names[self._rbcnamescount:]))
590 for b in self._names[self._rbcnamescount:]))
591 self._rbcsnameslen = f.tell()
591 self._rbcsnameslen = f.tell()
592 f.close()
592 f.close()
593 self._rbcnamescount = len(self._names)
593 self._rbcnamescount = len(self._names)
594
594
595 start = self._rbcrevslen * _rbcrecsize
595 start = self._rbcrevslen * _rbcrecsize
596 if start != len(self._rbcrevs):
596 if start != len(self._rbcrevs):
597 step = ''
597 step = ''
598 if wlock is None:
598 if wlock is None:
599 wlock = repo.wlock(wait=False)
599 wlock = repo.wlock(wait=False)
600 revs = min(len(repo.changelog),
600 revs = min(len(repo.changelog),
601 len(self._rbcrevs) // _rbcrecsize)
601 len(self._rbcrevs) // _rbcrecsize)
602 f = repo.cachevfs.open(_rbcrevs, 'ab')
602 f = repo.cachevfs.open(_rbcrevs, 'ab')
603 if f.tell() != start:
603 if f.tell() != start:
604 repo.ui.debug("truncating cache/%s to %d\n"
604 repo.ui.debug("truncating cache/%s to %d\n"
605 % (_rbcrevs, start))
605 % (_rbcrevs, start))
606 f.seek(start)
606 f.seek(start)
607 if f.tell() != start:
607 if f.tell() != start:
608 start = 0
608 start = 0
609 f.seek(start)
609 f.seek(start)
610 f.truncate()
610 f.truncate()
611 end = revs * _rbcrecsize
611 end = revs * _rbcrecsize
612 f.write(self._rbcrevs[start:end])
612 f.write(self._rbcrevs[start:end])
613 f.close()
613 f.close()
614 self._rbcrevslen = revs
614 self._rbcrevslen = revs
615 except (IOError, OSError, error.Abort, error.LockError) as inst:
615 except (IOError, OSError, error.Abort, error.LockError) as inst:
616 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
616 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
617 % (step, stringutil.forcebytestr(inst)))
617 % (step, stringutil.forcebytestr(inst)))
618 finally:
618 finally:
619 if wlock is not None:
619 if wlock is not None:
620 wlock.release()
620 wlock.release()
General Comments 0
You need to be logged in to leave comments. Login now