Add an activity monitor which profiles IO and CPU utilization.

This consists of a simple native program which dumps /proc/diskstats and
/proc/stat to a file at a regular interval. The dump is then processed host-side
into a JSON format which is charted in HTML via the Google Charts API.

The perf test runners are not upstreamed yet and when they are this will be part
of the set of profilers available.

BUG=136690
TEST=manual, run activity_monitor.py to try out.


Review URL: https://chromiumcodereview.appspot.com/10783020

git-svn-id: http://src.chromium.org/svn/trunk/src/build@147019 4ff67af0-8c30-449e-8e8b-ad334ec8d88c
This commit is contained in:
satish@chromium.org 2012-07-17 16:48:03 +00:00
Родитель 413027fa29
Коммит 5f332a2053
5 изменённых файлов: 302 добавлений и 1 удалений

Просмотреть файл

@ -39,6 +39,7 @@
'../third_party/WebKit/Source/WebKit/chromium/All.gyp:*',
# From here down: not added to run_tests.py yet.
'../jingle/jingle.gyp:jingle_unittests',
'../tools/android/device_stats_monitor/device_stats_monitor.gyp:device_stats_monitor',
'../tools/android/fake_dns/fake_dns.gyp:fake_dns',
'../tools/android/forwarder/forwarder.gyp:forwarder',
'../media/media.gyp:media_unittests',

41
android/device_stats_monitor.py Executable file
Просмотреть файл

@ -0,0 +1,41 @@
#!/usr/bin/env python
#
# Copyright (c) 2012 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.
"""Provides iotop/top style profiling for android.
Usage:
./device_stats_monitor.py --hz=20 --duration=5 --outfile=/tmp/foo
"""
import optparse
import os
import sys
import time
from pylib import android_commands
from pylib import device_stats_monitor
def main(argv):
option_parser = optparse.OptionParser()
option_parser.add_option('--hz', type='int', default=20,
help='Number of samples/sec.')
option_parser.add_option('--duration', type='int', default=5,
help='Seconds to monitor.')
option_parser.add_option('--outfile', default='/tmp/devicestatsmonitor',
help='Location to start output file.')
options, args = option_parser.parse_args(argv)
monitor = device_stats_monitor.DeviceStatsMonitor(
android_commands.AndroidCommands(), options.hz)
monitor.Start()
print 'Waiting for %d seconds while profiling.' % options.duration
time.sleep(options.duration)
url = monitor.StopAndCollect(options.outfile)
print 'View results in browser at %s' % url
if __name__ == '__main__':
sys.exit(main(sys.argv))

Просмотреть файл

@ -0,0 +1,143 @@
<!DOCTYPE html>
<!--
* Copyright (c) 2012 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.
-->
<html>
<head>
<title>Device Stats Monitor</title>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<style>
body {
font-family: sans-serif
}
</style>
</head>
<body>
<h2>Device Stats Monitor</h2>
<ul>
<li>Pass path to trace data via the <code>results</code> querystring param.
<li>Combine charts with the <code>combine</code> querystring param (e.g. <code>&combine=sectors_read,sectors_written</code>).
<li>Use <code>stacked=true</code> to stack combined charts instead of overlaying (default).
</ul>
</body>
<script>
google.load("visualization", "1", {packages:["corechart"]});
/**
* @returns The querystring param value for |name| or an empty string.
*/
function getQuerystringParam(name) {
name = name.replace(/[\[]/, "\\\[").replace(/[\]]/, "\\\]");
var regexS = "[\\?&]" + name + "=([^&#]*)";
var regex = new RegExp(regexS);
var results = regex.exec(window.location.search);
if (results == null)
return "";
else
return decodeURIComponent(results[1].replace(/\+/g, " "));
}
/**
* @returns An array of keys in |obj| sorted by value.
*/
function sortedKeys(obj) {
var keys = [];
for (var key in obj) {
keys.push(key);
}
keys.sort();
return keys;
}
/**
* Removes by value all params from array.
*/
Array.prototype.remove = function() {
var what, a = arguments, l = a.length, ax;
while (l && this.length) {
what = a[--l];
while ((ax = this.indexOf(what)) != -1) {
this.splice(ax, 1);
}
}
return this;
}
/**
* Displays a new chart.
*
* @param {Number} hz Number of sample per second of the data.
* @param {String} name Name to display on top of chart.
* @param {Number[][]} values Array of value arrays to display.
* @param {Boolean} stacked Whether to display values as stacked.
*/
function displayChart(hz, name, values, units, stacked) {
var data = new google.visualization.DataTable();
data.addColumn('number', 'ms');
var names = name.split(',');
for (var i = 0; i < names.length; i++) {
data.addColumn('number', names[i]);
}
var rows = [];
var interval = 1000.0 / hz;
for (var i = 0; i < values[0].length; i++) {
var row = [i*interval];
for (var j = 0; j < values.length; j++) {
row.push(values[j][i]);
}
rows.push(row);
}
data.addRows(rows);
var options = {
hAxis: {title: 'ms (' + hz + 'hz)'},
isStacked: stacked,
legend: {position: 'top'},
vAxis: {title: units},
};
var elem = document.createElement('DIV');
elem.style = 'width:100%;height:500px';
document.body.appendChild(elem);
var chart = new google.visualization.AreaChart(elem);
chart.draw(data, options);
}
/**
* Displays all charts.
*
* Invoked by the results script. JSONP is used to avoid security
* restrictions on XHRs for file:// URLs.
*/
function display(hz, results, units) {
var combine = getQuerystringParam('combine');
var keys = sortedKeys(results);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var name = key;
var values = [results[key]];
var unit = units[key];
if (combine.indexOf(key) >= 0) {
i--;
name = combine;
values = [];
var combined_keys = combine.split(',');
for (var j = 0; j < combined_keys.length; j++) {
values.push(results[combined_keys[j]]);
keys.remove(combined_keys[j]);
}
}
displayChart(hz, name, values, unit, !!getQuerystringParam('stacked'));
}
}
var resultsPath = getQuerystringParam('results');
if (resultsPath)
document.write("<script src='" + resultsPath + "'></"+"script>");
else
document.write("Please specify results querystring param.");
</script>
</html>

