collect_and_build_with_pgo.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. #!/usr/bin/env python3
  2. """
  3. This script:
  4. - Builds clang with user-defined flags
  5. - Uses that clang to build an instrumented clang, which can be used to collect
  6. PGO samples
  7. - Builds a user-defined set of sources (default: clang) to act as a
  8. "benchmark" to generate a PGO profile
  9. - Builds clang once more with the PGO profile generated above
  10. This is a total of four clean builds of clang (by default). This may take a
  11. while. :)
  12. """
  13. import argparse
  14. import collections
  15. import multiprocessing
  16. import os
  17. import shlex
  18. import shutil
  19. import subprocess
  20. import sys
  21. ### User configuration
  22. # If you want to use a different 'benchmark' than building clang, make this
  23. # function do what you want. out_dir is the build directory for clang, so all
  24. # of the clang binaries will live under "${out_dir}/bin/". Using clang in
  25. # ${out_dir} will magically have the profiles go to the right place.
  26. #
  27. # You may assume that out_dir is a freshly-built directory that you can reach
  28. # in to build more things, if you'd like.
  29. def _run_benchmark(env, out_dir, include_debug_info):
  30. """The 'benchmark' we run to generate profile data."""
  31. target_dir = env.output_subdir('instrumentation_run')
  32. # `check-llvm` and `check-clang` are cheap ways to increase coverage. The
  33. # former lets us touch on the non-x86 backends a bit if configured, and the
  34. # latter gives us more C to chew on (and will send us through diagnostic
  35. # paths a fair amount, though the `if (stuff_is_broken) { diag() ... }`
  36. # branches should still heavily be weighted in the not-taken direction,
  37. # since we built all of LLVM/etc).
  38. _build_things_in(env, out_dir, what=['check-llvm', 'check-clang'])
  39. # Building tblgen gets us coverage; don't skip it. (out_dir may also not
  40. # have them anyway, but that's less of an issue)
  41. cmake = _get_cmake_invocation_for_bootstrap_from(
  42. env, out_dir, skip_tablegens=False)
  43. if include_debug_info:
  44. cmake.add_flag('CMAKE_BUILD_TYPE', 'RelWithDebInfo')
  45. _run_fresh_cmake(env, cmake, target_dir)
  46. # Just build all the things. The more data we have, the better.
  47. _build_things_in(env, target_dir, what=['all'])
  48. ### Script
  49. class CmakeInvocation:
  50. _cflags = ['CMAKE_C_FLAGS', 'CMAKE_CXX_FLAGS']
  51. _ldflags = [
  52. 'CMAKE_EXE_LINKER_FLAGS',
  53. 'CMAKE_MODULE_LINKER_FLAGS',
  54. 'CMAKE_SHARED_LINKER_FLAGS',
  55. ]
  56. def __init__(self, cmake, maker, cmake_dir):
  57. self._prefix = [cmake, '-G', maker, cmake_dir]
  58. # Map of str -> (list|str).
  59. self._flags = {}
  60. for flag in CmakeInvocation._cflags + CmakeInvocation._ldflags:
  61. self._flags[flag] = []
  62. def add_new_flag(self, key, value):
  63. self.add_flag(key, value, allow_overwrites=False)
  64. def add_flag(self, key, value, allow_overwrites=True):
  65. if key not in self._flags:
  66. self._flags[key] = value
  67. return
  68. existing_value = self._flags[key]
  69. if isinstance(existing_value, list):
  70. existing_value.append(value)
  71. return
  72. if not allow_overwrites:
  73. raise ValueError('Invalid overwrite of %s requested' % key)
  74. self._flags[key] = value
  75. def add_cflags(self, flags):
  76. # No, I didn't intend to append ['-', 'O', '2'] to my flags, thanks :)
  77. assert not isinstance(flags, str)
  78. for f in CmakeInvocation._cflags:
  79. self._flags[f].extend(flags)
  80. def add_ldflags(self, flags):
  81. assert not isinstance(flags, str)
  82. for f in CmakeInvocation._ldflags:
  83. self._flags[f].extend(flags)
  84. def to_args(self):
  85. args = self._prefix.copy()
  86. for key, value in sorted(self._flags.items()):
  87. if isinstance(value, list):
  88. # We preload all of the list-y values (cflags, ...). If we've
  89. # nothing to add, don't.
  90. if not value:
  91. continue
  92. value = ' '.join(value)
  93. arg = '-D' + key
  94. if value != '':
  95. arg += '=' + value
  96. args.append(arg)
  97. return args
  98. class Env:
  99. def __init__(self, llvm_dir, use_make, output_dir, default_cmake_args,
  100. dry_run):
  101. self.llvm_dir = llvm_dir
  102. self.use_make = use_make
  103. self.output_dir = output_dir
  104. self.default_cmake_args = default_cmake_args.copy()
  105. self.dry_run = dry_run
  106. def get_default_cmake_args_kv(self):
  107. return self.default_cmake_args.items()
  108. def get_cmake_maker(self):
  109. return 'Ninja' if not self.use_make else 'Unix Makefiles'
  110. def get_make_command(self):
  111. if self.use_make:
  112. return ['make', '-j{}'.format(multiprocessing.cpu_count())]
  113. return ['ninja']
  114. def output_subdir(self, name):
  115. return os.path.join(self.output_dir, name)
  116. def has_llvm_subproject(self, name):
  117. if name == 'compiler-rt':
  118. subdir = 'projects/compiler-rt'
  119. elif name == 'clang':
  120. subdir = 'tools/clang'
  121. else:
  122. raise ValueError('Unknown subproject: %s' % name)
  123. return os.path.isdir(os.path.join(self.llvm_dir, subdir))
  124. # Note that we don't allow capturing stdout/stderr. This works quite nicely
  125. # with dry_run.
  126. def run_command(self,
  127. cmd,
  128. cwd=None,
  129. check=False,
  130. silent_unless_error=False):
  131. cmd_str = ' '.join(shlex.quote(s) for s in cmd)
  132. print(
  133. 'Running `%s` in %s' % (cmd_str, shlex.quote(cwd or os.getcwd())))
  134. if self.dry_run:
  135. return
  136. if silent_unless_error:
  137. stdout, stderr = subprocess.PIPE, subprocess.STDOUT
  138. else:
  139. stdout, stderr = None, None
  140. # Don't use subprocess.run because it's >= py3.5 only, and it's not too
  141. # much extra effort to get what it gives us anyway.
  142. popen = subprocess.Popen(
  143. cmd,
  144. stdin=subprocess.DEVNULL,
  145. stdout=stdout,
  146. stderr=stderr,
  147. cwd=cwd)
  148. stdout, _ = popen.communicate()
  149. return_code = popen.wait(timeout=0)
  150. if not return_code:
  151. return
  152. if silent_unless_error:
  153. print(stdout.decode('utf-8', 'ignore'))
  154. if check:
  155. raise subprocess.CalledProcessError(
  156. returncode=return_code, cmd=cmd, output=stdout, stderr=None)
  157. def _get_default_cmake_invocation(env):
  158. inv = CmakeInvocation(
  159. cmake='cmake', maker=env.get_cmake_maker(), cmake_dir=env.llvm_dir)
  160. for key, value in env.get_default_cmake_args_kv():
  161. inv.add_new_flag(key, value)
  162. return inv
  163. def _get_cmake_invocation_for_bootstrap_from(env, out_dir,
  164. skip_tablegens=True):
  165. clang = os.path.join(out_dir, 'bin', 'clang')
  166. cmake = _get_default_cmake_invocation(env)
  167. cmake.add_new_flag('CMAKE_C_COMPILER', clang)
  168. cmake.add_new_flag('CMAKE_CXX_COMPILER', clang + '++')
  169. # We often get no value out of building new tblgens; the previous build
  170. # should have them. It's still correct to build them, just slower.
  171. def add_tablegen(key, binary):
  172. path = os.path.join(out_dir, 'bin', binary)
  173. # Check that this exists, since the user's allowed to specify their own
  174. # stage1 directory (which is generally where we'll source everything
  175. # from). Dry runs should hope for the best from our user, as well.
  176. if env.dry_run or os.path.exists(path):
  177. cmake.add_new_flag(key, path)
  178. if skip_tablegens:
  179. add_tablegen('LLVM_TABLEGEN', 'llvm-tblgen')
  180. add_tablegen('CLANG_TABLEGEN', 'clang-tblgen')
  181. return cmake
  182. def _build_things_in(env, target_dir, what):
  183. cmd = env.get_make_command() + what
  184. env.run_command(cmd, cwd=target_dir, check=True)
  185. def _run_fresh_cmake(env, cmake, target_dir):
  186. if not env.dry_run:
  187. try:
  188. shutil.rmtree(target_dir)
  189. except FileNotFoundError:
  190. pass
  191. os.makedirs(target_dir, mode=0o755)
  192. cmake_args = cmake.to_args()
  193. env.run_command(
  194. cmake_args, cwd=target_dir, check=True, silent_unless_error=True)
  195. def _build_stage1_clang(env):
  196. target_dir = env.output_subdir('stage1')
  197. cmake = _get_default_cmake_invocation(env)
  198. _run_fresh_cmake(env, cmake, target_dir)
  199. _build_things_in(env, target_dir, what=['clang', 'llvm-profdata', 'profile'])
  200. return target_dir
  201. def _generate_instrumented_clang_profile(env, stage1_dir, profile_dir,
  202. output_file):
  203. llvm_profdata = os.path.join(stage1_dir, 'bin', 'llvm-profdata')
  204. if env.dry_run:
  205. profiles = [os.path.join(profile_dir, '*.profraw')]
  206. else:
  207. profiles = [
  208. os.path.join(profile_dir, f) for f in os.listdir(profile_dir)
  209. if f.endswith('.profraw')
  210. ]
  211. cmd = [llvm_profdata, 'merge', '-output=' + output_file] + profiles
  212. env.run_command(cmd, check=True)
  213. def _build_instrumented_clang(env, stage1_dir):
  214. assert os.path.isabs(stage1_dir)
  215. target_dir = os.path.join(env.output_dir, 'instrumented')
  216. cmake = _get_cmake_invocation_for_bootstrap_from(env, stage1_dir)
  217. cmake.add_new_flag('LLVM_BUILD_INSTRUMENTED', 'IR')
  218. # libcxx's configure step messes with our link order: we'll link
  219. # libclang_rt.profile after libgcc, and the former requires atexit from the
  220. # latter. So, configure checks fail.
  221. #
  222. # Since we don't need libcxx or compiler-rt anyway, just disable them.
  223. cmake.add_new_flag('LLVM_BUILD_RUNTIME', 'No')
  224. _run_fresh_cmake(env, cmake, target_dir)
  225. _build_things_in(env, target_dir, what=['clang', 'lld'])
  226. profiles_dir = os.path.join(target_dir, 'profiles')
  227. return target_dir, profiles_dir
  228. def _build_optimized_clang(env, stage1_dir, profdata_file):
  229. if not env.dry_run and not os.path.exists(profdata_file):
  230. raise ValueError('Looks like the profdata file at %s doesn\'t exist' %
  231. profdata_file)
  232. target_dir = os.path.join(env.output_dir, 'optimized')
  233. cmake = _get_cmake_invocation_for_bootstrap_from(env, stage1_dir)
  234. cmake.add_new_flag('LLVM_PROFDATA_FILE', os.path.abspath(profdata_file))
  235. # We'll get complaints about hash mismatches in `main` in tools/etc. Ignore
  236. # it.
  237. cmake.add_cflags(['-Wno-backend-plugin'])
  238. _run_fresh_cmake(env, cmake, target_dir)
  239. _build_things_in(env, target_dir, what=['clang'])
  240. return target_dir
  241. Args = collections.namedtuple('Args', [
  242. 'do_optimized_build',
  243. 'include_debug_info',
  244. 'profile_location',
  245. 'stage1_dir',
  246. ])
  247. def _parse_args():
  248. parser = argparse.ArgumentParser(
  249. description='Builds LLVM and Clang with instrumentation, collects '
  250. 'instrumentation profiles for them, and (optionally) builds things'
  251. 'with these PGO profiles. By default, it\'s assumed that you\'re '
  252. 'running this from your LLVM root, and all build artifacts will be '
  253. 'saved to $PWD/out.')
  254. parser.add_argument(
  255. '--cmake-extra-arg',
  256. action='append',
  257. default=[],
  258. help='an extra arg to pass to all cmake invocations. Note that this '
  259. 'is interpreted as a -D argument, e.g. --cmake-extra-arg FOO=BAR will '
  260. 'be passed as -DFOO=BAR. This may be specified multiple times.')
  261. parser.add_argument(
  262. '--dry-run',
  263. action='store_true',
  264. help='print commands instead of running them')
  265. parser.add_argument(
  266. '--llvm-dir',
  267. default='.',
  268. help='directory containing an LLVM checkout (default: $PWD)')
  269. parser.add_argument(
  270. '--no-optimized-build',
  271. action='store_true',
  272. help='disable the final, PGO-optimized build')
  273. parser.add_argument(
  274. '--out-dir',
  275. help='directory to write artifacts to (default: $llvm_dir/out)')
  276. parser.add_argument(
  277. '--profile-output',
  278. help='where to output the profile (default is $out/pgo_profile.prof)')
  279. parser.add_argument(
  280. '--stage1-dir',
  281. help='instead of having an initial build of everything, use the given '
  282. 'directory. It is expected that this directory will have clang, '
  283. 'llvm-profdata, and the appropriate libclang_rt.profile already built')
  284. parser.add_argument(
  285. '--use-debug-info-in-benchmark',
  286. action='store_true',
  287. help='use a regular build instead of RelWithDebInfo in the benchmark. '
  288. 'This increases benchmark execution time and disk space requirements, '
  289. 'but gives more coverage over debuginfo bits in LLVM and clang.')
  290. parser.add_argument(
  291. '--use-make',
  292. action='store_true',
  293. default=shutil.which('ninja') is None,
  294. help='use Makefiles instead of ninja')
  295. args = parser.parse_args()
  296. llvm_dir = os.path.abspath(args.llvm_dir)
  297. if args.out_dir is None:
  298. output_dir = os.path.join(llvm_dir, 'out')
  299. else:
  300. output_dir = os.path.abspath(args.out_dir)
  301. extra_args = {'CMAKE_BUILD_TYPE': 'Release'}
  302. for arg in args.cmake_extra_arg:
  303. if arg.startswith('-D'):
  304. arg = arg[2:]
  305. elif arg.startswith('-'):
  306. raise ValueError('Unknown not- -D arg encountered; you may need '
  307. 'to tweak the source...')
  308. split = arg.split('=', 1)
  309. if len(split) == 1:
  310. key, val = split[0], ''
  311. else:
  312. key, val = split
  313. extra_args[key] = val
  314. env = Env(
  315. default_cmake_args=extra_args,
  316. dry_run=args.dry_run,
  317. llvm_dir=llvm_dir,
  318. output_dir=output_dir,
  319. use_make=args.use_make,
  320. )
  321. if args.profile_output is not None:
  322. profile_location = args.profile_output
  323. else:
  324. profile_location = os.path.join(env.output_dir, 'pgo_profile.prof')
  325. result_args = Args(
  326. do_optimized_build=not args.no_optimized_build,
  327. include_debug_info=args.use_debug_info_in_benchmark,
  328. profile_location=profile_location,
  329. stage1_dir=args.stage1_dir,
  330. )
  331. return env, result_args
  332. def _looks_like_llvm_dir(directory):
  333. """Arbitrary set of heuristics to determine if `directory` is an llvm dir.
  334. Errs on the side of false-positives."""
  335. contents = set(os.listdir(directory))
  336. expected_contents = [
  337. 'CODE_OWNERS.TXT',
  338. 'cmake',
  339. 'docs',
  340. 'include',
  341. 'utils',
  342. ]
  343. if not all(c in contents for c in expected_contents):
  344. return False
  345. try:
  346. include_listing = os.listdir(os.path.join(directory, 'include'))
  347. except NotADirectoryError:
  348. return False
  349. return 'llvm' in include_listing
  350. def _die(*args, **kwargs):
  351. kwargs['file'] = sys.stderr
  352. print(*args, **kwargs)
  353. sys.exit(1)
  354. def _main():
  355. env, args = _parse_args()
  356. if not _looks_like_llvm_dir(env.llvm_dir):
  357. _die('Looks like %s isn\'t an LLVM directory; please see --help' %
  358. env.llvm_dir)
  359. if not env.has_llvm_subproject('clang'):
  360. _die('Need a clang checkout at tools/clang')
  361. if not env.has_llvm_subproject('compiler-rt'):
  362. _die('Need a compiler-rt checkout at projects/compiler-rt')
  363. def status(*args):
  364. print(*args, file=sys.stderr)
  365. if args.stage1_dir is None:
  366. status('*** Building stage1 clang...')
  367. stage1_out = _build_stage1_clang(env)
  368. else:
  369. stage1_out = args.stage1_dir
  370. status('*** Building instrumented clang...')
  371. instrumented_out, profile_dir = _build_instrumented_clang(env, stage1_out)
  372. status('*** Running profdata benchmarks...')
  373. _run_benchmark(env, instrumented_out, args.include_debug_info)
  374. status('*** Generating profile...')
  375. _generate_instrumented_clang_profile(env, stage1_out, profile_dir,
  376. args.profile_location)
  377. print('Final profile:', args.profile_location)
  378. if args.do_optimized_build:
  379. status('*** Building PGO-optimized binaries...')
  380. optimized_out = _build_optimized_clang(env, stage1_out,
  381. args.profile_location)
  382. print('Final build directory:', optimized_out)
  383. if __name__ == '__main__':
  384. _main()