##// END OF EJS Templates
revsetbenchmarks: display relative change when meaningful...
Pierre-Yves David -
r25539:460922c9 default
parent child Browse files
Show More
@@ -1,214 +1,265
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 # Measure the performance of a list of revsets against multiple revisions
3 # Measure the performance of a list of revsets against multiple revisions
4 # defined by parameter. Checkout one by one and run perfrevset with every
4 # defined by parameter. Checkout one by one and run perfrevset with every
5 # revset in the list to benchmark its performance.
5 # revset in the list to benchmark its performance.
6 #
6 #
7 # You should run this from the root of your mercurial repository.
7 # You should run this from the root of your mercurial repository.
8 #
8 #
9 # call with --help for details
9 # call with --help for details
10
10
11 import sys
11 import sys
12 import os
12 import os
13 import re
13 import re
14 import math
14 from subprocess import check_call, Popen, CalledProcessError, STDOUT, PIPE
15 from subprocess import check_call, Popen, CalledProcessError, STDOUT, PIPE
15 # cannot use argparse, python 2.7 only
16 # cannot use argparse, python 2.7 only
16 from optparse import OptionParser
17 from optparse import OptionParser
17
18
18 def check_output(*args, **kwargs):
19 def check_output(*args, **kwargs):
19 kwargs.setdefault('stderr', PIPE)
20 kwargs.setdefault('stderr', PIPE)
20 kwargs.setdefault('stdout', PIPE)
21 kwargs.setdefault('stdout', PIPE)
21 proc = Popen(*args, **kwargs)
22 proc = Popen(*args, **kwargs)
22 output, error = proc.communicate()
23 output, error = proc.communicate()
23 if proc.returncode != 0:
24 if proc.returncode != 0:
24 raise CalledProcessError(proc.returncode, ' '.join(args[0]))
25 raise CalledProcessError(proc.returncode, ' '.join(args[0]))
25 return output
26 return output
26
27
27 def update(rev):
28 def update(rev):
28 """update the repo to a revision"""
29 """update the repo to a revision"""
29 try:
30 try:
30 check_call(['hg', 'update', '--quiet', '--check', str(rev)])
31 check_call(['hg', 'update', '--quiet', '--check', str(rev)])
31 except CalledProcessError, exc:
32 except CalledProcessError, exc:
32 print >> sys.stderr, 'update to revision %s failed, aborting' % rev
33 print >> sys.stderr, 'update to revision %s failed, aborting' % rev
33 sys.exit(exc.returncode)
34 sys.exit(exc.returncode)
34
35
35
36
36 def hg(cmd, repo=None):
37 def hg(cmd, repo=None):
37 """run a mercurial command
38 """run a mercurial command
38
39
39 <cmd> is the list of command + argument,
40 <cmd> is the list of command + argument,
40 <repo> is an optional repository path to run this command in."""
41 <repo> is an optional repository path to run this command in."""
41 fullcmd = ['./hg']
42 fullcmd = ['./hg']
42 if repo is not None:
43 if repo is not None:
43 fullcmd += ['-R', repo]
44 fullcmd += ['-R', repo]
44 fullcmd += ['--config',
45 fullcmd += ['--config',
45 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
46 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
46 fullcmd += cmd
47 fullcmd += cmd
47 return check_output(fullcmd, stderr=STDOUT)
48 return check_output(fullcmd, stderr=STDOUT)
48
49
49 def perf(revset, target=None):
50 def perf(revset, target=None):
50 """run benchmark for this very revset"""
51 """run benchmark for this very revset"""
51 try:
52 try:
52 output = hg(['perfrevset', revset], repo=target)
53 output = hg(['perfrevset', revset], repo=target)
53 return parseoutput(output)
54 return parseoutput(output)
54 except CalledProcessError, exc:
55 except CalledProcessError, exc:
55 print >> sys.stderr, 'abort: cannot run revset benchmark: %s' % exc.cmd
56 print >> sys.stderr, 'abort: cannot run revset benchmark: %s' % exc.cmd
56 if exc.output is None:
57 if exc.output is None:
57 print >> sys.stderr, '(no ouput)'
58 print >> sys.stderr, '(no ouput)'
58 else:
59 else:
59 print >> sys.stderr, exc.output
60 print >> sys.stderr, exc.output
60 sys.exit(exc.returncode)
61 sys.exit(exc.returncode)
61
62
62 outputre = re.compile(r'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
63 outputre = re.compile(r'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
63 'sys (\d+.\d+) \(best of (\d+)\)')
64 'sys (\d+.\d+) \(best of (\d+)\)')
64
65
65 def parseoutput(output):
66 def parseoutput(output):
66 """parse a textual output into a dict
67 """parse a textual output into a dict
67
68
68 We cannot just use json because we want to compare with old
69 We cannot just use json because we want to compare with old
69 versions of Mercurial that may not support json output.
70 versions of Mercurial that may not support json output.
70 """
71 """
71 match = outputre.search(output)
72 match = outputre.search(output)
72 if not match:
73 if not match:
73 print >> sys.stderr, 'abort: invalid output:'
74 print >> sys.stderr, 'abort: invalid output:'
74 print >> sys.stderr, output
75 print >> sys.stderr, output
75 sys.exit(1)
76 sys.exit(1)
76 return {'comb': float(match.group(2)),
77 return {'comb': float(match.group(2)),
77 'count': int(match.group(5)),
78 'count': int(match.group(5)),
78 'sys': float(match.group(3)),
79 'sys': float(match.group(3)),
79 'user': float(match.group(4)),
80 'user': float(match.group(4)),
80 'wall': float(match.group(1)),
81 'wall': float(match.group(1)),
81 }
82 }
82
83
83 def printrevision(rev):
84 def printrevision(rev):
84 """print data about a revision"""
85 """print data about a revision"""
85 sys.stdout.write("Revision ")
86 sys.stdout.write("Revision ")
86 sys.stdout.flush()
87 sys.stdout.flush()
87 check_call(['hg', 'log', '--rev', str(rev), '--template',
88 check_call(['hg', 'log', '--rev', str(rev), '--template',
88 '{rev}:{node|short}: {desc|firstline}\n'])
89 '{rev}:{node|short}: {desc|firstline}\n'])
89
90
90 def idxwidth(nbidx):
91 def idxwidth(nbidx):
91 """return the max width of number used for index
92 """return the max width of number used for index
92
93
93 This is similar to log10(nbidx), but we use custom code here
94 This is similar to log10(nbidx), but we use custom code here
94 because we start with zero and we'd rather not deal with all the
95 because we start with zero and we'd rather not deal with all the
95 extra rounding business that log10 would imply.
96 extra rounding business that log10 would imply.
96 """
97 """
97 nbidx -= 1 # starts at 0
98 nbidx -= 1 # starts at 0
98 idxwidth = 0
99 idxwidth = 0
99 while nbidx:
100 while nbidx:
100 idxwidth += 1
101 idxwidth += 1
101 nbidx //= 10
102 nbidx //= 10
102 if not idxwidth:
103 if not idxwidth:
103 idxwidth = 1
104 idxwidth = 1
104 return idxwidth
105 return idxwidth
105
106
106 def printresult(idx, data, maxidx, verbose=False):
107 def getfactor(main, other, field, sensitivity=0.05):
108 """return the relative factor between values for 'field' in main and other
109
110 Return None if the factor is insignicant (less than <sensitivity>
111 variation)."""
112 factor = 1
113 if main is not None:
114 factor = other[field] / main[field]
115 low, high = 1 - sensitivity, 1 + sensitivity
116 if (low < factor < high):
117 return None
118 return factor
119
120 def formatfactor(factor):
121 """format a factor into a 4 char string
122
123 22%
124 156%
125 x2.4
126 x23
127 x789
128 x1e4
129 x5x7
130
131 """
132 if factor is None:
133 return ' '
134 elif factor < 2:
135 return '%3i%%' % (factor * 100)
136 elif factor < 10:
137 return 'x%3.1f' % factor
138 elif factor < 1000:
139 return '%4s' % ('x%i' % factor)
140 else:
141 order = int(math.log(factor)) + 1
142 while 1 < math.log(factor):
143 factor //= 0
144 return 'x%ix%i' % (factor, order)
145
146 _marker = object()
147 def printresult(idx, data, maxidx, verbose=False, reference=_marker):
107 """print a line of result to stdout"""
148 """print a line of result to stdout"""
108 mask = '%%0%ii) %%s' % idxwidth(maxidx)
149 mask = '%%0%ii) %%s' % idxwidth(maxidx)
109 out = ['%10.6f' % data['wall']]
150 out = ['%10.6f' % data['wall']]
151 if reference is not _marker:
152 factor = None
153 if reference is not None:
154 factor = getfactor(reference, data, 'wall')
155 out.append(formatfactor(factor))
110 if verbose:
156 if verbose:
111 out.append('%10.6f' % data['comb'])
157 out.append('%10.6f' % data['comb'])
112 out.append('%10.6f' % data['user'])
158 out.append('%10.6f' % data['user'])
113 out.append('%10.6f' % data['sys'])
159 out.append('%10.6f' % data['sys'])
114 out.append('%6d' % data['count'])
160 out.append('%6d' % data['count'])
115 print mask % (idx, ' '.join(out))
161 print mask % (idx, ' '.join(out))
116
162
117 def printheader(maxidx, verbose=False):
163 def printheader(maxidx, verbose=False, relative=False):
118 header = [' ' * (idxwidth(maxidx) + 1),
164 header = [' ' * (idxwidth(maxidx) + 1),
119 ' %-8s' % 'time']
165 ' %-8s' % 'time']
166 if relative:
167 header.append(' ')
120 if verbose:
168 if verbose:
121 header.append(' %-8s' % 'comb')
169 header.append(' %-8s' % 'comb')
122 header.append(' %-8s' % 'user')
170 header.append(' %-8s' % 'user')
123 header.append(' %-8s' % 'sys')
171 header.append(' %-8s' % 'sys')
124 header.append('%6s' % 'count')
172 header.append('%6s' % 'count')
125 print ' '.join(header)
173 print ' '.join(header)
126
174
127 def getrevs(spec):
175 def getrevs(spec):
128 """get the list of rev matched by a revset"""
176 """get the list of rev matched by a revset"""
129 try:
177 try:
130 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
178 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
131 except CalledProcessError, exc:
179 except CalledProcessError, exc:
132 print >> sys.stderr, "abort, can't get revision from %s" % spec
180 print >> sys.stderr, "abort, can't get revision from %s" % spec
133 sys.exit(exc.returncode)
181 sys.exit(exc.returncode)
134 return [r for r in out.split() if r]
182 return [r for r in out.split() if r]
135
183
136
184
137 parser = OptionParser(usage="usage: %prog [options] <revs>")
185 parser = OptionParser(usage="usage: %prog [options] <revs>")
138 parser.add_option("-f", "--file",
186 parser.add_option("-f", "--file",
139 help="read revset from FILE (stdin if omitted)",
187 help="read revset from FILE (stdin if omitted)",
140 metavar="FILE")
188 metavar="FILE")
141 parser.add_option("-R", "--repo",
189 parser.add_option("-R", "--repo",
142 help="run benchmark on REPO", metavar="REPO")
190 help="run benchmark on REPO", metavar="REPO")
143
191
144 parser.add_option("-v", "--verbose",
192 parser.add_option("-v", "--verbose",
145 action='store_true',
193 action='store_true',
146 help="display all timing data (not just best total time)")
194 help="display all timing data (not just best total time)")
147
195
148 (options, args) = parser.parse_args()
196 (options, args) = parser.parse_args()
149
197
150 if not args:
198 if not args:
151 parser.print_help()
199 parser.print_help()
152 sys.exit(255)
200 sys.exit(255)
153
201
154 # the directory where both this script and the perf.py extension live.
202 # the directory where both this script and the perf.py extension live.
155 contribdir = os.path.dirname(__file__)
203 contribdir = os.path.dirname(__file__)
156
204
157 revsetsfile = sys.stdin
205 revsetsfile = sys.stdin
158 if options.file:
206 if options.file:
159 revsetsfile = open(options.file)
207 revsetsfile = open(options.file)
160
208
161 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
209 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
162
210
163 print "Revsets to benchmark"
211 print "Revsets to benchmark"
164 print "----------------------------"
212 print "----------------------------"
165
213
166 for idx, rset in enumerate(revsets):
214 for idx, rset in enumerate(revsets):
167 print "%i) %s" % (idx, rset)
215 print "%i) %s" % (idx, rset)
168
216
169 print "----------------------------"
217 print "----------------------------"
170 print
218 print
171
219
172 revs = []
220 revs = []
173 for a in args:
221 for a in args:
174 revs.extend(getrevs(a))
222 revs.extend(getrevs(a))
175
223
176 results = []
224 results = []
177 for r in revs:
225 for r in revs:
178 print "----------------------------"
226 print "----------------------------"
179 printrevision(r)
227 printrevision(r)
180 print "----------------------------"
228 print "----------------------------"
181 update(r)
229 update(r)
182 res = []
230 res = []
183 results.append(res)
231 results.append(res)
184 printheader(len(revsets), verbose=options.verbose)
232 printheader(len(revsets), verbose=options.verbose)
185 for idx, rset in enumerate(revsets):
233 for idx, rset in enumerate(revsets):
186 data = perf(rset, target=options.repo)
234 data = perf(rset, target=options.repo)
187 res.append(data)
235 res.append(data)
188 printresult(idx, data, len(revsets), verbose=options.verbose)
236 printresult(idx, data, len(revsets), verbose=options.verbose)
189 sys.stdout.flush()
237 sys.stdout.flush()
190 print "----------------------------"
238 print "----------------------------"
191
239
192
240
193 print """
241 print """
194
242
195 Result by revset
243 Result by revset
196 ================
244 ================
197 """
245 """
198
246
199 print 'Revision:'
247 print 'Revision:'
200 for idx, rev in enumerate(revs):
248 for idx, rev in enumerate(revs):
201 sys.stdout.write('%i) ' % idx)
249 sys.stdout.write('%i) ' % idx)
202 sys.stdout.flush()
250 sys.stdout.flush()
203 printrevision(rev)
251 printrevision(rev)
204
252
205 print
253 print
206 print
254 print
207
255
208 for ridx, rset in enumerate(revsets):
256 for ridx, rset in enumerate(revsets):
209
257
210 print "revset #%i: %s" % (ridx, rset)
258 print "revset #%i: %s" % (ridx, rset)
211 printheader(len(results), verbose=options.verbose)
259 printheader(len(results), verbose=options.verbose, relative=True)
260 ref = None
212 for idx, data in enumerate(results):
261 for idx, data in enumerate(results):
213 printresult(idx, data[ridx], len(results), verbose=options.verbose)
262 printresult(idx, data[ridx], len(results), verbose=options.verbose,
263 reference=ref)
264 ref = data[ridx]
214 print
265 print
General Comments 0
You need to be logged in to leave comments. Login now