##// END OF EJS Templates
revsetbenchmarks: support combining variants with "+"...
Pierre-Yves David -
r25543:36336cc8 default
parent child Browse files
Show More
@@ -1,300 +1,302
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', 'reverse', 'sort']
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 for var in variant.split('+'):
206 revset = '%s(%s)' % (var, revset)
207 return revset
206
208
207
209
208 parser = OptionParser(usage="usage: %prog [options] <revs>")
210 parser = OptionParser(usage="usage: %prog [options] <revs>")
209 parser.add_option("-f", "--file",
211 parser.add_option("-f", "--file",
210 help="read revset from FILE (stdin if omitted)",
212 help="read revset from FILE (stdin if omitted)",
211 metavar="FILE")
213 metavar="FILE")
212 parser.add_option("-R", "--repo",
214 parser.add_option("-R", "--repo",
213 help="run benchmark on REPO", metavar="REPO")
215 help="run benchmark on REPO", metavar="REPO")
214
216
215 parser.add_option("-v", "--verbose",
217 parser.add_option("-v", "--verbose",
216 action='store_true',
218 action='store_true',
217 help="display all timing data (not just best total time)")
219 help="display all timing data (not just best total time)")
218
220
219 parser.add_option("", "--variants",
221 parser.add_option("", "--variants",
220 default=','.join(DEFAULTVARIANTS),
222 default=','.join(DEFAULTVARIANTS),
221 help="comma separated list of variant to test "
223 help="comma separated list of variant to test "
222 "(eg: plain,min,sorted) (plain = no modification)")
224 "(eg: plain,min,sorted) (plain = no modification)")
223
225
224 (options, args) = parser.parse_args()
226 (options, args) = parser.parse_args()
225
227
226 if not args:
228 if not args:
227 parser.print_help()
229 parser.print_help()
228 sys.exit(255)
230 sys.exit(255)
229
231
230 # the directory where both this script and the perf.py extension live.
232 # the directory where both this script and the perf.py extension live.
231 contribdir = os.path.dirname(__file__)
233 contribdir = os.path.dirname(__file__)
232
234
233 revsetsfile = sys.stdin
235 revsetsfile = sys.stdin
234 if options.file:
236 if options.file:
235 revsetsfile = open(options.file)
237 revsetsfile = open(options.file)
236
238
237 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
239 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
238
240
239 print "Revsets to benchmark"
241 print "Revsets to benchmark"
240 print "----------------------------"
242 print "----------------------------"
241
243
242 for idx, rset in enumerate(revsets):
244 for idx, rset in enumerate(revsets):
243 print "%i) %s" % (idx, rset)
245 print "%i) %s" % (idx, rset)
244
246
245 print "----------------------------"
247 print "----------------------------"
246 print
248 print
247
249
248 revs = []
250 revs = []
249 for a in args:
251 for a in args:
250 revs.extend(getrevs(a))
252 revs.extend(getrevs(a))
251
253
252 variants = options.variants.split(',')
254 variants = options.variants.split(',')
253
255
254 results = []
256 results = []
255 for r in revs:
257 for r in revs:
256 print "----------------------------"
258 print "----------------------------"
257 printrevision(r)
259 printrevision(r)
258 print "----------------------------"
260 print "----------------------------"
259 update(r)
261 update(r)
260 res = []
262 res = []
261 results.append(res)
263 results.append(res)
262 printheader(variants, len(revsets), verbose=options.verbose)
264 printheader(variants, len(revsets), verbose=options.verbose)
263 for idx, rset in enumerate(revsets):
265 for idx, rset in enumerate(revsets):
264 varres = {}
266 varres = {}
265 for var in variants:
267 for var in variants:
266 varrset = applyvariants(rset, var)
268 varrset = applyvariants(rset, var)
267 data = perf(varrset, target=options.repo)
269 data = perf(varrset, target=options.repo)
268 varres[var] = data
270 varres[var] = data
269 res.append(varres)
271 res.append(varres)
270 printresult(variants, idx, varres, len(revsets),
272 printresult(variants, idx, varres, len(revsets),
271 verbose=options.verbose)
273 verbose=options.verbose)
272 sys.stdout.flush()
274 sys.stdout.flush()
273 print "----------------------------"
275 print "----------------------------"
274
276
275
277
276 print """
278 print """
277
279
278 Result by revset
280 Result by revset
279 ================
281 ================
280 """
282 """
281
283
282 print 'Revision:'
284 print 'Revision:'
283 for idx, rev in enumerate(revs):
285 for idx, rev in enumerate(revs):
284 sys.stdout.write('%i) ' % idx)
286 sys.stdout.write('%i) ' % idx)
285 sys.stdout.flush()
287 sys.stdout.flush()
286 printrevision(rev)
288 printrevision(rev)
287
289
288 print
290 print
289 print
291 print
290
292
291 for ridx, rset in enumerate(revsets):
293 for ridx, rset in enumerate(revsets):
292
294
293 print "revset #%i: %s" % (ridx, rset)
295 print "revset #%i: %s" % (ridx, rset)
294 printheader(variants, len(results), verbose=options.verbose, relative=True)
296 printheader(variants, len(results), verbose=options.verbose, relative=True)
295 ref = None
297 ref = None
296 for idx, data in enumerate(results):
298 for idx, data in enumerate(results):
297 printresult(variants, idx, data[ridx], len(results),
299 printresult(variants, idx, data[ridx], len(results),
298 verbose=options.verbose, reference=ref)
300 verbose=options.verbose, reference=ref)
299 ref = data[ridx]
301 ref = data[ridx]
300 print
302 print
General Comments 0
You need to be logged in to leave comments. Login now