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