##// END OF EJS Templates
revsetbenchmark: do not abort on failure to run a revset...
Pierre-Yves David -
r25646:57e7a060 default
parent child Browse files
Show More
@@ -1,312 +1,317 b''
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 import math
15 from subprocess import check_call, Popen, CalledProcessError, STDOUT, PIPE
15 from subprocess import check_call, Popen, CalledProcessError, STDOUT, PIPE
16 # cannot use argparse, python 2.7 only
16 # cannot use argparse, python 2.7 only
17 from optparse import OptionParser
17 from optparse import OptionParser
18
18
19 DEFAULTVARIANTS = ['plain', 'min', 'max', 'first', 'last',
19 DEFAULTVARIANTS = ['plain', 'min', 'max', 'first', 'last',
20 'reverse', 'reverse+first', 'reverse+last',
20 'reverse', 'reverse+first', 'reverse+last',
21 'sort', 'sort+first', 'sort+last']
21 'sort', 'sort+first', 'sort+last']
22
22
23 def check_output(*args, **kwargs):
23 def check_output(*args, **kwargs):
24 kwargs.setdefault('stderr', PIPE)
24 kwargs.setdefault('stderr', PIPE)
25 kwargs.setdefault('stdout', PIPE)
25 kwargs.setdefault('stdout', PIPE)
26 proc = Popen(*args, **kwargs)
26 proc = Popen(*args, **kwargs)
27 output, error = proc.communicate()
27 output, error = proc.communicate()
28 if proc.returncode != 0:
28 if proc.returncode != 0:
29 raise CalledProcessError(proc.returncode, ' '.join(args[0]))
29 raise CalledProcessError(proc.returncode, ' '.join(args[0]))
30 return output
30 return output
31
31
32 def update(rev):
32 def update(rev):
33 """update the repo to a revision"""
33 """update the repo to a revision"""
34 try:
34 try:
35 check_call(['hg', 'update', '--quiet', '--check', str(rev)])
35 check_call(['hg', 'update', '--quiet', '--check', str(rev)])
36 except CalledProcessError, exc:
36 except CalledProcessError, exc:
37 print >> sys.stderr, 'update to revision %s failed, aborting' % rev
37 print >> sys.stderr, 'update to revision %s failed, aborting' % rev
38 sys.exit(exc.returncode)
38 sys.exit(exc.returncode)
39
39
40
40
41 def hg(cmd, repo=None):
41 def hg(cmd, repo=None):
42 """run a mercurial command
42 """run a mercurial command
43
43
44 <cmd> is the list of command + argument,
44 <cmd> is the list of command + argument,
45 <repo> is an optional repository path to run this command in."""
45 <repo> is an optional repository path to run this command in."""
46 fullcmd = ['./hg']
46 fullcmd = ['./hg']
47 if repo is not None:
47 if repo is not None:
48 fullcmd += ['-R', repo]
48 fullcmd += ['-R', repo]
49 fullcmd += ['--config',
49 fullcmd += ['--config',
50 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
50 'extensions.perf=' + os.path.join(contribdir, 'perf.py')]
51 fullcmd += cmd
51 fullcmd += cmd
52 return check_output(fullcmd, stderr=STDOUT)
52 return check_output(fullcmd, stderr=STDOUT)
53
53
54 def perf(revset, target=None):
54 def perf(revset, target=None):
55 """run benchmark for this very revset"""
55 """run benchmark for this very revset"""
56 try:
56 try:
57 output = hg(['perfrevset', revset], repo=target)
57 output = hg(['perfrevset', revset], repo=target)
58 return parseoutput(output)
58 return parseoutput(output)
59 except CalledProcessError, exc:
59 except CalledProcessError, exc:
60 print >> sys.stderr, 'abort: cannot run revset benchmark: %s' % exc.cmd
60 print >> sys.stderr, 'abort: cannot run revset benchmark: %s' % exc.cmd
61 if exc.output is None:
61 if exc.output is None:
62 print >> sys.stderr, '(no ouput)'
62 print >> sys.stderr, '(no ouput)'
63 else:
63 else:
64 print >> sys.stderr, exc.output
64 print >> sys.stderr, exc.output
65 sys.exit(exc.returncode)
65 return None
66
66
67 outputre = re.compile(r'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
67 outputre = re.compile(r'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) '
68 'sys (\d+.\d+) \(best of (\d+)\)')
68 'sys (\d+.\d+) \(best of (\d+)\)')
69
69
70 def parseoutput(output):
70 def parseoutput(output):
71 """parse a textual output into a dict
71 """parse a textual output into a dict
72
72
73 We cannot just use json because we want to compare with old
73 We cannot just use json because we want to compare with old
74 versions of Mercurial that may not support json output.
74 versions of Mercurial that may not support json output.
75 """
75 """
76 match = outputre.search(output)
76 match = outputre.search(output)
77 if not match:
77 if not match:
78 print >> sys.stderr, 'abort: invalid output:'
78 print >> sys.stderr, 'abort: invalid output:'
79 print >> sys.stderr, output
79 print >> sys.stderr, output
80 sys.exit(1)
80 sys.exit(1)
81 return {'comb': float(match.group(2)),
81 return {'comb': float(match.group(2)),
82 'count': int(match.group(5)),
82 'count': int(match.group(5)),
83 'sys': float(match.group(3)),
83 'sys': float(match.group(3)),
84 'user': float(match.group(4)),
84 'user': float(match.group(4)),
85 'wall': float(match.group(1)),
85 'wall': float(match.group(1)),
86 }
86 }
87
87
88 def printrevision(rev):
88 def printrevision(rev):
89 """print data about a revision"""
89 """print data about a revision"""
90 sys.stdout.write("Revision ")
90 sys.stdout.write("Revision ")
91 sys.stdout.flush()
91 sys.stdout.flush()
92 check_call(['hg', 'log', '--rev', str(rev), '--template',
92 check_call(['hg', 'log', '--rev', str(rev), '--template',
93 '{if(tags, " ({tags})")} '
93 '{if(tags, " ({tags})")} '
94 '{rev}:{node|short}: {desc|firstline}\n'])
94 '{rev}:{node|short}: {desc|firstline}\n'])
95
95
96 def idxwidth(nbidx):
96 def idxwidth(nbidx):
97 """return the max width of number used for index
97 """return the max width of number used for index
98
98
99 This is similar to log10(nbidx), but we use custom code here
99 This is similar to log10(nbidx), but we use custom code here
100 because we start with zero and we'd rather not deal with all the
100 because we start with zero and we'd rather not deal with all the
101 extra rounding business that log10 would imply.
101 extra rounding business that log10 would imply.
102 """
102 """
103 nbidx -= 1 # starts at 0
103 nbidx -= 1 # starts at 0
104 idxwidth = 0
104 idxwidth = 0
105 while nbidx:
105 while nbidx:
106 idxwidth += 1
106 idxwidth += 1
107 nbidx //= 10
107 nbidx //= 10
108 if not idxwidth:
108 if not idxwidth:
109 idxwidth = 1
109 idxwidth = 1
110 return idxwidth
110 return idxwidth
111
111
112 def getfactor(main, other, field, sensitivity=0.05):
112 def getfactor(main, other, field, sensitivity=0.05):
113 """return the relative factor between values for 'field' in main and other
113 """return the relative factor between values for 'field' in main and other
114
114
115 Return None if the factor is insignicant (less than <sensitivity>
115 Return None if the factor is insignicant (less than <sensitivity>
116 variation)."""
116 variation)."""
117 factor = 1
117 factor = 1
118 if main is not None:
118 if main is not None:
119 factor = other[field] / main[field]
119 factor = other[field] / main[field]
120 low, high = 1 - sensitivity, 1 + sensitivity
120 low, high = 1 - sensitivity, 1 + sensitivity
121 if (low < factor < high):
121 if (low < factor < high):
122 return None
122 return None
123 return factor
123 return factor
124
124
125 def formatfactor(factor):
125 def formatfactor(factor):
126 """format a factor into a 4 char string
126 """format a factor into a 4 char string
127
127
128 22%
128 22%
129 156%
129 156%
130 x2.4
130 x2.4
131 x23
131 x23
132 x789
132 x789
133 x1e4
133 x1e4
134 x5x7
134 x5x7
135
135
136 """
136 """
137 if factor is None:
137 if factor is None:
138 return ' '
138 return ' '
139 elif factor < 2:
139 elif factor < 2:
140 return '%3i%%' % (factor * 100)
140 return '%3i%%' % (factor * 100)
141 elif factor < 10:
141 elif factor < 10:
142 return 'x%3.1f' % factor
142 return 'x%3.1f' % factor
143 elif factor < 1000:
143 elif factor < 1000:
144 return '%4s' % ('x%i' % factor)
144 return '%4s' % ('x%i' % factor)
145 else:
145 else:
146 order = int(math.log(factor)) + 1
146 order = int(math.log(factor)) + 1
147 while 1 < math.log(factor):
147 while 1 < math.log(factor):
148 factor //= 0
148 factor //= 0
149 return 'x%ix%i' % (factor, order)
149 return 'x%ix%i' % (factor, order)
150
150
151 def formattiming(value):
151 def formattiming(value):
152 """format a value to strictly 8 char, dropping some precision if needed"""
152 """format a value to strictly 8 char, dropping some precision if needed"""
153 if value < 10**7:
153 if value < 10**7:
154 return ('%.6f' % value)[:8]
154 return ('%.6f' % value)[:8]
155 else:
155 else:
156 # value is HUGE very unlikely to happen (4+ month run)
156 # value is HUGE very unlikely to happen (4+ month run)
157 return '%i' % value
157 return '%i' % value
158
158
159 _marker = object()
159 _marker = object()
160 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
160 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
161 """print a line of result to stdout"""
161 """print a line of result to stdout"""
162 mask = '%%0%ii) %%s' % idxwidth(maxidx)
162 mask = '%%0%ii) %%s' % idxwidth(maxidx)
163
163 out = []
164 out = []
164 for var in variants:
165 for var in variants:
166 if data[var] is None:
167 out.append('error ')
168 out.append(' ' * 4)
169 continue
165 out.append(formattiming(data[var]['wall']))
170 out.append(formattiming(data[var]['wall']))
166 if reference is not _marker:
171 if reference is not _marker:
167 factor = None
172 factor = None
168 if reference is not None:
173 if reference is not None:
169 factor = getfactor(reference[var], data[var], 'wall')
174 factor = getfactor(reference[var], data[var], 'wall')
170 out.append(formatfactor(factor))
175 out.append(formatfactor(factor))
171 if verbose:
176 if verbose:
172 out.append(formattiming(data[var]['comb']))
177 out.append(formattiming(data[var]['comb']))
173 out.append(formattiming(data[var]['user']))
178 out.append(formattiming(data[var]['user']))
174 out.append(formattiming(data[var]['sys']))
179 out.append(formattiming(data[var]['sys']))
175 out.append('%6d' % data[var]['count'])
180 out.append('%6d' % data[var]['count'])
176 print mask % (idx, ' '.join(out))
181 print mask % (idx, ' '.join(out))
177
182
178 def printheader(variants, maxidx, verbose=False, relative=False):
183 def printheader(variants, maxidx, verbose=False, relative=False):
179 header = [' ' * (idxwidth(maxidx) + 1)]
184 header = [' ' * (idxwidth(maxidx) + 1)]
180 for var in variants:
185 for var in variants:
181 if not var:
186 if not var:
182 var = 'iter'
187 var = 'iter'
183 if 8 < len(var):
188 if 8 < len(var):
184 var = var[:3] + '..' + var[-3:]
189 var = var[:3] + '..' + var[-3:]
185 header.append('%-8s' % var)
190 header.append('%-8s' % var)
186 if relative:
191 if relative:
187 header.append(' ')
192 header.append(' ')
188 if verbose:
193 if verbose:
189 header.append('%-8s' % 'comb')
194 header.append('%-8s' % 'comb')
190 header.append('%-8s' % 'user')
195 header.append('%-8s' % 'user')
191 header.append('%-8s' % 'sys')
196 header.append('%-8s' % 'sys')
192 header.append('%6s' % 'count')
197 header.append('%6s' % 'count')
193 print ' '.join(header)
198 print ' '.join(header)
194
199
195 def getrevs(spec):
200 def getrevs(spec):
196 """get the list of rev matched by a revset"""
201 """get the list of rev matched by a revset"""
197 try:
202 try:
198 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
203 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
199 except CalledProcessError, exc:
204 except CalledProcessError, exc:
200 print >> sys.stderr, "abort, can't get revision from %s" % spec
205 print >> sys.stderr, "abort, can't get revision from %s" % spec
201 sys.exit(exc.returncode)
206 sys.exit(exc.returncode)
202 return [r for r in out.split() if r]
207 return [r for r in out.split() if r]
203
208
204
209
205 def applyvariants(revset, variant):
210 def applyvariants(revset, variant):
206 if variant == 'plain':
211 if variant == 'plain':
207 return revset
212 return revset
208 for var in variant.split('+'):
213 for var in variant.split('+'):
209 revset = '%s(%s)' % (var, revset)
214 revset = '%s(%s)' % (var, revset)
210 return revset
215 return revset
211
216
212 helptext="""This script will run multiple variants of provided revsets using
217 helptext="""This script will run multiple variants of provided revsets using
213 different revisions in your mercurial repository. After the benchmark are run
218 different revisions in your mercurial repository. After the benchmark are run
214 summary output is provided. Use itto demonstrate speed improvements or pin
219 summary output is provided. Use itto demonstrate speed improvements or pin
215 point regressions. Revsets to run are specified in a file (or from stdin), one
220 point regressions. Revsets to run are specified in a file (or from stdin), one
216 revsets per line. Line starting with '#' will be ignored, allowing insertion of
221 revsets per line. Line starting with '#' will be ignored, allowing insertion of
217 comments."""
222 comments."""
218 parser = OptionParser(usage="usage: %prog [options] <revs>",
223 parser = OptionParser(usage="usage: %prog [options] <revs>",
219 description=helptext)
224 description=helptext)
220 parser.add_option("-f", "--file",
225 parser.add_option("-f", "--file",
221 help="read revset from FILE (stdin if omitted)",
226 help="read revset from FILE (stdin if omitted)",
222 metavar="FILE")
227 metavar="FILE")
223 parser.add_option("-R", "--repo",
228 parser.add_option("-R", "--repo",
224 help="run benchmark on REPO", metavar="REPO")
229 help="run benchmark on REPO", metavar="REPO")
225
230
226 parser.add_option("-v", "--verbose",
231 parser.add_option("-v", "--verbose",
227 action='store_true',
232 action='store_true',
228 help="display all timing data (not just best total time)")
233 help="display all timing data (not just best total time)")
229
234
230 parser.add_option("", "--variants",
235 parser.add_option("", "--variants",
231 default=','.join(DEFAULTVARIANTS),
236 default=','.join(DEFAULTVARIANTS),
232 help="comma separated list of variant to test "
237 help="comma separated list of variant to test "
233 "(eg: plain,min,sorted) (plain = no modification)")
238 "(eg: plain,min,sorted) (plain = no modification)")
234
239
235 (options, args) = parser.parse_args()
240 (options, args) = parser.parse_args()
236
241
237 if not args:
242 if not args:
238 parser.print_help()
243 parser.print_help()
239 sys.exit(255)
244 sys.exit(255)
240
245
241 # the directory where both this script and the perf.py extension live.
246 # the directory where both this script and the perf.py extension live.
242 contribdir = os.path.dirname(__file__)
247 contribdir = os.path.dirname(__file__)
243
248
244 revsetsfile = sys.stdin
249 revsetsfile = sys.stdin
245 if options.file:
250 if options.file:
246 revsetsfile = open(options.file)
251 revsetsfile = open(options.file)
247
252
248 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
253 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
249 revsets = [l for l in revsets if l]
254 revsets = [l for l in revsets if l]
250
255
251 print "Revsets to benchmark"
256 print "Revsets to benchmark"
252 print "----------------------------"
257 print "----------------------------"
253
258
254 for idx, rset in enumerate(revsets):
259 for idx, rset in enumerate(revsets):
255 print "%i) %s" % (idx, rset)
260 print "%i) %s" % (idx, rset)
256
261
257 print "----------------------------"
262 print "----------------------------"
258 print
263 print
259
264
260 revs = []
265 revs = []
261 for a in args:
266 for a in args:
262 revs.extend(getrevs(a))
267 revs.extend(getrevs(a))
263
268
264 variants = options.variants.split(',')
269 variants = options.variants.split(',')
265
270
266 results = []
271 results = []
267 for r in revs:
272 for r in revs:
268 print "----------------------------"
273 print "----------------------------"
269 printrevision(r)
274 printrevision(r)
270 print "----------------------------"
275 print "----------------------------"
271 update(r)
276 update(r)
272 res = []
277 res = []
273 results.append(res)
278 results.append(res)
274 printheader(variants, len(revsets), verbose=options.verbose)
279 printheader(variants, len(revsets), verbose=options.verbose)
275 for idx, rset in enumerate(revsets):
280 for idx, rset in enumerate(revsets):
276 varres = {}
281 varres = {}
277 for var in variants:
282 for var in variants:
278 varrset = applyvariants(rset, var)
283 varrset = applyvariants(rset, var)
279 data = perf(varrset, target=options.repo)
284 data = perf(varrset, target=options.repo)
280 varres[var] = data
285 varres[var] = data
281 res.append(varres)
286 res.append(varres)
282 printresult(variants, idx, varres, len(revsets),
287 printresult(variants, idx, varres, len(revsets),
283 verbose=options.verbose)
288 verbose=options.verbose)
284 sys.stdout.flush()
289 sys.stdout.flush()
285 print "----------------------------"
290 print "----------------------------"
286
291
287
292
288 print """
293 print """
289
294
290 Result by revset
295 Result by revset
291 ================
296 ================
292 """
297 """
293
298
294 print 'Revision:'
299 print 'Revision:'
295 for idx, rev in enumerate(revs):
300 for idx, rev in enumerate(revs):
296 sys.stdout.write('%i) ' % idx)
301 sys.stdout.write('%i) ' % idx)
297 sys.stdout.flush()
302 sys.stdout.flush()
298 printrevision(rev)
303 printrevision(rev)
299
304
300 print
305 print
301 print
306 print
302
307
303 for ridx, rset in enumerate(revsets):
308 for ridx, rset in enumerate(revsets):
304
309
305 print "revset #%i: %s" % (ridx, rset)
310 print "revset #%i: %s" % (ridx, rset)
306 printheader(variants, len(results), verbose=options.verbose, relative=True)
311 printheader(variants, len(results), verbose=options.verbose, relative=True)
307 ref = None
312 ref = None
308 for idx, data in enumerate(results):
313 for idx, data in enumerate(results):
309 printresult(variants, idx, data[ridx], len(results),
314 printresult(variants, idx, data[ridx], len(results),
310 verbose=options.verbose, reference=ref)
315 verbose=options.verbose, reference=ref)
311 ref = data[ridx]
316 ref = data[ridx]
312 print
317 print
General Comments 0
You need to be logged in to leave comments. Login now