blob: 6c5823c9194edf8d5db3de615a481f7fd492d1ba [file] [log] [blame]
#!/usr/bin/env python
"""Performance test to compare the performance of buck between two revisions.
The general algorithm is:
Checkout <revisions_to_go_back - 1>
Warm up the cache:
Set .buckversion to old revision, build all targets
Set .buckversion to new revision, build all targets
For each revision to test:
- Rename directory being tested
- Build all targets, check to ensure everything pulls from dir cache
- Check out revision to test
- Clean Build all targets <iterations_per_diff> times, only reading from
cache, not writing (except for the last one, write that time)
- Buck build all targets to verify no-op build works.
"""
import argparse
import re
import subprocess
import os
import tempfile
import sys
from collections import defaultdict
from datetime import datetime
def createArgParser():
parser = argparse.ArgumentParser(
description='Run the buck performance test')
parser.add_argument(
'--perftest_id',
action='store',
type=str,
help='The identifier of this performance test')
parser.add_argument(
'--revisions_to_go_back',
action='store',
type=int,
help='The maximum number of revisions to go back when testing')
parser.add_argument(
'--iterations_per_diff',
action='store',
type=int,
help='The number of iterations to run on diff')
parser.add_argument(
'--targets_to_build',
action='append',
type=str,
help='The targets to build')
parser.add_argument(
'--repo_under_test',
action='store',
type=str,
help='Path to the repo under test')
parser.add_argument(
'--path_to_buck',
action='store',
type=str,
help='The path to the buck binary')
parser.add_argument(
'--old_buck_revision',
action='store',
type=str,
help='The original buck revision')
parser.add_argument(
'--new_buck_revision',
action='store',
type=str,
help='The new buck revision')
return parser
def log(message):
print '%s\t%s' % (str(datetime.now()), message)
sys.stdout.flush()
class BuildResult():
def __init__(self, time_delta, cache_results, rule_key_map):
self.time_delta = time_delta
self.cache_results = cache_results
self.rule_key_map = rule_key_map
def git_clean(cwd):
log('Running git clean.')
subprocess.check_call(
['git', 'clean', '--quiet', '-xfd'],
cwd=cwd)
def git_reset(cwd, revision):
subprocess.check_call(
['git', 'reset', '--quiet', '--hard', revision],
cwd=cwd)
def buck_clean(args, cwd):
log('Running buck clean.')
subprocess.check_call(
[args.path_to_buck, 'clean'],
cwd=cwd)
def git_get_revisions(args):
return list(reversed(subprocess.check_output(
['git', 'log', '--pretty=format:%H', 'HEAD', '-n',
str(args.revisions_to_go_back + 1)],
cwd=args.repo_under_test).splitlines()))
def git_checkout(revision, cwd):
log('Checking out %s.' % revision)
git_reset(cwd, 'HEAD')
subprocess.check_call(
['git', 'checkout', '--quiet', revision],
cwd=cwd)
BUILD_RESULT_LOG_LINE = re.compile(
r'BuildRuleFinished\((?P<rule_name>[\w_\-:#\/]+)\): (?P<result>[A-Z_]+) '
r'(?P<cache_result>[A-Z_]+) (?P<success_type>[A-Z_]+) '
r'(?P<rule_key>[0-9a-f]*)')
RULEKEY_LINE = re.compile(
r'^INFO: RuleKey (?P<rule_key>[0-9a-f]*)='
r'(?P<rule_key_debug>.*)$')
BUCK_LOG_RULEKEY_LINE = re.compile(
r'.*\[\w+\](?:\[command:[0-9a-f-]+\])?\[tid:\d+\]'
r'\[com.facebook.buck.rules.RuleKey[\$\.]Builder\] '
r'RuleKey (?P<rule_key>[0-9a-f]+)='
r'(?P<rule_key_debug>.*)$')
def buck_build_target(args, cwd, target, perftest_side, log_as_perftest=True):
"""Builds a target with buck and returns performance information.
"""
log('Running buck build %s.' % target)
bucklogging_properties_path = os.path.join(
cwd, '.bucklogging.local.properties')
with open(bucklogging_properties_path, 'w') as bucklogging_properties:
# The default configuration has the root logger and FileHandler
# discard anything below FINE level.
#
# We need RuleKey logging, which uses FINER (verbose), so the
# root logger and file handler both need to be reconfigured
# to enable verbose logging.
bucklogging_properties.write(
'''.level=FINER
java.util.logging.FileHandler.level=FINER''')
env = os.environ.copy()
# Force buck to pretend it's repo is clean.
env.update({
'BUCK_REPOSITORY_DIRTY': '0'
})
if log_as_perftest:
env.update({
'BUCK_EXTRA_JAVA_ARGS':
'-Dbuck.perftest_id=%s, -Dbuck.perftest_side=%s' % (
args.perftest_id, perftest_side)
})
start = datetime.now()
with tempfile.TemporaryFile() as tmpFile:
try:
subprocess.check_call(
[args.path_to_buck, 'build', target, '-v', '5'],
stdout=tmpFile,
stderr=tmpFile,
cwd=cwd,
env=env)
except:
tmpFile.seek(0)
log('Buck build failed: %s' % tmpFile.read())
raise
tmpFile.seek(0)
build_output = tmpFile.read()
finish = datetime.now()
java_utils_log_path = os.path.join(
cwd,
'buck-out', 'log', 'buck-0.log')
if os.path.exists(java_utils_log_path):
pattern = BUCK_LOG_RULEKEY_LINE
with open(java_utils_log_path) as f:
build_output = f.read()
else:
pattern = RULEKEY_LINE
rule_debug_map = {}
for line in build_output.splitlines():
match = pattern.match(line)
if match:
rule_debug_map[match.group('rule_key')] = match.group(
'rule_key_debug')
logfile_path = os.path.join(
cwd,
'buck-out', 'bin', 'build.log')
cache_results = defaultdict(list)
rule_key_map = {}
with open(logfile_path, 'r') as logfile:
for line in logfile.readlines():
line = line.strip()
match = BUILD_RESULT_LOG_LINE.search(line)
if match:
rule_name = match.group('rule_name')
rule_key = match.group('rule_key')
if not rule_key in rule_debug_map:
raise Exception('''ERROR: build.log contains an entry
which was not found in buck build -v 5 output.
Rule: {}, rule key: {}'''.format(rule_name, rule_key))
cache_results[match.group('cache_result')].append({
'rule_name': rule_name,
'rule_key': rule_key,
'rule_key_debug': rule_debug_map[rule_key]
})
rule_key_map[match.group('rule_name')] = rule_debug_map[
match.group('rule_key')]
result = BuildResult(finish - start, cache_results, rule_key_map)
cache_counts = {}
for key, value in result.cache_results.iteritems():
cache_counts[key] = len(value)
log('Test Build Finished! Elapsed Seconds: %d, Cache Counts: %s' % (
result.time_delta.total_seconds(), repr(cache_counts)))
return result
def set_perftest_side(
args,
cwd,
perftest_side,
cache_mode,
dir_cache_only=True):
log('Reconfiguring to test %s version of buck.' % perftest_side)
buckconfig_path = os.path.join(cwd, '.buckconfig.local')
with open(buckconfig_path, 'w') as buckconfig:
buckconfig.write('''[cache]
%s
dir = buck-cache-%s
dir_mode = %s
''' % ('mode = dir' if dir_cache_only else '', perftest_side, cache_mode))
buckconfig.truncate()
buckversion_path = os.path.join(cwd, '.buckversion')
with open(buckversion_path, 'w') as buckversion:
if perftest_side == 'old':
buckversion.write(args.old_buck_revision + os.linesep)
else:
buckversion.write(args.new_buck_revision + os.linesep)
buckversion.truncate()
def build_all_targets(
args,
cwd,
perftest_side,
cache_mode,
run_clean=True,
dir_cache_only=True,
log_as_perftest=True):
set_perftest_side(
args,
cwd,
perftest_side,
cache_mode,
dir_cache_only=dir_cache_only)
result = {}
for target in args.targets_to_build:
if run_clean:
buck_clean(args, cwd)
#TODO(royw): Do smart things with the results here.
result[target] = buck_build_target(
args,
cwd,
target,
perftest_side,
log_as_perftest=log_as_perftest)
return result
def run_tests_for_diff(args, revisions_to_test, test_index, last_results):
log('=== Running tests at revision %s ===' % revisions_to_test[test_index])
new_directory_name = (os.path.basename(args.repo_under_test) +
'_test_iteration_%d' % test_index)
# Rename the directory to flesh out any cache problems.
cwd = os.path.join(os.path.dirname(args.repo_under_test),
new_directory_name)
log('Renaming %s to %s' % (args.repo_under_test, cwd))
os.rename(args.repo_under_test, cwd)
try:
log('== Checking new revision for problems with absolute paths ==')
results = build_all_targets(args, cwd, 'new', 'readonly')
for target, result in results.iteritems():
if (len(result.cache_results.keys()) != 1 or
'DIR_HIT' not in result.cache_results):
# Remove DIR_HITs to make error message cleaner
result.cache_results.pop('DIR_HIT', None)
log('Building %s at revision %s with the new buck version '
'was unable to reuse the cache from a previous run. '
'This suggests one of the rule keys contains an '
'abosolute path.' % (
target,
revisions_to_test[test_index - 1]))
for rule in result.cache_results['MISS']:
rule_name = rule['rule_name']
old_key = last_results[target].rule_key_map[rule_name]
log('Rule %s missed.' % rule_name)
log('\tOld Rule Key: %s.' % old_key)
log('\tNew Rule Key: %s.' % result.rule_key_map[rule_name])
raise Exception('Failed to reuse cache across directories!!!')
git_checkout(revisions_to_test[test_index], cwd)
for attempt in xrange(args.iterations_per_diff):
cache_mode = 'readonly'
if attempt == args.iterations_per_diff - 1:
cache_mode = 'readwrite'
build_all_targets(args, cwd, 'old', cache_mode)
build_all_targets(args, cwd, 'new', cache_mode)
log('== Checking new revision to ensure noop build does nothing. ==')
results = build_all_targets(
args,
cwd,
'new',
cache_mode,
run_clean=False)
for target, result in results.iteritems():
if (len(result.cache_results.keys()) != 1 or
'LOCAL_KEY_UNCHANGED_HIT' not in result.cache_results):
result.cache_results.pop('DIR_HIT', None)
raise Exception(
'Doing a noop build for %s at revision %s with the new '
'buck version did not hit all of it\'s keys.\nMissed '
'Rules: %s' % (
target,
revisions_to_test[test_index - 1],
repr(result.cache_results)))
finally:
log('Renaming %s to %s' % (cwd, args.repo_under_test))
os.rename(cwd, args.repo_under_test)
return results
def main():
args = createArgParser().parse_args()
log('Running Performance Test!')
git_clean(args.repo_under_test)
revisions_to_test = git_get_revisions(args)
# Checkout the revision previous to the test and warm up the local dir
# cache.
# git_clean(args.repo_under_test)
log('=== Warming up cache ===')
git_checkout(revisions_to_test[0], args.repo_under_test)
build_all_targets(
args,
args.repo_under_test,
'old',
'readwrite',
dir_cache_only=False,
log_as_perftest=False)
results_for_new = build_all_targets(
args,
args.repo_under_test,
'new',
'readwrite',
dir_cache_only=False,
log_as_perftest=False)
log('=== Cache Warm! Running tests ===')
for i in xrange(1, args.revisions_to_go_back):
results_for_new = run_tests_for_diff(
args,
revisions_to_test,
i,
results_for_new)
if __name__ == '__main__':
main()