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