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 sleepgraph
as aslib
41 # ----------------- CLASSES --------------------
45 # A global, single-instance container used to
46 # store system values and test parameters
47 class SystemValues(aslib
.SystemValues
):
50 hostname
= 'localhost'
55 htmlfile
= 'bootgraph.html'
63 graph_filter
= 'do_one_initcall'
71 self
.hostname
= platform
.node()
72 self
.testtime
= datetime
.now().strftime('%Y-%m-%d_%H:%M:%S')
73 if os
.path
.exists('/proc/version'):
74 fp
= open('/proc/version', 'r')
75 val
= fp
.read().strip()
77 self
.kernel
= self
.kernelVersion(val
)
79 self
.kernel
= 'unknown'
80 self
.testdir
= datetime
.now().strftime('boot-%y%m%d-%H%M%S')
81 def kernelVersion(self
, msg
):
83 def checkFtraceKernelVersion(self
):
84 val
= tuple(map(int, self
.kernel
.split('-')[0].split('.')))
88 def kernelParams(self
):
89 cmdline
= 'initcall_debug log_buf_len=32M'
92 bs
= min(self
.memtotal
/ 2, 2*1024*1024) / self
.cpucount
95 cmdline
+= ' trace_buf_size=%dK trace_clock=global '\
96 'trace_options=nooverwrite,funcgraph-abstime,funcgraph-cpu,'\
97 'funcgraph-duration,funcgraph-proc,funcgraph-tail,'\
98 'nofuncgraph-overhead,context-info,graph-time '\
99 'ftrace=function_graph '\
100 'ftrace_graph_max_depth=%d '\
101 'ftrace_graph_filter=%s' % \
102 (bs
, self
.max_graph_depth
, self
.graph_filter
)
104 def setGraphFilter(self
, val
):
105 master
= self
.getBootFtraceFilterFunctions()
107 for i
in val
.split(','):
110 doError('badly formatted filter function string')
111 if '[' in func
or ']' in func
:
112 doError('loadable module functions not allowed - "%s"' % func
)
114 doError('spaces found in filter functions - "%s"' % func
)
115 if func
not in master
:
116 doError('function "%s" not available for ftrace' % func
)
122 doError('badly formatted filter function string')
123 self
.graph_filter
= fs
124 def getBootFtraceFilterFunctions(self
):
126 fp
= open(self
.tpath
+'available_filter_functions')
127 fulllist
= fp
.read().split('\n')
131 if not i
or ' ' in i
or '[' in i
or ']' in i
:
135 def myCronJob(self
, line
):
136 if '@reboot' not in line
:
138 if 'bootgraph' in line
or 'analyze_boot.py' in line
or '-cronjob' in line
:
141 def cronjobCmdString(self
):
142 cmdline
= '%s -cronjob' % os
.path
.abspath(sys
.argv
[0])
143 args
= iter(sys
.argv
[1:])
145 if arg
in ['-h', '-v', '-cronjob', '-reboot', '-verbose']:
147 elif arg
in ['-o', '-dmesg', '-ftrace', '-func']:
150 elif arg
== '-result':
151 cmdline
+= ' %s "%s"' % (arg
, os
.path
.abspath(args
.next()))
153 elif arg
== '-cgskip':
154 file = self
.configFile(args
.next())
155 cmdline
+= ' %s "%s"' % (arg
, os
.path
.abspath(file))
158 if self
.graph_filter
!= 'do_one_initcall':
159 cmdline
+= ' -func "%s"' % self
.graph_filter
160 cmdline
+= ' -o "%s"' % os
.path
.abspath(self
.testdir
)
162 def manualRebootRequired(self
):
163 cmdline
= self
.kernelParams()
164 pprint('To generate a new timeline manually, follow these steps:\n\n'\
165 '1. Add the CMDLINE string to your kernel command line.\n'\
166 '2. Reboot the system.\n'\
167 '3. After reboot, re-run this tool with the same arguments but no command (w/o -reboot or -manual).\n\n'\
168 'CMDLINE="%s"' % cmdline
)
172 for cmd
in ['update-grub', 'grub-mkconfig', 'grub2-mkconfig']:
175 blcmd
= self
.getExec(cmd
)
177 doError('[GRUB] missing update command')
178 if not os
.path
.exists('/etc/default/grub'):
179 doError('[GRUB] missing /etc/default/grub')
181 cfg
= '/boot/grub2/grub.cfg'
183 cfg
= '/boot/grub/grub.cfg'
184 if not os
.path
.exists(cfg
):
185 doError('[GRUB] missing %s' % cfg
)
186 if 'update-grub' in blcmd
:
187 self
.blexec
= [blcmd
]
189 self
.blexec
= [blcmd
, '-o', cfg
]
190 def getBootLoader(self
):
191 if self
.bootloader
== 'grub':
194 doError('unknown boot loader: %s' % self
.bootloader
)
195 def writeDatafileHeader(self
, filename
):
196 self
.kparams
= open('/proc/cmdline', 'r').read().strip()
197 fp
= open(filename
, 'w')
198 fp
.write(self
.teststamp
+'\n')
199 fp
.write(self
.sysstamp
+'\n')
200 fp
.write('# command | %s\n' % self
.cmdline
)
201 fp
.write('# kparams | %s\n' % self
.kparams
)
204 sysvals
= SystemValues()
208 # The primary container for test data.
209 class Data(aslib
.Data
):
210 dmesg
= {} # root data structure
211 start
= 0.0 # test start
213 dmesgtext
= [] # dmesg text file in memory
220 phases
= ['kernel', 'user']
221 do_one_initcall
= False
222 def __init__(self
, num
):
223 self
.testnumber
= num
227 'kernel': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
228 'order': 0, 'color': 'linear-gradient(to bottom, #fff, #bcf)'},
229 'user': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0,
230 'order': 1, 'color': '#fff'}
232 def deviceTopology(self
):
234 def newAction(self
, phase
, name
, pid
, start
, end
, ret
, ulen
):
235 # new device callback for a specific phase
236 self
.html_device_id
+= 1
237 devid
= '%s%d' % (self
.idstr
, self
.html_device_id
)
238 list = self
.dmesg
[phase
]['list']
240 if(start
>= 0 and end
>= 0):
245 name
= '%s[%d]' % (origname
, i
)
247 list[name
] = {'name': name
, 'start': start
, 'end': end
,
248 'pid': pid
, 'length': length
, 'row': 0, 'id': devid
,
249 'ret': ret
, 'ulen': ulen
}
251 def deviceMatch(self
, pid
, cg
):
252 if cg
.end
- cg
.start
== 0:
254 for p
in data
.phases
:
255 list = self
.dmesg
[p
]['list']
258 if pid
!= dev
['pid']:
260 if cg
.name
== 'do_one_initcall':
261 if(cg
.start
<= dev
['start'] and cg
.end
>= dev
['end'] and dev
['length'] > 0):
263 self
.do_one_initcall
= True
266 if(cg
.start
> dev
['start'] and cg
.end
< dev
['end']):
267 if 'ftraces' not in dev
:
269 dev
['ftraces'].append(cg
)
272 def printDetails(self
):
273 sysvals
.vprint('Timeline Details:')
274 sysvals
.vprint(' Host: %s' % sysvals
.hostname
)
275 sysvals
.vprint(' Kernel: %s' % sysvals
.kernel
)
276 sysvals
.vprint(' Test time: %s' % sysvals
.testtime
)
277 sysvals
.vprint(' Boot time: %s' % self
.boottime
)
278 for phase
in self
.phases
:
279 dc
= len(self
.dmesg
[phase
]['list'])
280 sysvals
.vprint('%9s mode: %.3f - %.3f (%d initcalls)' % (phase
,
281 self
.dmesg
[phase
]['start']*1000,
282 self
.dmesg
[phase
]['end']*1000, dc
))
284 # ----------------- FUNCTIONS --------------------
286 # Function: parseKernelLog
288 # parse a kernel log for boot data
289 def parseKernelLog():
290 sysvals
.vprint('Analyzing the dmesg data (%s)...' % \
291 os
.path
.basename(sysvals
.dmesgfile
))
294 data
.dmesg
['kernel']['start'] = data
.start
= ktime
= 0.0
296 'time': datetime
.now().strftime('%B %d %Y, %I:%M:%S %p'),
297 'host': sysvals
.hostname
,
298 'mode': 'boot', 'kernel': ''}
300 tp
= aslib
.TestProps()
302 if(sysvals
.dmesgfile
):
303 lf
= open(sysvals
.dmesgfile
, 'r')
305 lf
= Popen('dmesg', stdout
=PIPE
).stdout
307 line
= line
.replace('\r\n', '')
308 # grab the stamp and sysinfo
309 if re
.match(tp
.stampfmt
, line
):
312 elif re
.match(tp
.sysinfofmt
, line
):
315 elif re
.match(tp
.cmdlinefmt
, line
):
318 elif re
.match(tp
.kparamsfmt
, line
):
324 m
= re
.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line
)
327 ktime
= float(m
.group('ktime'))
331 data
.dmesgtext
.append(line
)
332 if(ktime
== 0.0 and re
.match('^Linux version .*', msg
)):
333 if(not sysvals
.stamp
['kernel']):
334 sysvals
.stamp
['kernel'] = sysvals
.kernelVersion(msg
)
336 m
= re
.match('.* setting system clock to (?P<t>.*) UTC.*', msg
)
338 bt
= datetime
.strptime(m
.group('t'), '%Y-%m-%d %H:%M:%S')
339 bt
= bt
- timedelta(seconds
=int(ktime
))
340 data
.boottime
= bt
.strftime('%Y-%m-%d_%H:%M:%S')
341 sysvals
.stamp
['time'] = bt
.strftime('%B %d %Y, %I:%M:%S %p')
343 m
= re
.match('^calling *(?P<f>.*)\+.* @ (?P<p>[0-9]*)', msg
)
346 pid
= int(m
.group('p'))
347 devtemp
[func
] = (ktime
, pid
)
349 m
= re
.match('^initcall *(?P<f>.*)\+.* returned (?P<r>.*) after (?P<t>.*) usecs', msg
)
353 f
, r
, t
= m
.group('f', 'r', 't')
355 start
, pid
= devtemp
[f
]
356 data
.newAction(phase
, f
, pid
, start
, ktime
, int(r
), int(t
))
359 if(re
.match('^Freeing unused kernel memory.*', msg
)):
360 data
.tUserMode
= ktime
361 data
.dmesg
['kernel']['end'] = ktime
362 data
.dmesg
['user']['start'] = ktime
367 tp
.parseStamp(data
, sysvals
)
368 data
.dmesg
['user']['end'] = data
.end
372 # Function: parseTraceLog
374 # Check if trace is available and copy to a temp file
375 def parseTraceLog(data
):
376 sysvals
.vprint('Analyzing the ftrace data (%s)...' % \
377 os
.path
.basename(sysvals
.ftracefile
))
378 # if available, calculate cgfilter allowable ranges
380 if len(sysvals
.cgfilter
) > 0:
381 for p
in data
.phases
:
382 list = data
.dmesg
[p
]['list']
383 for i
in sysvals
.cgfilter
:
385 cgfilter
.append([list[i
]['start']-0.0001,
386 list[i
]['end']+0.0001])
387 # parse the trace log
389 tp
= aslib
.TestProps()
390 tp
.setTracerType('function_graph')
391 tf
= open(sysvals
.ftracefile
, 'r')
395 m
= re
.match(tp
.ftrace_line_fmt
, line
.strip())
398 m_time
, m_proc
, m_pid
, m_msg
, m_dur
= \
399 m
.group('time', 'proc', 'pid', 'msg', 'dur')
401 if len(cgfilter
) > 0:
404 if t
>= r
[0] and t
< r
[1]:
411 if(m_time
and m_pid
and m_msg
):
412 t
= aslib
.FTraceLine(m_time
, m_msg
, m_dur
)
416 if t
.fevent
or t
.fkprobe
:
419 if(key
not in ftemp
):
421 ftemp
[key
].append(aslib
.FTraceCallGraph(pid
, sysvals
))
425 ftemp
[key
].append(aslib
.FTraceCallGraph(pid
, sysvals
))
427 ftemp
[key
][-1].addLine(t
)
431 # add the callgraph data to the device hierarchy
434 for cg
in ftemp
[key
]:
435 if len(cg
.list) < 1 or cg
.invalid
or (cg
.end
- cg
.start
== 0):
437 if(not cg
.postProcess()):
438 pprint('Sanity check failed for %s-%d' % (proc
, pid
))
440 # match cg data to devices
441 devname
= data
.deviceMatch(pid
, cg
)
446 sysvals
.vprint('%s callgraph found for %s %s-%d [%f - %f]' %\
447 (kind
, cg
.name
, proc
, pid
, cg
.start
, cg
.end
))
448 elif len(cg
.list) > 1000000:
449 pprint('WARNING: the callgraph found for %s is massive! (%d lines)' %\
450 (devname
, len(cg
.list)))
452 # Function: retrieveLogs
454 # Create copies of dmesg and/or ftrace for later processing
456 # check ftrace is configured first
457 if sysvals
.useftrace
:
458 tracer
= sysvals
.fgetVal('current_tracer').strip()
459 if tracer
!= 'function_graph':
460 doError('ftrace not configured for a boot callgraph')
461 # create the folder and get dmesg
462 sysvals
.systemInfo(aslib
.dmidecode(sysvals
.mempath
))
463 sysvals
.initTestOutput('boot')
464 sysvals
.writeDatafileHeader(sysvals
.dmesgfile
)
465 call('dmesg >> '+sysvals
.dmesgfile
, shell
=True)
466 if not sysvals
.useftrace
:
469 sysvals
.writeDatafileHeader(sysvals
.ftracefile
)
470 call('cat '+sysvals
.tpath
+'trace >> '+sysvals
.ftracefile
, shell
=True)
472 # Function: colorForName
474 # Generate a repeatable color from a list for a given name
475 def colorForName(name
):
492 total
+= ord(name
[i
])
494 return list[total
% count
]
496 def cgOverview(cg
, minlen
):
500 if l
.fcall
and l
.depth
== 1:
501 if l
.length
>= minlen
:
503 if l
.name
not in stats
:
504 stats
[l
.name
] = [0, 0.0]
505 stats
[l
.name
][0] += (l
.length
* 1000.0)
506 stats
[l
.name
][1] += 1
507 return (large
, stats
)
509 # Function: createBootGraph
511 # Create the output html file from the resident test data
513 # testruns: array of Data objects from parseKernelLog or parseTraceLog
515 # True if the html file was created, false if it failed
516 def createBootGraph(data
):
517 # html function templates
518 html_srccall
= '<div id={6} title="{5}" class="srccall" style="left:{1}%;top:{2}px;height:{3}px;width:{4}%;line-height:{3}px;">{0}</div>\n'
519 html_timetotal
= '<table class="time1">\n<tr>'\
520 '<td class="blue">Init process starts @ <b>{0} ms</b></td>'\
521 '<td class="blue">Last initcall ends @ <b>{1} ms</b></td>'\
525 devtl
= aslib
.Timeline(100, 20)
527 # write the test title and general info header
528 devtl
.createHeader(sysvals
, sysvals
.stamp
)
530 # Generate the header for this timeline
535 pprint('ERROR: No timeline data')
537 user_mode
= '%.0f'%(data
.tUserMode
*1000)
538 last_init
= '%.0f'%(tTotal
*1000)
539 devtl
.html
+= html_timetotal
.format(user_mode
, last_init
)
541 # determine the maximum number of rows we need to draw
543 for p
in data
.phases
:
544 list = data
.dmesg
[p
]['list']
546 d
= aslib
.DevItem(0, p
, list[devname
])
548 devtl
.getPhaseRows(devlist
, 0, 'start')
549 devtl
.calcTotalRows()
551 # draw the timeline background
552 devtl
.createZoomBox()
553 devtl
.html
+= devtl
.html_tblock
.format('boot', '0', '100', devtl
.scaleH
)
554 for p
in data
.phases
:
555 phase
= data
.dmesg
[p
]
556 length
= phase
['end']-phase
['start']
557 left
= '%.3f' % (((phase
['start']-t0
)*100.0)/tTotal
)
558 width
= '%.3f' % ((length
*100.0)/tTotal
)
559 devtl
.html
+= devtl
.html_phase
.format(left
, width
, \
560 '%.3f'%devtl
.scaleH
, '%.3f'%devtl
.bodyH
, \
563 # draw the device timeline
566 for phase
in data
.phases
:
567 list = data
.dmesg
[phase
]['list']
568 for devname
in sorted(list):
569 cls
, color
= colorForName(devname
)
571 info
= '@|%.3f|%.3f|%.3f|%d' % (dev
['start']*1000.0, dev
['end']*1000.0,
572 dev
['ulen']/1000.0, dev
['ret'])
573 devstats
[dev
['id']] = {'info':info
}
575 height
= devtl
.phaseRowHeight(0, phase
, dev
['row'])
576 top
= '%.6f' % ((dev
['row']*height
) + devtl
.scaleH
)
577 left
= '%.6f' % (((dev
['start']-t0
)*100)/tTotal
)
578 width
= '%.6f' % (((dev
['end']-dev
['start'])*100)/tTotal
)
579 length
= ' (%0.3f ms) ' % ((dev
['end']-dev
['start'])*1000)
580 devtl
.html
+= devtl
.html_device
.format(dev
['id'],
581 devname
+length
+phase
+'_mode', left
, top
, '%.3f'%height
,
582 width
, devname
, ' '+cls
, '')
583 rowtop
= devtl
.phaseRowTop(0, phase
, dev
['row'])
584 height
= '%.6f' % (devtl
.rowH
/ 2)
585 top
= '%.6f' % (rowtop
+ devtl
.scaleH
+ (devtl
.rowH
/ 2))
586 if data
.do_one_initcall
:
587 if('ftrace' not in dev
):
590 large
, stats
= cgOverview(cg
, 0.001)
591 devstats
[dev
['id']]['fstat'] = stats
593 left
= '%f' % (((l
.time
-t0
)*100)/tTotal
)
594 width
= '%f' % (l
.length
*100/tTotal
)
595 title
= '%s (%0.3fms)' % (l
.name
, l
.length
* 1000.0)
596 devtl
.html
+= html_srccall
.format(l
.name
, left
,
597 top
, height
, width
, title
, 'x%d'%num
)
600 if('ftraces' not in dev
):
602 for cg
in dev
['ftraces']:
603 left
= '%f' % (((cg
.start
-t0
)*100)/tTotal
)
604 width
= '%f' % ((cg
.end
-cg
.start
)*100/tTotal
)
605 cglen
= (cg
.end
- cg
.start
) * 1000.0
606 title
= '%s (%0.3fms)' % (cg
.name
, cglen
)
608 devtl
.html
+= html_srccall
.format(cg
.name
, left
,
609 top
, height
, width
, title
, dev
['id']+cg
.id)
612 # draw the time scale, try to make the number of labels readable
613 devtl
.createTimeScale(t0
, tMax
, tTotal
, 'boot')
614 devtl
.html
+= '</div>\n'
616 # timeline is finished
617 devtl
.html
+= '</div>\n</div>\n'
619 # draw a legend which describes the phases by color
620 devtl
.html
+= '<div class="legend">\n'
623 for phase
in data
.phases
:
624 order
= '%.2f' % ((data
.dmesg
[phase
]['order'] * pdelta
) + pmargin
)
625 devtl
.html
+= devtl
.html_legend
.format(order
, \
626 data
.dmesg
[phase
]['color'], phase
+'_mode', phase
[0])
627 devtl
.html
+= '</div>\n'
629 hf
= open(sysvals
.htmlfile
, 'w')
633 .c1 {background:rgba(209,0,0,0.4);}\n\
634 .c2 {background:rgba(255,102,34,0.4);}\n\
635 .c3 {background:rgba(255,218,33,0.4);}\n\
636 .c4 {background:rgba(51,221,0,0.4);}\n\
637 .c5 {background:rgba(17,51,204,0.4);}\n\
638 .c6 {background:rgba(34,0,102,0.4);}\n\
639 .c7 {background:rgba(51,0,68,0.4);}\n\
640 .c8 {background:rgba(204,255,204,0.4);}\n\
641 .c9 {background:rgba(169,208,245,0.4);}\n\
642 .c10 {background:rgba(255,255,204,0.4);}\n\
643 .vt {transform:rotate(-60deg);transform-origin:0 0;}\n\
644 table.fstat {table-layout:fixed;padding:150px 15px 0 0;font-size:10px;column-width:30px;}\n\
645 .fstat th {width:55px;}\n\
646 .fstat td {text-align:left;width:35px;}\n\
647 .srccall {position:absolute;font-size:10px;z-index:7;overflow:hidden;color:black;text-align:center;white-space:nowrap;border-radius:5px;border:1px solid black;background:linear-gradient(to bottom right,#CCC,#969696);}\n\
648 .srccall:hover {color:white;font-weight:bold;border:1px solid white;}\n'
649 aslib
.addCSS(hf
, sysvals
, 1, False, extra
)
651 # write the device timeline
654 # add boot specific html
655 statinfo
= 'var devstats = {\n'
656 for n
in sorted(devstats
):
657 statinfo
+= '\t"%s": [\n\t\t"%s",\n' % (n
, devstats
[n
]['info'])
658 if 'fstat' in devstats
[n
]:
659 funcs
= devstats
[n
]['fstat']
660 for f
in sorted(funcs
, key
=funcs
.get
, reverse
=True):
661 if funcs
[f
][0] < 0.01 and len(funcs
) > 10:
663 statinfo
+= '\t\t"%f|%s|%d",\n' % (funcs
[f
][0], f
, funcs
[f
][1])
667 '<div id="devicedetailtitle"></div>\n'\
668 '<div id="devicedetail" style="display:none;">\n'\
669 '<div id="devicedetail0">\n'
670 for p
in data
.phases
:
671 phase
= data
.dmesg
[p
]
672 html
+= devtl
.html_phaselet
.format(p
+'_mode', '0', '100', phase
['color'])
673 html
+= '</div>\n</div>\n'\
674 '<script type="text/javascript">\n'+statinfo
+\
678 # add the callgraph html
679 if(sysvals
.usecallgraph
):
680 aslib
.addCallgraphs(sysvals
, hf
, data
)
682 # add the test log as a hidden div
683 if sysvals
.testlog
and sysvals
.logmsg
:
684 hf
.write('<div id="testlog" style="display:none;">\n'+sysvals
.logmsg
+'</div>\n')
685 # add the dmesg log as a hidden div
687 hf
.write('<div id="dmesglog" style="display:none;">\n')
688 for line
in data
.dmesgtext
:
689 line
= line
.replace('<', '<').replace('>', '>')
693 # write the footer and close
694 aslib
.addScriptCode(hf
, [data
])
695 hf
.write('</body>\n</html>\n')
699 # Function: updateCron
701 # (restore=False) Set the tool to run automatically on reboot
702 # (restore=True) Restore the original crontab
703 def updateCron(restore
=False):
705 sysvals
.rootUser(True)
706 crondir
= '/var/spool/cron/crontabs/'
707 if not os
.path
.exists(crondir
):
708 crondir
= '/var/spool/cron/'
709 if not os
.path
.exists(crondir
):
710 doError('%s not found' % crondir
)
711 cronfile
= crondir
+'root'
712 backfile
= crondir
+'root-analyze_boot-backup'
713 cmd
= sysvals
.getExec('crontab')
715 doError('crontab not found')
716 # on restore: move the backup cron back into place
718 if os
.path
.exists(backfile
):
719 shutil
.move(backfile
, cronfile
)
720 call([cmd
, cronfile
])
722 # backup current cron and install new one with reboot
723 if os
.path
.exists(cronfile
):
724 shutil
.move(cronfile
, backfile
)
726 fp
= open(backfile
, 'w')
730 fp
= open(backfile
, 'r')
731 op
= open(cronfile
, 'w')
733 if not sysvals
.myCronJob(line
):
737 op
.write('@reboot python %s\n' % sysvals
.cronjobCmdString())
739 res
= call([cmd
, cronfile
])
741 pprint('Exception: %s' % str(e
))
742 shutil
.move(backfile
, cronfile
)
745 doError('crontab failed')
747 # Function: updateGrub
749 # update grub.cfg for all kernels with our parameters
750 def updateGrub(restore
=False):
751 # call update-grub on restore
754 call(sysvals
.blexec
, stderr
=PIPE
, stdout
=PIPE
,
755 env
={'PATH': '.:/sbin:/usr/sbin:/usr/bin:/sbin:/bin'})
757 pprint('Exception: %s\n' % str(e
))
759 # extract the option and create a grub config without it
760 sysvals
.rootUser(True)
761 tgtopt
= 'GRUB_CMDLINE_LINUX_DEFAULT'
763 grubfile
= '/etc/default/grub'
764 tempfile
= '/etc/default/grub.analyze_boot'
765 shutil
.move(grubfile
, tempfile
)
768 fp
= open(tempfile
, 'r')
769 op
= open(grubfile
, 'w')
773 if len(line
) == 0 or line
[0] == '#':
775 opt
= line
.split('=')[0].strip()
777 cmdline
= line
.split('=', 1)[1].strip('\\')
781 cmdline
+= line
.strip('\\')
785 op
.write('%s\n' % line
)
787 # if the target option value is in quotes, strip them
789 val
= cmdline
.strip()
790 if val
and (val
[0] == '\'' or val
[0] == '"'):
794 # append our cmd line options
797 cmdline
+= sysvals
.kernelParams()
798 # write out the updated target option
799 op
.write('\n%s=%s%s%s\n' % (tgtopt
, sp
, cmdline
, sp
))
801 res
= call(sysvals
.blexec
)
804 pprint('Exception: %s' % str(e
))
807 shutil
.move(tempfile
, grubfile
)
809 doError('update grub failed')
811 # Function: updateKernelParams
813 # update boot conf for all kernels with our parameters
814 def updateKernelParams(restore
=False):
815 # find the boot loader
816 sysvals
.getBootLoader()
817 if sysvals
.bootloader
== 'grub':
820 # Function: doError Description:
821 # generic error function for catastrphic failures
823 # msg: the error message to print
824 # help: True if printHelp should be called after, False otherwise
825 def doError(msg
, help=False):
828 pprint('ERROR: %s\n' % msg
)
829 sysvals
.outputResult({'error':msg
})
832 # Function: printHelp
834 # print out the help text
837 'Usage: bootgraph <options> <command>\n'\
840 ' This tool reads in a dmesg log of linux kernel boot and\n'\
841 ' creates an html representation of the boot timeline up to\n'\
842 ' the start of the init process.\n'\
844 ' If no specific command is given the tool reads the current dmesg\n'\
845 ' and/or ftrace log and creates a timeline\n'\
847 ' Generates output files in subdirectory: boot-yymmdd-HHMMSS\n'\
848 ' HTML output: <hostname>_boot.html\n'\
849 ' raw dmesg output: <hostname>_boot_dmesg.txt\n'\
850 ' raw ftrace output: <hostname>_boot_ftrace.txt\n'\
853 ' -h Print this help text\n'\
854 ' -v Print the current tool version\n'\
855 ' -verbose Print extra information during execution and analysis\n'\
856 ' -addlogs Add the dmesg log to the html output\n'\
857 ' -result fn Export a results table to a text file for parsing.\n'\
858 ' -o name Overrides the output subdirectory name when running a new test\n'\
859 ' default: boot-{date}-{time}\n'\
861 ' -fstat Use ftrace to add function detail and statistics (default: disabled)\n'\
862 ' -f/-callgraph Add callgraph detail, can be very large (default: disabled)\n'\
863 ' -maxdepth N limit the callgraph data to N call levels (default: 2)\n'\
864 ' -mincg ms Discard all callgraphs shorter than ms milliseconds (e.g. 0.001 for us)\n'\
865 ' -timeprec N Number of significant digits in timestamps (0:S, 3:ms, [6:us])\n'\
866 ' -expandcg pre-expand the callgraph data in the html output (default: disabled)\n'\
867 ' -func list Limit ftrace to comma-delimited list of functions (default: do_one_initcall)\n'\
868 ' -cgfilter S Filter the callgraph output in the timeline\n'\
869 ' -cgskip file Callgraph functions to skip, off to disable (default: cgskip.txt)\n'\
870 ' -bl name Use the following boot loader for kernel params (default: grub)\n'\
871 ' -reboot Reboot the machine automatically and generate a new timeline\n'\
872 ' -manual Show the steps to generate a new timeline manually (used with -reboot)\n'\
875 ' -flistall Print all functions capable of being captured in ftrace\n'\
876 ' -sysinfo Print out system info extracted from BIOS\n'\
878 ' -dmesg file Create HTML output using dmesg input (used with -ftrace)\n'\
879 ' -ftrace file Create HTML output using ftrace input (used with -dmesg)\n'\
880 '' % (sysvals
.title
, sysvals
.version
))
883 # ----------------- MAIN --------------------
884 # exec start (skipped if script is loaded as library)
885 if __name__
== '__main__':
886 # loop through the command line arguments
889 switchoff
= ['disable', 'off', 'false', '0']
890 simplecmds
= ['-sysinfo', '-kpupdate', '-flistall', '-checkbl']
893 cgskip
= sysvals
.configFile('cgskip.txt')
894 args
= iter(sys
.argv
[1:])
901 pprint("Version %s" % sysvals
.version
)
903 elif(arg
== '-verbose'):
904 sysvals
.verbose
= True
905 elif(arg
in simplecmds
):
907 elif(arg
== '-fstat'):
908 sysvals
.useftrace
= True
909 elif(arg
== '-callgraph' or arg
== '-f'):
910 sysvals
.useftrace
= True
911 sysvals
.usecallgraph
= True
912 elif(arg
== '-cgdump'):
913 sysvals
.cgdump
= True
914 elif(arg
== '-mincg'):
915 sysvals
.mincglen
= aslib
.getArgFloat('-mincg', args
, 0.0, 10000.0)
916 elif(arg
== '-cgfilter'):
920 doError('No callgraph functions supplied', True)
921 sysvals
.setCallgraphFilter(val
)
922 elif(arg
== '-cgskip'):
926 doError('No file supplied', True)
927 if val
.lower() in switchoff
:
930 cgskip
= sysvals
.configFile(val
)
932 doError('%s does not exist' % cgskip
)
937 doError('No boot loader name supplied', True)
938 if val
.lower() not in ['grub']:
939 doError('Unknown boot loader: %s' % val
, True)
940 sysvals
.bootloader
= val
.lower()
941 elif(arg
== '-timeprec'):
942 sysvals
.setPrecision(aslib
.getArgInt('-timeprec', args
, 0, 6))
943 elif(arg
== '-maxdepth'):
945 sysvals
.max_graph_depth
= aslib
.getArgInt('-maxdepth', args
, 0, 1000)
946 elif(arg
== '-func'):
950 doError('No filter functions supplied', True)
951 sysvals
.useftrace
= True
952 sysvals
.usecallgraph
= True
953 sysvals
.rootCheck(True)
954 sysvals
.setGraphFilter(val
)
955 elif(arg
== '-ftrace'):
959 doError('No ftrace file supplied', True)
960 if(os
.path
.exists(val
) == False):
961 doError('%s does not exist' % val
)
963 sysvals
.ftracefile
= val
964 elif(arg
== '-addlogs'):
965 sysvals
.dmesglog
= True
966 elif(arg
== '-expandcg'):
968 elif(arg
== '-dmesg'):
972 doError('No dmesg file supplied', True)
973 if(os
.path
.exists(val
) == False):
974 doError('%s does not exist' % val
)
976 sysvals
.dmesgfile
= val
981 doError('No subdirectory name supplied', True)
982 sysvals
.testdir
= sysvals
.setOutputFolder(val
)
983 elif(arg
== '-result'):
987 doError('No result file supplied', True)
989 elif(arg
== '-reboot'):
990 sysvals
.reboot
= True
991 elif(arg
== '-manual'):
992 sysvals
.reboot
= True
993 sysvals
.manual
= True
994 # remaining options are only for cron job use
995 elif(arg
== '-cronjob'):
996 sysvals
.iscronjob
= True
998 doError('Invalid argument: '+arg
, True)
1000 # compatibility errors and access checks
1001 if(sysvals
.iscronjob
and (sysvals
.reboot
or \
1002 sysvals
.dmesgfile
or sysvals
.ftracefile
or cmd
)):
1003 doError('-cronjob is meant for batch purposes only')
1004 if(sysvals
.reboot
and (sysvals
.dmesgfile
or sysvals
.ftracefile
)):
1005 doError('-reboot and -dmesg/-ftrace are incompatible')
1006 if cmd
or sysvals
.reboot
or sysvals
.iscronjob
or testrun
:
1007 sysvals
.rootCheck(True)
1008 if (testrun
and sysvals
.useftrace
) or cmd
== 'flistall':
1009 if not sysvals
.verifyFtrace():
1010 doError('Ftrace is not properly enabled')
1012 # run utility commands
1015 if cmd
== 'kpupdate':
1016 updateKernelParams()
1017 elif cmd
== 'flistall':
1018 for f
in sysvals
.getBootFtraceFilterFunctions():
1020 elif cmd
== 'checkbl':
1021 sysvals
.getBootLoader()
1022 pprint('Boot Loader: %s\n%s' % (sysvals
.bootloader
, sysvals
.blexec
))
1023 elif(cmd
== 'sysinfo'):
1024 sysvals
.printSystemInfo(True)
1027 # reboot: update grub, setup a cronjob, and reboot
1029 if (sysvals
.useftrace
or sysvals
.usecallgraph
) and \
1030 not sysvals
.checkFtraceKernelVersion():
1031 doError('Ftrace functionality requires kernel v4.10 or newer')
1032 if not sysvals
.manual
:
1033 updateKernelParams()
1037 sysvals
.manualRebootRequired()
1040 if sysvals
.usecallgraph
and cgskip
:
1041 sysvals
.vprint('Using cgskip file: %s' % cgskip
)
1042 sysvals
.setCallgraphBlacklist(cgskip
)
1044 # cronjob: remove the cronjob, grub changes, and disable ftrace
1045 if sysvals
.iscronjob
:
1047 updateKernelParams(True)
1049 sysvals
.fsetVal('0', 'tracing_on')
1053 # testrun: generate copies of the logs
1057 sysvals
.setOutputFile()
1059 # process the log data
1060 if sysvals
.dmesgfile
:
1062 sysvals
.max_graph_depth
= 0
1063 data
= parseKernelLog()
1065 doError('No initcall data found in %s' % sysvals
.dmesgfile
)
1066 if sysvals
.useftrace
and sysvals
.ftracefile
:
1072 doError('dmesg file required')
1074 sysvals
.vprint('Creating the html timeline (%s)...' % sysvals
.htmlfile
)
1075 sysvals
.vprint('Command:\n %s' % sysvals
.cmdline
)
1076 sysvals
.vprint('Kernel parameters:\n %s' % sysvals
.kparams
)
1078 createBootGraph(data
)
1080 # if running as root, change output dir owner to sudo_user
1081 if testrun
and os
.path
.isdir(sysvals
.testdir
) and \
1082 os
.getuid() == 0 and 'SUDO_USER' in os
.environ
:
1083 cmd
= 'chown -R {0}:{0} {1} > /dev/null 2>&1'
1084 call(cmd
.format(os
.environ
['SUDO_USER'], sysvals
.testdir
), shell
=True)
1086 sysvals
.stamp
['boot'] = (data
.tUserMode
- data
.start
) * 1000
1087 sysvals
.stamp
['lastinit'] = data
.end
* 1000
1088 sysvals
.outputResult(sysvals
.stamp
)