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