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