comparison 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
comparison
equal deleted inserted replaced
39209:1af95139e5ec 39210:1ddb296e0dee
1 # Copyright 2016-present Facebook. All Rights Reserved.
2 #
3 # format: defines the format used to output annotate result
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
8
9 from mercurial import (
10 encoding,
11 node,
12 pycompat,
13 templatefilters,
14 util,
15 )
16 from mercurial.utils import (
17 dateutil,
18 )
19
20 # imitating mercurial.commands.annotate, not using the vanilla formatter since
21 # the data structures are a bit different, and we have some fast paths.
22 class defaultformatter(object):
23 """the default formatter that does leftpad and support some common flags"""
24
25 def __init__(self, ui, repo, opts):
26 self.ui = ui
27 self.opts = opts
28
29 if ui.quiet:
30 datefunc = dateutil.shortdate
31 else:
32 datefunc = dateutil.datestr
33 datefunc = util.cachefunc(datefunc)
34 getctx = util.cachefunc(lambda x: repo[x[0]])
35 hexfunc = self._hexfunc
36
37 # special handling working copy "changeset" and "rev" functions
38 if self.opts.get('rev') == 'wdir()':
39 orig = hexfunc
40 hexfunc = lambda x: None if x is None else orig(x)
41 wnode = hexfunc(repo[None].p1().node()) + '+'
42 wrev = str(repo[None].p1().rev())
43 wrevpad = ''
44 if not opts.get('changeset'): # only show + if changeset is hidden
45 wrev += '+'
46 wrevpad = ' '
47 revenc = lambda x: wrev if x is None else str(x) + wrevpad
48 csetenc = lambda x: wnode if x is None else str(x) + ' '
49 else:
50 revenc = csetenc = str
51
52 # opt name, separator, raw value (for json/plain), encoder (for plain)
53 opmap = [('user', ' ', lambda x: getctx(x).user(), ui.shortuser),
54 ('number', ' ', lambda x: getctx(x).rev(), revenc),
55 ('changeset', ' ', lambda x: hexfunc(x[0]), csetenc),
56 ('date', ' ', lambda x: getctx(x).date(), datefunc),
57 ('file', ' ', lambda x: x[2], str),
58 ('line_number', ':', lambda x: x[1] + 1, str)]
59 fieldnamemap = {'number': 'rev', 'changeset': 'node'}
60 funcmap = [(get, sep, fieldnamemap.get(op, op), enc)
61 for op, sep, get, enc in opmap
62 if opts.get(op)]
63 # no separator for first column
64 funcmap[0] = list(funcmap[0])
65 funcmap[0][1] = ''
66 self.funcmap = funcmap
67
68 def write(self, annotatedresult, lines=None, existinglines=None):
69 """(annotateresult, [str], set([rev, linenum])) -> None. write output.
70 annotateresult can be [(node, linenum, path)], or [(node, linenum)]
71 """
72 pieces = [] # [[str]]
73 maxwidths = [] # [int]
74
75 # calculate padding
76 for f, sep, name, enc in self.funcmap:
77 l = [enc(f(x)) for x in annotatedresult]
78 pieces.append(l)
79 if name in ['node', 'date']: # node and date has fixed size
80 l = l[:1]
81 widths = map(encoding.colwidth, set(l))
82 maxwidth = (max(widths) if widths else 0)
83 maxwidths.append(maxwidth)
84
85 # buffered output
86 result = ''
87 for i in pycompat.xrange(len(annotatedresult)):
88 for j, p in enumerate(pieces):
89 sep = self.funcmap[j][1]
90 padding = ' ' * (maxwidths[j] - len(p[i]))
91 result += sep + padding + p[i]
92 if lines:
93 if existinglines is None:
94 result += ': ' + lines[i]
95 else: # extra formatting showing whether a line exists
96 key = (annotatedresult[i][0], annotatedresult[i][1])
97 if key in existinglines:
98 result += ': ' + lines[i]
99 else:
100 result += ': ' + self.ui.label('-' + lines[i],
101 'diff.deleted')
102
103 if result[-1] != '\n':
104 result += '\n'
105
106 self.ui.write(result)
107
108 @util.propertycache
109 def _hexfunc(self):
110 if self.ui.debugflag or self.opts.get('long_hash'):
111 return node.hex
112 else:
113 return node.short
114
115 def end(self):
116 pass
117
118 class jsonformatter(defaultformatter):
119 def __init__(self, ui, repo, opts):
120 super(jsonformatter, self).__init__(ui, repo, opts)
121 self.ui.write('[')
122 self.needcomma = False
123
124 def write(self, annotatedresult, lines=None, existinglines=None):
125 if annotatedresult:
126 self._writecomma()
127
128 pieces = [(name, map(f, annotatedresult))
129 for f, sep, name, enc in self.funcmap]
130 if lines is not None:
131 pieces.append(('line', lines))
132 pieces.sort()
133
134 seps = [','] * len(pieces[:-1]) + ['']
135
136 result = ''
137 lasti = len(annotatedresult) - 1
138 for i in pycompat.xrange(len(annotatedresult)):
139 result += '\n {\n'
140 for j, p in enumerate(pieces):
141 k, vs = p
142 result += (' "%s": %s%s\n'
143 % (k, templatefilters.json(vs[i], paranoid=False),
144 seps[j]))
145 result += ' }%s' % ('' if i == lasti else ',')
146 if lasti >= 0:
147 self.needcomma = True
148
149 self.ui.write(result)
150
151 def _writecomma(self):
152 if self.needcomma:
153 self.ui.write(',')
154 self.needcomma = False
155
156 @util.propertycache
157 def _hexfunc(self):
158 return node.hex
159
160 def end(self):
161 self.ui.write('\n]\n')