revsetbenchmarks.py
388 lines
| 10.2 KiB
| text/x-python
|
PythonLexer
/ contrib / revsetbenchmarks.py
Gregory Szorc
|
r46434 | #!/usr/bin/env python3 | ||
Pierre-Yves David
|
r20848 | |||
# Measure the performance of a list of revsets against multiple revisions | ||||
# defined by parameter. Checkout one by one and run perfrevset with every | ||||
# revset in the list to benchmark its performance. | ||||
# | ||||
Pierre-Yves David
|
r25535 | # You should run this from the root of your mercurial repository. | ||
Pierre-Yves David
|
r20848 | # | ||
Pierre-Yves David
|
r25535 | # call with --help for details | ||
Pierre-Yves David
|
r20848 | |||
Pulkit Goyal
|
r28564 | import math | ||
Yuya Nishihara
|
r29210 | import optparse # cannot use argparse, python 2.7 only | ||
Pierre-Yves David
|
r21548 | import os | ||
Pierre-Yves David
|
r25530 | import re | ||
Yuya Nishihara
|
r29210 | import subprocess | ||
Pulkit Goyal
|
r28564 | import sys | ||
Pierre-Yves David
|
r21287 | |||
Augie Fackler
|
r43346 | DEFAULTVARIANTS = [ | ||
'plain', | ||||
'min', | ||||
'max', | ||||
'first', | ||||
'last', | ||||
'reverse', | ||||
'reverse+first', | ||||
'reverse+last', | ||||
'sort', | ||||
'sort+first', | ||||
'sort+last', | ||||
] | ||||
Pierre-Yves David
|
r25540 | |||
Durham Goode
|
r20893 | def check_output(*args, **kwargs): | ||
Yuya Nishihara
|
r29210 | kwargs.setdefault('stderr', subprocess.PIPE) | ||
kwargs.setdefault('stdout', subprocess.PIPE) | ||||
proc = subprocess.Popen(*args, **kwargs) | ||||
Durham Goode
|
r20893 | output, error = proc.communicate() | ||
if proc.returncode != 0: | ||||
Yuya Nishihara
|
r29210 | raise subprocess.CalledProcessError(proc.returncode, ' '.join(args[0])) | ||
Durham Goode
|
r20893 | return output | ||
Pierre-Yves David
|
r20848 | |||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r20850 | def update(rev): | ||
"""update the repo to a revision""" | ||||
try: | ||||
Yuya Nishihara
|
r29210 | subprocess.check_call(['hg', 'update', '--quiet', '--check', str(rev)]) | ||
Augie Fackler
|
r43346 | check_output( | ||
['make', 'local'], stderr=None | ||||
) # suppress output except for error/warning | ||||
Yuya Nishihara
|
r29210 | except subprocess.CalledProcessError as exc: | ||
Augie Fackler
|
r43346 | print('update to revision %s failed, aborting' % rev, file=sys.stderr) | ||
Pierre-Yves David
|
r20850 | sys.exit(exc.returncode) | ||
Pierre-Yves David
|
r25528 | |||
def hg(cmd, repo=None): | ||||
"""run a mercurial command | ||||
<cmd> is the list of command + argument, | ||||
<repo> is an optional repository path to run this command in.""" | ||||
fullcmd = ['./hg'] | ||||
if repo is not None: | ||||
fullcmd += ['-R', repo] | ||||
Augie Fackler
|
r43346 | fullcmd += [ | ||
'--config', | ||||
'extensions.perf=' + os.path.join(contribdir, 'perf.py'), | ||||
] | ||||
Pierre-Yves David
|
r25528 | fullcmd += cmd | ||
Yuya Nishihara
|
r29210 | return check_output(fullcmd, stderr=subprocess.STDOUT) | ||
Pierre-Yves David
|
r25528 | |||
Augie Fackler
|
r43346 | |||
Gregory Szorc
|
r27073 | def perf(revset, target=None, contexts=False): | ||
Pierre-Yves David
|
r20851 | """run benchmark for this very revset""" | ||
try: | ||||
Boris Feld
|
r41308 | args = ['perfrevset'] | ||
Gregory Szorc
|
r27073 | if contexts: | ||
args.append('--contexts') | ||||
Boris Feld
|
r41308 | args.append('--') | ||
args.append(revset) | ||||
Gregory Szorc
|
r27073 | output = hg(args, repo=target) | ||
Pierre-Yves David
|
r25530 | return parseoutput(output) | ||
Yuya Nishihara
|
r29210 | except subprocess.CalledProcessError as exc: | ||
Augie Fackler
|
r43346 | print( | ||
'abort: cannot run revset benchmark: %s' % exc.cmd, file=sys.stderr | ||||
) | ||||
if getattr(exc, 'output', None) is None: # no output before 2.7 | ||||
Pulkit Goyal
|
r28564 | print('(no output)', file=sys.stderr) | ||
Pierre-Yves David
|
r25529 | else: | ||
Pulkit Goyal
|
r28564 | print(exc.output, file=sys.stderr) | ||
Pierre-Yves David
|
r25646 | return None | ||
Pierre-Yves David
|
r20851 | |||
Augie Fackler
|
r43346 | |||
outputre = re.compile( | ||||
br'! wall (\d+.\d+) comb (\d+.\d+) user (\d+.\d+) ' | ||||
br'sys (\d+.\d+) \(best of (\d+)\)' | ||||
) | ||||
Pierre-Yves David
|
r25530 | |||
def parseoutput(output): | ||||
"""parse a textual output into a dict | ||||
We cannot just use json because we want to compare with old | ||||
versions of Mercurial that may not support json output. | ||||
""" | ||||
match = outputre.search(output) | ||||
if not match: | ||||
Pulkit Goyal
|
r28564 | print('abort: invalid output:', file=sys.stderr) | ||
print(output, file=sys.stderr) | ||||
Pierre-Yves David
|
r25530 | sys.exit(1) | ||
Augie Fackler
|
r43346 | return { | ||
'comb': float(match.group(2)), | ||||
'count': int(match.group(5)), | ||||
'sys': float(match.group(3)), | ||||
'user': float(match.group(4)), | ||||
'wall': float(match.group(1)), | ||||
} | ||||
Pierre-Yves David
|
r25530 | |||
Pierre-Yves David
|
r20852 | def printrevision(rev): | ||
"""print data about a revision""" | ||||
Pierre-Yves David
|
r25538 | sys.stdout.write("Revision ") | ||
Pierre-Yves David
|
r20852 | sys.stdout.flush() | ||
Augie Fackler
|
r43346 | subprocess.check_call( | ||
[ | ||||
'hg', | ||||
'log', | ||||
'--rev', | ||||
str(rev), | ||||
'--template', | ||||
'{if(tags, " ({tags})")} ' '{rev}:{node|short}: {desc|firstline}\n', | ||||
] | ||||
) | ||||
Pierre-Yves David
|
r20852 | |||
Pierre-Yves David
|
r25532 | def idxwidth(nbidx): | ||
"""return the max width of number used for index | ||||
Augie Fackler
|
r25533 | This is similar to log10(nbidx), but we use custom code here | ||
because we start with zero and we'd rather not deal with all the | ||||
extra rounding business that log10 would imply. | ||||
""" | ||||
Augie Fackler
|
r43346 | nbidx -= 1 # starts at 0 | ||
Pierre-Yves David
|
r25532 | idxwidth = 0 | ||
while nbidx: | ||||
idxwidth += 1 | ||||
nbidx //= 10 | ||||
if not idxwidth: | ||||
idxwidth = 1 | ||||
return idxwidth | ||||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r25539 | def getfactor(main, other, field, sensitivity=0.05): | ||
"""return the relative factor between values for 'field' in main and other | ||||
Mads Kiilerich
|
r26781 | Return None if the factor is insignificant (less than <sensitivity> | ||
Pierre-Yves David
|
r25539 | variation).""" | ||
factor = 1 | ||||
if main is not None: | ||||
factor = other[field] / main[field] | ||||
low, high = 1 - sensitivity, 1 + sensitivity | ||||
Augie Fackler
|
r43346 | if low < factor < high: | ||
Pierre-Yves David
|
r25539 | return None | ||
return factor | ||||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r25539 | def formatfactor(factor): | ||
"""format a factor into a 4 char string | ||||
22% | ||||
156% | ||||
x2.4 | ||||
x23 | ||||
x789 | ||||
x1e4 | ||||
x5x7 | ||||
""" | ||||
if factor is None: | ||||
return ' ' | ||||
elif factor < 2: | ||||
return '%3i%%' % (factor * 100) | ||||
elif factor < 10: | ||||
return 'x%3.1f' % factor | ||||
elif factor < 1000: | ||||
return '%4s' % ('x%i' % factor) | ||||
else: | ||||
order = int(math.log(factor)) + 1 | ||||
Martin von Zweigbergk
|
r40065 | while math.log(factor) > 1: | ||
Pierre-Yves David
|
r25539 | factor //= 0 | ||
return 'x%ix%i' % (factor, order) | ||||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r25541 | def formattiming(value): | ||
"""format a value to strictly 8 char, dropping some precision if needed""" | ||||
Augie Fackler
|
r43346 | if value < 10 ** 7: | ||
Pierre-Yves David
|
r25541 | return ('%.6f' % value)[:8] | ||
else: | ||||
# value is HUGE very unlikely to happen (4+ month run) | ||||
return '%i' % value | ||||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r25539 | _marker = object() | ||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r25540 | def printresult(variants, idx, data, maxidx, verbose=False, reference=_marker): | ||
Pierre-Yves David
|
r25531 | """print a line of result to stdout""" | ||
Pierre-Yves David
|
r25532 | mask = '%%0%ii) %%s' % idxwidth(maxidx) | ||
Pierre-Yves David
|
r25646 | |||
Pierre-Yves David
|
r25540 | out = [] | ||
for var in variants: | ||||
Pierre-Yves David
|
r25646 | if data[var] is None: | ||
out.append('error ') | ||||
out.append(' ' * 4) | ||||
continue | ||||
Pierre-Yves David
|
r25541 | out.append(formattiming(data[var]['wall'])) | ||
Pierre-Yves David
|
r25540 | if reference is not _marker: | ||
factor = None | ||||
if reference is not None: | ||||
factor = getfactor(reference[var], data[var], 'wall') | ||||
out.append(formatfactor(factor)) | ||||
if verbose: | ||||
Pierre-Yves David
|
r25541 | out.append(formattiming(data[var]['comb'])) | ||
out.append(formattiming(data[var]['user'])) | ||||
out.append(formattiming(data[var]['sys'])) | ||||
Augie Fackler
|
r43346 | out.append('%6d' % data[var]['count']) | ||
Pulkit Goyal
|
r28564 | print(mask % (idx, ' '.join(out))) | ||
Pierre-Yves David
|
r25530 | |||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r25540 | def printheader(variants, maxidx, verbose=False, relative=False): | ||
header = [' ' * (idxwidth(maxidx) + 1)] | ||||
for var in variants: | ||||
if not var: | ||||
var = 'iter' | ||||
Martin von Zweigbergk
|
r40065 | if len(var) > 8: | ||
Pierre-Yves David
|
r25541 | var = var[:3] + '..' + var[-3:] | ||
header.append('%-8s' % var) | ||||
Pierre-Yves David
|
r25540 | if relative: | ||
header.append(' ') | ||||
if verbose: | ||||
Pierre-Yves David
|
r25541 | header.append('%-8s' % 'comb') | ||
header.append('%-8s' % 'user') | ||||
header.append('%-8s' % 'sys') | ||||
Pierre-Yves David
|
r25540 | header.append('%6s' % 'count') | ||
Pulkit Goyal
|
r28564 | print(' '.join(header)) | ||
Pierre-Yves David
|
r25530 | |||
Augie Fackler
|
r43346 | |||
Pierre-Yves David
|
r20853 | def getrevs(spec): | ||
"""get the list of rev matched by a revset""" | ||||
try: | ||||
out = check_output(['hg', 'log', '--template={rev}\n', '--rev', spec]) | ||||
Yuya Nishihara
|
r29210 | except subprocess.CalledProcessError as exc: | ||
Augie Fackler
|
r43346 | print("abort, can't get revision from %s" % spec, file=sys.stderr) | ||
Pierre-Yves David
|
r20853 | sys.exit(exc.returncode) | ||
return [r for r in out.split() if r] | ||||
Pierre-Yves David
|
r25540 | def applyvariants(revset, variant): | ||
if variant == 'plain': | ||||
return revset | ||||
Pierre-Yves David
|
r25543 | for var in variant.split('+'): | ||
revset = '%s(%s)' % (var, revset) | ||||
return revset | ||||
Pierre-Yves David
|
r25540 | |||
Augie Fackler
|
r43346 | |||
helptext = """This script will run multiple variants of provided revsets using | ||||
Pierre-Yves David
|
r25607 | different revisions in your mercurial repository. After the benchmark are run | ||
Mads Kiilerich
|
r26781 | summary output is provided. Use it to demonstrate speed improvements or pin | ||
Pierre-Yves David
|
r25607 | point regressions. Revsets to run are specified in a file (or from stdin), one | ||
revsets per line. Line starting with '#' will be ignored, allowing insertion of | ||||
comments.""" | ||||
Augie Fackler
|
r43346 | parser = optparse.OptionParser( | ||
usage="usage: %prog [options] <revs>", description=helptext | ||||
) | ||||
parser.add_option( | ||||
"-f", | ||||
"--file", | ||||
help="read revset from FILE (stdin if omitted)", | ||||
metavar="FILE", | ||||
) | ||||
parser.add_option("-R", "--repo", help="run benchmark on REPO", metavar="REPO") | ||||
Pierre-Yves David
|
r21287 | |||
Augie Fackler
|
r43346 | parser.add_option( | ||
"-v", | ||||
"--verbose", | ||||
action='store_true', | ||||
help="display all timing data (not just best total time)", | ||||
) | ||||
Pierre-Yves David
|
r25537 | |||
Augie Fackler
|
r43346 | parser.add_option( | ||
"", | ||||
"--variants", | ||||
default=','.join(DEFAULTVARIANTS), | ||||
help="comma separated list of variant to test " | ||||
"(eg: plain,min,sorted) (plain = no modification)", | ||||
) | ||||
parser.add_option( | ||||
'', | ||||
'--contexts', | ||||
action='store_true', | ||||
help='obtain changectx from results instead of integer revs', | ||||
) | ||||
Pierre-Yves David
|
r25540 | |||
Pierre-Yves David
|
r21287 | (options, args) = parser.parse_args() | ||
Pierre-Yves David
|
r20848 | |||
Pierre-Yves David
|
r25535 | if not args: | ||
Pierre-Yves David
|
r21287 | parser.print_help() | ||
Pierre-Yves David
|
r21286 | sys.exit(255) | ||
Pierre-Yves David
|
r21548 | # the directory where both this script and the perf.py extension live. | ||
contribdir = os.path.dirname(__file__) | ||||
Pierre-Yves David
|
r21287 | |||
Pierre-Yves David
|
r20848 | revsetsfile = sys.stdin | ||
Pierre-Yves David
|
r21287 | if options.file: | ||
revsetsfile = open(options.file) | ||||
Pierre-Yves David
|
r20848 | |||
Pierre-Yves David
|
r22556 | revsets = [l.strip() for l in revsetsfile if not l.startswith('#')] | ||
Pierre-Yves David
|
r25642 | revsets = [l for l in revsets if l] | ||
Pierre-Yves David
|
r20848 | |||
Pulkit Goyal
|
r28564 | print("Revsets to benchmark") | ||
print("----------------------------") | ||||
Pierre-Yves David
|
r20848 | |||
for idx, rset in enumerate(revsets): | ||||
Pulkit Goyal
|
r28564 | print("%i) %s" % (idx, rset)) | ||
Pierre-Yves David
|
r20848 | |||
Pulkit Goyal
|
r28564 | print("----------------------------") | ||
print() | ||||
Pierre-Yves David
|
r20848 | |||
Pierre-Yves David
|
r25535 | revs = [] | ||
for a in args: | ||||
revs.extend(getrevs(a)) | ||||
Pierre-Yves David
|
r20848 | |||
Pierre-Yves David
|
r25540 | variants = options.variants.split(',') | ||
Pierre-Yves David
|
r20855 | results = [] | ||
Pierre-Yves David
|
r20848 | for r in revs: | ||
Pulkit Goyal
|
r28564 | print("----------------------------") | ||
Pierre-Yves David
|
r20852 | printrevision(r) | ||
Pulkit Goyal
|
r28564 | print("----------------------------") | ||
Pierre-Yves David
|
r20850 | update(r) | ||
Pierre-Yves David
|
r20855 | res = [] | ||
results.append(res) | ||||
Pierre-Yves David
|
r25540 | printheader(variants, len(revsets), verbose=options.verbose) | ||
Pierre-Yves David
|
r20848 | for idx, rset in enumerate(revsets): | ||
Pierre-Yves David
|
r25540 | varres = {} | ||
for var in variants: | ||||
varrset = applyvariants(rset, var) | ||||
Gregory Szorc
|
r27073 | data = perf(varrset, target=options.repo, contexts=options.contexts) | ||
Pierre-Yves David
|
r25540 | varres[var] = data | ||
res.append(varres) | ||||
Augie Fackler
|
r43346 | printresult( | ||
variants, idx, varres, len(revsets), verbose=options.verbose | ||||
) | ||||
Pierre-Yves David
|
r20855 | sys.stdout.flush() | ||
Pulkit Goyal
|
r28564 | print("----------------------------") | ||
Pierre-Yves David
|
r20848 | |||
Pierre-Yves David
|
r20855 | |||
Augie Fackler
|
r43346 | print( | ||
""" | ||||
Pierre-Yves David
|
r20855 | |||
Result by revset | ||||
================ | ||||
Augie Fackler
|
r43346 | """ | ||
) | ||||
Pierre-Yves David
|
r20855 | |||
Pulkit Goyal
|
r28564 | print('Revision:') | ||
Pierre-Yves David
|
r20855 | for idx, rev in enumerate(revs): | ||
sys.stdout.write('%i) ' % idx) | ||||
sys.stdout.flush() | ||||
printrevision(rev) | ||||
Pulkit Goyal
|
r28564 | print() | ||
print() | ||||
Pierre-Yves David
|
r20855 | |||
for ridx, rset in enumerate(revsets): | ||||
Pulkit Goyal
|
r28564 | print("revset #%i: %s" % (ridx, rset)) | ||
Pierre-Yves David
|
r25540 | printheader(variants, len(results), verbose=options.verbose, relative=True) | ||
Pierre-Yves David
|
r25539 | ref = None | ||
Pierre-Yves David
|
r20855 | for idx, data in enumerate(results): | ||
Augie Fackler
|
r43346 | printresult( | ||
variants, | ||||
idx, | ||||
data[ridx], | ||||
len(results), | ||||
verbose=options.verbose, | ||||
reference=ref, | ||||
) | ||||
Pierre-Yves David
|
r25539 | ref = data[ridx] | ||
Pulkit Goyal
|
r28564 | print() | ||