mutation: improve performance of obsolete check

Summary:
The computation of whether a commit is obsolete or not can be improved.

We can cache which commits are known to not be obsolete.

We can also have a cache for each filter type so that we only need to compute
obsolete nodes that match the filter.

Finally, when we need to compute all obsolete commits, we can start by looking
for commits which are made obsolete by only their closest successors, and then
filling back obsolescence to the predecessors of these obsolete commits.

Reviewed By: DurhamG

Differential Revision: D14858655

fbshipit-source-id: 1d03e214ad878ecb6ae548f80373702e2a184146
This commit is contained in:
Mark Thomas 2019-04-11 02:40:06 -07:00 committed by Facebook Github Bot
parent a5207547aa
commit d562032896
2 changed files with 106 additions and 24 deletions

View File

@ -7,6 +7,8 @@
from __future__ import absolute_import
from collections import defaultdict
from . import error, node as nodemod, phases, repoview, util
from .rust.bindings import mutationstore
@ -216,34 +218,102 @@ def allsuccessors(repo, nodes, startdepth=None, stopdepth=None):
nextlevel = set()
def isobsolete(repo, node):
"""Returns true if the node is obsolete in the repository."""
if node not in repo:
return False
if not util.safehasattr(repo, "_mutationobsolete"):
repo._mutationobsolete = set()
obsolete = repo._mutationobsolete
if node in obsolete:
return True
unfi = repo.unfiltered()
clrev = unfi.changelog.rev
hiddenrevs = repoview.filterrevs(repo, "visible")
class obsoletecache(object):
def __init__(self):
# Set of commits that are known to be obsolete for each filter level.
self.obsolete = defaultdict(set)
for succ in allsuccessors(repo, [node], startdepth=1):
# If any successor is already known to be obsolete, we can
# assume that the current node is obsolete without checking further.
if succ in obsolete:
# Set of commits that are known to be not obsolete for each filter level.
self.notobsolete = defaultdict(set)
# If true, then the full set of obsolete commits is known for this
# filter level, and is stored in ``self.obsolete``.
self.complete = defaultdict(bool)
def isobsolete(self, repo, node):
"""Returns true if the node is obsolete in the repository."""
if node not in repo:
return False
obsolete = self.obsolete[repo.filtername]
if node in obsolete:
return True
# The node is obsolete if any successor is visible in the repo.
if succ in unfi:
if clrev(succ) not in hiddenrevs:
if self.complete[repo.filtername] or node in self.notobsolete[repo.filtername]:
return False
unfi = repo.unfiltered()
clhasnode = unfi.changelog.hasnode
clrev = unfi.changelog.rev
hiddenrevs = repoview.filterrevs(repo, "visible")
for succ in allsuccessors(repo, [node], startdepth=1):
# If any successor is already known to be obsolete, we can
# assume that the current node is obsolete without checking further.
if succ in obsolete:
obsolete.add(node)
return True
return False
# The node is obsolete if any successor is visible in the normal
# filtered repo.
if clhasnode(succ) and clrev(succ) not in hiddenrevs:
obsolete.add(node)
return True
self.notobsolete[repo.filtername].add(node)
return False
def obsoletenodes(self, repo):
if self.complete[repo.filtername]:
return self.obsolete[repo.filtername]
# Testing each node separately will result in lots of repeated tests.
# Instead, we can do the following:
# - Compute all nodes that are obsolete because one of their closest
# successors is visible.
# - Work back from these commits marking all of their predecessors as
# obsolete.
# Note that "visible" here means "visible in a normal filtered repo",
# even if the filter for this repo includes other commits.
clhasnode = repo.changelog.hasnode
clrev = repo.changelog.rev
obsolete = self.obsolete[repo.filtername]
hiddenrevs = repoview.filterrevs(repo, "visible")
for node in repo.nodes("not public()"):
succsets = successorssets(repo, node, closest=True)
if succsets != [[node]]:
if any(
clrev(succ) not in hiddenrevs
for succset in succsets
for succ in succset
):
obsolete.add(node)
candidates = set(obsolete)
seen = set(obsolete)
while candidates:
candidate = candidates.pop()
entry = lookupsplit(repo, candidate)
if entry:
for pred in entry.preds():
if pred not in obsolete and pred not in seen:
candidates.add(pred)
seen.add(pred)
if clhasnode(pred):
obsolete.add(pred)
self.obsolete[repo.filtername] = frozenset(obsolete)
self.complete[repo.filtername] = True
# Since we know all obsolete commits, no need to remember which ones
# are not obsolete.
if repo.filtername in self.notobsolete:
del self.notobsolete[repo.filtername]
return self.obsolete[repo.filtername]
def isobsolete(repo, node):
if not util.safehasattr(repo, "_mutationobsolete"):
repo._mutationobsolete = obsoletecache()
return repo._mutationobsolete.isobsolete(repo, node)
def obsoletenodes(repo):
return {node for node in repo.nodes("not public()") if isobsolete(repo, node)}
if not util.safehasattr(repo, "_mutationobsolete"):
repo._mutationobsolete = obsoletecache()
return repo._mutationobsolete.obsoletenodes(repo)
def clearobsoletecache(repo):
@ -389,11 +459,13 @@ def successorssets(repo, startnode, closest=False, cache=None):
def getsets(node):
return lookupsuccessors(repo, node) or [[node]]
clhasnode = repo.changelog.hasnode
succsets = [[startnode]]
nextsuccsets = getsets(startnode)
expanded = nextsuccsets != succsets
while expanded:
if all(s in repo for succset in nextsuccsets for s in succset):
if all(clhasnode(s) for succset in nextsuccsets for s in succset):
# We have found a set of successor sets that all contain visible
# commits - this is a valid set to return.
succsets = nextsuccsets
@ -426,7 +498,7 @@ def successorssets(repo, startnode, closest=False, cache=None):
[
_succproduct(
[
[[succ]] if succ in repo else getsets(succ)
[[succ]] if clhasnode(succ) else getsets(succ)
for succ in succset
]
)
@ -441,7 +513,7 @@ def successorssets(repo, startnode, closest=False, cache=None):
# visible. Remove the invisible commits, and continue with what's
# left.
newnextsuccsets = [
[s for s in succset if s in repo] for succset in nextsuccsets
[s for s in succset if clhasnode(s)] for succset in nextsuccsets
]
# Remove sets that are now empty.
newnextsuccsets = [succset for succset in newnextsuccsets if succset]

View File

@ -462,6 +462,16 @@ from as invisible commits.
|/
o 0: 426bada5c675 'A'
Also check the obsolete revset is consistent.
$ tglogm -r "obsolete()"
x 1: 112478962961 'B' (Amended as e60094faeb72)
|
~
$ tglogm --hidden -r "obsolete()"
x 1: 112478962961 'B' (Amended as e60094faeb72)
|
~
Unhiding them reveals them as new commits and now the old ones show their relationship
to the new ones.
$ hg unhide ec992ff1fd78