##// END OF EJS Templates
revsetbenchmarks: add main documention for the script...
Pierre-Yves David -
r25607:ddb2a648 default
parent child Browse files
Show More
@@ -1,305 +1,311 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 65 sys.exit(exc.returncode)
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 out = []
164 164 for var in variants:
165 165 out.append(formattiming(data[var]['wall']))
166 166 if reference is not _marker:
167 167 factor = None
168 168 if reference is not None:
169 169 factor = getfactor(reference[var], data[var], 'wall')
170 170 out.append(formatfactor(factor))
171 171 if verbose:
172 172 out.append(formattiming(data[var]['comb']))
173 173 out.append(formattiming(data[var]['user']))
174 174 out.append(formattiming(data[var]['sys']))
175 175 out.append('%6d' % data[var]['count'])
176 176 print mask % (idx, ' '.join(out))
177 177
178 178 def printheader(variants, maxidx, verbose=False, relative=False):
179 179 header = [' ' * (idxwidth(maxidx) + 1)]
180 180 for var in variants:
181 181 if not var:
182 182 var = 'iter'
183 183 if 8 < len(var):
184 184 var = var[:3] + '..' + var[-3:]
185 185 header.append('%-8s' % var)
186 186 if relative:
187 187 header.append(' ')
188 188 if verbose:
189 189 header.append('%-8s' % 'comb')
190 190 header.append('%-8s' % 'user')
191 191 header.append('%-8s' % 'sys')
192 192 header.append('%6s' % 'count')
193 193 print ' '.join(header)
194 194
195 195 def getrevs(spec):
196 196 """get the list of rev matched by a revset"""
197 197 try:
198 198 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
199 199 except CalledProcessError, exc:
200 200 print >> sys.stderr, "abort, can't get revision from %s" % spec
201 201 sys.exit(exc.returncode)
202 202 return [r for r in out.split() if r]
203 203
204 204
205 205 def applyvariants(revset, variant):
206 206 if variant == 'plain':
207 207 return revset
208 208 for var in variant.split('+'):
209 209 revset = '%s(%s)' % (var, revset)
210 210 return revset
211 211
212
213 parser = OptionParser(usage="usage: %prog [options] <revs>")
212 helptext="""This script will run multiple variants of provided revsets using
213 different revisions in your mercurial repository. After the benchmark are run
214 summary output is provided. Use itto demonstrate speed improvements or pin
215 point regressions. Revsets to run are specified in a file (or from stdin), one
216 revsets per line. Line starting with '#' will be ignored, allowing insertion of
217 comments."""
218 parser = OptionParser(usage="usage: %prog [options] <revs>",
219 description=helptext)
214 220 parser.add_option("-f", "--file",
215 221 help="read revset from FILE (stdin if omitted)",
216 222 metavar="FILE")
217 223 parser.add_option("-R", "--repo",
218 224 help="run benchmark on REPO", metavar="REPO")
219 225
220 226 parser.add_option("-v", "--verbose",
221 227 action='store_true',
222 228 help="display all timing data (not just best total time)")
223 229
224 230 parser.add_option("", "--variants",
225 231 default=','.join(DEFAULTVARIANTS),
226 232 help="comma separated list of variant to test "
227 233 "(eg: plain,min,sorted) (plain = no modification)")
228 234
229 235 (options, args) = parser.parse_args()
230 236
231 237 if not args:
232 238 parser.print_help()
233 239 sys.exit(255)
234 240
235 241 # the directory where both this script and the perf.py extension live.
236 242 contribdir = os.path.dirname(__file__)
237 243
238 244 revsetsfile = sys.stdin
239 245 if options.file:
240 246 revsetsfile = open(options.file)
241 247
242 248 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
243 249
244 250 print "Revsets to benchmark"
245 251 print "----------------------------"
246 252
247 253 for idx, rset in enumerate(revsets):
248 254 print "%i) %s" % (idx, rset)
249 255
250 256 print "----------------------------"
251 257 print
252 258
253 259 revs = []
254 260 for a in args:
255 261 revs.extend(getrevs(a))
256 262
257 263 variants = options.variants.split(',')
258 264
259 265 results = []
260 266 for r in revs:
261 267 print "----------------------------"
262 268 printrevision(r)
263 269 print "----------------------------"
264 270 update(r)
265 271 res = []
266 272 results.append(res)
267 273 printheader(variants, len(revsets), verbose=options.verbose)
268 274 for idx, rset in enumerate(revsets):
269 275 varres = {}
270 276 for var in variants:
271 277 varrset = applyvariants(rset, var)
272 278 data = perf(varrset, target=options.repo)
273 279 varres[var] = data
274 280 res.append(varres)
275 281 printresult(variants, idx, varres, len(revsets),
276 282 verbose=options.verbose)
277 283 sys.stdout.flush()
278 284 print "----------------------------"
279 285
280 286
281 287 print """
282 288
283 289 Result by revset
284 290 ================
285 291 """
286 292
287 293 print 'Revision:'
288 294 for idx, rev in enumerate(revs):
289 295 sys.stdout.write('%i) ' % idx)
290 296 sys.stdout.flush()
291 297 printrevision(rev)
292 298
293 299 print
294 300 print
295 301
296 302 for ridx, rset in enumerate(revsets):
297 303
298 304 print "revset #%i: %s" % (ridx, rset)
299 305 printheader(variants, len(results), verbose=options.verbose, relative=True)
300 306 ref = None
301 307 for idx, data in enumerate(results):
302 308 printresult(variants, idx, data[ridx], len(results),
303 309 verbose=options.verbose, reference=ref)
304 310 ref = data[ridx]
305 311 print
General Comments 0
You need to be logged in to leave comments. Login now