xref: /aosp_15_r20/external/perfetto/tools/java_heap_dump (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1#!/usr/bin/env python3
2
3# Copyright (C) 2020 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
17from __future__ import absolute_import
18from __future__ import division
19from __future__ import print_function
20
21import argparse
22import os
23import subprocess
24import sys
25import tempfile
26import time
27import uuid
28
29NULL = open(os.devnull)
30
31PACKAGES_LIST_CFG = '''data_sources {
32  config {
33    name: "android.packages_list"
34  }
35}
36'''
37
38CFG_INDENT = '      '
39CFG = '''buffers {{
40  size_kb: {size_kb}
41  fill_policy: DISCARD
42}}
43
44data_sources {{
45  config {{
46    name: "android.java_hprof"
47    java_hprof_config {{
48{target_cfg}
49{continuous_dump_config}
50    }}
51  }}
52}}
53
54data_source_stop_timeout_ms: {data_source_stop_timeout_ms}
55duration_ms: {duration_ms}
56'''
57
58OOM_CFG = '''buffers: {{
59    size_kb: {size_kb}
60    fill_policy: DISCARD
61}}
62
63data_sources: {{
64    config {{
65        name: "android.java_hprof.oom"
66        java_hprof_config {{
67{process_cfg}
68        }}
69    }}
70}}
71
72data_source_stop_timeout_ms: 100000
73
74trigger_config {{
75    trigger_mode: START_TRACING
76    trigger_timeout_ms: {wait_duration_ms}
77    triggers {{
78      name: "com.android.telemetry.art-outofmemory"
79      stop_delay_ms: 500
80    }}
81}}
82'''
83
84CONTINUOUS_DUMP = """
85      continuous_dump_config {{
86        dump_phase_ms: 0
87        dump_interval_ms: {dump_interval}
88      }}
89"""
90
91UUID = str(uuid.uuid4())[-6:]
92PROFILE_PATH = '/data/misc/perfetto-traces/java-profile-' + UUID
93
94PERFETTO_CMD = ('CFG=\'{cfg}\'; echo ${{CFG}} | '
95                'perfetto --txt -c - -o ' + PROFILE_PATH + ' -d')
96
97SDK = {
98    'S': 31,
99    'UpsideDownCake': 34,
100}
101
102
103def release_or_newer(release):
104  sdk = int(
105      subprocess.check_output(
106          ['adb', 'shell', 'getprop',
107           'ro.system.build.version.sdk']).decode('utf-8').strip())
108  if sdk >= SDK[release]:
109    return True
110  codename = subprocess.check_output(
111      ['adb', 'shell', 'getprop',
112       'ro.build.version.codename']).decode('utf-8').strip()
113  return codename == release
114
115def convert_size_to_kb(size):
116  if size.endswith("kb"):
117    return int(size[:-2])
118  elif size.endswith("mb"):
119    return int(size[:-2]) * 1024
120  elif size.endswith("gb"):
121    return int(size[:-2]) * 1024 * 1024
122  else:
123    return int(size)
124
125def generate_heap_dump_config(args):
126  fail = False
127  if args.pid is None and args.name is None:
128    print("FATAL: Neither PID nor NAME given.", file=sys.stderr)
129    fail = True
130
131  target_cfg = ""
132  if args.pid:
133    for pid in args.pid.split(','):
134      try:
135        pid = int(pid)
136      except ValueError:
137        print("FATAL: invalid PID %s" % pid, file=sys.stderr)
138        fail = True
139      target_cfg += '{}pid: {}\n'.format(CFG_INDENT, pid)
140  if args.name:
141    for name in args.name.split(','):
142      target_cfg += '{}process_cmdline: "{}"\n'.format(CFG_INDENT, name)
143  if args.dump_smaps:
144    target_cfg += '{}dump_smaps: true\n'.format(CFG_INDENT)
145
146  if fail:
147    return None
148
149  continuous_dump_cfg = ""
150  if args.continuous_dump:
151    continuous_dump_cfg = CONTINUOUS_DUMP.format(
152        dump_interval=args.continuous_dump)
153
154  if args.continuous_dump:
155    # Unlimited trace duration
156    duration_ms = 0
157  elif args.stop_when_done:
158    # Oneshot heapdump and the system supports data_source_stop_timeout_ms, we
159    # can use a short duration.
160    duration_ms = 1000
161  else:
162    # Oneshot heapdump, but the system doesn't supports
163    # data_source_stop_timeout_ms, we have to use a longer duration in the hope
164    # of giving enough time to capture the whole dump.
165    duration_ms = 20000
166
167  if args.stop_when_done:
168    data_source_stop_timeout_ms = 100000
169  else:
170    data_source_stop_timeout_ms = 0
171
172  return CFG.format(
173      size_kb=convert_size_to_kb(args.buffer_size),
174      target_cfg=target_cfg,
175      continuous_dump_config=continuous_dump_cfg,
176      duration_ms=duration_ms,
177      data_source_stop_timeout_ms=data_source_stop_timeout_ms)
178
179def generate_oom_config(args):
180  if not release_or_newer('UpsideDownCake'):
181    print("FATAL: OOM mode not supported for this android version",
182        file=sys.stderr)
183    return None
184
185  if args.pid:
186    print("FATAL: Specifying pid not supported in OOM mode",
187        file=sys.stderr)
188    return None
189
190  if not args.name:
191    print("FATAL: Must specify process in OOM mode (use --name '*' to match all)",
192        file=sys.stderr)
193    return None
194
195  if args.continuous_dump:
196    print("FATAL: Specifying continuous dump not supported in OOM mode",
197        file=sys.stderr)
198    return None
199
200  if args.dump_smaps:
201    print("FATAL: Dumping smaps not supported in OOM mode",
202        file=sys.stderr)
203    return None
204
205  process_cfg = ''
206  for name in args.name.split(','):
207    process_cfg += '{}process_cmdline: "{}"\n'.format(CFG_INDENT, name)
208
209  return OOM_CFG.format(
210      size_kb=convert_size_to_kb(args.buffer_size),
211      wait_duration_ms=args.oom_wait_seconds * 1000,
212      process_cfg=process_cfg)
213
214
215def main(argv):
216  parser = argparse.ArgumentParser()
217  parser.add_argument(
218      "-o",
219      "--output",
220      help="Filename to save profile to.",
221      metavar="FILE",
222      default=None)
223  parser.add_argument(
224      "-p",
225      "--pid",
226      help="Comma-separated list of PIDs to "
227      "profile.",
228      metavar="PIDS")
229  parser.add_argument(
230      "-n",
231      "--name",
232      help="Comma-separated list of process "
233      "names to profile.",
234      metavar="NAMES")
235  parser.add_argument(
236      "-b",
237      "--buffer-size",
238      help="Buffer size in memory that store the whole java heap graph. N(kb|mb|gb)",
239      type=str,
240      default="256mb")
241  parser.add_argument(
242      "-c",
243      "--continuous-dump",
244      help="Dump interval in ms. 0 to disable continuous dump. When continuous "
245      "dump is enabled, use CTRL+C to stop",
246      type=int,
247      default=0)
248  parser.add_argument(
249      "--no-versions",
250      action="store_true",
251      help="Do not get version information about APKs.")
252  parser.add_argument(
253      "--dump-smaps",
254      action="store_true",
255      help="Get information about /proc/$PID/smaps of target.")
256  parser.add_argument(
257      "--print-config",
258      action="store_true",
259      help="Print config instead of running. For debugging.")
260  parser.add_argument(
261      "--stop-when-done",
262      action="store_true",
263      default=None,
264      help="Use a new method to stop the profile when the dump is done. "
265      "Previously, we would hardcode a duration. Available and default on S.")
266  parser.add_argument(
267      "--no-stop-when-done",
268      action="store_false",
269      dest='stop_when_done',
270      help="Do not use a new method to stop the profile when the dump is done.")
271  parser.add_argument(
272      "--wait-for-oom",
273      action="store_true",
274      dest='wait_for_oom',
275      help="Starts a tracing session waiting for an OutOfMemoryError to be "
276      "thrown. Available on U.")
277  parser.add_argument(
278      "--oom-wait-seconds",
279      type=int,
280      default=60,
281      help="Seconds to wait for an OutOfMemoryError to be thrown. "
282      "Defaults to 60.")
283
284  args = parser.parse_args()
285
286  if args.stop_when_done is None:
287    args.stop_when_done = release_or_newer('S')
288
289  cfg = None
290  if args.wait_for_oom:
291    cfg = generate_oom_config(args)
292  else:
293    cfg = generate_heap_dump_config(args)
294
295  if not cfg:
296    parser.print_help()
297    return 1
298
299  if not args.no_versions:
300    cfg += PACKAGES_LIST_CFG
301
302  if args.print_config:
303    print(cfg)
304    return 0
305
306  output_file = args.output
307  if output_file is None:
308    fd, name = tempfile.mkstemp('profile')
309    os.close(fd)
310    output_file = name
311
312  user = subprocess.check_output(['adb', 'shell',
313                                  'whoami']).strip().decode('utf8')
314  perfetto_pid = subprocess.check_output(
315      ['adb', 'exec-out',
316       PERFETTO_CMD.format(cfg=cfg, user=user)]).strip().decode('utf8')
317  try:
318    int(perfetto_pid.strip())
319  except ValueError:
320    print("Failed to invoke perfetto: {}".format(perfetto_pid), file=sys.stderr)
321    return 1
322
323  if args.wait_for_oom:
324    print("Waiting for OutOfMemoryError")
325  else:
326    print("Dumping Java Heap.")
327
328  exists = True
329  ctrl_c_count = 0
330  # Wait for perfetto cmd to return.
331  while exists:
332    try:
333      exists = subprocess.call(
334          ['adb', 'shell', '[ -d /proc/{} ]'.format(perfetto_pid)]) == 0
335      time.sleep(1)
336    except KeyboardInterrupt as e:
337      ctrl_c_count += 1
338      subprocess.check_call(
339          ['adb', 'shell', 'kill -TERM {}'.format(perfetto_pid)])
340      if ctrl_c_count == 1:
341        print("Stopping perfetto and waiting for data...")
342      else:
343        raise e
344
345  subprocess.check_call(['adb', 'pull', PROFILE_PATH, output_file], stdout=NULL)
346
347  subprocess.check_call(['adb', 'shell', 'rm', '-f', PROFILE_PATH], stdout=NULL)
348
349  print("Wrote profile to {}".format(output_file))
350  print("This can be viewed using https://ui.perfetto.dev.")
351
352
353if __name__ == '__main__':
354  sys.exit(main(sys.argv))
355