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