torvalds/linux

View on GitHub
tools/power/pm-graph/bootgraph.py

Summary

Maintainability
F
1 wk
Test Coverage
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only
#
# Tool for analyzing boot timing
# Copyright (c) 2013, Intel Corporation.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms and conditions of the GNU General Public License,
# version 2, as published by the Free Software Foundation.
#
# This program is distributed in the hope 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.
#
# Authors:
#     Todd Brandt <todd.e.brandt@linux.intel.com>
#
# Description:
#     This tool is designed to assist kernel and OS developers in optimizing
#     their linux stack's boot time. It creates an html representation of
#     the kernel boot timeline up to the start of the init process.
#

# ----------------- LIBRARIES --------------------

import sys
import time
import os
import string
import re
import platform
import shutil
from datetime import datetime, timedelta
from subprocess import call, Popen, PIPE
import sleepgraph as aslib

def pprint(msg):
    print(msg)
    sys.stdout.flush()

# ----------------- CLASSES --------------------

# Class: SystemValues
# Description:
#     A global, single-instance container used to
#     store system values and test parameters
class SystemValues(aslib.SystemValues):
    title = 'BootGraph'
    version = '2.2'
    hostname = 'localhost'
    testtime = ''
    kernel = ''
    dmesgfile = ''
    ftracefile = ''
    htmlfile = 'bootgraph.html'
    testdir = ''
    kparams = ''
    result = ''
    useftrace = False
    usecallgraph = False
    suspendmode = 'boot'
    max_graph_depth = 2
    graph_filter = 'do_one_initcall'
    reboot = False
    manual = False
    iscronjob = False
    timeformat = '%.6f'
    bootloader = 'grub'
    blexec = []
    def __init__(self):
        self.kernel, self.hostname = 'unknown', platform.node()
        self.testtime = datetime.now().strftime('%Y-%m-%d_%H:%M:%S')
        if os.path.exists('/proc/version'):
            fp = open('/proc/version', 'r')
            self.kernel = self.kernelVersion(fp.read().strip())
            fp.close()
        self.testdir = datetime.now().strftime('boot-%y%m%d-%H%M%S')
    def kernelVersion(self, msg):
        m = re.match('^[Ll]inux *[Vv]ersion *(?P<v>\S*) .*', msg)
        if m:
            return m.group('v')
        return 'unknown'
    def checkFtraceKernelVersion(self):
        m = re.match('^(?P<x>[0-9]*)\.(?P<y>[0-9]*)\.(?P<z>[0-9]*).*', self.kernel)
        if m:
            val = tuple(map(int, m.groups()))
            if val >= (4, 10, 0):
                return True
        return False
    def kernelParams(self):
        cmdline = 'initcall_debug log_buf_len=32M'
        if self.useftrace:
            if self.cpucount > 0:
                bs = min(self.memtotal // 2, 2*1024*1024) // self.cpucount
            else:
                bs = 131072
            cmdline += ' trace_buf_size=%dK trace_clock=global '\
            'trace_options=nooverwrite,funcgraph-abstime,funcgraph-cpu,'\
            'funcgraph-duration,funcgraph-proc,funcgraph-tail,'\
            'nofuncgraph-overhead,context-info,graph-time '\
            'ftrace=function_graph '\
            'ftrace_graph_max_depth=%d '\
            'ftrace_graph_filter=%s' % \
                (bs, self.max_graph_depth, self.graph_filter)
        return cmdline
    def setGraphFilter(self, val):
        master = self.getBootFtraceFilterFunctions()
        fs = ''
        for i in val.split(','):
            func = i.strip()
            if func == '':
                doError('badly formatted filter function string')
            if '[' in func or ']' in func:
                doError('loadable module functions not allowed - "%s"' % func)
            if ' ' in func:
                doError('spaces found in filter functions - "%s"' % func)
            if func not in master:
                doError('function "%s" not available for ftrace' % func)
            if not fs:
                fs = func
            else:
                fs += ','+func
        if not fs:
            doError('badly formatted filter function string')
        self.graph_filter = fs
    def getBootFtraceFilterFunctions(self):
        self.rootCheck(True)
        fp = open(self.tpath+'available_filter_functions')
        fulllist = fp.read().split('\n')
        fp.close()
        list = []
        for i in fulllist:
            if not i or ' ' in i or '[' in i or ']' in i:
                continue
            list.append(i)
        return list
    def myCronJob(self, line):
        if '@reboot' not in line:
            return False
        if 'bootgraph' in line or 'analyze_boot.py' in line or '-cronjob' in line:
            return True
        return False
    def cronjobCmdString(self):
        cmdline = '%s -cronjob' % os.path.abspath(sys.argv[0])
        args = iter(sys.argv[1:])
        for arg in args:
            if arg in ['-h', '-v', '-cronjob', '-reboot', '-verbose']:
                continue
            elif arg in ['-o', '-dmesg', '-ftrace', '-func']:
                next(args)
                continue
            elif arg == '-result':
                cmdline += ' %s "%s"' % (arg, os.path.abspath(next(args)))
                continue
            elif arg == '-cgskip':
                file = self.configFile(next(args))
                cmdline += ' %s "%s"' % (arg, os.path.abspath(file))
                continue
            cmdline += ' '+arg
        if self.graph_filter != 'do_one_initcall':
            cmdline += ' -func "%s"' % self.graph_filter
        cmdline += ' -o "%s"' % os.path.abspath(self.testdir)
        return cmdline
    def manualRebootRequired(self):
        cmdline = self.kernelParams()
        pprint('To generate a new timeline manually, follow these steps:\n\n'\
        '1. Add the CMDLINE string to your kernel command line.\n'\
        '2. Reboot the system.\n'\
        '3. After reboot, re-run this tool with the same arguments but no command (w/o -reboot or -manual).\n\n'\
        'CMDLINE="%s"' % cmdline)
        sys.exit()
    def blGrub(self):
        blcmd = ''
        for cmd in ['update-grub', 'grub-mkconfig', 'grub2-mkconfig']:
            if blcmd:
                break
            blcmd = self.getExec(cmd)
        if not blcmd:
            doError('[GRUB] missing update command')
        if not os.path.exists('/etc/default/grub'):
            doError('[GRUB] missing /etc/default/grub')
        if 'grub2' in blcmd:
            cfg = '/boot/grub2/grub.cfg'
        else:
            cfg = '/boot/grub/grub.cfg'
        if not os.path.exists(cfg):
            doError('[GRUB] missing %s' % cfg)
        if 'update-grub' in blcmd:
            self.blexec = [blcmd]
        else:
            self.blexec = [blcmd, '-o', cfg]
    def getBootLoader(self):
        if self.bootloader == 'grub':
            self.blGrub()
        else:
            doError('unknown boot loader: %s' % self.bootloader)
    def writeDatafileHeader(self, filename):
        self.kparams = open('/proc/cmdline', 'r').read().strip()
        fp = open(filename, 'w')
        fp.write(self.teststamp+'\n')
        fp.write(self.sysstamp+'\n')
        fp.write('# command | %s\n' % self.cmdline)
        fp.write('# kparams | %s\n' % self.kparams)
        fp.close()

sysvals = SystemValues()

# Class: Data
# Description:
#     The primary container for test data.
class Data(aslib.Data):
    dmesg = {}  # root data structure
    start = 0.0 # test start
    end = 0.0   # test end
    dmesgtext = []   # dmesg text file in memory
    testnumber = 0
    idstr = ''
    html_device_id = 0
    valid = False
    tUserMode = 0.0
    boottime = ''
    phases = ['kernel', 'user']
    do_one_initcall = False
    def __init__(self, num):
        self.testnumber = num
        self.idstr = 'a'
        self.dmesgtext = []
        self.dmesg = {
            'kernel': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
                'order': 0, 'color': 'linear-gradient(to bottom, #fff, #bcf)'},
            'user': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
                'order': 1, 'color': '#fff'}
        }
    def deviceTopology(self):
        return ''
    def newAction(self, phase, name, pid, start, end, ret, ulen):
        # new device callback for a specific phase
        self.html_device_id += 1
        devid = '%s%d' % (self.idstr, self.html_device_id)
        list = self.dmesg[phase]['list']
        length = -1.0
        if(start >= 0 and end >= 0):
            length = end - start
        i = 2
        origname = name
        while(name in list):
            name = '%s[%d]' % (origname, i)
            i += 1
        list[name] = {'name': name, 'start': start, 'end': end,
            'pid': pid, 'length': length, 'row': 0, 'id': devid,
            'ret': ret, 'ulen': ulen }
        return name
    def deviceMatch(self, pid, cg):
        if cg.end - cg.start == 0:
            return ''
        for p in data.phases:
            list = self.dmesg[p]['list']
            for devname in list:
                dev = list[devname]
                if pid != dev['pid']:
                    continue
                if cg.name == 'do_one_initcall':
                    if(cg.start <= dev['start'] and cg.end >= dev['end'] and dev['length'] > 0):
                        dev['ftrace'] = cg
                        self.do_one_initcall = True
                        return devname
                else:
                    if(cg.start > dev['start'] and cg.end < dev['end']):
                        if 'ftraces' not in dev:
                            dev['ftraces'] = []
                        dev['ftraces'].append(cg)
                        return devname
        return ''
    def printDetails(self):
        sysvals.vprint('Timeline Details:')
        sysvals.vprint('          Host: %s' % sysvals.hostname)
        sysvals.vprint('        Kernel: %s' % sysvals.kernel)
        sysvals.vprint('     Test time: %s' % sysvals.testtime)
        sysvals.vprint('     Boot time: %s' % self.boottime)
        for phase in self.phases:
            dc = len(self.dmesg[phase]['list'])
            sysvals.vprint('%9s mode: %.3f - %.3f (%d initcalls)' % (phase,
                self.dmesg[phase]['start']*1000,
                self.dmesg[phase]['end']*1000, dc))

