blob: 1782d6a5c4e1cd3cfe177a6fa48f1f96a76aa49c [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright (c) 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import json
import optparse
import os
import re
import threading
import time
import sys
from sets import Set
from string import Template
sys.path.append(os.path.join(sys.path[0], os.pardir, os.pardir, os.pardir,
'build','android'))
from pylib import android_commands
from pylib import constants
_ENTRIES = [
('Total', '.* r... '),
('Read-only', '.* r--. '),
('Read-write', '.* rw.. '),
('Executable', '.* ..x. '),
('Anonymous total', '.* ""'),
('Anonymous read-write', '.* rw.. .* ""'),
('Anonymous executable (JIT\'ed code)', '.* ..x. .* ""'),
('File total', '.* .... .* "/.*"'),
('File read-write', '.* rw.. .* "/.*"'),
('File executable', '.* ..x. .* "/.*"'),
('/dev files', '.* r... .* "/dev/.*"'),
('Dalvik', '.* rw.. .* "/.*dalvik.*"'),
('Dalvik heap', '.* rw.. .* "/.*dalvik-heap.*"'),
('Native heap (malloc)', '.* r... .* ".*malloc.*"'),
('Ashmem', '.* rw.. .* "/dev/ashmem '),
('Native library total', '.* r... .* "/data/app-lib/'),
('Native library read-only', '.* r--. .* "/data/app-lib/'),
('Native library read-write', '.* rw-. .* "/data/app-lib/'),
('Native library executable', '.* r.x. .* "/data/app-lib/'),
]
def _CollectMemoryStats(memdump, region_filters):
processes = []
mem_usage_for_regions = None
regexps = {}
for region_filter in region_filters:
regexps[region_filter] = re.compile(region_filter)
for line in memdump:
if 'PID=' in line:
mem_usage_for_regions = {}
processes.append(mem_usage_for_regions)
continue
matched_regions = Set([])
for region_filter in region_filters:
if regexps[region_filter].match(line.rstrip('\r\n')):
matched_regions.add(region_filter)
if not region_filter in mem_usage_for_regions:
mem_usage_for_regions[region_filter] = {
'private_unevictable': 0,
'private': 0,
'shared_app': 0.0,
'shared_app_unevictable': 0.0,
'shared_other_unevictable': 0,
'shared_other': 0,
}
for matched_region in matched_regions:
mem_usage = mem_usage_for_regions[matched_region]
for key in mem_usage:
for token in line.split(' '):
if (key + '=') in token:
field = token.split('=')[1]
if key != 'shared_app':
mem_usage[key] += int(field)
else: # shared_app=[\d:\d,\d:\d...]
array = field[1:-1].split(',')
for i in xrange(len(array)):
shared_app, shared_app_unevictable = array[i].split(':')
mem_usage['shared_app'] += float(shared_app) / (i + 2)
mem_usage['shared_app_unevictable'] += \
float(shared_app_unevictable) / (i + 2)
break
return processes
def _ConvertMemoryField(field):
return str(field / (1024.0 * 1024))
def _DumpCSV(processes_stats):
total_map = {}
i = 0
for process in processes_stats:
i += 1
print (',Process ' + str(i) + ',private,private_unevictable,shared_app,' +
'shared_app_unevictable,shared_other,shared_other_unevictable,')
for (k, v) in _ENTRIES:
if not v in process:
print ',' + k + ',0,0,0,0,0,'
continue
if not v in total_map:
total_map[v] = {'resident':0, 'unevictable':0}
total_map[v]['resident'] += (process[v]['private'] +
process[v]['shared_app'])
total_map[v]['unevictable'] += process[v]['private_unevictable'] + \
process[v]['shared_app_unevictable']
print (
',' + k + ',' +
_ConvertMemoryField(process[v]['private']) + ',' +
_ConvertMemoryField(process[v]['private_unevictable']) + ',' +
_ConvertMemoryField(process[v]['shared_app']) + ',' +
_ConvertMemoryField(process[v]['shared_app_unevictable']) + ',' +
_ConvertMemoryField(process[v]['shared_other']) + ',' +
_ConvertMemoryField(process[v]['shared_other_unevictable']) + ','
)
print ''
for (k, v) in _ENTRIES:
if not v in total_map:
print ',' + k + ',0,0,'
continue
print (',' + k + ',' + _ConvertMemoryField(total_map[v]['resident']) + ',' +
_ConvertMemoryField(total_map[v]['unevictable']) + ',')
print ''
def _RunManualGraph(package_name, interval):
_AREA_TYPES = ('private', 'private_unevictable', 'shared_app',
'shared_app_unevictable', 'shared_other',
'shared_other_unevictable')
all_pids = {}
legends = ['Seconds'] + [entry + '_' + area
for entry, _ in _ENTRIES
for area in _AREA_TYPES]
should_quit = threading.Event()
def _GenerateGraph():
_HTML_TEMPLATE = """
<html>
<head>
<script type='text/javascript' src='https://www.google.com/jsapi'></script>
<script type='text/javascript'>
google.load('visualization', '1', {packages:['corechart', 'table']});
google.setOnLoadCallback(createPidSelector);
var pids = $JSON_PIDS;
var pids_info = $JSON_PIDS_INFO;
function drawVisualization(pid) {
var data = google.visualization.arrayToDataTable(
pids_info[pid]
);
var charOptions = {
title: 'Memory Report (KB) for ' + pid,
vAxis: {title: 'Time', titleTextStyle: {color: 'red'}},
isStacked : true
};
var chart = new google.visualization.BarChart(
document.getElementById('chart_div'));
chart.draw(data, charOptions);
var table = new google.visualization.Table(
document.getElementById('table_div'));
table.draw(data);
}
function createPidSelector() {
var pid_selector = document.getElementById('pid_selector');
for (pid in pids) {
var option = document.createElement('option');
option.text = option.value = pids[pid];
pid_selector.appendChild(option);
}
pid_selector.addEventListener('change',
function() {
drawVisualization(this.selectedOptions[0].value);
}
);
drawVisualization(pids[0]);
}
</script>
</head>
<body>
PIDS: <select id='pid_selector'></select>
<div id='chart_div' style="width: 1024px; height: 800px;"></div>
<div id='table_div' style="width: 1024px; height: 640px;"></div>
</body>
</html>
"""
pids = sorted(all_pids.keys())
pids_info = dict(zip(pids,
[ [legends] +
all_pids[p] for p in pids
]))
print Template(_HTML_TEMPLATE).safe_substitute({
'JSON_PIDS': json.dumps(pids),
'JSON_PIDS_INFO': json.dumps(pids_info)
})
def _CollectStats(count):
adb = android_commands.AndroidCommands()
pid_list = adb.ExtractPid(package_name)
memdump = adb.RunShellCommand('/data/local/tmp/memdump ' +
' '.join(pid_list))
process_stats = _CollectMemoryStats(memdump,
[value for (key, value) in _ENTRIES])
for (pid, process) in zip(pid_list, process_stats):
first_pid_entry = True
for (k, v) in _ENTRIES:
if v not in process:
continue
for area_type in _AREA_TYPES:
legend = k + '_' + area_type
if pid not in all_pids:
all_pids[pid] = []
if first_pid_entry:
all_pids[pid].append(['%ds' % (count * interval)] +
[0] * (len(legends) - 1))
first_pid_entry = False
mem_kb = process[v][area_type] / 1024
all_pids[pid][-1][legends.index(legend)] = mem_kb
def _Loop():
count = 0
while not should_quit.is_set():
print >>sys.stderr, 'Collecting ', count
_CollectStats(count)
count += 1
should_quit.wait(interval)
t = threading.Thread(target=_Loop)
print >>sys.stderr, 'Press enter or CTRL+C to stop'
t.start()
try:
_ = raw_input()
except KeyboardInterrupt:
pass
finally:
should_quit.set()
t.join()
_GenerateGraph()
def main(argv):
parser = optparse.OptionParser(usage='Usage: %prog [options]',
description=__doc__)
parser.add_option('-m',
'--manual-graph',
action='store_true',
help='Manually collect data and generate a graph.')
parser.add_option('-p',
'--package',
default=constants.PACKAGE_INFO['chrome'].package,
help='Package name to collect.')
parser.add_option('-i',
'--interval',
default=5,
type='int',
help='Interval in seconds for manual collections.')
options, args = parser.parse_args(argv)
if options.manual_graph:
return _RunManualGraph(options.package, options.interval)
_DumpCSV(_CollectMemoryStats(sys.stdin, [value for (key, value) in _ENTRIES]))
if __name__ == '__main__':
main(sys.argv)