#!/usr/bin/env python # # Copyright (C) 2019 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Sample Usage: # $ python update_profiles.py 500000 git_master ALL --profdata-suffix 2019-04-15 # # Additional/frequently-used arguments: # -b BUG adds a 'Bug: <BUG>' to the commit message when adding the profiles. # --do-not-merge adds a 'DO NOT MERGE' tag to the commit message to restrict # automerge of profiles from release branches. # # Try '-h' for a full list of command line arguments. import argparse import os import shutil import subprocess import sys import tempfile import zipfile import utils X20_BASE_LOCATION = '/google/data/ro/teams/android-pgo-data' class Benchmark(object): def __init__(self, name): self.name = name def x20_profile_location(self): raise NotImplementedError() def apct_job_name(self): raise NotImplementedError() def profdata_file(self, suffix=''): profdata = os.path.join(self.name, '{}.profdata'.format(self.name)) if suffix: profdata += '.' + suffix return profdata def profraw_files(self): raise NotImplementedError() def merge_profraws(self, profile_dir, output): profraws = [os.path.join(profile_dir, p) for p in self.profraw_files(profile_dir)] utils.run_llvm_profdata(profraws, output) class NativeExeBenchmark(Benchmark): def apct_job_name(self): return 'pgo-collector' def x20_profile_location(self): return os.path.join(X20_BASE_LOCATION, 'raw') def profraw_files(self, profile_dir): if self.name == 'hwui': return ['hwuimacro.profraw', 'hwuimacro_64.profraw', 'hwuimicro.profraw', 'hwuimicro_64.profraw', 'skia_nanobench.profraw', 'skia_nanobench_64.profraw'] elif self.name == 'hwbinder': return ['hwbinder.profraw', 'hwbinder_64.profraw'] class APKBenchmark(Benchmark): def apct_job_name(self): return 'apk-pgo-collector' def x20_profile_location(self): return os.path.join(X20_BASE_LOCATION, 'apk-raw') def profdata_file(self, suffix=''): profdata = os.path.join('art', '{}_arm_arm64.profdata'.format(self.name)) if suffix: profdata += '.' + suffix return profdata def profraw_files(self, profile_dir): return os.listdir(profile_dir) def BenchmarkFactory(benchmark_name): if benchmark_name == 'dex2oat': return APKBenchmark(benchmark_name) elif benchmark_name in ['hwui', 'hwbinder']: return NativeExeBenchmark(benchmark_name) else: raise RuntimeError('Unknown benchmark ' + benchmark_name) def extract_profiles(benchmark, branch, build, output_dir): # The APCT results are stored in # <x20_profile_base>/<branch>/<build>/<apct_job_name>/<arbitrary_invocation_dir>/ # # The PGO files are in _data_local_tmp_<id>.zip in the above directory. profile_base = os.path.join(benchmark.x20_profile_location(), branch, build, benchmark.apct_job_name()) invocation_dirs = os.listdir(profile_base) if len(invocation_dirs) == 0: raise RuntimeError('No invocations found in {}'.format(profile_base)) if len(invocation_dirs) > 1: # TODO Add option to pick/select an invocation from the command line. raise RuntimeError('More than one invocation found in {}'.format(profile_base)) profile_dir = os.path.join(profile_base, invocation_dirs[0]) zipfiles = [f for f in os.listdir(profile_dir) if f.startswith('_data_local_tmp')] if len(zipfiles) != 1: raise RuntimeError('Expected one zipfile in {}. Found {}'.format(profile_dir, len(zipfiles))) zipfile_name = os.path.join(profile_dir, zipfiles[0]) zip_ref = zipfile.ZipFile(zipfile_name) zip_ref.extractall(output_dir) zip_ref.close() KNOWN_BENCHMARKS = ['ALL', 'dex2oat', 'hwui', 'hwbinder'] def parse_args(): """Parses and returns command line arguments.""" parser = argparse.ArgumentParser() parser.add_argument( 'build', metavar='BUILD', help='Build number to pull from the build server.') parser.add_argument( '-b', '--bug', type=int, help='Bug to reference in commit message.') parser.add_argument( '--use-current-branch', action='store_true', help='Do not repo start a new branch for the update.') parser.add_argument( '--add-do-not-merge', action='store_true', help='Add \'DO NOT MERGE\' to the commit message.') parser.add_argument( '--profdata-suffix', type=str, default='', help='Suffix to append to merged profdata file') parser.add_argument( 'branch', metavar='BRANCH', help='Fetch profiles for BRANCH (e.g. git_qt-release)') parser.add_argument( 'benchmark', metavar='BENCHMARK', help='Update profiles for BENCHMARK. Choices are {}'.format(KNOWN_BENCHMARKS)) parser.add_argument( '--skip-cleanup', '-sc', action='store_true', default=False, help='Skip the cleanup, and leave intermediate files (in /tmp/pgo-profiles-*)') return parser.parse_args() def get_current_profile(benchmark): profile = benchmark.profdata_file() dirname, basename = os.path.split(profile) old_profiles = [f for f in os.listdir(dirname) if f.startswith(basename)] if len(old_profiles) == 0: return '' return os.path.join(dirname, old_profiles[0]) def main(): args = parse_args() if args.benchmark == 'ALL': worklist = KNOWN_BENCHMARKS[1:] else: worklist = [args.benchmark] profiles_project = os.path.join(utils.android_build_top(), 'toolchain', 'pgo-profiles') os.chdir(profiles_project) if not args.use_current_branch: branch_name = 'update-profiles-' + args.build utils.check_call(['repo', 'start', branch_name, '.']) for benchmark_name in worklist: benchmark = BenchmarkFactory(benchmark_name) # Existing profile file, which gets 'rm'-ed from 'git' down below. current_profile = get_current_profile(benchmark) # Extract profiles to a temporary directory. After extraction, we # expect to find one subdirectory with profraw files under the temporary # directory. extract_dir = tempfile.mkdtemp(prefix='pgo-profiles-'+benchmark_name) extract_profiles(benchmark, args.branch, args.build, extract_dir) if len(os.listdir(extract_dir)) != 1: raise RuntimeError("Expected one subdir under {}".format(extract_dir)) extract_subdir = os.path.join(extract_dir, os.listdir(extract_dir)[0]) # Merge profiles. profdata = benchmark.profdata_file(args.profdata_suffix) benchmark.merge_profraws(extract_subdir, profdata) # Construct 'git' commit message. message_lines = [ 'Update PGO profiles for {}'.format(benchmark_name), '', 'The profiles are from build {}.'.format(args.build), '' ] if args.add_do_not_merge: message_lines[0] = '[DO NOT MERGE] ' + message_lines[0] if args.bug: message_lines.append('') message_lines.append('Bug: http://b/{}'.format(args.bug)) message_lines.append('Test: Build (TH)') message = '\n'.join(message_lines) # Invoke git: Delete current profile, add new profile and commit these # changes. if current_profile: utils.check_call(['git', 'rm', current_profile]) utils.check_call(['git', 'add', profdata]) utils.check_call(['git', 'commit', '-m', message]) if not args.skip_cleanup: shutil.rmtree(extract_dir) if __name__ == '__main__': main()