comparison mercurial/branchmap.py @ 51536:718f28ea3af4

branchcache: add a "pure topological head" fast path In a narrow but actually quick common case, all topological heads are all on the same branch and all open. In this case, computing the branch map is very simple. We can quickly detect situation where this situation will not change. So we update the V3 format to be able to express this situation and upgrade the update code to detect we remains in that mode. The branch cache is populated with the actual value when the branch map is accessed, but the update_disk method can do the update without needing to populate it.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Thu, 07 Mar 2024 04:15:23 +0100
parents 03247e37ccf7
children 4a8bb136ee77
comparison
equal deleted inserted replaced
51535:03247e37ccf7 51536:718f28ea3af4
60 self._per_filter = {} 60 self._per_filter = {}
61 61
62 def __getitem__(self, repo): 62 def __getitem__(self, repo):
63 self.updatecache(repo) 63 self.updatecache(repo)
64 bcache = self._per_filter[repo.filtername] 64 bcache = self._per_filter[repo.filtername]
65 bcache._ensure_populated(repo)
65 assert bcache._filtername == repo.filtername, ( 66 assert bcache._filtername == repo.filtername, (
66 bcache._filtername, 67 bcache._filtername,
67 repo.filtername, 68 repo.filtername,
68 ) 69 )
69 return bcache 70 return bcache
482 self._hasnode = repo.changelog.hasnode 483 self._hasnode = repo.changelog.hasnode
483 484
484 def _compute_key_hashes(self, repo) -> Tuple[bytes]: 485 def _compute_key_hashes(self, repo) -> Tuple[bytes]:
485 raise NotImplementedError 486 raise NotImplementedError
486 487
488 def _ensure_populated(self, repo):
489 """make sure any lazily loaded values are fully populated"""
490
487 def validfor(self, repo): 491 def validfor(self, repo):
488 """check that cache contents are valid for (a subset of) this repo 492 """check that cache contents are valid for (a subset of) this repo
489 493
490 - False when the order of changesets changed or if we detect a strip. 494 - False when the order of changesets changed or if we detect a strip.
491 - True when cache is up-to-date for the current repo or its subset.""" 495 - True when cache is up-to-date for the current repo or its subset."""
859 """ 863 """
860 864
861 _base_filename = b"branch3" 865 _base_filename = b"branch3"
862 _default_key_hashes = (None, None) 866 _default_key_hashes = (None, None)
863 867
864 def _get_topo_heads(self, repo) -> List[int]: 868 def __init__(self, *args, pure_topo_branch=None, **kwargs):
869 super().__init__(*args, **kwargs)
870 self._pure_topo_branch = pure_topo_branch
871 self._needs_populate = self._pure_topo_branch is not None
872
873 def inherit_for(self, repo):
874 new = super().inherit_for(repo)
875 new._pure_topo_branch = self._pure_topo_branch
876 new._needs_populate = self._needs_populate
877 return new
878
879 def _get_topo_heads(self, repo):
865 """returns the topological head of a repoview content up to self.tiprev""" 880 """returns the topological head of a repoview content up to self.tiprev"""
866 cl = repo.changelog 881 cl = repo.changelog
867 if self.tiprev == nullrev: 882 if self.tiprev == nullrev:
868 return [] 883 return []
869 elif self.tiprev == cl.tiprev(): 884 elif self.tiprev == cl.tiprev():
881 if self.key_hashes: 896 if self.key_hashes:
882 if self.key_hashes[0] is not None: 897 if self.key_hashes[0] is not None:
883 cache_keys[b"filtered-hash"] = hex(self.key_hashes[0]) 898 cache_keys[b"filtered-hash"] = hex(self.key_hashes[0])
884 if self.key_hashes[1] is not None: 899 if self.key_hashes[1] is not None:
885 cache_keys[b"obsolete-hash"] = hex(self.key_hashes[1]) 900 cache_keys[b"obsolete-hash"] = hex(self.key_hashes[1])
901 if self._pure_topo_branch is not None:
902 cache_keys[b"topo-mode"] = b"pure"
886 pieces = (b"%s=%s" % i for i in sorted(cache_keys.items())) 903 pieces = (b"%s=%s" % i for i in sorted(cache_keys.items()))
887 fp.write(b" ".join(pieces) + b'\n') 904 fp.write(b" ".join(pieces) + b'\n')
905 if self._pure_topo_branch is not None:
906 label = encoding.fromlocal(self._pure_topo_branch)
907 fp.write(label + b'\n')
888 908
889 def _write_heads(self, repo, fp) -> int: 909 def _write_heads(self, repo, fp) -> int:
890 """write list of heads to a file 910 """write list of heads to a file
891 911
892 Return the number of heads written.""" 912 Return the number of heads written."""
893 nodecount = 0 913 nodecount = 0
894 topo_heads = set(self._get_topo_heads(repo)) 914 topo_heads = None
915 if self._pure_topo_branch is None:
916 topo_heads = set(self._get_topo_heads(repo))
895 to_rev = repo.changelog.index.rev 917 to_rev = repo.changelog.index.rev
896 for label, nodes in sorted(self._entries.items()): 918 for label, nodes in sorted(self._entries.items()):
919 if label == self._pure_topo_branch:
920 # not need to write anything the header took care of that
921 continue
897 label = encoding.fromlocal(label) 922 label = encoding.fromlocal(label)
898 for node in nodes: 923 for node in nodes:
899 rev = to_rev(node) 924 if topo_heads is not None:
900 if rev in topo_heads: 925 rev = to_rev(node)
901 continue 926 if rev in topo_heads:
927 continue
902 if node in self._closednodes: 928 if node in self._closednodes:
903 state = b'c' 929 state = b'c'
904 else: 930 else:
905 state = b'o' 931 state = b'o'
906 nodecount += 1 932 nodecount += 1
914 cache_keys = dict(p.split(b'=', 1) for p in pieces) 940 cache_keys = dict(p.split(b'=', 1) for p in pieces)
915 941
916 args = {} 942 args = {}
917 filtered_hash = None 943 filtered_hash = None
918 obsolete_hash = None 944 obsolete_hash = None
945 has_pure_topo_heads = False
919 for k, v in cache_keys.items(): 946 for k, v in cache_keys.items():
920 if k == b"tip-rev": 947 if k == b"tip-rev":
921 args["tiprev"] = int(v) 948 args["tiprev"] = int(v)
922 elif k == b"tip-node": 949 elif k == b"tip-node":
923 args["tipnode"] = bin(v) 950 args["tipnode"] = bin(v)
924 elif k == b"filtered-hash": 951 elif k == b"filtered-hash":
925 filtered_hash = bin(v) 952 filtered_hash = bin(v)
926 elif k == b"obsolete-hash": 953 elif k == b"obsolete-hash":
927 obsolete_hash = bin(v) 954 obsolete_hash = bin(v)
955 elif k == b"topo-mode":
956 if v == b"pure":
957 has_pure_topo_heads = True
958 else:
959 msg = b"unknown topo-mode: %r" % v
960 raise ValueError(msg)
928 else: 961 else:
929 msg = b"unknown cache key: %r" % k 962 msg = b"unknown cache key: %r" % k
930 raise ValueError(msg) 963 raise ValueError(msg)
931 args["key_hashes"] = (filtered_hash, obsolete_hash) 964 args["key_hashes"] = (filtered_hash, obsolete_hash)
965 if has_pure_topo_heads:
966 pure_line = next(lineiter).rstrip(b'\n')
967 args["pure_topo_branch"] = encoding.tolocal(pure_line)
932 return args 968 return args
933 969
934 def _load_heads(self, repo, lineiter): 970 def _load_heads(self, repo, lineiter):
935 """fully loads the branchcache by reading from the file using the line 971 """fully loads the branchcache by reading from the file using the line
936 iterator passed""" 972 iterator passed"""
937 super()._load_heads(repo, lineiter) 973 super()._load_heads(repo, lineiter)
974 if self._pure_topo_branch is not None:
975 # no need to read the repository heads, we know their value already.
976 return
938 cl = repo.changelog 977 cl = repo.changelog
939 getbranchinfo = repo.revbranchcache().branchinfo 978 getbranchinfo = repo.revbranchcache().branchinfo
940 obsrevs = obsolete.getrevs(repo, b'obsolete') 979 obsrevs = obsolete.getrevs(repo, b'obsolete')
941 to_node = cl.node 980 to_node = cl.node
942 touched_branch = set() 981 touched_branch = set()
957 """return the cache key hashes that match this repoview state""" 996 """return the cache key hashes that match this repoview state"""
958 return scmutil.filtered_and_obsolete_hash( 997 return scmutil.filtered_and_obsolete_hash(
959 repo, 998 repo,
960 self.tiprev, 999 self.tiprev,
961 ) 1000 )
1001
1002 def _process_new(
1003 self,
1004 repo,
1005 newbranches,
1006 new_closed,
1007 obs_ignored,
1008 max_rev,
1009 ) -> None:
1010 if (
1011 # note: the check about `obs_ignored` is too strict as the
1012 # obsolete revision could be non-topological, but lets keep
1013 # things simple for now
1014 #
1015 # The same apply to `new_closed` if the closed changeset are
1016 # not a head, we don't care that it is closed, but lets keep
1017 # things simple here too.
1018 not (obs_ignored or new_closed)
1019 and (
1020 not newbranches
1021 or (
1022 len(newbranches) == 1
1023 and (
1024 self.tiprev == nullrev
1025 or self._pure_topo_branch in newbranches
1026 )
1027 )
1028 )
1029 ):
1030 if newbranches:
1031 assert len(newbranches) == 1
1032 self._pure_topo_branch = list(newbranches.keys())[0]
1033 self._needs_populate = True
1034 self._entries.pop(self._pure_topo_branch, None)
1035 return
1036
1037 self._ensure_populated(repo)
1038 self._pure_topo_branch = None
1039 super()._process_new(
1040 repo,
1041 newbranches,
1042 new_closed,
1043 obs_ignored,
1044 max_rev,
1045 )
1046
1047 def _ensure_populated(self, repo):
1048 """make sure any lazily loaded values are fully populated"""
1049 if self._needs_populate:
1050 assert self._pure_topo_branch is not None
1051 cl = repo.changelog
1052 to_node = cl.node
1053 topo_heads = self._get_topo_heads(repo)
1054 heads = [to_node(r) for r in topo_heads]
1055 self._entries[self._pure_topo_branch] = heads
1056 self._needs_populate = False
962 1057
963 1058
964 class remotebranchcache(_BaseBranchCache): 1059 class remotebranchcache(_BaseBranchCache):
965 """Branchmap info for a remote connection, should not write locally""" 1060 """Branchmap info for a remote connection, should not write locally"""
966 1061