Mercurial > hg
changeset 44548:fdc802f29b2c
transactions: convert changes['phases'] to list of ranges
Consecutive revisions are often in the same phase, especially public
revisions. This means that a dictionary keyed by the revision for the
phase transitions is highly redundant. Build a list of (range, (old,
new)) entries instead and aggressively merge ranges with the same
transition. For the test case in issue5691, this reduces memory use by
~20MB.
Differential Revision: https://phab.mercurial-scm.org/D8125
author | Joerg Sonnenberger <joerg@bec.de> |
---|---|
date | Fri, 08 Dec 2017 02:29:02 +0100 |
parents | 81a873477feb |
children | 47f8c741df0f |
files | mercurial/localrepo.py mercurial/phases.py mercurial/scmutil.py tests/testlib/ext-phase-report.py |
diffstat | 4 files changed, 136 insertions(+), 49 deletions(-) [+] |
line wrap: on
line diff
--- a/mercurial/localrepo.py Wed Mar 11 17:42:56 2020 +0100 +++ b/mercurial/localrepo.py Fri Dec 08 02:29:02 2017 +0100 @@ -2178,15 +2178,16 @@ ) if hook.hashook(repo.ui, b'pretxnclose-phase'): cl = repo.unfiltered().changelog - for rev, (old, new) in tr.changes[b'phases'].items(): - args = tr.hookargs.copy() - node = hex(cl.node(rev)) - args.update(phases.preparehookargs(node, old, new)) - repo.hook( - b'pretxnclose-phase', - throw=True, - **pycompat.strkwargs(args) - ) + for revs, (old, new) in tr.changes[b'phases']: + for rev in revs: + args = tr.hookargs.copy() + node = hex(cl.node(rev)) + args.update(phases.preparehookargs(node, old, new)) + repo.hook( + b'pretxnclose-phase', + throw=True, + **pycompat.strkwargs(args) + ) repo.hook( b'pretxnclose', throw=True, **pycompat.strkwargs(tr.hookargs) @@ -2231,7 +2232,7 @@ ) tr.changes[b'origrepolen'] = len(self) tr.changes[b'obsmarkers'] = set() - tr.changes[b'phases'] = {} + tr.changes[b'phases'] = [] tr.changes[b'bookmarks'] = {} tr.hookargs[b'txnid'] = txnid @@ -2265,16 +2266,19 @@ if hook.hashook(repo.ui, b'txnclose-phase'): cl = repo.unfiltered().changelog - phasemv = sorted(tr.changes[b'phases'].items()) - for rev, (old, new) in phasemv: - args = tr.hookargs.copy() - node = hex(cl.node(rev)) - args.update(phases.preparehookargs(node, old, new)) - repo.hook( - b'txnclose-phase', - throw=False, - **pycompat.strkwargs(args) - ) + phasemv = sorted( + tr.changes[b'phases'], key=lambda r: r[0][0] + ) + for revs, (old, new) in phasemv: + for rev in revs: + args = tr.hookargs.copy() + node = hex(cl.node(rev)) + args.update(phases.preparehookargs(node, old, new)) + repo.hook( + b'txnclose-phase', + throw=False, + **pycompat.strkwargs(args) + ) repo.hook( b'txnclose', throw=False, **pycompat.strkwargs(hookargs)
--- a/mercurial/phases.py Wed Mar 11 17:42:56 2020 +0100 +++ b/mercurial/phases.py Fri Dec 08 02:29:02 2017 +0100 @@ -216,17 +216,101 @@ return headsbyphase +def _sortedrange_insert(data, idx, rev, t): + merge_before = False + if idx: + r1, t1 = data[idx - 1] + merge_before = r1[-1] + 1 == rev and t1 == t + merge_after = False + if idx < len(data): + r2, t2 = data[idx] + merge_after = r2[0] == rev + 1 and t2 == t + + if merge_before and merge_after: + data[idx - 1] = (pycompat.xrange(r1[0], r2[-1] + 1), t) + data.pop(idx) + elif merge_before: + data[idx - 1] = (pycompat.xrange(r1[0], rev + 1), t) + elif merge_after: + data[idx] = (pycompat.xrange(rev, r2[-1] + 1), t) + else: + data.insert(idx, (pycompat.xrange(rev, rev + 1), t)) + + +def _sortedrange_split(data, idx, rev, t): + r1, t1 = data[idx] + if t == t1: + return + t = (t1[0], t[1]) + if len(r1) == 1: + data.pop(idx) + _sortedrange_insert(data, idx, rev, t) + elif r1[0] == rev: + data[idx] = (pycompat.xrange(rev + 1, r1[-1] + 1), t1) + _sortedrange_insert(data, idx, rev, t) + elif r1[-1] == rev: + data[idx] = (pycompat.xrange(r1[0], rev), t1) + _sortedrange_insert(data, idx + 1, rev, t) + else: + data[idx : idx + 1] = [ + (pycompat.xrange(r1[0], rev), t1), + (pycompat.xrange(rev, rev + 1), t), + (pycompat.xrange(rev + 1, r1[-1] + 1), t1), + ] + + def _trackphasechange(data, rev, old, new): - """add a phase move the <data> dictionnary + """add a phase move to the <data> list of ranges If data is None, nothing happens. """ if data is None: return - existing = data.get(rev) - if existing is not None: - old = existing[0] - data[rev] = (old, new) + + # If data is empty, create a one-revision range and done + if not data: + data.insert(0, (pycompat.xrange(rev, rev + 1), (old, new))) + return + + low = 0 + high = len(data) + t = (old, new) + while low < high: + mid = (low + high) // 2 + revs = data[mid][0] + + if rev in revs: + _sortedrange_split(data, mid, rev, t) + return + + if revs[0] == rev + 1: + if mid and data[mid - 1][0][-1] == rev: + _sortedrange_split(data, mid - 1, rev, t) + else: + _sortedrange_insert(data, mid, rev, t) + return + + if revs[-1] == rev - 1: + if mid + 1 < len(data) and data[mid + 1][0][0] == rev: + _sortedrange_split(data, mid + 1, rev, t) + else: + _sortedrange_insert(data, mid + 1, rev, t) + return + + if revs[0] > rev: + high = mid + else: + low = mid + 1 + + if low == len(data): + data.append((pycompat.xrange(rev, rev + 1), t)) + return + + r1, t1 = data[low] + if r1[0] > rev: + data.insert(low, (pycompat.xrange(rev, rev + 1), t)) + else: + data.insert(low + 1, (pycompat.xrange(rev, rev + 1), t)) class phasecache(object): @@ -400,8 +484,9 @@ phasetracking = tr.changes[b'phases'] torev = repo.changelog.rev phase = self.phase - for n in nodes: - rev = torev(n) + revs = [torev(node) for node in nodes] + revs.sort() + for rev in revs: revphase = phase(repo, rev) _trackphasechange(phasetracking, rev, None, revphase) repo.invalidatevolatilesets() @@ -485,7 +570,7 @@ affected -= revs else: # public phase revs = affected - for r in revs: + for r in sorted(revs): _trackphasechange(phasetracking, r, phase, targetphase) repo.invalidatevolatilesets()
--- a/mercurial/scmutil.py Wed Mar 11 17:42:56 2020 +0100 +++ b/mercurial/scmutil.py Fri Dec 08 02:29:02 2017 +0100 @@ -2058,14 +2058,11 @@ pull/unbundle. """ origrepolen = tr.changes.get(b'origrepolen', len(repo)) - phasetracking = tr.changes.get(b'phases', {}) - if not phasetracking: - return - published = [ - rev - for rev, (old, new) in pycompat.iteritems(phasetracking) - if new == phases.public and rev < origrepolen - ] + published = [] + for revs, (old, new) in tr.changes.get(b'phases', []): + if new != phases.public: + continue + published.extend(rev for rev in revs if rev < origrepolen) if not published: return msg = _(b'%d local changesets published\n')
--- a/tests/testlib/ext-phase-report.py Wed Mar 11 17:42:56 2020 +0100 +++ b/tests/testlib/ext-phase-report.py Fri Dec 08 02:29:02 2017 +0100 @@ -5,21 +5,22 @@ def reposetup(ui, repo): def reportphasemove(tr): - for rev, move in sorted(tr.changes[b'phases'].items()): - if move[0] is None: - ui.write( - ( - b'test-debug-phase: new rev %d: x -> %d\n' - % (rev, move[1]) + for revs, move in sorted(tr.changes[b"phases"], key=lambda r: r[0][0]): + for rev in revs: + if move[0] is None: + ui.write( + ( + b'test-debug-phase: new rev %d: x -> %d\n' + % (rev, move[1]) + ) ) - ) - else: - ui.write( - ( - b'test-debug-phase: move rev %d: %d -> %d\n' - % (rev, move[0], move[1]) + else: + ui.write( + ( + b'test-debug-phase: move rev %d: %d -> %d\n' + % (rev, move[0], move[1]) + ) ) - ) class reportphaserepo(repo.__class__): def transaction(self, *args, **kwargs):