##// END OF EJS Templates
revsetbenchmarks: display even more compact timing result...
Pierre-Yves David -
r25541:80ce5bce default
parent child Browse files
Show More
@@ -1,290 +1,300
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']
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):
149 """format a value to strictly 8 char, dropping some precision if needed"""
150 if value < 10**7:
151 return ('%.6f' % value)[:8]
152 else:
153 # value is HUGE very unlikely to happen (4+ month run)
154 return '%i' % value
155
148 _marker = object()
156 _marker = object()
149 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
157 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
150 """print a line of result to stdout"""
158 """print a line of result to stdout"""
151 mask = '%%0%ii) %%s' % idxwidth(maxidx)
159 mask = '%%0%ii) %%s' % idxwidth(maxidx)
152 out = []
160 out = []
153 for var in variants:
161 for var in variants:
154 out.append('%10.6f' % data[var]['wall'])
162 out.append(formattiming(data[var]['wall']))
155 if reference is not _marker:
163 if reference is not _marker:
156 factor = None
164 factor = None
157 if reference is not None:
165 if reference is not None:
158 factor = getfactor(reference[var], data[var], 'wall')
166 factor = getfactor(reference[var], data[var], 'wall')
159 out.append(formatfactor(factor))
167 out.append(formatfactor(factor))
160 if verbose:
168 if verbose:
161 out.append('%10.6f' % data[var]['comb'])
169 out.append(formattiming(data[var]['comb']))
162 out.append('%10.6f' % data[var]['user'])
170 out.append(formattiming(data[var]['user']))
163 out.append('%10.6f' % data[var]['sys'])
171 out.append(formattiming(data[var]['sys']))
164 out.append('%6d' % data[var]['count'])
172 out.append('%6d' % data[var]['count'])
165 print mask % (idx, ' '.join(out))
173 print mask % (idx, ' '.join(out))
166
174
167 def printheader(variants, maxidx, verbose=False, relative=False):
175 def printheader(variants, maxidx, verbose=False, relative=False):
168 header = [' ' * (idxwidth(maxidx) + 1)]
176 header = [' ' * (idxwidth(maxidx) + 1)]
169 for var in variants:
177 for var in variants:
170 if not var:
178 if not var:
171 var = 'iter'
179 var = 'iter'
180 if 8 < len(var):
181 var = var[:3] + '..' + var[-3:]
172 header.append(' %-8s' % var)
182 header.append('%-8s' % var)
173 if relative:
183 if relative:
174 header.append(' ')
184 header.append(' ')
175 if verbose:
185 if verbose:
176 header.append(' %-8s' % 'comb')
186 header.append('%-8s' % 'comb')
177 header.append(' %-8s' % 'user')
187 header.append('%-8s' % 'user')
178 header.append(' %-8s' % 'sys')
188 header.append('%-8s' % 'sys')
179 header.append('%6s' % 'count')
189 header.append('%6s' % 'count')
180 print ' '.join(header)
190 print ' '.join(header)
181
191
182 def getrevs(spec):
192 def getrevs(spec):
183 """get the list of rev matched by a revset"""
193 """get the list of rev matched by a revset"""
184 try:
194 try:
185 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
195 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
186 except CalledProcessError, exc:
196 except CalledProcessError, exc:
187 print >> sys.stderr, "abort, can't get revision from %s" % spec
197 print >> sys.stderr, "abort, can't get revision from %s" % spec
188 sys.exit(exc.returncode)
198 sys.exit(exc.returncode)
189 return [r for r in out.split() if r]
199 return [r for r in out.split() if r]
190
200
191
201
192 def applyvariants(revset, variant):
202 def applyvariants(revset, variant):
193 if variant == 'plain':
203 if variant == 'plain':
194 return revset
204 return revset
195 return '%s(%s)' % (variant, revset)
205 return '%s(%s)' % (variant, revset)
196
206
197
207
198 parser = OptionParser(usage="usage: %prog [options] <revs>")
208 parser = OptionParser(usage="usage: %prog [options] <revs>")
199 parser.add_option("-f", "--file",
209 parser.add_option("-f", "--file",
200 help="read revset from FILE (stdin if omitted)",
210 help="read revset from FILE (stdin if omitted)",
201 metavar="FILE")
211 metavar="FILE")
202 parser.add_option("-R", "--repo",
212 parser.add_option("-R", "--repo",
203 help="run benchmark on REPO", metavar="REPO")
213 help="run benchmark on REPO", metavar="REPO")
204
214
205 parser.add_option("-v", "--verbose",
215 parser.add_option("-v", "--verbose",
206 action='store_true',
216 action='store_true',
207 help="display all timing data (not just best total time)")
217 help="display all timing data (not just best total time)")
208
218
209 parser.add_option("", "--variants",
219 parser.add_option("", "--variants",
210 default=','.join(DEFAULTVARIANTS),
220 default=','.join(DEFAULTVARIANTS),
211 help="comma separated list of variant to test "
221 help="comma separated list of variant to test "
212 "(eg: plain,min,sorted) (plain = no modification)")
222 "(eg: plain,min,sorted) (plain = no modification)")
213
223
214 (options, args) = parser.parse_args()
224 (options, args) = parser.parse_args()
215
225
216 if not args:
226 if not args:
217 parser.print_help()
227 parser.print_help()
218 sys.exit(255)
228 sys.exit(255)
219
229
220 # 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.
221 contribdir = os.path.dirname(__file__)
231 contribdir = os.path.dirname(__file__)
222
232
223 revsetsfile = sys.stdin
233 revsetsfile = sys.stdin
224 if options.file:
234 if options.file:
225 revsetsfile = open(options.file)
235 revsetsfile = open(options.file)
226
236
227 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
237 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
228
238
229 print "Revsets to benchmark"
239 print "Revsets to benchmark"
230 print "----------------------------"
240 print "----------------------------"
231
241
232 for idx, rset in enumerate(revsets):
242 for idx, rset in enumerate(revsets):
233 print "%i) %s" % (idx, rset)
243 print "%i) %s" % (idx, rset)
234
244
235 print "----------------------------"
245 print "----------------------------"
236 print
246 print
237
247
238 revs = []
248 revs = []
239 for a in args:
249 for a in args:
240 revs.extend(getrevs(a))
250 revs.extend(getrevs(a))
241
251
242 variants = options.variants.split(',')
252 variants = options.variants.split(',')
243
253
244 results = []
254 results = []
245 for r in revs:
255 for r in revs:
246 print "----------------------------"
256 print "----------------------------"
247 printrevision(r)
257 printrevision(r)
248 print "----------------------------"
258 print "----------------------------"
249 update(r)
259 update(r)
250 res = []
260 res = []
251 results.append(res)
261 results.append(res)
252 printheader(variants, len(revsets), verbose=options.verbose)
262 printheader(variants, len(revsets), verbose=options.verbose)
253 for idx, rset in enumerate(revsets):
263 for idx, rset in enumerate(revsets):
254 varres = {}
264 varres = {}
255 for var in variants:
265 for var in variants:
256 varrset = applyvariants(rset, var)
266 varrset = applyvariants(rset, var)
257 data = perf(varrset, target=options.repo)
267 data = perf(varrset, target=options.repo)
258 varres[var] = data
268 varres[var] = data
259 res.append(varres)
269 res.append(varres)
260 printresult(variants, idx, varres, len(revsets),
270 printresult(variants, idx, varres, len(revsets),
261 verbose=options.verbose)
271 verbose=options.verbose)
262 sys.stdout.flush()
272 sys.stdout.flush()
263 print "----------------------------"
273 print "----------------------------"
264
274
265
275
266 print """
276 print """
267
277
268 Result by revset
278 Result by revset
269 ================
279 ================
270 """
280 """
271
281
272 print 'Revision:'
282 print 'Revision:'
273 for idx, rev in enumerate(revs):
283 for idx, rev in enumerate(revs):
274 sys.stdout.write('%i) ' % idx)
284 sys.stdout.write('%i) ' % idx)
275 sys.stdout.flush()
285 sys.stdout.flush()
276 printrevision(rev)
286 printrevision(rev)
277
287
278 print
288 print
279 print
289 print
280
290
281 for ridx, rset in enumerate(revsets):
291 for ridx, rset in enumerate(revsets):
282
292
283 print "revset #%i: %s" % (ridx, rset)
293 print "revset #%i: %s" % (ridx, rset)
284 printheader(variants, len(results), verbose=options.verbose, relative=True)
294 printheader(variants, len(results), verbose=options.verbose, relative=True)
285 ref = None
295 ref = None
286 for idx, data in enumerate(results):
296 for idx, data in enumerate(results):
287 printresult(variants, idx, data[ridx], len(results),
297 printresult(variants, idx, data[ridx], len(results),
288 verbose=options.verbose, reference=ref)
298 verbose=options.verbose, reference=ref)
289 ref = data[ridx]
299 ref = data[ridx]
290 print
300 print
General Comments 0
You need to be logged in to leave comments. Login now