#! /usr/bin/env python
# bootlog.py - generate an illustration of the Linux boot process
#
# Copyright 2004  Jochen Voss <voss@seehuhn.de>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# $Id: bootlog.py 6039 2004-11-20 13:27:32Z voss $

from sys import argv
from __future__ import division
from matplotlib.matlab import *
from matplotlib.patches import Rectangle
from matplotlib.lines import Line2D
from matplotlib.ticker import MultipleLocator, NullLocator, FormatStrFormatter

fps=10

if len(argv)>1:
    nmax = fps*float(argv[1])
else:
    nmax = fps*75

######################################################################

def parse_perc(s):
    assert s[-1]=="%"
    return float(s[:-1])/100

def parse_mem(s):
    if s[-1]=="k":
        return float(s[:-1])*1024
    elif s[-1]=="m":
        return float(s[:-1])*1024*1024
    else:
        assert str(int(s))==s
        return int(s)

def parse_cpu_line(s):
    w=s.split()
    user=parse_perc(w[1])
    system=parse_perc(w[3])
    nice=parse_perc(w[5])
    idle=parse_perc(w[7])
    wait=parse_perc(w[9])
    hi=parse_perc(w[11])
    si=parse_perc(w[13])
    return user,system,nice,idle,wait,hi,si

def parse_mem_lines(s1,s2):
    w1=s1.split()
    w2=s2.split()
    mem_total=parse_mem(w1[1])
    mem_used=parse_mem(w1[3])
    mem_free=parse_mem(w1[5])
    buffers=parse_mem(w1[7])
    cached=parse_mem(w2[7])
    return mem_total, mem_used, mem_free, buffers, cached

def parse_swap_lines(s1,s2):
    w2=s2.split()
    swap_total=parse_mem(w2[1])
    swap_used=parse_mem(w2[3])
    swap_free=parse_mem(w2[5])
    return swap_total, swap_used, swap_free

def parse_proc_line(s):
    w=s.split()
    pid=int(w[0])
    ppid=int(w[1])
    virt=parse_mem(w[2])
    res=parse_mem(w[3])
    shared=parse_mem(w[4])
    state=w[5]
    cpu=float(w[6])/100
    cmd=" ".join(w[10:])
    return pid,ppid,state,cpu,cmd

######################################################################

class Process:
    def __init__(self, cmd, pid, ppid, start):
        self.states=[]
        self.cpu=[]
        if cmd.startswith("/bin/sh -e "): cmd=cmd.replace("/bin/sh -e ","",1)
        if cmd.startswith("/bin/sh -c "): cmd=cmd.replace("/bin/sh -c ","",1)
        if cmd.startswith("/bin/sh "): cmd=cmd.replace("/bin/sh ","",1)
        if cmd.startswith("./"): cmd=cmd[2:]
        n=cmd.find(" ")
        if n>=0: cmd=cmd[:n]
        n=cmd.rfind("/")
        if n>=0 and not (cmd.startswith("[") or cmd.startswith("/top")):
            cmd=cmd[n+1:]
        self.cmd=cmd
        self.pid=pid
        self.ppid=ppid
        self.start=start
        self.stop=start-1

######################################################################

n=0
data_time=[]
data_load=[]
data_io=[]
data_mem_used=[]
data_mem_cached=[]
procdata={}
piddata={}

def parse_frame(f):
    global n
    if not f: return
    
    data_time.append(n/fps)
    user,system,nice,idle,wait,hi,si = parse_cpu_line(f[2])
    data_load.append(user+system)
    data_io.append(wait)

    mem_total, mem_used, mem_free, buffers, cached = parse_mem_lines(f[3],f[4])
    data_mem_used.append(mem_used)
    data_mem_cached.append(cached+buffers)

    swap_total, swap_used, swap_free = parse_swap_lines(f[3],f[4])
    assert swap_used==0

    piddata[n]=[]
    for l in f[7:]:
        if not l: continue
        pid,ppid,state,cpu,cmd = parse_proc_line(l)
        piddata[n].append(pid)
        if not pid in procdata:
            procdata[pid] = Process(cmd,pid,ppid,n)
        p=procdata[pid]
        p.states.append(state)
        p.cpu.append(cpu)
        assert p.stop==n-1
        p.stop=n
    n+=1

frame=[]
for l in open("bootlog").readlines():
    l=l.rstrip()
    if l.startswith("top - "):
        parse_frame(frame)
        frame=[]
        if n>=nmax: break
    frame.append(l)

######################################################################

pids=procdata.keys()
pids.sort()
procs=[procdata[pid] for pid in pids]

