xref: /aosp_15_r20/development/scripts/acov-llvm.py (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1*90c8c64dSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*90c8c64dSAndroid Build Coastguard Worker#
3*90c8c64dSAndroid Build Coastguard Worker# Copyright (C) 2021 The Android Open Source Project
4*90c8c64dSAndroid Build Coastguard Worker#
5*90c8c64dSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*90c8c64dSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*90c8c64dSAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*90c8c64dSAndroid Build Coastguard Worker#
9*90c8c64dSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*90c8c64dSAndroid Build Coastguard Worker#
11*90c8c64dSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*90c8c64dSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*90c8c64dSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*90c8c64dSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*90c8c64dSAndroid Build Coastguard Worker# limitations under the License.
16*90c8c64dSAndroid Build Coastguard Worker
17*90c8c64dSAndroid Build Coastguard Worker# acov-llvm.py is a tool for gathering coverage information from a device and
18*90c8c64dSAndroid Build Coastguard Worker# generating an LLVM coverage report from that information. To use:
19*90c8c64dSAndroid Build Coastguard Worker#
20*90c8c64dSAndroid Build Coastguard Worker# This script would work only when the device image was built with the following
21*90c8c64dSAndroid Build Coastguard Worker# build variables:
22*90c8c64dSAndroid Build Coastguard Worker#     CLANG_COVERAGE=true NATIVE_COVERAGE_PATHS="<list-of-paths>"
23*90c8c64dSAndroid Build Coastguard Worker#
24*90c8c64dSAndroid Build Coastguard Worker# 1. [optional] Reset coverage information on the device
25*90c8c64dSAndroid Build Coastguard Worker#   $ acov-llvm.py clean-device
26*90c8c64dSAndroid Build Coastguard Worker#
27*90c8c64dSAndroid Build Coastguard Worker# 2. Run tests
28*90c8c64dSAndroid Build Coastguard Worker#
29*90c8c64dSAndroid Build Coastguard Worker# 3. Flush coverage
30*90c8c64dSAndroid Build Coastguard Worker# from select daemons and system processes on the device
31*90c8c64dSAndroid Build Coastguard Worker#   $ acov-llvm.py flush [list of process names]
32*90c8c64dSAndroid Build Coastguard Worker#   $ acov-llvm.py flush -p [list of process pids]
33*90c8c64dSAndroid Build Coastguard Worker# or from all processes on the device:
34*90c8c64dSAndroid Build Coastguard Worker#   $ acov-llvm.py flush
35*90c8c64dSAndroid Build Coastguard Worker#
36*90c8c64dSAndroid Build Coastguard Worker# 4. Pull coverage from device and generate coverage report
37*90c8c64dSAndroid Build Coastguard Worker#   $ acov-llvm.py report -s <one-or-more-source-paths-in-$ANDROID_BUILD_TOP> \
38*90c8c64dSAndroid Build Coastguard Worker#                         -b <one-or-more-binaries-in-$OUT> \
39*90c8c64dSAndroid Build Coastguard Worker# E.g.:
40*90c8c64dSAndroid Build Coastguard Worker# acov-llvm.py report \
41*90c8c64dSAndroid Build Coastguard Worker#         -s bionic \
42*90c8c64dSAndroid Build Coastguard Worker#         -b \
43*90c8c64dSAndroid Build Coastguard Worker#         $OUT/symbols/apex/com.android.runtime/lib/bionic/libc.so \
44*90c8c64dSAndroid Build Coastguard Worker#         $OUT/symbols/apex/com.android.runtime/lib/bionic/libm.so
45*90c8c64dSAndroid Build Coastguard Worker
46*90c8c64dSAndroid Build Coastguard Workerimport argparse
47*90c8c64dSAndroid Build Coastguard Workerimport logging
48*90c8c64dSAndroid Build Coastguard Workerimport os
49*90c8c64dSAndroid Build Coastguard Workerimport re
50*90c8c64dSAndroid Build Coastguard Workerimport subprocess
51*90c8c64dSAndroid Build Coastguard Workerimport time
52*90c8c64dSAndroid Build Coastguard Workerimport tempfile
53*90c8c64dSAndroid Build Coastguard Worker
54*90c8c64dSAndroid Build Coastguard Workerfrom pathlib import Path
55*90c8c64dSAndroid Build Coastguard Worker
56*90c8c64dSAndroid Build Coastguard WorkerFLUSH_SLEEP = 60
57*90c8c64dSAndroid Build Coastguard Worker
58*90c8c64dSAndroid Build Coastguard Worker
59*90c8c64dSAndroid Build Coastguard Workerdef android_build_top():
60*90c8c64dSAndroid Build Coastguard Worker    return Path(os.environ.get('ANDROID_BUILD_TOP', None))
61*90c8c64dSAndroid Build Coastguard Worker
62*90c8c64dSAndroid Build Coastguard Worker
63*90c8c64dSAndroid Build Coastguard Workerdef _get_clang_revision():
64*90c8c64dSAndroid Build Coastguard Worker    version_output = subprocess.check_output(
65*90c8c64dSAndroid Build Coastguard Worker        android_build_top() / 'build/soong/scripts/get_clang_version.py',
66*90c8c64dSAndroid Build Coastguard Worker        text=True)
67*90c8c64dSAndroid Build Coastguard Worker    return version_output.strip()
68*90c8c64dSAndroid Build Coastguard Worker
69*90c8c64dSAndroid Build Coastguard Worker
70*90c8c64dSAndroid Build Coastguard WorkerCLANG_TOP = android_build_top() / 'prebuilts/clang/host/linux-x86/' \
71*90c8c64dSAndroid Build Coastguard Worker        / _get_clang_revision()
72*90c8c64dSAndroid Build Coastguard WorkerLLVM_PROFDATA_PATH = CLANG_TOP / 'bin' / 'llvm-profdata'
73*90c8c64dSAndroid Build Coastguard WorkerLLVM_COV_PATH = CLANG_TOP / 'bin' / 'llvm-cov'
74*90c8c64dSAndroid Build Coastguard Worker
75*90c8c64dSAndroid Build Coastguard Worker
76*90c8c64dSAndroid Build Coastguard Workerdef check_output(cmd, *args, **kwargs):
77*90c8c64dSAndroid Build Coastguard Worker    """subprocess.check_output with logging."""
78*90c8c64dSAndroid Build Coastguard Worker    cmd_str = cmd if isinstance(cmd, str) else ' '.join(cmd)
79*90c8c64dSAndroid Build Coastguard Worker    logging.debug(cmd_str)
80*90c8c64dSAndroid Build Coastguard Worker    return subprocess.run(
81*90c8c64dSAndroid Build Coastguard Worker        cmd, *args, **kwargs, check=True, stdout=subprocess.PIPE).stdout
82*90c8c64dSAndroid Build Coastguard Worker
83*90c8c64dSAndroid Build Coastguard Worker
84*90c8c64dSAndroid Build Coastguard Workerdef adb(cmd, *args, **kwargs):
85*90c8c64dSAndroid Build Coastguard Worker    """call 'adb <cmd>' with logging."""
86*90c8c64dSAndroid Build Coastguard Worker    return check_output(['adb'] + cmd, *args, **kwargs)
87*90c8c64dSAndroid Build Coastguard Worker
88*90c8c64dSAndroid Build Coastguard Worker
89*90c8c64dSAndroid Build Coastguard Workerdef adb_root(*args, **kwargs):
90*90c8c64dSAndroid Build Coastguard Worker    """call 'adb root' with logging."""
91*90c8c64dSAndroid Build Coastguard Worker    return adb(['root'], *args, **kwargs)
92*90c8c64dSAndroid Build Coastguard Worker
93*90c8c64dSAndroid Build Coastguard Worker
94*90c8c64dSAndroid Build Coastguard Workerdef adb_shell(cmd, *args, **kwargs):
95*90c8c64dSAndroid Build Coastguard Worker    """call 'adb shell <cmd>' with logging."""
96*90c8c64dSAndroid Build Coastguard Worker    return adb(['shell'] + cmd, *args, **kwargs)
97*90c8c64dSAndroid Build Coastguard Worker
98*90c8c64dSAndroid Build Coastguard Worker
99*90c8c64dSAndroid Build Coastguard Workerdef send_flush_signal(pids=None):
100*90c8c64dSAndroid Build Coastguard Worker
101*90c8c64dSAndroid Build Coastguard Worker    def _has_handler_sig37(pid):
102*90c8c64dSAndroid Build Coastguard Worker        try:
103*90c8c64dSAndroid Build Coastguard Worker            status = adb_shell(['cat', f'/proc/{pid}/status'],
104*90c8c64dSAndroid Build Coastguard Worker                               text=True,
105*90c8c64dSAndroid Build Coastguard Worker                               stderr=subprocess.DEVNULL)
106*90c8c64dSAndroid Build Coastguard Worker        except subprocess.CalledProcessError:
107*90c8c64dSAndroid Build Coastguard Worker            logging.warning(f'Process {pid} is no longer active')
108*90c8c64dSAndroid Build Coastguard Worker            return False
109*90c8c64dSAndroid Build Coastguard Worker
110*90c8c64dSAndroid Build Coastguard Worker        status = status.split('\n')
111*90c8c64dSAndroid Build Coastguard Worker        sigcgt = [
112*90c8c64dSAndroid Build Coastguard Worker            line.split(':\t')[1] for line in status if line.startswith('SigCgt')
113*90c8c64dSAndroid Build Coastguard Worker        ]
114*90c8c64dSAndroid Build Coastguard Worker        if not sigcgt:
115*90c8c64dSAndroid Build Coastguard Worker            logging.warning(f'Cannot find \'SigCgt:\' in /proc/{pid}/status')
116*90c8c64dSAndroid Build Coastguard Worker            return False
117*90c8c64dSAndroid Build Coastguard Worker        return int(sigcgt[0], base=16) & (1 << 36)
118*90c8c64dSAndroid Build Coastguard Worker
119*90c8c64dSAndroid Build Coastguard Worker    if not pids:
120*90c8c64dSAndroid Build Coastguard Worker        output = adb_shell(['ps', '-eo', 'pid'], text=True)
121*90c8c64dSAndroid Build Coastguard Worker        pids = [pid.strip() for pid in output.split()]
122*90c8c64dSAndroid Build Coastguard Worker        pids = pids[1:]  # ignore the column header
123*90c8c64dSAndroid Build Coastguard Worker    pids = [pid for pid in pids if _has_handler_sig37(pid)]
124*90c8c64dSAndroid Build Coastguard Worker
125*90c8c64dSAndroid Build Coastguard Worker    if not pids:
126*90c8c64dSAndroid Build Coastguard Worker        logging.warning(
127*90c8c64dSAndroid Build Coastguard Worker            f'couldn\'t find any process with handler for signal 37')
128*90c8c64dSAndroid Build Coastguard Worker
129*90c8c64dSAndroid Build Coastguard Worker    # Some processes may have exited after we run `ps` command above - ignore failures when
130*90c8c64dSAndroid Build Coastguard Worker    # sending flush signal.
131*90c8c64dSAndroid Build Coastguard Worker    # We rely on kill(1) sending the signal to all pids on the command line even if some don't
132*90c8c64dSAndroid Build Coastguard Worker    # exist.  This is true of toybox and "probably implied" by POSIX, even if not explicitly called
133*90c8c64dSAndroid Build Coastguard Worker    # out [https://pubs.opengroup.org/onlinepubs/9699919799/utilities/kill.html].
134*90c8c64dSAndroid Build Coastguard Worker    try:
135*90c8c64dSAndroid Build Coastguard Worker        adb_shell(['kill', '-37'] + pids)
136*90c8c64dSAndroid Build Coastguard Worker    except subprocess.CalledProcessError:
137*90c8c64dSAndroid Build Coastguard Worker        logging.warning('Sending flush signal failed - some pids no longer active')
138*90c8c64dSAndroid Build Coastguard Worker
139*90c8c64dSAndroid Build Coastguard Worker
140*90c8c64dSAndroid Build Coastguard Workerdef do_clean_device(args):
141*90c8c64dSAndroid Build Coastguard Worker    adb_root()
142*90c8c64dSAndroid Build Coastguard Worker
143*90c8c64dSAndroid Build Coastguard Worker    logging.info('resetting coverage on device')
144*90c8c64dSAndroid Build Coastguard Worker    send_flush_signal()
145*90c8c64dSAndroid Build Coastguard Worker
146*90c8c64dSAndroid Build Coastguard Worker    logging.info(
147*90c8c64dSAndroid Build Coastguard Worker        f'sleeping for {FLUSH_SLEEP} seconds for coverage to be written')
148*90c8c64dSAndroid Build Coastguard Worker    time.sleep(FLUSH_SLEEP)
149*90c8c64dSAndroid Build Coastguard Worker
150*90c8c64dSAndroid Build Coastguard Worker    logging.info('deleting coverage data from device')
151*90c8c64dSAndroid Build Coastguard Worker    adb_shell(['rm', '-rf', '/data/misc/trace/*.profraw'])
152*90c8c64dSAndroid Build Coastguard Worker
153*90c8c64dSAndroid Build Coastguard Worker
154*90c8c64dSAndroid Build Coastguard Workerdef do_flush(args):
155*90c8c64dSAndroid Build Coastguard Worker    adb_root()
156*90c8c64dSAndroid Build Coastguard Worker
157*90c8c64dSAndroid Build Coastguard Worker    if args.procnames:
158*90c8c64dSAndroid Build Coastguard Worker        pids = adb_shell(['pidof'] + args.procnames, text=True).split()
159*90c8c64dSAndroid Build Coastguard Worker        logging.info(f'flushing coverage for pids: {pids}')
160*90c8c64dSAndroid Build Coastguard Worker    elif args.pids:
161*90c8c64dSAndroid Build Coastguard Worker        pids = args.pids
162*90c8c64dSAndroid Build Coastguard Worker        logging.info(f'flushing coverage for pids: {pids}')
163*90c8c64dSAndroid Build Coastguard Worker    else:
164*90c8c64dSAndroid Build Coastguard Worker        pids = None
165*90c8c64dSAndroid Build Coastguard Worker        logging.info('flushing coverage for all processes on device')
166*90c8c64dSAndroid Build Coastguard Worker
167*90c8c64dSAndroid Build Coastguard Worker    send_flush_signal(pids)
168*90c8c64dSAndroid Build Coastguard Worker
169*90c8c64dSAndroid Build Coastguard Worker    logging.info(
170*90c8c64dSAndroid Build Coastguard Worker        f'sleeping for {FLUSH_SLEEP} seconds for coverage to be written')
171*90c8c64dSAndroid Build Coastguard Worker    time.sleep(FLUSH_SLEEP)
172*90c8c64dSAndroid Build Coastguard Worker
173*90c8c64dSAndroid Build Coastguard Worker
174*90c8c64dSAndroid Build Coastguard Workerdef do_report(args):
175*90c8c64dSAndroid Build Coastguard Worker    adb_root()
176*90c8c64dSAndroid Build Coastguard Worker
177*90c8c64dSAndroid Build Coastguard Worker    temp_dir = tempfile.mkdtemp(
178*90c8c64dSAndroid Build Coastguard Worker        prefix='covreport-', dir=os.environ.get('ANDROID_BUILD_TOP', None))
179*90c8c64dSAndroid Build Coastguard Worker    logging.info(f'generating coverage report in {temp_dir}')
180*90c8c64dSAndroid Build Coastguard Worker
181*90c8c64dSAndroid Build Coastguard Worker    # Pull coverage files from /data/misc/trace on the device
182*90c8c64dSAndroid Build Coastguard Worker    compressed = adb_shell(['tar', '-czf', '-', '-C', '/data/misc', 'trace'])
183*90c8c64dSAndroid Build Coastguard Worker    check_output(['tar', 'zxvf', '-', '-C', temp_dir], input=compressed)
184*90c8c64dSAndroid Build Coastguard Worker
185*90c8c64dSAndroid Build Coastguard Worker    # Call llvm-profdata followed by llvm-cov
186*90c8c64dSAndroid Build Coastguard Worker    profdata = f'{temp_dir}/merged.profdata'
187*90c8c64dSAndroid Build Coastguard Worker    check_output(
188*90c8c64dSAndroid Build Coastguard Worker        f'{LLVM_PROFDATA_PATH} merge --failure-mode=all --output={profdata} {temp_dir}/trace/*.profraw',
189*90c8c64dSAndroid Build Coastguard Worker        shell=True)
190*90c8c64dSAndroid Build Coastguard Worker
191*90c8c64dSAndroid Build Coastguard Worker    object_flags = [args.binary[0]] + ['--object=' + b for b in args.binary[1:]]
192*90c8c64dSAndroid Build Coastguard Worker    source_dirs = ['/proc/self/cwd/' + s for s in args.source_dir]
193*90c8c64dSAndroid Build Coastguard Worker
194*90c8c64dSAndroid Build Coastguard Worker    output_dir = f'{temp_dir}/html'
195*90c8c64dSAndroid Build Coastguard Worker
196*90c8c64dSAndroid Build Coastguard Worker    check_output([
197*90c8c64dSAndroid Build Coastguard Worker        str(LLVM_COV_PATH), 'show', f'--instr-profile={profdata}',
198*90c8c64dSAndroid Build Coastguard Worker        '--format=html', f'--output-dir={output_dir}',
199*90c8c64dSAndroid Build Coastguard Worker        '--show-region-summary=false'
200*90c8c64dSAndroid Build Coastguard Worker    ] + object_flags + source_dirs)
201*90c8c64dSAndroid Build Coastguard Worker
202*90c8c64dSAndroid Build Coastguard Worker    check_output(['chmod', '+rx', temp_dir])
203*90c8c64dSAndroid Build Coastguard Worker
204*90c8c64dSAndroid Build Coastguard Worker    print(f'Coverage report data written in {output_dir}')
205*90c8c64dSAndroid Build Coastguard Worker
206*90c8c64dSAndroid Build Coastguard Worker
207*90c8c64dSAndroid Build Coastguard Workerdef parse_args():
208*90c8c64dSAndroid Build Coastguard Worker    parser = argparse.ArgumentParser()
209*90c8c64dSAndroid Build Coastguard Worker    parser.add_argument(
210*90c8c64dSAndroid Build Coastguard Worker        '-v',
211*90c8c64dSAndroid Build Coastguard Worker        '--verbose',
212*90c8c64dSAndroid Build Coastguard Worker        action='store_true',
213*90c8c64dSAndroid Build Coastguard Worker        default=False,
214*90c8c64dSAndroid Build Coastguard Worker        help='enable debug logging')
215*90c8c64dSAndroid Build Coastguard Worker
216*90c8c64dSAndroid Build Coastguard Worker    subparsers = parser.add_subparsers(dest='command', required=True)
217*90c8c64dSAndroid Build Coastguard Worker
218*90c8c64dSAndroid Build Coastguard Worker    clean_device = subparsers.add_parser(
219*90c8c64dSAndroid Build Coastguard Worker        'clean-device', help='reset coverage on device')
220*90c8c64dSAndroid Build Coastguard Worker    clean_device.set_defaults(func=do_clean_device)
221*90c8c64dSAndroid Build Coastguard Worker
222*90c8c64dSAndroid Build Coastguard Worker    flush = subparsers.add_parser(
223*90c8c64dSAndroid Build Coastguard Worker        'flush', help='flush coverage for processes on device')
224*90c8c64dSAndroid Build Coastguard Worker    flush.add_argument(
225*90c8c64dSAndroid Build Coastguard Worker        'procnames',
226*90c8c64dSAndroid Build Coastguard Worker        nargs='*',
227*90c8c64dSAndroid Build Coastguard Worker        metavar='PROCNAME',
228*90c8c64dSAndroid Build Coastguard Worker        help='flush coverage for one or more processes with name PROCNAME')
229*90c8c64dSAndroid Build Coastguard Worker    flush.add_argument(
230*90c8c64dSAndroid Build Coastguard Worker        '-p',
231*90c8c64dSAndroid Build Coastguard Worker        '--pids',
232*90c8c64dSAndroid Build Coastguard Worker        nargs='+',
233*90c8c64dSAndroid Build Coastguard Worker        metavar='PROCID',
234*90c8c64dSAndroid Build Coastguard Worker        required=False,
235*90c8c64dSAndroid Build Coastguard Worker        help='flush coverage for one or more processes with name PROCID')
236*90c8c64dSAndroid Build Coastguard Worker    flush.set_defaults(func=do_flush)
237*90c8c64dSAndroid Build Coastguard Worker
238*90c8c64dSAndroid Build Coastguard Worker    report = subparsers.add_parser(
239*90c8c64dSAndroid Build Coastguard Worker        'report', help='fetch coverage from device and generate report')
240*90c8c64dSAndroid Build Coastguard Worker    report.add_argument(
241*90c8c64dSAndroid Build Coastguard Worker        '-b',
242*90c8c64dSAndroid Build Coastguard Worker        '--binary',
243*90c8c64dSAndroid Build Coastguard Worker        nargs='+',
244*90c8c64dSAndroid Build Coastguard Worker        metavar='BINARY',
245*90c8c64dSAndroid Build Coastguard Worker        action='extend',
246*90c8c64dSAndroid Build Coastguard Worker        required=True,
247*90c8c64dSAndroid Build Coastguard Worker        help='generate coverage report for BINARY')
248*90c8c64dSAndroid Build Coastguard Worker    report.add_argument(
249*90c8c64dSAndroid Build Coastguard Worker        '-s',
250*90c8c64dSAndroid Build Coastguard Worker        '--source-dir',
251*90c8c64dSAndroid Build Coastguard Worker        nargs='+',
252*90c8c64dSAndroid Build Coastguard Worker        action='extend',
253*90c8c64dSAndroid Build Coastguard Worker        metavar='PATH',
254*90c8c64dSAndroid Build Coastguard Worker        required=True,
255*90c8c64dSAndroid Build Coastguard Worker        help='generate coverage report for source files in PATH')
256*90c8c64dSAndroid Build Coastguard Worker    report.set_defaults(func=do_report)
257*90c8c64dSAndroid Build Coastguard Worker    return parser.parse_args()
258*90c8c64dSAndroid Build Coastguard Worker
259*90c8c64dSAndroid Build Coastguard Worker
260*90c8c64dSAndroid Build Coastguard Workerdef main():
261*90c8c64dSAndroid Build Coastguard Worker    args = parse_args()
262*90c8c64dSAndroid Build Coastguard Worker    if args.verbose:
263*90c8c64dSAndroid Build Coastguard Worker        logging.basicConfig(level=logging.DEBUG)
264*90c8c64dSAndroid Build Coastguard Worker
265*90c8c64dSAndroid Build Coastguard Worker    args.func(args)
266*90c8c64dSAndroid Build Coastguard Worker
267*90c8c64dSAndroid Build Coastguard Worker
268*90c8c64dSAndroid Build Coastguard Workerif __name__ == '__main__':
269*90c8c64dSAndroid Build Coastguard Worker    main()
270