phase: improve retractboundary perf
The existing retractboundary implementation computed the new boundary by walking
all descendants of all existing roots and computing the new roots. This is
O(commits since first root), which on long repos can be hundreds of thousands of
commits.
The new algorithm only updates roots that are greater than the new root
locations. For common operations like commit on a repo with the earliest root
several hundred thousand commits ago, this makes retractboundary go from
1 second to 0.008 seconds.
I tested it by running the test suite with both implementations and checking
that the root results were always the identical.
There was some discussion on IRC about the safety of this (i.e. what if the new
nodes are already part of the phase, etc). I've looked into it and believe this
patch is safe:
1) The old existing code already filters the input nodes to only contain nodes
that require retracting (i.e. we only make node X a new root if the old phase
is less than the target phase), so there's no chance of us adding a
unnecessary root to the phase (unless the input root is made unnecessary by
another root in the same input, but see point #3).
2) Another way of thinking about this is: the only way the new algorithm would
be different from the old algorithm is if it added a root that is a
descendant of an old root (since the old algorithm would've caught this in
the big "roots(%ln::)". At the beginning of the function, when we filter out
roots that already meet the phase criteria, the *definition* of meeting the
phase criteria is "not being a descendant of an existing root". Therefore,
by definition none of the new roots we are processing are descendants of an
existing root.
3) If two nodes are passed in as input, and one node is an ancestor of the other
(and therefore the later node should not be a root), this is still caught by
the 'roots(%ln::)' revset. So there's no chance of an extra root being
introduced that way either.
--- a/mercurial/phases.py Thu Nov 12 13:51:09 2015 -0600
+++ b/mercurial/phases.py Sat Nov 07 16:11:49 2015 -0800
@@ -308,9 +308,19 @@
raise error.Abort(_('cannot change null revision phase'))
currentroots = currentroots.copy()
currentroots.update(newroots)
- ctxs = repo.set('roots(%ln::)', currentroots)
- currentroots.intersection_update(ctx.node() for ctx in ctxs)
- self._updateroots(targetphase, currentroots, tr)
+
+ # Only compute new roots for revs above the roots that are being
+ # retracted.
+ minnewroot = min(repo[n].rev() for n in newroots)
+ aboveroots = [n for n in currentroots
+ if repo[n].rev() >= minnewroot]
+ updatedroots = repo.set('roots(%ln::)', aboveroots)
+
+ finalroots = set(n for n in currentroots if repo[n].rev() <
+ minnewroot)
+ finalroots.update(ctx.node() for ctx in updatedroots)
+
+ self._updateroots(targetphase, finalroots, tr)
repo.invalidatevolatilesets()
def filterunknown(self, repo):