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