# ----------------- FUNCTIONS --------------------

# Function: parseKernelLog
# Description:
#     parse a kernel log for boot data
def parseKernelLog():
    sysvals.vprint('Analyzing the dmesg data (%s)...' % \
        os.path.basename(sysvals.dmesgfile))
    phase = 'kernel'
    data = Data(0)
    data.dmesg['kernel']['start'] = data.start = ktime = 0.0
    sysvals.stamp = {
        'time': datetime.now().strftime('%B %d %Y, %I:%M:%S %p'),
        'host': sysvals.hostname,
        'mode': 'boot', 'kernel': ''}

    tp = aslib.TestProps()
    devtemp = dict()
    if(sysvals.dmesgfile):
        lf = open(sysvals.dmesgfile, 'rb')
    else:
        lf = Popen('dmesg', stdout=PIPE).stdout
    for line in lf:
        line = aslib.ascii(line).replace('\r\n', '')
        # grab the stamp and sysinfo
        if re.match(tp.stampfmt, line):
            tp.stamp = line
            continue
        elif re.match(tp.sysinfofmt, line):
            tp.sysinfo = line
            continue
        elif re.match(tp.cmdlinefmt, line):
            tp.cmdline = line
            continue
        elif re.match(tp.kparamsfmt, line):
            tp.kparams = line
            continue
        idx = line.find('[')
        if idx > 1:
            line = line[idx:]
        m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
        if(not m):
            continue
        ktime = float(m.group('ktime'))
        if(ktime > 120):
            break
        msg = m.group('msg')
        data.dmesgtext.append(line)
        if(ktime == 0.0 and re.match('^Linux version .*', msg)):
            if(not sysvals.stamp['kernel']):
                sysvals.stamp['kernel'] = sysvals.kernelVersion(msg)
            continue
        m = re.match('.* setting system clock to (?P<d>[0-9\-]*)[ A-Z](?P<t>[0-9:]*) UTC.*', msg)
        if(m):
            bt = datetime.strptime(m.group('d')+' '+m.group('t'), '%Y-%m-%d %H:%M:%S')
            bt = bt - timedelta(seconds=int(ktime))
            data.boottime = bt.strftime('%Y-%m-%d_%H:%M:%S')
            sysvals.stamp['time'] = bt.strftime('%B %d %Y, %I:%M:%S %p')
            continue
        m = re.match('^calling *(?P<f>.*)\+.* @ (?P<p>[0-9]*)', msg)
        if(m):
            func = m.group('f')
            pid = int(m.group('p'))
            devtemp[func] = (ktime, pid)
            continue
        m = re.match('^initcall *(?P<f>.*)\+.* returned (?P<r>.*) after (?P<t>.*) usecs', msg)
        if(m):
            data.valid = True
            data.end = ktime
            f, r, t = m.group('f', 'r', 't')
            if(f in devtemp):
                start, pid = devtemp[f]
                data.newAction(phase, f, pid, start, ktime, int(r), int(t))
                del devtemp[f]
            continue
        if(re.match('^Freeing unused kernel .*', msg)):
            data.tUserMode = ktime
            data.dmesg['kernel']['end'] = ktime
            data.dmesg['user']['start'] = ktime
            phase = 'user'

    if tp.stamp:
        sysvals.stamp = 0
        tp.parseStamp(data, sysvals)
    data.dmesg['user']['end'] = data.end
    lf.close()
    return data

