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