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