Linux Audio

Check our new training course

Loading...
Note: File does not exist in v3.1.
   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)