1 # Copyright 2014 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Module containing the various utilities to build Chrome with AFDO.
7 For a description of AFDO see gcc.gnu.org/wiki/AutoFDO.
14 from chromite.cbuildbot import failures_lib
15 from chromite.cbuildbot import constants
16 from chromite.lib import cros_build_lib
17 from chromite.lib import git
18 from chromite.lib import gs
19 from chromite.lib import osutils
20 from chromite.lib import timeout_util
23 # AFDO-specific constants.
24 # Chrome URL where AFDO data is stored.
25 AFDO_PROD_URL = 'gs://chromeos-prebuilt/afdo-job/canonicals/'
26 AFDO_TEST_URL = '%s/afdo-job/canonicals/' % constants.TRASH_BUCKET
27 AFDO_BASE_URL = AFDO_PROD_URL
28 AFDO_CHROOT_ROOT = os.path.join('%(build_root)s', constants.DEFAULT_CHROOT_DIR)
29 AFDO_LOCAL_DIR = os.path.join('%(root)s', 'tmp')
30 AFDO_BUILDROOT_LOCAL = AFDO_LOCAL_DIR % {'root': AFDO_CHROOT_ROOT}
31 CHROME_ARCH_VERSION = '%(package)s-%(arch)s-%(version)s'
32 CHROME_PERF_AFDO_FILE = '%s.perf.data' % CHROME_ARCH_VERSION
33 CHROME_PERF_AFDO_URL = '%s%s.bz2' % (AFDO_BASE_URL, CHROME_PERF_AFDO_FILE)
34 CHROME_AFDO_FILE = '%s.afdo' % CHROME_ARCH_VERSION
35 CHROME_AFDO_URL = '%s%s.bz2' % (AFDO_BASE_URL, CHROME_AFDO_FILE)
36 CHROME_ARCH_RELEASE = '%(package)s-%(arch)s-%(release)s'
37 LATEST_CHROME_AFDO_FILE = 'latest-%s.afdo' % CHROME_ARCH_RELEASE
38 LATEST_CHROME_AFDO_URL = AFDO_BASE_URL + LATEST_CHROME_AFDO_FILE
39 CHROME_DEBUG_BIN = os.path.join('%(root)s',
40 'build/%(board)s/usr/lib/debug',
41 'opt/google/chrome/chrome.debug')
42 CHROME_DEBUG_BIN_URL = '%s%s.debug.bz2' % (AFDO_BASE_URL, CHROME_ARCH_VERSION)
44 AFDO_GENERATE_GCOV_TOOL = '/usr/bin/create_gcov'
46 # regex to find AFDO file for specific architecture within the ebuild file.
47 CHROME_EBUILD_AFDO_EXP = r'^(?P<bef>AFDO_FILE\["%s"\]=")(?P<name>.*)(?P<aft>")'
48 # and corresponding replacement string.
49 CHROME_EBUILD_AFDO_REPL = r'\g<bef>%s\g<aft>'
51 # How old can the AFDO data be? (in days).
52 AFDO_ALLOWED_STALE = 7
54 # TODO(llozano): Figure out exact list of boards/architectures that
55 # can generate 'perf' suitable for AFDO (with LBR events).
56 # Set of boards that can generate the AFDO profile (can generate 'perf'
57 # data with LBR events).
58 AFDO_DATA_GENERATORS = ('lumpy',)
59 # Set of boards that are allowed to update the Chrome ebuild with the
60 # appropriate AFDO file name.
61 # TODO(llozano): is it possible that the canary build for "falco" be
62 # executed before the PFQ for lumpy?. Is there a problem with this?
63 AFDO_EBUILD_UPDATERS = ('lumpy', 'daisy', 'x86-alex')
65 # For a given architecture, which architecture is used to generate
66 # the AFDO profile. Some architectures are not able to generate their
68 AFDO_ARCH_GENERATORS = {'amd64': 'amd64',
72 AFDO_ALERT_RECIPIENTS = ['chromeos-toolchain@google.com']
75 class MissingAFDOData(failures_lib.StepFailure):
76 """Exception thrown when necessary AFDO data is missing."""
79 class MissingAFDOMarkers(failures_lib.StepFailure):
80 """Exception thrown when necessary ebuild markers for AFDO are missing."""
83 def CompressAFDOFile(to_compress, buildroot):
84 """Compress file used by AFDO process.
87 to_compress: File to compress.
88 buildroot: buildroot where to store the compressed data.
91 Name of the compressed data file.
93 local_dir = AFDO_BUILDROOT_LOCAL % {'build_root': buildroot}
94 dest = os.path.join(local_dir, os.path.basename(to_compress)) + '.bz2'
95 cros_build_lib.CompressFile(to_compress, dest)
99 def UncompressAFDOFile(to_decompress, buildroot):
100 """Decompress file used by AFDO process.
103 to_decompress: File to decompress.
104 buildroot: buildroot where to store the decompressed data.
106 local_dir = AFDO_BUILDROOT_LOCAL % {'build_root': buildroot}
107 basename = os.path.basename(to_decompress)
108 dest_basename = basename.rsplit('.', 1)[0]
109 dest = os.path.join(local_dir, dest_basename)
110 cros_build_lib.UncompressFile(to_decompress, dest)
114 def GSUploadIfNotPresent(gs_context, src, dest):
115 """Upload a file to GS only if the file does not exist.
117 Will not generate an error if the file already exist in GS. It will
120 I could use GSContext.Copy(src,dest,version=0) here but it does not seem
121 to work for large files. Using GSContext.Exists(dest) instead. See
125 gs_context: GS context instance.
127 dest: Destination location.
130 True if file was uploaded. False otherwise.
132 if gs_context.Exists(dest):
133 cros_build_lib.Warning('File %s already in GS', dest)
136 gs_context.Copy(src, dest, acl='public-read')
140 def CheckAFDOPerfData(arch, cpv, buildroot, gs_context):
141 """Check whether AFDO perf data exists for the given architecture.
143 Check if 'perf' data file for this architecture and release is available
144 in GS. If so, copy it into a temp directory in the buildroot.
147 arch: architecture we're going to build Chrome for.
148 cpv: The portage_utilities.CPV object for chromeos-chrome.
149 buildroot: buildroot where AFDO data should be stored.
150 gs_context: GS context to retrieve data.
153 True if AFDO perf data is available. False otherwise.
155 # The file name of the perf data is based only in the chrome version.
156 # The test case that produces it does not know anything about the
158 # TODO(llozano): perf data filename should include the revision number.
159 version_number = cpv.version_no_rev.split('_')[0]
160 chrome_spec = {'package': cpv.package,
162 'version': version_number}
163 url = CHROME_PERF_AFDO_URL % chrome_spec
164 if not gs_context.Exists(url):
165 cros_build_lib.Info('Could not find AFDO perf data')
167 dest_dir = AFDO_BUILDROOT_LOCAL % {'build_root': buildroot}
168 dest_path = os.path.join(dest_dir, url.rsplit('/', 1)[1])
169 gs_context.Copy(url, dest_path)
171 UncompressAFDOFile(dest_path, buildroot)
172 cros_build_lib.Info('Found and retrieved AFDO perf data')
176 def WaitForAFDOPerfData(cpv, arch, buildroot, gs_context,
177 timeout=constants.AFDO_GENERATE_TIMEOUT):
178 """Wait for AFDO perf data to show up (with an appropriate timeout).
181 arch: architecture we're going to build Chrome for.
182 cpv: CPV object for Chrome.
183 buildroot: buildroot where AFDO data should be stored.
184 gs_context: GS context to retrieve data.
185 timeout: How long to wait total, in seconds.
188 True if found the AFDO perf data before the timeout expired.
192 timeout_util.WaitForReturnTrue(
194 func_args=(arch, cpv, buildroot, gs_context),
195 timeout=timeout, period=constants.SLEEP_TIMEOUT)
196 except timeout_util.TimeoutError:
201 def PatchChromeEbuildAFDOFile(ebuild_file, arch_profiles):
202 """Patch the Chrome ebuild with the dictionary of {arch: afdo_file} pairs.
205 ebuild_file: path of the ebuild file within the chroot.
206 arch_profiles: {arch: afdo_file} pairs to put into the ebuild.
208 original_ebuild = cros_build_lib.FromChrootPath(ebuild_file)
209 modified_ebuild = '%s.new' % original_ebuild
214 for arch in arch_profiles.keys():
215 arch_patterns[arch] = re.compile(CHROME_EBUILD_AFDO_EXP % arch)
216 arch_repls[arch] = CHROME_EBUILD_AFDO_REPL % arch_profiles[arch]
217 arch_markers[arch] = False
219 with open(original_ebuild, 'r') as original:
220 with open(modified_ebuild, 'w') as modified:
221 for line in original:
222 for arch in arch_profiles.keys():
223 matched = arch_patterns[arch].match(line)
225 arch_markers[arch] = True
226 modified.write(arch_patterns[arch].sub(arch_repls[arch], line))
228 else: # line without markers, just copy it.
231 for arch, found in arch_markers.iteritems():
233 raise MissingAFDOMarkers('Chrome ebuild file does not have appropriate '
234 'AFDO markers for arch %s' % arch)
236 os.rename(modified_ebuild, original_ebuild)
239 def UpdateChromeEbuildAFDOFile(board, arch_profiles):
240 """Update chrome ebuild with the dictionary of {arch: afdo_file} pairs.
242 Modifies the Chrome ebuild to set the appropriate AFDO file for each
243 given architecture. Regenerates the associated Manifest file and
244 commits the new ebuild and Manifest.
247 board: board we are building Chrome for.
248 arch_profiles: {arch: afdo_file} pairs to put into the ebuild.
250 # Find the Chrome ebuild file.
251 equery_prog = 'equery'
252 ebuild_prog = 'ebuild'
254 equery_prog += '-%s' % board
255 ebuild_prog += '-%s' % board
257 equery_cmd = [equery_prog, 'w', 'chromeos-chrome']
258 ebuild_file = cros_build_lib.RunCommand(equery_cmd,
260 redirect_stdout=True).output.rstrip()
262 # Patch the ebuild file with the names of the available afdo_files.
263 PatchChromeEbuildAFDOFile(ebuild_file, arch_profiles)
265 # Also patch the 9999 ebuild. This is necessary because the uprev
266 # process starts from the 9999 ebuild file and then compares to the
267 # current version to see if the uprev is really necessary. We dont
268 # want the names of the available afdo_files to show as differences.
269 # It also allows developers to do USE=afdo_use when using the 9999
271 ebuild_9999 = os.path.join(os.path.dirname(ebuild_file),
272 'chromeos-chrome-9999.ebuild')
273 PatchChromeEbuildAFDOFile(ebuild_9999, arch_profiles)
275 # Regenerate the Manifest file.
277 # If using the GS test location, pass this location to the
279 if AFDO_BASE_URL == AFDO_TEST_URL:
280 ebuild_gs_dir = {'AFDO_GS_DIRECTORY': AFDO_TEST_URL}
281 gen_manifest_cmd = [ebuild_prog, ebuild_file, 'manifest', '--force']
282 cros_build_lib.RunCommand(gen_manifest_cmd, enter_chroot=True,
283 extra_env=ebuild_gs_dir, print_cmd=True)
285 ebuild_dir = cros_build_lib.FromChrootPath(os.path.dirname(ebuild_file))
286 git.RunGit(ebuild_dir, ['add', 'Manifest'])
288 # Check if anything changed compared to the previous version.
289 mod_files = ['Manifest', os.path.basename(ebuild_file),
290 os.path.basename(ebuild_9999)]
291 modifications = git.RunGit(ebuild_dir,
292 ['status', '--porcelain', '--'] + mod_files,
293 capture_output=True, print_cmd=True).output
294 if not modifications:
295 cros_build_lib.Info('AFDO info for the Chrome ebuild did not change. '
299 # If there are changes to ebuild or Manifest, commit them.
300 commit_msg = ('"Set {arch: afdo_file} pairs %s and updated Manifest"'
302 git.RunGit(ebuild_dir,
303 ['commit', '-m', commit_msg, '--'] + mod_files,
307 def VerifyLatestAFDOFile(afdo_release_spec, buildroot, gs_context):
308 """Verify that the latest AFDO profile for a release is suitable.
310 Find the latest AFDO profile file for a particular release and check
311 that it is not too stale. The latest AFDO profile name for a release
312 can be found in a file in GS under the name
313 latest-chrome-<arch>-<release>.afdo.
316 afdo_release_spec: architecture and release to find the latest AFDO
318 buildroot: buildroot where AFDO data should be stored.
319 gs_context: GS context to retrieve data.
322 The name of the AFDO profile file if a suitable one was found.
325 latest_afdo_url = LATEST_CHROME_AFDO_URL % afdo_release_spec
327 # Check if latest-chrome-<arch>-<release>.afdo exists.
329 if gs_context.Exists(latest_afdo_url):
330 latest_detail = gs_context.LSWithDetails(latest_afdo_url)
331 if not latest_detail:
332 cros_build_lib.Info('Could not find latest AFDO info file %s' %
336 # Verify the AFDO profile file is not too stale.
337 mod_date = latest_detail[0][2]
338 curr_date = datetime.datetime.now()
339 allowed_stale_days = datetime.timedelta(days=AFDO_ALLOWED_STALE)
340 if (curr_date - mod_date) > allowed_stale_days:
341 cros_build_lib.Info('Found latest AFDO info file %s but it is too old' %
345 # Then get the name of the latest valid AFDO profile file.
346 local_dir = AFDO_BUILDROOT_LOCAL % {'build_root': buildroot }
347 latest_afdo_file = LATEST_CHROME_AFDO_FILE % afdo_release_spec
348 latest_afdo_path = os.path.join(local_dir, latest_afdo_file)
349 gs_context.Copy(latest_afdo_url, latest_afdo_path)
351 return osutils.ReadFile(latest_afdo_path).strip()
354 def GetLatestAFDOFile(cpv, arch, buildroot, gs_context):
355 """Try to find the latest suitable AFDO profile file.
357 Try to find the latest AFDO profile generated for current release
358 and architecture. If there is none, check the previous release (mostly
359 in case we have just branched).
362 cpv: cpv object for Chrome.
363 arch: architecture for which we are looking for AFDO profile.
364 buildroot: buildroot where AFDO data should be stored.
365 gs_context: GS context to retrieve data.
368 Name of latest suitable AFDO profile file if one is found.
371 generator_arch = AFDO_ARCH_GENERATORS[arch]
372 version_number = cpv.version
373 current_release = version_number.split('.')[0]
374 afdo_release_spec = {'package': cpv.package,
375 'arch': generator_arch,
376 'release': current_release}
377 afdo_file = VerifyLatestAFDOFile(afdo_release_spec, buildroot, gs_context)
381 # Could not find suitable AFDO file for the current release.
382 # Let's see if there is one from the previous release.
383 previous_release = str(int(current_release) - 1)
384 prev_release_spec = {'package': cpv.package,
385 'arch': generator_arch,
386 'release': previous_release}
387 return VerifyLatestAFDOFile(prev_release_spec, buildroot, gs_context)
390 def GenerateAFDOData(cpv, arch, board, buildroot, gs_context):
391 """Generate AFDO profile data from 'perf' data.
393 Given the 'perf' profile, generate an AFDO profile using create_gcov.
394 It also creates a latest-chrome-<arch>-<release>.afdo file pointing
395 to the generated AFDO profile.
396 Uploads the generated data to GS for retrieval by the chrome ebuild
397 file when doing an 'afdo_use' build.
398 It is possible the generated data has previously been uploaded to GS
399 in which case this routine will not upload the data again. Uploading
400 again may cause verication failures for the ebuild file referencing
401 the previous contents of the data.
404 cpv: cpv object for Chrome.
405 arch: architecture for which we are looking for AFDO profile.
406 board: board we are building for.
407 buildroot: buildroot where AFDO data should be stored.
408 gs_context: GS context to retrieve/store data.
411 Name of the AFDO profile file generated if successful.
413 CHROME_UNSTRIPPED_NAME = 'chrome.unstripped'
415 version_number = cpv.version
416 afdo_spec = {'package': cpv.package,
418 'version': version_number}
419 chroot_root = AFDO_CHROOT_ROOT % {'build_root': buildroot }
420 local_dir = AFDO_LOCAL_DIR % {'root': chroot_root }
421 in_chroot_local_dir = AFDO_LOCAL_DIR % {'root': '' }
423 # Upload compressed chrome debug binary to GS for triaging purposes.
424 # TODO(llozano): This simplifies things in case of need of triaging
425 # problems but is it really necessary?
426 debug_bin = CHROME_DEBUG_BIN % {'root': chroot_root,
428 comp_debug_bin_path = CompressAFDOFile(debug_bin, buildroot)
429 GSUploadIfNotPresent(gs_context, comp_debug_bin_path,
430 CHROME_DEBUG_BIN_URL % afdo_spec)
432 # create_gcov demands the name of the profiled binary exactly matches
433 # the name of the unstripped binary or it is named 'chrome.unstripped'.
434 # So create a symbolic link with the appropriate name.
435 local_debug_sym = os.path.join(local_dir, CHROME_UNSTRIPPED_NAME)
436 in_chroot_debug_bin = CHROME_DEBUG_BIN % {'root': '', 'board': board }
437 osutils.SafeUnlink(local_debug_sym)
438 os.symlink(in_chroot_debug_bin, local_debug_sym)
440 # Call create_gcov tool to generated AFDO profile from 'perf' profile
441 # and upload it to GS. Need to call from within chroot since this tool
442 # was built inside chroot.
443 debug_sym = os.path.join(in_chroot_local_dir, CHROME_UNSTRIPPED_NAME)
444 # The name of the 'perf' file is based only on the version of chrome. The
445 # revision number is not included.
446 afdo_spec_no_rev = {'package': cpv.package,
448 'version': cpv.version_no_rev.split('_')[0]}
449 perf_afdo_file = CHROME_PERF_AFDO_FILE % afdo_spec_no_rev
450 perf_afdo_path = os.path.join(in_chroot_local_dir, perf_afdo_file)
451 afdo_file = CHROME_AFDO_FILE % afdo_spec
452 afdo_path = os.path.join(in_chroot_local_dir, afdo_file)
453 afdo_cmd = [AFDO_GENERATE_GCOV_TOOL,
454 '--binary=%s' % debug_sym,
455 '--profile=%s' % perf_afdo_path,
456 '--gcov=%s' % afdo_path]
457 cros_build_lib.RunCommand(afdo_cmd, enter_chroot=True, capture_output=True,
460 afdo_local_path = os.path.join(local_dir, afdo_file)
461 comp_afdo_path = CompressAFDOFile(afdo_local_path, buildroot)
462 uploaded_afdo_file = GSUploadIfNotPresent(gs_context, comp_afdo_path,
463 CHROME_AFDO_URL % afdo_spec)
465 if uploaded_afdo_file:
466 # Create latest-chrome-<arch>-<release>.afdo pointing to the name
467 # of the AFDO profile file and upload to GS.
468 current_release = version_number.split('.')[0]
469 afdo_release_spec = {'package': cpv.package,
471 'release': current_release}
472 latest_afdo_file = LATEST_CHROME_AFDO_FILE % afdo_release_spec
473 latest_afdo_path = os.path.join(local_dir, latest_afdo_file)
474 osutils.WriteFile(latest_afdo_path, afdo_file)
475 gs_context.Copy(latest_afdo_path,
476 LATEST_CHROME_AFDO_URL % afdo_release_spec,
482 def CanGenerateAFDOData(board):
483 """Does this board has the capability of generating its own AFDO data?."""
484 return board in AFDO_DATA_GENERATORS
487 def CanUpdateEbuildAFDOData(board):
488 """Is this board one of the designated to update the chrome ebuild?."""
489 return board in AFDO_EBUILD_UPDATERS
492 def GenerateOrFindAFDOData(cpv, arch, board, buildroot):
493 """Generate or find the appropriate AFDO profile for the given architecture.
495 For the architectures that can generate a 'perf' tool profile, wait for
496 it to be generated by the autotest AFDO_generate and generate an AFDO
497 profile for it. In the generation of the 'perf' profile failed, use
498 a previously generated AFDO profile that is not too stale.
499 For the architectures that cannot generate a 'perf' tool profile, use
500 the AFDO profile from another architecture (e.g.: ARM can use a profile
502 Once we have an adequate AFDO profile, put this information in the
506 cpv: cpv object for Chrome.
507 arch: architecture for which we are looking for AFDO profile.
508 board: board we are building for.
509 buildroot: buildroot where AFDO data should be stored.
511 if not CanUpdateEbuildAFDOData(board):
514 gs_context = gs.GSContext()
516 if CanGenerateAFDOData(board):
517 # Generation of AFDO could fail for different reasons. Try to be
518 # resilient about this and in case of failure just find an
519 # older AFDO profile.
521 if WaitForAFDOPerfData(cpv, arch, buildroot, gs_context):
522 afdo_file = GenerateAFDOData(cpv, arch, board, buildroot, gs_context)
524 cros_build_lib.Info('Generated %s AFDO profile %s',
526 # Will let system-exiting exceptions through.
528 cros_build_lib.PrintBuildbotStepWarnings()
529 cros_build_lib.Warning('AFDO profile generation failed with exception ',
532 cros_build_lib.Info('Trying to find previous appropriate AFDO profile')
533 afdo_file = GetLatestAFDOFile(cpv, arch, buildroot, gs_context)
535 cros_build_lib.Info('Found previous %s AFDO profile %s',
538 raise MissingAFDOData('Could not generate or find appropriate AFDO profile')
540 # We found an AFDO profile. Lets put the info in the chrome ebuild.
541 UpdateChromeEbuildAFDOFile(board, {arch: afdo_file})