Back to home page

OSCL-LXR

 
 

    


0001 #!/usr/bin/env python3
0002 # SPDX-License-Identifier: GPL-2.0-only
0003 #
0004 # Tool for analyzing boot timing
0005 # Copyright (c) 2013, Intel Corporation.
0006 #
0007 # This program is free software; you can redistribute it and/or modify it
0008 # under the terms and conditions of the GNU General Public License,
0009 # version 2, as published by the Free Software Foundation.
0010 #
0011 # This program is distributed in the hope it will be useful, but WITHOUT
0012 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0013 # FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
0014 # more details.
0015 #
0016 # Authors:
0017 #    Todd Brandt <todd.e.brandt@linux.intel.com>
0018 #
0019 # Description:
0020 #    This tool is designed to assist kernel and OS developers in optimizing
0021 #    their linux stack's boot time. It creates an html representation of
0022 #    the kernel boot timeline up to the start of the init process.
0023 #
0024 
0025 # ----------------- LIBRARIES --------------------
0026 
0027 import sys
0028 import time
0029 import os
0030 import string
0031 import re
0032 import platform
0033 import shutil
0034 from datetime import datetime, timedelta
0035 from subprocess import call, Popen, PIPE
0036 import sleepgraph as aslib
0037 
0038 def pprint(msg):
0039     print(msg)
0040     sys.stdout.flush()
0041 
0042 # ----------------- CLASSES --------------------
0043 
0044 # Class: SystemValues
0045 # Description:
0046 #    A global, single-instance container used to
0047 #    store system values and test parameters
0048 class SystemValues(aslib.SystemValues):
0049     title = 'BootGraph'
0050     version = '2.2'
0051     hostname = 'localhost'
0052     testtime = ''
0053     kernel = ''
0054     dmesgfile = ''
0055     ftracefile = ''
0056     htmlfile = 'bootgraph.html'
0057     testdir = ''
0058     kparams = ''
0059     result = ''
0060     useftrace = False
0061     usecallgraph = False
0062     suspendmode = 'boot'
0063     max_graph_depth = 2
0064     graph_filter = 'do_one_initcall'
0065     reboot = False
0066     manual = False
0067     iscronjob = False
0068     timeformat = '%.6f'
0069     bootloader = 'grub'
0070     blexec = []
0071     def __init__(self):
0072         self.kernel, self.hostname = 'unknown', platform.node()
0073         self.testtime = datetime.now().strftime('%Y-%m-%d_%H:%M:%S')
0074         if os.path.exists('/proc/version'):
0075             fp = open('/proc/version', 'r')
0076             self.kernel = self.kernelVersion(fp.read().strip())
0077             fp.close()
0078         self.testdir = datetime.now().strftime('boot-%y%m%d-%H%M%S')
0079     def kernelVersion(self, msg):
0080         m = re.match('^[Ll]inux *[Vv]ersion *(?P<v>\S*) .*', msg)
0081         if m:
0082             return m.group('v')
0083         return 'unknown'
0084     def checkFtraceKernelVersion(self):
0085         m = re.match('^(?P<x>[0-9]*)\.(?P<y>[0-9]*)\.(?P<z>[0-9]*).*', self.kernel)
0086         if m:
0087             val = tuple(map(int, m.groups()))
0088             if val >= (4, 10, 0):
0089                 return True
0090         return False
0091     def kernelParams(self):
0092         cmdline = 'initcall_debug log_buf_len=32M'
0093         if self.useftrace:
0094             if self.cpucount > 0:
0095                 bs = min(self.memtotal // 2, 2*1024*1024) // self.cpucount
0096             else:
0097                 bs = 131072
0098             cmdline += ' trace_buf_size=%dK trace_clock=global '\
0099             'trace_options=nooverwrite,funcgraph-abstime,funcgraph-cpu,'\
0100             'funcgraph-duration,funcgraph-proc,funcgraph-tail,'\
0101             'nofuncgraph-overhead,context-info,graph-time '\
0102             'ftrace=function_graph '\
0103             'ftrace_graph_max_depth=%d '\
0104             'ftrace_graph_filter=%s' % \
0105                 (bs, self.max_graph_depth, self.graph_filter)
0106         return cmdline
0107     def setGraphFilter(self, val):
0108         master = self.getBootFtraceFilterFunctions()
0109         fs = ''
0110         for i in val.split(','):
0111             func = i.strip()
0112             if func == '':
0113                 doError('badly formatted filter function string')
0114             if '[' in func or ']' in func:
0115                 doError('loadable module functions not allowed - "%s"' % func)
0116             if ' ' in func:
0117                 doError('spaces found in filter functions - "%s"' % func)
0118             if func not in master:
0119                 doError('function "%s" not available for ftrace' % func)
0120             if not fs:
0121                 fs = func
0122             else:
0123                 fs += ','+func
0124         if not fs:
0125             doError('badly formatted filter function string')
0126         self.graph_filter = fs
0127     def getBootFtraceFilterFunctions(self):
0128         self.rootCheck(True)
0129         fp = open(self.tpath+'available_filter_functions')
0130         fulllist = fp.read().split('\n')
0131         fp.close()
0132         list = []
0133         for i in fulllist:
0134             if not i or ' ' in i or '[' in i or ']' in i:
0135                 continue
0136             list.append(i)
0137         return list
0138     def myCronJob(self, line):
0139         if '@reboot' not in line:
0140             return False
0141         if 'bootgraph' in line or 'analyze_boot.py' in line or '-cronjob' in line:
0142             return True
0143         return False
0144     def cronjobCmdString(self):
0145         cmdline = '%s -cronjob' % os.path.abspath(sys.argv[0])
0146         args = iter(sys.argv[1:])
0147         for arg in args:
0148             if arg in ['-h', '-v', '-cronjob', '-reboot', '-verbose']:
0149                 continue
0150             elif arg in ['-o', '-dmesg', '-ftrace', '-func']:
0151                 next(args)
0152                 continue
0153             elif arg == '-result':
0154                 cmdline += ' %s "%s"' % (arg, os.path.abspath(next(args)))
0155                 continue
0156             elif arg == '-cgskip':
0157                 file = self.configFile(next(args))
0158                 cmdline += ' %s "%s"' % (arg, os.path.abspath(file))
0159                 continue
0160             cmdline += ' '+arg
0161         if self.graph_filter != 'do_one_initcall':
0162             cmdline += ' -func "%s"' % self.graph_filter
0163         cmdline += ' -o "%s"' % os.path.abspath(self.testdir)
0164         return cmdline
0165     def manualRebootRequired(self):
0166         cmdline = self.kernelParams()
0167         pprint('To generate a new timeline manually, follow these steps:\n\n'\
0168         '1. Add the CMDLINE string to your kernel command line.\n'\
0169         '2. Reboot the system.\n'\
0170         '3. After reboot, re-run this tool with the same arguments but no command (w/o -reboot or -manual).\n\n'\
0171         'CMDLINE="%s"' % cmdline)
0172         sys.exit()
0173     def blGrub(self):
0174         blcmd = ''
0175         for cmd in ['update-grub', 'grub-mkconfig', 'grub2-mkconfig']:
0176             if blcmd:
0177                 break
0178             blcmd = self.getExec(cmd)
0179         if not blcmd:
0180             doError('[GRUB] missing update command')
0181         if not os.path.exists('/etc/default/grub'):
0182             doError('[GRUB] missing /etc/default/grub')
0183         if 'grub2' in blcmd:
0184             cfg = '/boot/grub2/grub.cfg'
0185         else:
0186             cfg = '/boot/grub/grub.cfg'
0187         if not os.path.exists(cfg):
0188             doError('[GRUB] missing %s' % cfg)
0189         if 'update-grub' in blcmd:
0190             self.blexec = [blcmd]
0191         else:
0192             self.blexec = [blcmd, '-o', cfg]
0193     def getBootLoader(self):
0194         if self.bootloader == 'grub':
0195             self.blGrub()
0196         else:
0197             doError('unknown boot loader: %s' % self.bootloader)
0198     def writeDatafileHeader(self, filename):
0199         self.kparams = open('/proc/cmdline', 'r').read().strip()
0200         fp = open(filename, 'w')
0201         fp.write(self.teststamp+'\n')
0202         fp.write(self.sysstamp+'\n')
0203         fp.write('# command | %s\n' % self.cmdline)
0204         fp.write('# kparams | %s\n' % self.kparams)
0205         fp.close()
0206 
0207 sysvals = SystemValues()
0208 
0209 # Class: Data
0210 # Description:
0211 #    The primary container for test data.
0212 class Data(aslib.Data):
0213     dmesg = {}  # root data structure
0214     start = 0.0 # test start
0215     end = 0.0   # test end
0216     dmesgtext = []   # dmesg text file in memory
0217     testnumber = 0
0218     idstr = ''
0219     html_device_id = 0
0220     valid = False
0221     tUserMode = 0.0
0222     boottime = ''
0223     phases = ['kernel', 'user']
0224     do_one_initcall = False
0225     def __init__(self, num):
0226         self.testnumber = num
0227         self.idstr = 'a'
0228         self.dmesgtext = []
0229         self.dmesg = {
0230             'kernel': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
0231                 'order': 0, 'color': 'linear-gradient(to bottom, #fff, #bcf)'},
0232             'user': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
0233                 'order': 1, 'color': '#fff'}
0234         }
0235     def deviceTopology(self):
0236         return ''
0237     def newAction(self, phase, name, pid, start, end, ret, ulen):
0238         # new device callback for a specific phase
0239         self.html_device_id += 1
0240         devid = '%s%d' % (self.idstr, self.html_device_id)
0241         list = self.dmesg[phase]['list']
0242         length = -1.0
0243         if(start >= 0 and end >= 0):
0244             length = end - start
0245         i = 2
0246         origname = name
0247         while(name in list):
0248             name = '%s[%d]' % (origname, i)
0249             i += 1
0250         list[name] = {'name': name, 'start': start, 'end': end,
0251             'pid': pid, 'length': length, 'row': 0, 'id': devid,
0252             'ret': ret, 'ulen': ulen }
0253         return name
0254     def deviceMatch(self, pid, cg):
0255         if cg.end - cg.start == 0:
0256             return ''
0257         for p in data.phases:
0258             list = self.dmesg[p]['list']
0259             for devname in list:
0260                 dev = list[devname]
0261                 if pid != dev['pid']:
0262                     continue
0263                 if cg.name == 'do_one_initcall':
0264                     if(cg.start <= dev['start'] and cg.end >= dev['end'] and dev['length'] > 0):
0265                         dev['ftrace'] = cg
0266                         self.do_one_initcall = True
0267                         return devname
0268                 else:
0269                     if(cg.start > dev['start'] and cg.end < dev['end']):
0270                         if 'ftraces' not in dev:
0271                             dev['ftraces'] = []
0272                         dev['ftraces'].append(cg)
0273                         return devname
0274         return ''
0275     def printDetails(self):
0276         sysvals.vprint('Timeline Details:')
0277         sysvals.vprint('          Host: %s' % sysvals.hostname)
0278         sysvals.vprint('        Kernel: %s' % sysvals.kernel)
0279         sysvals.vprint('     Test time: %s' % sysvals.testtime)
0280         sysvals.vprint('     Boot time: %s' % self.boottime)
0281         for phase in self.phases:
0282             dc = len(self.dmesg[phase]['list'])
0283             sysvals.vprint('%9s mode: %.3f - %.3f (%d initcalls)' % (phase,
0284                 self.dmesg[phase]['start']*1000,
0285                 self.dmesg[phase]['end']*1000, dc))
0286 
0287 # ----------------- FUNCTIONS --------------------
0288 
0289 # Function: parseKernelLog
0290 # Description:
0291 #    parse a kernel log for boot data
0292 def parseKernelLog():
0293     sysvals.vprint('Analyzing the dmesg data (%s)...' % \
0294         os.path.basename(sysvals.dmesgfile))
0295     phase = 'kernel'
0296     data = Data(0)
0297     data.dmesg['kernel']['start'] = data.start = ktime = 0.0
0298     sysvals.stamp = {
0299         'time': datetime.now().strftime('%B %d %Y, %I:%M:%S %p'),
0300         'host': sysvals.hostname,
0301         'mode': 'boot', 'kernel': ''}
0302 
0303     tp = aslib.TestProps()
0304     devtemp = dict()
0305     if(sysvals.dmesgfile):
0306         lf = open(sysvals.dmesgfile, 'rb')
0307     else:
0308         lf = Popen('dmesg', stdout=PIPE).stdout
0309     for line in lf:
0310         line = aslib.ascii(line).replace('\r\n', '')
0311         # grab the stamp and sysinfo
0312         if re.match(tp.stampfmt, line):
0313             tp.stamp = line
0314             continue
0315         elif re.match(tp.sysinfofmt, line):
0316             tp.sysinfo = line
0317             continue
0318         elif re.match(tp.cmdlinefmt, line):
0319             tp.cmdline = line
0320             continue
0321         elif re.match(tp.kparamsfmt, line):
0322             tp.kparams = line
0323             continue
0324         idx = line.find('[')
0325         if idx > 1:
0326             line = line[idx:]
0327         m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
0328         if(not m):
0329             continue
0330         ktime = float(m.group('ktime'))
0331         if(ktime > 120):
0332             break
0333         msg = m.group('msg')
0334         data.dmesgtext.append(line)
0335         if(ktime == 0.0 and re.match('^Linux version .*', msg)):
0336             if(not sysvals.stamp['kernel']):
0337                 sysvals.stamp['kernel'] = sysvals.kernelVersion(msg)
0338             continue
0339         m = re.match('.* setting system clock to (?P<d>[0-9\-]*)[ A-Z](?P<t>[0-9:]*) UTC.*', msg)
0340         if(m):
0341             bt = datetime.strptime(m.group('d')+' '+m.group('t'), '%Y-%m-%d %H:%M:%S')
0342             bt = bt - timedelta(seconds=int(ktime))
0343             data.boottime = bt.strftime('%Y-%m-%d_%H:%M:%S')
0344             sysvals.stamp['time'] = bt.strftime('%B %d %Y, %I:%M:%S %p')
0345             continue
0346         m = re.match('^calling *(?P<f>.*)\+.* @ (?P<p>[0-9]*)', msg)
0347         if(m):
0348             func = m.group('f')
0349             pid = int(m.group('p'))
0350             devtemp[func] = (ktime, pid)
0351             continue
0352         m = re.match('^initcall *(?P<f>.*)\+.* returned (?P<r>.*) after (?P<t>.*) usecs', msg)
0353         if(m):
0354             data.valid = True
0355             data.end = ktime
0356             f, r, t = m.group('f', 'r', 't')
0357             if(f in devtemp):
0358                 start, pid = devtemp[f]
0359                 data.newAction(phase, f, pid, start, ktime, int(r), int(t))
0360                 del devtemp[f]
0361             continue
0362         if(re.match('^Freeing unused kernel .*', msg)):
0363             data.tUserMode = ktime
0364             data.dmesg['kernel']['end'] = ktime
0365             data.dmesg['user']['start'] = ktime
0366             phase = 'user'
0367 
0368     if tp.stamp:
0369         sysvals.stamp = 0
0370         tp.parseStamp(data, sysvals)
0371     data.dmesg['user']['end'] = data.end
0372     lf.close()
0373     return data
0374 
0375 # Function: parseTraceLog
0376 # Description:
0377 #    Check if trace is available and copy to a temp file
0378 def parseTraceLog(data):
0379     sysvals.vprint('Analyzing the ftrace data (%s)...' % \
0380         os.path.basename(sysvals.ftracefile))
0381     # if available, calculate cgfilter allowable ranges
0382     cgfilter = []
0383     if len(sysvals.cgfilter) > 0:
0384         for p in data.phases:
0385             list = data.dmesg[p]['list']
0386             for i in sysvals.cgfilter:
0387                 if i in list:
0388                     cgfilter.append([list[i]['start']-0.0001,
0389                         list[i]['end']+0.0001])
0390     # parse the trace log
0391     ftemp = dict()
0392     tp = aslib.TestProps()
0393     tp.setTracerType('function_graph')
0394     tf = open(sysvals.ftracefile, 'r')
0395     for line in tf:
0396         if line[0] == '#':
0397             continue
0398         m = re.match(tp.ftrace_line_fmt, line.strip())
0399         if(not m):
0400             continue
0401         m_time, m_proc, m_pid, m_msg, m_dur = \
0402             m.group('time', 'proc', 'pid', 'msg', 'dur')
0403         t = float(m_time)
0404         if len(cgfilter) > 0:
0405             allow = False
0406             for r in cgfilter:
0407                 if t >= r[0] and t < r[1]:
0408                     allow = True
0409                     break
0410             if not allow:
0411                 continue
0412         if t > data.end:
0413             break
0414         if(m_time and m_pid and m_msg):
0415             t = aslib.FTraceLine(m_time, m_msg, m_dur)
0416             pid = int(m_pid)
0417         else:
0418             continue
0419         if t.fevent or t.fkprobe:
0420             continue
0421         key = (m_proc, pid)
0422         if(key not in ftemp):
0423             ftemp[key] = []
0424             ftemp[key].append(aslib.FTraceCallGraph(pid, sysvals))
0425         cg = ftemp[key][-1]
0426         res = cg.addLine(t)
0427         if(res != 0):
0428             ftemp[key].append(aslib.FTraceCallGraph(pid, sysvals))
0429         if(res == -1):
0430             ftemp[key][-1].addLine(t)
0431 
0432     tf.close()
0433 
0434     # add the callgraph data to the device hierarchy
0435     for key in ftemp:
0436         proc, pid = key
0437         for cg in ftemp[key]:
0438             if len(cg.list) < 1 or cg.invalid or (cg.end - cg.start == 0):
0439                 continue
0440             if(not cg.postProcess()):
0441                 pprint('Sanity check failed for %s-%d' % (proc, pid))
0442                 continue
0443             # match cg data to devices
0444             devname = data.deviceMatch(pid, cg)
0445             if not devname:
0446                 kind = 'Orphan'
0447                 if cg.partial:
0448                     kind = 'Partial'
0449                 sysvals.vprint('%s callgraph found for %s %s-%d [%f - %f]' %\
0450                     (kind, cg.name, proc, pid, cg.start, cg.end))
0451             elif len(cg.list) > 1000000:
0452                 pprint('WARNING: the callgraph found for %s is massive! (%d lines)' %\
0453                     (devname, len(cg.list)))
0454 
0455 # Function: retrieveLogs
0456 # Description:
0457 #    Create copies of dmesg and/or ftrace for later processing
0458 def retrieveLogs():
0459     # check ftrace is configured first
0460     if sysvals.useftrace:
0461         tracer = sysvals.fgetVal('current_tracer').strip()
0462         if tracer != 'function_graph':
0463             doError('ftrace not configured for a boot callgraph')
0464     # create the folder and get dmesg
0465     sysvals.systemInfo(aslib.dmidecode(sysvals.mempath))
0466     sysvals.initTestOutput('boot')
0467     sysvals.writeDatafileHeader(sysvals.dmesgfile)
0468     call('dmesg >> '+sysvals.dmesgfile, shell=True)
0469     if not sysvals.useftrace:
0470         return
0471     # get ftrace
0472     sysvals.writeDatafileHeader(sysvals.ftracefile)
0473     call('cat '+sysvals.tpath+'trace >> '+sysvals.ftracefile, shell=True)
0474 
0475 # Function: colorForName
0476 # Description:
0477 #    Generate a repeatable color from a list for a given name
0478 def colorForName(name):
0479     list = [
0480         ('c1', '#ec9999'),
0481         ('c2', '#ffc1a6'),
0482         ('c3', '#fff0a6'),
0483         ('c4', '#adf199'),
0484         ('c5', '#9fadea'),
0485         ('c6', '#a699c1'),
0486         ('c7', '#ad99b4'),
0487         ('c8', '#eaffea'),
0488         ('c9', '#dcecfb'),
0489         ('c10', '#ffffea')
0490     ]
0491     i = 0
0492     total = 0
0493     count = len(list)
0494     while i < len(name):
0495         total += ord(name[i])
0496         i += 1
0497     return list[total % count]
0498 
0499 def cgOverview(cg, minlen):
0500     stats = dict()
0501     large = []
0502     for l in cg.list:
0503         if l.fcall and l.depth == 1:
0504             if l.length >= minlen:
0505                 large.append(l)
0506             if l.name not in stats:
0507                 stats[l.name] = [0, 0.0]
0508             stats[l.name][0] += (l.length * 1000.0)
0509             stats[l.name][1] += 1
0510     return (large, stats)
0511 
0512 # Function: createBootGraph
0513 # Description:
0514 #    Create the output html file from the resident test data
0515 # Arguments:
0516 #    testruns: array of Data objects from parseKernelLog or parseTraceLog
0517 # Output:
0518 #    True if the html file was created, false if it failed
0519 def createBootGraph(data):
0520     # html function templates
0521     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'
0522     html_timetotal = '<table class="time1">\n<tr>'\
0523         '<td class="blue">Init process starts @ <b>{0} ms</b></td>'\
0524         '<td class="blue">Last initcall ends @ <b>{1} ms</b></td>'\
0525         '</tr>\n</table>\n'
0526 
0527     # device timeline
0528     devtl = aslib.Timeline(100, 20)
0529 
0530     # write the test title and general info header
0531     devtl.createHeader(sysvals, sysvals.stamp)
0532 
0533     # Generate the header for this timeline
0534     t0 = data.start
0535     tMax = data.end
0536     tTotal = tMax - t0
0537     if(tTotal == 0):
0538         pprint('ERROR: No timeline data')
0539         return False
0540     user_mode = '%.0f'%(data.tUserMode*1000)
0541     last_init = '%.0f'%(tTotal*1000)
0542     devtl.html += html_timetotal.format(user_mode, last_init)
0543 
0544     # determine the maximum number of rows we need to draw
0545     devlist = []
0546     for p in data.phases:
0547         list = data.dmesg[p]['list']
0548         for devname in list:
0549             d = aslib.DevItem(0, p, list[devname])
0550             devlist.append(d)
0551         devtl.getPhaseRows(devlist, 0, 'start')
0552     devtl.calcTotalRows()
0553 
0554     # draw the timeline background
0555     devtl.createZoomBox()
0556     devtl.html += devtl.html_tblock.format('boot', '0', '100', devtl.scaleH)
0557     for p in data.phases:
0558         phase = data.dmesg[p]
0559         length = phase['end']-phase['start']
0560         left = '%.3f' % (((phase['start']-t0)*100.0)/tTotal)
0561         width = '%.3f' % ((length*100.0)/tTotal)
0562         devtl.html += devtl.html_phase.format(left, width, \
0563             '%.3f'%devtl.scaleH, '%.3f'%devtl.bodyH, \
0564             phase['color'], '')
0565 
0566     # draw the device timeline
0567     num = 0
0568     devstats = dict()
0569     for phase in data.phases:
0570         list = data.dmesg[phase]['list']
0571         for devname in sorted(list):
0572             cls, color = colorForName(devname)
0573             dev = list[devname]
0574             info = '@|%.3f|%.3f|%.3f|%d' % (dev['start']*1000.0, dev['end']*1000.0,
0575                 dev['ulen']/1000.0, dev['ret'])
0576             devstats[dev['id']] = {'info':info}
0577             dev['color'] = color
0578             height = devtl.phaseRowHeight(0, phase, dev['row'])
0579             top = '%.6f' % ((dev['row']*height) + devtl.scaleH)
0580             left = '%.6f' % (((dev['start']-t0)*100)/tTotal)
0581             width = '%.6f' % (((dev['end']-dev['start'])*100)/tTotal)
0582             length = ' (%0.3f ms) ' % ((dev['end']-dev['start'])*1000)
0583             devtl.html += devtl.html_device.format(dev['id'],
0584                 devname+length+phase+'_mode', left, top, '%.3f'%height,
0585                 width, devname, ' '+cls, '')
0586             rowtop = devtl.phaseRowTop(0, phase, dev['row'])
0587             height = '%.6f' % (devtl.rowH / 2)
0588             top = '%.6f' % (rowtop + devtl.scaleH + (devtl.rowH / 2))
0589             if data.do_one_initcall:
0590                 if('ftrace' not in dev):
0591                     continue
0592                 cg = dev['ftrace']
0593                 large, stats = cgOverview(cg, 0.001)
0594                 devstats[dev['id']]['fstat'] = stats
0595                 for l in large:
0596                     left = '%f' % (((l.time-t0)*100)/tTotal)
0597                     width = '%f' % (l.length*100/tTotal)
0598                     title = '%s (%0.3fms)' % (l.name, l.length * 1000.0)
0599                     devtl.html += html_srccall.format(l.name, left,
0600                         top, height, width, title, 'x%d'%num)
0601                     num += 1
0602                 continue
0603             if('ftraces' not in dev):
0604                 continue
0605             for cg in dev['ftraces']:
0606                 left = '%f' % (((cg.start-t0)*100)/tTotal)
0607                 width = '%f' % ((cg.end-cg.start)*100/tTotal)
0608                 cglen = (cg.end - cg.start) * 1000.0
0609                 title = '%s (%0.3fms)' % (cg.name, cglen)
0610                 cg.id = 'x%d' % num
0611                 devtl.html += html_srccall.format(cg.name, left,
0612                     top, height, width, title, dev['id']+cg.id)
0613                 num += 1
0614 
0615     # draw the time scale, try to make the number of labels readable
0616     devtl.createTimeScale(t0, tMax, tTotal, 'boot')
0617     devtl.html += '</div>\n'
0618 
0619     # timeline is finished
0620     devtl.html += '</div>\n</div>\n'
0621 
0622     # draw a legend which describes the phases by color
0623     devtl.html += '<div class="legend">\n'
0624     pdelta = 20.0
0625     pmargin = 36.0
0626     for phase in data.phases:
0627         order = '%.2f' % ((data.dmesg[phase]['order'] * pdelta) + pmargin)
0628         devtl.html += devtl.html_legend.format(order, \
0629             data.dmesg[phase]['color'], phase+'_mode', phase[0])
0630     devtl.html += '</div>\n'
0631 
0632     hf = open(sysvals.htmlfile, 'w')
0633 
0634     # add the css
0635     extra = '\
0636         .c1 {background:rgba(209,0,0,0.4);}\n\
0637         .c2 {background:rgba(255,102,34,0.4);}\n\
0638         .c3 {background:rgba(255,218,33,0.4);}\n\
0639         .c4 {background:rgba(51,221,0,0.4);}\n\
0640         .c5 {background:rgba(17,51,204,0.4);}\n\
0641         .c6 {background:rgba(34,0,102,0.4);}\n\
0642         .c7 {background:rgba(51,0,68,0.4);}\n\
0643         .c8 {background:rgba(204,255,204,0.4);}\n\
0644         .c9 {background:rgba(169,208,245,0.4);}\n\
0645         .c10 {background:rgba(255,255,204,0.4);}\n\
0646         .vt {transform:rotate(-60deg);transform-origin:0 0;}\n\
0647         table.fstat {table-layout:fixed;padding:150px 15px 0 0;font-size:10px;column-width:30px;}\n\
0648         .fstat th {width:55px;}\n\
0649         .fstat td {text-align:left;width:35px;}\n\
0650         .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\
0651         .srccall:hover {color:white;font-weight:bold;border:1px solid white;}\n'
0652     aslib.addCSS(hf, sysvals, 1, False, extra)
0653 
0654     # write the device timeline
0655     hf.write(devtl.html)
0656 
0657     # add boot specific html
0658     statinfo = 'var devstats = {\n'
0659     for n in sorted(devstats):
0660         statinfo += '\t"%s": [\n\t\t"%s",\n' % (n, devstats[n]['info'])
0661         if 'fstat' in devstats[n]:
0662             funcs = devstats[n]['fstat']
0663             for f in sorted(funcs, key=lambda k:(funcs[k], k), reverse=True):
0664                 if funcs[f][0] < 0.01 and len(funcs) > 10:
0665                     break
0666                 statinfo += '\t\t"%f|%s|%d",\n' % (funcs[f][0], f, funcs[f][1])
0667         statinfo += '\t],\n'
0668     statinfo += '};\n'
0669     html = \
0670         '<div id="devicedetailtitle"></div>\n'\
0671         '<div id="devicedetail" style="display:none;">\n'\
0672         '<div id="devicedetail0">\n'
0673     for p in data.phases:
0674         phase = data.dmesg[p]
0675         html += devtl.html_phaselet.format(p+'_mode', '0', '100', phase['color'])
0676     html += '</div>\n</div>\n'\
0677         '<script type="text/javascript">\n'+statinfo+\
0678         '</script>\n'
0679     hf.write(html)
0680 
0681     # add the callgraph html
0682     if(sysvals.usecallgraph):
0683         aslib.addCallgraphs(sysvals, hf, data)
0684 
0685     # add the test log as a hidden div
0686     if sysvals.testlog and sysvals.logmsg:
0687         hf.write('<div id="testlog" style="display:none;">\n'+sysvals.logmsg+'</div>\n')
0688     # add the dmesg log as a hidden div
0689     if sysvals.dmesglog:
0690         hf.write('<div id="dmesglog" style="display:none;">\n')
0691         for line in data.dmesgtext:
0692             line = line.replace('<', '&lt').replace('>', '&gt')
0693             hf.write(line)
0694         hf.write('</div>\n')
0695 
0696     # write the footer and close
0697     aslib.addScriptCode(hf, [data])
0698     hf.write('</body>\n</html>\n')
0699     hf.close()
0700     return True
0701 
0702 # Function: updateCron
0703 # Description:
0704 #    (restore=False) Set the tool to run automatically on reboot
0705 #    (restore=True) Restore the original crontab
0706 def updateCron(restore=False):
0707     if not restore:
0708         sysvals.rootUser(True)
0709     crondir = '/var/spool/cron/crontabs/'
0710     if not os.path.exists(crondir):
0711         crondir = '/var/spool/cron/'
0712     if not os.path.exists(crondir):
0713         doError('%s not found' % crondir)
0714     cronfile = crondir+'root'
0715     backfile = crondir+'root-analyze_boot-backup'
0716     cmd = sysvals.getExec('crontab')
0717     if not cmd:
0718         doError('crontab not found')
0719     # on restore: move the backup cron back into place
0720     if restore:
0721         if os.path.exists(backfile):
0722             shutil.move(backfile, cronfile)
0723             call([cmd, cronfile])
0724         return
0725     # backup current cron and install new one with reboot
0726     if os.path.exists(cronfile):
0727         shutil.move(cronfile, backfile)
0728     else:
0729         fp = open(backfile, 'w')
0730         fp.close()
0731     res = -1
0732     try:
0733         fp = open(backfile, 'r')
0734         op = open(cronfile, 'w')
0735         for line in fp:
0736             if not sysvals.myCronJob(line):
0737                 op.write(line)
0738                 continue
0739         fp.close()
0740         op.write('@reboot python %s\n' % sysvals.cronjobCmdString())
0741         op.close()
0742         res = call([cmd, cronfile])
0743     except Exception as e:
0744         pprint('Exception: %s' % str(e))
0745         shutil.move(backfile, cronfile)
0746         res = -1
0747     if res != 0:
0748         doError('crontab failed')
0749 
0750 # Function: updateGrub
0751 # Description:
0752 #    update grub.cfg for all kernels with our parameters
0753 def updateGrub(restore=False):
0754     # call update-grub on restore
0755     if restore:
0756         try:
0757             call(sysvals.blexec, stderr=PIPE, stdout=PIPE,
0758                 env={'PATH': '.:/sbin:/usr/sbin:/usr/bin:/sbin:/bin'})
0759         except Exception as e:
0760             pprint('Exception: %s\n' % str(e))
0761         return
0762     # extract the option and create a grub config without it
0763     sysvals.rootUser(True)
0764     tgtopt = 'GRUB_CMDLINE_LINUX_DEFAULT'
0765     cmdline = ''
0766     grubfile = '/etc/default/grub'
0767     tempfile = '/etc/default/grub.analyze_boot'
0768     shutil.move(grubfile, tempfile)
0769     res = -1
0770     try:
0771         fp = open(tempfile, 'r')
0772         op = open(grubfile, 'w')
0773         cont = False
0774         for line in fp:
0775             line = line.strip()
0776             if len(line) == 0 or line[0] == '#':
0777                 continue
0778             opt = line.split('=')[0].strip()
0779             if opt == tgtopt:
0780                 cmdline = line.split('=', 1)[1].strip('\\')
0781                 if line[-1] == '\\':
0782                     cont = True
0783             elif cont:
0784                 cmdline += line.strip('\\')
0785                 if line[-1] != '\\':
0786                     cont = False
0787             else:
0788                 op.write('%s\n' % line)
0789         fp.close()
0790         # if the target option value is in quotes, strip them
0791         sp = '"'
0792         val = cmdline.strip()
0793         if val and (val[0] == '\'' or val[0] == '"'):
0794             sp = val[0]
0795             val = val.strip(sp)
0796         cmdline = val
0797         # append our cmd line options
0798         if len(cmdline) > 0:
0799             cmdline += ' '
0800         cmdline += sysvals.kernelParams()
0801         # write out the updated target option
0802         op.write('\n%s=%s%s%s\n' % (tgtopt, sp, cmdline, sp))
0803         op.close()
0804         res = call(sysvals.blexec)
0805         os.remove(grubfile)
0806     except Exception as e:
0807         pprint('Exception: %s' % str(e))
0808         res = -1
0809     # cleanup
0810     shutil.move(tempfile, grubfile)
0811     if res != 0:
0812         doError('update grub failed')
0813 
0814 # Function: updateKernelParams
0815 # Description:
0816 #    update boot conf for all kernels with our parameters
0817 def updateKernelParams(restore=False):
0818     # find the boot loader
0819     sysvals.getBootLoader()
0820     if sysvals.bootloader == 'grub':
0821         updateGrub(restore)
0822 
0823 # Function: doError Description:
0824 #    generic error function for catastrphic failures
0825 # Arguments:
0826 #    msg: the error message to print
0827 #    help: True if printHelp should be called after, False otherwise
0828 def doError(msg, help=False):
0829     if help == True:
0830         printHelp()
0831     pprint('ERROR: %s\n' % msg)
0832     sysvals.outputResult({'error':msg})
0833     sys.exit()
0834 
0835 # Function: printHelp
0836 # Description:
0837 #    print out the help text
0838 def printHelp():
0839     pprint('\n%s v%s\n'\
0840     'Usage: bootgraph <options> <command>\n'\
0841     '\n'\
0842     'Description:\n'\
0843     '  This tool reads in a dmesg log of linux kernel boot and\n'\
0844     '  creates an html representation of the boot timeline up to\n'\
0845     '  the start of the init process.\n'\
0846     '\n'\
0847     '  If no specific command is given the tool reads the current dmesg\n'\
0848     '  and/or ftrace log and creates a timeline\n'\
0849     '\n'\
0850     '  Generates output files in subdirectory: boot-yymmdd-HHMMSS\n'\
0851     '   HTML output:                    <hostname>_boot.html\n'\
0852     '   raw dmesg output:               <hostname>_boot_dmesg.txt\n'\
0853     '   raw ftrace output:              <hostname>_boot_ftrace.txt\n'\
0854     '\n'\
0855     'Options:\n'\
0856     '  -h            Print this help text\n'\
0857     '  -v            Print the current tool version\n'\
0858     '  -verbose      Print extra information during execution and analysis\n'\
0859     '  -addlogs      Add the dmesg log to the html output\n'\
0860     '  -result fn    Export a results table to a text file for parsing.\n'\
0861     '  -o name       Overrides the output subdirectory name when running a new test\n'\
0862     '                default: boot-{date}-{time}\n'\
0863     ' [advanced]\n'\
0864     '  -fstat        Use ftrace to add function detail and statistics (default: disabled)\n'\
0865     '  -f/-callgraph Add callgraph detail, can be very large (default: disabled)\n'\
0866     '  -maxdepth N   limit the callgraph data to N call levels (default: 2)\n'\
0867     '  -mincg ms     Discard all callgraphs shorter than ms milliseconds (e.g. 0.001 for us)\n'\
0868     '  -timeprec N   Number of significant digits in timestamps (0:S, 3:ms, [6:us])\n'\
0869     '  -expandcg     pre-expand the callgraph data in the html output (default: disabled)\n'\
0870     '  -func list    Limit ftrace to comma-delimited list of functions (default: do_one_initcall)\n'\
0871     '  -cgfilter S   Filter the callgraph output in the timeline\n'\
0872     '  -cgskip file  Callgraph functions to skip, off to disable (default: cgskip.txt)\n'\
0873     '  -bl name      Use the following boot loader for kernel params (default: grub)\n'\
0874     '  -reboot       Reboot the machine automatically and generate a new timeline\n'\
0875     '  -manual       Show the steps to generate a new timeline manually (used with -reboot)\n'\
0876     '\n'\
0877     'Other commands:\n'\
0878     '  -flistall     Print all functions capable of being captured in ftrace\n'\
0879     '  -sysinfo      Print out system info extracted from BIOS\n'\
0880     '  -which exec   Print an executable path, should function even without PATH\n'\
0881     ' [redo]\n'\
0882     '  -dmesg file   Create HTML output using dmesg input (used with -ftrace)\n'\
0883     '  -ftrace file  Create HTML output using ftrace input (used with -dmesg)\n'\
0884     '' % (sysvals.title, sysvals.version))
0885     return True
0886 
0887 # ----------------- MAIN --------------------
0888 # exec start (skipped if script is loaded as library)
0889 if __name__ == '__main__':
0890     # loop through the command line arguments
0891     cmd = ''
0892     testrun = True
0893     switchoff = ['disable', 'off', 'false', '0']
0894     simplecmds = ['-sysinfo', '-kpupdate', '-flistall', '-checkbl']
0895     cgskip = ''
0896     if '-f' in sys.argv:
0897         cgskip = sysvals.configFile('cgskip.txt')
0898     args = iter(sys.argv[1:])
0899     mdset = False
0900     for arg in args:
0901         if(arg == '-h'):
0902             printHelp()
0903             sys.exit()
0904         elif(arg == '-v'):
0905             pprint("Version %s" % sysvals.version)
0906             sys.exit()
0907         elif(arg == '-verbose'):
0908             sysvals.verbose = True
0909         elif(arg in simplecmds):
0910             cmd = arg[1:]
0911         elif(arg == '-fstat'):
0912             sysvals.useftrace = True
0913         elif(arg == '-callgraph' or arg == '-f'):
0914             sysvals.useftrace = True
0915             sysvals.usecallgraph = True
0916         elif(arg == '-cgdump'):
0917             sysvals.cgdump = True
0918         elif(arg == '-mincg'):
0919             sysvals.mincglen = aslib.getArgFloat('-mincg', args, 0.0, 10000.0)
0920         elif(arg == '-cgfilter'):
0921             try:
0922                 val = next(args)
0923             except:
0924                 doError('No callgraph functions supplied', True)
0925             sysvals.setCallgraphFilter(val)
0926         elif(arg == '-cgskip'):
0927             try:
0928                 val = next(args)
0929             except:
0930                 doError('No file supplied', True)
0931             if val.lower() in switchoff:
0932                 cgskip = ''
0933             else:
0934                 cgskip = sysvals.configFile(val)
0935                 if(not cgskip):
0936                     doError('%s does not exist' % cgskip)
0937         elif(arg == '-bl'):
0938             try:
0939                 val = next(args)
0940             except:
0941                 doError('No boot loader name supplied', True)
0942             if val.lower() not in ['grub']:
0943                 doError('Unknown boot loader: %s' % val, True)
0944             sysvals.bootloader = val.lower()
0945         elif(arg == '-timeprec'):
0946             sysvals.setPrecision(aslib.getArgInt('-timeprec', args, 0, 6))
0947         elif(arg == '-maxdepth'):
0948             mdset = True
0949             sysvals.max_graph_depth = aslib.getArgInt('-maxdepth', args, 0, 1000)
0950         elif(arg == '-func'):
0951             try:
0952                 val = next(args)
0953             except:
0954                 doError('No filter functions supplied', True)
0955             sysvals.useftrace = True
0956             sysvals.usecallgraph = True
0957             sysvals.rootCheck(True)
0958             sysvals.setGraphFilter(val)
0959         elif(arg == '-ftrace'):
0960             try:
0961                 val = next(args)
0962             except:
0963                 doError('No ftrace file supplied', True)
0964             if(os.path.exists(val) == False):
0965                 doError('%s does not exist' % val)
0966             testrun = False
0967             sysvals.ftracefile = val
0968         elif(arg == '-addlogs'):
0969             sysvals.dmesglog = True
0970         elif(arg == '-expandcg'):
0971             sysvals.cgexp = True
0972         elif(arg == '-dmesg'):
0973             try:
0974                 val = next(args)
0975             except:
0976                 doError('No dmesg file supplied', True)
0977             if(os.path.exists(val) == False):
0978                 doError('%s does not exist' % val)
0979             testrun = False
0980             sysvals.dmesgfile = val
0981         elif(arg == '-o'):
0982             try:
0983                 val = next(args)
0984             except:
0985                 doError('No subdirectory name supplied', True)
0986             sysvals.testdir = sysvals.setOutputFolder(val)
0987         elif(arg == '-result'):
0988             try:
0989                 val = next(args)
0990             except:
0991                 doError('No result file supplied', True)
0992             sysvals.result = val
0993         elif(arg == '-reboot'):
0994             sysvals.reboot = True
0995         elif(arg == '-manual'):
0996             sysvals.reboot = True
0997             sysvals.manual = True
0998         # remaining options are only for cron job use
0999         elif(arg == '-cronjob'):
1000             sysvals.iscronjob = True
1001         elif(arg == '-which'):
1002             try:
1003                 val = next(args)
1004             except:
1005                 doError('No executable supplied', True)
1006             out = sysvals.getExec(val)
1007             if not out:
1008                 print('%s not found' % val)
1009                 sys.exit(1)
1010             print(out)
1011             sys.exit(0)
1012         else:
1013             doError('Invalid argument: '+arg, True)
1014 
1015     # compatibility errors and access checks
1016     if(sysvals.iscronjob and (sysvals.reboot or \
1017         sysvals.dmesgfile or sysvals.ftracefile or cmd)):
1018         doError('-cronjob is meant for batch purposes only')
1019     if(sysvals.reboot and (sysvals.dmesgfile or sysvals.ftracefile)):
1020         doError('-reboot and -dmesg/-ftrace are incompatible')
1021     if cmd or sysvals.reboot or sysvals.iscronjob or testrun:
1022         sysvals.rootCheck(True)
1023     if (testrun and sysvals.useftrace) or cmd == 'flistall':
1024         if not sysvals.verifyFtrace():
1025             doError('Ftrace is not properly enabled')
1026 
1027     # run utility commands
1028     sysvals.cpuInfo()
1029     if cmd != '':
1030         if cmd == 'kpupdate':
1031             updateKernelParams()
1032         elif cmd == 'flistall':
1033             for f in sysvals.getBootFtraceFilterFunctions():
1034                 print(f)
1035         elif cmd == 'checkbl':
1036             sysvals.getBootLoader()
1037             pprint('Boot Loader: %s\n%s' % (sysvals.bootloader, sysvals.blexec))
1038         elif(cmd == 'sysinfo'):
1039             sysvals.printSystemInfo(True)
1040         sys.exit()
1041 
1042     # reboot: update grub, setup a cronjob, and reboot
1043     if sysvals.reboot:
1044         if (sysvals.useftrace or sysvals.usecallgraph) and \
1045             not sysvals.checkFtraceKernelVersion():
1046             doError('Ftrace functionality requires kernel v4.10 or newer')
1047         if not sysvals.manual:
1048             updateKernelParams()
1049             updateCron()
1050             call('reboot')
1051         else:
1052             sysvals.manualRebootRequired()
1053         sys.exit()
1054 
1055     if sysvals.usecallgraph and cgskip:
1056         sysvals.vprint('Using cgskip file: %s' % cgskip)
1057         sysvals.setCallgraphBlacklist(cgskip)
1058 
1059     # cronjob: remove the cronjob, grub changes, and disable ftrace
1060     if sysvals.iscronjob:
1061         updateCron(True)
1062         updateKernelParams(True)
1063         try:
1064             sysvals.fsetVal('0', 'tracing_on')
1065         except:
1066             pass
1067 
1068     # testrun: generate copies of the logs
1069     if testrun:
1070         retrieveLogs()
1071     else:
1072         sysvals.setOutputFile()
1073 
1074     # process the log data
1075     if sysvals.dmesgfile:
1076         if not mdset:
1077             sysvals.max_graph_depth = 0
1078         data = parseKernelLog()
1079         if(not data.valid):
1080             doError('No initcall data found in %s' % sysvals.dmesgfile)
1081         if sysvals.useftrace and sysvals.ftracefile:
1082             parseTraceLog(data)
1083         if sysvals.cgdump:
1084             data.debugPrint()
1085             sys.exit()
1086     else:
1087         doError('dmesg file required')
1088 
1089     sysvals.vprint('Creating the html timeline (%s)...' % sysvals.htmlfile)
1090     sysvals.vprint('Command:\n    %s' % sysvals.cmdline)
1091     sysvals.vprint('Kernel parameters:\n    %s' % sysvals.kparams)
1092     data.printDetails()
1093     createBootGraph(data)
1094 
1095     # if running as root, change output dir owner to sudo_user
1096     if testrun and os.path.isdir(sysvals.testdir) and \
1097         os.getuid() == 0 and 'SUDO_USER' in os.environ:
1098         cmd = 'chown -R {0}:{0} {1} > /dev/null 2>&1'
1099         call(cmd.format(os.environ['SUDO_USER'], sysvals.testdir), shell=True)
1100 
1101     sysvals.stamp['boot'] = (data.tUserMode - data.start) * 1000
1102     sysvals.stamp['lastinit'] = data.end * 1000
1103     sysvals.outputResult(sysvals.stamp)