remotephase: avoid full changelog iteration (issue5964) stable
authorBoris Feld <boris.feld@octobus.net>
Fri, 17 Aug 2018 20:35:52 +0200
branchstable
changeset 39146 f736fdbe546a
parent 39141 c89e2fb207a1
child 39147 b95b48a55c36
child 39193 cee9043c7dba
remotephase: avoid full changelog iteration (issue5964) Changeset 88efb7d6bcb6 introduced a performance regression by triggering a full ancestors walk. This changeset reworks this logic so that we no longer walk down the full changelog. The motivation for 88efb7d6bcb6, issue5939, is still fixed. mercurial compared to a draft repository ---------------------------------------- 8eeed92475d5: 0.012637 seconds 88efb7d6bcb6: 0.202699 seconds (x16) 46da52f4b820: 0.215551 seconds (+6%) this code: 0.008397 seconds (-33% from base) The payload size reduction we see in `test-bookmarks-pushpull.t` comes from a more aggressive filter of nullid and is harmless.
mercurial/phases.py
tests/test-bookmarks-pushpull.t
--- a/mercurial/phases.py	Fri Aug 17 16:00:32 2018 -0700
+++ b/mercurial/phases.py	Fri Aug 17 20:35:52 2018 +0200
@@ -664,11 +664,39 @@
 
     * `heads`: define the first subset
     * `roots`: define the second we subtract from the first"""
+    # prevent an import cycle
+    # phases > dagop > patch > copies > scmutil > obsolete > obsutil > phases
+    from . import dagop
+
+    repo = repo.unfiltered()
+    cl = repo.changelog
+    rev = cl.nodemap.get
+    if not roots:
+        return heads
+    if not heads or heads == [nullrev]:
+        return []
+    # The logic operated on revisions, convert arguments early for convenience
+    new_heads = set(rev(n) for n in heads if n != nullid)
+    roots = [rev(n) for n in roots]
     if not heads or not roots:
         return heads
-    repo = repo.unfiltered()
-    revs = repo.revs('heads(::%ln - (%ln::%ln))', heads, roots, heads)
-    return pycompat.maplist(repo.changelog.node, revs)
+    # compute the area we need to remove
+    affected_zone = repo.revs("(%ld::%ld)", roots, new_heads)
+    # heads in the area are no longer heads
+    new_heads.difference_update(affected_zone)
+    # revisions in the area have children outside of it,
+    # They might be new heads
+    candidates = repo.revs("parents(%ld + (%ld and merge())) and not null",
+                           roots, affected_zone)
+    candidates -= affected_zone
+    if new_heads or candidates:
+        # remove candidate that are ancestors of other heads
+        new_heads.update(candidates)
+        prunestart = repo.revs("parents(%ld) and not null", new_heads)
+        pruned = dagop.reachableroots(repo, candidates, prunestart)
+        new_heads.difference_update(pruned)
+
+    return pycompat.maplist(cl.node, sorted(new_heads))
 
 def newcommitphase(ui):
     """helper to get the target phase of new commit
--- a/tests/test-bookmarks-pushpull.t	Fri Aug 17 16:00:32 2018 -0700
+++ b/tests/test-bookmarks-pushpull.t	Fri Aug 17 20:35:52 2018 +0200
@@ -141,10 +141,10 @@
   bundle2-output: payload chunk size: 23
   bundle2-output: closing payload chunk
   bundle2-output: bundle part: "check:phases"
-  bundle2-output-part: "check:phases" 48 bytes payload
+  bundle2-output-part: "check:phases" 24 bytes payload
   bundle2-output: part 2: "CHECK:PHASES"
   bundle2-output: header chunk size: 19
-  bundle2-output: payload chunk size: 48
+  bundle2-output: payload chunk size: 24
   bundle2-output: closing payload chunk
   bundle2-output: bundle part: "pushkey"
   bundle2-output-part: "pushkey" (params: 4 mandatory) empty payload
@@ -180,9 +180,9 @@
   bundle2-input: part parameters: 0
   bundle2-input: found a handler for part check:phases
   bundle2-input-part: "check:phases" supported
-  bundle2-input: payload chunk size: 48
+  bundle2-input: payload chunk size: 24
   bundle2-input: payload chunk size: 0
-  bundle2-input-part: total payload size 48
+  bundle2-input-part: total payload size 24
   bundle2-input: part header size: 90
   bundle2-input: part type: "PUSHKEY"
   bundle2-input: part id: "3"
@@ -253,10 +253,10 @@
   bundle2-output: payload chunk size: 23
   bundle2-output: closing payload chunk
   bundle2-output: bundle part: "check:phases"
-  bundle2-output-part: "check:phases" 48 bytes payload
+  bundle2-output-part: "check:phases" 24 bytes payload
   bundle2-output: part 2: "CHECK:PHASES"
   bundle2-output: header chunk size: 19
-  bundle2-output: payload chunk size: 48
+  bundle2-output: payload chunk size: 24
   bundle2-output: closing payload chunk
   bundle2-output: bundle part: "bookmarks"
   bundle2-output-part: "bookmarks" 23 bytes payload
@@ -293,9 +293,9 @@
   bundle2-input: part parameters: 0
   bundle2-input: found a handler for part check:phases
   bundle2-input-part: "check:phases" supported
-  bundle2-input: payload chunk size: 48
+  bundle2-input: payload chunk size: 24
   bundle2-input: payload chunk size: 0
-  bundle2-input-part: total payload size 48
+  bundle2-input-part: total payload size 24
   bundle2-input: part header size: 16
   bundle2-input: part type: "BOOKMARKS"
   bundle2-input: part id: "3"