##// END OF EJS Templates
revsetbenchmarks: display even more compact timing result...
Pierre-Yves David -
r25541:80ce5bce default
parent child Browse files
Show More
@@ -1,290 +1,300
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']
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 def formattiming(value):
149 """format a value to strictly 8 char, dropping some precision if needed"""
150 if value < 10**7:
151 return ('%.6f' % value)[:8]
152 else:
153 # value is HUGE very unlikely to happen (4+ month run)
154 return '%i' % value
155
148 156 _marker = object()
149 157 def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker):
150 158 """print a line of result to stdout"""
151 159 mask = '%%0%ii) %%s' % idxwidth(maxidx)
152 160 out = []
153 161 for var in variants:
154 out.append('%10.6f' % data[var]['wall'])
162 out.append(formattiming(data[var]['wall']))
155 163 if reference is not _marker:
156 164 factor = None
157 165 if reference is not None:
158 166 factor = getfactor(reference[var], data[var], 'wall')
159 167 out.append(formatfactor(factor))
160 168 if verbose:
161 out.append('%10.6f' % data[var]['comb'])
162 out.append('%10.6f' % data[var]['user'])
163 out.append('%10.6f' % data[var]['sys'])
169 out.append(formattiming(data[var]['comb']))
170 out.append(formattiming(data[var]['user']))
171 out.append(formattiming(data[var]['sys']))
164 172 out.append('%6d' % data[var]['count'])
165 173 print mask % (idx, ' '.join(out))
166 174
167 175 def printheader(variants, maxidx, verbose=False, relative=False):
168 176 header = [' ' * (idxwidth(maxidx) + 1)]
169 177 for var in variants:
170 178 if not var:
171 179 var = 'iter'
180 if 8 < len(var):
181 var = var[:3] + '..' + var[-3:]
172 182 header.append(' %-8s' % var)
173 183 if relative:
174 184 header.append(' ')
175 185 if verbose:
176 186 header.append(' %-8s' % 'comb')
177 187 header.append(' %-8s' % 'user')
178 188 header.append(' %-8s' % 'sys')
179 189 header.append('%6s' % 'count')
180 190 print ' '.join(header)
181 191
182 192 def getrevs(spec):
183 193 """get the list of rev matched by a revset"""
184 194 try:
185 195 out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec])
186 196 except CalledProcessError, exc:
187 197 print >> sys.stderr, "abort, can't get revision from %s" % spec
188 198 sys.exit(exc.returncode)
189 199 return [r for r in out.split() if r]
190 200
191 201
192 202 def applyvariants(revset, variant):
193 203 if variant == 'plain':
194 204 return revset
195 205 return '%s(%s)' % (variant, revset)
196 206
197 207
198 208 parser = OptionParser(usage="usage: %prog [options] <revs>")
199 209 parser.add_option("-f", "--file",
200 210 help="read revset from FILE (stdin if omitted)",
201 211 metavar="FILE")
202 212 parser.add_option("-R", "--repo",
203 213 help="run benchmark on REPO", metavar="REPO")
204 214
205 215 parser.add_option("-v", "--verbose",
206 216 action='store_true',
207 217 help="display all timing data (not just best total time)")
208 218
209 219 parser.add_option("", "--variants",
210 220 default=','.join(DEFAULTVARIANTS),
211 221 help="comma separated list of variant to test "
212 222 "(eg: plain,min,sorted) (plain = no modification)")
213 223
214 224 (options, args) = parser.parse_args()
215 225
216 226 if not args:
217 227 parser.print_help()
218 228 sys.exit(255)
219 229
220 230 # the directory where both this script and the perf.py extension live.
221 231 contribdir = os.path.dirname(__file__)
222 232
223 233 revsetsfile = sys.stdin
224 234 if options.file:
225 235 revsetsfile = open(options.file)
226 236
227 237 revsets = [l.strip() for l in revsetsfile if not l.startswith('#')]
228 238
229 239 print "Revsets to benchmark"
230 240 print "----------------------------"
231 241
232 242 for idx, rset in enumerate(revsets):
233 243 print "%i) %s" % (idx, rset)
234 244
235 245 print "----------------------------"
236 246 print
237 247
238 248 revs = []
239 249 for a in args:
240 250 revs.extend(getrevs(a))
241 251
242 252 variants = options.variants.split(',')
243 253
244 254 results = []
245 255 for r in revs:
246 256 print "----------------------------"
247 257 printrevision(r)
248 258 print "----------------------------"
249 259 update(r)
250 260 res = []
251 261 results.append(res)
252 262 printheader(variants, len(revsets), verbose=options.verbose)
253 263 for idx, rset in enumerate(revsets):
254 264 varres = {}
255 265 for var in variants:
256 266 varrset = applyvariants(rset, var)
257 267 data = perf(varrset, target=options.repo)
258 268 varres[var] = data
259 269 res.append(varres)
260 270 printresult(variants, idx, varres, len(revsets),
261 271 verbose=options.verbose)
262 272 sys.stdout.flush()
263 273 print "----------------------------"
264 274
265 275
266 276 print """
267 277
268 278 Result by revset
269 279 ================
270 280 """
271 281
272 282 print 'Revision:'
273 283 for idx, rev in enumerate(revs):
274 284 sys.stdout.write('%i) ' % idx)
275 285 sys.stdout.flush()
276 286 printrevision(rev)
277 287
278 288 print
279 289 print
280 290
281 291 for ridx, rset in enumerate(revsets):
282 292
283 293 print "revset #%i: %s" % (ridx, rset)
284 294 printheader(variants, len(results), verbose=options.verbose, relative=True)
285 295 ref = None
286 296 for idx, data in enumerate(results):
287 297 printresult(variants, idx, data[ridx], len(results),
288 298 verbose=options.verbose, reference=ref)
289 299 ref = data[ridx]
290 300 print
General Comments 0
You need to be logged in to leave comments. Login now