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