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