xref: /aosp_15_r20/external/toolchain-utils/pgo_tools/benchmark_pgo_profiles.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li#!/usr/bin/env python3
2*760c253cSXin Li# Copyright 2023 The ChromiumOS Authors
3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be
4*760c253cSXin Li# found in the LICENSE file.
5*760c253cSXin Li
6*760c253cSXin Li"""Runs benchmarks, given potentially multiple PGO profiles.
7*760c253cSXin Li
8*760c253cSXin Li**This script is meant to be run from inside of the chroot.**
9*760c253cSXin Li
10*760c253cSXin LiThis script overwrites your chroot's LLVM temporarily, but if it runs to
11*760c253cSXin Licompletion, it will restore you to your previous version of LLVM. Care is taken
12*760c253cSXin Liso that the same baseline LLVM is used to build all LLVM versions this script
13*760c253cSXin Libenchmarks.
14*760c253cSXin Li"""
15*760c253cSXin Li
16*760c253cSXin Liimport argparse
17*760c253cSXin Liimport dataclasses
18*760c253cSXin Liimport enum
19*760c253cSXin Liimport json
20*760c253cSXin Liimport logging
21*760c253cSXin Lifrom pathlib import Path
22*760c253cSXin Liimport shlex
23*760c253cSXin Liimport shutil
24*760c253cSXin Liimport subprocess
25*760c253cSXin Liimport sys
26*760c253cSXin Lifrom typing import IO, List, Optional, Union
27*760c253cSXin Li
28*760c253cSXin Liimport pgo_tools
29*760c253cSXin Li
30*760c253cSXin Li
31*760c253cSXin Li# The full path to where `sys-devel/llvm` expects local profiles to be if
32*760c253cSXin Li# `USE=llvm_pgo_use_local` is specified.
33*760c253cSXin LiLOCAL_PROFILE_LOCATION = Path(
34*760c253cSXin Li    "/mnt/host/source/src/third_party/chromiumos-overlay",
35*760c253cSXin Li    "sys-devel/llvm/files/llvm-local.profdata",
36*760c253cSXin Li).resolve()
37*760c253cSXin Li
38*760c253cSXin LiCHROOT_HYPERFINE = Path.home() / ".cargo/bin/hyperfine"
39*760c253cSXin Li
40*760c253cSXin Li
41*760c253cSXin Liclass SpecialProfile(enum.Enum):
42*760c253cSXin Li    """An enum representing a 'special' (non-Path) profile."""
43*760c253cSXin Li
44*760c253cSXin Li    REMOTE = enum.auto()
45*760c253cSXin Li    NONE = enum.auto()
46*760c253cSXin Li
47*760c253cSXin Li    def __str__(self) -> str:
48*760c253cSXin Li        if self is self.REMOTE:
49*760c253cSXin Li            return "@remote"
50*760c253cSXin Li        if self is self.NONE:
51*760c253cSXin Li            return "@none"
52*760c253cSXin Li        raise ValueError(f"Unknown SpecialProfile value: {repr(self)}")
53*760c253cSXin Li
54*760c253cSXin Li
55*760c253cSXin Li@dataclasses.dataclass(frozen=True, eq=True)
56*760c253cSXin Liclass RunData:
57*760c253cSXin Li    """Data describing the results of one hyperfine run."""
58*760c253cSXin Li
59*760c253cSXin Li    tag: str
60*760c253cSXin Li    user_time: float
61*760c253cSXin Li    system_time: float
62*760c253cSXin Li
63*760c253cSXin Li    @staticmethod
64*760c253cSXin Li    def from_json(tag: str, json_contents: IO) -> "RunData":
65*760c253cSXin Li        """Converts a hyperfine JSON file's contents into a RunData."""
66*760c253cSXin Li        results = json.load(json_contents)["results"]
67*760c253cSXin Li        if len(results) != 1:
68*760c253cSXin Li            raise ValueError(f"Expected one run result; got {results}")
69*760c253cSXin Li        return RunData(
70*760c253cSXin Li            tag=tag,
71*760c253cSXin Li            user_time=results[0]["user"],
72*760c253cSXin Li            system_time=results[0]["system"],
73*760c253cSXin Li        )
74*760c253cSXin Li
75*760c253cSXin Li
76*760c253cSXin LiProfilePath = Union[SpecialProfile, Path]
77*760c253cSXin Li
78*760c253cSXin Li
79*760c253cSXin Lidef parse_profile_path(path: str) -> ProfilePath:
80*760c253cSXin Li    for p in SpecialProfile:
81*760c253cSXin Li        if path == str(p):
82*760c253cSXin Li            return p
83*760c253cSXin Li    return Path(path).resolve()
84*760c253cSXin Li
85*760c253cSXin Li
86*760c253cSXin Lidef ensure_hyperfine_is_installed():
87*760c253cSXin Li    if CHROOT_HYPERFINE.exists():
88*760c253cSXin Li        return
89*760c253cSXin Li
90*760c253cSXin Li    logging.info("Installing hyperfine for benchmarking...")
91*760c253cSXin Li    pgo_tools.run(
92*760c253cSXin Li        [
93*760c253cSXin Li            "cargo",
94*760c253cSXin Li            "install",
95*760c253cSXin Li            "hyperfine",
96*760c253cSXin Li        ]
97*760c253cSXin Li    )
98*760c253cSXin Li    assert (
99*760c253cSXin Li        CHROOT_HYPERFINE.exists()
100*760c253cSXin Li    ), f"hyperfine was installed, but wasn't at {CHROOT_HYPERFINE}"
101*760c253cSXin Li
102*760c253cSXin Li
103*760c253cSXin Lidef construct_hyperfine_cmd(
104*760c253cSXin Li    llvm_ebuild: Path,
105*760c253cSXin Li    profile: ProfilePath,
106*760c253cSXin Li    llvm_binpkg: Path,
107*760c253cSXin Li    use_thinlto: bool,
108*760c253cSXin Li    export_json: Optional[Path] = None,
109*760c253cSXin Li) -> pgo_tools.Command:
110*760c253cSXin Li    if isinstance(profile, Path):
111*760c253cSXin Li        if profile != LOCAL_PROFILE_LOCATION:
112*760c253cSXin Li            shutil.copyfile(profile, LOCAL_PROFILE_LOCATION)
113*760c253cSXin Li        use_flags = "-llvm_pgo_use llvm_pgo_use_local"
114*760c253cSXin Li    elif profile is SpecialProfile.NONE:
115*760c253cSXin Li        use_flags = "-llvm_pgo_use"
116*760c253cSXin Li    elif profile is SpecialProfile.REMOTE:
117*760c253cSXin Li        use_flags = "llvm_pgo_use"
118*760c253cSXin Li    else:
119*760c253cSXin Li        raise ValueError(f"Unknown profile type: {type(profile)}")
120*760c253cSXin Li
121*760c253cSXin Li    quickpkg_restore = shlex.join(
122*760c253cSXin Li        str(x)
123*760c253cSXin Li        for x in pgo_tools.generate_quickpkg_restoration_command(llvm_binpkg)
124*760c253cSXin Li    )
125*760c253cSXin Li
126*760c253cSXin Li    setup_cmd = (
127*760c253cSXin Li        f"{quickpkg_restore} && "
128*760c253cSXin Li        f"sudo FEATURES=ccache USE={shlex.quote(use_flags)}"
129*760c253cSXin Li        # Use buildpkg-exclude so our existing llvm binpackage isn't
130*760c253cSXin Li        # overwritten.
131*760c253cSXin Li        "  emerge sys-devel/llvm --buildpkg-exclude=sys-devel/llvm"
132*760c253cSXin Li    )
133*760c253cSXin Li
134*760c253cSXin Li    if use_thinlto:
135*760c253cSXin Li        benchmark_use = "thinlto"
136*760c253cSXin Li    else:
137*760c253cSXin Li        benchmark_use = "-thinlto"
138*760c253cSXin Li
139*760c253cSXin Li    ebuild_llvm = (
140*760c253cSXin Li        f"sudo USE={shlex.quote(benchmark_use)} "
141*760c253cSXin Li        f"ebuild {shlex.quote(str(llvm_ebuild))}"
142*760c253cSXin Li    )
143*760c253cSXin Li    cmd: pgo_tools.Command = [
144*760c253cSXin Li        CHROOT_HYPERFINE,
145*760c253cSXin Li        "--max-runs=3",
146*760c253cSXin Li        f"--setup={setup_cmd}",
147*760c253cSXin Li        f"--prepare={ebuild_llvm} clean prepare",
148*760c253cSXin Li    ]
149*760c253cSXin Li
150*760c253cSXin Li    if export_json:
151*760c253cSXin Li        cmd.append(f"--export-json={export_json}")
152*760c253cSXin Li
153*760c253cSXin Li    cmd += (
154*760c253cSXin Li        "--",
155*760c253cSXin Li        # At the moment, building LLVM seems to be an OK benchmark. It has some
156*760c253cSXin Li        # C in it, some C++, and each pass on Cloudtops takes no more than 7
157*760c253cSXin Li        # minutes.
158*760c253cSXin Li        f"{ebuild_llvm} compile",
159*760c253cSXin Li    )
160*760c253cSXin Li    return cmd
161*760c253cSXin Li
162*760c253cSXin Li
163*760c253cSXin Lidef validate_profiles(
164*760c253cSXin Li    parser: argparse.ArgumentParser, profiles: List[ProfilePath]
165*760c253cSXin Li):
166*760c253cSXin Li    number_of_path_profiles = 0
167*760c253cSXin Li    nonexistent_profiles = []
168*760c253cSXin Li    seen_profile_at_local_profile_location = False
169*760c253cSXin Li    for profile in profiles:
170*760c253cSXin Li        if not isinstance(profile, Path):
171*760c253cSXin Li            continue
172*760c253cSXin Li
173*760c253cSXin Li        if not profile.exists():
174*760c253cSXin Li            nonexistent_profiles.append(profile)
175*760c253cSXin Li
176*760c253cSXin Li        number_of_path_profiles += 1
177*760c253cSXin Li        if profile == LOCAL_PROFILE_LOCATION:
178*760c253cSXin Li            seen_profile_at_local_profile_location = True
179*760c253cSXin Li
180*760c253cSXin Li    if number_of_path_profiles > 1 and seen_profile_at_local_profile_location:
181*760c253cSXin Li        parser.error(
182*760c253cSXin Li            f"Cannot use the path {LOCAL_PROFILE_LOCATION} as a profile if "
183*760c253cSXin Li            "there are other profiles specified by path."
184*760c253cSXin Li        )
185*760c253cSXin Li
186*760c253cSXin Li    if nonexistent_profiles:
187*760c253cSXin Li        nonexistent_profiles.sort()
188*760c253cSXin Li        parser.error(
189*760c253cSXin Li            "One or more profiles do not exist: " f"{nonexistent_profiles}"
190*760c253cSXin Li        )
191*760c253cSXin Li
192*760c253cSXin Li
193*760c253cSXin Lidef run_benchmark(
194*760c253cSXin Li    use_thinlto: bool,
195*760c253cSXin Li    profiles: List[ProfilePath],
196*760c253cSXin Li) -> List[RunData]:
197*760c253cSXin Li    """Runs the PGO benchmark with the given parameters.
198*760c253cSXin Li
199*760c253cSXin Li    Args:
200*760c253cSXin Li        use_thinlto: whether to benchmark the use of ThinLTO
201*760c253cSXin Li        profiles: profiles to benchmark with
202*760c253cSXin Li        collect_run_data: whether to return a CombinedRunData
203*760c253cSXin Li
204*760c253cSXin Li    Returns:
205*760c253cSXin Li        A CombinedRunData instance capturing the performance of the benchmark
206*760c253cSXin Li        runs.
207*760c253cSXin Li    """
208*760c253cSXin Li    ensure_hyperfine_is_installed()
209*760c253cSXin Li
210*760c253cSXin Li    llvm_ebuild_path = Path(
211*760c253cSXin Li        pgo_tools.run(
212*760c253cSXin Li            ["equery", "w", "sys-devel/llvm"], stdout=subprocess.PIPE
213*760c253cSXin Li        ).stdout.strip()
214*760c253cSXin Li    )
215*760c253cSXin Li
216*760c253cSXin Li    baseline_llvm_binpkg = pgo_tools.quickpkg_llvm()
217*760c253cSXin Li    accumulated_run_data = []
218*760c253cSXin Li    with pgo_tools.temporary_file(
219*760c253cSXin Li        prefix="benchmark_pgo_profile"
220*760c253cSXin Li    ) as tmp_json_file:
221*760c253cSXin Li        for profile in profiles:
222*760c253cSXin Li            cmd = construct_hyperfine_cmd(
223*760c253cSXin Li                llvm_ebuild_path,
224*760c253cSXin Li                profile,
225*760c253cSXin Li                baseline_llvm_binpkg,
226*760c253cSXin Li                use_thinlto=use_thinlto,
227*760c253cSXin Li                export_json=tmp_json_file,
228*760c253cSXin Li            )
229*760c253cSXin Li            # Format the profile with `repr(str(profile))` so that we always
230*760c253cSXin Li            # get a quoted, but human-friendly, representation of the profile.
231*760c253cSXin Li            logging.info(
232*760c253cSXin Li                "Profile %r: Running %s",
233*760c253cSXin Li                str(profile),
234*760c253cSXin Li                shlex.join(str(x) for x in cmd),
235*760c253cSXin Li            )
236*760c253cSXin Li            pgo_tools.run(cmd)
237*760c253cSXin Li
238*760c253cSXin Li            with tmp_json_file.open(encoding="utf-8") as f:
239*760c253cSXin Li                accumulated_run_data.append(RunData.from_json(str(profile), f))
240*760c253cSXin Li
241*760c253cSXin Li    logging.info("Restoring original LLVM...")
242*760c253cSXin Li    pgo_tools.run(
243*760c253cSXin Li        pgo_tools.generate_quickpkg_restoration_command(baseline_llvm_binpkg)
244*760c253cSXin Li    )
245*760c253cSXin Li    return accumulated_run_data
246*760c253cSXin Li
247*760c253cSXin Li
248*760c253cSXin Lidef main(argv: List[str]):
249*760c253cSXin Li    logging.basicConfig(
250*760c253cSXin Li        format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: "
251*760c253cSXin Li        "%(message)s",
252*760c253cSXin Li        level=logging.INFO,
253*760c253cSXin Li    )
254*760c253cSXin Li
255*760c253cSXin Li    parser = argparse.ArgumentParser(
256*760c253cSXin Li        description=__doc__,
257*760c253cSXin Li        formatter_class=argparse.RawDescriptionHelpFormatter,
258*760c253cSXin Li    )
259*760c253cSXin Li    parser.add_argument(
260*760c253cSXin Li        "--thinlto",
261*760c253cSXin Li        action="store_true",
262*760c253cSXin Li        help="If specified, this will benchmark builds with ThinLTO enabled.",
263*760c253cSXin Li    )
264*760c253cSXin Li    parser.add_argument(
265*760c253cSXin Li        "profile",
266*760c253cSXin Li        nargs="+",
267*760c253cSXin Li        type=parse_profile_path,
268*760c253cSXin Li        help=f"""
269*760c253cSXin Li        The path to a profile to benchmark. There are two special values here:
270*760c253cSXin Li        '{SpecialProfile.REMOTE}' and '{SpecialProfile.NONE}'. For
271*760c253cSXin Li        '{SpecialProfile.REMOTE}', this will just use the default LLVM PGO
272*760c253cSXin Li        profile for a benchmark run. For '{SpecialProfile.NONE}', all PGO will
273*760c253cSXin Li        be disabled for a benchmark run.
274*760c253cSXin Li        """,
275*760c253cSXin Li    )
276*760c253cSXin Li    opts = parser.parse_args(argv)
277*760c253cSXin Li
278*760c253cSXin Li    pgo_tools.exit_if_not_in_chroot()
279*760c253cSXin Li
280*760c253cSXin Li    profiles = opts.profile
281*760c253cSXin Li    validate_profiles(parser, profiles)
282*760c253cSXin Li
283*760c253cSXin Li    run_benchmark(opts.thinlto, profiles)
284*760c253cSXin Li
285*760c253cSXin Li
286*760c253cSXin Liif __name__ == "__main__":
287*760c253cSXin Li    main(sys.argv[1:])
288