OSDN Git Service

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