# Function: parseTraceLog
# Description:
#     Check if trace is available and copy to a temp file
def parseTraceLog(data):
    sysvals.vprint('Analyzing the ftrace data (%s)...' % \
        os.path.basename(sysvals.ftracefile))
    # if available, calculate cgfilter allowable ranges
    cgfilter = []
    if len(sysvals.cgfilter) > 0:
        for p in data.phases:
            list = data.dmesg[p]['list']
            for i in sysvals.cgfilter:
                if i in list:
                    cgfilter.append([list[i]['start']-0.0001,
                        list[i]['end']+0.0001])
    # parse the trace log
    ftemp = dict()
    tp = aslib.TestProps()
    tp.setTracerType('function_graph')
    tf = open(sysvals.ftracefile, 'r')
    for line in tf:
        if line[0] == '#':
            continue
        m = re.match(tp.ftrace_line_fmt, line.strip())
        if(not m):
            continue
        m_time, m_proc, m_pid, m_msg, m_dur = \
            m.group('time', 'proc', 'pid', 'msg', 'dur')
        t = float(m_time)
        if len(cgfilter) > 0:
            allow = False
            for r in cgfilter:
                if t >= r[0] and t < r[1]:
                    allow = True
                    break
            if not allow:
                continue
        if t > data.end:
            break
        if(m_time and m_pid and m_msg):
            t = aslib.FTraceLine(m_time, m_msg, m_dur)
            pid = int(m_pid)
        else:
            continue
        if t.fevent or t.fkprobe:
            continue
        key = (m_proc, pid)
        if(key not in ftemp):
            ftemp[key] = []
            ftemp[key].append(aslib.FTraceCallGraph(pid, sysvals))
        cg = ftemp[key][-1]
        res = cg.addLine(t)
        if(res != 0):
            ftemp[key].append(aslib.FTraceCallGraph(pid, sysvals))
        if(res == -1):
            ftemp[key][-1].addLine(t)

    tf.close()

    # add the callgraph data to the device hierarchy
    for key in ftemp:
        proc, pid = key
        for cg in ftemp[key]:
            if len(cg.list) < 1 or cg.invalid or (cg.end - cg.start == 0):
                continue
            if(not cg.postProcess()):
                pprint('Sanity check failed for %s-%d' % (proc, pid))
                continue
            # match cg data to devices
            devname = data.deviceMatch(pid, cg)
            if not devname:
                kind = 'Orphan'
                if cg.partial:
                    kind = 'Partial'
                sysvals.vprint('%s callgraph found for %s %s-%d [%f - %f]' %\
                    (kind, cg.name, proc, pid, cg.start, cg.end))
            elif len(cg.list) > 1000000:
                pprint('WARNING: the callgraph found for %s is massive! (%d lines)' %\
                    (devname, len(cg.list)))

