1# Copyright 2014 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Command line frontend for Memory Inspector"""
6
7import json
8import memory_inspector
9import optparse
10import os
11import time
12
13from memory_inspector import constants
14from memory_inspector.classification import mmap_classifier
15from memory_inspector.core import backends
16from memory_inspector.data import serialization
17
18
19def main():
20  COMMANDS = ['devices', 'ps', 'stats', 'mmaps', 'classified_mmaps']
21  usage = ('%prog [options] ' + ' | '.join(COMMANDS))
22  parser = optparse.OptionParser(usage=usage)
23  parser.add_option('-b', '--backend', help='Backend name '
24                    '(e.g., Android)', type='string', default='Android')
25  parser.add_option('-s', '--device_id', help='Device '
26                    'id (e.g., Android serial)', type='string')
27  parser.add_option('-p', '--process_id', help='Target process id',
28                    type='int')
29  parser.add_option('-m', '--filter_process_name', help='Process '
30                    'name to match', type='string')
31  parser.add_option('-r', '--mmap_rule',
32                    help='mmap rule', type='string',
33                    default=os.path.join(constants.CLASSIFICATION_RULES_PATH,
34                        'default', 'mmap-android.py'))
35  (options, args) = parser.parse_args()
36
37  memory_inspector.RegisterAllBackends()
38
39  if not args or args[0] not in COMMANDS:
40    parser.print_help()
41    return -1
42
43  if args[0] == 'devices':
44    _ListDevices(options.backend)
45    return 0
46
47  number_of_devices = 0
48  if options.device_id:
49    device_id = options.device_id
50    number_of_devices = 1
51  else:
52    for device in backends.ListDevices():
53      if device.backend.name == options.backend:
54        number_of_devices += 1
55        device_id = device.id
56
57  if number_of_devices == 0:
58    print "No devices connected"
59    return -1
60
61  if number_of_devices > 1:
62    print ('More than 1 device connected. You need to provide'
63        ' --device_id')
64    return -1
65
66  device = backends.GetDevice(options.backend, device_id)
67  if not device:
68    print 'Device', device_id, 'does not exist'
69    return -1
70
71  device.Initialize()
72  if args[0] == 'ps':
73    if not options.filter_process_name:
74      print 'Listing all processes'
75    else:
76      print ('Listing processes matching '
77          + options.filter_process_name.lower())
78    print ''
79    print '%-10s : %-50s : %12s %12s %12s' % (
80        'Process ID', 'Process Name', 'RUN_TIME', 'THREADS',
81        'MEM_RSS_KB')
82    print ''
83    for process in device.ListProcesses():
84      if (not options.filter_process_name or
85          options.filter_process_name.lower() in process.name.lower()):
86        stats = process.GetStats()
87        run_time_min, run_time_sec = divmod(stats.run_time, 60)
88        print '%10s : %-50s : %6s m %2s s %8s %12s' % (
89            process.pid, _Truncate(process.name, 50), run_time_min,
90            run_time_sec, stats.threads, stats.vm_rss)
91    return 0
92
93  if not options.process_id:
94    print 'You need to provide --process_id'
95    return -1
96
97  process = device.GetProcess(options.process_id)
98
99  if not process:
100    print 'Cannot find process [%d] on device %s' % (
101        options.process_id, device.id)
102    return -1
103  elif args[0] == 'stats':
104    _ListProcessStats(process)
105    return 0
106  elif args[0] == 'mmaps':
107    _ListProcessMmaps(process)
108    return 0
109  elif args[0] == 'classified_mmaps':
110    _ListProcessClassifiedMmaps(process, options.mmap_rule)
111    return 0
112
113
114def _ListDevices(backend_name):
115  print 'Device list:'
116  print ''
117  for device in backends.ListDevices():
118    if device.backend.name == backend_name:
119      print '%-16s : %s' % (device.id, device.name)
120
121
122def _ListProcessStats(process):
123  """Prints process stats periodically
124  """
125  print 'Stats for process: [%d] %s' % (process.pid, process.name)
126  print '%-10s : %-50s : %12s %12s %13s %12s %14s' % (
127      'Process ID', 'Process Name', 'RUN_TIME', 'THREADS',
128      'CPU_USAGE', 'MEM_RSS_KB', 'PAGE_FAULTS')
129  print ''
130  while True:
131    stats = process.GetStats()
132    run_time_min, run_time_sec = divmod(stats.run_time, 60)
133    print '%10s : %-50s : %6s m %2s s %8s %12s %13s %11s' % (
134        process.pid, _Truncate(process.name, 50), run_time_min, run_time_sec,
135        stats.threads, stats.cpu_usage, stats.vm_rss, stats.page_faults)
136    time.sleep(1)
137
138
139def _ListProcessMmaps(process):
140  """Prints process memory maps
141  """
142  print 'Memory Maps for process: [%d] %s' % (process.pid, process.name)
143  print '%-10s %-10s %6s %12s %12s %13s %13s %-40s' % (
144      'START', 'END', 'FLAGS', 'PRIV.DIRTY', 'PRIV.CLEAN',
145      'SHARED DIRTY', 'SHARED CLEAN', 'MAPPED_FILE')
146  print '%38s %12s %12s %13s' % ('(kb)', '(kb)', '(kb)', '(kb)')
147  print ''
148  maps = process.DumpMemoryMaps()
149  for entry in maps.entries:
150    print '%-10x %-10x %6s %12s %12s %13s %13s %-40s' % (
151        entry.start, entry.end, entry.prot_flags,
152        entry.priv_dirty_bytes / 1024, entry.priv_clean_bytes / 1024,
153        entry.shared_dirty_bytes / 1024,
154        entry.shared_clean_bytes / 1024, entry.mapped_file)
155
156
157def _ListProcessClassifiedMmaps(process, mmap_rule):
158  """Prints process classified memory maps
159  """
160  maps = process.DumpMemoryMaps()
161  if not os.path.exists(mmap_rule):
162    print 'File', mmap_rule, 'not found'
163    return
164  with open(mmap_rule) as f:
165    rules = mmap_classifier.LoadRules(f.read())
166  classified_results_tree =  mmap_classifier.Classify(maps, rules)
167  print json.dumps(classified_results_tree, cls=serialization.Encoder)
168
169
170def _Truncate(name, max_length):
171  if len(name) <= max_length:
172    return name
173  return '%s...' % name[0:(max_length - 3)]
174