linux/tools/power/pm-graph/bootgraph.py
<<
>>
Prefs
   1#!/usr/bin/env python3
   2# SPDX-License-Identifier: GPL-2.0-only
   3#
   4# Tool for analyzing boot timing
   5# Copyright (c) 2013, Intel Corporation.
   6#
   7# This program is free software; you can redistribute it and/or modify it
   8# under the terms and conditions of the GNU General Public License,
   9# version 2, as published by the Free Software Foundation.
  10#
  11# This program is distributed in the hope it will be useful, but WITHOUT
  12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
  14# more details.
  15#
  16# Authors:
  17#        Todd Brandt <todd.e.brandt@linux.intel.com>
  18#
  19# Description:
  20#        This tool is designed to assist kernel and OS developers in optimizing
  21#        their linux stack's boot time. It creates an html representation of
  22#        the kernel boot timeline up to the start of the init process.
  23#
  24
  25# ----------------- LIBRARIES --------------------
  26
  27import sys
  28import time
  29import os
  30import string
  31import re
  32import platform
  33import shutil
  34from datetime import datetime, timedelta
  35from subprocess import call, Popen, PIPE
  36import sleepgraph as aslib
  37
  38def pprint(msg):
  39        print(msg)
  40        sys.stdout.flush()
  41
  42# ----------------- CLASSES --------------------
  43
  44# Class: SystemValues
  45# Description:
  46#        A global, single-instance container used to
  47#        store system values and test parameters
  48class SystemValues(aslib.SystemValues):
  49        title = 'BootGraph'
  50        version = '2.2'
  51        hostname = 'localhost'
  52        testtime = ''
  53        kernel = ''
  54        dmesgfile = ''
  55        ftracefile = ''
  56        htmlfile = 'bootgraph.html'
  57        testdir = ''
  58        kparams = ''
  59        result = ''
  60        useftrace = False
  61        usecallgraph = False
  62        suspendmode = 'boot'
  63        max_graph_depth = 2
  64        graph_filter = 'do_one_initcall'
  65        reboot = False
  66        manual = False
  67        iscronjob = False
  68        timeformat = '%.6f'
  69        bootloader = 'grub'
  70        blexec = []
  71        def __init__(self):
  72                self.hostname = platform.node()
  73                self.testtime = datetime.now().strftime('%Y-%m-%d_%H:%M:%S')
  74                if os.path.exists('/proc/version'):
  75                        fp = open('/proc/version', 'r')
  76                        val = fp.read().strip()
  77                        fp.close()
  78                        self.kernel = self.kernelVersion(val)
  79                else:
  80                        self.kernel = 'unknown'
  81                self.testdir = datetime.now().strftime('boot-%y%m%d-%H%M%S')
  82        def kernelVersion(self, msg):
  83                return msg.split()[2]
  84        def checkFtraceKernelVersion(self):
  85                val = tuple(map(int, self.kernel.split('-')[0].split('.')))
  86                if val >= (4, 10, 0):
  87                        return True
  88                return False
  89        def kernelParams(self):
  90                cmdline = 'initcall_debug log_buf_len=32M'
  91                if self.useftrace:
  92                        if self.cpucount > 0:
  93                                bs = min(self.memtotal // 2, 2*1024*1024) // self.cpucount
  94                        else:
  95                                bs = 131072
  96                        cmdline += ' trace_buf_size=%dK trace_clock=global '\
  97                        'trace_options=nooverwrite,funcgraph-abstime,funcgraph-cpu,'\
  98                        'funcgraph-duration,funcgraph-proc,funcgraph-tail,'\
  99                        'nofuncgraph-overhead,context-info,graph-time '\
 100                        'ftrace=function_graph '\
 101                        'ftrace_graph_max_depth=%d '\
 102                        'ftrace_graph_filter=%s' % \
 103                                (bs, self.max_graph_depth, self.graph_filter)
 104                return cmdline
 105        def setGraphFilter(self, val):
 106                master = self.getBootFtraceFilterFunctions()
 107                fs = ''
 108                for i in val.split(','):
 109                        func = i.strip()
 110                        if func == '':
 111                                doError('badly formatted filter function string')
 112                        if '[' in func or ']' in func:
 113                                doError('loadable module functions not allowed - "%s"' % func)
 114                        if ' ' in func:
 115                                doError('spaces found in filter functions - "%s"' % func)
 116                        if func not in master:
 117                                doError('function "%s" not available for ftrace' % func)
 118                        if not fs:
 119                                fs = func
 120                        else:
 121                                fs += ','+func
 122                if not fs:
 123                        doError('badly formatted filter function string')
 124                self.graph_filter = fs
 125        def getBootFtraceFilterFunctions(self):
 126                self.rootCheck(True)
 127                fp = open(self.tpath+'available_filter_functions')
 128                fulllist = fp.read().split('\n')
 129                fp.close()
 130                list = []
 131                for i in fulllist:
 132                        if not i or ' ' in i or '[' in i or ']' in i:
 133                                continue
 134                        list.append(i)
 135                return list
 136        def myCronJob(self, line):
 137                if '@reboot' not in line:
 138                        return False
 139                if 'bootgraph' in line or 'analyze_boot.py' in line or '-cronjob' in line:
 140                        return True
 141                return False
 142        def cronjobCmdString(self):
 143                cmdline = '%s -cronjob' % os.path.abspath(sys.argv[0])
 144                args = iter(sys.argv[1:])
 145                for arg in args:
 146                        if arg in ['-h', '-v', '-cronjob', '-reboot', '-verbose']:
 147                                continue
 148                        elif arg in ['-o', '-dmesg', '-ftrace', '-func']:
 149                                next(args)
 150                                continue
 151                        elif arg == '-result':
 152                                cmdline += ' %s "%s"' % (arg, os.path.abspath(next(args)))
 153                                continue
 154                        elif arg == '-cgskip':
 155                                file = self.configFile(next(args))
 156                                cmdline += ' %s "%s"' % (arg, os.path.abspath(file))
 157                                continue
 158                        cmdline += ' '+arg
 159                if self.graph_filter != 'do_one_initcall':
 160                        cmdline += ' -func "%s"' % self.graph_filter
 161                cmdline += ' -o "%s"' % os.path.abspath(self.testdir)
 162                return cmdline
 163        def manualRebootRequired(self):
 164                cmdline = self.kernelParams()
 165                pprint('To generate a new timeline manually, follow these steps:\n\n'\
 166                '1. Add the CMDLINE string to your kernel command line.\n'\
 167                '2. Reboot the system.\n'\
 168                '3. After reboot, re-run this tool with the same arguments but no command (w/o -reboot or -manual).\n\n'\
 169                'CMDLINE="%s"' % cmdline)
 170                sys.exit()
 171        def blGrub(self):
 172                blcmd = ''
 173                for cmd in ['update-grub', 'grub-mkconfig', 'grub2-mkconfig']:
 174                        if blcmd:
 175                                break
 176                        blcmd = self.getExec(cmd)
 177                if not blcmd:
 178                        doError('[GRUB] missing update command')
 179                if not os.path.exists('/etc/default/grub'):
 180                        doError('[GRUB] missing /etc/default/grub')
 181                if 'grub2' in blcmd:
 182                        cfg = '/boot/grub2/grub.cfg'
 183                else:
 184                        cfg = '/boot/grub/grub.cfg'
 185                if not os.path.exists(cfg):
 186                        doError('[GRUB] missing %s' % cfg)
 187                if 'update-grub' in blcmd:
 188                        self.blexec = [blcmd]
 189                else:
 190                        self.blexec = [blcmd, '-o', cfg]
 191        def getBootLoader(self):
 192                if self.bootloader == 'grub':
 193                        self.blGrub()
 194                else:
 195                        doError('unknown boot loader: %s' % self.bootloader)
 196        def writeDatafileHeader(self, filename):
 197                self.kparams = open('/proc/cmdline', 'r').read().strip()
 198                fp = open(filename, 'w')
 199                fp.write(self.teststamp+'\n')
 200                fp.write(self.sysstamp+'\n')
 201                fp.write('# command | %s\n' % self.cmdline)
 202                fp.write('# kparams | %s\n' % self.kparams)
 203                fp.close()
 204
 205sysvals = SystemValues()
 206
 207# Class: Data
 208# Description:
 209#        The primary container for test data.
 210class Data(aslib.Data):
 211        dmesg = {}  # root data structure
 212        start = 0.0 # test start
 213        end = 0.0   # test end
 214        dmesgtext = []   # dmesg text file in memory
 215        testnumber = 0
 216        idstr = ''
 217        html_device_id = 0
 218        valid = False
 219        tUserMode = 0.0
 220        boottime = ''
 221        phases = ['kernel', 'user']
 222        do_one_initcall = False
 223        def __init__(self, num):
 224                self.testnumber = num
 225                self.idstr = 'a'
 226                self.dmesgtext = []
 227                self.dmesg = {
 228                        'kernel': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
 229                                'order': 0, 'color': 'linear-gradient(to bottom, #fff, #bcf)'},
 230                        'user': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
 231                                'order': 1, 'color': '#fff'}
 232                }
 233        def deviceTopology(self):
 234                return ''
 235        def newAction(self, phase, name, pid, start, end, ret, ulen):
 236                # new device callback for a specific phase
 237                self.html_device_id += 1
 238                devid = '%s%d' % (self.idstr, self.html_device_id)
 239                list = self.dmesg[phase]['list']
 240                length = -1.0
 241                if(start >= 0 and end >= 0):
 242                        length = end - start
 243                i = 2
 244                origname = name
 245                while(name in list):
 246                        name = '%s[%d]' % (origname, i)
 247                        i += 1
 248                list[name] = {'name': name, 'start': start, 'end': end,
 249                        'pid': pid, 'length': length, 'row': 0, 'id': devid,
 250                        'ret': ret, 'ulen': ulen }
 251                return name
 252        def deviceMatch(self, pid, cg):
 253                if cg.end - cg.start == 0:
 254                        return ''
 255                for p in data.phases:
 256                        list = self.dmesg[p]['list']
 257                        for devname in list:
 258                                dev = list[devname]
 259                                if pid != dev['pid']:
 260                                        continue
 261                                if cg.name == 'do_one_initcall':
 262                                        if(cg.start <= dev['start'] and cg.end >= dev['end'] and dev['length'] > 0):
 263                                                dev['ftrace'] = cg
 264                                                self.do_one_initcall = True
 265                                                return devname
 266                                else:
 267                                        if(cg.start > dev['start'] and cg.end < dev['end']):
 268                                                if 'ftraces' not in dev:
 269                                                        dev['ftraces'] = []
 270                                                dev['ftraces'].append(cg)
 271                                                return devname
 272                return ''
 273        def printDetails(self):
 274                sysvals.vprint('Timeline Details:')
 275                sysvals.vprint('          Host: %s' % sysvals.hostname)
 276                sysvals.vprint('        Kernel: %s' % sysvals.kernel)
 277                sysvals.vprint('     Test time: %s' % sysvals.testtime)
 278                sysvals.vprint('     Boot time: %s' % self.boottime)
 279                for phase in self.phases:
 280                        dc = len(self.dmesg[phase]['list'])
 281                        sysvals.vprint('%9s mode: %.3f - %.3f (%d initcalls)' % (phase,
 282                                self.dmesg[phase]['start']*1000,
 283                                self.dmesg[phase]['end']*1000, dc))
 284
 285# ----------------- FUNCTIONS --------------------
 286
 287# Function: parseKernelLog
 288# Description:
 289#        parse a kernel log for boot data
 290def parseKernelLog():
 291        sysvals.vprint('Analyzing the dmesg data (%s)...' % \
 292                os.path.basename(sysvals.dmesgfile))
 293        phase = 'kernel'
 294        data = Data(0)
 295        data.dmesg['kernel']['start'] = data.start = ktime = 0.0
 296        sysvals.stamp = {
 297                'time': datetime.now().strftime('%B %d %Y, %I:%M:%S %p'),
 298                'host': sysvals.hostname,
 299                'mode': 'boot', 'kernel': ''}
 300
 301        tp = aslib.TestProps()
 302        devtemp = dict()
 303        if(sysvals.dmesgfile):
 304                lf = open(sysvals.dmesgfile, 'rb')
 305        else:
 306                lf = Popen('dmesg', stdout=PIPE).stdout
 307        for line in lf:
 308                line = aslib.ascii(line).replace('\r\n', '')
 309                # grab the stamp and sysinfo
 310                if re.match(tp.stampfmt, line):
 311                        tp.stamp = line
 312                        continue
 313                elif re.match(tp.sysinfofmt, line):
 314                        tp.sysinfo = line
 315                        continue
 316                elif re.match(tp.cmdlinefmt, line):
 317                        tp.cmdline = line
 318                        continue
 319                elif re.match(tp.kparamsfmt, line):
 320                        tp.kparams = line
 321                        continue
 322                idx = line.find('[')
 323                if idx > 1:
 324                        line = line[idx:]
 325                m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
 326                if(not m):
 327                        continue
 328                ktime = float(m.group('ktime'))
 329                if(ktime > 120):
 330                        break
 331                msg = m.group('msg')
 332                data.dmesgtext.append(line)
 333                if(ktime == 0.0 and re.match('^Linux version .*', msg)):
 334                        if(not sysvals.stamp['kernel']):
 335                                sysvals.stamp['kernel'] = sysvals.kernelVersion(msg)
 336                        continue
 337                m = re.match('.* setting system clock to (?P<d>[0-9\-]*)[ A-Z](?P<t>[0-9:]*) UTC.*', msg)
 338                if(m):
 339                        bt = datetime.strptime(m.group('d')+' '+m.group('t'), '%Y-%m-%d %H:%M:%S')
 340                        bt = bt - timedelta(seconds=int(ktime))
 341                        data.boottime = bt.strftime('%Y-%m-%d_%H:%M:%S')
 342                        sysvals.stamp['time'] = bt.strftime('%B %d %Y, %I:%M:%S %p')
 343                        continue
 344                m = re.match('^calling *(?P<f>.*)\+.* @ (?P<p>[0-9]*)', msg)
 345                if(m):
 346                        func = m.group('f')
 347                        pid = int(m.group('p'))
 348                        devtemp[func] = (ktime, pid)
 349                        continue
 350                m = re.match('^initcall *(?P<f>.*)\+.* returned (?P<r>.*) after (?P<t>.*) usecs', msg)
 351                if(m):
 352                        data.valid = True
 353                        data.end = ktime
 354                        f, r, t = m.group('f', 'r', 't')
 355                        if(f in devtemp):
 356                                start, pid = devtemp[f]
 357                                data.newAction(phase, f, pid, start, ktime, int(r), int(t))
 358                                del devtemp[f]
 359                        continue
 360                if(re.match('^Freeing unused kernel .*', msg)):
 361                        data.tUserMode = ktime
 362                        data.dmesg['kernel']['end'] = ktime
 363                        data.dmesg['user']['start'] = ktime
 364                        phase = 'user'
 365
 366        if tp.stamp:
 367                sysvals.stamp = 0
 368                tp.parseStamp(data, sysvals)
 369        data.dmesg['user']['end'] = data.end
 370        lf.close()
 371        return data
 372
 373# Function: parseTraceLog
 374# Description:
 375#        Check if trace is available and copy to a temp file
 376def parseTraceLog(data):
 377        sysvals.vprint('Analyzing the ftrace data (%s)...' % \
 378                os.path.basename(sysvals.ftracefile))
 379        # if available, calculate cgfilter allowable ranges
 380        cgfilter = []
 381        if len(sysvals.cgfilter) > 0:
 382                for p in data.phases:
 383                        list = data.dmesg[p]['list']
 384                        for i in sysvals.cgfilter:
 385                                if i in list:
 386                                        cgfilter.append([list[i]['start']-0.0001,
 387                                                list[i]['end']+0.0001])
 388        # parse the trace log
 389        ftemp = dict()
 390        tp = aslib.TestProps()
 391        tp.setTracerType('function_graph')
 392        tf = open(sysvals.ftracefile, 'r')
 393        for line in tf:
 394                if line[0] == '#':
 395                        continue
 396                m = re.match(tp.ftrace_line_fmt, line.strip())
 397                if(not m):
 398                        continue
 399                m_time, m_proc, m_pid, m_msg, m_dur = \
 400                        m.group('time', 'proc', 'pid', 'msg', 'dur')
 401                t = float(m_time)
 402                if len(cgfilter) > 0:
 403                        allow = False
 404                        for r in cgfilter:
 405                                if t >= r[0] and t < r[1]:
 406                                        allow = True
 407                                        break
 408                        if not allow:
 409                                continue
 410                if t > data.end:
 411                        break
 412                if(m_time and m_pid and m_msg):
 413                        t = aslib.FTraceLine(m_time, m_msg, m_dur)
 414                        pid = int(m_pid)
 415                else:
 416                        continue
 417                if t.fevent or t.fkprobe:
 418                        continue
 419                key = (m_proc, pid)
 420                if(key not in ftemp):
 421                        ftemp[key] = []
 422                        ftemp[key].append(aslib.FTraceCallGraph(pid, sysvals))
 423                cg = ftemp[key][-1]
 424                res = cg.addLine(t)
 425                if(res != 0):
 426                        ftemp[key].append(aslib.FTraceCallGraph(pid, sysvals))
 427                if(res == -1):
 428                        ftemp[key][-1].addLine(t)
 429
 430        tf.close()
 431
 432        # add the callgraph data to the device hierarchy
 433        for key in ftemp:
 434                proc, pid = key
 435                for cg in ftemp[key]:
 436                        if len(cg.list) < 1 or cg.invalid or (cg.end - cg.start == 0):
 437                                continue
 438                        if(not cg.postProcess()):
 439                                pprint('Sanity check failed for %s-%d' % (proc, pid))
 440                                continue
 441                        # match cg data to devices
 442                        devname = data.deviceMatch(pid, cg)
 443                        if not devname:
 444                                kind = 'Orphan'
 445                                if cg.partial:
 446                                        kind = 'Partial'
 447                                sysvals.vprint('%s callgraph found for %s %s-%d [%f - %f]' %\
 448                                        (kind, cg.name, proc, pid, cg.start, cg.end))
 449                        elif len(cg.list) > 1000000:
 450                                pprint('WARNING: the callgraph found for %s is massive! (%d lines)' %\
 451                                        (devname, len(cg.list)))
 452
 453# Function: retrieveLogs
 454# Description:
 455#        Create copies of dmesg and/or ftrace for later processing
 456def retrieveLogs():
 457        # check ftrace is configured first
 458        if sysvals.useftrace:
 459                tracer = sysvals.fgetVal('current_tracer').strip()
 460                if tracer != 'function_graph':
 461                        doError('ftrace not configured for a boot callgraph')
 462        # create the folder and get dmesg
 463        sysvals.systemInfo(aslib.dmidecode(sysvals.mempath))
 464        sysvals.initTestOutput('boot')
 465        sysvals.writeDatafileHeader(sysvals.dmesgfile)
 466        call('dmesg >> '+sysvals.dmesgfile, shell=True)
 467        if not sysvals.useftrace:
 468                return
 469        # get ftrace
 470        sysvals.writeDatafileHeader(sysvals.ftracefile)
 471        call('cat '+sysvals.tpath+'trace >> '+sysvals.ftracefile, shell=True)
 472
 473# Function: colorForName
 474# Description:
 475#        Generate a repeatable color from a list for a given name
 476def colorForName(name):
 477        list = [
 478                ('c1', '#ec9999'),
 479                ('c2', '#ffc1a6'),
 480                ('c3', '#fff0a6'),
 481                ('c4', '#adf199'),
 482                ('c5', '#9fadea'),
 483                ('c6', '#a699c1'),
 484                ('c7', '#ad99b4'),
 485                ('c8', '#eaffea'),
 486                ('c9', '#dcecfb'),
 487                ('c10', '#ffffea')
 488        ]
 489        i = 0
 490        total = 0
 491        count = len(list)
 492        while i < len(name):
 493                total += ord(name[i])
 494                i += 1
 495        return list[total % count]
 496
 497def cgOverview(cg, minlen):
 498        stats = dict()
 499        large = []
 500        for l in cg.list:
 501                if l.fcall and l.depth == 1:
 502                        if l.length >= minlen:
 503                                large.append(l)
 504                        if l.name not in stats:
 505                                stats[l.name] = [0, 0.0]
 506                        stats[l.name][0] += (l.length * 1000.0)
 507                        stats[l.name][1] += 1
 508        return (large, stats)
 509
 510# Function: createBootGraph
 511# Description:
 512#        Create the output html file from the resident test data
 513# Arguments:
 514#        testruns: array of Data objects from parseKernelLog or parseTraceLog
 515# Output:
 516#        True if the html file was created, false if it failed
 517def createBootGraph(data):
 518        # html function templates
 519        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'
 520        html_timetotal = '<table class="time1">\n<tr>'\
 521                '<td class="blue">Init process starts @ <b>{0} ms</b></td>'\
 522                '<td class="blue">Last initcall ends @ <b>{1} ms</b></td>'\
 523                '</tr>\n</table>\n'
 524
 525        # device timeline
 526        devtl = aslib.Timeline(100, 20)
 527
 528        # write the test title and general info header
 529        devtl.createHeader(sysvals, sysvals.stamp)
 530
 531        # Generate the header for this timeline
 532        t0 = data.start
 533        tMax = data.end
 534        tTotal = tMax - t0
 535        if(tTotal == 0):
 536                pprint('ERROR: No timeline data')
 537                return False
 538        user_mode = '%.0f'%(data.tUserMode*1000)
 539        last_init = '%.0f'%(tTotal*1000)
 540        devtl.html += html_timetotal.format(user_mode, last_init)
 541
 542        # determine the maximum number of rows we need to draw
 543        devlist = []
 544        for p in data.phases:
 545                list = data.dmesg[p]['list']
 546                for devname in list:
 547                        d = aslib.DevItem(0, p, list[devname])
 548                        devlist.append(d)
 549                devtl.getPhaseRows(devlist, 0, 'start')
 550        devtl.calcTotalRows()
 551
 552        # draw the timeline background
 553        devtl.createZoomBox()
 554        devtl.html += devtl.html_tblock.format('boot', '0', '100', devtl.scaleH)
 555        for p in data.phases:
 556                phase = data.dmesg[p]
 557                length = phase['end']-phase['start']
 558                left = '%.3f' % (((phase['start']-t0)*100.0)/tTotal)
 559                width = '%.3f' % ((length*100.0)/tTotal)
 560                devtl.html += devtl.html_phase.format(left, width, \
 561                        '%.3f'%devtl.scaleH, '%.3f'%devtl.bodyH, \
 562                        phase['color'], '')
 563
 564        # draw the device timeline
 565        num = 0
 566        devstats = dict()
 567        for phase in data.phases:
 568                list = data.dmesg[phase]['list']
 569                for devname in sorted(list):
 570                        cls, color = colorForName(devname)
 571                        dev = list[devname]
 572                        info = '@|%.3f|%.3f|%.3f|%d' % (dev['start']*1000.0, dev['end']*1000.0,
 573                                dev['ulen']/1000.0, dev['ret'])
 574                        devstats[dev['id']] = {'info':info}
 575                        dev['color'] = color
 576                        height = devtl.phaseRowHeight(0, phase, dev['row'])
 577                        top = '%.6f' % ((dev['row']*height) + devtl.scaleH)
 578                        left = '%.6f' % (((dev['start']-t0)*100)/tTotal)
 579                        width = '%.6f' % (((dev['end']-dev['start'])*100)/tTotal)
 580                        length = ' (%0.3f ms) ' % ((dev['end']-dev['start'])*1000)
 581                        devtl.html += devtl.html_device.format(dev['id'],
 582                                devname+length+phase+'_mode', left, top, '%.3f'%height,
 583                                width, devname, ' '+cls, '')
 584                        rowtop = devtl.phaseRowTop(0, phase, dev['row'])
 585                        height = '%.6f' % (devtl.rowH / 2)
 586                        top = '%.6f' % (rowtop + devtl.scaleH + (devtl.rowH / 2))
 587                        if data.do_one_initcall:
 588                                if('ftrace' not in dev):
 589                                        continue
 590                                cg = dev['ftrace']
 591                                large, stats = cgOverview(cg, 0.001)
 592                                devstats[dev['id']]['fstat'] = stats
 593                                for l in large:
 594                                        left = '%f' % (((l.time-t0)*100)/tTotal)
 595                                        width = '%f' % (l.length*100/tTotal)
 596                                        title = '%s (%0.3fms)' % (l.name, l.length * 1000.0)
 597                                        devtl.html += html_srccall.format(l.name, left,
 598                                                top, height, width, title, 'x%d'%num)
 599                                        num += 1
 600                                continue
 601                        if('ftraces' not in dev):
 602                                continue
 603                        for cg in dev['ftraces']:
 604                                left = '%f' % (((cg.start-t0)*100)/tTotal)
 605                                width = '%f' % ((cg.end-cg.start)*100/tTotal)
 606                                cglen = (cg.end - cg.start) * 1000.0
 607                                title = '%s (%0.3fms)' % (cg.name, cglen)
 608                                cg.id = 'x%d' % num
 609                                devtl.html += html_srccall.format(cg.name, left,
 610                                        top, height, width, title, dev['id']+cg.id)
 611                                num += 1
 612
 613        # draw the time scale, try to make the number of labels readable
 614        devtl.createTimeScale(t0, tMax, tTotal, 'boot')
 615        devtl.html += '</div>\n'
 616
 617        # timeline is finished
 618        devtl.html += '</div>\n</div>\n'
 619
 620        # draw a legend which describes the phases by color
 621        devtl.html += '<div class="legend">\n'
 622        pdelta = 20.0
 623        pmargin = 36.0
 624        for phase in data.phases:
 625                order = '%.2f' % ((data.dmesg[phase]['order'] * pdelta) + pmargin)
 626                devtl.html += devtl.html_legend.format(order, \
 627                        data.dmesg[phase]['color'], phase+'_mode', phase[0])
 628        devtl.html += '</div>\n'
 629
 630        hf = open(sysvals.htmlfile, 'w')
 631
 632        # add the css
 633        extra = '\
 634                .c1 {background:rgba(209,0,0,0.4);}\n\
 635                .c2 {background:rgba(255,102,34,0.4);}\n\
 636                .c3 {background:rgba(255,218,33,0.4);}\n\
 637                .c4 {background:rgba(51,221,0,0.4);}\n\
 638                .c5 {background:rgba(17,51,204,0.4);}\n\
 639                .c6 {background:rgba(34,0,102,0.4);}\n\
 640                .c7 {background:rgba(51,0,68,0.4);}\n\
 641                .c8 {background:rgba(204,255,204,0.4);}\n\
 642                .c9 {background:rgba(169,208,245,0.4);}\n\
 643                .c10 {background:rgba(255,255,204,0.4);}\n\
 644                .vt {transform:rotate(-60deg);transform-origin:0 0;}\n\
 645                table.fstat {table-layout:fixed;padding:150px 15px 0 0;font-size:10px;column-width:30px;}\n\
 646                .fstat th {width:55px;}\n\
 647                .fstat td {text-align:left;width:35px;}\n\
 648                .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\
 649                .srccall:hover {color:white;font-weight:bold;border:1px solid white;}\n'
 650        aslib.addCSS(hf, sysvals, 1, False, extra)
 651
 652        # write the device timeline
 653        hf.write(devtl.html)
 654
 655        # add boot specific html
 656        statinfo = 'var devstats = {\n'
 657        for n in sorted(devstats):
 658                statinfo += '\t"%s": [\n\t\t"%s",\n' % (n, devstats[n]['info'])
 659                if 'fstat' in devstats[n]:
 660                        funcs = devstats[n]['fstat']
 661                        for f in sorted(funcs, key=lambda k:(funcs[k], k), reverse=True):
 662                                if funcs[f][0] < 0.01 and len(funcs) > 10:
 663                                        break
 664                                statinfo += '\t\t"%f|%s|%d",\n' % (funcs[f][0], f, funcs[f][1])
 665                statinfo += '\t],\n'
 666        statinfo += '};\n'
 667        html = \
 668                '<div id="devicedetailtitle"></div>\n'\
 669                '<div id="devicedetail" style="display:none;">\n'\
 670                '<div id="devicedetail0">\n'
 671        for p in data.phases:
 672                phase = data.dmesg[p]
 673                html += devtl.html_phaselet.format(p+'_mode', '0', '100', phase['color'])
 674        html += '</div>\n</div>\n'\
 675                '<script type="text/javascript">\n'+statinfo+\
 676                '</script>\n'
 677        hf.write(html)
 678
 679        # add the callgraph html
 680        if(sysvals.usecallgraph):
 681                aslib.addCallgraphs(sysvals, hf, data)
 682
 683        # add the test log as a hidden div
 684        if sysvals.testlog and sysvals.logmsg:
 685                hf.write('<div id="testlog" style="display:none;">\n'+sysvals.logmsg+'</div>\n')
 686        # add the dmesg log as a hidden div
 687        if sysvals.dmesglog:
 688                hf.write('<div id="dmesglog" style="display:none;">\n')
 689                for line in data.dmesgtext:
 690                        line = line.replace('<', '&lt').replace('>', '&gt')
 691                        hf.write(line)
 692                hf.write('</div>\n')
 693
 694        # write the footer and close
 695        aslib.addScriptCode(hf, [data])
 696        hf.write('</body>\n</html>\n')
 697        hf.close()
 698        return True
 699
 700# Function: updateCron
 701# Description:
 702#    (restore=False) Set the tool to run automatically on reboot
 703#    (restore=True) Restore the original crontab
 704def updateCron(restore=False):
 705        if not restore:
 706                sysvals.rootUser(True)
 707        crondir = '/var/spool/cron/crontabs/'
 708        if not os.path.exists(crondir):
 709                crondir = '/var/spool/cron/'
 710        if not os.path.exists(crondir):
 711                doError('%s not found' % crondir)
 712        cronfile = crondir+'root'
 713        backfile = crondir+'root-analyze_boot-backup'
 714        cmd = sysvals.getExec('crontab')
 715        if not cmd:
 716                doError('crontab not found')
 717        # on restore: move the backup cron back into place
 718        if restore:
 719                if os.path.exists(backfile):
 720                        shutil.move(backfile, cronfile)
 721                        call([cmd, cronfile])
 722                return
 723        # backup current cron and install new one with reboot
 724        if os.path.exists(cronfile):
 725                shutil.move(cronfile, backfile)
 726        else:
 727                fp = open(backfile, 'w')
 728                fp.close()
 729        res = -1
 730        try:
 731                fp = open(backfile, 'r')
 732                op = open(cronfile, 'w')
 733                for line in fp:
 734                        if not sysvals.myCronJob(line):
 735                                op.write(line)
 736                                continue
 737                fp.close()
 738                op.write('@reboot python %s\n' % sysvals.cronjobCmdString())
 739                op.close()
 740                res = call([cmd, cronfile])
 741        except Exception as e:
 742                pprint('Exception: %s' % str(e))
 743                shutil.move(backfile, cronfile)
 744                res = -1
 745        if res != 0:
 746                doError('crontab failed')
 747
 748# Function: updateGrub
 749# Description:
 750#        update grub.cfg for all kernels with our parameters
 751def updateGrub(restore=False):
 752        # call update-grub on restore
 753        if restore:
 754                try:
 755                        call(sysvals.blexec, stderr=PIPE, stdout=PIPE,
 756                                env={'PATH': '.:/sbin:/usr/sbin:/usr/bin:/sbin:/bin'})
 757                except Exception as e:
 758                        pprint('Exception: %s\n' % str(e))
 759                return
 760        # extract the option and create a grub config without it
 761        sysvals.rootUser(True)
 762        tgtopt = 'GRUB_CMDLINE_LINUX_DEFAULT'
 763        cmdline = ''
 764        grubfile = '/etc/default/grub'
 765        tempfile = '/etc/default/grub.analyze_boot'
 766        shutil.move(grubfile, tempfile)
 767        res = -1
 768        try:
 769                fp = open(tempfile, 'r')
 770                op = open(grubfile, 'w')
 771                cont = False
 772                for line in fp:
 773                        line = line.strip()
 774                        if len(line) == 0 or line[0] == '#':
 775                                continue
 776                        opt = line.split('=')[0].strip()
 777                        if opt == tgtopt:
 778                                cmdline = line.split('=', 1)[1].strip('\\')
 779                                if line[-1] == '\\':
 780                                        cont = True
 781                        elif cont:
 782                                cmdline += line.strip('\\')
 783                                if line[-1] != '\\':
 784                                        cont = False
 785                        else:
 786                                op.write('%s\n' % line)
 787                fp.close()
 788                # if the target option value is in quotes, strip them
 789                sp = '"'
 790                val = cmdline.strip()
 791                if val and (val[0] == '\'' or val[0] == '"'):
 792                        sp = val[0]
 793                        val = val.strip(sp)
 794                cmdline = val
 795                # append our cmd line options
 796                if len(cmdline) > 0:
 797                        cmdline += ' '
 798                cmdline += sysvals.kernelParams()
 799                # write out the updated target option
 800                op.write('\n%s=%s%s%s\n' % (tgtopt, sp, cmdline, sp))
 801                op.close()
 802                res = call(sysvals.blexec)
 803                os.remove(grubfile)
 804        except Exception as e:
 805                pprint('Exception: %s' % str(e))
 806                res = -1
 807        # cleanup
 808        shutil.move(tempfile, grubfile)
 809        if res != 0:
 810                doError('update grub failed')
 811
 812# Function: updateKernelParams
 813# Description:
 814#        update boot conf for all kernels with our parameters
 815def updateKernelParams(restore=False):
 816        # find the boot loader
 817        sysvals.getBootLoader()
 818        if sysvals.bootloader == 'grub':
 819                updateGrub(restore)
 820
 821# Function: doError Description:
 822#        generic error function for catastrphic failures
 823# Arguments:
 824#        msg: the error message to print
 825#        help: True if printHelp should be called after, False otherwise
 826def doError(msg, help=False):
 827        if help == True:
 828                printHelp()
 829        pprint('ERROR: %s\n' % msg)
 830        sysvals.outputResult({'error':msg})
 831        sys.exit()
 832
 833# Function: printHelp
 834# Description:
 835#        print out the help text
 836def printHelp():
 837        pprint('\n%s v%s\n'\
 838        'Usage: bootgraph <options> <command>\n'\
 839        '\n'\
 840        'Description:\n'\
 841        '  This tool reads in a dmesg log of linux kernel boot and\n'\
 842        '  creates an html representation of the boot timeline up to\n'\
 843        '  the start of the init process.\n'\
 844        '\n'\
 845        '  If no specific command is given the tool reads the current dmesg\n'\
 846        '  and/or ftrace log and creates a timeline\n'\
 847        '\n'\
 848        '  Generates output files in subdirectory: boot-yymmdd-HHMMSS\n'\
 849        '   HTML output:                    <hostname>_boot.html\n'\
 850        '   raw dmesg output:               <hostname>_boot_dmesg.txt\n'\
 851        '   raw ftrace output:              <hostname>_boot_ftrace.txt\n'\
 852        '\n'\
 853        'Options:\n'\
 854        '  -h            Print this help text\n'\
 855        '  -v            Print the current tool version\n'\
 856        '  -verbose      Print extra information during execution and analysis\n'\
 857        '  -addlogs      Add the dmesg log to the html output\n'\
 858        '  -result fn    Export a results table to a text file for parsing.\n'\
 859        '  -o name       Overrides the output subdirectory name when running a new test\n'\
 860        '                default: boot-{date}-{time}\n'\
 861        ' [advanced]\n'\
 862        '  -fstat        Use ftrace to add function detail and statistics (default: disabled)\n'\
 863        '  -f/-callgraph Add callgraph detail, can be very large (default: disabled)\n'\
 864        '  -maxdepth N   limit the callgraph data to N call levels (default: 2)\n'\
 865        '  -mincg ms     Discard all callgraphs shorter than ms milliseconds (e.g. 0.001 for us)\n'\
 866        '  -timeprec N   Number of significant digits in timestamps (0:S, 3:ms, [6:us])\n'\
 867        '  -expandcg     pre-expand the callgraph data in the html output (default: disabled)\n'\
 868        '  -func list    Limit ftrace to comma-delimited list of functions (default: do_one_initcall)\n'\
 869        '  -cgfilter S   Filter the callgraph output in the timeline\n'\
 870        '  -cgskip file  Callgraph functions to skip, off to disable (default: cgskip.txt)\n'\
 871        '  -bl name      Use the following boot loader for kernel params (default: grub)\n'\
 872        '  -reboot       Reboot the machine automatically and generate a new timeline\n'\
 873        '  -manual       Show the steps to generate a new timeline manually (used with -reboot)\n'\
 874        '\n'\
 875        'Other commands:\n'\
 876        '  -flistall     Print all functions capable of being captured in ftrace\n'\
 877        '  -sysinfo      Print out system info extracted from BIOS\n'\
 878        '  -which exec   Print an executable path, should function even without PATH\n'\
 879        ' [redo]\n'\
 880        '  -dmesg file   Create HTML output using dmesg input (used with -ftrace)\n'\
 881        '  -ftrace file  Create HTML output using ftrace input (used with -dmesg)\n'\
 882        '' % (sysvals.title, sysvals.version))
 883        return True
 884
 885# ----------------- MAIN --------------------
 886# exec start (skipped if script is loaded as library)
 887if __name__ == '__main__':
 888        # loop through the command line arguments
 889        cmd = ''
 890        testrun = True
 891        switchoff = ['disable', 'off', 'false', '0']
 892        simplecmds = ['-sysinfo', '-kpupdate', '-flistall', '-checkbl']
 893        cgskip = ''
 894        if '-f' in sys.argv:
 895                cgskip = sysvals.configFile('cgskip.txt')
 896        args = iter(sys.argv[1:])
 897        mdset = False
 898        for arg in args:
 899                if(arg == '-h'):
 900                        printHelp()
 901                        sys.exit()
 902                elif(arg == '-v'):
 903                        pprint("Version %s" % sysvals.version)
 904                        sys.exit()
 905                elif(arg == '-verbose'):
 906                        sysvals.verbose = True
 907                elif(arg in simplecmds):
 908                        cmd = arg[1:]
 909                elif(arg == '-fstat'):
 910                        sysvals.useftrace = True
 911                elif(arg == '-callgraph' or arg == '-f'):
 912                        sysvals.useftrace = True
 913                        sysvals.usecallgraph = True
 914                elif(arg == '-cgdump'):
 915                        sysvals.cgdump = True
 916                elif(arg == '-mincg'):
 917                        sysvals.mincglen = aslib.getArgFloat('-mincg', args, 0.0, 10000.0)
 918                elif(arg == '-cgfilter'):
 919                        try:
 920                                val = next(args)
 921                        except:
 922                                doError('No callgraph functions supplied', True)
 923                        sysvals.setCallgraphFilter(val)
 924                elif(arg == '-cgskip'):
 925                        try:
 926                                val = next(args)
 927                        except:
 928                                doError('No file supplied', True)
 929                        if val.lower() in switchoff:
 930                                cgskip = ''
 931                        else:
 932                                cgskip = sysvals.configFile(val)
 933                                if(not cgskip):
 934                                        doError('%s does not exist' % cgskip)
 935                elif(arg == '-bl'):
 936                        try:
 937                                val = next(args)
 938                        except:
 939                                doError('No boot loader name supplied', True)
 940                        if val.lower() not in ['grub']:
 941                                doError('Unknown boot loader: %s' % val, True)
 942                        sysvals.bootloader = val.lower()
 943                elif(arg == '-timeprec'):
 944                        sysvals.setPrecision(aslib.getArgInt('-timeprec', args, 0, 6))
 945                elif(arg == '-maxdepth'):
 946                        mdset = True
 947                        sysvals.max_graph_depth = aslib.getArgInt('-maxdepth', args, 0, 1000)
 948                elif(arg == '-func'):
 949                        try:
 950                                val = next(args)
 951                        except:
 952                                doError('No filter functions supplied', True)
 953                        sysvals.useftrace = True
 954                        sysvals.usecallgraph = True
 955                        sysvals.rootCheck(True)
 956                        sysvals.setGraphFilter(val)
 957                elif(arg == '-ftrace'):
 958                        try:
 959                                val = next(args)
 960                        except:
 961                                doError('No ftrace file supplied', True)
 962                        if(os.path.exists(val) == False):
 963                                doError('%s does not exist' % val)
 964                        testrun = False
 965                        sysvals.ftracefile = val
 966                elif(arg == '-addlogs'):
 967                        sysvals.dmesglog = True
 968                elif(arg == '-expandcg'):
 969                        sysvals.cgexp = True
 970                elif(arg == '-dmesg'):
 971                        try:
 972                                val = next(args)
 973                        except:
 974                                doError('No dmesg file supplied', True)
 975                        if(os.path.exists(val) == False):
 976                                doError('%s does not exist' % val)
 977                        testrun = False
 978                        sysvals.dmesgfile = val
 979                elif(arg == '-o'):
 980                        try:
 981                                val = next(args)
 982                        except:
 983                                doError('No subdirectory name supplied', True)
 984                        sysvals.testdir = sysvals.setOutputFolder(val)
 985                elif(arg == '-result'):
 986                        try:
 987                                val = next(args)
 988                        except:
 989                                doError('No result file supplied', True)
 990                        sysvals.result = val
 991                elif(arg == '-reboot'):
 992                        sysvals.reboot = True
 993                elif(arg == '-manual'):
 994                        sysvals.reboot = True
 995                        sysvals.manual = True
 996                # remaining options are only for cron job use
 997                elif(arg == '-cronjob'):
 998                        sysvals.iscronjob = True
 999                elif(arg == '-which'):