# Function: retrieveLogs
# Description:
#     Create copies of dmesg and/or ftrace for later processing
def retrieveLogs():
    # check ftrace is configured first
    if sysvals.useftrace:
        tracer = sysvals.fgetVal('current_tracer').strip()
        if tracer != 'function_graph':
            doError('ftrace not configured for a boot callgraph')
    # create the folder and get dmesg
    sysvals.systemInfo(aslib.dmidecode(sysvals.mempath))
    sysvals.initTestOutput('boot')
    sysvals.writeDatafileHeader(sysvals.dmesgfile)
    call('dmesg >> '+sysvals.dmesgfile, shell=True)
    if not sysvals.useftrace:
        return
    # get ftrace
    sysvals.writeDatafileHeader(sysvals.ftracefile)
    call('cat '+sysvals.tpath+'trace >> '+sysvals.ftracefile, shell=True)

# Function: colorForName
# Description:
#     Generate a repeatable color from a list for a given name
def colorForName(name):
    list = [
        ('c1', '#ec9999'),
        ('c2', '#ffc1a6'),
        ('c3', '#fff0a6'),
        ('c4', '#adf199'),
        ('c5', '#9fadea'),
        ('c6', '#a699c1'),
        ('c7', '#ad99b4'),
        ('c8', '#eaffea'),
        ('c9', '#dcecfb'),
        ('c10', '#ffffea')
    ]
    i = 0
    total = 0
    count = len(list)
    while i < len(name):
        total += ord(name[i])
        i += 1
    return list[total % count]

def cgOverview(cg, minlen):
    stats = dict()
    large = []
    for l in cg.list:
        if l.fcall and l.depth == 1:
            if l.length >= minlen:
                large.append(l)
            if l.name not in stats:
                stats[l.name] = [0, 0.0]
            stats[l.name][0] += (l.length * 1000.0)
            stats[l.name][1] += 1
    return (large, stats)

