Mercurial > public > mercurial-scm > hg
diff hgext/fastannotate/formatter.py @ 39210:1ddb296e0dee
fastannotate: initial import from Facebook's hg-experimental
I made as few changes as I could to get the tests to pass, but this
was a bit involved due to some churn in the blame code since someone
last gave fastannotate any TLC.
There's still follow-up work here to rip out support for old versions
of hg and to integrate the protocol with modern standards.
Some performance numbers (all on my 2016 MacBook Pro with a 2.6Ghz i7):
Mercurial mercurial/manifest.py
traditional blame
time: real 1.050 secs (user 0.990+0.000 sys 0.060+0.000)
build cache
time: real 5.900 secs (user 5.720+0.000 sys 0.110+0.000)
fastannotate
time: real 0.120 secs (user 0.100+0.000 sys 0.020+0.000)
Mercurial mercurial/localrepo.py
traditional blame
time: real 3.330 secs (user 3.220+0.000 sys 0.070+0.000)
build cache
time: real 30.610 secs (user 30.190+0.000 sys 0.230+0.000)
fastannotate
time: real 0.180 secs (user 0.160+0.000 sys 0.020+0.000)
mozilla-central dom/ipc/ContentParent.cpp
traditional blame
time: real 7.640 secs (user 7.210+0.000 sys 0.380+0.000)
build cache
time: real 98.650 secs (user 97.000+0.000 sys 0.950+0.000)
fastannotate
time: real 1.580 secs (user 1.340+0.000 sys 0.240+0.000)
mozilla-central dom/base/nsDocument.cpp
traditional blame
time: real 17.110 secs (user 16.490+0.000 sys 0.500+0.000)
build cache
time: real 399.750 secs (user 394.520+0.000 sys 2.610+0.000)
fastannotate
time: real 1.780 secs (user 1.530+0.000 sys 0.240+0.000)
So building the cache is expensive (but might be faster with xdiff
enabled), but the blame results are *way* faster.
Differential Revision: https://phab.mercurial-scm.org/D3994
author | Augie Fackler <augie@google.com> |
---|---|
date | Mon, 30 Jul 2018 22:50:00 -0400 |
parents | |
children | 7e3ce2131882 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hgext/fastannotate/formatter.py Mon Jul 30 22:50:00 2018 -0400 @@ -0,0 +1,161 @@ +# Copyright 2016-present Facebook. All Rights Reserved. +# +# format: defines the format used to output annotate result +# +# 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 + +from mercurial import ( + encoding, + node, + pycompat, + templatefilters, + util, +) +from mercurial.utils import ( + dateutil, +) + +# imitating mercurial.commands.annotate, not using the vanilla formatter since +# the data structures are a bit different, and we have some fast paths. +class defaultformatter(object): + """the default formatter that does leftpad and support some common flags""" + + def __init__(self, ui, repo, opts): + self.ui = ui + self.opts = opts + + if ui.quiet: + datefunc = dateutil.shortdate + else: + datefunc = dateutil.datestr + datefunc = util.cachefunc(datefunc) + getctx = util.cachefunc(lambda x: repo[x[0]]) + hexfunc = self._hexfunc + + # special handling working copy "changeset" and "rev" functions + if self.opts.get('rev') == 'wdir()': + orig = hexfunc + hexfunc = lambda x: None if x is None else orig(x) + wnode = hexfunc(repo[None].p1().node()) + '+' + wrev = str(repo[None].p1().rev()) + wrevpad = '' + if not opts.get('changeset'): # only show + if changeset is hidden + wrev += '+' + wrevpad = ' ' + revenc = lambda x: wrev if x is None else str(x) + wrevpad + csetenc = lambda x: wnode if x is None else str(x) + ' ' + else: + revenc = csetenc = str + + # opt name, separator, raw value (for json/plain), encoder (for plain) + opmap = [('user', ' ', lambda x: getctx(x).user(), ui.shortuser), + ('number', ' ', lambda x: getctx(x).rev(), revenc), + ('changeset', ' ', lambda x: hexfunc(x[0]), csetenc), + ('date', ' ', lambda x: getctx(x).date(), datefunc), + ('file', ' ', lambda x: x[2], str), + ('line_number', ':', lambda x: x[1] + 1, str)] + fieldnamemap = {'number': 'rev', 'changeset': 'node'} + funcmap = [(get, sep, fieldnamemap.get(op, op), enc) + for op, sep, get, enc in opmap + if opts.get(op)] + # no separator for first column + funcmap[0] = list(funcmap[0]) + funcmap[0][1] = '' + self.funcmap = funcmap + + def write(self, annotatedresult, lines=None, existinglines=None): + """(annotateresult, [str], set([rev, linenum])) -> None. write output. + annotateresult can be [(node, linenum, path)], or [(node, linenum)] + """ + pieces = [] # [[str]] + maxwidths = [] # [int] + + # calculate padding + for f, sep, name, enc in self.funcmap: + l = [enc(f(x)) for x in annotatedresult] + pieces.append(l) + if name in ['node', 'date']: # node and date has fixed size + l = l[:1] + widths = map(encoding.colwidth, set(l)) + maxwidth = (max(widths) if widths else 0) + maxwidths.append(maxwidth) + + # buffered output + result = '' + for i in pycompat.xrange(len(annotatedresult)): + for j, p in enumerate(pieces): + sep = self.funcmap[j][1] + padding = ' ' * (maxwidths[j] - len(p[i])) + result += sep + padding + p[i] + if lines: + if existinglines is None: + result += ': ' + lines[i] + else: # extra formatting showing whether a line exists + key = (annotatedresult[i][0], annotatedresult[i][1]) + if key in existinglines: + result += ': ' + lines[i] + else: + result += ': ' + self.ui.label('-' + lines[i], + 'diff.deleted') + + if result[-1] != '\n': + result += '\n' + + self.ui.write(result) + + @util.propertycache + def _hexfunc(self): + if self.ui.debugflag or self.opts.get('long_hash'): + return node.hex + else: + return node.short + + def end(self): + pass + +class jsonformatter(defaultformatter): + def __init__(self, ui, repo, opts): + super(jsonformatter, self).__init__(ui, repo, opts) + self.ui.write('[') + self.needcomma = False + + def write(self, annotatedresult, lines=None, existinglines=None): + if annotatedresult: + self._writecomma() + + pieces = [(name, map(f, annotatedresult)) + for f, sep, name, enc in self.funcmap] + if lines is not None: + pieces.append(('line', lines)) + pieces.sort() + + seps = [','] * len(pieces[:-1]) + [''] + + result = '' + lasti = len(annotatedresult) - 1 + for i in pycompat.xrange(len(annotatedresult)): + result += '\n {\n' + for j, p in enumerate(pieces): + k, vs = p + result += (' "%s": %s%s\n' + % (k, templatefilters.json(vs[i], paranoid=False), + seps[j])) + result += ' }%s' % ('' if i == lasti else ',') + if lasti >= 0: + self.needcomma = True + + self.ui.write(result) + + def _writecomma(self): + if self.needcomma: + self.ui.write(',') + self.needcomma = False + + @util.propertycache + def _hexfunc(self): + return node.hex + + def end(self): + self.ui.write('\n]\n')