bootgraph.py 33 KB

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