Mercurial > hg
changeset 28487:98d98a645e9d
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.
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Sun, 06 Mar 2016 14:28:02 -0800 |
parents | 50314dc3ae4e |
children | 437c32dcec7d |
files | mercurial/changelog.py |
diffstat | 1 files changed, 98 insertions(+), 33 deletions(-) [+] |
line wrap: on
line diff
--- 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): """