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