[PATCH 01 of 10 lazy-changelog-parse] changelog: add class to represent parsed changelog revisions

Gregory Szorc gregory.szorc at gmail.com
Sun Mar 6 18:58:47 EST 2016


# HG changeset patch
# User Gregory Szorc <gregory.szorc at gmail.com>
# Date 1457303282 28800
#      Sun Mar 06 14:28:02 2016 -0800
# Node ID 7531b8572b611e13fe45a9882a1c7d9aa6e793d6
# Parent  9974b8236cac50945d7b529e7c4fae9cf4974443
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.

diff --git a/mercurial/changelog.py b/mercurial/changelog.py
--- a/mercurial/changelog.py
+++ b/mercurial/changelog.py
@@ -2,16 +2,18 @@
 #
 # Copyright 2005-2007 Matt Mackall <mpm at selenic.com>
 #
 # This software may be used and distributed according to the terms of the
 # GNU General Public License version 2 or any later version.
 
 from __future__ import absolute_import
 
+import collections
+
 from .i18n import _
 from .node import (
     bin,
     hex,
     nullid,
 )
 
 from . import (
@@ -131,16 +133,87 @@ def _divertopener(opener, target):
 def _delayopener(opener, target, buf):
     """build an opener that stores chunks in 'buf' instead of 'target'"""
     def _delay(name, mode='r'):
         if name != target:
             return opener(name, mode)
         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")
         if self._initempty:
             # changelogs don't benefit from generaldelta
             self.version &= ~revlog.REVLOGGENERALDELTA
             self._generaldelta = False
         self._realopener = opener
@@ -318,52 +391,44 @@ class changelog(revlog.revlog):
 
         return False
 
     def checkinlinesize(self, tr, fp=None):
         if not self._delayed:
             revlog.revlog.checkinlinesize(self, tr, fp)
 
     def read(self, node):
+        """Obtain data from a parsed changelog revision.
+
+        Returns a 6-tuple of:
+
+           - 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.
         """
-        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)
+        c = changelogrevision(self.revision(node))
+        return (
+            c.manifest,
+            c.user,
+            c.date,
+            c.files,
+            c.description,
+            c.extra
+        )
 
-        changelog v0 doesn't use extra
-        """
-        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])
-
-        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):
         """
         short version of read that only returns the files modified by the cset
         """
         text = self.revision(node)
         if not text:
             return []


More information about the Mercurial-devel mailing list