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