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