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