Home | History | Annotate | Download | only in memdump
      1 #!/usr/bin/env python
      2 #
      3 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 import collections
      8 import json
      9 import optparse
     10 import os
     11 import re
     12 import threading
     13 import time
     14 import sys
     15 
     16 from sets import Set
     17 from string import Template
     18 
     19 sys.path.append(os.path.join(sys.path[0], os.pardir, os.pardir, os.pardir,
     20                              'build','android'))
     21 from pylib import android_commands
     22 from pylib import constants
     23 
     24 
     25 _ENTRIES = [
     26     ('Total', '.* r... '),
     27     ('Read-only', '.* r--. '),
     28     ('Read-write', '.* rw.. '),
     29     ('Executable', '.* ..x. '),
     30     ('Anonymous total', '.* ""'),
     31     ('Anonymous read-write', '.* rw.. .* ""'),
     32     ('Anonymous executable (JIT\'ed code)', '.* ..x. .* ""'),
     33     ('File total', '.* .... .* "/.*"'),
     34     ('File read-write', '.* rw.. .* "/.*"'),
     35     ('File executable', '.* ..x. .* "/.*"'),
     36     ('/dev files', '.* r... .* "/dev/.*"'),
     37     ('Dalvik', '.* rw.. .* "/.*dalvik.*"'),
     38     ('Dalvik heap', '.* rw.. .* "/.*dalvik-heap.*"'),
     39     ('Native heap (malloc)', '.* r... .* ".*malloc.*"'),
     40     ('Ashmem', '.* rw.. .* "/dev/ashmem '),
     41     ('Native library total', '.* r... .* "/data/app-lib/'),
     42     ('Native library read-only', '.* r--. .* "/data/app-lib/'),
     43     ('Native library read-write', '.* rw-. .* "/data/app-lib/'),
     44     ('Native library executable', '.* r.x. .* "/data/app-lib/'),
     45 ]
     46 
     47 
     48 def _CollectMemoryStats(memdump, region_filters):
     49   processes = []
     50   mem_usage_for_regions = None
     51   regexps = {}
     52   for region_filter in region_filters:
     53     regexps[region_filter] = re.compile(region_filter)
     54   for line in memdump:
     55     if 'PID=' in line:
     56       mem_usage_for_regions = {}
     57       processes.append(mem_usage_for_regions)
     58       continue
     59     matched_regions = Set([])
     60     for region_filter in region_filters:
     61       if regexps[region_filter].match(line.rstrip('\r\n')):
     62         matched_regions.add(region_filter)
     63         if not region_filter in mem_usage_for_regions:
     64           mem_usage_for_regions[region_filter] = {
     65               'private_unevictable': 0,
     66               'private': 0,
     67               'shared_app': 0.0,
     68               'shared_app_unevictable': 0.0,
     69               'shared_other_unevictable': 0,
     70               'shared_other': 0,
     71           }
     72     for matched_region in matched_regions:
     73       mem_usage = mem_usage_for_regions[matched_region]
     74       for key in mem_usage:
     75         for token in line.split(' '):
     76           if (key + '=') in token:
     77             field = token.split('=')[1]
     78             if key != 'shared_app':
     79               mem_usage[key] += int(field)
     80             else:  # shared_app=[\d:\d,\d:\d...]
     81               array = field[1:-1].split(',')
     82               for i in xrange(len(array)):
     83                 shared_app, shared_app_unevictable = array[i].split(':')
     84                 mem_usage['shared_app'] += float(shared_app) / (i + 2)
     85                 mem_usage['shared_app_unevictable'] += \
     86                     float(shared_app_unevictable) / (i + 2)
     87             break
     88   return processes
     89 
     90 
     91 def _ConvertMemoryField(field):
     92   return str(field / (1024.0 * 1024))
     93 
     94 
     95 def _DumpCSV(processes_stats):
     96   total_map = {}
     97   i = 0
     98   for process in processes_stats:
     99     i += 1
    100     print (',Process ' + str(i) + ',private,private_unevictable,shared_app,' +
    101            'shared_app_unevictable,shared_other,shared_other_unevictable,')
    102     for (k, v) in _ENTRIES:
    103       if not v in process:
    104         print ',' + k + ',0,0,0,0,0,'
    105         continue
    106       if not v in total_map:
    107         total_map[v] = {'resident':0, 'unevictable':0}
    108       total_map[v]['resident'] += (process[v]['private'] +
    109                                    process[v]['shared_app'])
    110       total_map[v]['unevictable'] += process[v]['private_unevictable'] + \
    111           process[v]['shared_app_unevictable']
    112       print (
    113           ',' + k + ',' +
    114           _ConvertMemoryField(process[v]['private']) + ',' +
    115           _ConvertMemoryField(process[v]['private_unevictable']) + ',' +
    116           _ConvertMemoryField(process[v]['shared_app']) + ',' +
    117           _ConvertMemoryField(process[v]['shared_app_unevictable']) + ',' +
    118           _ConvertMemoryField(process[v]['shared_other']) + ',' +
    119           _ConvertMemoryField(process[v]['shared_other_unevictable']) + ','
    120           )
    121     print ''
    122 
    123   for (k, v) in _ENTRIES:
    124     if not v in total_map:
    125       print ',' + k + ',0,0,'
    126       continue
    127     print (',' + k + ',' + _ConvertMemoryField(total_map[v]['resident']) + ',' +
    128            _ConvertMemoryField(total_map[v]['unevictable']) + ',')
    129   print ''
    130 
    131 
    132 def _RunManualGraph(package_name, interval):
    133   _AREA_TYPES = ('private', 'private_unevictable', 'shared_app',
    134                  'shared_app_unevictable', 'shared_other',
    135                  'shared_other_unevictable')
    136   all_pids = {}
    137   legends = ['Seconds'] + [entry + '_' + area
    138                            for entry, _ in _ENTRIES
    139                            for area in _AREA_TYPES]
    140   should_quit = threading.Event()
    141 
    142   def _GenerateGraph():
    143     _HTML_TEMPLATE = """
    144 <html>
    145   <head>
    146     <script type='text/javascript' src='https://www.google.com/jsapi'></script>
    147     <script type='text/javascript'>
    148       google.load('visualization', '1', {packages:['corechart', 'table']});
    149       google.setOnLoadCallback(createPidSelector);
    150       var pids = $JSON_PIDS;
    151       var pids_info = $JSON_PIDS_INFO;
    152       function drawVisualization(pid) {
    153         var data = google.visualization.arrayToDataTable(
    154           pids_info[pid]
    155         );
    156 
    157         var charOptions = {
    158           title: 'Memory Report (KB) for ' + pid,
    159           vAxis: {title: 'Time',  titleTextStyle: {color: 'red'}},
    160           isStacked : true
    161         };
    162 
    163         var chart = new google.visualization.BarChart(
    164             document.getElementById('chart_div'));
    165         chart.draw(data, charOptions);
    166 
    167         var table = new google.visualization.Table(
    168             document.getElementById('table_div'));
    169         table.draw(data);
    170       }
    171 
    172       function createPidSelector() {
    173         var pid_selector = document.getElementById('pid_selector');
    174         for (pid in pids) {
    175           var option = document.createElement('option');
    176           option.text = option.value = pids[pid];
    177           pid_selector.appendChild(option);
    178         }
    179         pid_selector.addEventListener('change',
    180           function() {
    181             drawVisualization(this.selectedOptions[0].value);
    182           }
    183         );
    184         drawVisualization(pids[0]);
    185       }
    186     </script>
    187   </head>
    188   <body>
    189     PIDS: <select id='pid_selector'></select>
    190     <div id='chart_div' style="width: 1024px; height: 800px;"></div>
    191     <div id='table_div' style="width: 1024px; height: 640px;"></div>
    192   </body>
    193 </html>
    194 """
    195     pids = sorted(all_pids.keys())
    196     pids_info = dict(zip(pids,
    197                          [ [legends] +
    198                            all_pids[p] for p in pids
    199                          ]))
    200     print Template(_HTML_TEMPLATE).safe_substitute({
    201         'JSON_PIDS': json.dumps(pids),
    202         'JSON_PIDS_INFO': json.dumps(pids_info)
    203     })
    204 
    205 
    206 
    207   def _CollectStats(count):
    208     adb = android_commands.AndroidCommands()
    209     pid_list = adb.ExtractPid(package_name)
    210     memdump = adb.RunShellCommand('/data/local/tmp/memdump ' +
    211                                   ' '.join(pid_list))
    212     process_stats = _CollectMemoryStats(memdump,
    213                                         [value for (key, value) in _ENTRIES])
    214     for (pid, process) in zip(pid_list, process_stats):
    215       first_pid_entry = True
    216       for (k, v) in _ENTRIES:
    217         if v not in process:
    218           continue
    219         for area_type in _AREA_TYPES:
    220           legend = k + '_' + area_type
    221           if pid not in all_pids:
    222             all_pids[pid] = []
    223           if first_pid_entry:
    224             all_pids[pid].append(['%ds' % (count * interval)] +
    225                                  [0] * (len(legends) - 1))
    226             first_pid_entry = False
    227           mem_kb = process[v][area_type] / 1024
    228           all_pids[pid][-1][legends.index(legend)] = mem_kb
    229 
    230   def _Loop():
    231     count = 0
    232     while not should_quit.is_set():
    233       print >>sys.stderr, 'Collecting ', count
    234       _CollectStats(count)
    235       count += 1
    236       should_quit.wait(interval)
    237 
    238   t = threading.Thread(target=_Loop)
    239 
    240 
    241   print >>sys.stderr, 'Press enter or CTRL+C to stop'
    242   t.start()
    243   try:
    244     _ = raw_input()
    245   except KeyboardInterrupt:
    246     pass
    247   finally:
    248     should_quit.set()
    249 
    250   t.join()
    251 
    252   _GenerateGraph()
    253 
    254 
    255 def main(argv):
    256   parser = optparse.OptionParser(usage='Usage: %prog [options]',
    257                                  description=__doc__)
    258   parser.add_option('-m',
    259                     '--manual-graph',
    260                     action='store_true',
    261                     help='Manually collect data and generate a graph.')
    262   parser.add_option('-p',
    263                     '--package',
    264                     default=constants.PACKAGE_INFO['chrome'].package,
    265                     help='Package name to collect.')
    266   parser.add_option('-i',
    267                     '--interval',
    268                     default=5,
    269                     type='int',
    270                     help='Interval in seconds for manual collections.')
    271   options, args = parser.parse_args(argv)
    272   if options.manual_graph:
    273     return _RunManualGraph(options.package, options.interval)
    274   _DumpCSV(_CollectMemoryStats(sys.stdin, [value for (key, value) in _ENTRIES]))
    275 
    276 
    277 if __name__ == '__main__':
    278   main(sys.argv)
    279