changelog: add class to represent parsed changelog revisions
authorGregory Szorc <gregory.szorc@gmail.com>
Sun, 06 Mar 2016 14:28:02 -0800
changeset 28487 98d98a645e9d
parent 28486 50314dc3ae4e
child 28488 437c32dcec7d
changelog: add class to represent parsed changelog revisions Currently, changelog entries are parsed into their respective components at read time. Many operations are only interested in a subset of fields of a changelog entry. The parsing and storing of all the fields adds avoidable overhead. This patch introduces the "changelogrevision" class. It takes changelog raw text and exposes the parsed results as attributes. The code for parsing changelog entries has been moved into its construction function. changelog.read() has been modified to use the new class internally while maintaining its existing API. Future patches will make revision parsing lazy. We implement the construction function of the new class with __new__ instead of __init__ so we can use a named tuple to represent the empty revision. This saves overhead and complexity of coercing later versions of this class to represent an empty instance. While we are here, we add a method on changelog to obtain an instance of the new type. The overhead of constructing the new class regresses performance of revsets accessing this data: author(mpm) 0.896565 0.929984 desc(bug) 0.887169 0.935642 105% date(2015) 0.878797 0.908094 extra(rebase_source) 0.865446 0.922624 106% author(mpm) or author(greg) 1.801832 1.902112 105% author(mpm) or desc(bug) 1.812438 1.860977 date(2015) or branch(default) 0.968276 1.005824 author(mpm) or desc(bug) or date(2015) or extra(rebase_source) 3.656193 3.743381 Once lazy parsing is implemented, these revsets will all be faster than before. There is no performance change on revsets that do not access this data. There /could/ be a performance regression on operations that perform several changelog reads. However, I can't think of anything outside of revsets and `hg log` (basically the same as a revset) that would be impacted.
mercurial/changelog.py
--- a/mercurial/changelog.py	Fri Mar 11 11:51:22 2016 -0500
+++ b/mercurial/changelog.py	Sun Mar 06 14:28:02 2016 -0800
@@ -7,6 +7,8 @@
 
 from __future__ import absolute_import
 
+import collections
+
 from .i18n import _
 from .node import (
     bin,
@@ -136,6 +138,77 @@
         return appender(opener, name, mode, buf)
     return _delay
 
+_changelogrevision = collections.namedtuple('changelogrevision',
+                                            ('manifest', 'user', 'date',
+                                             'files', 'description', 'extra'))
+
+class changelogrevision(object):
+    """Holds results of a parsed changelog revision.
+
+    Changelog revisions consist of multiple pieces of data, including
+    the manifest node, user, and date. This object exposes a view into
+    the parsed object.
+    """
+
+    __slots__ = (
+        'date',
+        'description',
+        'extra',
+        'files',
+        'manifest',
+        'user',
+    )
+
+    def __new__(cls, text):
+        if not text:
+            return _changelogrevision(
+                manifest=nullid,
+                user='',
+                date=(0, 0),
+                files=[],
+                description='',
+                extra=_defaultextra,
+            )
+
+        self = super(changelogrevision, cls).__new__(cls)
+        # We could return here and implement the following as an __init__.
+        # But doing it here is equivalent and saves an extra function call.
+
+        # format used:
+        # nodeid\n        : manifest node in ascii
+        # user\n          : user, no \n or \r allowed
+        # time tz extra\n : date (time is int or float, timezone is int)
+        #                 : extra is metadata, encoded and separated by '\0'
+        #                 : older versions ignore it
+        # files\n\n       : files modified by the cset, no \n or \r allowed
+        # (.*)            : comment (free text, ideally utf-8)
+        #
+        # changelog v0 doesn't use extra
+
+        last = text.index("\n\n")
+        self.description = encoding.tolocal(text[last + 2:])
+        l = text[:last].split('\n')
+        self.manifest = bin(l[0])
+        self.user = encoding.tolocal(l[1])
+
+        tdata = l[2].split(' ', 2)
+        if len(tdata) != 3:
+            time = float(tdata[0])
+            try:
+                # various tools did silly things with the time zone field.
+                timezone = int(tdata[1])
+            except ValueError:
+                timezone = 0
+            self.extra = _defaultextra
+        else:
+            time, timezone = float(tdata[0]), int(tdata[1])
+            self.extra = decodeextra(tdata[2])
+
+        self.date = (time, timezone)
+        self.files = l[3:]
+
+        return self
+
 class changelog(revlog.revlog):
     def __init__(self, opener):
         revlog.revlog.__init__(self, opener, "00changelog.i")
@@ -323,42 +396,34 @@
             revlog.revlog.checkinlinesize(self, tr, fp)
 
     def read(self, node):
-        """
-        format used:
-        nodeid\n        : manifest node in ascii
-        user\n          : user, no \n or \r allowed
-        time tz extra\n : date (time is int or float, timezone is int)
-                        : extra is metadata, encoded and separated by '\0'
-                        : older versions ignore it
-        files\n\n       : files modified by the cset, no \n or \r allowed
-        (.*)            : comment (free text, ideally utf-8)
+        """Obtain data from a parsed changelog revision.
+
+        Returns a 6-tuple of:
 
-        changelog v0 doesn't use extra
+           - manifest node in binary
+           - author/user as a localstr
+           - date as a 2-tuple of (time, timezone)
+           - list of files
+           - commit message as a localstr
+           - dict of extra metadata
+
+        Unless you need to access all fields, consider calling
+        ``changelogrevision`` instead, as it is faster for partial object
+        access.
         """
-        text = self.revision(node)
-        if not text:
-            return nullid, "", (0, 0), [], "", _defaultextra
-        last = text.index("\n\n")
-        desc = encoding.tolocal(text[last + 2:])
-        l = text[:last].split('\n')
-        manifest = bin(l[0])
-        user = encoding.tolocal(l[1])
+        c = changelogrevision(self.revision(node))
+        return (
+            c.manifest,
+            c.user,
+            c.date,
+            c.files,
+            c.description,
+            c.extra
+        )
 
-        tdata = l[2].split(' ', 2)
-        if len(tdata) != 3:
-            time = float(tdata[0])
-            try:
-                # various tools did silly things with the time zone field.
-                timezone = int(tdata[1])
-            except ValueError:
-                timezone = 0
-            extra = _defaultextra
-        else:
-            time, timezone = float(tdata[0]), int(tdata[1])
-            extra = decodeextra(tdata[2])
-
-        files = l[3:]
-        return manifest, user, (time, timezone), files, desc, extra
+    def changelogrevision(self, nodeorrev):
+        """Obtain a ``changelogrevision`` for a node or revision."""
+        return changelogrevision(self.revision(nodeorrev))
 
     def readfiles(self, node):
         """