Linux Audio

Check our new training course

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