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