# Function: createBootGraph
# Description:
#     Create the output html file from the resident test data
# Arguments:
#     testruns: array of Data objects from parseKernelLog or parseTraceLog
# Output:
#     True if the html file was created, false if it failed
def createBootGraph(data):
    # html function templates
    html_srccall = '<div id={6} title="{5}" class="srccall" style="left:{1}%;top:{2}px;height:{3}px;width:{4}%;line-height:{3}px;">{0}</div>\n'
    html_timetotal = '<table class="time1">\n<tr>'\
        '<td class="blue">Init process starts @ <b>{0} ms</b></td>'\
        '<td class="blue">Last initcall ends @ <b>{1} ms</b></td>'\
        '</tr>\n</table>\n'

    # device timeline
    devtl = aslib.Timeline(100, 20)

    # write the test title and general info header
    devtl.createHeader(sysvals, sysvals.stamp)

    # Generate the header for this timeline
    t0 = data.start
    tMax = data.end
    tTotal = tMax - t0
    if(tTotal == 0):
        pprint('ERROR: No timeline data')
        return False
    user_mode = '%.0f'%(data.tUserMode*1000)
    last_init = '%.0f'%(tTotal*1000)
    devtl.html += html_timetotal.format(user_mode, last_init)

    # determine the maximum number of rows we need to draw
    devlist = []
    for p in data.phases:
        list = data.dmesg[p]['list']
        for devname in list:
            d = aslib.DevItem(0, p, list[devname])
            devlist.append(d)
        devtl.getPhaseRows(devlist, 0, 'start')
    devtl.calcTotalRows()

    # draw the timeline background
    devtl.createZoomBox()
    devtl.html += devtl.html_tblock.format('boot', '0', '100', devtl.scaleH)
    for p in data.phases:
        phase = data.dmesg[p]
        length = phase['end']-phase['start']
        left = '%.3f' % (((phase['start']-t0)*100.0)/tTotal)
        width = '%.3f' % ((length*100.0)/tTotal)
        devtl.html += devtl.html_phase.format(left, width, \
            '%.3f'%devtl.scaleH, '%.3f'%devtl.bodyH, \
            phase['color'], '')

    # draw the device timeline
    num = 0
    devstats = dict()
    for phase in data.phases:
        list = data.dmesg[phase]['list']
        for devname in sorted(list):
            cls, color = colorForName(devname)
            dev = list[devname]
            info = '@|%.3f|%.3f|%.3f|%d' % (dev['start']*1000.0, dev['end']*1000.0,
                dev['ulen']/1000.0, dev['ret'])
            devstats[dev['id']] = {'info':info}
            dev['color'] = color
            height = devtl.phaseRowHeight(0, phase, dev['row'])
            top = '%.6f' % ((dev['row']*height) + devtl.scaleH)
            left = '%.6f' % (((dev['start']-t0)*100)/tTotal)
            width = '%.6f' % (((dev['end']-dev['start'])*100)/tTotal)
            length = ' (%0.3f ms) ' % ((dev['end']-dev['start'])*1000)
            devtl.html += devtl.html_device.format(dev['id'],
                devname+length+phase+'_mode', left, top, '%.3f'%height,
                width, devname, ' '+cls, '')
            rowtop = devtl.phaseRowTop(0, phase, dev['row'])
            height = '%.6f' % (devtl.rowH / 2)
            top = '%.6f' % (rowtop + devtl.scaleH + (devtl.rowH / 2))
            if data.do_one_initcall:
                if('ftrace' not in dev):
                    continue
                cg = dev['ftrace']
                large, stats = cgOverview(cg, 0.001)
                devstats[dev['id']]['fstat'] = stats
                for l in large:
                    left = '%f' % (((l.time-t0)*100)/tTotal)
                    width = '%f' % (l.length*100/tTotal)
                    title = '%s (%0.3fms)' % (l.name, l.length * 1000.0)
                    devtl.html += html_srccall.format(l.name, left,
                        top, height, width, title, 'x%d'%num)
                    num += 1
                continue
            if('ftraces' not in dev):
                continue
            for cg in dev['ftraces']:
                left = '%f' % (((cg.start-t0)*100)/tTotal)
                width = '%f' % ((cg.end-cg.start)*100/tTotal)
                cglen = (cg.end - cg.start) * 1000.0
                title = '%s (%0.3fms)' % (cg.name, cglen)
                cg.id = 'x%d' % num
                devtl.html += html_srccall.format(cg.name, left,
                    top, height, width, title, dev['id']+cg.id)
                num += 1

    # draw the time scale, try to make the number of labels readable
    devtl.createTimeScale(t0, tMax, tTotal, 'boot')
    devtl.html += '</div>\n'

    # timeline is finished
    devtl.html += '</div>\n</div>\n'

    # draw a legend which describes the phases by color
    devtl.html += '<div class="legend">\n'
    pdelta = 20.0
    pmargin = 36.0
    for phase in data.phases:
        order = '%.2f' % ((data.dmesg[phase]['order'] * pdelta) + pmargin)
        devtl.html += devtl.html_legend.format(order, \
            data.dmesg[phase]['color'], phase+'_mode', phase[0])
    devtl.html += '</div>\n'

    hf = open(sysvals.htmlfile, 'w')

    # add the css
    extra = '\
        .c1 {background:rgba(209,0,0,0.4);}\n\
        .c2 {background:rgba(255,102,34,0.4);}\n\
        .c3 {background:rgba(255,218,33,0.4);}\n\
        .c4 {background:rgba(51,221,0,0.4);}\n\
        .c5 {background:rgba(17,51,204,0.4);}\n\
        .c6 {background:rgba(34,0,102,0.4);}\n\
        .c7 {background:rgba(51,0,68,0.4);}\n\
        .c8 {background:rgba(204,255,204,0.4);}\n\
        .c9 {background:rgba(169,208,245,0.4);}\n\
        .c10 {background:rgba(255,255,204,0.4);}\n\
        .vt {transform:rotate(-60deg);transform-origin:0 0;}\n\
        table.fstat {table-layout:fixed;padding:150px 15px 0 0;font-size:10px;column-width:30px;}\n\
        .fstat th {width:55px;}\n\
        .fstat td {text-align:left;width:35px;}\n\
        .srccall {position:absolute;font-size:10px;z-index:7;overflow:hidden;color:black;text-align:center;white-space:nowrap;border-radius:5px;border:1px solid black;background:linear-gradient(to bottom right,#CCC,#969696);}\n\
        .srccall:hover {color:white;font-weight:bold;border:1px solid white;}\n'
    aslib.addCSS(hf, sysvals, 1, False, extra)

    # write the device timeline
    hf.write(devtl.html)

    # add boot specific html
    statinfo = 'var devstats = {\n'
    for n in sorted(devstats):
        statinfo += '\t"%s": [\n\t\t"%s",\n' % (n, devstats[n]['info'])
        if 'fstat' in devstats[n]:
            funcs = devstats[n]['fstat']
            for f in sorted(funcs, key=lambda k:(funcs[k], k), reverse=True):
                if funcs[f][0] < 0.01 and len(funcs) > 10:
                    break
                statinfo += '\t\t"%f|%s|%d",\n' % (funcs[f][0], f, funcs[f][1])
        statinfo += '\t],\n'
    statinfo += '};\n'
    html = \
        '<div id="devicedetailtitle"></div>\n'\
        '<div id="devicedetail" style="display:none;">\n'\
        '<div id="devicedetail0">\n'
    for p in data.phases:
        phase = data.dmesg[p]
        html += devtl.html_phaselet.format(p+'_mode', '0', '100', phase['color'])
    html += '</div>\n</div>\n'\
        '<script type="text/javascript">\n'+statinfo+\
        '</script>\n'
    hf.write(html)

    # add the callgraph html
    if(sysvals.usecallgraph):
        aslib.addCallgraphs(sysvals, hf, data)

    # add the test log as a hidden div
    if sysvals.testlog and sysvals.logmsg:
        hf.write('<div id="testlog" style="display:none;">\n'+sysvals.logmsg+'</div>\n')
    # add the dmesg log as a hidden div
    if sysvals.dmesglog:
        hf.write('<div id="dmesglog" style="display:none;">\n')
        for line in data.dmesgtext:
            line = line.replace('<', '&lt').replace('>', '&gt')
            hf.write(line)
        hf.write('</div>\n')

    # write the footer and close
    aslib.addScriptCode(hf, [data])
    hf.write('</body>\n</html>\n')
    hf.close()
    return True

