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