def slot_busy(slot,p,sl,gap=fps):
    """Return True, iff SLOT is unavailable for P under layout SL"""
    start=p.start
    stop=p.stop
    tenants=[procdata[pid] for pid in sl if sl[pid]==slot]
    for q in tenants:
        if not ((q.stop+gap < start and q.start+1.2*len(q.cmd) < start)
                or (stop+gap < q.start and start+1.2*len(p.cmd) < q.start)):
            return True
    return False

def layout_collides(sl, slc, offset):
    for pid in slc:
        if slot_busy(slc[pid]+offset,procdata[pid],sl): return True
    return False

def calculate_layout(p):
    sl={}
    sl[p.pid]=0
    
    children=[q for q in procs if q.ppid==p.pid]
    children.reverse()
    for child in children:
        if child.cmd.startswith("[") or child.cmd.startswith("/top"):
            continue
        slc=calculate_layout(child)
        offset=1
        while layout_collides(sl,slc,offset):
            offset += 1
        for pid in slc:
            sl[pid]=slc[pid]+offset
    return sl

init=procdata[1]
sl=calculate_layout(init)

#maxslot=max([procdata[pid].slot for pid in pids])
maxslot=0
for pid in pids[1:]:
    if pid in sl:
        if sl[pid] > maxslot: maxslot=sl[pid]
    else:
        p=procdata[pid]
        print "ignoring "+p.cmd

######################################################################

figure(figsize=(26,15))

axes([0.05, 0.85, 0.93, 0.1])
title("memory usage (bytes)")
ax=gca()
ax.xaxis.set_minor_locator(MultipleLocator(1))
ax.xaxis.set_major_locator(MultipleLocator(5))
ax.xaxis.grid(True, which="minor")
ax.xaxis.grid(True, which="major")
ax.xaxis.set_major_formatter(FormatStrFormatter('%ds'))
ax.yaxis.grid(False)
x=map(lambda x,y:x-y,data_mem_used,data_mem_cached)
semilogy(data_time,x,"g",basey=2)
l=gca().get_xlim()

axes([0.05, 0.7, 0.93, 0.1])
grid(True)
ax=gca()
ax.xaxis.set_minor_locator(MultipleLocator(1))
ax.xaxis.set_major_locator(MultipleLocator(5))
ax.xaxis.grid(True, which="minor")
ax.xaxis.set_major_formatter(FormatStrFormatter('%ds'))
ax.yaxis.grid(False)
title("CPU usage (blue=user+sys, red=I/O wait)")
x=map(lambda x,y:x+y,data_load,data_io)
fill([0]+data_time+[data_time[-1]],[0]+x+[0],
     facecolor=(0.96,0.39,0.47))
fill([0]+data_time+[data_time[-1]],[0]+data_load+[0],
     facecolor=(0.64,0.27,0.93))
axis([l[0],l[1],0,1])

axes([0.05, 0.05, 0.93, 0.6])
axis([l[0],l[1],0,maxslot+1])
grid(True)
ax=gca()
ax.xaxis.set_minor_locator(MultipleLocator(1))
ax.xaxis.set_major_locator(MultipleLocator(5))
ax.xaxis.grid(True, which="minor")
ax.xaxis.set_major_formatter(FormatStrFormatter('%ds'))
ax.yaxis.set_major_locator(NullLocator())

for pid in pids:
    p=procdata[pid]
    if pid>1 and pid not in sl: continue
    rect = Rectangle((p.start/fps, sl[pid]), (p.stop-p.start+1)/fps, 0.85,
                     fill=False)
    ax.add_patch(rect)
    for i in range(p.start,p.stop+1):
        state=p.states[i-p.start]
        cpu=p.cpu[i-p.start]
        if state=="D":
            col=0.7
        elif state=="S":
            col=0.93
        elif state=="Z":
            col=0.4
        elif state=="R":
            col=(cpu*0.94 + (1-cpu)*0.98,
                 cpu*0.88 + (1-cpu)*1,
                 cpu*0.0 + (1-cpu)*0.67)
        else:
            print state
            assert(0)
        rect = Rectangle((i/fps, sl[pid]), 1/fps, 0.85,
                         linewidth=0, fill=True, fc=col)
        ax.add_patch(rect)
        

for pid in pids:
    p=procdata[pid]
    if pid not in sl: continue
    if not p.ppid: continue
    x=p.start/fps
    y1=sl[pid]+0.5
    y0=sl[p.ppid]+0.5
    l = Line2D([x+0.06,x],[y0,y1],color="r")
    ax.add_line(l)

for pid in pids:
    p=procdata[pid]
    if pid>1 and pid not in sl: continue
    text(p.start/fps+0.1,sl[pid]+0.5,p.cmd,
         verticalalignment='center',fontsize=8)

savefig("bootlog.png")
#show()