# Function: updateCron
# Description:
#    (restore=False) Set the tool to run automatically on reboot
#    (restore=True) Restore the original crontab
def updateCron(restore=False):
    if not restore:
        sysvals.rootUser(True)
    crondir = '/var/spool/cron/crontabs/'
    if not os.path.exists(crondir):
        crondir = '/var/spool/cron/'
    if not os.path.exists(crondir):
        doError('%s not found' % crondir)
    cronfile = crondir+'root'
    backfile = crondir+'root-analyze_boot-backup'
    cmd = sysvals.getExec('crontab')
    if not cmd:
        doError('crontab not found')
    # on restore: move the backup cron back into place
    if restore:
        if os.path.exists(backfile):
            shutil.move(backfile, cronfile)
            call([cmd, cronfile])
        return
    # backup current cron and install new one with reboot
    if os.path.exists(cronfile):
        shutil.move(cronfile, backfile)
    else:
        fp = open(backfile, 'w')
        fp.close()
    res = -1
    try:
        fp = open(backfile, 'r')
        op = open(cronfile, 'w')
        for line in fp:
            if not sysvals.myCronJob(line):
                op.write(line)
                continue
        fp.close()
        op.write('@reboot python %s\n' % sysvals.cronjobCmdString())
        op.close()
        res = call([cmd, cronfile])
    except Exception as e:
        pprint('Exception: %s' % str(e))
        shutil.move(backfile, cronfile)
        res = -1
    if res != 0:
        doError('crontab failed')

# Function: updateGrub
# Description:
#     update grub.cfg for all kernels with our parameters
def updateGrub(restore=False):
    # call update-grub on restore
    if restore:
        try:
            call(sysvals.blexec, stderr=PIPE, stdout=PIPE,
                env={'PATH': '.:/sbin:/usr/sbin:/usr/bin:/sbin:/bin'})
        except Exception as e:
            pprint('Exception: %s\n' % str(e))
        return
    # extract the option and create a grub config without it
    sysvals.rootUser(True)
    tgtopt = 'GRUB_CMDLINE_LINUX_DEFAULT'
    cmdline = ''
    grubfile = '/etc/default/grub'
    tempfile = '/etc/default/grub.analyze_boot'
    shutil.move(grubfile, tempfile)
    res = -1
    try:
        fp = open(tempfile, 'r')
        op = open(grubfile, 'w')
        cont = False
        for line in fp:
            line = line.strip()
            if len(line) == 0 or line[0] == '#':
                continue
            opt = line.split('=')[0].strip()
            if opt == tgtopt:
                cmdline = line.split('=', 1)[1].strip('\\')
                if line[-1] == '\\':
                    cont = True
            elif cont:
                cmdline += line.strip('\\')
                if line[-1] != '\\':
                    cont = False
            else:
                op.write('%s\n' % line)
        fp.close()
        # if the target option value is in quotes, strip them
        sp = '"'
        val = cmdline.strip()
        if val and (val[0] == '\'' or val[0] == '"'):
            sp = val[0]
            val = val.strip(sp)
        cmdline = val
        # append our cmd line options
        if len(cmdline) > 0:
            cmdline += ' '
        cmdline += sysvals.kernelParams()
        # write out the updated target option
        op.write('\n%s=%s%s%s\n' % (tgtopt, sp, cmdline, sp))
        op.close()
        res = call(sysvals.blexec)
        os.remove(grubfile)
    except Exception as e:
        pprint('Exception: %s' % str(e))
        res = -1
    # cleanup
    shutil.move(tempfile, grubfile)
    if res != 0:
        doError('update grub failed')

# Function: updateKernelParams
# Description:
#     update boot conf for all kernels with our parameters
def updateKernelParams(restore=False):
    # find the boot loader
    sysvals.getBootLoader()
    if sysvals.bootloader == 'grub':
        updateGrub(restore)

# Function: doError Description:
#     generic error function for catastrphic failures
# Arguments:
#     msg: the error message to print
#     help: True if printHelp should be called after, False otherwise
def doError(msg, help=False):
    if help == True:
        printHelp()
    pprint('ERROR: %s\n' % msg)
    sysvals.outputResult({'error':msg})
    sys.exit()

