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