xref: /aosp_15_r20/external/toolchain-utils/pgo_tools/create_chroot_and_generate_pgo_profile.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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