1#!/usr/bin/env python
2#
3# Copyright (C) 2019 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17# Sample Usage:
18# $ python update_profiles.py 500000 ALL --profdata-suffix 2019-04-15
19#
20# Additional/frequently-used arguments:
21#   -b BUG adds a 'Bug: <BUG>' to the commit message when adding the profiles.
22#   --do-not-merge adds a 'DO NOT MERGE' tag to the commit message to restrict
23#                  automerge of profiles from release branches.
24#
25# Try '-h' for a full list of command line arguments.
26
27import argparse
28import os
29import shutil
30import subprocess
31import sys
32import tempfile
33import zipfile
34
35import utils
36
37from android_build_client import AndroidBuildClient
38
39
40class Benchmark(object):
41
42    def __init__(self, name):
43        self.name = name
44
45    def apct_test_tag(self):
46        raise NotImplementedError()
47
48    def profdata_file(self, suffix=''):
49        profdata = os.path.join(self.name, '{}.profdata'.format(self.name))
50        if suffix:
51            profdata += '.' + suffix
52        return profdata
53
54    def profraw_files(self):
55        raise NotImplementedError()
56
57    def merge_profraws(self, profile_dir, output):
58        profraws = [
59            os.path.join(profile_dir, p)
60            for p in self.profraw_files(profile_dir)
61        ]
62        utils.run_llvm_profdata(profraws, output)
63
64
65class NativeExeBenchmark(Benchmark):
66
67    def apct_test_tag(self):
68        return 'apct/perf/pgo/profile-collector'
69
70    def profraw_files(self, profile_dir):
71        if self.name == 'hwui':
72            return [
73                'hwuimacro.profraw', 'hwuimacro_64.profraw',
74                'hwuimicro.profraw', 'hwuimicro_64.profraw',
75                'skia_nanobench.profraw', 'skia_nanobench_64.profraw'
76            ]
77        elif self.name == 'hwbinder':
78            return ['hwbinder.profraw', 'hwbinder_64.profraw']
79
80
81class APKBenchmark(Benchmark):
82
83    def apct_test_tag(self):
84        return 'apct/perf/pgo/apk-profile-collector'
85
86    def profdata_file(self, suffix=''):
87        profdata = os.path.join('art',
88                                '{}_arm_arm64.profdata'.format(self.name))
89        if suffix:
90            profdata += '.' + suffix
91        return profdata
92
93    def profraw_files(self, profile_dir):
94        return os.listdir(profile_dir)
95
96
97def BenchmarkFactory(benchmark_name):
98    if benchmark_name == 'dex2oat':
99        return APKBenchmark(benchmark_name)
100    elif benchmark_name in ['hwui', 'hwbinder']:
101        return NativeExeBenchmark(benchmark_name)
102    else:
103        raise RuntimeError('Unknown benchmark ' + benchmark_name)
104
105
106def extract_profiles(build, test_tag, build_client, output_dir):
107    pgo_zip = build_client.download_pgo_zip(build, test_tag, output_dir)
108
109    zipfile_name = os.path.join(pgo_zip)
110    zip_ref = zipfile.ZipFile(zipfile_name)
111    zip_ref.extractall(output_dir)
112    zip_ref.close()
113
114
115KNOWN_BENCHMARKS = ['ALL', 'dex2oat', 'hwui', 'hwbinder']
116
117
118def parse_args():
119    """Parses and returns command line arguments."""
120    parser = argparse.ArgumentParser()
121
122    parser.add_argument(
123        'build',
124        metavar='BUILD',
125        help='Build number to pull from the build server.')
126
127    parser.add_argument(
128        '-b', '--bug', type=int, help='Bug to reference in commit message.')
129
130    parser.add_argument(
131        '--use-current-branch',
132        action='store_true',
133        help='Do not repo start a new branch for the update.')
134
135    parser.add_argument(
136        '--add-do-not-merge',
137        action='store_true',
138        help='Add \'DO NOT MERGE\' to the commit message.')
139
140    parser.add_argument(
141        '--profdata-suffix',
142        type=str,
143        default='',
144        help='Suffix to append to merged profdata file')
145
146    parser.add_argument(
147        'benchmark',
148        metavar='BENCHMARK',
149        help='Update profiles for BENCHMARK.  Choices are {}'.format(
150            KNOWN_BENCHMARKS))
151
152    parser.add_argument(
153        '--skip-cleanup',
154        '-sc',
155        action='store_true',
156        default=False,
157        help='Skip the cleanup, and leave intermediate files (in /tmp/pgo-profiles-*)'
158    )
159
160    return parser.parse_args()
161
162
163def get_current_profile(benchmark):
164    profile = benchmark.profdata_file()
165    dirname, basename = os.path.split(profile)
166
167    old_profiles = [f for f in os.listdir(dirname) if f.startswith(basename)]
168    if len(old_profiles) == 0:
169        return ''
170    return os.path.join(dirname, old_profiles[0])
171
172
173def main():
174    args = parse_args()
175
176    if args.benchmark == 'ALL':
177        worklist = KNOWN_BENCHMARKS[1:]
178    else:
179        worklist = [args.benchmark]
180
181    profiles_project = os.path.join(utils.android_build_top(), 'toolchain',
182                                    'pgo-profiles')
183    os.chdir(profiles_project)
184
185    if not args.use_current_branch:
186        branch_name = 'update-profiles-' + args.build
187        utils.check_call(['repo', 'start', branch_name, '.'])
188
189    build_client = AndroidBuildClient()
190
191    for benchmark_name in worklist:
192        benchmark = BenchmarkFactory(benchmark_name)
193
194        # Existing profile file, which gets 'rm'-ed from 'git' down below.
195        current_profile = get_current_profile(benchmark)
196
197        # Extract profiles to a temporary directory.  After extraction, we
198        # expect to find one subdirectory with profraw files under the temporary
199        # directory.
200        extract_dir = tempfile.mkdtemp(prefix='pgo-profiles-' + benchmark_name)
201        extract_profiles(args.build, benchmark.apct_test_tag(), build_client,
202                         extract_dir)
203
204        extract_subdirs = [
205            os.path.join(extract_dir, sub)
206            for sub in os.listdir(extract_dir)
207            if os.path.isdir(os.path.join(extract_dir, sub))
208        ]
209        if len(extract_subdirs) != 1:
210            raise RuntimeError(
211                'Expected one subdir under {}'.format(extract_dir))
212
213        # Merge profiles.
214        profdata = benchmark.profdata_file(args.profdata_suffix)
215        benchmark.merge_profraws(extract_subdirs[0], profdata)
216
217        # Construct 'git' commit message.
218        message_lines = [
219            'Update PGO profiles for {}'.format(benchmark_name), '',
220            'The profiles are from build {}.'.format(args.build), ''
221        ]
222
223        if args.add_do_not_merge:
224            message_lines[0] = '[DO NOT MERGE] ' + message_lines[0]
225
226        if args.bug:
227            message_lines.append('')
228            message_lines.append('Bug: http://b/{}'.format(args.bug))
229        message_lines.append('Test: Build (TH)')
230        message = '\n'.join(message_lines)
231
232        # Invoke git: Delete current profile, add new profile and commit these
233        # changes.
234        if current_profile:
235            utils.check_call(['git', 'rm', current_profile])
236        utils.check_call(['git', 'add', profdata])
237        utils.check_call(['git', 'commit', '-m', message])
238
239        if not args.skip_cleanup:
240            shutil.rmtree(extract_dir)
241
242
243if __name__ == '__main__':
244    main()
245