1000                        try:
1001                                val = next(args)
1002                        except:
1003                                doError('No executable supplied', True)
1004                        out = sysvals.getExec(val)
1005                        if not out:
1006                                print('%s not found' % val)
1007                                sys.exit(1)
1008                        print(out)
1009                        sys.exit(0)
1010                else:
1011                        doError('Invalid argument: '+arg, True)
1012
1013        # compatibility errors and access checks
1014        if(sysvals.iscronjob and (sysvals.reboot or \
1015                sysvals.dmesgfile or sysvals.ftracefile or cmd)):
1016                doError('-cronjob is meant for batch purposes only')
1017        if(sysvals.reboot and (sysvals.dmesgfile or sysvals.ftracefile)):
1018                doError('-reboot and -dmesg/-ftrace are incompatible')
1019        if cmd or sysvals.reboot or sysvals.iscronjob or testrun:
1020                sysvals.rootCheck(True)
1021        if (testrun and sysvals.useftrace) or cmd == 'flistall':
1022                if not sysvals.verifyFtrace():
1023                        doError('Ftrace is not properly enabled')
1024
1025        # run utility commands
1026        sysvals.cpuInfo()
1027        if cmd != '':
1028                if cmd == 'kpupdate':
1029                        updateKernelParams()
1030                elif cmd == 'flistall':
1031                        for f in sysvals.getBootFtraceFilterFunctions():
1032                                print(f)
1033                elif cmd == 'checkbl':
1034                        sysvals.getBootLoader()
1035                        pprint('Boot Loader: %s\n%s' % (sysvals.bootloader, sysvals.blexec))
1036                elif(cmd == 'sysinfo'):
1037                        sysvals.printSystemInfo(True)
1038                sys.exit()
1039
1040        # reboot: update grub, setup a cronjob, and reboot
1041        if sysvals.reboot:
1042                if (sysvals.useftrace or sysvals.usecallgraph) and \
1043                        not sysvals.checkFtraceKernelVersion():
1044                        doError('Ftrace functionality requires kernel v4.10 or newer')
1045                if not sysvals.manual:
1046                        updateKernelParams()
1047                        updateCron()
1048                        call('reboot')
1049                else:
1050                        sysvals.manualRebootRequired()
1051                sys.exit()
1052
1053        if sysvals.usecallgraph and cgskip:
1054                sysvals.vprint('Using cgskip file: %s' % cgskip)
1055                sysvals.setCallgraphBlacklist(cgskip)
1056
1057        # cronjob: remove the cronjob, grub changes, and disable ftrace
1058        if sysvals.iscronjob:
1059                updateCron(True)
1060                updateKernelParams(True)
1061                try:
1062                        sysvals.fsetVal('0', 'tracing_on')
1063                except:
1064                        pass
1065
1066        # testrun: generate copies of the logs
1067        if testrun:
1068                retrieveLogs()
1069        else:
1070                sysvals.setOutputFile()
1071
1072        # process the log data
1073        if sysvals.dmesgfile:
1074                if not mdset:
1075                        sysvals.max_graph_depth = 0
1076                data = parseKernelLog()
1077                if(not data.valid):
1078                        doError('No initcall data found in %s' % sysvals.dmesgfile)
1079                if sysvals.useftrace and sysvals.ftracefile:
1080                        parseTraceLog(data)
1081                if sysvals.cgdump:
1082                        data.debugPrint()
1083                        sys.exit()
1084        else:
1085                doError('dmesg file required')
1086
1087        sysvals.vprint('Creating the html timeline (%s)...' % sysvals.htmlfile)
1088        sysvals.vprint('Command:\n    %s' % sysvals.cmdline)
1089        sysvals.vprint('Kernel parameters:\n    %s' % sysvals.kparams)
1090        data.printDetails()
1091        createBootGraph(data)
1092
1093        # if running as root, change output dir owner to sudo_user
1094        if testrun and os.path.isdir(sysvals.testdir) and \
1095                os.getuid() == 0 and 'SUDO_USER' in os.environ:
1096                cmd = 'chown -R {0}:{0} {1} > /dev/null 2>&1'
1097                call(cmd.format(os.environ['SUDO_USER'], sysvals.testdir), shell=True)
1098
1099        sysvals.stamp['boot'] = (data.tUserMode - data.start) * 1000
1100        sysvals.stamp['lastinit'] = data.end * 1000
1101        sysvals.outputResult(sysvals.stamp)
1102