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