# Function: printHelp
# Description:
#     print out the help text
def printHelp():
    pprint('\n%s v%s\n'\
    'Usage: bootgraph <options> <command>\n'\
    '\n'\
    'Description:\n'\
    '  This tool reads in a dmesg log of linux kernel boot and\n'\
    '  creates an html representation of the boot timeline up to\n'\
    '  the start of the init process.\n'\
    '\n'\
    '  If no specific command is given the tool reads the current dmesg\n'\
    '  and/or ftrace log and creates a timeline\n'\
    '\n'\
    '  Generates output files in subdirectory: boot-yymmdd-HHMMSS\n'\
    '   HTML output:                    <hostname>_boot.html\n'\
    '   raw dmesg output:               <hostname>_boot_dmesg.txt\n'\
    '   raw ftrace output:              <hostname>_boot_ftrace.txt\n'\
    '\n'\
    'Options:\n'\
    '  -h            Print this help text\n'\
    '  -v            Print the current tool version\n'\
    '  -verbose      Print extra information during execution and analysis\n'\
    '  -addlogs      Add the dmesg log to the html output\n'\
    '  -result fn    Export a results table to a text file for parsing.\n'\
    '  -o name       Overrides the output subdirectory name when running a new test\n'\
    '                default: boot-{date}-{time}\n'\
    ' [advanced]\n'\
    '  -fstat        Use ftrace to add function detail and statistics (default: disabled)\n'\
    '  -f/-callgraph Add callgraph detail, can be very large (default: disabled)\n'\
    '  -maxdepth N   limit the callgraph data to N call levels (default: 2)\n'\
    '  -mincg ms     Discard all callgraphs shorter than ms milliseconds (e.g. 0.001 for us)\n'\
    '  -timeprec N   Number of significant digits in timestamps (0:S, 3:ms, [6:us])\n'\
    '  -expandcg     pre-expand the callgraph data in the html output (default: disabled)\n'\
    '  -func list    Limit ftrace to comma-delimited list of functions (default: do_one_initcall)\n'\
    '  -cgfilter S   Filter the callgraph output in the timeline\n'\
    '  -cgskip file  Callgraph functions to skip, off to disable (default: cgskip.txt)\n'\
    '  -bl name      Use the following boot loader for kernel params (default: grub)\n'\
    '  -reboot       Reboot the machine automatically and generate a new timeline\n'\
    '  -manual       Show the steps to generate a new timeline manually (used with -reboot)\n'\
    '\n'\
    'Other commands:\n'\
    '  -flistall     Print all functions capable of being captured in ftrace\n'\
    '  -sysinfo      Print out system info extracted from BIOS\n'\
    '  -which exec   Print an executable path, should function even without PATH\n'\
    ' [redo]\n'\
    '  -dmesg file   Create HTML output using dmesg input (used with -ftrace)\n'\
    '  -ftrace file  Create HTML output using ftrace input (used with -dmesg)\n'\
    '' % (sysvals.title, sysvals.version))
    return True

