procstatreport.py revision 376b3009a0d438a16fb7d6c27e19839cd1bb3f1f
1#!/usr/bin/python
2#
3# Copyright (C) 2010 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import cgi
18import csv
19import json
20import math
21import os
22import re
23import sys
24import time
25import urllib
26
27"""Interpret output from procstatlog and write an HTML report file."""
28
29
30# TODO: Rethink dygraph-combined.js source URL?
31PAGE_BEGIN = """
32<html><head>
33<title>%(filename)s</title>
34<script type="text/javascript" src="http://www.corp.google.com/~egnor/no_crawl/dygraph-combined.js"></script>
35<script>
36var allCharts = [];
37var inDrawCallback = false;
38
39OnDraw = function(me, initial) {
40    if (inDrawCallback || initial) return;
41    inDrawCallback = true;
42    var range = me.xAxisRange();
43    for (var j = 0; j < allCharts.length; j++) {
44        if (allCharts[j] == me) continue;
45        allCharts[j].updateOptions({dateWindow: range});
46    }
47    inDrawCallback = false;
48}
49
50MakeChart = function(id, filename, options) {
51    options.width = "75%%";
52    options.xTicker = Dygraph.dateTicker;
53    options.xValueFormatter = Dygraph.dateString_;
54    options.xAxisLabelFormatter = Dygraph.dateAxisFormatter;
55    options.drawCallback = OnDraw;
56    allCharts.push(new Dygraph(document.getElementById(id), filename, options));
57}
58</script>
59</head><body>
60<p>
61<span style="font-size: 150%%">%(filename)s</span>
62- stat report generated by %(user)s on %(date)s</p>
63<table cellpadding=0 cellspacing=0 margin=0 border=0>
64"""
65
66CHART = """
67<tr>
68<td valign=top width=25%%>%(label_html)s</td>
69<td id="%(id)s"> </td>
70</tr>
71<script>
72MakeChart(%(id_js)s, %(filename_js)s, %(options_js)s)
73
74</script>
75"""
76
77SPACER = """
78<tr><td colspan=2 height=20> </td></tr>
79"""
80
81TOTAL_CPU_LABEL = """
82<b style="font-size: 150%%">Total CPU</b><br>
83jiffies: <nobr>%(sys)d sys</nobr>, <nobr>%(user)d user</nobr>
84"""
85
86CPU_SPEED_LABEL = """
87<nobr>average CPU speed</nobr>
88"""
89
90CONTEXT_LABEL = """
91context: <nobr>%(switches)d switches</nobr>
92"""
93
94FAULTS_LABEL = """
95<nobr>page faults:</nobr> <nobr>%(major)d major</nobr>
96"""
97
98BINDER_LABEL = """
99binder: <nobr>%(calls)d calls</nobr>
100"""
101
102PROC_CPU_LABEL = """
103<span style="font-size: 150%%">%(process)s</span> (%(pid)d)<br>
104jiffies: <nobr>%(sys)d sys</nobr>, <nobr>%(user)d user</nobr>
105</div>
106"""
107
108YAFFS_LABEL = """
109<span style="font-size: 150%%">yaffs: %(partition)s</span><br>
110pages: <nobr>%(nPageReads)d read</nobr>,
111<nobr>%(nPageWrites)d written</nobr><br>
112blocks: <nobr>%(nBlockErasures)d erased</nobr>
113"""
114
115NET_LABEL = """
116<span style="font-size: 150%%">net: %(interface)s</span><br>
117bytes: <nobr>%(tx)d tx</nobr>,
118<nobr>%(rx)d rx</nobr><br>
119"""
120
121PAGE_END = """
122</table></body></html>
123"""
124
125
126def WriteChartData(titles, datasets, filename):
127    writer = csv.writer(file(filename, "w"))
128    writer.writerow(["Time"] + titles)
129
130    merged_rows = {}
131    for set_num, data in enumerate(datasets):
132        for when, datum in data.iteritems():
133            if type(datum) == tuple: datum = "%d/%d" % datum
134            merged_rows.setdefault(when, {})[set_num] = datum
135
136    num_cols = len(datasets)
137    for when, values in sorted(merged_rows.iteritems()):
138        msec = "%d" % (when * 1000)
139        writer.writerow([msec] + [values.get(n, "") for n in range(num_cols)])
140
141
142def WriteOutput(history, log_filename, filename):
143    out = []
144
145    out.append(PAGE_BEGIN % {
146        "filename": cgi.escape(log_filename),
147        "user": cgi.escape(os.environ.get("USER", "unknown")),
148        "date": cgi.escape(time.ctime()),
149    })
150
151    files_dir = "%s_files" % os.path.splitext(filename)[0]
152    files_url = os.path.basename(files_dir)
153    if not os.path.isdir(files_dir): os.makedirs(files_dir)
154
155    sorted_history = sorted(history.iteritems())
156    date_window = [1000 * sorted_history[1][0], 1000 * sorted_history[-1][0]]
157
158    #
159    # Output total CPU statistics
160    #
161
162    sys_jiffies = {}
163    sys_user_jiffies = {}
164    all_jiffies = {}
165    total_sys = total_user = 0
166
167    last_state = {}
168    for when, state in sorted_history:
169        last = last_state.get("/proc/stat:cpu", "").split()
170        next = state.get("/proc/stat:cpu", "").split()
171        if last and next:
172            stime = sum([int(next[x]) - int(last[x]) for x in [2, 5, 6]])
173            utime = sum([int(next[x]) - int(last[x]) for x in [0, 1]])
174            idle = sum([int(next[x]) - int(last[x]) for x in [3, 4]])
175            all = stime + utime + idle
176            total_sys += stime
177            total_user += utime
178
179            sys_jiffies[when] = (stime, all)
180            sys_user_jiffies[when] = (stime + utime, all)
181            all_jiffies[when] = all
182
183        last_state = state
184
185    WriteChartData(
186        ["sys", "sys+user"],
187        [sys_jiffies, sys_user_jiffies],
188        os.path.join(files_dir, "total_cpu.csv"))
189
190    out.append(CHART % {
191        "id": cgi.escape("total_cpu"),
192        "id_js": json.write("total_cpu"),
193        "label_html": TOTAL_CPU_LABEL % {"sys": total_sys, "user": total_user},
194        "filename_js": json.write(files_url + "/total_cpu.csv"),
195        "options_js": json.write({
196            "colors": ["blue", "green"],
197            "dateWindow": date_window,
198            "fillGraph": True,
199            "fractions": True,
200            "height": 100,
201            "valueRange": [0, 110],
202        }),
203    })
204
205    #
206    # Output CPU speed statistics
207    #
208
209    cpu_speed = {}
210    speed_key = "/sys/devices/system/cpu/cpu0/cpufreq/stats/time_in_state:"
211
212    last_state = {}
213    for when, state in sorted_history:
214        total_time = total_cycles = 0
215        for key in state:
216            if not key.startswith(speed_key): continue
217
218            last = int(last_state.get(key, -1))
219            next = int(state.get(key, -1))
220            if last != -1 and next != -1:
221                speed = int(key[len(speed_key):])
222                total_time += next - last
223                total_cycles += (next - last) * speed
224
225        if total_time > 0: cpu_speed[when] = total_cycles / total_time
226        last_state = state
227
228    WriteChartData(
229        ["kHz"], [cpu_speed],
230        os.path.join(files_dir, "cpu_speed.csv"))
231
232    out.append(CHART % {
233        "id": cgi.escape("cpu_speed"),
234        "id_js": json.write("cpu_speed"),
235        "label_html": CPU_SPEED_LABEL,
236        "filename_js": json.write(files_url + "/cpu_speed.csv"),
237        "options_js": json.write({
238            "colors": ["navy"],
239            "dateWindow": date_window,
240            "fillGraph": True,
241            "height": 50,
242            "includeZero": True,
243        }),
244    })
245
246    #
247    # Output total context switch statistics
248    #
249
250    context_switches = {}
251
252    last_state = {}
253    for when, state in sorted_history:
254        last = int(last_state.get("/proc/stat:ctxt", -1))
255        next = int(state.get("/proc/stat:ctxt", -1))
256        if last != -1 and next != -1: context_switches[when] = next - last
257        last_state = state
258
259    WriteChartData(
260        ["switches"], [context_switches],
261        os.path.join(files_dir, "context_switches.csv"))
262
263    total_switches = sum(context_switches.values())
264    out.append(CHART % {
265        "id": cgi.escape("context_switches"),
266        "id_js": json.write("context_switches"),
267        "label_html": CONTEXT_LABEL % {"switches": total_switches},
268        "filename_js": json.write(files_url + "/context_switches.csv"),
269        "options_js": json.write({
270            "colors": ["blue"],
271            "dateWindow": date_window,
272            "fillGraph": True,
273            "height": 50,
274            "includeZero": True,
275        }),
276    })
277
278    #
279    # Collect (no output yet) per-process CPU and major faults
280    #
281
282    process_name = {}
283    process_start = {}
284    process_sys = {}
285    process_sys_user = {}
286
287    process_faults = {}
288    total_faults = {}
289    max_faults = 0
290
291    last_state = {}
292    zero_stat = "0 (zero) Z 0 0 0 0 0 0 0 0 0 0 0 0"
293    for when, state in sorted_history:
294        for key in state:
295            if not key.endswith("/stat"): continue
296
297            last = last_state.get(key, zero_stat).split()
298            next = state.get(key, "").split()
299            if not next: continue
300
301            pid = int(next[0])
302            process_start.setdefault(pid, when)
303            process_name[pid] = next[1][1:-1]
304
305            all = all_jiffies.get(when, 0)
306            if not all: continue
307
308            faults = int(next[11]) - int(last[11])
309            process_faults.setdefault(pid, {})[when] = faults
310            tf = total_faults[when] = total_faults.get(when, 0) + faults
311            max_faults = max(max_faults, tf)
312
313            stime = int(next[14]) - int(last[14])
314            utime = int(next[13]) - int(last[13])
315            process_sys.setdefault(pid, {})[when] = (stime, all)
316            process_sys_user.setdefault(pid, {})[when] = (stime + utime, all)
317
318        last_state = state
319
320    #
321    # Output total major faults (sum over all processes)
322    #
323
324    WriteChartData(
325        ["major"], [total_faults],
326        os.path.join(files_dir, "total_faults.csv"))
327
328    out.append(CHART % {
329        "id": cgi.escape("total_faults"),
330        "id_js": json.write("total_faults"),
331        "label_html": FAULTS_LABEL % {"major": sum(total_faults.values())},
332        "filename_js": json.write(files_url + "/total_faults.csv"),
333        "options_js": json.write({
334            "colors": ["gray"],
335            "dateWindow": date_window,
336            "fillGraph": True,
337            "height": 50,
338            "valueRange": [0, max_faults * 11 / 10],
339        }),
340    })
341
342    #
343    # Output binder transaactions
344    #
345
346    binder_calls = {}
347
348    last_state = {}
349    for when, state in sorted_history:
350        last = int(last_state.get("/proc/binder/stats:BC_TRANSACTION", -1))
351        next = int(state.get("/proc/binder/stats:BC_TRANSACTION", -1))
352        if last != -1 and next != -1: binder_calls[when] = next - last
353        last_state = state
354
355    WriteChartData(
356        ["calls"], [binder_calls],
357        os.path.join(files_dir, "binder_calls.csv"))
358
359    out.append(CHART % {
360        "id": cgi.escape("binder_calls"),
361        "id_js": json.write("binder_calls"),
362        "label_html": BINDER_LABEL % {"calls": sum(binder_calls.values())},
363        "filename_js": json.write(files_url + "/binder_calls.csv"),
364        "options_js": json.write({
365            "colors": ["green"],
366            "dateWindow": date_window,
367            "fillGraph": True,
368            "height": 50,
369            "includeZero": True,
370        })
371    })
372
373    #
374    # Output network interface statistics
375    #
376
377    if out[-1] != SPACER: out.append(SPACER)
378
379    interface_rx = {}
380    interface_tx = {}
381    max_bytes = 0
382
383    last_state = {}
384    for when, state in sorted_history:
385        for key in state:
386            if not key.startswith("/proc/net/dev:"): continue
387
388            last = last_state.get(key, "").split()
389            next = state.get(key, "").split()
390            if not (last and next): continue
391
392            rx = int(next[0]) - int(last[0])
393            tx = int(next[8]) - int(last[8])
394            max_bytes = max(max_bytes, rx, tx)
395
396            net, interface = key.split(":", 1)
397            interface_rx.setdefault(interface, {})[when] = rx
398            interface_tx.setdefault(interface, {})[when] = tx
399
400        last_state = state
401
402    for num, interface in enumerate(sorted(interface_rx.keys())):
403        rx, tx = interface_rx[interface], interface_tx[interface]
404        total_rx, total_tx = sum(rx.values()), sum(tx.values())
405        if not (total_rx or total_tx): continue
406
407        WriteChartData(
408            ["rx", "tx"], [rx, tx],
409            os.path.join(files_dir, "net%d.csv" % num))
410
411        out.append(CHART % {
412            "id": cgi.escape("net%d" % num),
413            "id_js": json.write("net%d" % num),
414            "label_html": NET_LABEL % {
415                "interface": cgi.escape(interface),
416                "rx": total_rx,
417                "tx": total_tx
418            },
419            "filename_js": json.write("%s/net%d.csv" % (files_url, num)),
420            "options_js": json.write({
421                "colors": ["black", "purple"],
422                "dateWindow": date_window,
423                "fillGraph": True,
424                "height": 75,
425                "valueRange": [0, max_bytes * 11 / 10],
426            })
427        })
428
429    #
430    # Output YAFFS statistics
431    #
432
433    if out[-1] != SPACER: out.append(SPACER)
434
435    yaffs_vars = ["nBlockErasures", "nPageReads", "nPageWrites"]
436    partition_ops = {}
437
438    last_state = {}
439    for when, state in sorted_history:
440        for key in state:
441            if not key.startswith("/proc/yaffs:"): continue
442
443            last = int(last_state.get(key, -1))
444            next = int(state.get(key, -1))
445            if last == -1 or next == -1: continue
446
447            value = next - last
448            yaffs, partition, var = key.split(":", 2)
449            ops = partition_ops.setdefault(partition, {})
450            if var in yaffs_vars:
451                ops.setdefault(var, {})[when] = value
452
453        last_state = state
454
455    for num, (partition, ops) in enumerate(sorted(partition_ops.iteritems())):
456        totals = [sum(ops.get(var, {}).values()) for var in yaffs_vars]
457        if not sum(totals): continue
458
459        WriteChartData(
460            yaffs_vars,
461            [ops.get(var, {}) for var in yaffs_vars],
462            os.path.join(files_dir, "yaffs%d.csv" % num))
463
464        values = {"partition": partition}
465        values.update(zip(yaffs_vars, totals))
466        out.append(CHART % {
467            "id": cgi.escape("yaffs%d" % num),
468            "id_js": json.write("yaffs%d" % num),
469            "label_html": YAFFS_LABEL % values,
470            "filename_js": json.write("%s/yaffs%d.csv" % (files_url, num)),
471            "options_js": json.write({
472                "colors": ["maroon", "gray", "teal"],
473                "dateWindow": date_window,
474                "fillGraph": True,
475                "height": 75,
476                "includeZero": True,
477            })
478        })
479
480    #
481    # Output per-process CPU and page faults collected earlier
482    #
483
484    cpu_cutoff = (total_sys + total_user) / 1000
485    faults_cutoff = sum(total_faults.values()) / 200
486    for start, pid in sorted([(s, p) for p, s in process_start.iteritems()]):
487        sys = sum([n for n, d in process_sys.get(pid, {}).values()])
488        sys_user = sum([n for n, d in process_sys_user.get(pid, {}).values()])
489        if sys_user <= cpu_cutoff: continue
490
491        if out[-1] != SPACER: out.append(SPACER)
492
493        WriteChartData(
494            ["sys", "sys+user"],
495            [process_sys.get(pid, {}), process_sys_user.get(pid, {})],
496            os.path.join(files_dir, "proc%d.csv" % pid))
497
498        out.append(CHART % {
499            "id": cgi.escape("proc%d" % pid),
500            "id_js": json.write("proc%d" % pid),
501            "label_html": PROC_CPU_LABEL % {
502                "pid": pid,
503                "process": cgi.escape(process_name.get(pid, "(unknown)")),
504                "sys": sys,
505                "user": sys_user - sys,
506            },
507            "filename_js": json.write("%s/proc%d.csv" % (files_url, pid)),
508            "options_js": json.write({
509                "colors": ["blue", "green"],
510                "dateWindow": date_window,
511                "fillGraph": True,
512                "fractions": True,
513                "height": 75,
514                "valueRange": [0, 110],
515            }),
516        })
517
518        faults = sum(process_faults.get(pid, {}).values())
519        if faults <= faults_cutoff: continue
520
521        WriteChartData(
522            ["major"], [process_faults.get(pid, {})],
523            os.path.join(files_dir, "proc%d_faults.csv" % pid))
524
525        out.append(CHART % {
526            "id": cgi.escape("proc%d_faults" % pid),
527            "id_js": json.write("proc%d_faults" % pid),
528            "label_html": FAULTS_LABEL % {"major": faults},
529            "filename_js": json.write("%s/proc%d_faults.csv" % (files_url, pid)),
530            "options_js": json.write({
531                "colors": ["gray"],
532                "dateWindow": date_window,
533                "fillGraph": True,
534                "height": 50,
535                "valueRange": [0, max_faults * 11 / 10],
536            }),
537        })
538
539    out.append(PAGE_END)
540    file(filename, "w").write("\n".join(out))
541
542
543def main(argv):
544    if len(argv) != 3:
545        print >>sys.stderr, "usage: procstatreport.py procstat.log output.html"
546        return 2
547
548    history = {}
549    current_state = {}
550    scan_time = 0.0
551
552    for line in file(argv[1]):
553        if not line.endswith("\n"): continue
554
555        parts = line.split(None, 2)
556        if len(parts) < 2 or parts[1] not in "+-=":
557            print >>sys.stderr, "Invalid input:", line
558            sys.exit(1)
559
560        name, op = parts[:2]
561
562        if name == "T" and op == "+":  # timestamp: scan about to begin
563            scan_time = float(line[4:])
564            continue
565
566        if name == "T" and op == "-":  # timestamp: scan complete
567            time = (scan_time + float(line[4:])) / 2.0
568            history[time] = dict(current_state)
569
570        elif op == "-":
571            if name in current_state: del current_state[name]
572
573        else:
574            current_state[name] = "".join(parts[2:]).strip()
575
576    if len(history) < 2:
577        print >>sys.stderr, "error: insufficient history to chart"
578        return 1
579
580    WriteOutput(history, argv[1], argv[2])
581
582
583if __name__ == "__main__":
584    sys.exit(main(sys.argv))
585