mirror of
https://github.com/facebook/sapling.git
synced 2024-10-09 00:14:35 +03:00
20f4e198ee
Summary: A stub class in metrics.py can be overwritten by dedicated implementations. Reviewed By: quark-zju Differential Revision: D7673553 fbshipit-source-id: f713abb3203d393e356f96fb834111ec2c37498a
237 lines
7.5 KiB
Python
237 lines
7.5 KiB
Python
# Copyright 2018 Facebook, Inc.
|
|
#
|
|
# This software may be used and distributed according to the terms of the
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import contextlib
|
|
import random
|
|
import subprocess
|
|
import time
|
|
|
|
from mercurial.i18n import _
|
|
from mercurial import (
|
|
commands,
|
|
encoding,
|
|
error,
|
|
metrics,
|
|
registrar,
|
|
scmutil,
|
|
util,
|
|
)
|
|
|
|
from . import (
|
|
editsgenerator,
|
|
)
|
|
|
|
cmdtable = {}
|
|
command = registrar.command(cmdtable)
|
|
|
|
testedwith = 'ships-with-fb-hgext'
|
|
|
|
class perftestsuite(object):
|
|
"""
|
|
A simple integration test suite that runs against an existing repo, with the
|
|
goal of logging perf numbers to CI.
|
|
"""
|
|
def __init__(self, repo, publish=False, profile=False, printout=False):
|
|
self.repo = repo
|
|
self.ui = repo.ui
|
|
self.publish = publish
|
|
self.profile = profile
|
|
self.printout = printout
|
|
self.order = [
|
|
'commit',
|
|
'amend',
|
|
'status',
|
|
'revert',
|
|
'rebase',
|
|
'immrebase',
|
|
'pull',
|
|
]
|
|
self.editsgenerator = editsgenerator.randomeditsgenerator(repo[None])
|
|
|
|
if profile:
|
|
self.profileargs = ['--profile']
|
|
else:
|
|
self.profileargs = []
|
|
|
|
def run(self):
|
|
for cmd in self.order:
|
|
func = getattr(self, 'test' + cmd)
|
|
func()
|
|
|
|
def _runtimeprofile(self, cmd, args=None, metricname=None):
|
|
"""Runs ``cmd`` with ``args`` and sends th result to ODS; also adds
|
|
--profile, if requested."""
|
|
if args is None:
|
|
args = []
|
|
if metricname is None:
|
|
metricname = cmd
|
|
with self.time(metricname):
|
|
self._run([cmd] + args + self.profileargs)
|
|
|
|
@contextlib.contextmanager
|
|
def time(self, cmd):
|
|
"""Times the given block and logs that time to ODS"""
|
|
reponame = self.ui.config('remotefilelog', 'reponame')
|
|
if not reponame:
|
|
raise error.Abort(_("must set remotefilelog.reponame"))
|
|
start = time.time()
|
|
try:
|
|
yield
|
|
finally:
|
|
duration = time.time() - start
|
|
self.ui.warn(_("ran '%s' in %0.2f sec\n") % (cmd, duration))
|
|
if self.publish:
|
|
metrics.client(self.ui).gauge('hg.perfsuite.%s' % reponame,
|
|
'%s.time' % cmd, duration)
|
|
|
|
def testcommit(self):
|
|
self.editsgenerator.makerandomedits(self.repo[None])
|
|
self._run(['status'])
|
|
self._run(['addremove'])
|
|
self._runtimeprofile('commit', ['-m', 'test commit'])
|
|
|
|
def testamend(self):
|
|
self.editsgenerator.makerandomedits(self.repo[None])
|
|
self._run(['status'])
|
|
self._run(['addremove'])
|
|
self._runtimeprofile('amend')
|
|
|
|
def teststatus(self):
|
|
self.editsgenerator.makerandomedits(self.repo[None])
|
|
self._runtimeprofile('status')
|
|
|
|
def testrevert(self):
|
|
self.editsgenerator.makerandomedits(self.repo[None])
|
|
self._runtimeprofile('revert', ['--all'])
|
|
|
|
def testpull(self):
|
|
# TODO: Log the master rev at start, (real)strip N commits, then pull
|
|
# that rev, to reduce the variability.
|
|
self._runtimeprofile('pull')
|
|
|
|
def testrebase(self):
|
|
dist = self.ui.configint("perfsuite", "rebase.masterdistance", 1000)
|
|
self._runtimeprofile('rebase', ['-s', '. % master', '-d',
|
|
"master~%d" % dist])
|
|
|
|
def testimmrebase(self):
|
|
dist = self.ui.configint("perfsuite", "immrebase.masterdistance", 100)
|
|
self._run(['update', '-C', 'master'])
|
|
configs = {
|
|
('rebase', 'experimental.inmemory.nomergedriver'): False,
|
|
('rebase', 'experimental.inmemory'): True,
|
|
}
|
|
with self.ui.configoverride(configs):
|
|
self._runtimeprofile('rebase', ['-r', 'draft()', '-d',
|
|
"master~%d" % dist], metricname='immrebase')
|
|
|
|
def _run(self, cmd, cwd=None, env=None, stderr=False, utf8decode=True,
|
|
input=None, timeout=0, returncode=False):
|
|
"""Adapted from fbcode/scm/lib/_repo.py:Repository::run"""
|
|
cmd = [util.hgexecutable(), '-R', self.repo.origroot] + cmd
|
|
stdin = None
|
|
if input:
|
|
stdin = subprocess.PIPE
|
|
p = self._spawn(cmd, cwd=cwd, env=env, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, stdin=stdin, timeout=timeout)
|
|
if input:
|
|
if not isinstance(input, bytes):
|
|
input = input.encode('utf-8')
|
|
out, err = p.communicate(input=input)
|
|
else:
|
|
out, err = p.communicate()
|
|
|
|
if out is not None and utf8decode:
|
|
out = out.decode('utf-8')
|
|
if err is not None and utf8decode:
|
|
err = err.decode('utf-8')
|
|
|
|
if p.returncode != 0 and returncode is False:
|
|
self.ui.warn(_('run call failed!\n'))
|
|
# Sometimes git or hg error output can be very big.
|
|
# Let's limit stderr and stdout to 1000
|
|
OUTPUT_LIMIT = 1000
|
|
out = out[:OUTPUT_LIMIT]
|
|
err = err[:OUTPUT_LIMIT]
|
|
out = "STDOUT: %s\nSTDERR: %s\n" % (out, err)
|
|
cmdstr = ' '.join([self._safe_bytes_to_str(entry) for entry in cmd])
|
|
cmdstr += '\n%s' % out
|
|
ex = subprocess.CalledProcessError(p.returncode, cmdstr)
|
|
ex.output = out
|
|
raise ex
|
|
|
|
if out and self.printout:
|
|
self.ui.warn(_("stdout: %s\n") % out)
|
|
if err and self.printout:
|
|
self.ui.warn(_("stderr: %s\n") % err)
|
|
|
|
if returncode:
|
|
return out, err, p.returncode
|
|
|
|
if stderr:
|
|
return out, err, None
|
|
return out, "", None
|
|
|
|
def _safe_bytes_to_str(self, val):
|
|
if isinstance(val, bytes):
|
|
val = val.decode('utf-8', 'replace')
|
|
return val
|
|
|
|
def _spawn(self, cmd, cwd=None, env=None,
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=None,
|
|
timeout=0):
|
|
"""Adapted from fbcode/scm/lib/_repo.py:Repository::spawn"""
|
|
environ = encoding.environ.copy()
|
|
if env:
|
|
environ.update(env)
|
|
|
|
if timeout != 0:
|
|
timeoutcmd = ['timeout', str(timeout)]
|
|
timeoutcmd.extend(cmd)
|
|
cmd = timeoutcmd
|
|
|
|
return subprocess.Popen(cmd, stdin=stdin, stdout=stdout,
|
|
stderr=stderr,
|
|
cwd=cwd, env=environ, close_fds=True)
|
|
|
|
@command('perftestsuite', [
|
|
('r', 'rev', '', _('rev to update to first')),
|
|
('', 'publish', False, _('whether to publish the metrics')),
|
|
('', 'use-profile', False, _('whether to run commands in profile mode')),
|
|
('', 'print', False, _('whether to print commands\' stdout and stderr')),
|
|
('', 'seed', 0, _('random seed to use')),
|
|
], _('hg perftestsuite'))
|
|
def perftestsuitecmd(ui, repo, *revs, **opts):
|
|
"""Runs an in-depth performance suite and logs results to a metrics
|
|
framework.
|
|
|
|
The rebase distance is configurable::
|
|
|
|
[perfsuite]
|
|
rebase.masterdistance = 100
|
|
immrebase.masterdistance = 100
|
|
|
|
The metrics endpoint is configurable::
|
|
|
|
[ods]
|
|
endpoint = https://somehost/metrics
|
|
"""
|
|
if opts['seed']:
|
|
random.seed(opts['seed'])
|
|
|
|
if opts['rev']:
|
|
ui.status(_("updating to %s...\n") % (opts['rev']))
|
|
commands.update(ui, repo, scmutil.revsingle(repo, opts['rev']).rev())
|
|
|
|
suite = perftestsuite(repo,
|
|
publish=opts['publish'],
|
|
profile=opts['use_profile'],
|
|
printout=opts['print'],
|
|
)
|
|
suite.run()
|
|
|