# ----------------- MAIN --------------------
# exec start (skipped if script is loaded as library)
if __name__ == '__main__':
    # loop through the command line arguments
    cmd = ''
    testrun = True
    switchoff = ['disable', 'off', 'false', '0']
    simplecmds = ['-sysinfo', '-kpupdate', '-flistall', '-checkbl']
    cgskip = ''
    if '-f' in sys.argv:
        cgskip = sysvals.configFile('cgskip.txt')
    args = iter(sys.argv[1:])
    mdset = False
    for arg in args:
        if(arg == '-h'):
            printHelp()
            sys.exit()
        elif(arg == '-v'):
            pprint("Version %s" % sysvals.version)
            sys.exit()
        elif(arg == '-verbose'):
            sysvals.verbose = True
        elif(arg in simplecmds):
            cmd = arg[1:]
        elif(arg == '-fstat'):
            sysvals.useftrace = True
        elif(arg == '-callgraph' or arg == '-f'):
            sysvals.useftrace = True
            sysvals.usecallgraph = True
        elif(arg == '-cgdump'):
            sysvals.cgdump = True
        elif(arg == '-mincg'):
            sysvals.mincglen = aslib.getArgFloat('-mincg', args, 0.0, 10000.0)
        elif(arg == '-cgfilter'):
            try:
                val = next(args)
            except:
                doError('No callgraph functions supplied', True)
            sysvals.setCallgraphFilter(val)
        elif(arg == '-cgskip'):
            try:
                val = next(args)
            except:
                doError('No file supplied', True)
            if val.lower() in switchoff:
                cgskip = ''
            else:
                cgskip = sysvals.configFile(val)
                if(not cgskip):
                    doError('%s does not exist' % cgskip)
        elif(arg == '-bl'):
            try:
                val = next(args)
            except:
                doError('No boot loader name supplied', True)
            if val.lower() not in ['grub']:
                doError('Unknown boot loader: %s' % val, True)
            sysvals.bootloader = val.lower()
        elif(arg == '-timeprec'):
            sysvals.setPrecision(aslib.getArgInt('-timeprec', args, 0, 6))
        elif(arg == '-maxdepth'):
            mdset = True
            sysvals.max_graph_depth = aslib.getArgInt('-maxdepth', args, 0, 1000)
        elif(arg == '-func'):
            try:
                val = next(args)
            except:
                doError('No filter functions supplied', True)
            sysvals.useftrace = True
            sysvals.usecallgraph = True
            sysvals.rootCheck(True)
            sysvals.setGraphFilter(val)
        elif(arg == '-ftrace'):
            try:
                val = next(args)
            except:
                doError('No ftrace file supplied', True)
            if(os.path.exists(val) == False):
                doError('%s does not exist' % val)
            testrun = False
            sysvals.ftracefile = val
        elif(arg == '-addlogs'):
            sysvals.dmesglog = True
        elif(arg == '-expandcg'):
            sysvals.cgexp = True
        elif(arg == '-dmesg'):
            try:
                val = next(args)
            except:
                doError('No dmesg file supplied', True)
            if(os.path.exists(val) == False):
                doError('%s does not exist' % val)
            testrun = False
            sysvals.dmesgfile = val
        elif(arg == '-o'):
            try:
                val = next(args)
            except:
                doError('No subdirectory name supplied', True)
            sysvals.testdir = sysvals.setOutputFolder(val)
        elif(arg == '-result'):
            try:
                val = next(args)
            except:
                doError('No result file supplied', True)
            sysvals.result = val
        elif(arg == '-reboot'):
            sysvals.reboot = True
        elif(arg == '-manual'):
            sysvals.reboot = True
            sysvals.manual = True
        # remaining options are only for cron job use
        elif(arg == '-cronjob'):
            sysvals.iscronjob = True
        elif(arg == '-which'):
            try:
                val = next(args)
            except:
                doError('No executable supplied', True)
            out = sysvals.getExec(val)
            if not out:
                print('%s not found' % val)
                sys.exit(1)
            print(out)
            sys.exit(0)
        else:
            doError('Invalid argument: '+arg, True)

    # compatibility errors and access checks
    if(sysvals.iscronjob and (sysvals.reboot or \
        sysvals.dmesgfile or sysvals.ftracefile or cmd)):
        doError('-cronjob is meant for batch purposes only')
    if(sysvals.reboot and (sysvals.dmesgfile or sysvals.ftracefile)):
        doError('-reboot and -dmesg/-ftrace are incompatible')
    if cmd or sysvals.reboot or sysvals.iscronjob or testrun:
        sysvals.rootCheck(True)
    if (testrun and sysvals.useftrace) or cmd == 'flistall':
        if not sysvals.verifyFtrace():
            doError('Ftrace is not properly enabled')

    # run utility commands
    sysvals.cpuInfo()
    if cmd != '':
        if cmd == 'kpupdate':
            updateKernelParams()
        elif cmd == 'flistall':
            for f in sysvals.getBootFtraceFilterFunctions():
                print(f)
        elif cmd == 'checkbl':
            sysvals.getBootLoader()
            pprint('Boot Loader: %s\n%s' % (sysvals.bootloader, sysvals.blexec))
        elif(cmd == 'sysinfo'):
            sysvals.printSystemInfo(True)
        sys.exit()

    # reboot: update grub, setup a cronjob, and reboot
    if sysvals.reboot:
        if (sysvals.useftrace or sysvals.usecallgraph) and \
            not sysvals.checkFtraceKernelVersion():
            doError('Ftrace functionality requires kernel v4.10 or newer')
        if not sysvals.manual:
            updateKernelParams()
            updateCron()
            call('reboot')
        else:
            sysvals.manualRebootRequired()
        sys.exit()

    if sysvals.usecallgraph and cgskip:
        sysvals.vprint('Using cgskip file: %s' % cgskip)
        sysvals.setCallgraphBlacklist(cgskip)

    # cronjob: remove the cronjob, grub changes, and disable ftrace
    if sysvals.iscronjob:
        updateCron(True)
        updateKernelParams(True)
        try:
            sysvals.fsetVal('0', 'tracing_on')
        except:
            pass

    # testrun: generate copies of the logs
    if testrun:
        retrieveLogs()
    else:
        sysvals.setOutputFile()

    # process the log data
    if sysvals.dmesgfile:
        if not mdset:
            sysvals.max_graph_depth = 0
        data = parseKernelLog()
        if(not data.valid):
            doError('No initcall data found in %s' % sysvals.dmesgfile)
        if sysvals.useftrace and sysvals.ftracefile:
            parseTraceLog(data)
        if sysvals.cgdump:
            data.debugPrint()
            sys.exit()
    else:
        doError('dmesg file required')

    sysvals.vprint('Creating the html timeline (%s)...' % sysvals.htmlfile)
    sysvals.vprint('Command:\n    %s' % sysvals.cmdline)
    sysvals.vprint('Kernel parameters:\n    %s' % sysvals.kparams)
    data.printDetails()
    createBootGraph(data)

    # if running as root, change output dir owner to sudo_user
    if testrun and os.path.isdir(sysvals.testdir) and \
        os.getuid() == 0 and 'SUDO_USER' in os.environ:
        cmd = 'chown -R {0}:{0} {1} > /dev/null 2>&1'
        call(cmd.format(os.environ['SUDO_USER'], sysvals.testdir), shell=True)

    sysvals.stamp['boot'] = (data.tUserMode - data.start) * 1000
    sysvals.stamp['lastinit'] = data.end * 1000
    sysvals.outputResult(sysvals.stamp)