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