Просмотреть файл

@ -0,0 +1,116 @@
# Copyright (c) 2012 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.
"""Utilities for iotop/top style profiling for android."""
import collections
import json
import os
import subprocess
import sys
import urllib
import constants
import io_stats_parser
class DeviceStatsMonitor(object):
"""Class for collecting device stats such as IO/CPU usage.
Args:
adb: Instance of AndroidComannds.
hz: Frequency at which to sample device stats.
"""
DEVICE_PATH = '/data/local/tmp/device_stats_monitor'
HOST_PATH = os.path.abspath(os.path.join(
constants.CHROME_DIR, 'out', 'Release', 'device_stats_monitor'))
PROFILE_PATH = '/sdcard/Download/device_stats_monitor.profile'
RESULT_VIEWER_PATH = os.path.abspath(os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'device_stats_monitor.html'))
def __init__(self, adb, hz):
self._adb = adb
self._adb.PushIfNeeded(DeviceStatsMonitor.HOST_PATH,
DeviceStatsMonitor.DEVICE_PATH)
self._hz = hz
def Start(self):
"""Starts device stats monitor on the device."""
self._adb.SetFileContents(DeviceStatsMonitor.PROFILE_PATH, '')
self._process = subprocess.Popen(
['adb', 'shell', '%s --hz=%d %s' % (
DeviceStatsMonitor.DEVICE_PATH, self._hz,
DeviceStatsMonitor.PROFILE_PATH)])
def StopAndCollect(self, output_path):
"""Stops monitoring and saves results.
Args:
output_path: Path to save results.
Returns:
String of URL to load results in browser.
"""
assert self._process
self._adb.KillAll(DeviceStatsMonitor.DEVICE_PATH)
self._process.wait()
profile = self._adb.GetFileContents(DeviceStatsMonitor.PROFILE_PATH)
results = collections.defaultdict(list)
last_io_stats = None
last_cpu_stats = None
for line in profile:
if ' mmcblk0 ' in line:
stats = io_stats_parser.ParseIoStatsLine(line)
if last_io_stats:
results['sectors_read'].append(stats.num_sectors_read -
last_io_stats.num_sectors_read)
results['sectors_written'].append(stats.num_sectors_written -
last_io_stats.num_sectors_written)
last_io_stats = stats
elif line.startswith('cpu '):
stats = self._ParseCpuStatsLine(line)
if last_cpu_stats:
results['user'].append(stats.user - last_cpu_stats.user)
results['nice'].append(stats.nice - last_cpu_stats.nice)
results['system'].append(stats.system - last_cpu_stats.system)
results['idle'].append(stats.idle - last_cpu_stats.idle)
results['iowait'].append(stats.iowait - last_cpu_stats.iowait)
results['irq'].append(stats.irq - last_cpu_stats.irq)
results['softirq'].append(stats.softirq- last_cpu_stats.softirq)
last_cpu_stats = stats
units = {
'sectors_read': 'sectors',
'sectors_written': 'sectors',
'user': 'jiffies',
'nice': 'jiffies',
'system': 'jiffies',
'idle': 'jiffies',
'iowait': 'jiffies',
'irq': 'jiffies',
'softirq': 'jiffies',
}
with open(output_path, 'w') as f:
f.write('display(%d, %s, %s);' % (self._hz, json.dumps(results), units))
return 'file://%s?results=file://%s' % (
DeviceStatsMonitor.RESULT_VIEWER_PATH, urllib.quote(output_path))
@staticmethod
def _ParseCpuStatsLine(line):
"""Parses a line of cpu stats into a CpuStats named tuple."""
# Field definitions: http://www.linuxhowtos.org/System/procstat.htm
cpu_stats = collections.namedtuple('CpuStats',
['device',
'user',
'nice',
'system',
'idle',
'iowait',
'irq',
'softirq',
])
fields = line.split()
return cpu_stats._make([fields[0]] + [int(f) for f in fields[1:8]])

Просмотреть файл

@ -26,7 +26,7 @@ def CreateTestRunnerOptionParser(usage=None, default_timeout=60):
default=0,
action='count',
help='Verbose level (multiple times for more)')
profilers = ['activitymonitor', 'chrometrace', 'dumpheap', 'smaps',
profilers = ['devicestatsmonitor', 'chrometrace', 'dumpheap', 'smaps',
'traceview']
option_parser.add_option('--profiler', dest='profilers', action='append',
choices=profilers,