##// END OF EJS Templates
revsetbenchmarks: support combining variants with "+"...
Pierre-Yves David -
r25543:36336cc8 default
parent child Browse files
Show More
@@ -1,300 +1,302
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', '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 return '%s(%s)' % (variant, revset)
205 for var in variant.split('+'):
206 revset = '%s(%s)' % (var, revset)
207 return revset
206 208
207 209
208 210 parser = OptionParser(usage="usage: %prog [options] <revs>")
209 211 parser.add_option("-f", "--file",
210 212 help="read revset from FILE (stdin if omitted)",
211 213 metavar="FILE")
212 214 parser.add_option("-R", "--repo",
213 215 help="run benchmark on REPO", metavar="REPO")
214 216
215 217 parser.add_option("-v", "--verbose",
216 218 action='store_true',
217 219 help="display all timing data (not just best total time)")
218 220
219 221 parser.add_option("", "--variants",
220 222 default=','.join(DEFAULTVARIANTS),
221 223 help="comma separated list of variant to test "
222 224 "(eg: plain,min,sorted) (plain = no modification)")
223 225
224 226 (options, args) = parser.parse_args()
225 227
226 228 if not args:
227 229 parser.print_help()
228 230 sys.exit(255)
229 231
230 232 # the directory where both this script and the perf.py extension live.
231 233 contribdir = os.path.dirname(__file__)
232 234
233 235 revsetsfile = sys.stdin
234 236 if options.file:
235 237 revsetsfile = open(options.file)
236 238
237 239 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
238 240
239 241 print "Revsets to benchmark"
240 242 print "----------------------------"
241 243
242 244 for idx, rset in enumerate(revsets):
243 245 print "%i) %s" % (idx, rset)
244 246
245 247 print "----------------------------"
246 248 print
247 249
248 250 revs = []
249 251 for a in args:
250 252 revs.extend(getrevs(a))
251 253
252 254 variants = options.variants.split(',')
253 255
254 256 results = []
255 257 for r in revs:
256 258 print "----------------------------"
257 259 printrevision(r)
258 260 print "----------------------------"
259 261 update(r)
260 262 res = []
261 263 results.append(res)
262 264 printheader(variants, len(revsets), verbose=options.verbose)
263 265 for idx, rset in enumerate(revsets):
264 266 varres = {}
265 267 for var in variants:
266 268 varrset = applyvariants(rset, var)
267 269 data = perf(varrset, target=options.repo)
268 270 varres[var] = data
269 271 res.append(varres)
270 272 printresult(variants, idx, varres, len(revsets),
271 273 verbose=options.verbose)
272 274 sys.stdout.flush()
273 275 print "----------------------------"
274 276
275 277
276 278 print """
277 279
278 280 Result by revset
279 281 ================
280 282 """
281 283
282 284 print 'Revision:'
283 285 for idx, rev in enumerate(revs):
284 286 sys.stdout.write('%i) ' % idx)
285 287 sys.stdout.flush()
286 288 printrevision(rev)
287 289
288 290 print
289 291 print
290 292
291 293 for ridx, rset in enumerate(revsets):
292 294
293 295 print "revset #%i: %s" % (ridx, rset)
294 296 printheader(variants, len(results), verbose=options.verbose, relative=True)
295 297 ref = None
296 298 for idx, data in enumerate(results):
297 299 printresult(variants, idx, data[ridx], len(results),
298 300 verbose=options.verbose, reference=ref)
299 301 ref = data[ridx]
300 302 print
General Comments 0
You need to be logged in to leave comments. Login now