topicmap: write and read format from disc
authorPierre-Yves David <pierre-yves.david@fb.com>
Sun, 13 Mar 2016 23:44:04 +0000
changeset 1890 e846b8f402d0
parent 1889 d9b929bcc3ad
child 1891 077c40f206d1
topicmap: write and read format from disc To prevent too awful performance we allow writing and reading topicmap cache. This is done with a lot of code duplication from core because core is not extensible enough.
src/topic/__init__.py
src/topic/topicmap.py
--- a/src/topic/__init__.py	Mon Mar 14 00:15:54 2016 +0000
+++ b/src/topic/__init__.py	Sun Mar 13 23:44:04 2016 +0000
@@ -69,10 +69,12 @@
     """use awful monkey patching to update the topic cache"""
     oldbranchcache = branchmap.branchcache
     oldfilename = branchmap._filename
+    oldread = branchmap.read
     oldcaches =  getattr(repo, '_branchcaches', {})
     try:
         branchmap.branchcache = topicmap.topiccache
         branchmap._filename = topicmap._filename
+        branchmap.read = topicmap.readtopicmap
         repo._branchcaches = getattr(repo, '_topiccaches', {})
         yield
         repo._topiccaches = repo._branchcaches
@@ -80,6 +82,7 @@
         repo._branchcaches = oldcaches
         branchmap.branchcache = oldbranchcache
         branchmap._filename = oldfilename
+        branchmap.read = oldread
 
 def cgapply(orig, repo, *args, **kwargs):
     with usetopicmap(repo):
--- a/src/topic/topicmap.py	Mon Mar 14 00:15:54 2016 +0000
+++ b/src/topic/topicmap.py	Sun Mar 13 23:44:04 2016 +0000
@@ -1,4 +1,9 @@
 from mercurial import branchmap
+from mercurial import encoding
+from mercurial import error
+from mercurial import scmutil
+from mercurial import util
+from mercurial.node import hex, bin, nullid
 
 def _filename(repo):
     """name of a branchcache file for a given repo or repoview"""
@@ -9,6 +14,25 @@
 
 oldbranchcache = branchmap.branchcache
 
+def _phaseshash(repo, maxrev):
+    revs = set()
+    cl = repo.changelog
+    fr = cl.filteredrevs
+    nm = cl.nodemap
+    for roots in repo._phasecache.phaseroots[1:]:
+        for n in roots:
+            r = nm.get(n)
+            if r not in fr and r < maxrev:
+                revs.add(r)
+    key = nullid
+    revs = sorted(revs)
+    if revs:
+        s = util.sha1()
+        for rev in revs:
+            s.update('%s;' % rev)
+        key = s.digest()
+    return key
+
 class topiccache(oldbranchcache):
 
     def __init__(self, *args, **kwargs):
@@ -16,10 +40,22 @@
         try:
             # super() call may fail otherwise
             branchmap.branchcache = oldbranchcache
-            return super(topiccache, self).__init__(*args, **kwargs)
+            super(topiccache, self).__init__(*args, **kwargs)
+            if self.filteredhash is None:
+                self.filteredhash = nullid
+            self.phaseshash = nullid
         finally:
             branchmap.branchcache = otherbranchcache
 
