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:
Родитель
413027fa29
Коммит
5f332a2053
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
Загрузка…
Ссылка в новой задаче