| # Copyright 2019 The ChromiumOS Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Toolchain and related functionality.""" |
| |
| import base64 |
| import collections |
| import datetime |
| import glob |
| import json |
| import logging |
| import os |
| from pathlib import Path |
| import re |
| import shutil |
| from typing import Any, Callable, Iterable, List, Optional, Tuple |
| |
| from chromite.lib import alerts |
| from chromite.lib import constants |
| from chromite.lib import cros_build_lib |
| from chromite.lib import gob_util |
| from chromite.lib import gs |
| from chromite.lib import osutils |
| from chromite.lib.parser import package_info |
| from chromite.utils import pformat |
| |
| |
| class PrepareForBuildReturn: |
| """Return values for PrepareForBuild call.""" |
| |
| UNSPECIFIED = 0 |
| # Build is necessary to generate artifacts. |
| NEEDED = 1 |
| # Defer to other artifacts. Used primarily for aggregation of artifact |
| # results. |
| UNKNOWN = 2 |
| # Artifacts are already generated. The build is pointless. |
| POINTLESS = 3 |
| |
| |
| # URLs |
| # FIXME(tcwang): Remove access to GS buckets from this lib. |
| # There are plans in the future to remove all network |
| # operations from chromite, including access to GS buckets. |
| # Need to use build API and recipes to communicate to GS buckets in |
| # the future. |
| BENCHMARK_AFDO_GS_URL = ( |
| "gs://chromeos-toolchain-artifacts/afdo/unvetted/benchmark" |
| ) |
| CWP_AFDO_GS_URL = "gs://chromeos-prebuilt/afdo-job/cwp/chrome/" |
| KERNEL_PROFILE_URL = "gs://chromeos-prebuilt/afdo-job/cwp/kernel/{arch}" |
| KERNEL_PROFILE_VETTED_URL = ( |
| "gs://chromeos-prebuilt/afdo-job/vetted/kernel/{arch}" |
| ) |
| RELEASE_PROFILE_VETTED_URL = "gs://chromeos-prebuilt/afdo-job/vetted/release" |
| |
| # Constants |
| AFDO_SUFFIX = ".afdo" |
| BZ2_COMPRESSION_SUFFIX = ".bz2" |
| XZ_COMPRESSION_SUFFIX = ".xz" |
| KERNEL_AFDO_COMPRESSION_SUFFIX = ".gcov.xz" |
| # FIXME: we should only use constants.SOURCE_ROOT and use |
| # path_util.ToChrootPath to convert to inchroot path when needed. So we |
| # need fix all the use cases for this variable (we can remove all but one |
| # when legacy is retired). |
| TOOLCHAIN_UTILS_PATH = os.path.join( |
| constants.CHROOT_SOURCE_ROOT, "src/third_party/toolchain-utils" |
| ) |
| MERGED_AFDO_NAME = "chromeos-chrome-{arch}-{name}" |
| |
| # How old can the Kernel AFDO data be? (in days). |
| KERNEL_ALLOWED_STALE_DAYS = 42 |
| # How old can the Kernel AFDO data be before detective got noticed? (in days). |
| KERNEL_WARN_STALE_DAYS = 14 |
| # How old an Arm profile can be before it gets replaced with atom. |
| CHROME_ARM_CWP_ALLOWED_STALE_DAYS = 21 |
| |
| # For merging release Chrome profiles. |
| RELEASE_CWP_MERGE_WEIGHT = 75 |
| RELEASE_BENCHMARK_MERGE_WEIGHT = 100 - RELEASE_CWP_MERGE_WEIGHT |
| |
| # Paths used in AFDO generation. |
| _AFDO_GENERATE_LLVM_PROF = "/usr/bin/create_llvm_prof" |
| _CHROME_DEBUG_BIN = os.path.join( |
| "%(root)s", "%(sysroot)s/usr/lib/debug", "opt/google/chrome/chrome.debug" |
| ) |
| |
| # Set of boards that can generate the AFDO profile (can generate 'perf' |
| # data with LBR events). Currently, it needs to be a device that has |
| # at least 4GB of memory. |
| # |
| # This must be consistent with the definitions in autotest. |
| CHROME_AFDO_VERIFIER_BOARDS = { |
| "chell": "atom", |
| "eve": "bigcore", |
| "trogdor": "arm", |
| } |
| |
| AFDO_ALERT_RECIPIENTS = ["chromeos-toolchain-oncall1@google.com"] |
| |
| # Full path to the chromiumos-overlay directory. |
| _CHROMIUMOS_OVERLAY = os.path.join( |
| constants.SOURCE_ROOT, constants.CHROMIUMOS_OVERLAY_DIR |
| ) |
| |
| # RegExps |
| AFDO_ARTIFACT_EBUILD_REGEX = ( |
| r'(?P<bef>\b%s\b=)(?P<name>("[^"]*"|.*))(?P<aft>.*)' |
| ) |
| AFDO_ARTIFACT_EBUILD_REPL = r'\g<bef>"%s"\g<aft>' |
| |
| ChromeVersion = collections.namedtuple( |
| "ChromeVersion", ["major", "minor", "build", "patch", "revision"] |
| ) |
| |
| BENCHMARK_PROFILE_NAME_REGEX = r""" |
| ^chromeos-chrome-(?:\w+)- |
| (\d+)\. # Major |
| (\d+)\. # Minor |
| (\d+)\. # Build |
| (\d+) # Patch |
| (?:_rc)?-r(\d+) # Revision |
| (-merged)?\. |
| afdo(?:\.bz2)?$ # We don't care about the presence of .bz2, |
| # so we use the ignore-group '?:' operator. |
| """ |
| |
| BenchmarkProfileVersion = collections.namedtuple( |
| "BenchmarkProfileVersion", |
| ["major", "minor", "build", "patch", "revision", "is_merged"], |
| ) |
| |
| CWP_PROFILE_NAME_REGEX = r""" |
| ^R(\d+)- # Major |
| (\d+)\. # Build |
| (\d+)- # Patch |
| (\d+) # Clock; breaks ties sometimes. |
| (?:\.afdo|\.gcov)? # Optional: CWP profiles have ".afdo", |
| # historically we had ".gcov", and |
| # names in ebuilds sometimes don't |
| # have this part at all. |
| (?:\.xz)?$ # We don't care about the presence of xz |
| """ |
| |
| CWPProfileVersion = collections.namedtuple( |
| "CWPProfileVersion", ["major", "build", "patch", "clock"] |
| ) |
| |
| MERGED_PROFILE_NAME_REGEX = r""" |
| ^chromeos-chrome |
| -(?:amd64|arm) # prefix for release profile. |
| # CWP parts |
| -(?:\w+) # Profile type |
| -(\d+) # Major |
| -(\d+) # Build |
| \.(\d+) # Patch |
| -(\d+) # Clock; breaks ties sometimes. |
| # Benchmark parts |
| -benchmark |
| -(\d+) # Major |
| \.(\d+) # Minor |
| \.(\d+) # Build |
| \.(\d+) # Patch |
| -r(\d+) # Revision |
| -redacted\.afdo # suffix for release profile. |
| (?:\.xz)?$ |
| """ |
| |
| CHROME_ARCH_VERSION = "%(package)s-%(arch)s-%(version)s" |
| CHROME_PERF_AFDO_FILE = "%(package)s-%(arch)s-%(versionnorev)s.perf.data" |
| CHROME_BENCHMARK_AFDO_FILE = "%s%s" % (CHROME_ARCH_VERSION, AFDO_SUFFIX) |
| CHROME_DEBUG_BINARY_NAME = "%s.debug" % CHROME_ARCH_VERSION |
| |
| CHROME_BINARY_PATH = ( |
| "/var/cache/chromeos-chrome/chrome-src-internal/" |
| "src/out_{board}/Release/chrome" |
| ) |
| |
| # cros-artifacts go here in the chroot. |
| _PACKAGE_ARTIFACTS_PATH = "var/lib/chromeos/package-artifacts" |
| |
| |
| class Error(Exception): |
| """Base module error class.""" |
| |
| |
| class PrepareForBuildHandlerError(Error): |
| """Error for PrepareForBuildHandler class.""" |
| |
| |
| class BundleArtifactsHandlerError(Error): |
| """Error for BundleArtifactsHandler class.""" |
| |
| |
| class GetUpdatedFilesForCommitError(Error): |
| """Error for GetUpdatedFilesForCommit class.""" |
| |
| |
| class NoArtifactsToBundleError(Error): |
| """Error for bundling empty collection of artifacts.""" |
| |
| |
| class ProfilesNameHelperError(Error): |
| """Error for helper functions related to profile naming.""" |
| |
| |
| class UpdateEbuildWithAFDOArtifactsError(Error): |
| """Error for UpdateEbuildWithAFDOArtifacts class.""" |
| |
| |
| class NoProfilesInGsBucketError(Error): |
| """Raised when _FindLatestAFDOArtifact doesn't find profiles.""" |
| |
| |
| def _ParseBenchmarkProfileName(profile_name): |
| """Parse the name of a benchmark profile for Chrome. |
| |
| Examples: |
| with input: profile_name='chromeos-chrome-amd64-77.0.3849.0_rc-r1.afdo' |
| the function returns: |
| BenchmarkProfileVersion( |
| major=77, minor=0, build=3849, patch=0, revision=1, is_merged=False) |
| |
| Args: |
| profile_name: The name of a benchmark profile. |
| |
| Returns: |
| Named tuple of BenchmarkProfileVersion if the profile is parsable |
| |
| Raises if the name is not parsable. |
| """ |
| pattern = re.compile(BENCHMARK_PROFILE_NAME_REGEX, re.VERBOSE) |
| match = pattern.match(profile_name) |
| if not match: |
| raise ProfilesNameHelperError( |
| "Unparseable benchmark profile name: %s" % profile_name |
| ) |
| |
| groups = match.groups() |
| version_groups = groups[:-1] |
| is_merged = groups[-1] |
| return BenchmarkProfileVersion( |
| *[int(x) for x in version_groups], is_merged=bool(is_merged) |
| ) |
| |
| |
| def _ParseCWPProfileName(profile_name): |
| """Parse the name of a CWP profile for Chrome. |
| |
| Examples: |
| With input profile_name='R77-3809.38-1562580965.afdo', |
| the function returns: |
| CWPProfileVersion(major=77, build=3809, patch=38, clock=1562580965) |
| |
| Args: |
| profile_name: The name of a CWP profile. |
| |
| Returns: |
| Named tuple of CWPProfileVersion. |
| """ |
| pattern = re.compile(CWP_PROFILE_NAME_REGEX, re.VERBOSE) |
| match = pattern.match(profile_name) |
| if not match: |
| raise ProfilesNameHelperError( |
| "Unparseable CWP profile name: %s" % profile_name |
| ) |
| return CWPProfileVersion(*[int(x) for x in match.groups()]) |
| |
| |
| def _ParseMergedProfileName( |
| artifact_name: str, |
| ) -> Tuple[BenchmarkProfileVersion, CWPProfileVersion]: |
| """Parse the name of a release profile for Chrome. |
| |
| Examples: |
| With input: profile_name='chromeos-chrome-amd64 |
| -field-77-3809.38-1562580965 |
| -benchmark-77.0.3849.0_rc-r1.afdo.xz' |
| the function returns: |
| ( |
| BenchmarkProfileVersion( |
| major=77, |
| minor=0, |
| build=3849, |
| patch=0, |
| revision=1, |
| is_merged=False, |
| ), |
| CWPProfileVersion(major=77, build=3809, patch=38, clock=1562580965) |
| ) |
| |
| Args: |
| artifact_name: The name of a release AFDO profile. |
| |
| Returns: |
| A tuple of (BenchmarkProfileVersion, CWPProfileVersion) |
| """ |
| pattern = re.compile(MERGED_PROFILE_NAME_REGEX, re.VERBOSE) |
| match = pattern.match(artifact_name) |
| if not match: |
| raise ProfilesNameHelperError( |
| "Unparseable merged AFDO name: %s" % artifact_name |
| ) |
| groups = match.groups() |
| cwp_groups = groups[:4] |
| benchmark_groups = groups[4:] |
| return ( |
| BenchmarkProfileVersion( |
| *[int(x) for x in benchmark_groups], is_merged=False |
| ), |
| CWPProfileVersion(*[int(x) for x in cwp_groups]), |
| ) |
| |
| |
| def _GetCombinedAFDOName(cwp_versions, cwp_arch, benchmark_versions): |
| """Construct a name mixing CWP and benchmark AFDO names. |
| |
| Examples: |
| If benchmark AFDO is BenchmarkProfileVersion( |
| major=77, minor=0, build=3849, patch=0, revision=1, is_merged=False) |
| and CWP AFDO is CWPProfileVersion( |
| major=77, build=3809, patch=38, clock=1562580965), |
| and cwp_arch is 'atom', |
| the returned name is: |
| atom-77-3809.38-1562580965-benchmark-77.0.3849.0-r1 |
| |
| Args: |
| cwp_versions: CWP profile as a namedtuple CWPProfileVersion. |
| cwp_arch: Architecture used to differentiate CWP profiles. |
| benchmark_versions: Benchmark profile as a namedtuple |
| BenchmarkProfileVersion. |
| |
| Returns: |
| A name using the combination of CWP + benchmark AFDO names. |
| """ |
| cwp_piece = "%s-%d-%d.%d-%d" % ( |
| cwp_arch, |
| cwp_versions.major, |
| cwp_versions.build, |
| cwp_versions.patch, |
| cwp_versions.clock, |
| ) |
| benchmark_piece = "benchmark-%d.%d.%d.%d-r%d" % ( |
| benchmark_versions.major, |
| benchmark_versions.minor, |
| benchmark_versions.build, |
| benchmark_versions.patch, |
| benchmark_versions.revision, |
| ) |
| return "%s-%s" % (cwp_piece, benchmark_piece) |
| |
| |
| def _CompressAFDOFiles( |
| targets: Iterable[Path], input_dir: Path, output_dir: Path, suffix: str |
| ) -> List[Path]: |
| """Compress files using AFDO compression type. |
| |
| Args: |
| targets: List of files to compress. Only the basename is needed. |
| input_dir: Paths to the targets (outside chroot). If None, use |
| the targets as full path. |
| output_dir: Paths to save the compressed file (outside chroot). |
| suffix: Compression suffix. |
| |
| Returns: |
| List of full paths of the generated tarballs. |
| |
| Raises: |
| RuntimeError if the file to compress does not exist. |
| """ |
| ret = [] |
| for t in targets: |
| name = os.path.basename(t) |
| compressed = name + suffix |
| if input_dir: |
| input_path = os.path.join(input_dir, name) |
| else: |
| input_path = t |
| if not os.path.exists(input_path): |
| raise RuntimeError( |
| "file %s to compress does not exist" % input_path |
| ) |
| output_path = os.path.join(output_dir, compressed) |
| cros_build_lib.CompressFile(input_path, output_path) |
| logging.info( |
| "_CompressAFDOFiles produced %s, size %.1fMB", |
| output_path, |
| os.path.getsize(output_path) / (1024 * 1024), |
| ) |
| ret.append(output_path) |
| return ret |
| |
| |
| def _RankValidCWPProfiles(name: str) -> int: |
| """Calculate a value used to rank valid CWP profiles. |
| |
| Args: |
| name: A name or a full path of a possible CWP profile. |
| |
| Returns: |
| The "clock" part of the CWP profile, used for ranking if the |
| name is a valid CWP profile. Otherwise, returns None. |
| """ |
| try: |
| return _ParseCWPProfileName(os.path.basename(name)).clock |
| except ProfilesNameHelperError: |
| return None |
| |
| |
| def _GetProfileAge(profile: str, artifact_type: str) -> int: |
| """Tell the age of profile_version in days. |
| |
| Args: |
| profile: Name of the profile. Different artifact_type has different |
| format. For kernel_afdo, it looks like: R78-12371.11-1565602499. |
| The last part is the timestamp. |
| artifact_type: Only 'kernel_afdo' and 'cwp' are supported now. |
| |
| Returns: |
| Age of profile_version in days. |
| |
| Raises: |
| ValueError: if the artifact_type is not supported. |
| """ |
| if artifact_type in ("kernel_afdo", "cwp"): |
| return ( |
| datetime.datetime.now(tz=datetime.timezone.utc) |
| - datetime.datetime.fromtimestamp( |
| int(profile.split("-")[-1]), datetime.timezone.utc |
| ) |
| ).days |
| |
| raise ValueError( |
| f"'{artifact_type}' is currently not supported to check profile age." |
| ) |
| |
| |
| def _WarnDetectiveAboutKernelProfileExpiration( |
| kver: str, profile_path: str |
| ) -> None: |
| """Send emails to toolchain detective to warn the soon expired profiles. |
| |
| Args: |
| kver: Kernel version. |
| profile_path: Absolute path to the profile. |
| """ |
| subject_msg = ( |
| f"[Test Async builder] Kernel AutoFDO profile too old for kernel {kver}" |
| ) |
| alert_msg = ( |
| f"The latest AutoFDO profile is too old for the kernel {kver}.\n" |
| f"Path={profile_path}.\n" |
| "Check if this is a known bug at " |
| "https://buganizer.corp.google.com/issues?q=componentid:87200" |
| "%20%22AutoFDO%20profile%20generation%20for%20kernel%22 or contact " |
| "the cwp-team@google.com." |
| ) |
| alerts.SendEmailLog( |
| subject_msg, |
| AFDO_ALERT_RECIPIENTS, |
| message=alert_msg, |
| ) |
| |
| |
| _EbuildInfo = collections.namedtuple("_EbuildInfo", ["path", "CPV"]) |
| |
| |
| class _CommonPrepareBundle: |
| """Information about Ebuild files we care about.""" |
| |
| def __init__( |
| self, |
| artifact_name, |
| chroot=None, |
| sysroot_path=None, |
| build_target=None, |
| input_artifacts=None, |
| profile_info=None, |
| ) -> None: |
| self._gs_context = None |
| self.artifact_name = artifact_name |
| self.chroot = chroot |
| self.sysroot_path = sysroot_path |
| self.build_target = build_target |
| self.input_artifacts = input_artifacts or {} |
| self.profile_info = profile_info or {} |
| self.profile = self.profile_info.get("chrome_cwp_profile", "") |
| self.arch = self.profile_info.get("arch", "") |
| if profile_info and not self.arch: |
| raise ValueError("No 'arch' specified in ArtifactProfileInfo") |
| self._ebuild_info = {} |
| |
| @property |
| def gs_context(self): |
| """Get the current GS context. May create one.""" |
| if not self._gs_context: |
| self._gs_context = gs.GSContext() |
| return self._gs_context |
| |
| @property |
| def chrome_branch(self): |
| """Return the branch number for chrome.""" |
| pkg = constants.CHROME_PN |
| info = self._ebuild_info.get(pkg, self._GetEbuildInfo(pkg)) |
| return info.CPV.version.split(".")[0] |
| |
| def _GetEbuildInfo( |
| self, package: str, category: Optional[str] = None |
| ) -> _EbuildInfo: |
| """Get the ebuild info for a category/package in chromiumos-overlay. |
| |
| Args: |
| package: package name (e.g. chromeos-chrome or chromeos-kernel-4_4) |
| category: category (e.g. chromeos-base, or sys-kernel) |
| |
| Returns: |
| _EbuildInfo for the stable ebuild. |
| """ |
| if package in self._ebuild_info: |
| return self._ebuild_info[package] |
| |
| if category is None: |
| if package == constants.CHROME_PN: |
| category = constants.CHROME_CN |
| else: |
| category = "sys-kernel" |
| |
| # The stable ebuild path has at least one '.' in the version. |
| glob_path_str = os.path.join( |
| _CHROMIUMOS_OVERLAY, |
| category, |
| package, |
| "*-*.*.ebuild", |
| ) |
| paths = glob.glob(glob_path_str) |
| logging.info("Glob path %s yielded: %s", glob_path_str, paths) |
| if len(paths) == 1: |
| PV = os.path.splitext(os.path.split(paths[0])[1])[0] |
| info = _EbuildInfo( |
| paths[0], package_info.parse("%s/%s" % (category, PV)) |
| ) |
| self._ebuild_info[constants.CHROME_PN] = info |
| return info |
| else: |
| latest_version = ChromeVersion(0, 0, 0, 0, 0) |
| candidate = None |
| for p in paths: |
| PV = os.path.splitext(os.path.split(p)[1])[0] |
| info = _EbuildInfo( |
| p, package_info.parse("%s/%s" % (category, PV)) |
| ) |
| if not info.CPV.revision: |
| # Ignore versions without a rev |
| continue |
| version_re = re.compile( |
| r"^chromeos-chrome-(\d+)\.(\d+)\.(\d+)\.(\d+)_rc-r(\d+)" |
| ) |
| m = version_re.search(PV) |
| assert m, f"failed to recognize Chrome ebuild name {p}" |
| version = ChromeVersion(*[int(x) for x in m.groups()]) |
| if version > latest_version: |
| latest_version = version |
| candidate = info |
| if not candidate: |
| raise PrepareForBuildHandlerError( |
| f"No valid Chrome ebuild found among: {paths}" |
| ) |
| self._ebuild_info[constants.CHROME_PN] = candidate |
| return candidate |
| |
| def _GetBenchmarkAFDOName( |
| self, template=CHROME_BENCHMARK_AFDO_FILE, wildcard_version=False |
| ): |
| """Get the name of the benchmark AFDO file from the Chrome ebuild. |
| |
| wildcard_version=True replaces chrome version with *. |
| """ |
| pkg = self._GetEbuildInfo(constants.CHROME_PN).CPV |
| if wildcard_version: |
| ver = "*" |
| vernorev = "*" |
| else: |
| ver = pkg.vr |
| vernorev = pkg.version.split("_")[0] |
| afdo_spec = { |
| "arch": self.arch, |
| "package": pkg.package, |
| "version": ver, |
| "versionnorev": vernorev, |
| } |
| return template % afdo_spec |
| |
| def _GetArtifactVersionInGob(self, profile_arch: str) -> str: |
| """Find the version (name) of AFDO artifact from GoB. |
| |
| Args: |
| profile_arch: There are two AFDO profiles in chromium: atom or |
| bigcore. |
| |
| Returns: |
| The name of the AFDO artifact found on GoB, or None if not found. |
| |
| Raises: |
| ValueError: when "profile_arch" is not a supported. |
| RuntimeError: when the file containing AFDO profile_arch name can't |
| be found. |
| """ |
| if profile_arch not in list(CHROME_AFDO_VERIFIER_BOARDS.values()): |
| raise ValueError( |
| f"Invalid architecture {profile_arch} to use in AFDO " |
| "profile_arch" |
| ) |
| |
| chrome_info = self._GetEbuildInfo(constants.CHROME_PN) |
| version = chrome_info.CPV.version |
| if version.endswith("_rc"): |
| version = version[:-3] |
| profile_path = ( |
| f"chromium/src/+/refs/tags/{version}/chromeos/profiles/" |
| f"{profile_arch}.afdo.newest.txt?format=text" |
| ) |
| |
| contents = gob_util.FetchUrl(constants.EXTERNAL_GOB_HOST, profile_path) |
| if not contents: |
| raise RuntimeError( |
| "Could not fetch https://" |
| f"{constants.EXTERNAL_GOB_HOST}/{profile_path}" |
| ) |
| |
| return base64.decodebytes(contents).decode("utf-8") |
| |
| def _GetArtifactVersionInEbuild(self, package, variable): |
| """Find the version (name) of AFDO artifact from the ebuild. |
| |
| Args: |
| package: name of the package (such as, 'chromeos-chrome') |
| variable: name of the variable to find. |
| |
| Returns: |
| The name of the AFDO artifact found in the ebuild, or None if not |
| found. |
| """ |
| info = self._GetEbuildInfo(package) |
| ebuild = info.path |
| pattern = re.compile(AFDO_ARTIFACT_EBUILD_REGEX % variable) |
| with open(ebuild, encoding="utf-8") as f: |
| for line in f: |
| match = pattern.search(line) |
| if match: |
| ret = match.group("name") |
| if ret.startswith('"') and ret.endswith('"'): |
| return ret[1:-1] |
| return ret |
| |
| logging.info("%s is not found in the ebuild: %s", variable, ebuild) |
| return None |
| |
| def _FindLatestAFDOArtifact( |
| self, |
| gs_urls: Iterable[str], |
| rank_func: Callable[[str], Any], |
| arch: str = "", |
| ) -> str: |
| """Find the latest AFDO artifact in a bucket. |
| |
| Args: |
| gs_urls: List of full gs:// directory paths to check. |
| rank_func: A function to compare two URLs. It is passed two URLs, |
| and returns whether the first is more or less preferred than the |
| second: |
| negative: less preferred. |
| zero: equally preferred. |
| positive: more preferred. |
| arch: Profile architecture, default is self.arch which is passed |
| from recipe. |
| |
| Returns: |
| The path of the latest eligible AFDO artifact. |
| |
| Raises: |
| NoProfilesInGsBucketError: If no profiles in GS bucket. |
| RuntimeError: If no valid latest profiles. |
| ValueError: if regex is not valid. |
| """ |
| if not arch: |
| arch = self.arch |
| |
| def _FilesOnBranch( |
| all_files: Iterable[gs.GSListResult], |
| branch: str, |
| ): |
| """Return the files that are on this branch. |
| |
| Legacy PFQ results look like: latest-chromeos-chrome-amd64-79.afdo. |
| The branch should appear in the name either as: |
| - R78-12371.22-1566207135 for kernel/CWP profiles, OR |
| - chromeos-chrome-amd64-78.0.3877.0 for benchmark profiles |
| |
| Args: |
| all_files: list of files from GS. |
| branch: branch number. |
| |
| Returns: |
| Files matching the branch. |
| """ |
| cwp_afdo_pattern = re.compile(rf"R{branch}") |
| # Search by the arch and branch number. |
| bench_afdo_pattern = re.compile(rf"chromeos-chrome-{arch}-{branch}") |
| # Search for the benchmark branch version and ignore the cwp |
| # version. When main branch switches from 100 to 101 and we are |
| # checking 100 branch we have to ignore |
| # "-field-100-*-benchmark-101-" profiles which are going to come |
| # from main. |
| results = [] |
| for x in all_files: |
| x_name = os.path.basename(x.url) |
| # Filter in CWP and benchmark AFDO. |
| if cwp_afdo_pattern.match(x_name) or bench_afdo_pattern.match( |
| x_name |
| ): |
| results.append(x) |
| |
| return results |
| |
| # Obtain all files from the gs_urls. |
| all_files = [] |
| for gs_url in gs_urls: |
| try: |
| all_files += self.gs_context.List(gs_url, details=True) |
| except gs.GSNoSuchKey: |
| pass |
| |
| results = _FilesOnBranch(all_files, self.chrome_branch) |
| if not results: |
| # If no results found, it's maybe because we just branched. |
| # Try to find the latest profile from last branch. |
| results = _FilesOnBranch( |
| all_files, str(int(self.chrome_branch) - 1) |
| ) |
| |
| if not results: |
| raise NoProfilesInGsBucketError( |
| "No files for branch %s found in %s" |
| % (self.chrome_branch, " ".join(gs_urls)) |
| ) |
| |
| latest = None |
| for res in results: |
| rank = rank_func(res.url) |
| if rank and (not latest or [rank, ""] > latest): |
| latest = [rank, res.url] |
| |
| if not latest: |
| raise RuntimeError( |
| f"No valid latest artifact was found in {','.join(gs_urls)}" |
| f" (example of invalid artifact: {results[0].url})." |
| ) |
| |
| name = latest[1] |
| logging.info("Latest AFDO artifact is %s", name) |
| return name |
| |
| def _AfdoTmpPath(self, path: str = "") -> str: |
| """Return the directory for benchmark-afdo-generate artifacts. |
| |
| Args: |
| path: path relative to the directory. |
| |
| Returns: |
| Path to the directory. |
| """ |
| gen_dir = "/tmp/benchmark-afdo-generate" |
| if path: |
| return os.path.join(gen_dir, path.lstrip(os.path.sep)) |
| else: |
| return gen_dir |
| |
| def _FindArtifact(self, name: str, gs_urls: Iterable[str]) -> Optional[str]: |
| """Find an artifact |name|, from a list of |gs_urls|. |
| |
| Args: |
| name: The name of the artifact (supports wildcards). |
| gs_urls: List of full gs:// directory paths to check. |
| |
| Returns: |
| The url of the located artifact, or None. |
| """ |
| for url in gs_urls: |
| path = os.path.join(url, name) |
| found_paths = self.gs_context.LS(path) |
| if found_paths: |
| if len(found_paths) > 1: |
| raise PrepareForBuildHandlerError( |
| f"Found {found_paths} artifacts at {url}. Expected ONE " |
| "file." |
| ) |
| return found_paths[0] |
| return None |
| |
| def _PatchEbuild(self, info, rules, uprev): |
| """Patch an ebuild file, possibly uprevving it. |
| |
| Args: |
| info: _EbuildInfo describing the ebuild file. |
| rules: dict of key:value pairs to apply to the ebuild. |
| uprev: whether to increment the revision. Should be False for 9999 |
| ebuilds, and True otherwise. |
| |
| Returns: |
| Updated CPV for the ebuild. |
| """ |
| logging.info("Patching %s with %s", info.path, str(rules)) |
| old_name = info.path |
| new_name = "%s.new" % old_name |
| |
| _Patterns = collections.namedtuple("_Patterns", ["match", "sub"]) |
| patterns = set( |
| _Patterns( |
| re.compile(AFDO_ARTIFACT_EBUILD_REGEX % k), |
| AFDO_ARTIFACT_EBUILD_REPL % v, |
| ) |
| for k, v in rules.items() |
| ) |
| |
| want = patterns.copy() |
| with open(old_name, encoding="utf-8") as old, open( |
| new_name, "w", encoding="utf-8" |
| ) as new: |
| for line in old: |
| for match, sub in patterns: |
| line, count = match.subn(sub, line, count=1) |
| if count: |
| want.remove((match, sub)) |
| # Can only match one pattern. |
| break |
| new.write(line) |
| if want: |
| logging.info( |
| "Unable to update %s in the ebuild", [x.sub for x in want] |
| ) |
| raise UpdateEbuildWithAFDOArtifactsError( |
| "Ebuild file does not have appropriate marker for AFDO." |
| ) |
| |
| CPV = info.CPV |
| if uprev: |
| assert CPV.version != "9999" |
| new_CPV = ( |
| f"{CPV.category}/{CPV.package}-{CPV.version}" |
| f"-r{CPV.revision + 1}" |
| ) |
| new_path = os.path.join( |
| os.path.dirname(info.path), |
| "%s.ebuild" % os.path.basename(new_CPV), |
| ) |
| os.rename(new_name, new_path) |
| osutils.SafeUnlink(old_name) |
| ebuild_file = new_path |
| CPV = _EbuildInfo(new_path, package_info.SplitCPV(new_CPV)) |
| else: |
| assert CPV.version == "9999" |
| os.rename(new_name, old_name) |
| ebuild_file = old_name |
| |
| if self.build_target: |
| ebuild_prog = "ebuild-%s" % self.build_target |
| cmd = [ |
| ebuild_prog, |
| self.chroot.chroot_path(ebuild_file), |
| "manifest", |
| "--force", |
| ] |
| self.chroot.run(cmd) |
| |
| return CPV |
| |
| @staticmethod |
| def _ValidBenchmarkProfileVersion(name): |
| """Calculate a value used to rank valid benchmark profiles. |
| |
| Args: |
| name: A name or a full path of a possible benchmark profile. |
| |
| Returns: |
| A BenchmarkProfileNamedTuple used for ranking if the name |
| of the benchmark profile is valid and it's not merged. |
| Otherwise, returns None. |
| """ |
| try: |
| version = _ParseBenchmarkProfileName(os.path.basename(name)) |
| # Filter out merged benchmark profiles. |
| if version.is_merged: |
| return None |
| return version |
| except ProfilesNameHelperError: |
| return None |
| |
| def _CreateReleaseChromeAFDO( |
| self, cwp_url, bench_url, output_dir, merged_name |
| ): |
| """Create an AFDO profile to be used in release Chrome. |
| |
| This means we want to merge the CWP and benchmark AFDO profiles into |
| one, and redact all ICF symbols. |
| |
| Args: |
| cwp_url: Full (GS) path to the discovered CWP file to use. |
| bench_url: Full (GS) path to the verified benchmark profile. |
| output_dir: A directory to store the created artifact. Must be |
| inside the chroot. |
| merged_name: Basename for the merged profile. |
| |
| Returns: |
| Full path to a generated release AFDO profile. |
| """ |
| # Download the compressed profiles from GS. |
| cwp_compressed = os.path.join(output_dir, os.path.basename(cwp_url)) |
| bench_compressed = os.path.join(output_dir, os.path.basename(bench_url)) |
| self.gs_context.Copy(cwp_url, cwp_compressed) |
| self.gs_context.Copy(bench_url, bench_compressed) |
| |
| # Decompress the files. |
| cwp_local = os.path.splitext(cwp_compressed)[0] |
| bench_local = os.path.splitext(bench_compressed)[0] |
| cros_build_lib.UncompressFile(cwp_compressed, cwp_local) |
| cros_build_lib.UncompressFile(bench_compressed, bench_local) |
| |
| # Merge profiles. |
| merge_weights = [ |
| (cwp_local, RELEASE_CWP_MERGE_WEIGHT), |
| (bench_local, RELEASE_BENCHMARK_MERGE_WEIGHT), |
| ] |
| merged_path = os.path.join(output_dir, merged_name) |
| self._MergeAFDOProfiles(merge_weights, merged_path) |
| |
| # Redact profiles. |
| redacted_path = merged_path + "-redacted.afdo" |
| # Trim the profile to contain 20k functions, as our current profile has |
| # ~20k functions so this modification brings less impact on prod. |
| self._ProcessAFDOProfile( |
| merged_path, |
| redacted_path, |
| redact=True, |
| remove=True, |
| reduce_functions=20000, |
| extbinary=True, |
| ) |
| |
| return redacted_path |
| |
| def _MergeAFDOProfiles( |
| self, profile_list, output_profile, use_extbinary=False |
| ) -> None: |
| """Merges the given profile list. |
| |
| This is ultimately derived from afdo.py, but runs OUTSIDE of the chroot. |
| It converts paths to chroot-relative paths, and runs llvm-profdata in |
| the chroot. |
| |
| Args: |
| profile_list: a list of (profile_path, profile_weight). |
| Profile_weight is an int that tells us how to weight the profile |
| relative to everything else. |
| output_profile: where to store the result profile. |
| use_extbinary: whether to use the new extensible binary AFDO |
| profile format. |
| """ |
| if not profile_list: |
| raise ValueError("Need profiles to merge") |
| |
| # A regular llvm-profdata command looks like: |
| # llvm-profdata merge [-sample] -output=/path/to/output input1 [...] |
| # |
| # Alternatively, we can specify inputs by `-weighted-input=A,file`, |
| # where A is a multiplier of the sample counts in the profile. |
| merge_command = [ |
| "llvm-profdata", |
| "merge", |
| "-sample", |
| "-output=" + self.chroot.chroot_path(output_profile), |
| ] + [ |
| "-weighted-input=%d,%s" % (weight, self.chroot.chroot_path(name)) |
| for name, weight in profile_list |
| ] |
| |
| # Here only because this was copied from afdo.py |
| if use_extbinary: |
| merge_command.append("--extbinary") |
| self.chroot.run(merge_command, print_cmd=True) |
| |
| def _ProcessAFDOProfile( |
| self, |
| input_path, |
| output_path, |
| redact=False, |
| remove=False, |
| reduce_functions=None, |
| extbinary=False, |
| ) -> None: |
| """Process the AFDO profile with different editings. |
| |
| In this function, we will convert an AFDO profile into textual version, |
| do the editings and convert it back. |
| |
| This function runs outside of the chroot, and enters the chroot. |
| |
| Args: |
| input_path: Full path (outside chroot) to input AFDO profile. |
| output_path: Full path (outside chroot) to output AFDO profile. |
| redact: Redact ICF'ed symbols from AFDO profiles. |
| ICF can cause inflation on AFDO sampling results, so we want to |
| remove them from AFDO profiles used for Chrome. |
| See http://crbug.com/916024 for more details. |
| remove: Remove indirect call targets from the given profile. |
| reduce_functions: Remove the cold functions in the profile until the |
| given number is met. |
| extbinary: Whether to convert the final profile into extbinary |
| type. |
| |
| Raises: |
| BundleArtifactsHandlerError: If the output profile is empty. |
| """ |
| profdata_command_base = ["llvm-profdata", "merge", "-sample"] |
| # Convert the extbinary profiles to text profiles. |
| input_to_text_temp = input_path + ".text.temp" |
| cmd_to_text = profdata_command_base + [ |
| "-text", |
| self.chroot.chroot_path(input_path), |
| "-output", |
| self.chroot.chroot_path(input_to_text_temp), |
| ] |
| self.chroot.run(cmd_to_text, print_cmd=True) |
| |
| current_input_file = input_to_text_temp |
| if redact: |
| # Call the redaction script. |
| redacted_temp = input_path + ".redacted.temp" |
| with open(current_input_file, "rb") as f: |
| self.chroot.run( |
| ["redact_textual_afdo_profile"], |
| input=f, |
| stdout=redacted_temp, |
| print_cmd=True, |
| ) |
| current_input_file = redacted_temp |
| |
| if remove: |
| # Call the remove indirect call script |
| removed_temp = input_path + ".removed.temp" |
| self.chroot.run( |
| [ |
| "remove_indirect_calls", |
| "--input=" + self.chroot.chroot_path(current_input_file), |
| "--output=" + self.chroot.chroot_path(removed_temp), |
| ], |
| print_cmd=True, |
| ) |
| current_input_file = removed_temp |
| |
| if reduce_functions: |
| # Remove cold functions in the profile. Trim the profile to contain |
| # 20k functions, as our current profile has ~20k functions so this |
| # modification brings less impact on prod. |
| reduced_tmp = input_path + ".reduced.tmp" |
| self.chroot.run( |
| [ |
| "remove_cold_functions", |
| "--input=" + self.chroot.chroot_path(current_input_file), |
| "--output=" + self.chroot.chroot_path(reduced_tmp), |
| "--number=" + str(reduce_functions), |
| ], |
| print_cmd=True, |
| ) |
| current_input_file = reduced_tmp |
| |
| # Convert the profiles back to binary profiles. |
| cmd_to_binary = profdata_command_base + [ |
| self.chroot.chroot_path(current_input_file), |
| "-output", |
| self.chroot.chroot_path(output_path), |
| ] |
| if extbinary: |
| # Using `extbinary` profiles saves us hundreds of MB of RAM per |
| # compilation, since it allows profiles to be lazily loaded. |
| cmd_to_binary.append("--extbinary") |
| self.chroot.run( |
| cmd_to_binary, |
| print_cmd=True, |
| ) |
| |
| profile_size = os.path.getsize(output_path) |
| logging.info( |
| "_ProcessAFDOProfile produced AFDO profile %s, size %.1fMB", |
| output_path, |
| profile_size / (1024 * 1024), |
| ) |
| # Verify the profile size. |
| # Empty profiles in a binary format can have a non-zero size |
| # because of the header but they won't exceed the page size. |
| # Normal profiles are usually >1MB. |
| if profile_size < 4096: |
| raise BundleArtifactsHandlerError( |
| "_ProcessAFDOProfile produced empty AFDO profile, " |
| f"{profile_size}" |
| ) |
| |
| def _CreateAndUploadMergedAFDOProfile( |
| self, unmerged_profile, output_dir, recent_to_merge=5, max_age_days=14 |
| ): |
| """Create a merged AFDO profile from recent AFDO profiles and upload it. |
| |
| Args: |
| unmerged_profile: Path to the AFDO profile we've just created. No |
| profiles whose names are lexicographically ordered after this |
| are candidates for selection. |
| output_dir: Path to location to store merged profiles for uploading. |
| recent_to_merge: The maximum number of profiles to merge (include |
| the current profile). |
| max_age_days: Don't merge profiles older than max_age_days days old. |
| |
| Returns: |
| The name of a merged profile if the AFDO profile is a candidate for |
| merging and ready to be merged and uploaded. Otherwise, None. |
| """ |
| if recent_to_merge == 1: |
| # Merging the unmerged_profile into itself is a NOP. |
| return None |
| |
| unmerged_name = os.path.basename(unmerged_profile) |
| merged_suffix = "-merged" |
| profile_suffix = AFDO_SUFFIX + BZ2_COMPRESSION_SUFFIX |
| benchmark_url = self.input_artifacts.get( |
| "UnverifiedChromeBenchmarkAfdoFile", [BENCHMARK_AFDO_GS_URL] |
| )[0] |
| try: |
| benchmark_listing = self.gs_context.List( |
| os.path.join( |
| benchmark_url, |
| f"chromeos-chrome-{self.arch}-*" + profile_suffix, |
| ), |
| details=True, |
| ) |
| except (gs.GSCommandError, gs.GSNoSuchKey): |
| # This can happen in a new GS bucket where there are no profiles |
| # yet. |
| logging.warning( |
| "Did not find valid benchmark profiles. Skip profile merge.", |
| ) |
| return None |
| |
| unmerged_version = _ParseBenchmarkProfileName(unmerged_name) |
| |
| def _GetOrderedMergeableProfiles( |
| benchmark_listing: Iterable[gs.GSListResult], |
| ) -> Iterable[gs.GSListResult]: |
| """Get list of mergeable profiles ordered by increasing version.""" |
| # Exclude merged profiles, because merging merged profiles into |
| # merged profiles is likely bad. _ValidBenchmarkProfileVersion takes |
| # care of it. |
| profile_versions = [ |
| (self._ValidBenchmarkProfileVersion(x.url), x) |
| for x in benchmark_listing |
| ] |
| # Filter in only necessary profiles. |
| candidates = sorted( |
| (version, x) |
| for version, x in profile_versions |
| if version and unmerged_version >= version |
| ) |
| return [x for _, x in candidates] |
| |
| benchmark_profiles = _GetOrderedMergeableProfiles(benchmark_listing) |
| if not benchmark_profiles: |
| logging.warning( |
| "Skipping merged profile creation: no merge candidates found" |
| ) |
| return None |
| |
| # The input "unmerged_name" should never be in GS bucket, as recipe |
| # builder executes only when the artifact not exists. |
| if ( |
| os.path.splitext(os.path.basename(benchmark_profiles[-1].url))[0] |
| == unmerged_name |
| ): |
| benchmark_profiles = benchmark_profiles[:-1] |
| |
| # assert os.path.splitext(os.path.basename( |
| # benchmark_profiles[-1].url))[0] != unmerged_name, unmerged_name |
| |
| base_time = datetime.datetime.fromtimestamp( |
| os.path.getmtime(unmerged_profile) |
| ) |
| time_cutoff = base_time - datetime.timedelta(days=max_age_days) |
| merge_candidates = [ |
| p for p in benchmark_profiles if p.creation_time >= time_cutoff |
| ] |
| |
| # Pick (recent_to_merge-1) from the GS URL, because we also need to pick |
| # the current profile locally. |
| merge_candidates = merge_candidates[-(recent_to_merge - 1) :] |
| |
| # This should never happen, but be sure we're not merging a profile into |
| # itself anyway. It's really easy for that to silently slip through, and |
| # can lead to overrepresentation of a single profile, which just causes |
| # more noise. |
| assert len(set(p.url for p in merge_candidates)) == len( |
| merge_candidates |
| ) |
| |
| # Merging a profile into itself is pointless. |
| if not merge_candidates: |
| logging.warning( |
| "Skipping merged profile creation: we only have a single " |
| "merge candidate." |
| ) |
| return None |
| |
| afdo_files = [] |
| for candidate in merge_candidates: |
| # It would be slightly less complex to just name these off as |
| # profile-1.afdo, profile-2.afdo, ... but the logs are more readable |
| # if we keep the basename from gs://. |
| candidate_name = os.path.basename(candidate.url) |
| candidate_uncompressed = candidate_name[ |
| : -len(BZ2_COMPRESSION_SUFFIX) |
| ] |
| |
| copy_from = candidate.url |
| copy_to = os.path.join(output_dir, candidate_name) |
| copy_to_uncompressed = os.path.join( |
| output_dir, candidate_uncompressed |
| ) |
| |
| self.gs_context.Copy(copy_from, copy_to) |
| cros_build_lib.UncompressFile(copy_to, copy_to_uncompressed) |
| afdo_files.append(copy_to_uncompressed) |
| |
| afdo_files.append(unmerged_profile) |
| afdo_basename = os.path.basename(afdo_files[-1]) |
| assert afdo_basename.endswith(AFDO_SUFFIX) |
| afdo_basename = afdo_basename[: -len(AFDO_SUFFIX)] |
| |
| raw_merged_basename = ( |
| "raw-" + afdo_basename + merged_suffix + AFDO_SUFFIX |
| ) |
| raw_merged_output_path = os.path.join(output_dir, raw_merged_basename) |
| |
| # Weight all profiles equally. |
| self._MergeAFDOProfiles( |
| [(profile, 1) for profile in afdo_files], raw_merged_output_path |
| ) |
| |
| profile_to_upload_basename = afdo_basename + merged_suffix + AFDO_SUFFIX |
| profile_to_upload_path = os.path.join( |
| output_dir, profile_to_upload_basename |
| ) |
| |
| # Remove indirect calls and remove cold functions |
| # Since the benchmark precisions increased, the number of functions in |
| # merged profiles also grow. To stabilize the impact on production |
| # profiles for Android/Linux, reduce the number of functions to 70k, |
| # which aligns with recent 3 merged benchmark profiles. |
| reduce_functions = 70000 |
| redact = False |
| remove = True |
| if self.arch == "arm": |
| # Redaction has significant effect on performance gain (+15% on |
| # speedometer2) but also has a drastic impact on the binary size |
| # (+16MB). |
| # With the native arm profile we can trim the binary size with |
| # sample-profile-accurate. On trogdor it shaves another 20MB with a |
| # slight performance impact, drop from 12 to 11 %. |
| redact = True |
| self._ProcessAFDOProfile( |
| raw_merged_output_path, |
| profile_to_upload_path, |
| redact=redact, |
| remove=remove, |
| reduce_functions=reduce_functions, |
| extbinary=False, |
| ) |
| |
| result_basename = os.path.basename(profile_to_upload_path) |
| return result_basename |
| |
| def _CleanupArtifactDirectory(self, src_dir) -> None: |
| """Cleanup a directory before build so we can safely use the artifacts. |
| |
| Args: |
| src_dir: A temp path holding the possible artifacts. It needs to be |
| an absolute path. |
| """ |
| assert os.path.isabs(src_dir), ( |
| "%s needs to be an absolute path " % src_dir |
| ) |
| check_dirs = [ |
| self.chroot.full_path(x) |
| for x in [src_dir, os.path.join(self.sysroot_path, src_dir[1:])] |
| ] |
| for directory in check_dirs: |
| if not os.path.exists(directory): |
| continue |
| |
| logging.info( |
| "toolchain-logs: Cleaning up %s before build", directory |
| ) |
| osutils.RmDir(directory, sudo=True) |
| |
| |
| class PrepareForBuildHandler(_CommonPrepareBundle): |
| """Methods for updating ebuilds for toolchain artifacts.""" |
| |
| def __init__( |
| self, |
| artifact_name, |
| chroot, |
| sysroot_path, |
| build_target, |
| input_artifacts, |
| profile_info, |
| ) -> None: |
| super().__init__( |
| artifact_name, |
| chroot, |
| sysroot_path, |
| build_target, |
| input_artifacts=input_artifacts, |
| profile_info=profile_info, |
| ) |
| self._prepare_func = getattr(self, "_Prepare" + artifact_name) |
| |
| def Prepare(self): |
| return self._prepare_func() |
| |
| def _CommonPrepareBasedOnGsPathExists(self, name, url, key): |
| """Helper function to determine if an artifact in the GS path or not.""" |
| gs_url = self.input_artifacts.get(key, [url])[0] |
| path = os.path.join(gs_url, name) |
| if self.gs_context.Exists(path): |
| # Artifact already created. |
| logging.info("Pointless build: Found %s on %s", name, path) |
| return PrepareForBuildReturn.POINTLESS |
| logging.info("Build needed: No %s found. %s does not exist", key, path) |
| return PrepareForBuildReturn.NEEDED |
| |
| def _PrepareChromeClangWarningsFile(self): |
| # We always build this artifact. |
| return PrepareForBuildReturn.NEEDED |
| |
| def _UnverifiedAfdoFileExists(self): |
| """Check if the unverified AFDO benchmark file exists. |
| |
| This is used by both the UnverifiedChromeBenchmark Perf and Afdo file |
| prep methods. |
| |
| PrepareForBuildReturn. |
| """ |
| # We do not check for the existence of the (intermediate) perf.data file |
| # since that is tied to the build, and the orchestrator decided that we |
| # should run (no build to recycle). |
| # |
| # Check if there is already a published AFDO artifact for this version |
| # of Chrome. |
| return self._CommonPrepareBasedOnGsPathExists( |
| name=self._GetBenchmarkAFDOName() + BZ2_COMPRESSION_SUFFIX, |
| url=BENCHMARK_AFDO_GS_URL, |
| key="UnverifiedChromeBenchmarkAfdoFile", |
| ) |
| |
| def _PrepareUnverifiedChromeBenchmarkPerfFile(self): |
| """Prepare to build the Chrome benchmark perf.data file.""" |
| return self._UnverifiedAfdoFileExists() |
| |
| def _PrepareUnverifiedChromeBenchmarkAfdoFile(self): |
| """Prepare to build an Unverified Chrome benchmark AFDO file.""" |
| ret = self._UnverifiedAfdoFileExists() |
| if self.chroot: |
| # Fetch the CHROME_DEBUG_BINARY and |
| # UNVERIFIED_CHROME_BENCHMARK_PERF_FILE artifacts and unpack them |
| # for the Bundle call. |
| workdir_full = self.chroot.full_path(self._AfdoTmpPath()) |
| # Clean out the workdir. |
| osutils.RmDir(workdir_full, ignore_missing=True, sudo=True) |
| osutils.SafeMakedirs(workdir_full) |
| |
| # We don't need a strict version from ebuild because it can change |
| # in the timeframe between afdo-generate and afdo-process (right, it |
| # happens!). Another edge case is revbump of chrome with patches in |
| # 9999. |
| bin_name = ( |
| self._GetBenchmarkAFDOName( |
| CHROME_DEBUG_BINARY_NAME, wildcard_version=True |
| ) |
| + BZ2_COMPRESSION_SUFFIX |
| ) |
| gs_loc = self.input_artifacts.get("ChromeDebugBinary", []) |
| # url contains a concrete chrome version. |
| bin_url = self._FindArtifact(bin_name, gs_loc) |
| if not bin_url: |
| raise PrepareForBuildHandlerError( |
| "Could not find an artifact matching the pattern " |
| f'"{bin_name}" in {gs_loc}.' |
| ) |
| # Extract the name with a concrete version of chrome. |
| bin_name = os.path.basename(bin_url) |
| bin_compressed = self._AfdoTmpPath(bin_name) |
| self.chroot.run( |
| [ |
| "gsutil", |
| "-o", |
| "Boto:num_retries=10", |
| "cp", |
| "-v", |
| "--", |
| bin_url, |
| bin_compressed, |
| ], |
| print_cmd=True, |
| ) |
| self.chroot.run( |
| ["bzip2", "-d", bin_compressed], |
| print_cmd=True, |
| ) |
| |
| perf_name = ( |
| self._GetBenchmarkAFDOName(template=CHROME_PERF_AFDO_FILE) |
| + BZ2_COMPRESSION_SUFFIX |
| ) |
| perf_compressed = self._AfdoTmpPath(perf_name) |
| gs_loc = self.input_artifacts.get( |
| "UnverifiedChromeBenchmarkPerfFile", [] |
| ) |
| perf_url = self._FindArtifact(perf_name, gs_loc) |
| if not perf_url: |
| raise PrepareForBuildHandlerError( |
| f'Could not find "{perf_name}" in {gs_loc}.' |
| ) |
| self.gs_context.Copy( |
| perf_url, self.chroot.full_path(perf_compressed) |
| ) |
| self.chroot.run( |
| ["bzip2", "-d", perf_compressed], |
| print_cmd=True, |
| ) |
| return ret |
| |
| def _PrepareChromeAFDOProfileForAndroidLinux(self): |
| """Prepare to build Chrome AFDO profile for Android/Linux.""" |
| if self._UnverifiedAfdoFileExists() == PrepareForBuildReturn.POINTLESS: |
| # Only generate new Android/Linux profiles when there's a need to |
| # generate new benchmark profiles |
| return PrepareForBuildReturn.POINTLESS |
| |
| return self._CommonPrepareBasedOnGsPathExists( |
| name=self._GetBenchmarkAFDOName() |
| + "-merged" |
| + BZ2_COMPRESSION_SUFFIX, |
| url=BENCHMARK_AFDO_GS_URL, |
| key="ChromeAFDOProfileForAndroidLinux", |
| ) |
| |
| def _PrepareVerifiedChromeBenchmarkAfdoFile(self) -> None: |
| """Unused: see _PrepareVerifiedReleaseAfdoFile.""" |
| raise PrepareForBuildHandlerError( |
| "Unexpected artifact type %s." % self.artifact_name |
| ) |
| |
| def _PrepareChromeDebugBinary(self): |
| """See _PrepareUnverifiedChromeBenchmarkPerfFile.""" |
| return PrepareForBuildReturn.POINTLESS |
| |
| def _PrepareUnverifiedKernelCwpAfdoFile(self) -> None: |
| """Unused: CWP is from elsewhere.""" |
| raise PrepareForBuildHandlerError( |
| "Unexpected artifact type %s." % self.artifact_name |
| ) |
| |
| def _PrepareVerifiedKernelCwpAfdoFile(self): |
| """Prepare to verify the kernel CWP AFDO artifact.""" |
| ret = PrepareForBuildReturn.NEEDED |
| kernel_version = self.profile_info.get("kernel_version") |
| if not kernel_version: |
| raise PrepareForBuildHandlerError( |
| "Could not find kernel version to verify." |
| ) |
| |
| verified_profile_url = KERNEL_PROFILE_VETTED_URL.format(arch=self.arch) |
| profile_url = KERNEL_PROFILE_URL.format(arch=self.arch) |
| profile_var_name = "AFDO_PROFILE_VERSION" |
| if self.arch == "arm": |
| profile_var_name = "ARM_AFDO_PROFILE_VERSION" |
| |
| cwp_locs = list( |
| self.input_artifacts.get( |
| "UnverifiedKernelCwpAfdoFile", |
| [os.path.join(profile_url, kernel_version)], |
| ) |
| ) |
| afdo_path = self._FindLatestAFDOArtifact( |
| cwp_locs, _RankValidCWPProfiles |
| ) |
| |
| published_path = os.path.join( |
| self.input_artifacts.get( |
| "VerifiedKernelCwpAfdoFile", |
| [os.path.join(verified_profile_url, kernel_version)], |
| )[0], |
| os.path.basename(afdo_path), |
| ) |
| if self.gs_context.Exists(published_path): |
| # The verified artifact is already present: we are done. |
| logging.info('Pointless build: "%s" exists.', published_path) |
| ret = PrepareForBuildReturn.POINTLESS |
| |
| afdo_dir, afdo_name = os.path.split( |
| afdo_path.replace(KERNEL_AFDO_COMPRESSION_SUFFIX, "") |
| ) |
| # The package name cannot have dots, so an underscore is used instead. |
| # For example: chromeos-kernel-4_4-4.4.214-r2087.ebuild. |
| kernel_version = kernel_version.replace(".", "_") |
| |
| # Check freshness. |
| age = _GetProfileAge(afdo_name, "kernel_afdo") |
| if age > KERNEL_ALLOWED_STALE_DAYS: |
| logging.info( |
| "Found an expired afdo for kernel %s: %s, skip.", |
| kernel_version, |
| afdo_name, |
| ) |
| ret = PrepareForBuildReturn.POINTLESS |
| |
| if age > KERNEL_WARN_STALE_DAYS: |
| _WarnDetectiveAboutKernelProfileExpiration( |
| kernel_version, afdo_path |
| ) |
| |
| # If we don't have an SDK, then we cannot update the manifest. |
| if self.chroot: |
| self._PatchEbuild( |
| self._GetEbuildInfo( |
| "chromeos-kernel-%s" % kernel_version, "sys-kernel" |
| ), |
| {profile_var_name: afdo_name, "AFDO_LOCATION": afdo_dir}, |
| uprev=True, |
| ) |
| return ret |
| |
| def _PrepareUnverifiedChromeCwpAfdoFile(self) -> None: |
| """Unused: CWP is from elsewhere.""" |
| raise PrepareForBuildHandlerError( |
| "Unexpected artifact type %s." % self.artifact_name |
| ) |
| |
| def _PrepareVerifiedChromeCwpAfdoFile(self) -> None: |
| """Unused: see _PrepareVerifiedReleaseAfdoFile.""" |
| raise PrepareForBuildHandlerError( |
| "Unexpected artifact type %s." % self.artifact_name |
| ) |
| |
| def _PrepareVerifiedReleaseAfdoFile(self): |
| """Prepare to verify the Chrome AFDO artifact and release it. |
| |
| See also "chrome_afdo" code elsewhere in this file. |
| """ |
| ret = PrepareForBuildReturn.NEEDED |
| if not self.profile: |
| raise PrepareForBuildHandlerError( |
| "Profile name is not set. " |
| "Is 'chrome_cwp_profile' missing in profile_info?" |
| ) |
| bench_locs = self.input_artifacts.get( |
| "UnverifiedChromeBenchmarkAfdoFile", [BENCHMARK_AFDO_GS_URL] |
| ) |
| cwp_locs = self.input_artifacts.get( |
| "UnverifiedChromeCwpAfdoFile", [CWP_AFDO_GS_URL] |
| ) |
| |
| # AFDO Experiment can tweak both the source of benchmark and CWP |
| # profiles. |
| # -<arch> suffix forces use of the specified architecture for |
| # the benchmark profile. For example exp-amd64 is going to use |
| # benchmark profiles from amd64. |
| bench_arch = self.arch |
| if self.profile.startswith("exp-"): |
| bench_arch = self.profile.replace("exp-", "", 1) |
| |
| # This will raise a NoProfilesInGsBucketError if no artifact is found. |
| bench = self._FindLatestAFDOArtifact( |
| bench_locs, self._ValidBenchmarkProfileVersion, bench_arch |
| ) |
| # CWP source in the AFDO Experiment is configured directly from recipe |
| # there is no dependency on the architecture here. |
| cwp = self._FindLatestAFDOArtifact(cwp_locs, _RankValidCWPProfiles) |
| bench_name = os.path.split(bench)[1] |
| cwp_name = os.path.split(cwp)[1] |
| |
| # Check to see if we already have a verified AFDO profile. We only look |
| # at the first path in the list of vetted locations, since that is where |
| # we will publish the verified profile. |
| published_loc = self.input_artifacts.get( |
| "VerifiedReleaseAfdoFile", [RELEASE_PROFILE_VETTED_URL] |
| )[0] |
| |
| profile = self.profile |
| if self.arch == "arm" and self.profile == "arm": |
| # arm/arm profile is generated on arm64 and used on all arm |
| # production devices. Profile rollers track the -arm-none- verified |
| # profiles. |
| # All other arm profile variants are intended only for testing or |
| # experiments. Merged profiles with variants will be stored in gs |
| # bucket but ignored by the production pipeline. |
| # For example arm32 profiles are generated on arm arch (arm64) and |
| # verified on arm32 target. This profile is not used on production. |
| profile = "none" |
| # Strip suffix from experimental profile. |
| if profile.startswith("exp"): |
| profile = "exp" |
| merged_name = MERGED_AFDO_NAME.format( |
| arch=self.arch, |
| name=_GetCombinedAFDOName( |
| _ParseCWPProfileName(os.path.splitext(cwp_name)[0]), |
| profile, |
| _ParseBenchmarkProfileName(os.path.splitext(bench_name)[0]), |
| ), |
| ) |
| published_name = merged_name + "-redacted.afdo" + XZ_COMPRESSION_SUFFIX |
| published_path = os.path.join(published_loc, published_name) |
| |
| if self.gs_context.Exists(published_path): |
| # The verified artifact is already present: we are done. |
| logging.info('Pointless build: "%s" exists.', published_path) |
| ret = PrepareForBuildReturn.POINTLESS |
| |
| # If we don't have an SDK, then we cannot update the manifest. |
| if self.chroot: |
| # Generate the AFDO profile to verify in ${CHROOT}/tmp/. |
| with self.chroot.tempdir() as tempdir: |
| art = self._CreateReleaseChromeAFDO( |
| cwp, bench, tempdir, merged_name |
| ) |
| afdo_profile = os.path.join( |
| self.chroot.tmp, os.path.basename(art) |
| ) |
| os.rename(art, afdo_profile) |
| self._PatchEbuild( |
| self._GetEbuildInfo(constants.CHROME_PN), |
| {"UNVETTED_AFDO_FILE": self.chroot.chroot_path(afdo_profile)}, |
| uprev=True, |
| ) |
| return ret |
| |
| def _PrepareToolchainWarningLogs(self): |
| # We always build this artifact. |
| return PrepareForBuildReturn.NEEDED |
| |
| def _PrepareClangCrashDiagnoses(self): |
| # We always build this artifact. |
| return PrepareForBuildReturn.NEEDED |
| |
| def _PrepareCompilerRusageLogs(self): |
| # We always build this artifact. |
| return PrepareForBuildReturn.UNKNOWN |
| |
| |
| class BundleArtifactHandler(_CommonPrepareBundle): |
| """Methods for updating ebuilds for toolchain artifacts.""" |
| |
| def __init__( |
| self, |
| artifact_name, |
| chroot, |
| sysroot_path, |
| build_target, |
| output_dir, |
| profile_info, |
| ) -> None: |
| super().__init__( |
| artifact_name, |
| chroot, |
| sysroot_path, |
| build_target, |
| profile_info=profile_info, |
| ) |
| self._bundle_func = getattr(self, "_Bundle" + artifact_name) |
| self.output_dir = output_dir |
| |
| def Bundle(self): |
| return self._bundle_func() |
| |
| def _CheckArguments(self, chrome_binary: Path) -> None: |
| """Make sure the arguments received are correct.""" |
| if not os.path.isdir(self.output_dir): |
| raise BundleArtifactsHandlerError( |
| f"Non-existent directory '{self.output_dir}' specified for " |
| "--out-dir" |
| ) |
| |
| chrome_binary_path_outside = self.chroot.full_path( |
| self.sysroot_path, chrome_binary |
| ) |
| if not os.path.exists(chrome_binary_path_outside): |
| raise BundleArtifactsHandlerError( |
| f"'{chrome_binary_path_outside}' chrome binary does not exist" |
| ) |
| |
| def _BundleChromeClangWarningsFile(self): |
| """Bundle clang-tidy warnings file.""" |
| with self.chroot.tempdir() as tempdir: |
| in_chroot_tempdir = self.chroot.chroot_path(tempdir) |
| now = datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d") |
| clang_tidy_tarball = ( |
| f"{self.build_target}.{now}" ".clang_tidy_warnings.tar.xz" |
| ) |
| cmd = [ |
| "cros_generate_tidy_warnings", |
| "--out-file", |
| clang_tidy_tarball, |
| "--out-dir", |
| in_chroot_tempdir, |
| "--board", |
| self.build_target, |
| "--logs-dir", |
| os.path.join("/tmp/clang-tidy-logs", self.build_target), |
| ] |
| self.chroot.run(cmd, cwd=self.chroot.path) |
| artifact_path = os.path.join(self.output_dir, clang_tidy_tarball) |
| shutil.copy2( |
| os.path.join(tempdir, clang_tidy_tarball), artifact_path |
| ) |
| return [artifact_path] |
| |
| def _GetProfileNames(self, datadir): |
| """Return list of profiles. |
| |
| This function is for ease in test writing. |
| |
| Args: |
| datadir: Absolute path to build/coverage_data in the sysroot. |
| |
| Returns: |
| list of chroot-relative paths to profiles found. |
| """ |
| return [ |
| self.chroot.chroot_path(os.path.join(dir_name, file_name)) |
| for dir_name, _, files in os.walk(datadir) |
| for file_name in files |
| if os.path.basename(dir_name) == "raw_profiles" |
| ] |
| |
| def _BundleUnverifiedChromeBenchmarkPerfFile(self): |
| """Bundle the unverified Chrome benchmark perf.data file. |
| |
| The perf.data file is created in the HW Test, and afdo_process needs the |
| matching unstripped Chrome binary in order to generate the profile. |
| """ |
| return [] |
| |
| def _BundleChromeDebugBinary(self): |
| """Bundle the unstripped Chrome binary.""" |
| debug_bin_inside = _CHROME_DEBUG_BIN % { |
| "root": "", |
| "sysroot": self.sysroot_path, |
| } |
| binary_name = self._GetBenchmarkAFDOName(CHROME_DEBUG_BINARY_NAME) |
| bin_path = os.path.join( |
| self.output_dir, binary_name + BZ2_COMPRESSION_SUFFIX |
| ) |
| with open(bin_path, "w", encoding="utf-8") as f: |
| self.chroot.run( |
| ["bzip2", "-c", debug_bin_inside], |
| stdout=f, |
| print_cmd=True, |
| ) |
| return [bin_path] |
| |
| @staticmethod |
| def _LocateChromeDebugInfo(afdo_tmp_path: Path) -> Path: |
| """Locates debuginfo for a Chrome binary in the given path. |
| |
| Returns: |
| The path to debuginfo. |
| |
| Raises: |
| BundleArtifactsHandlerError: if the number of files that seem to be |
| Chrome debuginfo is not exactly one. |
| """ |
| debug_glob = "chromeos-chrome*.debug" |
| matches = list(afdo_tmp_path.glob(debug_glob)) |
| if len(matches) == 1: |
| return matches[0] |
| |
| if matches: |
| msg = f"Too many chrome debug files found; results: {matches}" |
| else: |
| msg = f"No files found matching {afdo_tmp_path / debug_glob}" |
| raise BundleArtifactsHandlerError(msg) |
| |
| def _BundleUnverifiedChromeBenchmarkAfdoFile(self): |
| """Bundle a benchmark Chrome AFDO profile. |
| |
| Raises: |
| BundleArtifactsHandlerError: If the output profile is empty. |
| """ |
| files = [] |
| # If the name of the provided binary is not 'chrome.unstripped', then |
| # create_llvm_prof demands it exactly matches the name of the unstripped |
| # binary. Create a symbolic link named 'chrome.unstripped'. |
| CHROME_UNSTRIPPED_NAME = "chrome.unstripped" |
| bin_path_in = self._AfdoTmpPath(CHROME_UNSTRIPPED_NAME) |
| benchmark_afdo_name = self._LocateChromeDebugInfo( |
| afdo_tmp_path=Path(self.chroot.full_path(self._AfdoTmpPath())), |
| ).name |
| benchmark_chroot_path = self.chroot.full_path(bin_path_in) |
| logging.info( |
| "Linking %s => %s", benchmark_afdo_name, benchmark_chroot_path |
| ) |
| osutils.SafeSymlink(benchmark_afdo_name, benchmark_chroot_path) |
| perf_path_inside = self._AfdoTmpPath( |
| self._GetBenchmarkAFDOName(template=CHROME_PERF_AFDO_FILE) |
| ) |
| afdo_name = self._GetBenchmarkAFDOName() |
| afdo_path_inside = self._AfdoTmpPath(afdo_name) |
| # Generate the afdo profile. |
| self.chroot.run( |
| [ |
| _AFDO_GENERATE_LLVM_PROF, |
| "--binary=%s" % self._AfdoTmpPath(CHROME_UNSTRIPPED_NAME), |
| "--profile=%s" % perf_path_inside, |
| "--out=%s" % afdo_path_inside, |
| # Do not set any sample threshold, so the AFDO profile can be as |
| # precise as the raw profile. |
| "--sample_threshold_frac=0", |
| ], |
| print_cmd=True, |
| ) |
| profile_size = os.path.getsize(self.chroot.full_path(afdo_path_inside)) |
| # Check if the profile is empty. |
| # Empty profiles in a binary format can have a non-zero size |
| # because of the header but they won't exceed the page size. |
| # Normal profiles are usually >1MB. |
| if profile_size < 4096: |
| raise BundleArtifactsHandlerError( |
| f"AFDO profile size has invalid size, {profile_size}" |
| ) |
| logging.info( |
| "Generated %s AFDO profile %s, size %.1fMB", |
| self.arch, |
| afdo_name, |
| profile_size / (1024 * 1024), |
| ) |
| |
| # Compress and deliver the profile. |
| afdo_path = os.path.join( |
| self.output_dir, afdo_name + BZ2_COMPRESSION_SUFFIX |
| ) |
| with open(afdo_path, "w", encoding="utf-8") as f: |
| self.chroot.run( |
| ["bzip2", "-c", afdo_path_inside], |
| stdout=f, |
| print_cmd=True, |
| ) |
| files.append(afdo_path) |
| return files |
| |
| def _BundleChromeAFDOProfileForAndroidLinux(self): |
| """Bundle Android/Linux Chrome profiles.""" |
| afdo_name = self._GetBenchmarkAFDOName() |
| output_dir_full = self.chroot.full_path(self._AfdoTmpPath()) |
| afdo_path = os.path.join(output_dir_full, afdo_name) |
| # The _BundleUnverifiedChromeBenchmarkAfdoFile should always run |
| # before this, so the AFDO profile should already be created. |
| assert os.path.exists( |
| afdo_path |
| ), "No new AFDO profile created before creating Android/Linux profiles" |
| |
| files = [] |
| # Merge recent benchmark profiles for Android/Linux use |
| merged_profile = self._CreateAndUploadMergedAFDOProfile( |
| os.path.join(output_dir_full, afdo_name), output_dir_full |
| ) |
| if not merged_profile: |
| return [] |
| |
| merged_profile_inside = self._AfdoTmpPath( |
| os.path.basename(merged_profile) |
| ) |
| merged_profile_compressed = os.path.join( |
| self.output_dir, |
| os.path.basename(merged_profile) + BZ2_COMPRESSION_SUFFIX, |
| ) |
| |
| with open(merged_profile_compressed, "wb") as f: |
| self.chroot.run( |
| ["bzip2", "-c", merged_profile_inside], |
| stdout=f, |
| print_cmd=True, |
| ) |
| files.append(merged_profile_compressed) |
| return files |
| |
| def _BundleVerifiedChromeBenchmarkAfdoFile(self) -> None: |
| """Unused: see _BundleVerifiedReleaseAfdoFile.""" |
| raise BundleArtifactsHandlerError( |
| "Unexpected artifact type %s." % self.artifact_name |
| ) |
| |
| def _BundleUnverifiedKernelCwpAfdoFile(self) -> None: |
| """Unused: this artifact comes from CWP.""" |
| raise BundleArtifactsHandlerError( |
| "Unexpected artifact type %s." % self.artifact_name |
| ) |
| |
| def _BundleVerifiedKernelCwpAfdoFile(self): |
| """Bundle the verified kernel CWP AFDO file.""" |
| kernel_version = self.profile_info.get("kernel_version") |
| if not kernel_version: |
| raise BundleArtifactsHandlerError("kernel_version not provided.") |
| |
| profile_var_name = "AFDO_PROFILE_VERSION" |
| if self.arch == "arm": |
| profile_var_name = "ARM_AFDO_PROFILE_VERSION" |
| |
| kernel_version = kernel_version.replace(".", "_") |
| profile_name = self._GetArtifactVersionInEbuild( |
| f"chromeos-kernel-{kernel_version}", profile_var_name |
| ) |
| if not profile_name: |
| raise BundleArtifactsHandlerError( |
| "Could not find AFDO_PROFILE_VERSION in " |
| f"chromeos-kernel-{kernel_version}." |
| ) |
| profile_name += KERNEL_AFDO_COMPRESSION_SUFFIX |
| # The verified profile is in the sysroot with a name similar to: |
| # /usr/lib/debug/boot/chromeos-kernel-4_4-R82-12874.0-1581935639.afdo.xz |
| profile_path = self.chroot.full_path( |
| self.sysroot_path, |
| "usr", |
| "lib", |
| "debug", |
| "boot", |
| f"chromeos-kernel-{kernel_version}-{profile_name}", |
| ) |
| verified_profile = os.path.join(self.output_dir, profile_name) |
| shutil.copy2(profile_path, verified_profile) |
| return [verified_profile] |
| |
| def _BundleUnverifiedChromeCwpAfdoFile(self) -> None: |
| """Unused: this artifact comes from CWP.""" |
| raise BundleArtifactsHandlerError( |
| "Unexpected artifact type %s." % self.artifact_name |
| ) |
| |
| def _BundleVerifiedChromeCwpAfdoFile(self) -> None: |
| """Unused: see _BundleVerifiedReleaseAfdoFile.""" |
| raise BundleArtifactsHandlerError( |
| "Unexpected artifact type %s." % self.artifact_name |
| ) |
| |
| def _BundleVerifiedReleaseAfdoFile(self): |
| """Bundle the verified Release AFDO file for Chrome.""" |
| profile_path = self.chroot.full_path( |
| self._GetArtifactVersionInEbuild( |
| constants.CHROME_PN, "UNVETTED_AFDO_FILE" |
| ) |
| ) |
| |
| logging.info("Verifying that Chrome was successfully installed...") |
| # This hands back a non-zero exit code if it couldn't find any matches. |
| self.chroot.run( |
| [ |
| f"equery-{self.build_target}", |
| "l", |
| "chromeos-base/chromeos-chrome", |
| ], |
| check=True, |
| ) |
| logging.info("Chrome build was successful.") |
| |
| return _CompressAFDOFiles( |
| [profile_path], None, self.output_dir, XZ_COMPRESSION_SUFFIX |
| ) |
| |
| @staticmethod |
| def _ListTransitiveFiles(base_directory: str): |
| for dir_path, _dir_names, file_names in os.walk(base_directory): |
| for file_name in file_names: |
| yield os.path.join(dir_path, file_name) |
| |
| def _FindAllCrOSArtifactDirs( |
| self, include_incomplete_packages: bool |
| ) -> List[str]: |
| """Finds all cros-artifacts directories in the chroot.""" |
| portage_roots = ["/"] |
| portage_roots.extend( |
| f"/build/{x.name}" |
| for x in Path(self.chroot.full_path("/build")).glob("*/") |
| ) |
| |
| subpaths_to_search = [_PACKAGE_ARTIFACTS_PATH] |
| if include_incomplete_packages: |
| subpaths_to_search.append("var/tmp/portage") |
| |
| artifact_dirs = [] |
| for root in portage_roots: |
| for subpath in subpaths_to_search: |
| full_dir = Path( |
| self.chroot.full_path(os.path.join(root, subpath)) |
| ) |
| artifact_dirs.extend( |
| str(x) for x in full_dir.glob("*/*/cros-artifacts/") |
| ) |
| |
| return artifact_dirs |
| |
| def _CollectCrOSArtifactFiles( |
| self, |
| artifact_subdir: str, |
| dest_dir: str, |
| include_file: Callable[[str], bool], |
| include_incomplete_packages: bool, |
| ): |
| """Collects files from all cros-artifacts dirs in a chroot. |
| |
| Args: |
| artifact_subdir: the subdirectory of artifact directories to |
| inspect. |
| dest_dir: the path of the directory to copy files to (will be |
| created if it doesn't exist and files need to be copied). |
| include_file: a callable that returns True if a file should be |
| copied; False otherwise. |
| include_incomplete_packages: True if cros-artifacts directories |
| should be included for packages that weren't successfully |
| built. |
| |
| Returns: |
| A list of all files that were copied, relative to the chroot's /. |
| """ |
| artifact_dirs = self._FindAllCrOSArtifactDirs( |
| include_incomplete_packages |
| ) |
| |
| output = [] |
| for artifact_dir in artifact_dirs: |
| directory = os.path.join(artifact_dir, artifact_subdir) |
| if not os.path.isdir(directory): |
| logging.info( |
| "toolchain-logs: artifact subdir %s does not exist; skip", |
| directory, |
| ) |
| continue |
| |
| chroot_dir_path = self.chroot.chroot_path(directory) |
| assert chroot_dir_path.startswith("/"), chroot_dir_path |
| |
| logging.info("toolchain-logs: scanning %s", directory) |
| for src_path in self._ListTransitiveFiles(directory): |
| rel_path = os.path.relpath(src_path, start=directory) |
| logging.info("toolchain-logs: checking %s", rel_path) |
| if not include_file(rel_path): |
| logging.warning( |
| "toolchain-logs: skipped file: %s", rel_path |
| ) |
| continue |
| |
| # Chop the leading '/' from the chroot path. |
| dest_path = os.path.join( |
| dest_dir, chroot_dir_path[1:], rel_path |
| ) |
| while os.path.exists(dest_path): |
| file_noext, file_ext = os.path.splitext(dest_path) |
| dest_path = f"{file_noext}0{file_ext}" |
| |
| osutils.SafeMakedirs(os.path.dirname(dest_path)) |
| rel_dest_path = os.path.relpath(dest_path, start=dest_dir) |
| |
| logging.info( |
| "toolchain-logs: adding path %s as %s", src_path, dest_path |
| ) |
| shutil.copy(src_path, dest_path) |
| output.append(rel_dest_path) |
| |
| logging.info("%d files collected", len(output)) |
| return output |
| |
| def _CreateCrOSArtifactBundle( |
| self, |
| src_subdir: str, |
| tarball: str, |
| destination: str, |
| extension: Optional[str] = None, |
| include_incomplete_packages: bool = False, |
| ) -> str: |
| """Bundle the files from src_dir into a tar.xz file. |
| |
| Args: |
| src_subdir: the path to the directory to copy files from. |
| tarball: name of the generated tarballfile (build target, time |
| stamp, and .tar.xz extension will be added automatically). |
| destination: path to create tarball in |
| extension: type of file to search for in src_dir. |
| If extension is None (default), all file types will be allowed. |
| include_incomplete_packages: if True, this will also bundle files |
| from cros-artifacts dirs that weren't emerged (e.g., due to |
| build failures) |
| |
| Returns: |
| Path to the generated tar.xz file |
| """ |
| |
| def FilterFile(file_path: str) -> bool: |
| return extension is None or file_path.endswith(extension) |
| |
| files = self._CollectCrOSArtifactFiles( |
| src_subdir, |
| destination, |
| include_file=FilterFile, |
| include_incomplete_packages=include_incomplete_packages, |
| ) |
| if not files: |
| logging.info("No data found for %s, skip bundle artifact", tarball) |
| raise NoArtifactsToBundleError( |
| f"No {extension} files in {src_subdir}" |
| ) |
| |
| now = datetime.datetime.strftime(datetime.datetime.now(), "%Y%m%d") |
| name = f"{self.build_target}.{now}.{tarball}.tar.xz" |
| output_compressed = os.path.join(self.output_dir, name) |
| cros_build_lib.CreateTarball( |
| output_compressed, destination, inputs=files |
| ) |
| return output_compressed |
| |
| def _BundleToolchainWarningLogs(self): |
| """Bundle the compiler warnings for upload for werror checker.""" |
| with self.chroot.tempdir() as tempdir: |
| try: |
| return [ |
| self._CreateCrOSArtifactBundle( |
| "toolchain/fatal_clang_warnings", |
| "fatal_clang_warnings", |
| tempdir, |
| ".json", |
| # Collecting warning logs is generally only done with |
| # experimental toolchains (e.g., llvm-next), so a green |
| # ToT is not expected. |
| include_incomplete_packages=True, |
| ) |
| ] |
| except NoArtifactsToBundleError: |
| return [] |
| |
| def _BundleClangCrashDiagnoses(self): |
| """Bundle all clang crash diagnoses in chroot for uploading. |
| |
| See bugs.chromium.org/p/chromium/issues/detail?id=1056904 for context. |
| """ |
| with osutils.TempDir(prefix="clang_crash_diagnoses_tarball") as tempdir: |
| try: |
| return [ |
| self._CreateCrOSArtifactBundle( |
| "toolchain/clang_crash_diagnoses", |
| "clang_crash_diagnoses", |
| tempdir, |
| # If the compiler crashed, the package almost |
| # definitely failed to build. |
| include_incomplete_packages=True, |
| ) |
| ] |
| except NoArtifactsToBundleError: |
| return [] |
| |
| def _BundleCompilerRusageLogs(self): |
| """Bundle the rusage files created by compiler invocations. |
| |
| This is useful for monitoring changes in compiler performance. |
| These files are created when the TOOLCHAIN_RUSAGE_OUTPUT variable |
| is set in the environment for monitoring compiler performance. |
| """ |
| with self.chroot.tempdir() as tempdir: |
| try: |
| return [ |
| self._CreateCrOSArtifactBundle( |
| "toolchain/clang_rusage_logs", |
| "clang_rusage_logs", |
| tempdir, |
| ".json", |
| include_incomplete_packages=False, |
| ) |
| ] |
| except NoArtifactsToBundleError: |
| return [] |
| |
| |
| def PrepareForBuild( |
| artifact_name, |
| chroot, |
| sysroot_path, |
| build_target, |
| input_artifacts, |
| profile_info, |
| ): |
| """Prepare for building artifacts. |
| |
| This code is called OUTSIDE the chroot, before it is set up. |
| |
| Args: |
| artifact_name: artifact name |
| chroot: chroot_lib.Chroot instance for chroot. |
| sysroot_path: path to sysroot, relative to chroot path, or None. |
| build_target: name of build target, or None. |
| input_artifacts: List(InputArtifactInfo) of available artifact |
| locations. |
| profile_info: dict(key=value) See ArtifactProfileInfo. |
| |
| Returns: |
| PrepareForBuildReturn |
| """ |
| |
| return PrepareForBuildHandler( |
| artifact_name, |
| chroot, |
| sysroot_path, |
| build_target, |
| input_artifacts=input_artifacts, |
| profile_info=profile_info, |
| ).Prepare() |
| |
| |
| def BundleArtifacts( |
| name, chroot, sysroot_path, build_target, output_dir, profile_info |
| ): |
| """Prepare for building artifacts. |
| |
| This code is called OUTSIDE the chroot, after it is set up. |
| |
| Args: |
| name: artifact name |
| chroot: chroot_lib.Chroot instance for chroot. |
| sysroot_path: path to sysroot, relative to chroot path. |
| build_target: name of build target |
| output_dir: path in which to place the artifacts. |
| profile_info: dict(key=value) See ArtifactProfileInfo. |
| |
| Returns: |
| list of artifacts, relative to output_dir. |
| """ |
| return BundleArtifactHandler( |
| name, |
| chroot, |
| sysroot_path, |
| build_target, |
| output_dir, |
| profile_info=profile_info, |
| ).Bundle() |
| |
| |
| class GetUpdatedFilesHandler: |
| """Find all changed files in the checkout and create a commit message.""" |
| |
| @staticmethod |
| def _UpdateKernelMetadata(kernel_version: str, profile_version: str): |
| """Update afdo_metadata json file""" |
| kernel_version = kernel_version.replace(".", "_") |
| json_file = os.path.join( |
| TOOLCHAIN_UTILS_PATH, |
| "afdo_metadata", |
| f"kernel_afdo_{kernel_version}.json", |
| ) |
| assert os.path.exists( |
| json_file |
| ), f"Metadata for {kernel_version} does not exist" |
| afdo_versions = json.loads(osutils.ReadFile(json_file)) |
| kernel_name = f"chromeos-kernel-{kernel_version}" |
| assert ( |
| kernel_name in afdo_versions |
| ), f"To update {kernel_name}, the entry should be in kernel_afdo.json" |
| old_value = afdo_versions[kernel_name]["name"] |
| update_to_newer_profile = _RankValidCWPProfiles( |
| old_value |
| ) < _RankValidCWPProfiles(profile_version) |
| # This function is called after Bundle, so normally the profile is newer |
| # is guaranteed because Bundle function only runs when a new profile is |
| # needed to verify at the beginning of the builder. This check is to |
| # make sure there's no other updates happen between the start of the |
| # builder and the time of this function call. |
| assert update_to_newer_profile, ( |
| f"Failed to update JSON file because {profile_version} is not " |
| f"newer than {old_value}" |
| ) |
| afdo_versions[kernel_name]["name"] = profile_version |
| pformat.json(afdo_versions, fp=json_file) |
| return [json_file] |
| |
| def __init__(self, artifact_type, artifact_path, profile_info) -> None: |
| self.artifact_path = artifact_path |
| self.profile_info = profile_info |
| if artifact_type == "VerifiedKernelCwpAfdoFile": |
| self._update_func = self.UpdateKernelProfileMetadata |
| else: |
| raise GetUpdatedFilesForCommitError( |
| f"{artifact_type} has no handler in GetUpdatedFiles" |
| ) |
| |
| def UpdateKernelProfileMetadata(self): |
| kernel_version = self.profile_info.get("kernel_version") |
| if not kernel_version: |
| raise GetUpdatedFilesForCommitError("kernel_version not provided") |
| # The path obtained from artifact_path is the full path, containing |
| # extension, so we need to remove it here. |
| profile_version = os.path.basename(self.artifact_path).replace( |
| KERNEL_AFDO_COMPRESSION_SUFFIX, "" |
| ) |
| files = self._UpdateKernelMetadata(kernel_version, profile_version) |
| commit_message = ( |
| "afdo_metadata: Publish new kernel profiles for " |
| f"{kernel_version}\n\n" |
| f"Update {kernel_version} to {profile_version}\n\n" |
| "Automatically generated in kernel verifier.\n\n" |
| "BUG=None\n" |
| "TEST=Verified in kernel-release-afdo-verify-orchestrator\n" |
| ) |
| return files, commit_message |
| |
| def Update(self): |
| return self._update_func() |
| |
| |
| def GetUpdatedFiles(artifact_type, artifact_path, profile_info): |
| return GetUpdatedFilesHandler( |
| artifact_type, artifact_path, profile_info |
| ).Update() |