3 # Tool for analyzing boot timing
4 # Copyright (c) 2013, Intel Corporation.
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.
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
16 # Todd Brandt <todd.e.brandt@linux.intel.com>
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.
24 # ----------------- LIBRARIES --------------------
33 from datetime
import datetime
, timedelta
34 from subprocess
import call
, Popen
, PIPE
35 import analyze_suspend
as aslib
37 # ----------------- CLASSES --------------------
41 # A global, single-instance container used to
42 # store system values and test parameters
43 class SystemValues(aslib
.SystemValues
):
46 hostname
= 'localhost'
51 htmlfile
= 'bootgraph.html'
54 testdirprefix
= 'boot'
64 graph_filter
= 'do_one_initcall'
72 if('LOG_FILE' in os
.environ
and 'TEST_RESULTS_IDENTIFIER' in os
.environ
):
75 self
.outfile
= os
.environ
['LOG_FILE']
76 self
.htmlfile
= os
.environ
['LOG_FILE']
77 self
.hostname
= platform
.node()
78 self
.testtime
= datetime
.now().strftime('%Y-%m-%d_%H:%M:%S')
79 if os
.path
.exists('/proc/version'):
80 fp
= open('/proc/version', 'r')
81 val
= fp
.read().strip()
83 self
.kernel
= self
.kernelVersion(val
)
85 self
.kernel
= 'unknown'
86 self
.testdir
= datetime
.now().strftime('boot-%y%m%d-%H%M%S')
87 def kernelVersion(self
, msg
):
89 def checkFtraceKernelVersion(self
):
90 val
= tuple(map(int, self
.kernel
.split('-')[0].split('.')))
94 def kernelParams(self
):
95 cmdline
= 'initcall_debug log_buf_len=32M'
98 bs
= min(self
.memtotal
/ 2, 2*1024*1024) / self
.cpucount
101 cmdline
+= ' trace_buf_size=%dK trace_clock=global '\
102 'trace_options=nooverwrite,funcgraph-abstime,funcgraph-cpu,'\
103 'funcgraph-duration,funcgraph-proc,funcgraph-tail,'\
104 'nofuncgraph-overhead,context-info,graph-time '\
105 'ftrace=function_graph '\
106 'ftrace_graph_max_depth=%d '\
107 'ftrace_graph_filter=%s' % \
108 (bs
, self
.max_graph_depth
, self
.graph_filter
)
110 def setGraphFilter(self
, val
):
111 master
= self
.getBootFtraceFilterFunctions()
113 for i
in val
.split(','):
116 doError('badly formatted filter function string')
117 if '[' in func
or ']' in func
:
118 doError('loadable module functions not allowed - "%s"' % func
)
120 doError('spaces found in filter functions - "%s"' % func
)
121 if func
not in master
:
122 doError('function "%s" not available for ftrace' % func
)
128 doError('badly formatted filter function string')
129 self
.graph_filter
= fs
130 def getBootFtraceFilterFunctions(self
):
132 fp
= open(self
.tpath
+'available_filter_functions')
133 fulllist
= fp
.read().split('\n')
137 if not i
or ' ' in i
or '[' in i
or ']' in i
:
141 def myCronJob(self
, line
):
142 if '@reboot' not in line
:
144 if 'bootgraph' in line
or 'analyze_boot.py' in line
or '-cronjob' in line
:
147 def cronjobCmdString(self
):
148 cmdline
= '%s -cronjob' % os
.path
.abspath(sys
.argv
[0])
149 args
= iter(sys
.argv
[1:])
151 if arg
in ['-h', '-v', '-cronjob', '-reboot']:
153 elif arg
in ['-o', '-dmesg', '-ftrace', '-func']:
157 if self
.graph_filter
!= 'do_one_initcall':
158 cmdline
+= ' -func "%s"' % self
.graph_filter
159 cmdline
+= ' -o "%s"' % os
.path
.abspath(self
.testdir
)
161 def manualRebootRequired(self
):
162 cmdline
= self
.kernelParams()
163 print 'To generate a new timeline manually, follow these steps:\n'
164 print '1. Add the CMDLINE string to your kernel command line.'
165 print '2. Reboot the system.'
166 print '3. After reboot, re-run this tool with the same arguments but no command (w/o -reboot or -manual).\n'
167 print 'CMDLINE="%s"' % cmdline
169 def getExec(self
, cmd
):
170 dirlist
= ['/sbin', '/bin', '/usr/sbin', '/usr/bin',
171 '/usr/local/sbin', '/usr/local/bin']
173 cmdfull
= os
.path
.join(path
, cmd
)
174 if os
.path
.exists(cmdfull
):
179 for cmd
in ['update-grub', 'grub-mkconfig', 'grub2-mkconfig']:
182 blcmd
= self
.getExec(cmd
)
184 doError('[GRUB] missing update command')
185 if not os
.path
.exists('/etc/default/grub'):
186 doError('[GRUB] missing /etc/default/grub')
188 cfg
= '/boot/grub2/grub.cfg'
190 cfg
= '/boot/grub/grub.cfg'
191 if not os
.path
.exists(cfg
):
192 doError('[GRUB] missing %s' % cfg
)
193 if 'update-grub' in blcmd
:
194 self
.blexec
= [blcmd
]
196 self
.blexec
= [blcmd
, '-o', cfg
]
197 def getBootLoader(self
):
198 if self
.bootloader
== 'grub':
201 doError('unknown boot loader: %s' % self
.bootloader
)
203 sysvals
= SystemValues()
207 # The primary container for test data.
208 class Data(aslib
.Data
):
209 dmesg
= {} # root data structure
210 start
= 0.0 # test start
212 dmesgtext
= [] # dmesg text file in memory
219 phases
= ['kernel', 'user']
220 do_one_initcall
= False
221 def __init__(self
, num
):
222 self
.testnumber
= num
226 'kernel': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
227 'order': 0, 'color': 'linear-gradient(to bottom, #fff, #bcf)'},
228 'user': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
229 'order': 1, 'color': '#fff'}
231 def deviceTopology(self
):
233 def newAction(self
, phase
, name
, pid
, start
, end
, ret
, ulen
):
234 # new device callback for a specific phase
235 self
.html_device_id
+= 1
236 devid
= '%s%d' % (self
.idstr
, self
.html_device_id
)
237 list = self
.dmesg
[phase
]['list']
239 if(start
>= 0 and end
>= 0):
244 name
= '%s[%d]' % (origname
, i
)
246 list[name
] = {'name': name
, 'start': start
, 'end': end
,
247 'pid': pid
, 'length': length
, 'row': 0, 'id': devid
,
248 'ret': ret
, 'ulen': ulen
}
250 def deviceMatch(self
, pid
, cg
):
251 if cg
.end
- cg
.start
== 0:
253 for p
in data
.phases
:
254 list = self
.dmesg
[p
]['list']
257 if pid
!= dev
['pid']:
259 if cg
.name
== 'do_one_initcall':
260 if(cg
.start
<= dev
['start'] and cg
.end
>= dev
['end'] and dev
['length'] > 0):
262 self
.do_one_initcall
= True
265 if(cg
.start
> dev
['start'] and cg
.end
< dev
['end']):
266 if 'ftraces' not in dev
:
268 dev
['ftraces'].append(cg
)
272 # ----------------- FUNCTIONS --------------------
274 # Function: parseKernelLog
276 # parse a kernel log for boot data
277 def parseKernelLog():
280 data
.dmesg
['kernel']['start'] = data
.start
= ktime
= 0.0
282 'time': datetime
.now().strftime('%B %d %Y, %I:%M:%S %p'),
283 'host': sysvals
.hostname
,
284 'mode': 'boot', 'kernel': ''}
286 tp
= aslib
.TestProps()
288 if(sysvals
.dmesgfile
):
289 lf
= open(sysvals
.dmesgfile
, 'r')
291 lf
= Popen('dmesg', stdout
=PIPE
).stdout
293 line
= line
.replace('\r\n', '')
294 # grab the stamp and sysinfo
295 if re
.match(tp
.stampfmt
, line
):
298 elif re
.match(tp
.sysinfofmt
, line
):
304 m
= re
.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line
)
307 ktime
= float(m
.group('ktime'))
311 data
.dmesgtext
.append(line
)
312 if(ktime
== 0.0 and re
.match('^Linux version .*', msg
)):
313 if(not sysvals
.stamp
['kernel']):
314 sysvals
.stamp
['kernel'] = sysvals
.kernelVersion(msg
)
316 m
= re
.match('.* setting system clock to (?P<t>.*) UTC.*', msg
)
318 bt
= datetime
.strptime(m
.group('t'), '%Y-%m-%d %H:%M:%S')
319 bt
= bt
- timedelta(seconds
=int(ktime
))
320 data
.boottime
= bt
.strftime('%Y-%m-%d_%H:%M:%S')
321 sysvals
.stamp
['time'] = bt
.strftime('%B %d %Y, %I:%M:%S %p')
323 m
= re
.match('^calling *(?P<f>.*)\+.* @ (?P<p>[0-9]*)', msg
)
326 pid
= int(m
.group('p'))
327 devtemp
[func
] = (ktime
, pid
)
329 m
= re
.match('^initcall *(?P<f>.*)\+.* returned (?P<r>.*) after (?P<t>.*) usecs', msg
)
333 f
, r
, t
= m
.group('f', 'r', 't')
335 start
, pid
= devtemp
[f
]
336 data
.newAction(phase
, f
, pid
, start
, ktime
, int(r
), int(t
))
339 if(re
.match('^Freeing unused kernel memory.*', msg
)):
340 data
.tUserMode
= ktime
341 data
.dmesg
['kernel']['end'] = ktime
342 data
.dmesg
['user']['start'] = ktime
347 tp
.parseStamp(data
, sysvals
)
348 data
.dmesg
['user']['end'] = data
.end
352 # Function: parseTraceLog
354 # Check if trace is available and copy to a temp file
355 def parseTraceLog(data
):
356 # parse the trace log
358 tp
= aslib
.TestProps()
359 tp
.setTracerType('function_graph')
360 tf
= open(sysvals
.ftracefile
, 'r')
364 m
= re
.match(tp
.ftrace_line_fmt
, line
.strip())
367 m_time
, m_proc
, m_pid
, m_msg
, m_dur
= \
368 m
.group('time', 'proc', 'pid', 'msg', 'dur')
369 if float(m_time
) > data
.end
:
371 if(m_time
and m_pid
and m_msg
):
372 t
= aslib
.FTraceLine(m_time
, m_msg
, m_dur
)
376 if t
.fevent
or t
.fkprobe
:
379 if(key
not in ftemp
):
381 ftemp
[key
].append(aslib
.FTraceCallGraph(pid
))
384 ftemp
[key
].append(aslib
.FTraceCallGraph(pid
))
387 # add the callgraph data to the device hierarchy
390 for cg
in ftemp
[key
]:
391 if len(cg
.list) < 1 or cg
.invalid
:
393 if(not cg
.postProcess()):
394 print('Sanity check failed for %s-%d' % (proc
, pid
))
396 # match cg data to devices
397 if not data
.deviceMatch(pid
, cg
):
398 print ' BAD: %s %s-%d [%f - %f]' % (cg
.name
, proc
, pid
, cg
.start
, cg
.end
)
400 # Function: retrieveLogs
402 # Create copies of dmesg and/or ftrace for later processing
404 # check ftrace is configured first
405 if sysvals
.useftrace
:
406 tracer
= sysvals
.fgetVal('current_tracer').strip()
407 if tracer
!= 'function_graph':
408 doError('ftrace not configured for a boot callgraph')
409 # create the folder and get dmesg
410 sysvals
.systemInfo(aslib
.dmidecode(sysvals
.mempath
))
411 sysvals
.initTestOutput('boot')
412 sysvals
.writeDatafileHeader(sysvals
.dmesgfile
)
413 call('dmesg >> '+sysvals
.dmesgfile
, shell
=True)
414 if not sysvals
.useftrace
:
417 sysvals
.writeDatafileHeader(sysvals
.ftracefile
)
418 call('cat '+sysvals
.tpath
+'trace >> '+sysvals
.ftracefile
, shell
=True)
420 # Function: colorForName
422 # Generate a repeatable color from a list for a given name
423 def colorForName(name
):
440 total
+= ord(name
[i
])
442 return list[total
% count
]
444 def cgOverview(cg
, minlen
):
448 if l
.fcall
and l
.depth
== 1:
449 if l
.length
>= minlen
:
451 if l
.name
not in stats
:
452 stats
[l
.name
] = [0, 0.0]
453 stats
[l
.name
][0] += (l
.length
* 1000.0)
454 stats
[l
.name
][1] += 1
455 return (large
, stats
)
457 # Function: createBootGraph
459 # Create the output html file from the resident test data
461 # testruns: array of Data objects from parseKernelLog or parseTraceLog
463 # True if the html file was created, false if it failed
464 def createBootGraph(data
):
465 # html function templates
466 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'
467 html_timetotal
= '<table class="time1">\n<tr>'\
468 '<td class="blue">Init process starts @ <b>{0} ms</b></td>'\
469 '<td class="blue">Last initcall ends @ <b>{1} ms</b></td>'\
473 devtl
= aslib
.Timeline(100, 20)
475 # write the test title and general info header
476 devtl
.createHeader(sysvals
)
478 # Generate the header for this timeline
483 print('ERROR: No timeline data')
485 user_mode
= '%.0f'%(data
.tUserMode
*1000)
486 last_init
= '%.0f'%(tTotal
*1000)
487 devtl
.html
+= html_timetotal
.format(user_mode
, last_init
)
489 # determine the maximum number of rows we need to draw
491 for p
in data
.phases
:
492 list = data
.dmesg
[p
]['list']
494 d
= aslib
.DevItem(0, p
, list[devname
])
496 devtl
.getPhaseRows(devlist
, 0, 'start')
497 devtl
.calcTotalRows()
499 # draw the timeline background
500 devtl
.createZoomBox()
501 devtl
.html
+= devtl
.html_tblock
.format('boot', '0', '100', devtl
.scaleH
)
502 for p
in data
.phases
:
503 phase
= data
.dmesg
[p
]
504 length
= phase
['end']-phase
['start']
505 left
= '%.3f' % (((phase
['start']-t0
)*100.0)/tTotal
)
506 width
= '%.3f' % ((length
*100.0)/tTotal
)
507 devtl
.html
+= devtl
.html_phase
.format(left
, width
, \
508 '%.3f'%devtl
.scaleH
, '%.3f'%devtl
.bodyH
, \
511 # draw the device timeline
514 for phase
in data
.phases
:
515 list = data
.dmesg
[phase
]['list']
516 for devname
in sorted(list):
517 cls
, color
= colorForName(devname
)
519 info
= '@|%.3f|%.3f|%.3f|%d' % (dev
['start']*1000.0, dev
['end']*1000.0,
520 dev
['ulen']/1000.0, dev
['ret'])
521 devstats
[dev
['id']] = {'info':info
}
523 height
= devtl
.phaseRowHeight(0, phase
, dev
['row'])
524 top
= '%.6f' % ((dev
['row']*height
) + devtl
.scaleH
)
525 left
= '%.6f' % (((dev
['start']-t0
)*100)/tTotal
)
526 width
= '%.6f' % (((dev
['end']-dev
['start'])*100)/tTotal
)
527 length
= ' (%0.3f ms) ' % ((dev
['end']-dev
['start'])*1000)
528 devtl
.html
+= devtl
.html_device
.format(dev
['id'],
529 devname
+length
+phase
+'_mode', left
, top
, '%.3f'%height
,
530 width
, devname
, ' '+cls
, '')
531 rowtop
= devtl
.phaseRowTop(0, phase
, dev
['row'])
532 height
= '%.6f' % (devtl
.rowH
/ 2)
533 top
= '%.6f' % (rowtop
+ devtl
.scaleH
+ (devtl
.rowH
/ 2))
534 if data
.do_one_initcall
:
535 if('ftrace' not in dev
):
538 large
, stats
= cgOverview(cg
, 0.001)
539 devstats
[dev
['id']]['fstat'] = stats
541 left
= '%f' % (((l
.time
-t0
)*100)/tTotal
)
542 width
= '%f' % (l
.length
*100/tTotal
)
543 title
= '%s (%0.3fms)' % (l
.name
, l
.length
* 1000.0)
544 devtl
.html
+= html_srccall
.format(l
.name
, left
,
545 top
, height
, width
, title
, 'x%d'%num
)
548 if('ftraces' not in dev
):
550 for cg
in dev
['ftraces']:
551 left
= '%f' % (((cg
.start
-t0
)*100)/tTotal
)
552 width
= '%f' % ((cg
.end
-cg
.start
)*100/tTotal
)
553 cglen
= (cg
.end
- cg
.start
) * 1000.0
554 title
= '%s (%0.3fms)' % (cg
.name
, cglen
)
556 devtl
.html
+= html_srccall
.format(cg
.name
, left
,
557 top
, height
, width
, title
, dev
['id']+cg
.id)
560 # draw the time scale, try to make the number of labels readable
561 devtl
.createTimeScale(t0
, tMax
, tTotal
, 'boot')
562 devtl
.html
+= '</div>\n'
564 # timeline is finished
565 devtl
.html
+= '</div>\n</div>\n'
567 # draw a legend which describes the phases by color
568 devtl
.html
+= '<div class="legend">\n'
571 for phase
in data
.phases
:
572 order
= '%.2f' % ((data
.dmesg
[phase
]['order'] * pdelta
) + pmargin
)
573 devtl
.html
+= devtl
.html_legend
.format(order
, \
574 data
.dmesg
[phase
]['color'], phase
+'_mode', phase
[0])
575 devtl
.html
+= '</div>\n'
577 if(sysvals
.outfile
== sysvals
.htmlfile
):
578 hf
= open(sysvals
.htmlfile
, 'a')
580 hf
= open(sysvals
.htmlfile
, 'w')
582 # add the css if this is not an embedded run
584 .c1 {background:rgba(209,0,0,0.4);}\n\
585 .c2 {background:rgba(255,102,34,0.4);}\n\
586 .c3 {background:rgba(255,218,33,0.4);}\n\
587 .c4 {background:rgba(51,221,0,0.4);}\n\
588 .c5 {background:rgba(17,51,204,0.4);}\n\
589 .c6 {background:rgba(34,0,102,0.4);}\n\
590 .c7 {background:rgba(51,0,68,0.4);}\n\
591 .c8 {background:rgba(204,255,204,0.4);}\n\
592 .c9 {background:rgba(169,208,245,0.4);}\n\
593 .c10 {background:rgba(255,255,204,0.4);}\n\
594 .vt {transform:rotate(-60deg);transform-origin:0 0;}\n\
595 table.fstat {table-layout:fixed;padding:150px 15px 0 0;font-size:10px;column-width:30px;}\n\
596 .fstat th {width:55px;}\n\
597 .fstat td {text-align:left;width:35px;}\n\
598 .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\
599 .srccall:hover {color:white;font-weight:bold;border:1px solid white;}\n'
600 if(not sysvals
.embedded
):
601 aslib
.addCSS(hf
, sysvals
, 1, False, extra
)
603 # write the device timeline
606 # add boot specific html
607 statinfo
= 'var devstats = {\n'
608 for n
in sorted(devstats
):
609 statinfo
+= '\t"%s": [\n\t\t"%s",\n' % (n
, devstats
[n
]['info'])
610 if 'fstat' in devstats
[n
]:
611 funcs
= devstats
[n
]['fstat']
612 for f
in sorted(funcs
, key
=funcs
.get
, reverse
=True):
613 if funcs
[f
][0] < 0.01 and len(funcs
) > 10:
615 statinfo
+= '\t\t"%f|%s|%d",\n' % (funcs
[f
][0], f
, funcs
[f
][1])
619 '<div id="devicedetailtitle"></div>\n'\
620 '<div id="devicedetail" style="display:none;">\n'\
621 '<div id="devicedetail0">\n'
622 for p
in data
.phases
:
623 phase
= data
.dmesg
[p
]
624 html
+= devtl
.html_phaselet
.format(p
+'_mode', '0', '100', phase
['color'])
625 html
+= '</div>\n</div>\n'\
626 '<script type="text/javascript">\n'+statinfo
+\
630 # add the callgraph html
631 if(sysvals
.usecallgraph
):
632 aslib
.addCallgraphs(sysvals
, hf
, data
)
634 # add the dmesg log as a hidden div
636 hf
.write('<div id="dmesglog" style="display:none;">\n')
637 for line
in data
.dmesgtext
:
638 line
= line
.replace('<', '<').replace('>', '>')
642 if(not sysvals
.embedded
):
643 # write the footer and close
644 aslib
.addScriptCode(hf
, [data
])
645 hf
.write('</body>\n</html>\n')
647 # embedded out will be loaded in a page, skip the js
648 hf
.write('<div id=bounds style=display:none>%f,%f</div>' % \
649 (data
.start
*1000, data
.end
*1000))
653 # Function: updateCron
655 # (restore=False) Set the tool to run automatically on reboot
656 # (restore=True) Restore the original crontab
657 def updateCron(restore
=False):
659 sysvals
.rootUser(True)
660 crondir
= '/var/spool/cron/crontabs/'
661 if not os
.path
.exists(crondir
):
662 crondir
= '/var/spool/cron/'
663 if not os
.path
.exists(crondir
):
664 doError('%s not found' % crondir
)
665 cronfile
= crondir
+'root'
666 backfile
= crondir
+'root-analyze_boot-backup'
667 cmd
= sysvals
.getExec('crontab')
669 doError('crontab not found')
670 # on restore: move the backup cron back into place
672 if os
.path
.exists(backfile
):
673 shutil
.move(backfile
, cronfile
)
674 call([cmd
, cronfile
])
676 # backup current cron and install new one with reboot
677 if os
.path
.exists(cronfile
):
678 shutil
.move(cronfile
, backfile
)
680 fp
= open(backfile
, 'w')
684 fp
= open(backfile
, 'r')
685 op
= open(cronfile
, 'w')
687 if not sysvals
.myCronJob(line
):
691 op
.write('@reboot python %s\n' % sysvals
.cronjobCmdString())
693 res
= call([cmd
, cronfile
])
695 print 'Exception: %s' % str(e
)
696 shutil
.move(backfile
, cronfile
)
699 doError('crontab failed')
701 # Function: updateGrub
703 # update grub.cfg for all kernels with our parameters
704 def updateGrub(restore
=False):
705 # call update-grub on restore
708 call(sysvals
.blexec
, stderr
=PIPE
, stdout
=PIPE
,
709 env
={'PATH': '.:/sbin:/usr/sbin:/usr/bin:/sbin:/bin'})
711 print 'Exception: %s\n' % str(e
)
713 # extract the option and create a grub config without it
714 sysvals
.rootUser(True)
715 tgtopt
= 'GRUB_CMDLINE_LINUX_DEFAULT'
717 grubfile
= '/etc/default/grub'
718 tempfile
= '/etc/default/grub.analyze_boot'
719 shutil
.move(grubfile
, tempfile
)
722 fp
= open(tempfile
, 'r')
723 op
= open(grubfile
, 'w')
727 if len(line
) == 0 or line
[0] == '#':
729 opt
= line
.split('=')[0].strip()
731 cmdline
= line
.split('=', 1)[1].strip('\\')
735 cmdline
+= line
.strip('\\')
739 op
.write('%s\n' % line
)
741 # if the target option value is in quotes, strip them
743 val
= cmdline
.strip()
744 if val
and (val
[0] == '\'' or val
[0] == '"'):
748 # append our cmd line options
751 cmdline
+= sysvals
.kernelParams()
752 # write out the updated target option
753 op
.write('\n%s=%s%s%s\n' % (tgtopt
, sp
, cmdline
, sp
))
755 res
= call(sysvals
.blexec
)
758 print 'Exception: %s' % str(e
)
761 shutil
.move(tempfile
, grubfile
)
763 doError('update grub failed')
765 # Function: updateKernelParams
767 # update boot conf for all kernels with our parameters
768 def updateKernelParams(restore
=False):
769 # find the boot loader
770 sysvals
.getBootLoader()
771 if sysvals
.bootloader
== 'grub':
774 # Function: doError Description:
775 # generic error function for catastrphic failures
777 # msg: the error message to print
778 # help: True if printHelp should be called after, False otherwise
779 def doError(msg
, help=False):
782 print 'ERROR: %s\n' % msg
785 # Function: printHelp
787 # print out the help text
790 print('%s v%s' % (sysvals
.title
, sysvals
.version
))
791 print('Usage: bootgraph <options> <command>')
793 print('Description:')
794 print(' This tool reads in a dmesg log of linux kernel boot and')
795 print(' creates an html representation of the boot timeline up to')
796 print(' the start of the init process.')
798 print(' If no specific command is given the tool reads the current dmesg')
799 print(' and/or ftrace log and creates a timeline')
801 print(' Generates output files in subdirectory: boot-yymmdd-HHMMSS')
802 print(' HTML output: <hostname>_boot.html')
803 print(' raw dmesg output: <hostname>_boot_dmesg.txt')
804 print(' raw ftrace output: <hostname>_boot_ftrace.txt')
807 print(' -h Print this help text')
808 print(' -v Print the current tool version')
809 print(' -addlogs Add the dmesg log to the html output')
810 print(' -o name Overrides the output subdirectory name when running a new test')
811 print(' default: boot-{date}-{time}')
813 print(' -f Use ftrace to add function detail (default: disabled)')
814 print(' -callgraph Add callgraph detail, can be very large (default: disabled)')
815 print(' -maxdepth N limit the callgraph data to N call levels (default: 2)')
816 print(' -mincg ms Discard all callgraphs shorter than ms milliseconds (e.g. 0.001 for us)')
817 print(' -timeprec N Number of significant digits in timestamps (0:S, 3:ms, [6:us])')
818 print(' -expandcg pre-expand the callgraph data in the html output (default: disabled)')
819 print(' -func list Limit ftrace to comma-delimited list of functions (default: do_one_initcall)')
820 print(' -cgfilter S Filter the callgraph output in the timeline')
821 print(' -bl name Use the following boot loader for kernel params (default: grub)')
822 print(' -reboot Reboot the machine automatically and generate a new timeline')
823 print(' -manual Show the steps to generate a new timeline manually (used with -reboot)')
825 print('Other commands:')
826 print(' -flistall Print all functions capable of being captured in ftrace')
827 print(' -sysinfo Print out system info extracted from BIOS')
829 print(' -dmesg file Create HTML output using dmesg input (used with -ftrace)')
830 print(' -ftrace file Create HTML output using ftrace input (used with -dmesg)')
834 # ----------------- MAIN --------------------
835 # exec start (skipped if script is loaded as library)
836 if __name__
== '__main__':
837 # loop through the command line arguments
840 simplecmds
= ['-sysinfo', '-kpupdate', '-flistall', '-checkbl']
841 args
= iter(sys
.argv
[1:])
847 print("Version %s" % sysvals
.version
)
849 elif(arg
in simplecmds
):
852 sysvals
.useftrace
= True
853 elif(arg
== '-callgraph'):
854 sysvals
.useftrace
= True
855 sysvals
.usecallgraph
= True
856 elif(arg
== '-mincg'):
857 sysvals
.mincglen
= aslib
.getArgFloat('-mincg', args
, 0.0, 10000.0)
858 elif(arg
== '-cgfilter'):
862 doError('No callgraph functions supplied', True)
863 sysvals
.setDeviceFilter(val
)
868 doError('No boot loader name supplied', True)
869 if val
.lower() not in ['grub']:
870 doError('Unknown boot loader: %s' % val
, True)
871 sysvals
.bootloader
= val
.lower()
872 elif(arg
== '-timeprec'):
873 sysvals
.setPrecision(aslib
.getArgInt('-timeprec', args
, 0, 6))
874 elif(arg
== '-maxdepth'):
875 sysvals
.max_graph_depth
= aslib
.getArgInt('-maxdepth', args
, 0, 1000)
876 elif(arg
== '-func'):
880 doError('No filter functions supplied', True)
881 sysvals
.useftrace
= True
882 sysvals
.usecallgraph
= True
883 sysvals
.rootCheck(True)
884 sysvals
.setGraphFilter(val
)
885 elif(arg
== '-ftrace'):
889 doError('No ftrace file supplied', True)
890 if(os
.path
.exists(val
) == False):
891 doError('%s does not exist' % val
)
893 sysvals
.ftracefile
= val
894 elif(arg
== '-addlogs'):
895 sysvals
.dmesglog
= True
896 elif(arg
== '-expandcg'):
898 elif(arg
== '-dmesg'):
902 doError('No dmesg file supplied', True)
903 if(os
.path
.exists(val
) == False):
904 doError('%s does not exist' % val
)
905 if(sysvals
.htmlfile
== val
or sysvals
.outfile
== val
):
906 doError('Output filename collision')
908 sysvals
.dmesgfile
= val
913 doError('No subdirectory name supplied', True)
914 sysvals
.testdir
= sysvals
.setOutputFolder(val
)
915 elif(arg
== '-reboot'):
916 sysvals
.reboot
= True
917 elif(arg
== '-manual'):
918 sysvals
.reboot
= True
919 sysvals
.manual
= True
920 # remaining options are only for cron job use
921 elif(arg
== '-cronjob'):
922 sysvals
.iscronjob
= True
924 doError('Invalid argument: '+arg
, True)
926 # compatibility errors and access checks
927 if(sysvals
.iscronjob
and (sysvals
.reboot
or \
928 sysvals
.dmesgfile
or sysvals
.ftracefile
or cmd
)):
929 doError('-cronjob is meant for batch purposes only')
930 if(sysvals
.reboot
and (sysvals
.dmesgfile
or sysvals
.ftracefile
)):
931 doError('-reboot and -dmesg/-ftrace are incompatible')
932 if cmd
or sysvals
.reboot
or sysvals
.iscronjob
or testrun
:
933 sysvals
.rootCheck(True)
934 if (testrun
and sysvals
.useftrace
) or cmd
== 'flistall':
935 if not sysvals
.verifyFtrace():
936 doError('Ftrace is not properly enabled')
938 # run utility commands
941 if cmd
== 'kpupdate':
943 elif cmd
== 'flistall':
944 for f
in sysvals
.getBootFtraceFilterFunctions():
946 elif cmd
== 'checkbl':
947 sysvals
.getBootLoader()
948 print 'Boot Loader: %s\n%s' % (sysvals
.bootloader
, sysvals
.blexec
)
949 elif(cmd
== 'sysinfo'):
950 sysvals
.printSystemInfo()
953 # reboot: update grub, setup a cronjob, and reboot
955 if (sysvals
.useftrace
or sysvals
.usecallgraph
) and \
956 not sysvals
.checkFtraceKernelVersion():
957 doError('Ftrace functionality requires kernel v4.10 or newer')
958 if not sysvals
.manual
:
963 sysvals
.manualRebootRequired()
966 # cronjob: remove the cronjob, grub changes, and disable ftrace
967 if sysvals
.iscronjob
:
969 updateKernelParams(True)
971 sysvals
.fsetVal('0', 'tracing_on')
975 # testrun: generate copies of the logs
979 sysvals
.setOutputFile()
981 # process the log data
982 if sysvals
.dmesgfile
:
983 data
= parseKernelLog()
985 doError('No initcall data found in %s' % sysvals
.dmesgfile
)
986 if sysvals
.useftrace
and sysvals
.ftracefile
:
989 doError('dmesg file required')
991 print(' Host: %s' % sysvals
.hostname
)
992 print(' Test time: %s' % sysvals
.testtime
)
993 print(' Boot time: %s' % data
.boottime
)
994 print('Kernel Version: %s' % sysvals
.kernel
)
995 print(' Kernel start: %.3f' % (data
.start
* 1000))
996 print('Usermode start: %.3f' % (data
.tUserMode
* 1000))
997 print('Last Init Call: %.3f' % (data
.end
* 1000))
999 # handle embedded output logs
1000 if(sysvals
.outfile
and sysvals
.embedded
):
1001 fp
= open(sysvals
.outfile
, 'w')
1002 fp
.write('pass %s initstart %.3f end %.3f boot %s\n' %
1003 (data
.valid
, data
.tUserMode
*1000, data
.end
*1000, data
.boottime
))
1006 createBootGraph(data
)
1008 # if running as root, change output dir owner to sudo_user
1009 if testrun
and os
.path
.isdir(sysvals
.testdir
) and \
1010 os
.getuid() == 0 and 'SUDO_USER' in os
.environ
:
1011 cmd
= 'chown -R {0}:{0} {1} > /dev/null 2>&1'
1012 call(cmd
.format(os
.environ
['SUDO_USER'], sysvals
.testdir
), shell
=True)