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}} | ' |
| 94 | 'perfetto -t -c - -o /data/misc/perfetto-traces/profile -b') |
| 95 | IS_INTERRUPTED = False |
| 96 | def sigint_handler(sig, frame): |
| 97 | global IS_INTERRUPTED |
| 98 | IS_INTERRUPTED = True |
| 99 | |
| 100 | |
| 101 | def on_exit(enforcing): |
| 102 | subprocess.check_output(['adb', 'shell', 'su root stop heapprofd']) |
| 103 | subprocess.check_output( |
| 104 | ['adb', 'shell', 'su root setenforce %s' % enforcing]) |
| 105 | |
| 106 | |
| 107 | def main(argv): |
| 108 | parser = argparse.ArgumentParser() |
| 109 | parser.add_argument("-i", "--interval", help="Sampling interval. " |
| 110 | "Default 128000 (128kB)", type=int, default=128000) |
| 111 | parser.add_argument("-d", "--duration", help="Duration of profile (ms). " |
| 112 | "Default 1 minute", type=int, default=60000) |
| 113 | parser.add_argument("-a", "--all", help="Profile the whole system", |
| 114 | action='store_true') |
| 115 | parser.add_argument("-p", "--pid", help="PIDs to profile", nargs='+', |
| 116 | type=int) |
| 117 | parser.add_argument("-n", "--name", help="Process names to profile", |
| 118 | nargs='+') |
| 119 | parser.add_argument("-t", "--trace-to-text-binary", |
| 120 | help="Path to local trace to text. For debugging.") |
| 121 | |
| 122 | args = parser.parse_args() |
| 123 | |
| 124 | fail = False |
| 125 | if args.all is None and args.pid is None and args.name is None: |
| 126 | print("FATAL: Neither --all nor PID nor NAME given.", file=sys.stderr) |
| 127 | fail = True |
| 128 | if args.duration is None: |
| 129 | print("FATAL: No duration given.", file=sys.stderr) |
| 130 | fail = True |
| 131 | if args.interval is None: |
| 132 | print("FATAL: No interval given.", file=sys.stderr) |
| 133 | fail = True |
| 134 | if fail: |
| 135 | parser.print_help() |
| 136 | return 1 |
| 137 | target_cfg = "" |
| 138 | if args.pid: |
| 139 | for pid in args.pid: |
| 140 | target_cfg += '{}pid: {}\n'.format(CFG_IDENT, pid) |
| 141 | if args.name: |
| 142 | for name in args.name: |
| 143 | target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_IDENT, name) |
| 144 | |
| 145 | trace_to_text_binary = args.trace_to_text_binary |
| 146 | if trace_to_text_binary is None: |
| 147 | platform = None |
| 148 | if sys.platform.startswith('linux'): |
| 149 | platform = 'linux' |
| 150 | elif sys.platform.startswith('darwin'): |
| 151 | platform = 'mac' |
| 152 | else: |
| 153 | print("Invalid platform: {}".format(sys.platform), file=sys.stderr) |
| 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 | |
| 160 | enforcing = subprocess.check_output(['adb', 'shell', 'getenforce']) |
| 161 | atexit.register(on_exit, enforcing) |
| 162 | |
| 163 | subprocess.check_call(['adb', 'shell', 'su root setenforce 0']) |
| 164 | subprocess.check_call(['adb', 'shell', 'su root start heapprofd']) |
| 165 | |
| 166 | perfetto_pid = subprocess.check_output( |
| 167 | ['adb', 'exec-out', PERFETTO_CMD.format(cfg)]).strip() |
| 168 | |
| 169 | old_handler = signal.signal(signal.SIGINT, sigint_handler) |
| 170 | print("Profiling active. Press Ctrl+C to terminate.") |
| 171 | exists = True |
| 172 | while exists and not IS_INTERRUPTED: |
| 173 | exists = subprocess.call( |
| 174 | ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0 |
| 175 | time.sleep(1) |
| 176 | signal.signal(signal.SIGINT, old_handler) |
| 177 | if IS_INTERRUPTED: |
| 178 | # Not check_call because it could have existed in the meantime. |
| 179 | subprocess.call(['adb', 'shell', 'kill', '-INT', perfetto_pid]) |
| 180 | |
| 181 | subprocess.check_call(['adb', 'pull', '/data/misc/perfetto-traces/profile', |
| 182 | '/tmp/profile'], stdout=NULL) |
| 183 | trace_to_text_output = subprocess.check_output( |
| 184 | [trace_to_text_binary, 'profile', '/tmp/profile'], |
| 185 | stderr=NULL) |
| 186 | profile_path = None |
| 187 | for word in trace_to_text_output.split(): |
| 188 | if 'heap_profile-' in word: |
| 189 | profile_path = word |
| 190 | if profile_path is None: |
| 191 | print("Could not find trace_to_text output path.", file=sys.stderr) |
| 192 | return 1 |
| 193 | |
| 194 | profile_files = os.listdir(profile_path) |
| 195 | if not profile_files: |
| 196 | print("No profiles generated", file=sys.stderr) |
| 197 | return 1 |
| 198 | |
| 199 | subprocess.check_call(['gzip'] + [os.path.join(profile_path, x) for x in |
| 200 | os.listdir(profile_path)]) |
| 201 | print("Wrote profiles to {}".format(profile_path)) |
| 202 | print("These can be viewed using pprof. Googlers: head to pprof/ and " |
| 203 | "upload them.") |
| 204 | |
| 205 | |
| 206 | if __name__ == '__main__': |
| 207 | sys.exit(main(sys.argv)) |