676 lines
20 KiB
Python
Executable file
676 lines
20 KiB
Python
Executable file
#!/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))
|