import argparse
import collections
import multiprocessing
import os
import shlex
import shutil
import subprocess
import sys
def _run_benchmark(env, out_dir, include_debug_info):
target_dir = env.output_subdir('instrumentation_run')
_build_things_in(env, out_dir, what=['check-llvm', 'check-clang'])
cmake = _get_cmake_invocation_for_bootstrap_from(
env, out_dir, skip_tablegens=False)
if include_debug_info:
cmake.add_flag('CMAKE_BUILD_TYPE', 'RelWithDebInfo')
_run_fresh_cmake(env, cmake, target_dir)
_build_things_in(env, target_dir, what=['all'])
class CmakeInvocation:
_cflags = ['CMAKE_C_FLAGS', 'CMAKE_CXX_FLAGS']
_ldflags = [
'CMAKE_EXE_LINKER_FLAGS',
'CMAKE_MODULE_LINKER_FLAGS',
'CMAKE_SHARED_LINKER_FLAGS',
]
def __init__(self, cmake, maker, cmake_dir):
self._prefix = [cmake, '-G', maker, cmake_dir]
self._flags = {}
for flag in CmakeInvocation._cflags + CmakeInvocation._ldflags:
self._flags[flag] = []
def add_new_flag(self, key, value):
self.add_flag(key, value, allow_overwrites=False)
def add_flag(self, key, value, allow_overwrites=True):
if key not in self._flags:
self._flags[key] = value
return
existing_value = self._flags[key]
if isinstance(existing_value, list):
existing_value.append(value)
return
if not allow_overwrites:
raise ValueError('Invalid overwrite of %s requested' % key)
self._flags[key] = value
def add_cflags(self, flags):
assert not isinstance(flags, str)
for f in CmakeInvocation._cflags:
self._flags[f].extend(flags)
def add_ldflags(self, flags):
assert not isinstance(flags, str)
for f in CmakeInvocation._ldflags:
self._flags[f].extend(flags)
def to_args(self):
args = self._prefix.copy()
for key, value in sorted(self._flags.items()):
if isinstance(value, list):
if not value:
continue
value = ' '.join(value)
arg = '-D' + key
if value != '':
arg += '=' + value
args.append(arg)
return args
class Env:
def __init__(self, llvm_dir, use_make, output_dir, default_cmake_args,
dry_run):
self.llvm_dir = llvm_dir
self.use_make = use_make
self.output_dir = output_dir
self.default_cmake_args = default_cmake_args.copy()
self.dry_run = dry_run
def get_default_cmake_args_kv(self):
return self.default_cmake_args.items()
def get_cmake_maker(self):
return 'Ninja' if not self.use_make else 'Unix Makefiles'
def get_make_command(self):
if self.use_make:
return ['make', '-j{}'.format(multiprocessing.cpu_count())]
return ['ninja']
def output_subdir(self, name):
return os.path.join(self.output_dir, name)
def has_llvm_subproject(self, name):
if name == 'compiler-rt':
subdir = '../compiler-rt'
elif name == 'clang':
subdir = '../clang'
else:
raise ValueError('Unknown subproject: %s' % name)
return os.path.isdir(os.path.join(self.llvm_dir, subdir))
def run_command(self,
cmd,
cwd=None,
check=False,
silent_unless_error=False):
print(
'Running `%s` in %s' % (cmd, shlex.quote(cwd or os.getcwd())))
if self.dry_run:
return
if silent_unless_error:
stdout, stderr = subprocess.PIPE, subprocess.STDOUT
else:
stdout, stderr = None, None
popen = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=stdout,
stderr=stderr,
cwd=cwd)
stdout, _ = popen.communicate()
return_code = popen.wait(timeout=0)
if not return_code:
return
if silent_unless_error:
print(stdout.decode('utf-8', 'ignore'))
if check:
raise subprocess.CalledProcessError(
returncode=return_code, cmd=cmd, output=stdout, stderr=None)
def _get_default_cmake_invocation(env):
inv = CmakeInvocation(
cmake='cmake', maker=env.get_cmake_maker(), cmake_dir=env.llvm_dir)
for key, value in env.get_default_cmake_args_kv():
inv.add_new_flag(key, value)
return inv
def _get_cmake_invocation_for_bootstrap_from(env, out_dir,
skip_tablegens=True):
clang = os.path.join(out_dir, 'bin', 'clang')
cmake = _get_default_cmake_invocation(env)
cmake.add_new_flag('CMAKE_C_COMPILER', clang)
cmake.add_new_flag('CMAKE_CXX_COMPILER', clang + '++')
def add_tablegen(key, binary):
path = os.path.join(out_dir, 'bin', binary)
if env.dry_run or os.path.exists(path):
cmake.add_new_flag(key, path)
if skip_tablegens:
add_tablegen('LLVM_TABLEGEN', 'llvm-tblgen')
add_tablegen('CLANG_TABLEGEN', 'clang-tblgen')
return cmake
def _build_things_in(env, target_dir, what):
cmd = env.get_make_command() + what
env.run_command(cmd, cwd=target_dir, check=True)
def _run_fresh_cmake(env, cmake, target_dir):
if not env.dry_run:
try:
shutil.rmtree(target_dir)
except FileNotFoundError:
pass
os.makedirs(target_dir, mode=0o755)
cmake_args = cmake.to_args()
env.run_command(
cmake_args, cwd=target_dir, check=True, silent_unless_error=True)
def _build_stage1_clang(env):
target_dir = env.output_subdir('stage1')
cmake = _get_default_cmake_invocation(env)
_run_fresh_cmake(env, cmake, target_dir)
_build_things_in(env, target_dir, what=['clang', 'llvm-profdata', 'profile'])
return target_dir
def _generate_instrumented_clang_profile(env, stage1_dir, profile_dir,
output_file):
llvm_profdata = os.path.join(stage1_dir, 'bin', 'llvm-profdata')
if env.dry_run:
profiles = [os.path.join(profile_dir, '*.profraw')]
else:
profiles = [
os.path.join(profile_dir, f) for f in os.listdir(profile_dir)
if f.endswith('.profraw')
]
cmd = [llvm_profdata, 'merge', '-output=' + output_file] + profiles
env.run_command(cmd, check=True)
def _build_instrumented_clang(env, stage1_dir):
assert os.path.isabs(stage1_dir)
target_dir = os.path.join(env.output_dir, 'instrumented')
cmake = _get_cmake_invocation_for_bootstrap_from(env, stage1_dir)
cmake.add_new_flag('LLVM_BUILD_INSTRUMENTED', 'IR')
cmake.add_new_flag('LLVM_BUILD_RUNTIME', 'No')
_run_fresh_cmake(env, cmake, target_dir)
_build_things_in(env, target_dir, what=['clang', 'lld'])
profiles_dir = os.path.join(target_dir, 'profiles')
return target_dir, profiles_dir
def _build_optimized_clang(env, stage1_dir, profdata_file):
if not env.dry_run and not os.path.exists(profdata_file):
raise ValueError('Looks like the profdata file at %s doesn\'t exist' %
profdata_file)
target_dir = os.path.join(env.output_dir, 'optimized')
cmake = _get_cmake_invocation_for_bootstrap_from(env, stage1_dir)
cmake.add_new_flag('LLVM_PROFDATA_FILE', os.path.abspath(profdata_file))
cmake.add_cflags(['-Wno-backend-plugin'])
_run_fresh_cmake(env, cmake, target_dir)
_build_things_in(env, target_dir, what=['clang'])
return target_dir
Args = collections.namedtuple('Args', [
'do_optimized_build',
'include_debug_info',
'profile_location',
'stage1_dir',
])
def _parse_args():
parser = argparse.ArgumentParser(
description='Builds LLVM and Clang with instrumentation, collects '
'instrumentation profiles for them, and (optionally) builds things '
'with these PGO profiles. By default, it\'s assumed that you\'re '
'running this from your LLVM root, and all build artifacts will be '
'saved to $PWD/out.')
parser.add_argument(
'--cmake-extra-arg',
action='append',
default=[],
help='an extra arg to pass to all cmake invocations. Note that this '
'is interpreted as a -D argument, e.g. --cmake-extra-arg FOO=BAR will '
'be passed as -DFOO=BAR. This may be specified multiple times.')
parser.add_argument(
'--dry-run',
action='store_true',
help='print commands instead of running them')
parser.add_argument(
'--llvm-dir',
default='.',
help='directory containing an LLVM checkout (default: $PWD)')
parser.add_argument(
'--no-optimized-build',
action='store_true',
help='disable the final, PGO-optimized build')
parser.add_argument(
'--out-dir',
help='directory to write artifacts to (default: $llvm_dir/out)')
parser.add_argument(
'--profile-output',
help='where to output the profile (default is $out/pgo_profile.prof)')
parser.add_argument(
'--stage1-dir',
help='instead of having an initial build of everything, use the given '
'directory. It is expected that this directory will have clang, '
'llvm-profdata, and the appropriate libclang_rt.profile already built')
parser.add_argument(
'--use-debug-info-in-benchmark',
action='store_true',
help='use a regular build instead of RelWithDebInfo in the benchmark. '
'This increases benchmark execution time and disk space requirements, '
'but gives more coverage over debuginfo bits in LLVM and clang.')
parser.add_argument(
'--use-make',
action='store_true',
default=shutil.which('ninja') is None,
help='use Makefiles instead of ninja')
args = parser.parse_args()
llvm_dir = os.path.abspath(args.llvm_dir)
if args.out_dir is None:
output_dir = os.path.join(llvm_dir, 'out')
else:
output_dir = os.path.abspath(args.out_dir)
extra_args = {'CMAKE_BUILD_TYPE': 'Release',
'LLVM_ENABLE_PROJECTS': 'clang;compiler-rt;lld'}
for arg in args.cmake_extra_arg:
if arg.startswith('-D'):
arg = arg[2:]
elif arg.startswith('-'):
raise ValueError('Unknown not- -D arg encountered; you may need '
'to tweak the source...')
split = arg.split('=', 1)
if len(split) == 1:
key, val = split[0], ''
else:
key, val = split
extra_args[key] = val
env = Env(
default_cmake_args=extra_args,
dry_run=args.dry_run,
llvm_dir=llvm_dir,
output_dir=output_dir,
use_make=args.use_make,
)
if args.profile_output is not None:
profile_location = args.profile_output
else:
profile_location = os.path.join(env.output_dir, 'pgo_profile.prof')
result_args = Args(
do_optimized_build=not args.no_optimized_build,
include_debug_info=args.use_debug_info_in_benchmark,
profile_location=profile_location,
stage1_dir=args.stage1_dir,
)
return env, result_args
def _looks_like_llvm_dir(directory):
contents = set(os.listdir(directory))
expected_contents = [
'CODE_OWNERS.TXT',
'cmake',
'docs',
'include',
'utils',
]
if not all(c in contents for c in expected_contents):
return False
try:
include_listing = os.listdir(os.path.join(directory, 'include'))
except NotADirectoryError:
return False
return 'llvm' in include_listing
def _die(*args, **kwargs):
kwargs['file'] = sys.stderr
print(*args, **kwargs)
sys.exit(1)
def _main():
env, args = _parse_args()
if not _looks_like_llvm_dir(env.llvm_dir):
_die('Looks like %s isn\'t an LLVM directory; please see --help' %
env.llvm_dir)
if not env.has_llvm_subproject('clang'):
_die('Need a clang checkout at tools/clang')
if not env.has_llvm_subproject('compiler-rt'):
_die('Need a compiler-rt checkout at projects/compiler-rt')
def status(*args):
print(*args, file=sys.stderr)
if args.stage1_dir is None:
status('*** Building stage1 clang...')
stage1_out = _build_stage1_clang(env)
else:
stage1_out = args.stage1_dir
status('*** Building instrumented clang...')
instrumented_out, profile_dir = _build_instrumented_clang(env, stage1_out)
status('*** Running profdata benchmarks...')
_run_benchmark(env, instrumented_out, args.include_debug_info)
status('*** Generating profile...')
_generate_instrumented_clang_profile(env, stage1_out, profile_dir,
args.profile_location)
print('Final profile:', args.profile_location)
if args.do_optimized_build:
status('*** Building PGO-optimized binaries...')
optimized_out = _build_optimized_clang(env, stage1_out,
args.profile_location)
print('Final build directory:', optimized_out)
if __name__ == '__main__':
_main()