##// END OF EJS Templates
branchmap: split a long condition in branchcache.validfor(), add comments...
av6 -
r49568:02e9ad08 default
parent child Browse files
Show More
@@ -1,873 +1,881
1 1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
2 2 #
3 3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import struct
11 11
12 12 from .node import (
13 13 bin,
14 14 hex,
15 15 nullrev,
16 16 )
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 obsolete,
21 21 pycompat,
22 22 scmutil,
23 23 util,
24 24 )
25 25 from .utils import (
26 26 repoviewutil,
27 27 stringutil,
28 28 )
29 29
30 30 if pycompat.TYPE_CHECKING:
31 31 from typing import (
32 32 Any,
33 33 Callable,
34 34 Dict,
35 35 Iterable,
36 36 List,
37 37 Optional,
38 38 Set,
39 39 Tuple,
40 40 Union,
41 41 )
42 42 from . import localrepo
43 43
44 44 assert any(
45 45 (
46 46 Any,
47 47 Callable,
48 48 Dict,
49 49 Iterable,
50 50 List,
51 51 Optional,
52 52 Set,
53 53 Tuple,
54 54 Union,
55 55 localrepo,
56 56 )
57 57 )
58 58
59 59 subsettable = repoviewutil.subsettable
60 60
61 61 calcsize = struct.calcsize
62 62 pack_into = struct.pack_into
63 63 unpack_from = struct.unpack_from
64 64
65 65
66 66 class BranchMapCache(object):
67 67 """mapping of filtered views of repo with their branchcache"""
68 68
69 69 def __init__(self):
70 70 self._per_filter = {}
71 71
72 72 def __getitem__(self, repo):
73 73 self.updatecache(repo)
74 74 return self._per_filter[repo.filtername]
75 75
76 76 def updatecache(self, repo):
77 77 """Update the cache for the given filtered view on a repository"""
78 78 # This can trigger updates for the caches for subsets of the filtered
79 79 # view, e.g. when there is no cache for this filtered view or the cache
80 80 # is stale.
81 81
82 82 cl = repo.changelog
83 83 filtername = repo.filtername
84 84 bcache = self._per_filter.get(filtername)
85 85 if bcache is None or not bcache.validfor(repo):
86 86 # cache object missing or cache object stale? Read from disk
87 87 bcache = branchcache.fromfile(repo)
88 88
89 89 revs = []
90 90 if bcache is None:
91 91 # no (fresh) cache available anymore, perhaps we can re-use
92 92 # the cache for a subset, then extend that to add info on missing
93 93 # revisions.
94 94 subsetname = subsettable.get(filtername)
95 95 if subsetname is not None:
96 96 subset = repo.filtered(subsetname)
97 97 bcache = self[subset].copy()
98 98 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
99 99 revs.extend(r for r in extrarevs if r <= bcache.tiprev)
100 100 else:
101 101 # nothing to fall back on, start empty.
102 102 bcache = branchcache(repo)
103 103
104 104 revs.extend(cl.revs(start=bcache.tiprev + 1))
105 105 if revs:
106 106 bcache.update(repo, revs)
107 107
108 108 assert bcache.validfor(repo), filtername
109 109 self._per_filter[repo.filtername] = bcache
110 110
111 111 def replace(self, repo, remotebranchmap):
112 112 """Replace the branchmap cache for a repo with a branch mapping.
113 113
114 114 This is likely only called during clone with a branch map from a
115 115 remote.
116 116
117 117 """
118 118 cl = repo.changelog
119 119 clrev = cl.rev
120 120 clbranchinfo = cl.branchinfo
121 121 rbheads = []
122 122 closed = set()
123 123 for bheads in pycompat.itervalues(remotebranchmap):
124 124 rbheads += bheads
125 125 for h in bheads:
126 126 r = clrev(h)
127 127 b, c = clbranchinfo(r)
128 128 if c:
129 129 closed.add(h)
130 130
131 131 if rbheads:
132 132 rtiprev = max((int(clrev(node)) for node in rbheads))
133 133 cache = branchcache(
134 134 repo,
135 135 remotebranchmap,
136 136 repo[rtiprev].node(),
137 137 rtiprev,
138 138 closednodes=closed,
139 139 )
140 140
141 141 # Try to stick it as low as possible
142 142 # filter above served are unlikely to be fetch from a clone
143 143 for candidate in (b'base', b'immutable', b'served'):
144 144 rview = repo.filtered(candidate)
145 145 if cache.validfor(rview):
146 146 self._per_filter[candidate] = cache
147 147 cache.write(rview)
148 148 return
149 149
150 150 def clear(self):
151 151 self._per_filter.clear()
152 152
153 153 def write_delayed(self, repo):
154 154 unfi = repo.unfiltered()
155 155 for filtername, cache in self._per_filter.items():
156 156 if cache._delayed:
157 157 repo = unfi.filtered(filtername)
158 158 cache.write(repo)
159 159
160 160
161 161 def _unknownnode(node):
162 162 """raises ValueError when branchcache found a node which does not exists"""
163 163 raise ValueError('node %s does not exist' % pycompat.sysstr(hex(node)))
164 164
165 165
166 166 def _branchcachedesc(repo):
167 167 if repo.filtername is not None:
168 168 return b'branch cache (%s)' % repo.filtername
169 169 else:
170 170 return b'branch cache'
171 171
172 172
173 173 class branchcache(object):
174 174 """A dict like object that hold branches heads cache.
175 175
176 176 This cache is used to avoid costly computations to determine all the
177 177 branch heads of a repo.
178 178
179 179 The cache is serialized on disk in the following format:
180 180
181 181 <tip hex node> <tip rev number> [optional filtered repo hex hash]
182 182 <branch head hex node> <open/closed state> <branch name>
183 183 <branch head hex node> <open/closed state> <branch name>
184 184 ...
185 185
186 186 The first line is used to check if the cache is still valid. If the
187 187 branch cache is for a filtered repo view, an optional third hash is
188 188 included that hashes the hashes of all filtered and obsolete revisions.
189 189
190 190 The open/closed state is represented by a single letter 'o' or 'c'.
191 191 This field can be used to avoid changelog reads when determining if a
192 192 branch head closes a branch or not.
193 193 """
194 194
195 195 def __init__(
196 196 self,
197 197 repo,
198 198 entries=(),
199 199 tipnode=None,
200 200 tiprev=nullrev,
201 201 filteredhash=None,
202 202 closednodes=None,
203 203 hasnode=None,
204 204 ):
205 205 # type: (localrepo.localrepository, Union[Dict[bytes, List[bytes]], Iterable[Tuple[bytes, List[bytes]]]], bytes, int, Optional[bytes], Optional[Set[bytes]], Optional[Callable[[bytes], bool]]) -> None
206 206 """hasnode is a function which can be used to verify whether changelog
207 207 has a given node or not. If it's not provided, we assume that every node
208 208 we have exists in changelog"""
209 209 self._repo = repo
210 210 self._delayed = False
211 211 if tipnode is None:
212 212 self.tipnode = repo.nullid
213 213 else:
214 214 self.tipnode = tipnode
215 215 self.tiprev = tiprev
216 216 self.filteredhash = filteredhash
217 217 # closednodes is a set of nodes that close their branch. If the branch
218 218 # cache has been updated, it may contain nodes that are no longer
219 219 # heads.
220 220 if closednodes is None:
221 221 self._closednodes = set()
222 222 else:
223 223 self._closednodes = closednodes
224 224 self._entries = dict(entries)
225 225 # whether closed nodes are verified or not
226 226 self._closedverified = False
227 227 # branches for which nodes are verified
228 228 self._verifiedbranches = set()
229 229 self._hasnode = hasnode
230 230 if self._hasnode is None:
231 231 self._hasnode = lambda x: True
232 232
233 233 def _verifyclosed(self):
234 234 """verify the closed nodes we have"""
235 235 if self._closedverified:
236 236 return
237 237 for node in self._closednodes:
238 238 if not self._hasnode(node):
239 239 _unknownnode(node)
240 240
241 241 self._closedverified = True
242 242
243 243 def _verifybranch(self, branch):
244 244 """verify head nodes for the given branch."""
245 245 if branch not in self._entries or branch in self._verifiedbranches:
246 246 return
247 247 for n in self._entries[branch]:
248 248 if not self._hasnode(n):
249 249 _unknownnode(n)
250 250
251 251 self._verifiedbranches.add(branch)
252 252
253 253 def _verifyall(self):
254 254 """verifies nodes of all the branches"""
255 255 needverification = set(self._entries.keys()) - self._verifiedbranches
256 256 for b in needverification:
257 257 self._verifybranch(b)
258 258
259 259 def __iter__(self):
260 260 return iter(self._entries)
261 261
262 262 def __setitem__(self, key, value):
263 263 self._entries[key] = value
264 264
265 265 def __getitem__(self, key):
266 266 self._verifybranch(key)
267 267 return self._entries[key]
268 268
269 269 def __contains__(self, key):
270 270 self._verifybranch(key)
271 271 return key in self._entries
272 272
273 273 def iteritems(self):
274 274 for k, v in pycompat.iteritems(self._entries):
275 275 self._verifybranch(k)
276 276 yield k, v
277 277
278 278 items = iteritems
279 279
280 280 def hasbranch(self, label):
281 281 """checks whether a branch of this name exists or not"""
282 282 self._verifybranch(label)
283 283 return label in self._entries
284 284
285 285 @classmethod
286 286 def fromfile(cls, repo):
287 287 f = None
288 288 try:
289 289 f = repo.cachevfs(cls._filename(repo))
290 290 lineiter = iter(f)
291 291 cachekey = next(lineiter).rstrip(b'\n').split(b" ", 2)
292 292 last, lrev = cachekey[:2]
293 293 last, lrev = bin(last), int(lrev)
294 294 filteredhash = None
295 295 hasnode = repo.changelog.hasnode
296 296 if len(cachekey) > 2:
297 297 filteredhash = bin(cachekey[2])
298 298 bcache = cls(
299 299 repo,
300 300 tipnode=last,
301 301 tiprev=lrev,
302 302 filteredhash=filteredhash,
303 303 hasnode=hasnode,
304 304 )
305 305 if not bcache.validfor(repo):
306 306 # invalidate the cache
307 307 raise ValueError('tip differs')
308 308 bcache.load(repo, lineiter)
309 309 except (IOError, OSError):
310 310 return None
311 311
312 312 except Exception as inst:
313 313 if repo.ui.debugflag:
314 314 msg = b'invalid %s: %s\n'
315 315 repo.ui.debug(
316 316 msg
317 317 % (
318 318 _branchcachedesc(repo),
319 319 stringutil.forcebytestr(inst),
320 320 )
321 321 )
322 322 bcache = None
323 323
324 324 finally:
325 325 if f:
326 326 f.close()
327 327
328 328 return bcache
329 329
330 330 def load(self, repo, lineiter):
331 331 """fully loads the branchcache by reading from the file using the line
332 332 iterator passed"""
333 333 for line in lineiter:
334 334 line = line.rstrip(b'\n')
335 335 if not line:
336 336 continue
337 337 node, state, label = line.split(b" ", 2)
338 338 if state not in b'oc':
339 339 raise ValueError('invalid branch state')
340 340 label = encoding.tolocal(label.strip())
341 341 node = bin(node)
342 342 self._entries.setdefault(label, []).append(node)
343 343 if state == b'c':
344 344 self._closednodes.add(node)
345 345
346 346 @staticmethod
347 347 def _filename(repo):
348 348 """name of a branchcache file for a given repo or repoview"""
349 349 filename = b"branch2"
350 350 if repo.filtername:
351 351 filename = b'%s-%s' % (filename, repo.filtername)
352 352 return filename
353 353
354 354 def validfor(self, repo):
355 """Is the cache content valid regarding a repo
355 """check that cache contents are valid for (a subset of) this repo
356 356
357 - False when cached tipnode is unknown or if we detect a strip.
358 - True when cache is up to date or a subset of current repo."""
357 - False when the order of changesets changed or if we detect a strip.
358 - True when cache is up-to-date for the current repo or its subset."""
359 359 try:
360 return (self.tipnode == repo.changelog.node(self.tiprev)) and (
361 self.filteredhash
362 == scmutil.filteredhash(repo, self.tiprev, needobsolete=True)
363 )
360 node = repo.changelog.node(self.tiprev)
364 361 except IndexError:
362 # changesets were stripped and now we don't even have enough to
363 # find tiprev
365 364 return False
365 if self.tipnode != node:
366 # tiprev doesn't correspond to tipnode: repo was stripped, or this
367 # repo has a different order of changesets
368 return False
369 tiphash = scmutil.filteredhash(repo, self.tiprev, needobsolete=True)
370 # hashes don't match if this repo view has a different set of filtered
371 # revisions (e.g. due to phase changes) or obsolete revisions (e.g.
372 # history was rewritten)
373 return self.filteredhash == tiphash
366 374
367 375 def _branchtip(self, heads):
368 376 """Return tuple with last open head in heads and false,
369 377 otherwise return last closed head and true."""
370 378 tip = heads[-1]
371 379 closed = True
372 380 for h in reversed(heads):
373 381 if h not in self._closednodes:
374 382 tip = h
375 383 closed = False
376 384 break
377 385 return tip, closed
378 386
379 387 def branchtip(self, branch):
380 388 """Return the tipmost open head on branch head, otherwise return the
381 389 tipmost closed head on branch.
382 390 Raise KeyError for unknown branch."""
383 391 return self._branchtip(self[branch])[0]
384 392
385 393 def iteropen(self, nodes):
386 394 return (n for n in nodes if n not in self._closednodes)
387 395
388 396 def branchheads(self, branch, closed=False):
389 397 self._verifybranch(branch)
390 398 heads = self._entries[branch]
391 399 if not closed:
392 400 heads = list(self.iteropen(heads))
393 401 return heads
394 402
395 403 def iterbranches(self):
396 404 for bn, heads in pycompat.iteritems(self):
397 405 yield (bn, heads) + self._branchtip(heads)
398 406
399 407 def iterheads(self):
400 408 """returns all the heads"""
401 409 self._verifyall()
402 410 return pycompat.itervalues(self._entries)
403 411
404 412 def copy(self):
405 413 """return an deep copy of the branchcache object"""
406 414 return type(self)(
407 415 self._repo,
408 416 self._entries,
409 417 self.tipnode,
410 418 self.tiprev,
411 419 self.filteredhash,
412 420 self._closednodes,
413 421 )
414 422
415 423 def write(self, repo):
416 424 tr = repo.currenttransaction()
417 425 if not getattr(tr, 'finalized', True):
418 426 # Avoid premature writing.
419 427 #
420 428 # (The cache warming setup by localrepo will update the file later.)
421 429 self._delayed = True
422 430 return
423 431 try:
424 432 f = repo.cachevfs(self._filename(repo), b"w", atomictemp=True)
425 433 cachekey = [hex(self.tipnode), b'%d' % self.tiprev]
426 434 if self.filteredhash is not None:
427 435 cachekey.append(hex(self.filteredhash))
428 436 f.write(b" ".join(cachekey) + b'\n')
429 437 nodecount = 0
430 438 for label, nodes in sorted(pycompat.iteritems(self._entries)):
431 439 label = encoding.fromlocal(label)
432 440 for node in nodes:
433 441 nodecount += 1
434 442 if node in self._closednodes:
435 443 state = b'c'
436 444 else:
437 445 state = b'o'
438 446 f.write(b"%s %s %s\n" % (hex(node), state, label))
439 447 f.close()
440 448 repo.ui.log(
441 449 b'branchcache',
442 450 b'wrote %s with %d labels and %d nodes\n',
443 451 _branchcachedesc(repo),
444 452 len(self._entries),
445 453 nodecount,
446 454 )
447 455 self._delayed = False
448 456 except (IOError, OSError, error.Abort) as inst:
449 457 # Abort may be raised by read only opener, so log and continue
450 458 repo.ui.debug(
451 459 b"couldn't write branch cache: %s\n"
452 460 % stringutil.forcebytestr(inst)
453 461 )
454 462
455 463 def update(self, repo, revgen):
456 464 """Given a branchhead cache, self, that may have extra nodes or be
457 465 missing heads, and a generator of nodes that are strictly a superset of
458 466 heads missing, this function updates self to be correct.
459 467 """
460 468 starttime = util.timer()
461 469 cl = repo.changelog
462 470 # collect new branch entries
463 471 newbranches = {}
464 472 getbranchinfo = repo.revbranchcache().branchinfo
465 473 for r in revgen:
466 474 branch, closesbranch = getbranchinfo(r)
467 475 newbranches.setdefault(branch, []).append(r)
468 476 if closesbranch:
469 477 self._closednodes.add(cl.node(r))
470 478
471 479 # new tip revision which we found after iterating items from new
472 480 # branches
473 481 ntiprev = self.tiprev
474 482
475 483 # Delay fetching the topological heads until they are needed.
476 484 # A repository without non-continous branches can skip this part.
477 485 topoheads = None
478 486
479 487 # If a changeset is visible, its parents must be visible too, so
480 488 # use the faster unfiltered parent accessor.
481 489 parentrevs = repo.unfiltered().changelog.parentrevs
482 490
483 491 # Faster than using ctx.obsolete()
484 492 obsrevs = obsolete.getrevs(repo, b'obsolete')
485 493
486 494 for branch, newheadrevs in pycompat.iteritems(newbranches):
487 495 # For every branch, compute the new branchheads.
488 496 # A branchhead is a revision such that no descendant is on
489 497 # the same branch.
490 498 #
491 499 # The branchheads are computed iteratively in revision order.
492 500 # This ensures topological order, i.e. parents are processed
493 501 # before their children. Ancestors are inclusive here, i.e.
494 502 # any revision is an ancestor of itself.
495 503 #
496 504 # Core observations:
497 505 # - The current revision is always a branchhead for the
498 506 # repository up to that point.
499 507 # - It is the first revision of the branch if and only if
500 508 # there was no branchhead before. In that case, it is the
501 509 # only branchhead as there are no possible ancestors on
502 510 # the same branch.
503 511 # - If a parent is on the same branch, a branchhead can
504 512 # only be an ancestor of that parent, if it is parent
505 513 # itself. Otherwise it would have been removed as ancestor
506 514 # of that parent before.
507 515 # - Therefore, if all parents are on the same branch, they
508 516 # can just be removed from the branchhead set.
509 517 # - If one parent is on the same branch and the other is not
510 518 # and there was exactly one branchhead known, the existing
511 519 # branchhead can only be an ancestor if it is the parent.
512 520 # Otherwise it would have been removed as ancestor of
513 521 # the parent before. The other parent therefore can't have
514 522 # a branchhead as ancestor.
515 523 # - In all other cases, the parents on different branches
516 524 # could have a branchhead as ancestor. Those parents are
517 525 # kept in the "uncertain" set. If all branchheads are also
518 526 # topological heads, they can't have descendants and further
519 527 # checks can be skipped. Otherwise, the ancestors of the
520 528 # "uncertain" set are removed from branchheads.
521 529 # This computation is heavy and avoided if at all possible.
522 530 bheads = self._entries.get(branch, [])
523 531 bheadset = {cl.rev(node) for node in bheads}
524 532 uncertain = set()
525 533 for newrev in sorted(newheadrevs):
526 534 if newrev in obsrevs:
527 535 # We ignore obsolete changesets as they shouldn't be
528 536 # considered heads.
529 537 continue
530 538
531 539 if not bheadset:
532 540 bheadset.add(newrev)
533 541 continue
534 542
535 543 parents = [p for p in parentrevs(newrev) if p != nullrev]
536 544 samebranch = set()
537 545 otherbranch = set()
538 546 obsparents = set()
539 547 for p in parents:
540 548 if p in obsrevs:
541 549 # We ignored this obsolete changeset earlier, but now
542 550 # that it has non-ignored children, we need to make
543 551 # sure their ancestors are not considered heads. To
544 552 # achieve that, we will simply treat this obsolete
545 553 # changeset as a parent from other branch.
546 554 obsparents.add(p)
547 555 elif p in bheadset or getbranchinfo(p)[0] == branch:
548 556 samebranch.add(p)
549 557 else:
550 558 otherbranch.add(p)
551 559 if not (len(bheadset) == len(samebranch) == 1):
552 560 uncertain.update(otherbranch)
553 561 uncertain.update(obsparents)
554 562 bheadset.difference_update(samebranch)
555 563 bheadset.add(newrev)
556 564
557 565 if uncertain:
558 566 if topoheads is None:
559 567 topoheads = set(cl.headrevs())
560 568 if bheadset - topoheads:
561 569 floorrev = min(bheadset)
562 570 if floorrev <= max(uncertain):
563 571 ancestors = set(cl.ancestors(uncertain, floorrev))
564 572 bheadset -= ancestors
565 573 if bheadset:
566 574 self[branch] = [cl.node(rev) for rev in sorted(bheadset)]
567 575 tiprev = max(newheadrevs)
568 576 if tiprev > ntiprev:
569 577 ntiprev = tiprev
570 578
571 579 if ntiprev > self.tiprev:
572 580 self.tiprev = ntiprev
573 581 self.tipnode = cl.node(ntiprev)
574 582
575 583 if not self.validfor(repo):
576 584 # old cache key is now invalid for the repo, but we've just updated
577 585 # the cache and we assume it's valid, so let's make the cache key
578 586 # valid as well by recomputing it from the cached data
579 587 self.tipnode = repo.nullid
580 588 self.tiprev = nullrev
581 589 for heads in self.iterheads():
582 590 if not heads:
583 591 # all revisions on a branch are obsolete
584 592 continue
585 593 # note: tiprev is not necessarily the tip revision of repo,
586 594 # because the tip could be obsolete (i.e. not a head)
587 595 tiprev = max(cl.rev(node) for node in heads)
588 596 if tiprev > self.tiprev:
589 597 self.tipnode = cl.node(tiprev)
590 598 self.tiprev = tiprev
591 599 self.filteredhash = scmutil.filteredhash(
592 600 repo, self.tiprev, needobsolete=True
593 601 )
594 602
595 603 duration = util.timer() - starttime
596 604 repo.ui.log(
597 605 b'branchcache',
598 606 b'updated %s in %.4f seconds\n',
599 607 _branchcachedesc(repo),
600 608 duration,
601 609 )
602 610
603 611 self.write(repo)
604 612
605 613
606 614 class remotebranchcache(branchcache):
607 615 """Branchmap info for a remote connection, should not write locally"""
608 616
609 617 def write(self, repo):
610 618 pass
611 619
612 620
613 621 # Revision branch info cache
614 622
615 623 _rbcversion = b'-v1'
616 624 _rbcnames = b'rbc-names' + _rbcversion
617 625 _rbcrevs = b'rbc-revs' + _rbcversion
618 626 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
619 627 _rbcrecfmt = b'>4sI'
620 628 _rbcrecsize = calcsize(_rbcrecfmt)
621 629 _rbcmininc = 64 * _rbcrecsize
622 630 _rbcnodelen = 4
623 631 _rbcbranchidxmask = 0x7FFFFFFF
624 632 _rbccloseflag = 0x80000000
625 633
626 634
627 635 class revbranchcache(object):
628 636 """Persistent cache, mapping from revision number to branch name and close.
629 637 This is a low level cache, independent of filtering.
630 638
631 639 Branch names are stored in rbc-names in internal encoding separated by 0.
632 640 rbc-names is append-only, and each branch name is only stored once and will
633 641 thus have a unique index.
634 642
635 643 The branch info for each revision is stored in rbc-revs as constant size
636 644 records. The whole file is read into memory, but it is only 'parsed' on
637 645 demand. The file is usually append-only but will be truncated if repo
638 646 modification is detected.
639 647 The record for each revision contains the first 4 bytes of the
640 648 corresponding node hash, and the record is only used if it still matches.
641 649 Even a completely trashed rbc-revs fill thus still give the right result
642 650 while converging towards full recovery ... assuming no incorrectly matching
643 651 node hashes.
644 652 The record also contains 4 bytes where 31 bits contains the index of the
645 653 branch and the last bit indicate that it is a branch close commit.
646 654 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
647 655 and will grow with it but be 1/8th of its size.
648 656 """
649 657
650 658 def __init__(self, repo, readonly=True):
651 659 assert repo.filtername is None
652 660 self._repo = repo
653 661 self._names = [] # branch names in local encoding with static index
654 662 self._rbcrevs = bytearray()
655 663 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
656 664 try:
657 665 bndata = repo.cachevfs.read(_rbcnames)
658 666 self._rbcsnameslen = len(bndata) # for verification before writing
659 667 if bndata:
660 668 self._names = [
661 669 encoding.tolocal(bn) for bn in bndata.split(b'\0')
662 670 ]
663 671 except (IOError, OSError):
664 672 if readonly:
665 673 # don't try to use cache - fall back to the slow path
666 674 self.branchinfo = self._branchinfo
667 675
668 676 if self._names:
669 677 try:
670 678 data = repo.cachevfs.read(_rbcrevs)
671 679 self._rbcrevs[:] = data
672 680 except (IOError, OSError) as inst:
673 681 repo.ui.debug(
674 682 b"couldn't read revision branch cache: %s\n"
675 683 % stringutil.forcebytestr(inst)
676 684 )
677 685 # remember number of good records on disk
678 686 self._rbcrevslen = min(
679 687 len(self._rbcrevs) // _rbcrecsize, len(repo.changelog)
680 688 )
681 689 if self._rbcrevslen == 0:
682 690 self._names = []
683 691 self._rbcnamescount = len(self._names) # number of names read at
684 692 # _rbcsnameslen
685 693
686 694 def _clear(self):
687 695 self._rbcsnameslen = 0
688 696 del self._names[:]
689 697 self._rbcnamescount = 0
690 698 self._rbcrevslen = len(self._repo.changelog)
691 699 self._rbcrevs = bytearray(self._rbcrevslen * _rbcrecsize)
692 700 util.clearcachedproperty(self, b'_namesreverse')
693 701
694 702 @util.propertycache
695 703 def _namesreverse(self):
696 704 return {b: r for r, b in enumerate(self._names)}
697 705
698 706 def branchinfo(self, rev):
699 707 """Return branch name and close flag for rev, using and updating
700 708 persistent cache."""
701 709 changelog = self._repo.changelog
702 710 rbcrevidx = rev * _rbcrecsize
703 711
704 712 # avoid negative index, changelog.read(nullrev) is fast without cache
705 713 if rev == nullrev:
706 714 return changelog.branchinfo(rev)
707 715
708 716 # if requested rev isn't allocated, grow and cache the rev info
709 717 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
710 718 return self._branchinfo(rev)
711 719
712 720 # fast path: extract data from cache, use it if node is matching
713 721 reponode = changelog.node(rev)[:_rbcnodelen]
714 722 cachenode, branchidx = unpack_from(
715 723 _rbcrecfmt, util.buffer(self._rbcrevs), rbcrevidx
716 724 )
717 725 close = bool(branchidx & _rbccloseflag)
718 726 if close:
719 727 branchidx &= _rbcbranchidxmask
720 728 if cachenode == b'\0\0\0\0':
721 729 pass
722 730 elif cachenode == reponode:
723 731 try:
724 732 return self._names[branchidx], close
725 733 except IndexError:
726 734 # recover from invalid reference to unknown branch
727 735 self._repo.ui.debug(
728 736 b"referenced branch names not found"
729 737 b" - rebuilding revision branch cache from scratch\n"
730 738 )
731 739 self._clear()
732 740 else:
733 741 # rev/node map has changed, invalidate the cache from here up
734 742 self._repo.ui.debug(
735 743 b"history modification detected - truncating "
736 744 b"revision branch cache to revision %d\n" % rev
737 745 )
738 746 truncate = rbcrevidx + _rbcrecsize
739 747 del self._rbcrevs[truncate:]
740 748 self._rbcrevslen = min(self._rbcrevslen, truncate)
741 749
742 750 # fall back to slow path and make sure it will be written to disk
743 751 return self._branchinfo(rev)
744 752
745 753 def _branchinfo(self, rev):
746 754 """Retrieve branch info from changelog and update _rbcrevs"""
747 755 changelog = self._repo.changelog
748 756 b, close = changelog.branchinfo(rev)
749 757 if b in self._namesreverse:
750 758 branchidx = self._namesreverse[b]
751 759 else:
752 760 branchidx = len(self._names)
753 761 self._names.append(b)
754 762 self._namesreverse[b] = branchidx
755 763 reponode = changelog.node(rev)
756 764 if close:
757 765 branchidx |= _rbccloseflag
758 766 self._setcachedata(rev, reponode, branchidx)
759 767 return b, close
760 768
761 769 def setdata(self, rev, changelogrevision):
762 770 """add new data information to the cache"""
763 771 branch, close = changelogrevision.branchinfo
764 772
765 773 if branch in self._namesreverse:
766 774 branchidx = self._namesreverse[branch]
767 775 else:
768 776 branchidx = len(self._names)
769 777 self._names.append(branch)
770 778 self._namesreverse[branch] = branchidx
771 779 if close:
772 780 branchidx |= _rbccloseflag
773 781 self._setcachedata(rev, self._repo.changelog.node(rev), branchidx)
774 782 # If no cache data were readable (non exists, bad permission, etc)
775 783 # the cache was bypassing itself by setting:
776 784 #
777 785 # self.branchinfo = self._branchinfo
778 786 #
779 787 # Since we now have data in the cache, we need to drop this bypassing.
780 788 if 'branchinfo' in vars(self):
781 789 del self.branchinfo
782 790
783 791 def _setcachedata(self, rev, node, branchidx):
784 792 """Writes the node's branch data to the in-memory cache data."""
785 793 if rev == nullrev:
786 794 return
787 795 rbcrevidx = rev * _rbcrecsize
788 796 requiredsize = rbcrevidx + _rbcrecsize
789 797 rbccur = len(self._rbcrevs)
790 798 if rbccur < requiredsize:
791 799 # bytearray doesn't allocate extra space at least in Python 3.7.
792 800 # When multiple changesets are added in a row, precise resize would
793 801 # result in quadratic complexity. Overallocate to compensate by
794 802 # use the classic doubling technique for dynamic arrays instead.
795 803 # If there was a gap in the map before, less space will be reserved.
796 804 self._rbcrevs.extend(b'\0' * max(_rbcmininc, requiredsize))
797 805 pack_into(_rbcrecfmt, self._rbcrevs, rbcrevidx, node, branchidx)
798 806 self._rbcrevslen = min(self._rbcrevslen, rev)
799 807
800 808 tr = self._repo.currenttransaction()
801 809 if tr:
802 810 tr.addfinalize(b'write-revbranchcache', self.write)
803 811
804 812 def write(self, tr=None):
805 813 """Save branch cache if it is dirty."""
806 814 repo = self._repo
807 815 wlock = None
808 816 step = b''
809 817 try:
810 818 # write the new names
811 819 if self._rbcnamescount < len(self._names):
812 820 wlock = repo.wlock(wait=False)
813 821 step = b' names'
814 822 self._writenames(repo)
815 823
816 824 # write the new revs
817 825 start = self._rbcrevslen * _rbcrecsize
818 826 if start != len(self._rbcrevs):
819 827 step = b''
820 828 if wlock is None:
821 829 wlock = repo.wlock(wait=False)
822 830 self._writerevs(repo, start)
823 831
824 832 except (IOError, OSError, error.Abort, error.LockError) as inst:
825 833 repo.ui.debug(
826 834 b"couldn't write revision branch cache%s: %s\n"
827 835 % (step, stringutil.forcebytestr(inst))
828 836 )
829 837 finally:
830 838 if wlock is not None:
831 839 wlock.release()
832 840
833 841 def _writenames(self, repo):
834 842 """write the new branch names to revbranchcache"""
835 843 if self._rbcnamescount != 0:
836 844 f = repo.cachevfs.open(_rbcnames, b'ab')
837 845 if f.tell() == self._rbcsnameslen:
838 846 f.write(b'\0')
839 847 else:
840 848 f.close()
841 849 repo.ui.debug(b"%s changed - rewriting it\n" % _rbcnames)
842 850 self._rbcnamescount = 0
843 851 self._rbcrevslen = 0
844 852 if self._rbcnamescount == 0:
845 853 # before rewriting names, make sure references are removed
846 854 repo.cachevfs.unlinkpath(_rbcrevs, ignoremissing=True)
847 855 f = repo.cachevfs.open(_rbcnames, b'wb')
848 856 f.write(
849 857 b'\0'.join(
850 858 encoding.fromlocal(b)
851 859 for b in self._names[self._rbcnamescount :]
852 860 )
853 861 )
854 862 self._rbcsnameslen = f.tell()
855 863 f.close()
856 864 self._rbcnamescount = len(self._names)
857 865
858 866 def _writerevs(self, repo, start):
859 867 """write the new revs to revbranchcache"""
860 868 revs = min(len(repo.changelog), len(self._rbcrevs) // _rbcrecsize)
861 869 with repo.cachevfs.open(_rbcrevs, b'ab') as f:
862 870 if f.tell() != start:
863 871 repo.ui.debug(
864 872 b"truncating cache/%s to %d\n" % (_rbcrevs, start)
865 873 )
866 874 f.seek(start)
867 875 if f.tell() != start:
868 876 start = 0
869 877 f.seek(start)
870 878 f.truncate()
871 879 end = revs * _rbcrecsize
872 880 f.write(self._rbcrevs[start:end])
873 881 self._rbcrevslen = revs
General Comments 0
You need to be logged in to leave comments. Login now