1#!/usr/bin/env python3 2# Copyright 2023 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""This script generates a PGO profile for llvm-next. 7 8Do not run it inside of a chroot. It establishes a chroot of its own. 9""" 10 11import argparse 12import dataclasses 13import logging 14import os 15from pathlib import Path 16import re 17import shlex 18import shutil 19import sys 20from typing import List 21 22import pgo_tools 23 24 25@dataclasses.dataclass(frozen=True) 26class ChrootInfo: 27 """Info that describes a unique chroot.""" 28 29 chroot_name: str 30 out_dir_name: str 31 32 33def find_repo_root(base_dir: Path) -> Path: 34 """Returns the root of the user's ChromeOS checkout.""" 35 if (base_dir / ".repo").exists(): 36 return base_dir 37 38 for parent in base_dir.parents: 39 if (parent / ".repo").exists(): 40 return parent 41 42 raise ValueError(f"No repo found above {base_dir}") 43 44 45def create_fresh_bootstrap_chroot(repo_root: Path, chroot_info: ChrootInfo): 46 """Creates a `--bootstrap` chroot without any updates applied.""" 47 pgo_tools.run( 48 [ 49 "cros_sdk", 50 "--replace", 51 f"--chroot={chroot_info.chroot_name}", 52 f"--out-dir={chroot_info.out_dir_name}", 53 "--bootstrap", 54 "--skip-chroot-upgrade", 55 ], 56 cwd=repo_root, 57 ) 58 59 60def generate_pgo_profile( 61 repo_root: Path, 62 chroot_info: ChrootInfo, 63 chroot_output_file: Path, 64 use_var: str, 65): 66 """Generates a PGO profile to `chroot_output_file`.""" 67 pgo_tools.run( 68 [ 69 "cros_sdk", 70 f"--chroot={chroot_info.chroot_name}", 71 f"--out-dir={chroot_info.out_dir_name}", 72 "--skip-chroot-upgrade", 73 f"USE={use_var}", 74 "--", 75 "/mnt/host/source/src/third_party/toolchain-utils/pgo_tools/" 76 "generate_pgo_profile.py", 77 f"--output={chroot_output_file}", 78 ], 79 cwd=repo_root, 80 ) 81 82 83def compress_pgo_profile(pgo_profile: Path) -> Path: 84 """Compresses a PGO profile for upload to gs://.""" 85 pgo_tools.run( 86 ["xz", "-9", "-k", pgo_profile], 87 ) 88 return Path(str(pgo_profile) + ".xz") 89 90 91def translate_chroot_path_to_out_of_chroot( 92 repo_root: Path, path: Path, info: ChrootInfo 93) -> Path: 94 """Translates a chroot path into an out-of-chroot path.""" 95 path_str = str(path) 96 assert path_str.startswith("/tmp"), path 97 # Remove the leading `/` from the output file so it joins properly. 98 return repo_root / info.out_dir_name / str(path)[1:] 99 100 101def locate_current_llvm_ebuild(repo_root: Path) -> Path: 102 """Returns the path to our current LLVM ebuild.""" 103 llvm_subdir = ( 104 repo_root / "src/third_party/chromiumos-overlay/sys-devel/llvm" 105 ) 106 candidates = [ 107 x for x in llvm_subdir.glob("*pre*ebuild") if not x.is_symlink() 108 ] 109 assert ( 110 len(candidates) == 1 111 ), f"Found {len(candidates)} viable ebuilds; expected 1: {candidates}" 112 return candidates[0] 113 114 115def parse_llvm_next_hash(llvm_ebuild_contents: str) -> List[str]: 116 """Parses the LLVM_NEXT hash from our LLVM ebuild.""" 117 matches = re.findall( 118 r'^LLVM_NEXT_HASH="([a-f0-9A-F]{40})" # r\d+$', 119 llvm_ebuild_contents, 120 re.MULTILINE, 121 ) 122 assert ( 123 len(matches) == 1 124 ), f"Got {len(matches)} matches for llvm hash; expected 1" 125 return matches[0] 126 127 128def determine_upload_command( 129 repo_root: Path, profile_path: Path 130) -> pgo_tools.Command: 131 """Returns a command that can be used to upload our PGO profile.""" 132 llvm_ebuild = locate_current_llvm_ebuild(repo_root) 133 llvm_next_hash = parse_llvm_next_hash( 134 llvm_ebuild.read_text(encoding="utf-8") 135 ) 136 upload_target = ( 137 "gs://chromeos-localmirror/distfiles/llvm-profdata-" 138 f"{llvm_next_hash}.xz" 139 ) 140 return [ 141 "gsutil", 142 "cp", 143 "-n", 144 "-a", 145 "public-read", 146 profile_path, 147 upload_target, 148 ] 149 150 151def main(argv: List[str]): 152 logging.basicConfig( 153 format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: " 154 "%(message)s", 155 level=logging.INFO, 156 ) 157 158 parser = argparse.ArgumentParser( 159 description=__doc__, 160 formatter_class=argparse.RawDescriptionHelpFormatter, 161 ) 162 parser.add_argument( 163 "--chroot", 164 default="llvm-next-pgo-chroot", 165 help=""" 166 Name of the chroot to create. Will be clobbered if it exists already. 167 """, 168 ) 169 parser.add_argument( 170 "--out-dir", 171 default="llvm-next-pgo-chroot_out", 172 help=""" 173 Name of the out/ directory to use. Will be clobbered if it exists 174 already. 175 """, 176 ) 177 parser.add_argument( 178 "--upload", 179 action="store_true", 180 help="Upload the profile after creation. Implies --compress.", 181 ) 182 parser.add_argument( 183 "--output", 184 type=Path, 185 help=""" 186 Additionally put the uncompressed profile at the this path after 187 creation. 188 """, 189 ) 190 # This flag is required because the most common use-case (pardon the pun) 191 # for this script is "generate the PGO profile for the next LLVM roll." It: 192 # - seems very easy to forget to apply `USE=llvm-next`, 193 # - is awkward to _force_ llvm-next silently, since the "most common" 194 # use-case is not the _only_ use-case, and 195 # - is awkward to have a duo of `--llvm-next` / `--no-llvm-next` flags, 196 # since a single `--use=` provides way more flexibility. 197 parser.add_argument( 198 "--use", 199 required=True, 200 help=""" 201 The value to set for the USE variable when generating the profile. If 202 you're the mage, you want --use=llvm-next. If you don't want to use 203 anything, just pass `--use=`. 204 """, 205 ) 206 opts = parser.parse_args(argv) 207 208 pgo_tools.exit_if_in_chroot() 209 210 repo_root = find_repo_root(Path(os.getcwd())) 211 logging.info("Repo root is %s", repo_root) 212 213 logging.info("Creating new SDK") 214 chroot_info = ChrootInfo(opts.chroot, opts.out_dir) 215 try: 216 create_fresh_bootstrap_chroot(repo_root, chroot_info) 217 chroot_profile_path = Path("/tmp/llvm-next-pgo-profile.prof") 218 generate_pgo_profile( 219 repo_root, chroot_info, chroot_profile_path, opts.use 220 ) 221 profile_path = translate_chroot_path_to_out_of_chroot( 222 repo_root, chroot_profile_path, chroot_info 223 ) 224 if opts.output: 225 shutil.copyfile(profile_path, opts.output) 226 227 compressed_profile_path = compress_pgo_profile(profile_path) 228 upload_command = determine_upload_command( 229 repo_root, compressed_profile_path 230 ) 231 if opts.upload: 232 pgo_tools.run(upload_command) 233 else: 234 friendly_upload_command = shlex.join(str(x) for x in upload_command) 235 logging.info( 236 "To upload the profile, run %r in %r", 237 friendly_upload_command, 238 repo_root, 239 ) 240 except: 241 logging.warning( 242 "NOTE: Chroot left at %s and out dir is left at %s. " 243 "If you don't plan to rerun this script, delete them.", 244 chroot_info.chroot_name, 245 chroot_info.out_dir_name, 246 ) 247 raise 248 else: 249 logging.info( 250 "Feel free to delete chroot %s and out dir %s when you're done " 251 "with them.", 252 chroot_info.chroot_name, 253 chroot_info.out_dir_name, 254 ) 255 256 257if __name__ == "__main__": 258 main(sys.argv[1:]) 259