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