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