3 # Tool for analyzing boot timing
4 # Copyright (c) 2013, Intel Corporation.
6 # This program is free software; you can redistribute it and/or modify it
7 # under the terms and conditions of the GNU General Public License,
8 # version 2, as published by the Free Software Foundation.
10 # This program is distributed in the hope it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
16 # Todd Brandt <todd.e.brandt@linux.intel.com>
19 # This tool is designed to assist kernel and OS developers in optimizing
20 # their linux stack's boot time. It creates an html representation of
21 # the kernel boot timeline up to the start of the init process.
24 # ----------------- LIBRARIES --------------------
33 from datetime import datetime, timedelta
34 from subprocess import call, Popen, PIPE
35 import analyze_suspend as aslib
37 # ----------------- CLASSES --------------------
41 # A global, single-instance container used to
42 # store system values and test parameters
43 class SystemValues(aslib.SystemValues):
46 hostname = 'localhost'
51 htmlfile = 'bootgraph.html'
59 graph_filter = 'do_one_initcall'
65 if('LOG_FILE' in os.environ and 'TEST_RESULTS_IDENTIFIER' in os.environ):
68 self.outfile = os.environ['LOG_FILE']
69 self.htmlfile = os.environ['LOG_FILE']
70 self.hostname = platform.node()
71 self.testtime = datetime.now().strftime('%Y-%m-%d_%H:%M:%S')
72 if os.path.exists('/proc/version'):
73 fp = open('/proc/version', 'r')
74 val = fp.read().strip()
76 self.kernel = self.kernelVersion(val)
78 self.kernel = 'unknown'
79 def kernelVersion(self, msg):
81 def kernelParams(self):
82 cmdline = 'initcall_debug log_buf_len=32M'
84 cmdline += ' trace_buf_size=128M trace_clock=global '\
85 'trace_options=nooverwrite,funcgraph-abstime,funcgraph-cpu,'\
86 'funcgraph-duration,funcgraph-proc,funcgraph-tail,'\
87 'nofuncgraph-overhead,context-info,graph-time '\
88 'ftrace=function_graph '\
89 'ftrace_graph_max_depth=%d '\
90 'ftrace_graph_filter=%s' % \
91 (self.max_graph_depth, self.graph_filter)
93 def setGraphFilter(self, val):
94 fp = open(self.tpath+'available_filter_functions')
95 master = fp.read().split('\n')
97 for i in val.split(','):
99 if func not in master:
100 doError('function "%s" not available for ftrace' % func)
101 self.graph_filter = val
102 def cronjobCmdString(self):
103 cmdline = '%s -cronjob' % os.path.abspath(sys.argv[0])
104 args = iter(sys.argv[1:])
106 if arg in ['-h', '-v', '-cronjob', '-reboot']:
108 elif arg in ['-o', '-dmesg', '-ftrace', '-filter']:
112 if self.graph_filter != 'do_one_initcall':
113 cmdline += ' -filter "%s"' % self.graph_filter
114 cmdline += ' -o "%s"' % os.path.abspath(self.htmlfile)
116 def manualRebootRequired(self):
117 cmdline = self.kernelParams()
118 print 'To generate a new timeline manually, follow these steps:\n'
119 print '1. Add the CMDLINE string to your kernel command line.'
120 print '2. Reboot the system.'
121 print '3. After reboot, re-run this tool with the same arguments but no command (w/o -reboot or -manual).\n'
122 print 'CMDLINE="%s"' % cmdline
125 sysvals = SystemValues()
129 # The primary container for test data.
130 class Data(aslib.Data):
131 dmesg = {} # root data structure
132 start = 0.0 # test start
134 dmesgtext = [] # dmesg text file in memory
142 do_one_initcall = False
143 def __init__(self, num):
144 self.testnumber = num
148 'boot': {'list': dict(), 'start': -1.0, 'end': -1.0, 'row': 0, 'color': '#dddddd'}
150 def deviceTopology(self):
152 def newAction(self, phase, name, start, end, ret, ulen):
153 # new device callback for a specific phase
154 self.html_device_id += 1
155 devid = '%s%d' % (self.idstr, self.html_device_id)
156 list = self.dmesg[phase]['list']
158 if(start >= 0 and end >= 0):
163 name = '%s[%d]' % (origname, i)
165 list[name] = {'name': name, 'start': start, 'end': end,
166 'pid': 0, 'length': length, 'row': 0, 'id': devid,
167 'ret': ret, 'ulen': ulen }
169 def deviceMatch(self, cg):
170 if cg.end - cg.start == 0:
172 list = self.dmesg['boot']['list']
175 if cg.name == 'do_one_initcall':
176 if(cg.start <= dev['start'] and cg.end >= dev['end'] and dev['length'] > 0):
178 self.do_one_initcall = True
181 if(cg.start > dev['start'] and cg.end < dev['end']):
182 if 'ftraces' not in dev:
184 dev['ftraces'].append(cg)
188 # ----------------- FUNCTIONS --------------------
190 # Function: loadKernelLog
192 # Load a raw kernel log from dmesg
195 data.dmesg['boot']['start'] = data.start = ktime = 0.0
197 'time': datetime.now().strftime('%B %d %Y, %I:%M:%S %p'),
198 'host': sysvals.hostname,
199 'mode': 'boot', 'kernel': ''}
202 if(sysvals.dmesgfile):
203 lf = open(sysvals.dmesgfile, 'r')
205 lf = Popen('dmesg', stdout=PIPE).stdout
207 line = line.replace('\r\n', '')
211 m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
214 ktime = float(m.group('ktime'))
218 data.end = data.initstart = ktime
219 data.dmesgtext.append(line)
220 if(ktime == 0.0 and re.match('^Linux version .*', msg)):
221 if(not sysvals.stamp['kernel']):
222 sysvals.stamp['kernel'] = sysvals.kernelVersion(msg)
224 m = re.match('.* setting system clock to (?P<t>.*) UTC.*', msg)
226 bt = datetime.strptime(m.group('t'), '%Y-%m-%d %H:%M:%S')
227 bt = bt - timedelta(seconds=int(ktime))
228 data.boottime = bt.strftime('%Y-%m-%d_%H:%M:%S')
229 sysvals.stamp['time'] = bt.strftime('%B %d %Y, %I:%M:%S %p')
231 m = re.match('^calling *(?P<f>.*)\+.*', msg)
233 devtemp[m.group('f')] = ktime
235 m = re.match('^initcall *(?P<f>.*)\+.* returned (?P<r>.*) after (?P<t>.*) usecs', msg)
238 f, r, t = m.group('f', 'r', 't')
240 data.newAction('boot', f, devtemp[f], ktime, int(r), int(t))
244 if(re.match('^Freeing unused kernel memory.*', msg)):
247 data.dmesg['boot']['end'] = data.end
251 # Function: loadTraceLog
253 # Check if trace is available and copy to a temp file
254 def loadTraceLog(data):
255 # load the data to a temp file if none given
256 if not sysvals.ftracefile:
258 aslib.rootCheck(True)
259 if not lib.verifyFtrace():
260 doError('ftrace not available')
261 if lib.fgetVal('current_tracer').strip() != 'function_graph':
262 doError('ftrace not configured for a boot callgraph')
263 sysvals.ftracefile = '/tmp/boot_ftrace.%s.txt' % os.getpid()
264 call('cat '+lib.tpath+'trace > '+sysvals.ftracefile, shell=True)
265 if not sysvals.ftracefile:
266 doError('No trace data available')
268 # parse the trace log
270 tp = aslib.TestProps()
271 tp.setTracerType('function_graph')
272 tf = open(sysvals.ftracefile, 'r')
276 m = re.match(tp.ftrace_line_fmt, line.strip())
279 m_time, m_proc, m_pid, m_msg, m_dur = \
280 m.group('time', 'proc', 'pid', 'msg', 'dur')
281 if float(m_time) > data.end:
283 if(m_time and m_pid and m_msg):
284 t = aslib.FTraceLine(m_time, m_msg, m_dur)
288 if t.fevent or t.fkprobe:
291 if(key not in ftemp):
293 ftemp[key].append(aslib.FTraceCallGraph(pid))
296 ftemp[key].append(aslib.FTraceCallGraph(pid))
299 # add the callgraph data to the device hierarchy
302 for cg in ftemp[key]:
303 if len(cg.list) < 1 or cg.invalid:
305 if(not cg.postProcess()):
306 print('Sanity check failed for %s-%d' % (proc, pid))
308 # match cg data to devices
309 if not data.deviceMatch(cg):
310 print ' BAD: %s %s-%d [%f - %f]' % (cg.name, proc, pid, cg.start, cg.end)
312 # Function: colorForName
314 # Generate a repeatable color from a list for a given name
315 def colorForName(name):
332 total += ord(name[i])
334 return list[total % count]
336 def cgOverview(cg, minlen):
340 if l.fcall and l.depth == 1:
341 if l.length >= minlen:
343 if l.name not in stats:
344 stats[l.name] = [0, 0.0]
345 stats[l.name][0] += (l.length * 1000.0)
346 stats[l.name][1] += 1
347 return (large, stats)
349 # Function: createBootGraph
351 # Create the output html file from the resident test data
353 # testruns: array of Data objects from parseKernelLog or parseTraceLog
355 # True if the html file was created, false if it failed
356 def createBootGraph(data, embedded):
357 # html function templates
358 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'
359 html_timetotal = '<table class="time1">\n<tr>'\
360 '<td class="blue">Time from Kernel Boot to start of User Mode: <b>{0} ms</b></td>'\
364 devtl = aslib.Timeline(100, 20)
366 # write the test title and general info header
367 devtl.createHeader(sysvals, 'noftrace')
369 # Generate the header for this timeline
374 print('ERROR: No timeline data')
376 boot_time = '%.0f'%(tTotal*1000)
377 devtl.html += html_timetotal.format(boot_time)
379 # determine the maximum number of rows we need to draw
381 list = data.dmesg[phase]['list']
384 d = aslib.DevItem(0, phase, list[devname])
386 devtl.getPhaseRows(devlist)
387 devtl.calcTotalRows()
389 # draw the timeline background
390 devtl.createZoomBox()
391 boot = data.dmesg[phase]
392 length = boot['end']-boot['start']
393 left = '%.3f' % (((boot['start']-t0)*100.0)/tTotal)
394 width = '%.3f' % ((length*100.0)/tTotal)
395 devtl.html += devtl.html_tblock.format(phase, left, width, devtl.scaleH)
396 devtl.html += devtl.html_phase.format('0', '100', \
397 '%.3f'%devtl.scaleH, '%.3f'%devtl.bodyH, \
400 # draw the device timeline
403 for devname in sorted(list):
404 cls, color = colorForName(devname)
406 info = '@|%.3f|%.3f|%.3f|%d' % (dev['start']*1000.0, dev['end']*1000.0,
407 dev['ulen']/1000.0, dev['ret'])
408 devstats[dev['id']] = {'info':info}
410 height = devtl.phaseRowHeight(0, phase, dev['row'])
411 top = '%.6f' % ((dev['row']*height) + devtl.scaleH)
412 left = '%.6f' % (((dev['start']-t0)*100)/tTotal)
413 width = '%.6f' % (((dev['end']-dev['start'])*100)/tTotal)
414 length = ' (%0.3f ms) ' % ((dev['end']-dev['start'])*1000)
415 devtl.html += devtl.html_device.format(dev['id'],
416 devname+length+'kernel_mode', left, top, '%.3f'%height,
417 width, devname, ' '+cls, '')
418 rowtop = devtl.phaseRowTop(0, phase, dev['row'])
419 height = '%.6f' % (devtl.rowH / 2)
420 top = '%.6f' % (rowtop + devtl.scaleH + (devtl.rowH / 2))
421 if data.do_one_initcall:
422 if('ftrace' not in dev):
425 large, stats = cgOverview(cg, 0.001)
426 devstats[dev['id']]['fstat'] = stats
428 left = '%f' % (((l.time-t0)*100)/tTotal)
429 width = '%f' % (l.length*100/tTotal)
430 title = '%s (%0.3fms)' % (l.name, l.length * 1000.0)
431 devtl.html += html_srccall.format(l.name, left,
432 top, height, width, title, 'x%d'%num)
435 if('ftraces' not in dev):
437 for cg in dev['ftraces']:
438 left = '%f' % (((cg.start-t0)*100)/tTotal)
439 width = '%f' % ((cg.end-cg.start)*100/tTotal)
440 cglen = (cg.end - cg.start) * 1000.0
441 title = '%s (%0.3fms)' % (cg.name, cglen)
443 devtl.html += html_srccall.format(cg.name, left,
444 top, height, width, title, dev['id']+cg.id)
447 # draw the time scale, try to make the number of labels readable
448 devtl.createTimeScale(t0, tMax, tTotal, phase)
449 devtl.html += '</div>\n'
451 # timeline is finished
452 devtl.html += '</div>\n</div>\n'
454 if(sysvals.outfile == sysvals.htmlfile):
455 hf = open(sysvals.htmlfile, 'a')
457 hf = open(sysvals.htmlfile, 'w')
459 # add the css if this is not an embedded run
461 .c1 {background:rgba(209,0,0,0.4);}\n\
462 .c2 {background:rgba(255,102,34,0.4);}\n\
463 .c3 {background:rgba(255,218,33,0.4);}\n\
464 .c4 {background:rgba(51,221,0,0.4);}\n\
465 .c5 {background:rgba(17,51,204,0.4);}\n\
466 .c6 {background:rgba(34,0,102,0.4);}\n\
467 .c7 {background:rgba(51,0,68,0.4);}\n\
468 .c8 {background:rgba(204,255,204,0.4);}\n\
469 .c9 {background:rgba(169,208,245,0.4);}\n\
470 .c10 {background:rgba(255,255,204,0.4);}\n\
471 .vt {transform:rotate(-60deg);transform-origin:0 0;}\n\
472 table.fstat {table-layout:fixed;padding:150px 15px 0 0;font-size:10px;column-width:30px;}\n\
473 .fstat th {width:55px;}\n\
474 .fstat td {text-align:left;width:35px;}\n\
475 .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\
476 .srccall:hover {color:white;font-weight:bold;border:1px solid white;}\n'
478 aslib.addCSS(hf, sysvals, 1, False, extra)
480 # write the device timeline
483 # add boot specific html
484 statinfo = 'var devstats = {\n'
485 for n in sorted(devstats):
486 statinfo += '\t"%s": [\n\t\t"%s",\n' % (n, devstats[n]['info'])
487 if 'fstat' in devstats[n]:
488 funcs = devstats[n]['fstat']
489 for f in sorted(funcs, key=funcs.get, reverse=True):
490 if funcs[f][0] < 0.01 and len(funcs) > 10:
492 statinfo += '\t\t"%f|%s|%d",\n' % (funcs[f][0], f, funcs[f][1])
496 '<div id="devicedetailtitle"></div>\n'\
497 '<div id="devicedetail" style="display:none;">\n'\
498 '<div id="devicedetail0">\n'\
499 '<div id="kernel_mode" class="phaselet" style="left:0%;width:100%;background:#DDDDDD"></div>\n'\
501 '<script type="text/javascript">\n'+statinfo+\
505 # add the callgraph html
506 if(sysvals.usecallgraph):
507 aslib.addCallgraphs(sysvals, hf, data)
509 # add the dmesg log as a hidden div
511 hf.write('<div id="dmesglog" style="display:none;">\n')
512 for line in data.dmesgtext:
513 line = line.replace('<', '<').replace('>', '>')
518 # write the footer and close
519 aslib.addScriptCode(hf, [data])
520 hf.write('</body>\n</html>\n')
522 # embedded out will be loaded in a page, skip the js
523 hf.write('<div id=bounds style=display:none>%f,%f</div>' % \
524 (data.start*1000, data.initstart*1000))
528 # Function: updateCron
530 # (restore=False) Set the tool to run automatically on reboot
531 # (restore=True) Restore the original crontab
532 def updateCron(restore=False):
534 sysvals.rootUser(True)
535 crondir = '/var/spool/cron/crontabs/'
536 cronfile = crondir+'root'
537 backfile = crondir+'root-analyze_boot-backup'
538 if not os.path.exists(crondir):
539 doError('%s not found' % crondir)
540 out = Popen(['which', 'crontab'], stdout=PIPE).stdout.read()
542 doError('crontab not found')
543 # on restore: move the backup cron back into place
545 if os.path.exists(backfile):
546 shutil.move(backfile, cronfile)
548 # backup current cron and install new one with reboot
549 if os.path.exists(cronfile):
550 shutil.move(cronfile, backfile)
552 fp = open(backfile, 'w')
556 fp = open(backfile, 'r')
557 op = open(cronfile, 'w')
559 if '@reboot' not in line:
563 op.write('@reboot python %s\n' % sysvals.cronjobCmdString())
565 res = call('crontab %s' % cronfile, shell=True)
567 print 'Exception: %s' % str(e)
568 shutil.move(backfile, cronfile)
571 doError('crontab failed')
573 # Function: updateGrub
575 # update grub.cfg for all kernels with our parameters
576 def updateGrub(restore=False):
577 # call update-grub on restore
580 call(['update-grub'], stderr=PIPE, stdout=PIPE,
581 env={'PATH': '.:/sbin:/usr/sbin:/usr/bin:/sbin:/bin'})
583 print 'Exception: %s\n' % str(e)
585 # verify we can do this
586 sysvals.rootUser(True)
587 grubfile = '/etc/default/grub'
588 if not os.path.exists(grubfile):
589 print 'ERROR: Unable to set the kernel parameters via grub.\n'
590 sysvals.manualRebootRequired()
591 out = Popen(['which', 'update-grub'], stdout=PIPE).stdout.read()
593 print 'ERROR: Unable to set the kernel parameters via grub.\n'
594 sysvals.manualRebootRequired()
596 # extract the option and create a grub config without it
597 tgtopt = 'GRUB_CMDLINE_LINUX_DEFAULT'
599 tempfile = '/etc/default/grub.analyze_boot'
600 shutil.move(grubfile, tempfile)
603 fp = open(tempfile, 'r')
604 op = open(grubfile, 'w')
608 if len(line) == 0 or line[0] == '#':
610 opt = line.split('=')[0].strip()
612 cmdline = line.split('=', 1)[1].strip('\\')
616 cmdline += line.strip('\\')
620 op.write('%s\n' % line)
622 # if the target option value is in quotes, strip them
624 val = cmdline.strip()
625 if val[0] == '\'' or val[0] == '"':
629 # append our cmd line options
632 cmdline += sysvals.kernelParams()
633 # write out the updated target option
634 op.write('\n%s=%s%s%s\n' % (tgtopt, sp, cmdline, sp))
636 res = call('update-grub')
639 print 'Exception: %s' % str(e)
642 shutil.move(tempfile, grubfile)
644 doError('update-grub failed')
648 # generic error function for catastrphic failures
650 # msg: the error message to print
651 # help: True if printHelp should be called after, False otherwise
652 def doError(msg, help=False):
655 print 'ERROR: %s\n' % msg
658 # Function: printHelp
660 # print out the help text
663 print('%s v%.1f' % (sysvals.title, sysvals.version))
664 print('Usage: bootgraph <options> <command>')
666 print('Description:')
667 print(' This tool reads in a dmesg log of linux kernel boot and')
668 print(' creates an html representation of the boot timeline up to')
669 print(' the start of the init process.')
671 print(' If no specific command is given the tool reads the current dmesg')
672 print(' and/or ftrace log and outputs bootgraph.html')
675 print(' -h Print this help text')
676 print(' -v Print the current tool version')
677 print(' -addlogs Add the dmesg log to the html output')
678 print(' -o file Html timeline name (default: bootgraph.html)')
680 print(' -f Use ftrace to add function detail (default: disabled)')
681 print(' -callgraph Add callgraph detail, can be very large (default: disabled)')
682 print(' -maxdepth N limit the callgraph data to N call levels (default: 2)')
683 print(' -mincg ms Discard all callgraphs shorter than ms milliseconds (e.g. 0.001 for us)')
684 print(' -timeprec N Number of significant digits in timestamps (0:S, 3:ms, [6:us])')
685 print(' -expandcg pre-expand the callgraph data in the html output (default: disabled)')
686 print(' -filter list Limit ftrace to comma-delimited list of functions (default: do_one_initcall)')
688 print(' -reboot Reboot the machine automatically and generate a new timeline')
689 print(' -manual Show the requirements to generate a new timeline manually')
690 print(' -dmesg file Load a stored dmesg file (used with -ftrace)')
691 print(' -ftrace file Load a stored ftrace file (used with -dmesg)')
692 print(' -flistall Print all functions capable of being captured in ftrace')
696 # ----------------- MAIN --------------------
697 # exec start (skipped if script is loaded as library)
698 if __name__ == '__main__':
699 # loop through the command line arguments
701 simplecmds = ['-updategrub', '-flistall']
702 args = iter(sys.argv[1:])
708 print("Version %.1f" % sysvals.version)
710 elif(arg in simplecmds):
713 sysvals.useftrace = True
714 elif(arg == '-callgraph'):
715 sysvals.useftrace = True
716 sysvals.usecallgraph = True
717 elif(arg == '-mincg'):
718 sysvals.mincglen = aslib.getArgFloat('-mincg', args, 0.0, 10000.0)
719 elif(arg == '-timeprec'):
720 sysvals.setPrecision(aslib.getArgInt('-timeprec', args, 0, 6))
721 elif(arg == '-maxdepth'):
722 sysvals.max_graph_depth = aslib.getArgInt('-maxdepth', args, 0, 1000)
723 elif(arg == '-filter'):
727 doError('No filter functions supplied', True)
728 aslib.rootCheck(True)
729 sysvals.setGraphFilter(val)
730 elif(arg == '-ftrace'):
734 doError('No ftrace file supplied', True)
735 if(os.path.exists(val) == False):
736 doError('%s does not exist' % val)
737 sysvals.ftracefile = val
738 elif(arg == '-addlogs'):
739 sysvals.addlogs = True
740 elif(arg == '-expandcg'):
742 elif(arg == '-dmesg'):
746 doError('No dmesg file supplied', True)
747 if(os.path.exists(val) == False):
748 doError('%s does not exist' % val)
749 if(sysvals.htmlfile == val or sysvals.outfile == val):
750 doError('Output filename collision')
751 sysvals.dmesgfile = val
756 doError('No HTML filename supplied', True)
757 if(sysvals.dmesgfile == val or sysvals.ftracefile == val):
758 doError('Output filename collision')
759 sysvals.htmlfile = val
760 elif(arg == '-reboot'):
761 if sysvals.iscronjob:
762 doError('-reboot and -cronjob are incompatible')
763 sysvals.reboot = True
764 elif(arg == '-manual'):
765 sysvals.reboot = True
766 sysvals.manual = True
767 # remaining options are only for cron job use
768 elif(arg == '-cronjob'):
769 sysvals.iscronjob = True
771 doError('-reboot and -cronjob are incompatible')
773 doError('Invalid argument: '+arg, True)
776 if cmd == 'updategrub':
778 elif cmd == 'flistall':
779 sysvals.getFtraceFilterFunctions(False)
782 # update grub, setup a cronjob, and reboot
784 if not sysvals.manual:
789 sysvals.manualRebootRequired()
792 # disable the cronjob
793 if sysvals.iscronjob:
797 data = loadKernelLog()
798 if sysvals.useftrace:
800 if sysvals.iscronjob:
802 sysvals.fsetVal('0', 'tracing_on')
806 if(sysvals.outfile and sysvals.phoronix):
807 fp = open(sysvals.outfile, 'w')
808 fp.write('pass %s initstart %.3f end %.3f boot %s\n' %
809 (data.valid, data.initstart*1000, data.end*1000, data.boottime))
812 if sysvals.dmesgfile:
813 doError('No initcall data found in %s' % sysvals.dmesgfile)
815 doError('No initcall data found, is initcall_debug enabled?')
817 print(' Host: %s' % sysvals.hostname)
818 print(' Test time: %s' % sysvals.testtime)
819 print(' Boot time: %s' % data.boottime)
820 print('Kernel Version: %s' % sysvals.kernel)
821 print(' Kernel start: %.3f' % (data.start * 1000))
822 print(' init start: %.3f' % (data.initstart * 1000))
824 createBootGraph(data, sysvals.phoronix)