blob: cf0f9b0c0996d0da78228737049deb3f3b46e14d [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module containing the various utilities to build Chrome with AFDO.
For a description of AFDO see
from __future__ import print_function
import bisect
import collections
import datetime
import glob
import json
import os
import re
from chromite.lib import constants, cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import (failures_lib, git, gs, osutils, path_util,
# AFDO-specific constants.
AFDO_SUFFIX = '.afdo'
# Chrome URL where AFDO data is stored.
_gsurls = {}
AFDO_CHROOT_ROOT = os.path.join('%(build_root)s', constants.DEFAULT_CHROOT_DIR)
AFDO_LOCAL_DIR = os.path.join('%(root)s', 'tmp')
CHROME_ARCH_VERSION = '%(package)s-%(arch)s-%(version)s'
CHROME_ARCH_RELEASE = '%(package)s-%(arch)s-%(release)s'
CHROME_DEBUG_BIN = os.path.join('%(root)s', 'build/%(board)s/usr/lib/debug',
# regex to find AFDO file for specific architecture within the ebuild file.
# and corresponding replacement string.
CHROME_EBUILD_AFDO_REPL = r'\g<bef>%s\g<aft>'
GSURL_BASE_BENCH = 'gs://chromeos-prebuilt/afdo-job/llvm'
GSURL_BASE_CWP = 'gs://chromeos-prebuilt/afdo-job/cwp/chrome'
GSURL_BASE_RELEASE = 'gs://chromeos-prebuilt/afdo-job/release-merged'
GSURL_CHROME_DEBUG_BIN = os.path.join(
AFDO_GENERATE_LLVM_PROF = '/usr/bin/create_llvm_prof'
# An AFDO data is considered stale when BOTH of the following two metrics don't
# meet. For example, if an AFDO data is generated 20 days ago but only 5 builds
# away, it is considered valid.
# How old can the AFDO data be? (in days).
# How old can the AFDO data be? (in difference of builds).
# How old can the Kernel AFDO data be? (in days).
# How old can the Kernel AFDO data be before sheriff got noticed? (in days).
# 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.
AFDO_DATA_GENERATORS_LLVM = ('chell', 'samus')
KERNEL_PROFILE_URL = 'gs://chromeos-prebuilt/afdo-job/cwp/kernel/'
KERNEL_EBUILD_ROOT = os.path.join(
constants.SOURCE_ROOT, 'src/third_party/chromiumos-overlay/sys-kernel')
# Kernels that we can't generate afdo anymore because of reasons like
# too few samples etc.
'silvermont': '',
'airmont': 'airmont',
'haswell': 'haswell',
'broadwell': 'broadwell',
# Relative weights we should use when merging our 'release' profiles. The
# counters in our benchmark/cwp profiles end up being multiplied by these
# numbers, so they can technically be anything, but we have them sum to 100 for
# ease of understanding.
# Filename pattern of CWP profiles for Chrome
(\d+)\. # Major
(\d+)\. # Minor
(\d+)\. # Build
(\d+) # Patch
(?:_rc)?-r(\d+) # Revision
afdo(?:\.bz2)?$ # We don't care about the presence of .bz2,
# so we use the ignore-group '?:' operator.
''', re.VERBOSE)
BenchmarkProfileVersion = collections.namedtuple(
['major', 'minor', 'build', 'patch', 'revision', 'is_merged'])
class MissingAFDOData(failures_lib.StepFailure):
"""Exception thrown when necessary AFDO data is missing."""
class MissingAFDOMarkers(failures_lib.StepFailure):
"""Exception thrown when necessary ebuild markers for AFDO are missing."""
class UnknownKernelVersion(failures_lib.StepFailure):
"""Exception thrown when the Kernel version can't be inferred."""
class NoValidProfileFound(failures_lib.StepFailure):
"""Exception thrown when there is no valid profile found."""
def CompressAFDOFile(to_compress, buildroot):
"""Compress file used by AFDO process.
to_compress: File to compress.
buildroot: buildroot where to store the compressed data.
Name of the compressed data file.
local_dir = AFDO_BUILDROOT_LOCAL % {'build_root': buildroot}
dest = os.path.join(local_dir, os.path.basename(to_compress)) + \
cros_build_lib.CompressFile(to_compress, dest)
return dest
def UncompressAFDOFile(to_decompress, buildroot):
"""Decompress file used by AFDO process.
to_decompress: File to decompress.
buildroot: buildroot where to store the decompressed data.
local_dir = AFDO_BUILDROOT_LOCAL % {'build_root': buildroot}
basename = os.path.basename(to_decompress)
dest_basename = basename.rsplit('.', 1)[0]
dest = os.path.join(local_dir, dest_basename)
cros_build_lib.UncompressFile(to_decompress, dest)
return dest
def GSUploadIfNotPresent(gs_context, src, dest):
"""Upload a file to GS only if the file does not exist.
Will not generate an error if the file already exist in GS. It will
only emit a warning.
I could use GSContext.Copy(src,dest,version=0) here but it does not seem
to work for large files. Using GSContext.Exists(dest) instead. See
gs_context: GS context instance.
src: File to copy.
dest: Destination location.
True if file was uploaded. False otherwise.
if gs_context.Exists(dest):
logging.warning('File %s already in GS', dest)
return False
gs_context.Copy(src, dest, acl='public-read')
return True
def GetAFDOPerfDataURL(cpv, arch):
"""Return the location URL for the AFDO per data file.
Build the URL for the 'perf' data file given the release and architecture.
cpv: The portage_util.CPV object for chromeos-chrome.
arch: architecture we're going to build Chrome for.
URL of the location of the 'perf' data file.
# The file name of the perf data is based only in the chrome version.
# The test case that produces it does not know anything about the
# revision number.
# TODO(llozano): perf data filename should include the revision number.
version_number = cpv.version_no_rev.split('_')[0]
chrome_spec = {
'package': cpv.package,
'arch': arch,
'version': version_number
return GSURL_CHROME_PERF % chrome_spec
def CheckAFDOPerfData(cpv, arch, gs_context):
"""Check whether AFDO perf data exists for the given architecture.
Check if 'perf' data file for this architecture and release is available
in GS.
cpv: The portage_util.CPV object for chromeos-chrome.
arch: architecture we're going to build Chrome for.
gs_context: GS context to retrieve data.
True if AFDO perf data is available. False otherwise.
url = GetAFDOPerfDataURL(cpv, arch)
if not gs_context.Exists(url):'Could not find AFDO perf data at %s', url)
return False'Found AFDO perf data at %s', url)
return True
def WaitForAFDOPerfData(cpv,
"""Wait for AFDO perf data to show up (with an appropriate timeout).
Wait for AFDO 'perf' data to show up in GS and copy it into a temp
directory in the buildroot.
arch: architecture we're going to build Chrome for.
cpv: CPV object for Chrome.
buildroot: buildroot where AFDO data should be stored.
gs_context: GS context to retrieve data.
timeout: How long to wait total, in seconds.
True if found the AFDO perf data before the timeout expired.
False otherwise.
func_args=(cpv, arch, gs_context),
except timeout_util.TimeoutError:'Could not find AFDO perf data before timeout')
return False
url = GetAFDOPerfDataURL(cpv, arch)
dest_dir = AFDO_BUILDROOT_LOCAL % {'build_root': buildroot}
dest_path = os.path.join(dest_dir, url.rsplit('/', 1)[1])
gs_context.Copy(url, dest_path)
UncompressAFDOFile(dest_path, buildroot)'Retrieved AFDO perf data to %s', dest_path)
return True
def _BuildrootToWorkDirs(buildroot):
chroot_root = AFDO_CHROOT_ROOT % {'build_root': buildroot}
local_dir = AFDO_LOCAL_DIR % {'root': chroot_root}
in_chroot_local_dir = AFDO_LOCAL_DIR % {'root': ''}
return chroot_root, local_dir, in_chroot_local_dir
def _EnumerateMostRecentProfiles(gs_context, milestones, glob_url,
"""Enumerates the most recent AFDO profiles for the given Chrome releases.
gs_context: How we talk to gs://
milestones: A list of ints; each one is a major Chrome version. We'll
try to get the most recent profile for each of these.
glob_url: A URL to query gsutil with.
parse_profile_name: A callable that transforms a profile's filename into
an object that:
- is orderable such that |a < b| implies that |a| is an older profile
than |b|
- has a |major| attribute that indicates Chrome's major version number
Alternatively, if it returns None, we skip the given profile.
A dict of {milestone_number: latest_profile_gs_url}. The keys in this
milestone are a (not-strict) subset of the values in |milestones|.
profile_listing = gs_context.List(glob_url)
if not profile_listing:
raise ValueError('No profiles matched %s' % glob_url)
parsed_profiles = []
for profile in profile_listing:
url = profile.url
parsed = parse_profile_name(os.path.basename(url))
if parsed is not None:
parsed_profiles.append((parsed, url))
newest = {}
for version in milestones:
profiles = [(v, url) for v, url in parsed_profiles if v.major == version]
if not profiles:
_, url = max(profiles)
newest[version] = url
return newest
def _EnumerateMostRecentCWPProfiles(gs_context, milestones):
"""Enumerates the most recent CWP AFDO profiles for Chrome releases.
See _EnumerateMostRecentProfiles for info about args/return value.
profile_suffix = AFDO_SUFFIX + '.xz'
glob_url = os.path.join(GSURL_BASE_CWP, '*' + profile_suffix)
# e.g. R75-3729.38-1554716539.afdo.xz
profile_name_re = re.compile(
^R(\d+)- # Major
(\d+)\. # Build
(\d+)- # Patch
(\d+) # Clock; breaks ties sometimes.
''', re.VERBOSE)
ProfileVersion = collections.namedtuple('ProfileVersion',
['major', 'build', 'patch', 'clock'])
def parse_profile_name(url_basename):
match = profile_name_re.match(url_basename)
if not match:
raise ValueError('Unparseable CWP profile name: %s' % url_basename)
return ProfileVersion(*[int(x) for x in match.groups()])
return _EnumerateMostRecentProfiles(gs_context, milestones, glob_url,
def _ParseBenchmarkProfileName(profile_name):
match = BENCHMARK_PROFILE_NAME_RE.match(profile_name)
if not match:
raise ValueError('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 _EnumerateMostRecentBenchmarkProfiles(gs_context, milestones):
"""Enumerates the most recent benchmark AFDO profiles for Chrome releases.
See _EnumerateMostRecentProfiles for info about args/return value.
glob_url = os.path.join(GSURL_BASE_BENCH, '*' + profile_suffix)
def parse_profile_name(url_basename):
parsed = _ParseBenchmarkProfileName(url_basename)
# We don't want to merge a merged profile; merged profiles are primarily
# for stability, and we have CWP to provide us that.
return None if parsed.is_merged else parsed
return _EnumerateMostRecentProfiles(gs_context, milestones, glob_url,
def GenerateReleaseProfileMergePlan(gs_context, milestones):
"""Generates a plan to merge release profiles for Chrome milestones.
gs_context: How we talk to gs://
milestones: A list of ints; Chrome milestones
A tuple (a, b), where:
- |b| is a dict of {milestone: (cwp_profile, benchmark_profile)}, where
|benchmark_profile| and |cwp_profile| are paths in gs:// that point to
the most recent benchmark and CWP profiles for |milestone|.
- |a| is a sorted list of milestones that aren't present in |b|, but are
present in |milestones|.
benchmark_profiles = _EnumerateMostRecentBenchmarkProfiles(
gs_context, milestones)
cwp_profiles = _EnumerateMostRecentCWPProfiles(gs_context, milestones)
planned_merges = {
version: (cwp_profiles[version], benchmark_profile)
for version, benchmark_profile in benchmark_profiles.items()
if version in cwp_profiles
skipped = sorted(set(milestones) - set(planned_merges))
return skipped, planned_merges
def ExecuteReleaseProfileMergePlan(gs_context, buildroot, merge_plan):
"""Generates release profiles, given a release profile merge plan.
gs_context: How we talk to gs://
buildroot: Our buildroot
merge_plan: The second result of GenerateReleaseProfileMergePlan. This
determines the profiles we pull and merge.
_, work_dir, chroot_work_dir = _BuildrootToWorkDirs(buildroot)
def path_pair(suffix):
outside_chroot = os.path.join(work_dir, suffix)
in_chroot = os.path.join(chroot_work_dir, suffix)
return in_chroot, outside_chroot
chroot_work_dir, work_dir = path_pair('afdo_data_merge')
def copy_profile(gs_path, local_path):
assert local_path.endswith('.afdo'), local_path
assert not gs_path.endswith('.afdo'), gs_path
compression_suffix = os.path.splitext(gs_path)[1]
temp_path = local_path + compression_suffix
gs_context.Copy(gs_path, temp_path)
cros_build_lib.UncompressFile(temp_path, local_path)
merge_results = {}
for version, (cwp_profile, benchmark_profile) in merge_plan.items():
chroot_benchmark_path, benchmark_path = path_pair('benchmark.afdo')
copy_profile(benchmark_profile, benchmark_path)
chroot_cwp_path, cwp_path = path_pair('cwp.afdo')
copy_profile(cwp_profile, cwp_path)
chroot_merged_path, merged_path = path_pair('m%d.afdo' % version)
merge_weights = [
(chroot_cwp_path, _RELEASE_CWP_MERGE_WEIGHT),
(chroot_benchmark_path, _RELEASE_BENCHMARK_MERGE_WEIGHT),
_MergeAFDOProfiles(merge_weights, chroot_merged_path, use_compbinary=True)
comp_merged_path = merged_path + COMPRESSION_SUFFIX
cros_build_lib.CompressFile(merged_path, comp_merged_path)
merge_results[version] = comp_merged_path
return merge_results
def UploadReleaseProfiles(gs_context, run_id, merge_plan, merge_results):
"""Uploads the profiles in merge_results to our release profile bucket.
gs_context: Our GS context
run_id: A unique identifier for this run. Generally recommended to be the
number of seconds since the unix epoch, or something similarly difficult
to 'collide' with other runs. This is used in paths to guarantee
merge_plan: The merge plan that generated the given |merge_results|. Only
used to write to a metadata file, so we know what went into this profile.
merge_results: A map describing the profiles to upload; you can get one
from ExecuteReleaseProfileMergePlan.
gs_url_base = os.path.join(GSURL_BASE_RELEASE, run_id)
def copy_file_to_gs(local_path, remote_path):
# Note that version=0 implies that we'll never overwrite anything. If
# run_id is truly unique, this should never make a difference.
gs_context.Copy(local_path, remote_path, acl='public-read', version=0)
for version, profile in merge_results.items():
suffix = os.path.splitext(profile)[1]
assert suffix != '.afdo', 'All profiles should be compressed.'
output_path = os.path.join(gs_url_base,
'profiles/m%d.afdo%s' % (version, suffix))
copy_file_to_gs(profile, output_path)
# Write a map describing the profiles that have been uploaded. Not
# compressed, because it's expected to be <500 bytes. At the time of writing,
# no automated system relies on these; we just write them so it's easier to
# understand what 'gs://path/to/profiles/m75.afdo' actually consists of.
temp_dir = osutils.GetGlobalTempDir()
meta_file_path = os.path.join(temp_dir, 'meta.json')
osutils.WriteFile(meta_file_path, json.dumps(merge_plan))
copy_file_to_gs(meta_file_path, os.path.join(gs_url_base, 'meta.json'))
def _MergeAFDOProfiles(chroot_profile_list,
"""Merges the given profile list.
chroot_profile_list: a list of (profile_path, profile_weight).
Profile_weight is an int that tells us how to weight the profile compared
to everything else.
chroot_output_profile: where to store the result profile.
use_compbinary: whether to use the new compressed binary AFDO profile
if not chroot_profile_list:
raise ValueError('Need profiles to merge')
# A regular llvm-profdata command looks like:
# llvm-profdata merge [-sample] -output=/path/to/output input1 [input2
# [input3 ...]]
# Alternatively, we can specify inputs by `-weighted-input=A,file`, where A
# is a multiplier of the sample counts in the profile.
merge_command = [
'-output=' + chroot_output_profile,
merge_command += [
'-weighted-input=%d,%s' % (weight, name)
for name, weight in chroot_profile_list
if use_compbinary:
merge_command, enter_chroot=True, capture_output=True, print_cmd=True)
def CreateAndUploadMergedAFDOProfile(gs_context,
"""Create a merged AFDO profile from recent AFDO profiles and upload it.
If the upload would overwrite an existing merged file, this skips the upload.
gs_context: GS Context
buildroot: The build root
unmerged_name: name of the AFDO profile we've just uploaded. No profiles
whose names are lexicographically ordered after this are candidates for
recent_to_merge: The maximum number of profiles to merge
max_age_days: Don't merge profiles older than max_age_days days old.
A (str, bool) of:
- The name of a merged profile in GSURL_BASE_BENCH if the AFDO profile is
a candidate for merging. Otherwise, None.
- Whether we uploaded a merged profile.
_, work_dir, chroot_work_dir = _BuildrootToWorkDirs(buildroot)
merged_suffix = '-merged'
glob_url = os.path.join(GSURL_BASE_BENCH, '*' + profile_suffix)
benchmark_listing = gs_context.List(glob_url, details=True)
unmerged_version = _ParseBenchmarkProfileName(unmerged_name)
def is_merge_candidate(x):
version = _ParseBenchmarkProfileName(os.path.basename(x.url))
# Exclude merged profiles, because merging merged profiles into merged
# profiles is likely bad.
return unmerged_version >= version and not version.is_merged
benchmark_profiles = [p for p in benchmark_listing if is_merge_candidate(p)]
if not benchmark_profiles:
logging.warning('Skipping merged profile creation: no merge candidates '
return None, False
latest_profile = benchmark_profiles[-1]
latest_profile_version = _ParseBenchmarkProfileName(
# branch profiles don't play nicely with non-branch
# profiles. Hence, we want to avoid merging branch profiles into ToT
# profiles. Merging ToT profiles -> branch is fine, since that's all we
# realistically have in our past, but ToT should always prefer profiles
# collected from ToT only.
def is_differing_branch_profile(x):
parsed = _ParseBenchmarkProfileName(os.path.basename(x.url))
# Branches bump patch levels.
if parsed.patch == 0:
return False
return parsed.major != latest_profile_version.major
benchmark_profiles = [
p for p in benchmark_profiles if not is_differing_branch_profile(p)
# We'll always have |latest_profile| in |benchmark_profiles|. If not,
# |is_differing_branch_profile| is broken, since it should only filter out
# branch profiles with a major version != |latest_profile|'s.
assert benchmark_profiles
base_time = latest_profile.creation_time
time_cutoff = base_time - datetime.timedelta(days=max_age_days)
merge_candidates = [
p for p in benchmark_profiles if p.creation_time >= time_cutoff
merge_candidates = merge_candidates[-recent_to_merge:]
# 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 len(merge_candidates) == 1:
logging.warning('Skipping merged profile creation: we only have a single '
'merge candidate.')
return None, False
chroot_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_name = candidate_name[:-len(COMPRESSION_SUFFIX)]
copy_from = candidate.url
copy_to = os.path.join(work_dir, candidate_name)
copy_to_uncompressed = os.path.join(work_dir, candidate_uncompressed_name)
chroot_file = os.path.join(chroot_work_dir, candidate_uncompressed_name)
gs_context.Copy(copy_from, copy_to)
cros_build_lib.UncompressFile(copy_to, copy_to_uncompressed)
merged_basename = os.path.basename(chroot_afdo_files[-1])
assert merged_basename.endswith(AFDO_SUFFIX)
merged_basename = merged_basename[:-len(AFDO_SUFFIX)] + merged_suffix + \
merged_output_path = os.path.join(work_dir, merged_basename)
chroot_merged_output_path = os.path.join(chroot_work_dir, merged_basename)
# Weight all profiles equally.
_MergeAFDOProfiles([(profile, 1) for profile in chroot_afdo_files],
compressed_path = CompressAFDOFile(merged_output_path, buildroot)
compressed_basename = os.path.basename(compressed_path)
gs_target = os.path.join(GSURL_BASE_BENCH, compressed_basename)
uploaded = GSUploadIfNotPresent(gs_context, compressed_path, gs_target)
return merged_basename, uploaded
def PatchChromeEbuildAFDOFile(ebuild_file, profiles):
"""Patch the Chrome ebuild with the dictionary of {arch: afdo_file} pairs.
ebuild_file: path of the ebuild file within the chroot.
profiles: {source: afdo_file} pairs to put into the ebuild.
original_ebuild = path_util.FromChrootPath(ebuild_file)
modified_ebuild = '' % original_ebuild
patterns = {}
repls = {}
markers = {}
for source in profiles.keys():
patterns[source] = re.compile(CHROME_EBUILD_AFDO_REGEX % source)
repls[source] = CHROME_EBUILD_AFDO_REPL % profiles[source]
markers[source] = False
with open(original_ebuild, 'r') as original:
with open(modified_ebuild, 'w') as modified:
for line in original:
for source in profiles.keys():
matched = patterns[source].match(line)
if matched:
markers[source] = True
modified.write(patterns[source].sub(repls[source], line))
else: # line without markers, just copy it.
for source, found in markers.items():
if not found:
raise MissingAFDOMarkers('Chrome ebuild file does not have appropriate '
'AFDO markers for source %s' % source)
os.rename(modified_ebuild, original_ebuild)
def UpdateManifest(ebuild_file, ebuild_prog='ebuild'):
"""Regenerate the Manifest file.
ebuild_file: path to the ebuild file
ebuild_prog: the ebuild command; can be board specific
gen_manifest_cmd = [ebuild_prog, ebuild_file, 'manifest', '--force']
cros_build_lib.RunCommand(gen_manifest_cmd, enter_chroot=True, print_cmd=True)
def CommitIfChanged(ebuild_dir, message):
"""If there are changes to ebuild or Manifest, commit them.
ebuild_dir: the path to the directory of ebuild in the chroot
message: commit message
# Check if anything changed compared to the previous version.
modifications = git.RunGit(
ebuild_dir, ['status', '--porcelain', '-uno'],
if not modifications:'AFDO info for the ebuilds did not change. '
'Nothing to commit')
git.RunGit(ebuild_dir, ['commit', '-a', '-m', message], print_cmd=True)
def UpdateChromeEbuildAFDOFile(board, profiles):
"""Update chrome ebuild with the dictionary of {arch: afdo_file} pairs.
Modifies the Chrome ebuild to set the appropriate AFDO file for each
given architecture. Regenerates the associated Manifest file and
commits the new ebuild and Manifest.
board: board we are building Chrome for.
profiles: {arch: afdo_file} pairs to put into the ebuild.
These are profiles from selected benchmarks.
# Find the Chrome ebuild file.
equery_prog = 'equery'
ebuild_prog = 'ebuild'
if board:
equery_prog += '-%s' % board
ebuild_prog += '-%s' % board
equery_cmd = [equery_prog, 'w', 'chromeos-chrome']
ebuild_file = cros_build_lib.RunCommand(
equery_cmd, enter_chroot=True, redirect_stdout=True).output.rstrip()
# Patch the ebuild file with the names of the available afdo_files.
PatchChromeEbuildAFDOFile(ebuild_file, profiles)
# Also patch the 9999 ebuild. This is necessary because the uprev
# process starts from the 9999 ebuild file and then compares to the
# current version to see if the uprev is really necessary. We dont
# want the names of the available afdo_files to show as differences.
# It also allows developers to do USE=afdo_use when using the 9999
# ebuild.
ebuild_9999 = os.path.join(
os.path.dirname(ebuild_file), 'chromeos-chrome-9999.ebuild')
PatchChromeEbuildAFDOFile(ebuild_9999, profiles)
UpdateManifest(ebuild_9999, ebuild_prog)
ebuild_dir = path_util.FromChrootPath(os.path.dirname(ebuild_file))
CommitIfChanged(ebuild_dir, 'Update profiles and manifests for Chrome.')
def VerifyLatestAFDOFile(afdo_release_spec, buildroot, gs_context):
"""Verify that the latest AFDO profile for a release is suitable.
Find the latest AFDO profile file for a particular release and check
that it is not too stale. The latest AFDO profile name for a release
can be found in a file in GS under the name
afdo_release_spec: architecture and release to find the latest AFDO
profile for.
buildroot: buildroot where AFDO data should be stored.
gs_context: GS context to retrieve data.
The first return value is the name of the AFDO profile file found. None
The second return value indicates whether the profile found is expired or
not. False when no profile is found.
latest_afdo_url = GSURL_LATEST_CHROME_AFDO % afdo_release_spec
# Check if latest-chrome-<arch>-<release>.afdo exists.
latest_detail = gs_context.List(latest_afdo_url, details=True)
except gs.GSNoSuchKey:'Could not find latest AFDO info file %s', latest_afdo_url)
return None, False
# Then get the name of the latest valid AFDO profile file.
local_dir = AFDO_BUILDROOT_LOCAL % {'build_root': buildroot}
latest_afdo_file = LATEST_CHROME_AFDO_FILE % afdo_release_spec
latest_afdo_path = os.path.join(local_dir, latest_afdo_file)
gs_context.Copy(latest_afdo_url, latest_afdo_path)
cand = osutils.ReadFile(latest_afdo_path).strip()
cand_build = int(cand.split('.')[2])
curr_build = int(afdo_release_spec['build'])
# Verify the AFDO profile file is not too stale.
mod_date = latest_detail[0].creation_time
curr_date =
allowed_stale_days = datetime.timedelta(days=AFDO_ALLOWED_STALE_DAYS)
if ((curr_date - mod_date) > allowed_stale_days and
(curr_build - cand_build) > AFDO_ALLOWED_STALE_BUILDS):'Found latest AFDO info file %s but it is too old',
return cand, True
return cand, False
def GetBenchmarkProfile(cpv, _source, buildroot, gs_context):
"""Try to find the latest suitable AFDO profile file.
Try to find the latest AFDO profile generated for current release
and architecture. If there is none, check the previous release (mostly
in case we have just branched).
cpv: cpv object for Chrome.
source: benchmark source for which we are looking
buildroot: buildroot where AFDO data should be stored.
gs_context: GS context to retrieve data.
Name of latest suitable AFDO profile file if one is found.
None otherwise.
# Currently, benchmark based profiles can only be generated on amd64.
arch = 'amd64'
version_numbers = cpv.version.split('.')
current_release = version_numbers[0]
current_build = version_numbers[2]
afdo_release_spec = {
'package': cpv.package,
'arch': arch,
'release': current_release,
'build': current_build
afdo_file, expired = VerifyLatestAFDOFile(afdo_release_spec, buildroot,
if afdo_file and not expired:
return afdo_file
# The profile found in current release is too old. This clearly is a sign of
# problem. Therefore, don't try to find another one in previous branch.
if expired:
return None
# Could not find suitable AFDO file for the current release.
# Let's see if there is one from the previous release.
previous_release = str(int(current_release) - 1)
prev_release_spec = {
'package': cpv.package,
'arch': arch,
'release': previous_release,
'build': current_build
afdo_file, expired = VerifyLatestAFDOFile(prev_release_spec, buildroot,
if expired:
return None
return afdo_file
def UpdateLatestAFDOProfileInGS(cpv, arch, buildroot, profile_name, gs_context):
"""Updates our 'latest profile' file in GS to point to `profile_name`.
cpv: cpv object for Chrome.
arch: architecture for which we are looking for AFDO profile.
buildroot: buildroot where AFDO data should be stored.
profile_name: Name of the profile to point the 'latest profile' file at.
gs_context: GS context.
_, local_dir, _ = _BuildrootToWorkDirs(buildroot)
# Create latest-chrome-<arch>-<release>.afdo pointing to the name
# of the AFDO profile file and upload to GS.
current_release = cpv.version.split('.')[0]
afdo_release_spec = {
'package': cpv.package,
'arch': arch,
'release': current_release
latest_afdo_file = LATEST_CHROME_AFDO_FILE % afdo_release_spec
latest_afdo_path = os.path.join(local_dir, latest_afdo_file)
osutils.WriteFile(latest_afdo_path, profile_name)
GSURL_LATEST_CHROME_AFDO % afdo_release_spec,
def GenerateAFDOData(cpv, arch, board, buildroot, gs_context):
"""Generate AFDO profile data from 'perf' data.
Given the 'perf' profile, generate an AFDO profile using create_llvm_prof.
It also creates a latest-chrome-<arch>-<release>.afdo file pointing
to the generated AFDO profile.
Uploads the generated data to GS for retrieval by the chrome ebuild
file when doing an 'afdo_use' build.
It is possible the generated data has previously been uploaded to GS
in which case this routine will not upload the data again. Uploading
again may cause verication failures for the ebuild file referencing
the previous contents of the data.
cpv: cpv object for Chrome.
arch: architecture for which we are looking for AFDO profile.
board: board we are building for.
buildroot: buildroot where AFDO data should be stored.
gs_context: GS context to retrieve/store data.
Name of the AFDO profile file generated if successful, and whether or not
it was uploaded.
CHROME_UNSTRIPPED_NAME = 'chrome.unstripped'
version_number = cpv.version
afdo_spec = {'package': cpv.package, 'arch': arch, 'version': version_number}
chroot_root, local_dir, in_chroot_local_dir = _BuildrootToWorkDirs(buildroot)
# Upload compressed chrome debug binary to GS for triaging purposes.
# TODO(llozano): This simplifies things in case of need of triaging
# problems but is it really necessary?
debug_bin = CHROME_DEBUG_BIN % {'root': chroot_root, 'board': board}
comp_debug_bin_path = CompressAFDOFile(debug_bin, buildroot)
GSUploadIfNotPresent(gs_context, comp_debug_bin_path,
# create_llvm_prof demands the name of the profiled binary exactly matches
# the name of the unstripped binary or it is named 'chrome.unstripped'.
# So create a symbolic link with the appropriate name.
local_debug_sym = os.path.join(local_dir, CHROME_UNSTRIPPED_NAME)
in_chroot_debug_bin = CHROME_DEBUG_BIN % {'root': '', 'board': board}
os.symlink(in_chroot_debug_bin, local_debug_sym)
# Call create_llvm_prof tool to generated AFDO profile from 'perf' profile
# and upload it to GS. Need to call from within chroot since this tool
# was built inside chroot.
debug_sym = os.path.join(in_chroot_local_dir, CHROME_UNSTRIPPED_NAME)
# The name of the 'perf' file is based only on the version of chrome. The
# revision number is not included.
afdo_spec_no_rev = {
'package': cpv.package,
'arch': arch,
'version': cpv.version_no_rev.split('_')[0]
perf_afdo_file = CHROME_PERF_AFDO_FILE % afdo_spec_no_rev
perf_afdo_path = os.path.join(in_chroot_local_dir, perf_afdo_file)
afdo_file = CHROME_AFDO_FILE % afdo_spec
afdo_path = os.path.join(in_chroot_local_dir, afdo_file)
afdo_cmd = [
'--binary=%s' % debug_sym,
'--profile=%s' % perf_afdo_path,
'--out=%s' % afdo_path
afdo_cmd, enter_chroot=True, capture_output=True, print_cmd=True)
afdo_local_path = os.path.join(local_dir, afdo_file)
comp_afdo_path = CompressAFDOFile(afdo_local_path, buildroot)
uploaded_afdo_file = GSUploadIfNotPresent(gs_context, comp_afdo_path,
GSURL_CHROME_AFDO % afdo_spec)
return afdo_file, uploaded_afdo_file
def CanGenerateAFDOData(board):
"""Does this board has the capability of generating its own AFDO data?."""
def FindLatestProfile(target, versions):
"""Find latest profile that is usable by the target.
target: the target version
versions: a list of versions
latest profile that is older than the target
cand = bisect.bisect(versions, target) - 1
if cand >= 0:
return versions[cand]
return None
def PatchKernelEbuild(filename, version):
"""Update the AFDO_PROFILE_VERSION string in the given kernel ebuild file.
filename: name of the ebuild
version: e.g., [61, 9752, 0, 0]
contents = []
for line in osutils.ReadFile(filename).splitlines():
contents.append(KERNEL_PROFILE_WRITE_PATTERN % tuple(version) + '\n')
contents.append(line + '\n')
osutils.WriteFile(filename, contents, atomic=True)
def CWPProfileToVersionTuple(url):
"""Convert a CWP profile url to a version tuple
url: for example, gs://chromeos-prebuilt/afdo-job/cwp/chrome/
A tuple of (milestone, major, minor, timestamp)
fn_mat = (
r'([0-9]+)' for _ in range(0, 4)))
fn_mat.replace('.', '\\.')
return [int(x) for x in re.match(fn_mat, os.path.basename(url)).groups()]
def GetCWPProfile(cpv, source, _buildroot, gs_context):
"""Try to find the latest suitable AFDO profile file for cwp.
Try to find the latest AFDO profile generated for current release
and architecture.
cpv: cpv object for Chrome.
source: profile source
buildroot: buildroot where AFDO data should be stored.
gs_context: GS context to retrieve data.
Name of latest suitable AFDO profile file if one is found.
None otherwise.
ver_mat = r'([0-9]+)\.[0-9]+\.([0-9]+)\.([0-9]+)_rc-r[0-9]+'
target = [int(x) for x in re.match(ver_mat, cpv.version).groups()]
# Check 2 most recent milestones.
# When a branch just happens, the milestone of master increases by 1. There
# will be no profile from that milestone until a dev release is pushed for a
# short period of time. Therefore, a profile from previous branches must be
# picked instead.
# Originally, we search toward root in the branch tree for a profile. Now we
# prefer to look at the previous milestone if there's no profile from current
# milestone, because:
# 1. dev channel has few samples. The profile quality is much better from
# beta, which is always in a branch.
# 2. Master is actually closer to the branch tip than to the branch point,
# assuming that most of the changes on a branch are cherry-picked from
# master.
versions = []
for milestone in (target[0], target[0] - 1):
gs_ls_url = os.path.join(
CWP_CHROME_PROFILE_NAME_PATTERN % (milestone, '*', '*', '*'))
res = gs_context.List(gs_ls_url)
versions.extend(CWPProfileToVersionTuple(x) for x in [r.url for r in res])
except gs.GSNoSuchKey:
if not versions:'profile not found for: %s', cpv.version)
return None
cand = FindLatestProfile(target, versions)
# reconstruct the filename and strip .xz
return (CWP_CHROME_PROFILE_NAME_PATTERN % tuple(cand))[:-3]
def GetAvailableKernelProfiles():
"""Get available profiles on specified gsurl.
a dictionary that maps kernel version, e.g. "4_4" to a list of
[milestone, major, minor, timestamp]. E.g,
[62, 9901, 21, 1506581147]
gs_context = gs.GSContext()
res = gs_context.List(gs_ls_url)
except gs.GSNoSuchKey:'gs files not found: %s', gs_ls_url)
return {}
all_matches = [re.match(gs_match_url, x.url) for x in res]
matches = [x for x in all_matches if x]
versions = {}
for m in matches:
versions.setdefault(, []).append(
[int(x) for x in m.groups()[1:]])
for v in versions:
return versions
def FindKernelEbuilds():
"""Find all ebuilds that specify AFDO_PROFILE_VERSION.
The only assumption is that the ebuild files are named as the match pattern
in kver(). If it fails to recognize the ebuild filename, an error will be
equery is not used because that would require enumerating the boards, which
is no easier than enumerating the kernel versions or ebuilds.
a list of (ebuilds, kernel rev)
def kver(ebuild):
matched = re.match(r'.*/chromeos-kernel-([0-9]+_[0-9]+)-.+\.ebuild$',
if matched:
return'_', '.')
raise UnknownKernelVersion(
'Kernel version cannot be inferred from ebuild filename "%s".' % ebuild)
for fn in glob.glob(os.path.join(KERNEL_EBUILD_ROOT, '*', '*.ebuild')):
for line in osutils.ReadFile(fn).splitlines():
yield (fn, kver(fn))
def ProfileAge(profile_version):
"""Tell the age of profile_version in days.
profile_version: [chrome milestone, cros major, cros minor, timestamp]
e.g., [61, 9752, 0, 1500000000]
Age of profile_version in days.
return (datetime.datetime.utcnow() - datetime.datetime.utcfromtimestamp(
'benchmark': GetBenchmarkProfile,
'silvermont': GetCWPProfile,
'airmont': GetCWPProfile,
'haswell': GetCWPProfile,
'broadwell': GetCWPProfile,