+    def copy(self):
+        """return an deep copy of the branchcache object"""
+        new =  topiccache(self, self.tipnode, self.tiprev, self.filteredhash,
+                          self._closednodes)
+        if self.filteredhash is None:
+            self.filteredhash = nullid
+        new.phaseshash = self.phaseshash
+        return new
+
     def branchtip(self, branch, topic=''):
         '''Return the tipmost open head on branch head, otherwise return the
         tipmost closed head on branch.
@@ -33,10 +69,60 @@
             branch = '%s:%s' % (branch, topic)
         return super(topiccache, self).branchheads(branch, closed=closed)
 
+    def validfor(self, repo):
+        """Is the cache content valid regarding a repo
+
+        - False when cached tipnode is unknown or if we detect a strip.
+        - True when cache is up to date or a subset of current repo."""
+        # This is copy paste of mercurial.branchmap.branchcache.validfor in
+        # 69077c65919d With a small changes to the cache key handling to
+        # include phase information that impact the topic cache.
+        #
+        # All code changes should be flagged on site.
+        try:
+            if (self.tipnode == repo.changelog.node(self.tiprev)):
+                fh = scmutil.filteredhash(repo, self.tiprev)
+                if fh is None:
+                    fh = nullid
+                if ((self.filteredhash == fh)
+                     and (self.phaseshash == _phaseshash(repo, self.tiprev))):
+                    return True
+            return False
+        except IndexError:
+            return False
+
     def write(self, repo):
-        # The cache key needs to take phase root in account because change to
-        # what is public affect topics. We can't write on disk until we have this.
-        return
+        # This is copy paste of mercurial.branchmap.branchcache.write in
+        # 69077c65919d With a small changes to the cache key handling to
+        # include phase information that impact the topic cache.
+        #
+        # All code changes should be flagged on site.
+        try:
+            f = repo.vfs(_filename(repo), "w", atomictemp=True)
+            cachekey = [hex(self.tipnode), str(self.tiprev)]
+            # [CHANGE] we need a hash in all cases
+            assert self.filteredhash is not None
+            cachekey.append(hex(self.filteredhash))
+            cachekey.append(hex(self.phaseshash))
+            f.write(" ".join(cachekey) + '\n')
+            nodecount = 0
+            for label, nodes in sorted(self.iteritems()):
+                for node in nodes:
+                    nodecount += 1
+                    if node in self._closednodes:
+                        state = 'c'
+                    else:
+                        state = 'o'
+                    f.write("%s %s %s\n" % (hex(node), state,
+                                            encoding.fromlocal(label)))
+            f.close()
+            repo.ui.log('branchcache',
+                        'wrote %s branch cache with %d labels and %d nodes\n',
+                        repo.filtername, len(self), nodecount)
+        except (IOError, OSError, error.Abort) as inst:
+            repo.ui.debug("couldn't write branch cache: %s\n" % inst)
+            # Abort may be raise by read only opener
+            pass
 
     def update(self, repo, revgen):
         """Given a branchhead cache, self, that may have extra nodes or be
@@ -56,6 +142,59 @@
                     branch = '%s:%s' % (branch, topic)
                 return (branch, info[1])
             repo.revbranchcache().branchinfo = branchinfo
-            return super(topiccache, self).update(repo, revgen)
+            super(topiccache, self).update(repo, revgen)
+            if self.filteredhash is None:
+                self.filteredhash = nullid
+            self.phaseshash = _phaseshash(repo, self.tiprev)
         finally:
             repo.revbranchcache().branchinfo = oldgetbranchinfo
+
+def readtopicmap(repo):
+    # This is copy paste of mercurial.branchmap.read in 69077c65919d
+    # With a small changes to the cache key handling to include phase
+    # information that impact the topic cache.
+    #
+    # All code changes should be flagged on site.
+    try:
+        f = repo.vfs(_filename(repo))
+        lines = f.read().split('\n')
+        f.close()
+    except (IOError, OSError):
+        return None
+
+    try:
+        cachekey = lines.pop(0).split(" ", 2)
+        last, lrev = cachekey[:2]
+        last, lrev = bin(last), int(lrev)
+        filteredhash = bin(cachekey[2]) # [CHANGE] unconditional filteredhash
+        partial = branchcache(tipnode=last, tiprev=lrev,
+                              filteredhash=filteredhash)
+        partial.phaseshash = bin(cachekey[3]) # [CHANGE] read phaseshash
+        if not partial.validfor(repo):
+            # invalidate the cache
+            raise ValueError('tip differs')
+        cl = repo.changelog
+        for l in lines:
+            if not l:
+                continue
+            node, state, label = l.split(" ", 2)
+            if state not in 'oc':
+                raise ValueError('invalid branch state')
+            label = encoding.tolocal(label.strip())
+            node = bin(node)
+            if not cl.hasnode(node):
+                raise ValueError('node %s does not exist' % hex(node))
+            partial.setdefault(label, []).append(node)
+            if state == 'c':
+                partial._closednodes.add(node)
+    except KeyboardInterrupt:
+        raise
+    except Exception as inst:
+        if repo.ui.debugflag:
+            msg = 'invalid branchheads cache'
+            if repo.filtername is not None:
+                msg += ' (%s)' % repo.filtername
+            msg += ': %s\n'
+            repo.ui.debug(msg % inst)
+        partial = None
+    return partial