#!/usr/bin/python
#
# Copyright (C) 2010 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import cgi
import csv
import json
import math
import os
import re
import sys
import time
import urllib
"""Interpret output from procstatlog and write an HTML report file."""
# TODO: Rethink dygraph-combined.js source URL?
PAGE_BEGIN = """
<html><head>
<title>%(filename)s</title>
<script type="text/javascript" src="http://www.corp.google.com/~egnor/no_crawl/dygraph-combined.js"></script>
<script>
var allCharts = [];
var inDrawCallback = false;
OnDraw = function(me, initial) {
if (inDrawCallback || initial) return;
inDrawCallback = true;
var range = me.xAxisRange();
for (var j = 0; j < allCharts.length; j++) {
if (allCharts[j] == me) continue;
allCharts[j].updateOptions({dateWindow: range});
}
inDrawCallback = false;
}
MakeChart = function(id, filename, options) {
options.width = "75%%";
options.xTicker = Dygraph.dateTicker;
options.xValueFormatter = Dygraph.dateString_;
options.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
options.drawCallback = OnDraw;
allCharts.push(new Dygraph(document.getElementById(id), filename, options));
}
</script>
</head><body>
<p>
<span style="font-size: 150%%">%(filename)s</span>
- stat report generated by %(user)s on %(date)s</p>
<table cellpadding=0 cellspacing=0 margin=0 border=0>
"""
CHART = """
<tr>
<td valign=top width=25%%>%(label_html)s</td>
<td id="%(id)s"> </td>
</tr>
<script>
MakeChart(%(id_js)s, %(filename_js)s, %(options_js)s)
</script>
"""
SPACER = """
<tr><td colspan=2 height=20> </td></tr>
"""
TOTAL_CPU_LABEL = """
<b style="font-size: 150%%">Total CPU</b><br>
jiffies: <nobr>%(sys)d sys</nobr>, <nobr>%(user)d user</nobr>
"""
CPU_SPEED_LABEL = """
<nobr>average CPU speed</nobr>
"""
CONTEXT_LABEL = """
context: <nobr>%(switches)d switches</nobr>
"""
FAULTS_LABEL = """
<nobr>page faults:</nobr> <nobr>%(major)d major</nobr>
"""
BINDER_LABEL = """
binder: <nobr>%(calls)d calls</nobr>
"""
PROC_CPU_LABEL = """
<span style="font-size: 150%%">%(process)s</span> (%(pid)d)<br>
jiffies: <nobr>%(sys)d sys</nobr>, <nobr>%(user)d user</nobr>
</div>
"""
YAFFS_LABEL = """
<span style="font-size: 150%%">yaffs: %(partition)s</span><br>
pages: <nobr>%(nPageReads)d read</nobr>,
<nobr>%(nPageWrites)d written</nobr><br>
blocks: <nobr>%(nBlockErasures)d erased</nobr>
"""
DISK_LABEL = """
<span style="font-size: 150%%">disk: %(device)s</span><br>
sectors: <nobr>%(reads)d read</nobr>, <nobr>%(writes)d written</nobr>
"""
DISK_TIME_LABEL = """
msec: <nobr>%(msec)d waiting</nobr>
"""
NET_LABEL = """
<span style="font-size: 150%%">net: %(interface)s</span><br>
bytes: <nobr>%(tx)d tx</nobr>,
<nobr>%(rx)d rx</nobr>
"""
PAGE_END = """
</table></body></html>
"""
def WriteChartData(titles, datasets, filename):
writer = csv.writer(file(filename, "w"))
writer.writerow(["Time"] + titles)
merged_rows = {}
for set_num, data in enumerate(datasets):
for when, datum in data.iteritems():
if type(datum) == tuple: datum = "%d/%d" % datum
merged_rows.setdefault(when, {})[set_num] = datum
num_cols = len(datasets)
for when, values in sorted(merged_rows.iteritems()):
msec = "%d" % (when * 1000)
writer.writerow([msec] + [values.get(n, "") for n in range(num_cols)])
def WriteOutput(history, log_filename, filename):
out = []
out.append(PAGE_BEGIN % {
"filename": cgi.escape(log_filename),
"user": cgi.escape(os.environ.get("USER", "unknown")),
"date": cgi.escape(time.ctime()),
})
files_dir = "%s_files" % os.path.splitext(filename)[0]
files_url = os.path.basename(files_dir)
if not os.path.isdir(files_dir): os.makedirs(files_dir)
sorted_history = sorted(history.iteritems())
date_window = [1000 * sorted_history[1][0], 1000 * sorted_history[-1][0]]
#
# Output total CPU statistics
#
sys_jiffies = {}
sys_user_jiffies = {}
all_jiffies = {}
total_sys = total_user = 0
last_state = {}
for when, state in sorted_history:
last = last_state.get("/proc/stat:cpu", "").split()
next = state.get("/proc/stat:cpu", "").split()
if last and next:
stime = sum([int(next[x]) - int(last[x]) for x in [2, 5, 6]])
utime = sum([int(next[x]) - int(last[x]) for x in [0, 1]])
idle = sum([int(next[x]) - int(last[x]) for x in [3, 4]])
all = stime + utime + idle
total_sys += stime
total_user += utime
sys_jiffies[when] = (stime, all)
sys_user_jiffies[when] = (stime + utime, all)
all_jiffies[when] = all
last_state = state
WriteChartData(
["sys", "sys+user"],
[sys_jiffies, sys_user_jiffies],
os.path.join(files_dir, "total_cpu.csv"))
out.append(CHART % {
"id": cgi.escape("total_cpu"),
"id_js": json.write("total_cpu"),
"label_html": TOTAL_CPU_LABEL % {"sys": total_sys, "user": total_user},
"filename_js": json.write(files_url + "/total_cpu.csv"),
"options_js": json.write({
"colors": ["blue", "green"],
"dateWindow": date_window,
"fillGraph": True,
"fractions": True,
"height": 100,
"valueRange": [0, 110],
}),
})
#
# Output CPU speed statistics
#
cpu_speed = {}
speed_key = "/sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state:"
last_state = {}
for when, state in sorted_history:
total_time = total_cycles = 0
for key in state:
if not key.startswith(speed_key): continue
last = int(last_state.get(key, -1))
next = int(state.get(key, -1))
if last != -1 and next != -1:
speed = int(key[len(speed_key):])
total_time += next - last
total_cycles += (next - last) * speed
if total_time > 0: cpu_speed[when] = total_cycles / total_time
last_state = state
WriteChartData(
["kHz"], [cpu_speed],
os.path.join(files_dir, "cpu_speed.csv"))
out.append(CHART % {
"id": cgi.escape("cpu_speed"),
"id_js": json.write("cpu_speed"),
"label_html": CPU_SPEED_LABEL,
"filename_js": json.write(files_url + "/cpu_speed.csv"),
"options_js": json.write({
"colors": ["navy"],
"dateWindow": date_window,
"fillGraph": True,
"height": 50,
"includeZero": True,
}),
})
#
# Output total context switch statistics
#
context_switches = {}
last_state = {}
for when, state in sorted_history:
last = int(last_state.get("/proc/stat:ctxt", -1))
next = int(state.get("/proc/stat:ctxt", -1))
if last != -1 and next != -1: context_switches[when] = next - last
last_state = state
WriteChartData(
["switches"], [context_switches],
os.path.join(files_dir, "context_switches.csv"))
total_switches = sum(context_switches.values())
out.append(CHART % {
"id": cgi.escape("context_switches"),
"id_js": json.write("context_switches"),
"label_html": CONTEXT_LABEL % {"switches": total_switches},
"filename_js": json.write(files_url + "/context_switches.csv"),
"options_js": json.write({
"colors": ["blue"],
"dateWindow": date_window,
"fillGraph": True,
"height": 50,
"includeZero": True,
}),
})
#
# Collect (no output yet) per-process CPU and major faults
#
process_name = {}
process_start = {}
process_sys = {}
process_sys_user = {}
process_faults = {}
total_faults = {}
max_faults = 0
last_state = {}
zero_stat = "0 (zero) Z 0 0 0 0 0 0 0 0 0 0 0 0"
for when, state in sorted_history:
for key in state:
if not key.endswith("/stat"): continue
last = last_state.get(key, zero_stat).split()
next = state.get(key, "").split()
if not next: continue
pid = int(next[0])
process_start.setdefault(pid, when)
process_name[pid] = next[1][1:-1]
all = all_jiffies.get(when, 0)
if not all: continue
faults = int(next[11]) - int(last[11])
process_faults.setdefault(pid, {})[when] = faults
tf = total_faults[when] = total_faults.get(when, 0) + faults
max_faults = max(max_faults, tf)
stime = int(next[14]) - int(last[14])
utime = int(next[13]) - int(last[13])
process_sys.setdefault(pid, {})[when] = (stime, all)
process_sys_user.setdefault(pid, {})[when] = (stime + utime, all)
last_state = state
#
# Output total major faults (sum over all processes)
#
WriteChartData(
["major"], [total_faults],
os.path.join(files_dir, "total_faults.csv"))
out.append(CHART % {
"id": cgi.escape("total_faults"),
"id_js": json.write("total_faults"),
"label_html": FAULTS_LABEL % {"major": sum(total_faults.values())},
"filename_js": json.write(files_url + "/total_faults.csv"),
"options_js": json.write({
"colors": ["gray"],
"dateWindow": date_window,
"fillGraph": True,
"height": 50,
"valueRange": [0, max_faults * 11 / 10],
}),
})
#
# Output binder transaactions
#
binder_calls = {}
last_state = {}
for when, state in sorted_history:
last = int(last_state.get("/proc/binder/stats:BC_TRANSACTION", -1))
next = int(state.get("/proc/binder/stats:BC_TRANSACTION", -1))
if last != -1 and next != -1: binder_calls[when] = next - last
last_state = state
WriteChartData(
["calls"], [binder_calls],
os.path.join(files_dir, "binder_calls.csv"))
out.append(CHART % {
"id": cgi.escape("binder_calls"),
"id_js": json.write("binder_calls"),
"label_html": BINDER_LABEL % {"calls": sum(binder_calls.values())},
"filename_js": json.write(files_url + "/binder_calls.csv"),
"options_js": json.write({
"colors": ["green"],
"dateWindow": date_window,
"fillGraph": True,
"height": 50,
"includeZero": True,
})
})
#
# Output network interface statistics
#
if out[-1] != SPACER: out.append(SPACER)
interface_rx = {}
interface_tx = {}
max_bytes = 0
last_state = {}
for when, state in sorted_history:
for key in state:
if not key.startswith("/proc/net/dev:"): continue
last = last_state.get(key, "").split()
next = state.get(key, "").split()
if not (last and next): continue
rx = int(next[0]) - int(last[0])
tx = int(next[8]) - int(last[8])
max_bytes = max(max_bytes, rx, tx)
net, interface = key.split(":", 1)
interface_rx.setdefault(interface, {})[when] = rx
interface_tx.setdefault(interface, {})[when] = tx
last_state = state
for num, interface in enumerate(sorted(interface_rx.keys())):
rx, tx = interface_rx[interface], interface_tx[interface]
total_rx, total_tx = sum(rx.values()), sum(tx.values())
if not (total_rx or total_tx): continue
WriteChartData(
["rx", "tx"], [rx, tx],
os.path.join(files_dir, "net%d.csv" % num))
out.append(CHART % {
"id": cgi.escape("net%d" % num),
"id_js": json.write("net%d" % num),
"label_html": NET_LABEL % {
"interface": cgi.escape(interface),
"rx": total_rx,
"tx": total_tx
},
"filename_js": json.write("%s/net%d.csv" % (files_url, num)),
"options_js": json.write({
"colors": ["black", "purple"],
"dateWindow": date_window,
"fillGraph": True,
"height": 75,
"valueRange": [0, max_bytes * 11 / 10],
})
})
#
# Output YAFFS statistics
#
if out[-1] != SPACER: out.append(SPACER)
yaffs_vars = ["nBlockErasures", "nPageReads", "nPageWrites"]
partition_ops = {}
last_state = {}
for when, state in sorted_history:
for key in state:
if not key.startswith("/proc/yaffs:"): continue
last = int(last_state.get(key, -1))
next = int(state.get(key, -1))
if last == -1 or next == -1: continue
value = next - last
yaffs, partition, var = key.split(":", 2)
ops = partition_ops.setdefault(partition, {})
if var in yaffs_vars:
ops.setdefault(var, {})[when] = value
last_state = state
for num, (partition, ops) in enumerate(sorted(partition_ops.iteritems())):
totals = [sum(ops.get(var, {}).values()) for var in yaffs_vars]
if not sum(totals): continue
WriteChartData(
yaffs_vars,
[ops.get(var, {}) for var in yaffs_vars],
os.path.join(files_dir, "yaffs%d.csv" % num))
values = {"partition": partition}
values.update(zip(yaffs_vars, totals))
out.append(CHART % {
"id": cgi.escape("yaffs%d" % num),
"id_js": json.write("yaffs%d" % num),
"label_html": YAFFS_LABEL % values,
"filename_js": json.write("%s/yaffs%d.csv" % (files_url, num)),
"options_js": json.write({
"colors": ["maroon", "gray", "teal"],
"dateWindow": date_window,
"fillGraph": True,
"height": 75,
"includeZero": True,
})
})
#
# Output non-YAFFS statistics
#
disk_reads = {}
disk_writes = {}
disk_msec = {}
total_io = max_io = max_msec = 0
last_state = {}
for when, state in sorted_history:
for key in state:
if not key.startswith("/proc/diskstats:"): continue
last = last_state.get(key, "").split()
next = state.get(key, "").split()
if not (last and next): continue
reads = int(next[2]) - int(last[2])
writes = int(next[6]) - int(last[6])
msec = int(next[10]) - int(last[10])
total_io += reads + writes
max_io = max(max_io, reads, writes)
max_msec = max(max_msec, msec)
diskstats, device = key.split(":", 1)
disk_reads.setdefault(device, {})[when] = reads
disk_writes.setdefault(device, {})[when] = writes
disk_msec.setdefault(device, {})[when] = msec
last_state = state
io_cutoff = total_io / 100
for num, device in enumerate(sorted(disk_reads.keys())):
if [d for d in disk_reads.keys()
if d.startswith(device) and d != device]: continue
reads, writes = disk_reads[device], disk_writes[device]
total_reads, total_writes = sum(reads.values()), sum(writes.values())
if total_reads + total_writes <= io_cutoff: continue
WriteChartData(
["reads", "writes"], [reads, writes],
os.path.join(files_dir, "disk%d.csv" % num))
out.append(CHART % {
"id": cgi.escape("disk%d" % num),
"id_js": json.write("disk%d" % num),
"label_html": DISK_LABEL % {
"device": cgi.escape(device),
"reads": total_reads,
"writes": total_writes,
},
"filename_js": json.write("%s/disk%d.csv" % (files_url, num)),
"options_js": json.write({
"colors": ["gray", "teal"],
"dateWindow": date_window,
"fillGraph": True,
"height": 75,
"valueRange": [0, max_io * 11 / 10],
}),
})
msec = disk_msec[device]
WriteChartData(
["msec"], [msec],
os.path.join(files_dir, "disk%d_time.csv" % num))
out.append(CHART % {
"id": cgi.escape("disk%d_time" % num),
"id_js": json.write("disk%d_time" % num),
"label_html": DISK_TIME_LABEL % {"msec": sum(msec.values())},
"filename_js": json.write("%s/disk%d_time.csv" % (files_url, num)),
"options_js": json.write({
"colors": ["blue"],
"dateWindow": date_window,
"fillGraph": True,
"height": 50,
"valueRange": [0, max_msec * 11 / 10],
}),
})
#
# Output per-process CPU and page faults collected earlier
#
cpu_cutoff = (total_sys + total_user) / 200
faults_cutoff = sum(total_faults.values()) / 100
for start, pid in sorted([(s, p) for p, s in process_start.iteritems()]):
sys = sum([n for n, d in process_sys.get(pid, {}).values()])
sys_user = sum([n for n, d in process_sys_user.get(pid, {}).values()])
if sys_user <= cpu_cutoff: continue
if out[-1] != SPACER: out.append(SPACER)
WriteChartData(
["sys", "sys+user"],
[process_sys.get(pid, {}), process_sys_user.get(pid, {})],
os.path.join(files_dir, "proc%d.csv" % pid))
out.append(CHART % {
"id": cgi.escape("proc%d" % pid),
"id_js": json.write("proc%d" % pid),
"label_html": PROC_CPU_LABEL % {
"pid": pid,
"process": cgi.escape(process_name.get(pid, "(unknown)")),
"sys": sys,
"user": sys_user - sys,
},
"filename_js": json.write("%s/proc%d.csv" % (files_url, pid)),
"options_js": json.write({
"colors": ["blue", "green"],
"dateWindow": date_window,
"fillGraph": True,
"fractions": True,
"height": 75,
"valueRange": [0, 110],
}),
})
faults = sum(process_faults.get(pid, {}).values())
if faults <= faults_cutoff: continue
WriteChartData(
["major"], [process_faults.get(pid, {})],
os.path.join(files_dir, "proc%d_faults.csv" % pid))
out.append(CHART % {
"id": cgi.escape("proc%d_faults" % pid),
"id_js": json.write("proc%d_faults" % pid),
"label_html": FAULTS_LABEL % {"major": faults},
"filename_js": json.write("%s/proc%d_faults.csv" % (files_url, pid)),
"options_js": json.write({
"colors": ["gray"],
"dateWindow": date_window,
"fillGraph": True,
"height": 50,
"valueRange": [0, max_faults * 11 / 10],
}),
})
out.append(PAGE_END)
file(filename, "w").write("\n".join(out))
def main(argv):
if len(argv) != 3:
print >>sys.stderr, "usage: procstatreport.py procstat.log output.html"
return 2
history = {}
current_state = {}
scan_time = 0.0
for line in file(argv[1]):
if not line.endswith("\n"): continue
parts = line.split(None, 2)
if len(parts) < 2 or parts[1] not in "+-=":
print >>sys.stderr, "Invalid input:", line
sys.exit(1)
name, op = parts[:2]
if name == "T" and op == "+": # timestamp: scan about to begin
scan_time = float(line[4:])
continue
if name == "T" and op == "-": # timestamp: scan complete
time = (scan_time + float(line[4:])) / 2.0
history[time] = dict(current_state)
elif op == "-":
if name in current_state: del current_state[name]
else:
current_state[name] = "".join(parts[2:]).strip()
if len(history) < 2:
print >>sys.stderr, "error: insufficient history to chart"
return 1
WriteOutput(history, argv[1], argv[2])
if __name__ == "__main__":
sys.exit(main(sys.argv))