Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | # Copyright (C) 2017 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 | from __future__ import absolute_import |
| 18 | from __future__ import division |
| 19 | from __future__ import print_function |
| 20 | |
| 21 | import argparse |
| 22 | import atexit |
| 23 | import hashlib |
| 24 | import os |
| 25 | import signal |
| 26 | import subprocess |
| 27 | import sys |
| 28 | import tempfile |
| 29 | import time |
| 30 | import urllib |
| 31 | |
| 32 | TRACE_TO_TEXT_SHAS = { |
| 33 | 'linux': 'c704cf765b9c5445c16a8e7ec5757cad364a8d92', |
| 34 | 'mac': 'aed4ad02da526a3f1e4f9df47d4989ae9305b30e', |
| 35 | } |
| 36 | TRACE_TO_TEXT_PATH = tempfile.gettempdir() |
| 37 | TRACE_TO_TEXT_BASE_URL = ( |
| 38 | 'https://storage.googleapis.com/perfetto/') |
| 39 | |
| 40 | def check_hash(file_name, sha_value): |
| 41 | with open(file_name, 'rb') as fd: |
| 42 | # TODO(fmayer): Chunking. |
| 43 | file_hash = hashlib.sha1(fd.read()).hexdigest() |
| 44 | return file_hash == sha_value |
| 45 | |
| 46 | |
| 47 | def load_trace_to_text(platform): |
| 48 | sha_value = TRACE_TO_TEXT_SHAS[platform] |
| 49 | file_name = 'trace_to_text-' + platform + '-' + sha_value |
| 50 | local_file = os.path.join(TRACE_TO_TEXT_PATH, file_name) |
| 51 | |
| 52 | if os.path.exists(local_file): |
| 53 | if not check_hash(local_file, sha_value): |
| 54 | os.remove(local_file) |
| 55 | else: |
| 56 | return local_file |
| 57 | |
| 58 | url = TRACE_TO_TEXT_BASE_URL + file_name |
| 59 | urllib.urlretrieve(url, local_file) |
| 60 | if not check_hash(local_file, sha_value): |
| 61 | os.remove(local_file) |
| 62 | raise ValueError("Invalid signature.") |
| 63 | os.chmod(local_file, 0o755) |
| 64 | return local_file |
| 65 | |
| 66 | |
| 67 | NULL = open('/dev/null', 'r') |
| 68 | |
| 69 | CFG_IDENT = ' ' |
| 70 | CFG='''buffers {{ |
| 71 | size_kb: 32768 |
| 72 | }} |
| 73 | |
| 74 | data_sources {{ |
| 75 | config {{ |
| 76 | name: "android.heapprofd" |
| 77 | heapprofd_config {{ |
| 78 | |
| 79 | all: {all} |
| 80 | sampling_interval_bytes: {interval} |
| 81 | {target_cfg} |
| 82 | continuous_dump_config {{ |
| 83 | dump_phase_ms: 0 |
| 84 | dump_interval_ms: 1000 |
| 85 | }} |
| 86 | }} |
| 87 | }} |
| 88 | }} |
| 89 | |
| 90 | duration_ms: {duration} |
| 91 | ''' |
| 92 | |
| 93 | PERFETTO_CMD=('CFG=\'{}\'; echo ${{CFG}} | ' |
Florian Mayer | eed8974 | 2018-12-05 10:56:22 +0000 | [diff] [blame] | 94 | 'perfetto --txt -c - -o /data/misc/perfetto-traces/profile -d') |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 95 | IS_INTERRUPTED = False |
| 96 | def sigint_handler(sig, frame): |
| 97 | global IS_INTERRUPTED |
| 98 | IS_INTERRUPTED = True |
| 99 | |
| 100 | |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 101 | def main(argv): |
| 102 | parser = argparse.ArgumentParser() |
| 103 | parser.add_argument("-i", "--interval", help="Sampling interval. " |
| 104 | "Default 128000 (128kB)", type=int, default=128000) |
| 105 | parser.add_argument("-d", "--duration", help="Duration of profile (ms). " |
| 106 | "Default 1 minute", type=int, default=60000) |
| 107 | parser.add_argument("-a", "--all", help="Profile the whole system", |
| 108 | action='store_true') |
Florian Mayer | 33ceb8d | 2019-01-11 14:51:28 +0000 | [diff] [blame] | 109 | parser.add_argument("--no-start", help="Do not start heapprofd", |
| 110 | action='store_true') |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 111 | parser.add_argument("-p", "--pid", help="PIDs to profile", nargs='+', |
| 112 | type=int) |
| 113 | parser.add_argument("-n", "--name", help="Process names to profile", |
| 114 | nargs='+') |
| 115 | parser.add_argument("-t", "--trace-to-text-binary", |
| 116 | help="Path to local trace to text. For debugging.") |
Florian Mayer | 6ae9526 | 2018-12-06 16:10:29 +0000 | [diff] [blame] | 117 | parser.add_argument("--disable-selinux", action="store_true", |
| 118 | help="Disable SELinux enforcement for duration of " |
| 119 | "profile") |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 120 | |
| 121 | args = parser.parse_args() |
| 122 | |
| 123 | fail = False |
| 124 | if args.all is None and args.pid is None and args.name is None: |
| 125 | print("FATAL: Neither --all nor PID nor NAME given.", file=sys.stderr) |
| 126 | fail = True |
| 127 | if args.duration is None: |
| 128 | print("FATAL: No duration given.", file=sys.stderr) |
| 129 | fail = True |
| 130 | if args.interval is None: |
| 131 | print("FATAL: No interval given.", file=sys.stderr) |
| 132 | fail = True |
| 133 | if fail: |
| 134 | parser.print_help() |
| 135 | return 1 |
| 136 | target_cfg = "" |
| 137 | if args.pid: |
| 138 | for pid in args.pid: |
| 139 | target_cfg += '{}pid: {}\n'.format(CFG_IDENT, pid) |
| 140 | if args.name: |
| 141 | for name in args.name: |
| 142 | target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_IDENT, name) |
| 143 | |
| 144 | trace_to_text_binary = args.trace_to_text_binary |
| 145 | if trace_to_text_binary is None: |
| 146 | platform = None |
| 147 | if sys.platform.startswith('linux'): |
| 148 | platform = 'linux' |
| 149 | elif sys.platform.startswith('darwin'): |
| 150 | platform = 'mac' |
| 151 | else: |
| 152 | print("Invalid platform: {}".format(sys.platform), file=sys.stderr) |
Florian Mayer | b627963 | 2018-11-29 13:31:49 +0000 | [diff] [blame] | 153 | return 1 |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 154 | |
| 155 | trace_to_text_binary = load_trace_to_text(platform) |
| 156 | |
| 157 | cfg = CFG.format(all=str(args.all == True).lower(), interval=args.interval, |
| 158 | duration=args.duration, target_cfg=target_cfg) |
| 159 | |
Florian Mayer | 6ae9526 | 2018-12-06 16:10:29 +0000 | [diff] [blame] | 160 | if args.disable_selinux: |
| 161 | enforcing = subprocess.check_output(['adb', 'shell', 'getenforce']) |
| 162 | atexit.register(subprocess.check_call, |
| 163 | ['adb', 'shell', 'su root setenforce %s' % enforcing]) |
| 164 | subprocess.check_call(['adb', 'shell', 'su root setenforce 0']) |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 165 | |
Florian Mayer | 33ceb8d | 2019-01-11 14:51:28 +0000 | [diff] [blame] | 166 | if not args.no_start: |
| 167 | atexit.register(subprocess.check_call, |
| 168 | ['adb', 'shell', 'su root stop heapprofd']) |
| 169 | subprocess.check_call(['adb', 'shell', 'su root start heapprofd']) |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 170 | |
| 171 | perfetto_pid = subprocess.check_output( |
| 172 | ['adb', 'exec-out', PERFETTO_CMD.format(cfg)]).strip() |
| 173 | |
| 174 | old_handler = signal.signal(signal.SIGINT, sigint_handler) |
| 175 | print("Profiling active. Press Ctrl+C to terminate.") |
| 176 | exists = True |
| 177 | while exists and not IS_INTERRUPTED: |
| 178 | exists = subprocess.call( |
| 179 | ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 |
| 180 | time.sleep(1) |
| 181 | signal.signal(signal.SIGINT, old_handler) |
| 182 | if IS_INTERRUPTED: |
| 183 | # Not check_call because it could have existed in the meantime. |
Florian Mayer | 6ae9526 | 2018-12-06 16:10:29 +0000 | [diff] [blame] | 184 | subprocess.call(['adb', 'shell', 'su', 'root', 'kill', '-INT', |
| 185 | perfetto_pid]) |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 186 | |
Florian Mayer | ddbe31e | 2018-11-30 14:49:30 +0000 | [diff] [blame] | 187 | # Wait for perfetto cmd to return. |
| 188 | while exists: |
| 189 | exists = subprocess.call( |
| 190 | ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 |
| 191 | time.sleep(1) |
| 192 | |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 193 | subprocess.check_call(['adb', 'pull', '/data/misc/perfetto-traces/profile', |
| 194 | '/tmp/profile'], stdout=NULL) |
| 195 | trace_to_text_output = subprocess.check_output( |
| 196 | [trace_to_text_binary, 'profile', '/tmp/profile'], |
| 197 | stderr=NULL) |
| 198 | profile_path = None |
| 199 | for word in trace_to_text_output.split(): |
| 200 | if 'heap_profile-' in word: |
| 201 | profile_path = word |
| 202 | if profile_path is None: |
| 203 | print("Could not find trace_to_text output path.", file=sys.stderr) |
| 204 | return 1 |
| 205 | |
| 206 | profile_files = os.listdir(profile_path) |
| 207 | if not profile_files: |
| 208 | print("No profiles generated", file=sys.stderr) |
| 209 | return 1 |
| 210 | |
| 211 | subprocess.check_call(['gzip'] + [os.path.join(profile_path, x) for x in |
| 212 | os.listdir(profile_path)]) |
Florian Mayer | 82f43d1 | 2019-01-17 14:37:45 +0000 | [diff] [blame^] | 213 | |
| 214 | symlink_path = os.path.join(os.path.dirname(profile_path), |
| 215 | "heap_profile-latest") |
| 216 | os.unlink(symlink_path) |
| 217 | os.symlink(profile_path, symlink_path) |
| 218 | |
| 219 | print("Wrote profiles to {} (symlink {})".format(profile_path, symlink_path)) |
Florian Mayer | 801349e | 2018-11-29 10:15:25 +0000 | [diff] [blame] | 220 | print("These can be viewed using pprof. Googlers: head to pprof/ and " |
| 221 | "upload them.") |
| 222 | |
| 223 | |
| 224 | if __name__ == '__main__': |
| 225 | sys.exit(main(sys.argv)) |