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