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