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