##// END OF EJS Templates
revsetbenchmarks: also display tag when printing a revision...
Pierre-Yves David -
r25546:59cdf631 default
parent child Browse files
Show More
@@ -1,304 +1,305
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 '{if(tags, " ({tags})")} '
93 94 '{rev}:{node|short}: {desc|firstline}\n'])
94 95
95 96 def idxwidth(nbidx):
96 97 """return the max width of number used for index
97 98
98 99 This is similar to log10(nbidx), but we use custom code here
99 100 because we start with zero and we'd rather not deal with all the
100 101 extra rounding business that log10 would imply.
101 102 """
102 103 nbidx -= 1 # starts at 0
103 104 idxwidth = 0
104 105 while nbidx:
105 106 idxwidth += 1
106 107 nbidx //= 10
107 108 if not idxwidth:
108 109 idxwidth = 1
109 110 return idxwidth
110 111
111 112 def getfactor(main, other, field, sensitivity=0.05):
112 113 """return the relative factor between values for 'field' in main and other
113 114
114 115 Return None if the factor is insignicant (less than <sensitivity>
115 116 variation)."""
116 117 factor = 1
117 118 if main is not None:
118 119 factor = other[field] / main[field]
119 120 low, high = 1 - sensitivity, 1 + sensitivity
120 121 if (low < factor < high):
121 122 return None
122 123 return factor
123 124
124 125 def formatfactor(factor):
125 126 """format a factor into a 4 char string
126 127
127 128 22%
128 129 156%
129 130 x2.4
130 131 x23
131 132 x789
132 133 x1e4
133 134 x5x7
134 135
135 136 """
136 137 if factor is None:
137 138 return ' '
138 139 elif factor < 2:
139 140 return '%3i%%' % (factor * 100)
140 141 elif factor < 10:
141 142 return 'x%3.1f' % factor
142 143 elif factor < 1000:
143 144 return '%4s' % ('x%i' % factor)
144 145 else:
145 146 order = int(math.log(factor)) + 1
146 147 while 1 < math.log(factor):
147 148 factor //= 0
148 149 return 'x%ix%i' % (factor, order)
149 150
150 151 def formattiming(value):
151 152 """format a value to strictly 8 char, dropping some precision if needed"""
152 153 if value < 10**7:
153 154 return ('%.6f' % value)[:8]
154 155 else:
155 156 # value is HUGE very unlikely to happen (4+ month run)
156 157 return '%i' % value
157 158
158 159 _marker = object()
159 160 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
160 161 """print a line of result to stdout"""
161 162 mask = '%%0%ii) %%s' % idxwidth(maxidx)
162 163 out = []
163 164 for var in variants:
164 165 out.append(formattiming(data[var]['wall']))
165 166 if reference is not _marker:
166 167 factor = None
167 168 if reference is not None:
168 169 factor = getfactor(reference[var], data[var], 'wall')
169 170 out.append(formatfactor(factor))
170 171 if verbose:
171 172 out.append(formattiming(data[var]['comb']))
172 173 out.append(formattiming(data[var]['user']))
173 174 out.append(formattiming(data[var]['sys']))
174 175 out.append('%6d' % data[var]['count'])
175 176 print mask % (idx, ' '.join(out))
176 177
177 178 def printheader(variants, maxidx, verbose=False, relative=False):
178 179 header = [' ' * (idxwidth(maxidx) + 1)]
179 180 for var in variants:
180 181 if not var:
181 182 var = 'iter'
182 183 if 8 < len(var):
183 184 var = var[:3] + '..' + var[-3:]
184 185 header.append('%-8s' % var)
185 186 if relative:
186 187 header.append(' ')
187 188 if verbose:
188 189 header.append('%-8s' % 'comb')
189 190 header.append('%-8s' % 'user')
190 191 header.append('%-8s' % 'sys')
191 192 header.append('%6s' % 'count')
192 193 print ' '.join(header)
193 194
194 195 def getrevs(spec):
195 196 """get the list of rev matched by a revset"""
196 197 try:
197 198 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
198 199 except CalledProcessError, exc:
199 200 print >> sys.stderr, "abort, can't get revision from %s" % spec
200 201 sys.exit(exc.returncode)
201 202 return [r for r in out.split() if r]
202 203
203 204
204 205 def applyvariants(revset, variant):
205 206 if variant == 'plain':
206 207 return revset
207 208 for var in variant.split('+'):
208 209 revset = '%s(%s)' % (var, revset)
209 210 return revset
210 211
211 212
212 213 parser = OptionParser(usage="usage: %prog [options] <revs>")
213 214 parser.add_option("-f", "--file",
214 215 help="read revset from FILE (stdin if omitted)",
215 216 metavar="FILE")
216 217 parser.add_option("-R", "--repo",
217 218 help="run benchmark on REPO", metavar="REPO")
218 219
219 220 parser.add_option("-v", "--verbose",
220 221 action='store_true',
221 222 help="display all timing data (not just best total time)")
222 223
223 224 parser.add_option("", "--variants",
224 225 default=','.join(DEFAULTVARIANTS),
225 226 help="comma separated list of variant to test "
226 227 "(eg: plain,min,sorted) (plain = no modification)")
227 228
228 229 (options, args) = parser.parse_args()
229 230
230 231 if not args:
231 232 parser.print_help()
232 233 sys.exit(255)
233 234
234 235 # the directory where both this script and the perf.py extension live.
235 236 contribdir = os.path.dirname(__file__)
236 237
237 238 revsetsfile = sys.stdin
238 239 if options.file:
239 240 revsetsfile = open(options.file)
240 241
241 242 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
242 243
243 244 print "Revsets to benchmark"
244 245 print "----------------------------"
245 246
246 247 for idx, rset in enumerate(revsets):
247 248 print "%i) %s" % (idx, rset)
248 249
249 250 print "----------------------------"
250 251 print
251 252
252 253 revs = []
253 254 for a in args:
254 255 revs.extend(getrevs(a))
255 256
256 257 variants = options.variants.split(',')
257 258
258 259 results = []
259 260 for r in revs:
260 261 print "----------------------------"
261 262 printrevision(r)
262 263 print "----------------------------"
263 264 update(r)
264 265 res = []
265 266 results.append(res)
266 267 printheader(variants, len(revsets), verbose=options.verbose)
267 268 for idx, rset in enumerate(revsets):
268 269 varres = {}
269 270 for var in variants:
270 271 varrset = applyvariants(rset, var)
271 272 data = perf(varrset, target=options.repo)
272 273 varres[var] = data
273 274 res.append(varres)
274 275 printresult(variants, idx, varres, len(revsets),
275 276 verbose=options.verbose)
276 277 sys.stdout.flush()
277 278 print "----------------------------"
278 279
279 280
280 281 print """
281 282
282 283 Result by revset
283 284 ================
284 285 """
285 286
286 287 print 'Revision:'
287 288 for idx, rev in enumerate(revs):
288 289 sys.stdout.write('%i) ' % idx)
289 290 sys.stdout.flush()
290 291 printrevision(rev)
291 292
292 293 print
293 294 print
294 295
295 296 for ridx, rset in enumerate(revsets):
296 297
297 298 print "revset #%i: %s" % (ridx, rset)
298 299 printheader(variants, len(results), verbose=options.verbose, relative=True)
299 300 ref = None
300 301 for idx, data in enumerate(results):
301 302 printresult(variants, idx, data[ridx], len(results),
302 303 verbose=options.verbose, reference=ref)
303 304 ref = data[ridx]
304 305 print
General Comments 0
You need to be logged in to leave comments. Login now