1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0-only
3#
4# Tool for analyzing suspend/resume timing
5# Copyright (c) 2013, Intel Corporation.
6#
7# This program is free software; you can redistribute it and/or modify it
8# under the terms and conditions of the GNU General Public License,
9# version 2, as published by the Free Software Foundation.
10#
11# This program is distributed in the hope it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
14# more details.
15#
16# Authors:
17#	 Todd Brandt <todd.e.brandt@linux.intel.com>
18#
19# Links:
20#	 Home Page
21#	   https://01.org/pm-graph
22#	 Source repo
23#	   git@github.com:intel/pm-graph
24#
25# Description:
26#	 This tool is designed to assist kernel and OS developers in optimizing
27#	 their linux stack's suspend/resume time. Using a kernel image built
28#	 with a few extra options enabled, the tool will execute a suspend and
29#	 will capture dmesg and ftrace data until resume is complete. This data
30#	 is transformed into a device timeline and a callgraph to give a quick
31#	 and detailed view of which devices and callbacks are taking the most
32#	 time in suspend/resume. The output is a single html file which can be
33#	 viewed in firefox or chrome.
34#
35#	 The following kernel build options are required:
36#		 CONFIG_DEVMEM=y
37#		 CONFIG_PM_DEBUG=y
38#		 CONFIG_PM_SLEEP_DEBUG=y
39#		 CONFIG_FTRACE=y
40#		 CONFIG_FUNCTION_TRACER=y
41#		 CONFIG_FUNCTION_GRAPH_TRACER=y
42#		 CONFIG_KPROBES=y
43#		 CONFIG_KPROBES_ON_FTRACE=y
44#
45#	 For kernel versions older than 3.15:
46#	 The following additional kernel parameters are required:
47#		 (e.g. in file /etc/default/grub)
48#		 GRUB_CMDLINE_LINUX_DEFAULT="... initcall_debug log_buf_len=16M ..."
49#
50
51# ----------------- LIBRARIES --------------------
52
53import sys
54import time
55import os
56import string
57import re
58import platform
59import signal
60import codecs
61from datetime import datetime, timedelta
62import struct
63import configparser
64import gzip
65from threading import Thread
66from subprocess import call, Popen, PIPE
67import base64
68
69def pprint(msg):
70	print(msg)
71	sys.stdout.flush()
72
73def ascii(text):
74	return text.decode('ascii', 'ignore')
75
76# ----------------- CLASSES --------------------
77
78# Class: SystemValues
79# Description:
80#	 A global, single-instance container used to
81#	 store system values and test parameters
82class SystemValues:
83	title = 'SleepGraph'
84	version = '5.6'
85	ansi = False
86	rs = 0
87	display = ''
88	gzip = False
89	sync = False
90	wifi = False
91	verbose = False
92	testlog = True
93	dmesglog = True
94	ftracelog = False
95	tstat = True
96	mindevlen = 0.0
97	mincglen = 0.0
98	cgphase = ''
99	cgtest = -1
100	cgskip = ''
101	maxfail = 0
102	multitest = {'run': False, 'count': 1000000, 'delay': 0}
103	max_graph_depth = 0
104	callloopmaxgap = 0.0001
105	callloopmaxlen = 0.005
106	bufsize = 0
107	cpucount = 0
108	memtotal = 204800
109	memfree = 204800
110	srgap = 0
111	cgexp = False
112	testdir = ''
113	outdir = ''
114	tpath = '/sys/kernel/debug/tracing/'
115	fpdtpath = '/sys/firmware/acpi/tables/FPDT'
116	epath = '/sys/kernel/debug/tracing/events/power/'
117	pmdpath = '/sys/power/pm_debug_messages'
118	traceevents = [
119		'suspend_resume',
120		'wakeup_source_activate',
121		'wakeup_source_deactivate',
122		'device_pm_callback_end',
123		'device_pm_callback_start'
124	]
125	logmsg = ''
126	testcommand = ''
127	mempath = '/dev/mem'
128	powerfile = '/sys/power/state'
129	mempowerfile = '/sys/power/mem_sleep'
130	diskpowerfile = '/sys/power/disk'
131	suspendmode = 'mem'
132	memmode = ''
133	diskmode = ''
134	hostname = 'localhost'
135	prefix = 'test'
136	teststamp = ''
137	sysstamp = ''
138	dmesgstart = 0.0
139	dmesgfile = ''
140	ftracefile = ''
141	htmlfile = 'output.html'
142	result = ''
143	rtcwake = True
144	rtcwaketime = 15
145	rtcpath = ''
146	devicefilter = []
147	cgfilter = []
148	stamp = 0
149	execcount = 1
150	x2delay = 0
151	skiphtml = False
152	usecallgraph = False
153	ftopfunc = 'pm_suspend'
154	ftop = False
155	usetraceevents = False
156	usetracemarkers = True
157	usekprobes = True
158	usedevsrc = False
159	useprocmon = False
160	notestrun = False
161	cgdump = False
162	devdump = False
163	mixedphaseheight = True
164	devprops = dict()
165	platinfo = []
166	predelay = 0
167	postdelay = 0
168	pmdebug = ''
169	tmstart = 'SUSPEND START %Y%m%d-%H:%M:%S.%f'
170	tmend = 'RESUME COMPLETE %Y%m%d-%H:%M:%S.%f'
171	tracefuncs = {
172		'sys_sync': {},
173		'ksys_sync': {},
174		'__pm_notifier_call_chain': {},
175		'pm_prepare_console': {},
176		'pm_notifier_call_chain': {},
177		'freeze_processes': {},
178		'freeze_kernel_threads': {},
179		'pm_restrict_gfp_mask': {},
180		'acpi_suspend_begin': {},
181		'acpi_hibernation_begin': {},
182		'acpi_hibernation_enter': {},
183		'acpi_hibernation_leave': {},
184		'acpi_pm_freeze': {},
185		'acpi_pm_thaw': {},
186		'acpi_s2idle_end': {},
187		'acpi_s2idle_sync': {},
188		'acpi_s2idle_begin': {},
189		'acpi_s2idle_prepare': {},
190		'acpi_s2idle_prepare_late': {},
191		'acpi_s2idle_wake': {},
192		'acpi_s2idle_wakeup': {},
193		'acpi_s2idle_restore': {},
194		'acpi_s2idle_restore_early': {},
195		'hibernate_preallocate_memory': {},
196		'create_basic_memory_bitmaps': {},
197		'swsusp_write': {},
198		'suspend_console': {},
199		'acpi_pm_prepare': {},
200		'syscore_suspend': {},
201		'arch_thaw_secondary_cpus_end': {},
202		'syscore_resume': {},
203		'acpi_pm_finish': {},
204		'resume_console': {},
205		'acpi_pm_end': {},
206		'pm_restore_gfp_mask': {},
207		'thaw_processes': {},
208		'pm_restore_console': {},
209		'CPU_OFF': {
210			'func':'_cpu_down',
211			'args_x86_64': {'cpu':'%di:s32'},
212			'format': 'CPU_OFF[{cpu}]'
213		},
214		'CPU_ON': {
215			'func':'_cpu_up',
216			'args_x86_64': {'cpu':'%di:s32'},
217			'format': 'CPU_ON[{cpu}]'
218		},
219	}
220	dev_tracefuncs = {
221		# general wait/delay/sleep
222		'msleep': { 'args_x86_64': {'time':'%di:s32'}, 'ub': 1 },
223		'schedule_timeout': { 'args_x86_64': {'timeout':'%di:s32'}, 'ub': 1 },
224		'udelay': { 'func':'__const_udelay', 'args_x86_64': {'loops':'%di:s32'}, 'ub': 1 },
225		'usleep_range': { 'args_x86_64': {'min':'%di:s32', 'max':'%si:s32'}, 'ub': 1 },
226		'mutex_lock_slowpath': { 'func':'__mutex_lock_slowpath', 'ub': 1 },
227		'acpi_os_stall': {'ub': 1},
228		'rt_mutex_slowlock': {'ub': 1},
229		# ACPI
230		'acpi_resume_power_resources': {},
231		'acpi_ps_execute_method': { 'args_x86_64': {
232			'fullpath':'+0(+40(%di)):string',
233		}},
234		# mei_me
235		'mei_reset': {},
236		# filesystem
237		'ext4_sync_fs': {},
238		# 80211
239		'ath10k_bmi_read_memory': { 'args_x86_64': {'length':'%cx:s32'} },
240		'ath10k_bmi_write_memory': { 'args_x86_64': {'length':'%cx:s32'} },
241		'ath10k_bmi_fast_download': { 'args_x86_64': {'length':'%cx:s32'} },
242		'iwlagn_mac_start': {},
243		'iwlagn_alloc_bcast_station': {},
244		'iwl_trans_pcie_start_hw': {},
245		'iwl_trans_pcie_start_fw': {},
246		'iwl_run_init_ucode': {},
247		'iwl_load_ucode_wait_alive': {},
248		'iwl_alive_start': {},
249		'iwlagn_mac_stop': {},
250		'iwlagn_mac_suspend': {},
251		'iwlagn_mac_resume': {},
252		'iwlagn_mac_add_interface': {},
253		'iwlagn_mac_remove_interface': {},
254		'iwlagn_mac_change_interface': {},
255		'iwlagn_mac_config': {},
256		'iwlagn_configure_filter': {},
257		'iwlagn_mac_hw_scan': {},
258		'iwlagn_bss_info_changed': {},
259		'iwlagn_mac_channel_switch': {},
260		'iwlagn_mac_flush': {},
261		# ATA
262		'ata_eh_recover': { 'args_x86_64': {'port':'+36(%di):s32'} },
263		# i915
264		'i915_gem_resume': {},
265		'i915_restore_state': {},
266		'intel_opregion_setup': {},
267		'g4x_pre_enable_dp': {},
268		'vlv_pre_enable_dp': {},
269		'chv_pre_enable_dp': {},
270		'g4x_enable_dp': {},
271		'vlv_enable_dp': {},
272		'intel_hpd_init': {},
273		'intel_opregion_register': {},
274		'intel_dp_detect': {},
275		'intel_hdmi_detect': {},
276		'intel_opregion_init': {},
277		'intel_fbdev_set_suspend': {},
278	}
279	infocmds = [
280		[0, 'kparams', 'cat', '/proc/cmdline'],
281		[0, 'mcelog', 'mcelog'],
282		[0, 'pcidevices', 'lspci', '-tv'],
283		[0, 'usbdevices', 'lsusb', '-t'],
284		[1, 'interrupts', 'cat', '/proc/interrupts'],
285		[1, 'wakeups', 'cat', '/sys/kernel/debug/wakeup_sources'],
286		[2, 'gpecounts', 'sh', '-c', 'grep -v invalid /sys/firmware/acpi/interrupts/*'],
287		[2, 'suspendstats', 'sh', '-c', 'grep -v invalid /sys/power/suspend_stats/*'],
288		[2, 'cpuidle', 'sh', '-c', 'grep -v invalid /sys/devices/system/cpu/cpu*/cpuidle/state*/s2idle/*'],
289		[2, 'battery', 'sh', '-c', 'grep -v invalid /sys/class/power_supply/*/*'],
290	]
291	cgblacklist = []
292	kprobes = dict()
293	timeformat = '%.3f'
294	cmdline = '%s %s' % \
295			(os.path.basename(sys.argv[0]), ' '.join(sys.argv[1:]))
296	sudouser = ''
297	def __init__(self):
298		self.archargs = 'args_'+platform.machine()
299		self.hostname = platform.node()
300		if(self.hostname == ''):
301			self.hostname = 'localhost'
302		rtc = "rtc0"
303		if os.path.exists('/dev/rtc'):
304			rtc = os.readlink('/dev/rtc')
305		rtc = '/sys/class/rtc/'+rtc
306		if os.path.exists(rtc) and os.path.exists(rtc+'/date') and \
307			os.path.exists(rtc+'/time') and os.path.exists(rtc+'/wakealarm'):
308			self.rtcpath = rtc
309		if (hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()):
310			self.ansi = True
311		self.testdir = datetime.now().strftime('suspend-%y%m%d-%H%M%S')
312		if os.getuid() == 0 and 'SUDO_USER' in os.environ and \
313			os.environ['SUDO_USER']:
314			self.sudouser = os.environ['SUDO_USER']
315	def resetlog(self):
316		self.logmsg = ''
317		self.platinfo = []
318	def vprint(self, msg):
319		self.logmsg += msg+'\n'
320		if self.verbose or msg.startswith('WARNING:'):
321			pprint(msg)
322	def signalHandler(self, signum, frame):
323		if not self.result:
324			return
325		signame = self.signames[signum] if signum in self.signames else 'UNKNOWN'
326		msg = 'Signal %s caused a tool exit, line %d' % (signame, frame.f_lineno)
327		self.outputResult({'error':msg})
328		sys.exit(3)
329	def signalHandlerInit(self):
330		capture = ['BUS', 'SYS', 'XCPU', 'XFSZ', 'PWR', 'HUP', 'INT', 'QUIT',
331			'ILL', 'ABRT', 'FPE', 'SEGV', 'TERM']
332		self.signames = dict()
333		for i in capture:
334			s = 'SIG'+i
335			try:
336				signum = getattr(signal, s)
337				signal.signal(signum, self.signalHandler)
338			except:
339				continue
340			self.signames[signum] = s
341	def rootCheck(self, fatal=True):
342		if(os.access(self.powerfile, os.W_OK)):
343			return True
344		if fatal:
345			msg = 'This command requires sysfs mount and root access'
346			pprint('ERROR: %s\n' % msg)
347			self.outputResult({'error':msg})
348			sys.exit(1)
349		return False
350	def rootUser(self, fatal=False):
351		if 'USER' in os.environ and os.environ['USER'] == 'root':
352			return True
353		if fatal:
354			msg = 'This command must be run as root'
355			pprint('ERROR: %s\n' % msg)
356			self.outputResult({'error':msg})
357			sys.exit(1)
358		return False
359	def usable(self, file):
360		return (os.path.exists(file) and os.path.getsize(file) > 0)
361	def getExec(self, cmd):
362		try:
363			fp = Popen(['which', cmd], stdout=PIPE, stderr=PIPE).stdout
364			out = ascii(fp.read()).strip()
365			fp.close()
366		except:
367			out = ''
368		if out:
369			return out
370		for path in ['/sbin', '/bin', '/usr/sbin', '/usr/bin',
371			'/usr/local/sbin', '/usr/local/bin']:
372			cmdfull = os.path.join(path, cmd)
373			if os.path.exists(cmdfull):
374				return cmdfull
375		return out
376	def setPrecision(self, num):
377		if num < 0 or num > 6:
378			return
379		self.timeformat = '%.{0}f'.format(num)
380	def setOutputFolder(self, value):
381		args = dict()
382		n = datetime.now()
383		args['date'] = n.strftime('%y%m%d')
384		args['time'] = n.strftime('%H%M%S')
385		args['hostname'] = args['host'] = self.hostname
386		args['mode'] = self.suspendmode
387		return value.format(**args)
388	def setOutputFile(self):
389		if self.dmesgfile != '':
390			m = re.match('(?P<name>.*)_dmesg\.txt.*', self.dmesgfile)
391			if(m):
392				self.htmlfile = m.group('name')+'.html'
393		if self.ftracefile != '':
394			m = re.match('(?P<name>.*)_ftrace\.txt.*', self.ftracefile)
395			if(m):
396				self.htmlfile = m.group('name')+'.html'
397	def systemInfo(self, info):
398		p = m = ''
399		if 'baseboard-manufacturer' in info:
400			m = info['baseboard-manufacturer']
401		elif 'system-manufacturer' in info:
402			m = info['system-manufacturer']
403		if 'system-product-name' in info:
404			p = info['system-product-name']
405		elif 'baseboard-product-name' in info:
406			p = info['baseboard-product-name']
407		if m[:5].lower() == 'intel' and 'baseboard-product-name' in info:
408			p = info['baseboard-product-name']
409		c = info['processor-version'] if 'processor-version' in info else ''
410		b = info['bios-version'] if 'bios-version' in info else ''
411		r = info['bios-release-date'] if 'bios-release-date' in info else ''
412		self.sysstamp = '# sysinfo | man:%s | plat:%s | cpu:%s | bios:%s | biosdate:%s | numcpu:%d | memsz:%d | memfr:%d' % \
413			(m, p, c, b, r, self.cpucount, self.memtotal, self.memfree)
414	def printSystemInfo(self, fatal=False):
415		self.rootCheck(True)
416		out = dmidecode(self.mempath, fatal)
417		if len(out) < 1:
418			return
419		fmt = '%-24s: %s'
420		for name in sorted(out):
421			print(fmt % (name, out[name]))
422		print(fmt % ('cpucount', ('%d' % self.cpucount)))
423		print(fmt % ('memtotal', ('%d kB' % self.memtotal)))
424		print(fmt % ('memfree', ('%d kB' % self.memfree)))
425	def cpuInfo(self):
426		self.cpucount = 0
427		fp = open('/proc/cpuinfo', 'r')
428		for line in fp:
429			if re.match('^processor[ \t]*:[ \t]*[0-9]*', line):
430				self.cpucount += 1
431		fp.close()
432		fp = open('/proc/meminfo', 'r')
433		for line in fp:
434			m = re.match('^MemTotal:[ \t]*(?P<sz>[0-9]*) *kB', line)
435			if m:
436				self.memtotal = int(m.group('sz'))
437			m = re.match('^MemFree:[ \t]*(?P<sz>[0-9]*) *kB', line)
438			if m:
439				self.memfree = int(m.group('sz'))
440		fp.close()
441	def initTestOutput(self, name):
442		self.prefix = self.hostname
443		v = open('/proc/version', 'r').read().strip()
444		kver = v.split()[2]
445		fmt = name+'-%m%d%y-%H%M%S'
446		testtime = datetime.now().strftime(fmt)
447		self.teststamp = \
448			'# '+testtime+' '+self.prefix+' '+self.suspendmode+' '+kver
449		ext = ''
450		if self.gzip:
451			ext = '.gz'
452		self.dmesgfile = \
453			self.testdir+'/'+self.prefix+'_'+self.suspendmode+'_dmesg.txt'+ext
454		self.ftracefile = \
455			self.testdir+'/'+self.prefix+'_'+self.suspendmode+'_ftrace.txt'+ext
456		self.htmlfile = \
457			self.testdir+'/'+self.prefix+'_'+self.suspendmode+'.html'
458		if not os.path.isdir(self.testdir):
459			os.makedirs(self.testdir)
460		self.sudoUserchown(self.testdir)
461	def getValueList(self, value):
462		out = []
463		for i in value.split(','):
464			if i.strip():
465				out.append(i.strip())
466		return out
467	def setDeviceFilter(self, value):
468		self.devicefilter = self.getValueList(value)
469	def setCallgraphFilter(self, value):
470		self.cgfilter = self.getValueList(value)
471	def skipKprobes(self, value):
472		for k in self.getValueList(value):
473			if k in self.tracefuncs:
474				del self.tracefuncs[k]
475			if k in self.dev_tracefuncs:
476				del self.dev_tracefuncs[k]
477	def setCallgraphBlacklist(self, file):
478		self.cgblacklist = self.listFromFile(file)
479	def rtcWakeAlarmOn(self):
480		call('echo 0 > '+self.rtcpath+'/wakealarm', shell=True)
481		nowtime = open(self.rtcpath+'/since_epoch', 'r').read().strip()
482		if nowtime:
483			nowtime = int(nowtime)
484		else:
485			# if hardware time fails, use the software time
486			nowtime = int(datetime.now().strftime('%s'))
487		alarm = nowtime + self.rtcwaketime
488		call('echo %d > %s/wakealarm' % (alarm, self.rtcpath), shell=True)
489	def rtcWakeAlarmOff(self):
490		call('echo 0 > %s/wakealarm' % self.rtcpath, shell=True)
491	def initdmesg(self):
492		# get the latest time stamp from the dmesg log
493		fp = Popen('dmesg', stdout=PIPE).stdout
494		ktime = '0'
495		for line in fp:
496			line = ascii(line).replace('\r\n', '')
497			idx = line.find('[')
498			if idx > 1:
499				line = line[idx:]
500			m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
501			if(m):
502				ktime = m.group('ktime')
503		fp.close()
504		self.dmesgstart = float(ktime)
505	def getdmesg(self, testdata):
506		op = self.writeDatafileHeader(self.dmesgfile, testdata)
507		# store all new dmesg lines since initdmesg was called
508		fp = Popen('dmesg', stdout=PIPE).stdout
509		for line in fp:
510			line = ascii(line).replace('\r\n', '')
511			idx = line.find('[')
512			if idx > 1:
513				line = line[idx:]
514			m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
515			if(not m):
516				continue
517			ktime = float(m.group('ktime'))
518			if ktime > self.dmesgstart:
519				op.write(line)
520		fp.close()
521		op.close()
522	def listFromFile(self, file):
523		list = []
524		fp = open(file)
525		for i in fp.read().split('\n'):
526			i = i.strip()
527			if i and i[0] != '#':
528				list.append(i)
529		fp.close()
530		return list
531	def addFtraceFilterFunctions(self, file):
532		for i in self.listFromFile(file):
533			if len(i) < 2:
534				continue
535			self.tracefuncs[i] = dict()
536	def getFtraceFilterFunctions(self, current):
537		self.rootCheck(True)
538		if not current:
539			call('cat '+self.tpath+'available_filter_functions', shell=True)
540			return
541		master = self.listFromFile(self.tpath+'available_filter_functions')
542		for i in sorted(self.tracefuncs):
543			if 'func' in self.tracefuncs[i]:
544				i = self.tracefuncs[i]['func']
545			if i in master:
546				print(i)
547			else:
548				print(self.colorText(i))
549	def setFtraceFilterFunctions(self, list):
550		master = self.listFromFile(self.tpath+'available_filter_functions')
551		flist = ''
552		for i in list:
553			if i not in master:
554				continue
555			if ' [' in i:
556				flist += i.split(' ')[0]+'\n'
557			else:
558				flist += i+'\n'
559		fp = open(self.tpath+'set_graph_function', 'w')
560		fp.write(flist)
561		fp.close()
562	def basicKprobe(self, name):
563		self.kprobes[name] = {'name': name,'func': name,'args': dict(),'format': name}
564	def defaultKprobe(self, name, kdata):
565		k = kdata
566		for field in ['name', 'format', 'func']:
567			if field not in k:
568				k[field] = name
569		if self.archargs in k:
570			k['args'] = k[self.archargs]
571		else:
572			k['args'] = dict()
573			k['format'] = name
574		self.kprobes[name] = k
575	def kprobeColor(self, name):
576		if name not in self.kprobes or 'color' not in self.kprobes[name]:
577			return ''
578		return self.kprobes[name]['color']
579	def kprobeDisplayName(self, name, dataraw):
580		if name not in self.kprobes:
581			self.basicKprobe(name)
582		data = ''
583		quote=0
584		# first remvoe any spaces inside quotes, and the quotes
585		for c in dataraw:
586			if c == '"':
587				quote = (quote + 1) % 2
588			if quote and c == ' ':
589				data += '_'
590			elif c != '"':
591				data += c
592		fmt, args = self.kprobes[name]['format'], self.kprobes[name]['args']
593		arglist = dict()
594		# now process the args
595		for arg in sorted(args):
596			arglist[arg] = ''
597			m = re.match('.* '+arg+'=(?P<arg>.*) ', data);
598			if m:
599				arglist[arg] = m.group('arg')
600			else:
601				m = re.match('.* '+arg+'=(?P<arg>.*)', data);
602				if m:
603					arglist[arg] = m.group('arg')
604		out = fmt.format(**arglist)
605		out = out.replace(' ', '_').replace('"', '')
606		return out
607	def kprobeText(self, kname, kprobe):
608		name = fmt = func = kname
609		args = dict()
610		if 'name' in kprobe:
611			name = kprobe['name']
612		if 'format' in kprobe:
613			fmt = kprobe['format']
614		if 'func' in kprobe:
615			func = kprobe['func']
616		if self.archargs in kprobe:
617			args = kprobe[self.archargs]
618		if 'args' in kprobe:
619			args = kprobe['args']
620		if re.findall('{(?P<n>[a-z,A-Z,0-9]*)}', func):
621			doError('Kprobe "%s" has format info in the function name "%s"' % (name, func))
622		for arg in re.findall('{(?P<n>[a-z,A-Z,0-9]*)}', fmt):
623			if arg not in args:
624				doError('Kprobe "%s" is missing argument "%s"' % (name, arg))
625		val = 'p:%s_cal %s' % (name, func)
626		for i in sorted(args):
627			val += ' %s=%s' % (i, args[i])
628		val += '\nr:%s_ret %s $retval\n' % (name, func)
629		return val
630	def addKprobes(self, output=False):
631		if len(self.kprobes) < 1:
632			return
633		if output:
634			pprint('    kprobe functions in this kernel:')
635		# first test each kprobe
636		rejects = []
637		# sort kprobes: trace, ub-dev, custom, dev
638		kpl = [[], [], [], []]
639		linesout = len(self.kprobes)
640		for name in sorted(self.kprobes):
641			res = self.colorText('YES', 32)
642			if not self.testKprobe(name, self.kprobes[name]):
643				res = self.colorText('NO')
644				rejects.append(name)
645			else:
646				if name in self.tracefuncs:
647					kpl[0].append(name)
648				elif name in self.dev_tracefuncs:
649					if 'ub' in self.dev_tracefuncs[name]:
650						kpl[1].append(name)
651					else:
652						kpl[3].append(name)
653				else:
654					kpl[2].append(name)
655			if output:
656				pprint('         %s: %s' % (name, res))
657		kplist = kpl[0] + kpl[1] + kpl[2] + kpl[3]
658		# remove all failed ones from the list
659		for name in rejects:
660			self.kprobes.pop(name)
661		# set the kprobes all at once
662		self.fsetVal('', 'kprobe_events')
663		kprobeevents = ''
664		for kp in kplist:
665			kprobeevents += self.kprobeText(kp, self.kprobes[kp])
666		self.fsetVal(kprobeevents, 'kprobe_events')
667		if output:
668			check = self.fgetVal('kprobe_events')
669			linesack = (len(check.split('\n')) - 1) // 2
670			pprint('    kprobe functions enabled: %d/%d' % (linesack, linesout))
671		self.fsetVal('1', 'events/kprobes/enable')
672	def testKprobe(self, kname, kprobe):
673		self.fsetVal('0', 'events/kprobes/enable')
674		kprobeevents = self.kprobeText(kname, kprobe)
675		if not kprobeevents:
676			return False
677		try:
678			self.fsetVal(kprobeevents, 'kprobe_events')
679			check = self.fgetVal('kprobe_events')
680		except:
681			return False
682		linesout = len(kprobeevents.split('\n'))
683		linesack = len(check.split('\n'))
684		if linesack < linesout:
685			return False
686		return True
687	def setVal(self, val, file):
688		if not os.path.exists(file):
689			return False
690		try:
691			fp = open(file, 'wb', 0)
692			fp.write(val.encode())
693			fp.flush()
694			fp.close()
695		except:
696			return False
697		return True
698	def fsetVal(self, val, path):
699		return self.setVal(val, self.tpath+path)
700	def getVal(self, file):
701		res = ''
702		if not os.path.exists(file):
703			return res
704		try:
705			fp = open(file, 'r')
706			res = fp.read()
707			fp.close()
708		except:
709			pass
710		return res
711	def fgetVal(self, path):
712		return self.getVal(self.tpath+path)
713	def cleanupFtrace(self):
714		if(self.usecallgraph or self.usetraceevents or self.usedevsrc):
715			self.fsetVal('0', 'events/kprobes/enable')
716			self.fsetVal('', 'kprobe_events')
717			self.fsetVal('1024', 'buffer_size_kb')
718		if self.pmdebug:
719			self.setVal(self.pmdebug, self.pmdpath)
720	def setupAllKprobes(self):
721		for name in self.tracefuncs:
722			self.defaultKprobe(name, self.tracefuncs[name])
723		for name in self.dev_tracefuncs:
724			self.defaultKprobe(name, self.dev_tracefuncs[name])
725	def isCallgraphFunc(self, name):
726		if len(self.tracefuncs) < 1 and self.suspendmode == 'command':
727			return True
728		for i in self.tracefuncs:
729			if 'func' in self.tracefuncs[i]:
730				f = self.tracefuncs[i]['func']
731			else:
732				f = i
733			if name == f:
734				return True
735		return False
736	def initFtrace(self, quiet=False):
737		if not quiet:
738			sysvals.printSystemInfo(False)
739			pprint('INITIALIZING FTRACE...')
740		# turn trace off
741		self.fsetVal('0', 'tracing_on')
742		self.cleanupFtrace()
743		# pm debug messages
744		pv = self.getVal(self.pmdpath)
745		if pv != '1':
746			self.setVal('1', self.pmdpath)
747			self.pmdebug = pv
748		# set the trace clock to global
749		self.fsetVal('global', 'trace_clock')
750		self.fsetVal('nop', 'current_tracer')
751		# set trace buffer to an appropriate value
752		cpus = max(1, self.cpucount)
753		if self.bufsize > 0:
754			tgtsize = self.bufsize
755		elif self.usecallgraph or self.usedevsrc:
756			bmax = (1*1024*1024) if self.suspendmode in ['disk', 'command'] \
757				else (3*1024*1024)
758			tgtsize = min(self.memfree, bmax)
759		else:
760			tgtsize = 65536
761		while not self.fsetVal('%d' % (tgtsize // cpus), 'buffer_size_kb'):
762			# if the size failed to set, lower it and keep trying
763			tgtsize -= 65536
764			if tgtsize < 65536:
765				tgtsize = int(self.fgetVal('buffer_size_kb')) * cpus
766				break
767		self.vprint('Setting trace buffers to %d kB (%d kB per cpu)' % (tgtsize, tgtsize/cpus))
768		# initialize the callgraph trace
769		if(self.usecallgraph):
770			# set trace type
771			self.fsetVal('function_graph', 'current_tracer')
772			self.fsetVal('', 'set_ftrace_filter')
773			# set trace format options
774			self.fsetVal('print-parent', 'trace_options')
775			self.fsetVal('funcgraph-abstime', 'trace_options')
776			self.fsetVal('funcgraph-cpu', 'trace_options')
777			self.fsetVal('funcgraph-duration', 'trace_options')
778			self.fsetVal('funcgraph-proc', 'trace_options')
779			self.fsetVal('funcgraph-tail', 'trace_options')
780			self.fsetVal('nofuncgraph-overhead', 'trace_options')
781			self.fsetVal('context-info', 'trace_options')
782			self.fsetVal('graph-time', 'trace_options')
783			self.fsetVal('%d' % self.max_graph_depth, 'max_graph_depth')
784			cf = ['dpm_run_callback']
785			if(self.usetraceevents):
786				cf += ['dpm_prepare', 'dpm_complete']
787			for fn in self.tracefuncs:
788				if 'func' in self.tracefuncs[fn]:
789					cf.append(self.tracefuncs[fn]['func'])
790				else:
791					cf.append(fn)
792			if self.ftop:
793				self.setFtraceFilterFunctions([self.ftopfunc])
794			else:
795				self.setFtraceFilterFunctions(cf)
796		# initialize the kprobe trace
797		elif self.usekprobes:
798			for name in self.tracefuncs:
799				self.defaultKprobe(name, self.tracefuncs[name])
800			if self.usedevsrc:
801				for name in self.dev_tracefuncs:
802					self.defaultKprobe(name, self.dev_tracefuncs[name])
803			if not quiet:
804				pprint('INITIALIZING KPROBES...')
805			self.addKprobes(self.verbose)
806		if(self.usetraceevents):
807			# turn trace events on
808			events = iter(self.traceevents)
809			for e in events:
810				self.fsetVal('1', 'events/power/'+e+'/enable')
811		# clear the trace buffer
812		self.fsetVal('', 'trace')
813	def verifyFtrace(self):
814		# files needed for any trace data
815		files = ['buffer_size_kb', 'current_tracer', 'trace', 'trace_clock',
816				 'trace_marker', 'trace_options', 'tracing_on']
817		# files needed for callgraph trace data
818		tp = self.tpath
819		if(self.usecallgraph):
820			files += [
821				'available_filter_functions',
822				'set_ftrace_filter',
823				'set_graph_function'
824			]
825		for f in files:
826			if(os.path.exists(tp+f) == False):
827				return False
828		return True
829	def verifyKprobes(self):
830		# files needed for kprobes to work
831		files = ['kprobe_events', 'events']
832		tp = self.tpath
833		for f in files:
834			if(os.path.exists(tp+f) == False):
835				return False
836		return True
837	def colorText(self, str, color=31):
838		if not self.ansi:
839			return str
840		return '\x1B[%d;40m%s\x1B[m' % (color, str)
841	def writeDatafileHeader(self, filename, testdata):
842		fp = self.openlog(filename, 'w')
843		fp.write('%s\n%s\n# command | %s\n' % (self.teststamp, self.sysstamp, self.cmdline))
844		for test in testdata:
845			if 'fw' in test:
846				fw = test['fw']
847				if(fw):
848					fp.write('# fwsuspend %u fwresume %u\n' % (fw[0], fw[1]))
849			if 'turbo' in test:
850				fp.write('# turbostat %s\n' % test['turbo'])
851			if 'wifi' in test:
852				fp.write('# wifi %s\n' % test['wifi'])
853			if test['error'] or len(testdata) > 1:
854				fp.write('# enter_sleep_error %s\n' % test['error'])
855		return fp
856	def sudoUserchown(self, dir):
857		if os.path.exists(dir) and self.sudouser:
858			cmd = 'chown -R {0}:{0} {1} > /dev/null 2>&1'
859			call(cmd.format(self.sudouser, dir), shell=True)
860	def outputResult(self, testdata, num=0):
861		if not self.result:
862			return
863		n = ''
864		if num > 0:
865			n = '%d' % num
866		fp = open(self.result, 'a')
867		if 'error' in testdata:
868			fp.write('result%s: fail\n' % n)
869			fp.write('error%s: %s\n' % (n, testdata['error']))
870		else:
871			fp.write('result%s: pass\n' % n)
872		for v in ['suspend', 'resume', 'boot', 'lastinit']:
873			if v in testdata:
874				fp.write('%s%s: %.3f\n' % (v, n, testdata[v]))
875		for v in ['fwsuspend', 'fwresume']:
876			if v in testdata:
877				fp.write('%s%s: %.3f\n' % (v, n, testdata[v] / 1000000.0))
878		if 'bugurl' in testdata:
879			fp.write('url%s: %s\n' % (n, testdata['bugurl']))
880		fp.close()
881		self.sudoUserchown(self.result)
882	def configFile(self, file):
883		dir = os.path.dirname(os.path.realpath(__file__))
884		if os.path.exists(file):
885			return file
886		elif os.path.exists(dir+'/'+file):
887			return dir+'/'+file
888		elif os.path.exists(dir+'/config/'+file):
889			return dir+'/config/'+file
890		return ''
891	def openlog(self, filename, mode):
892		isgz = self.gzip
893		if mode == 'r':
894			try:
895				with gzip.open(filename, mode+'t') as fp:
896					test = fp.read(64)
897				isgz = True
898			except:
899				isgz = False
900		if isgz:
901			return gzip.open(filename, mode+'t')
902		return open(filename, mode)
903	def b64unzip(self, data):
904		try:
905			out = codecs.decode(base64.b64decode(data), 'zlib').decode()
906		except:
907			out = data
908		return out
909	def b64zip(self, data):
910		out = base64.b64encode(codecs.encode(data.encode(), 'zlib')).decode()
911		return out
912	def platforminfo(self, cmdafter):
913		# add platform info on to a completed ftrace file
914		if not os.path.exists(self.ftracefile):
915			return False
916		footer = '#\n'
917
918		# add test command string line if need be
919		if self.suspendmode == 'command' and self.testcommand:
920			footer += '# platform-testcmd: %s\n' % (self.testcommand)
921
922		# get a list of target devices from the ftrace file
923		props = dict()
924		tp = TestProps()
925		tf = self.openlog(self.ftracefile, 'r')
926		for line in tf:
927			# determine the trace data type (required for further parsing)
928			m = re.match(tp.tracertypefmt, line)
929			if(m):
930				tp.setTracerType(m.group('t'))
931				continue
932			# parse only valid lines, if this is not one move on
933			m = re.match(tp.ftrace_line_fmt, line)
934			if(not m or 'device_pm_callback_start' not in line):
935				continue
936			m = re.match('.*: (?P<drv>.*) (?P<d>.*), parent: *(?P<p>.*), .*', m.group('msg'));
937			if(not m):
938				continue
939			dev = m.group('d')
940			if dev not in props:
941				props[dev] = DevProps()
942		tf.close()
943
944		# now get the syspath for each target device
945		for dirname, dirnames, filenames in os.walk('/sys/devices'):
946			if(re.match('.*/power', dirname) and 'async' in filenames):
947				dev = dirname.split('/')[-2]
948				if dev in props and (not props[dev].syspath or len(dirname) < len(props[dev].syspath)):
949					props[dev].syspath = dirname[:-6]
950
951		# now fill in the properties for our target devices
952		for dev in sorted(props):
953			dirname = props[dev].syspath
954			if not dirname or not os.path.exists(dirname):
955				continue
956			with open(dirname+'/power/async') as fp:
957				text = fp.read()
958				props[dev].isasync = False
959				if 'enabled' in text:
960					props[dev].isasync = True
961			fields = os.listdir(dirname)
962			if 'product' in fields:
963				with open(dirname+'/product', 'rb') as fp:
964					props[dev].altname = ascii(fp.read())
965			elif 'name' in fields:
966				with open(dirname+'/name', 'rb') as fp:
967					props[dev].altname = ascii(fp.read())
968			elif 'model' in fields:
969				with open(dirname+'/model', 'rb') as fp:
970					props[dev].altname = ascii(fp.read())
971			elif 'description' in fields:
972				with open(dirname+'/description', 'rb') as fp:
973					props[dev].altname = ascii(fp.read())
974			elif 'id' in fields:
975				with open(dirname+'/id', 'rb') as fp:
976					props[dev].altname = ascii(fp.read())
977			elif 'idVendor' in fields and 'idProduct' in fields:
978				idv, idp = '', ''
979				with open(dirname+'/idVendor', 'rb') as fp:
980					idv = ascii(fp.read()).strip()
981				with open(dirname+'/idProduct', 'rb') as fp:
982					idp = ascii(fp.read()).strip()
983				props[dev].altname = '%s:%s' % (idv, idp)
984			if props[dev].altname:
985				out = props[dev].altname.strip().replace('\n', ' ')\
986					.replace(',', ' ').replace(';', ' ')
987				props[dev].altname = out
988
989		# add a devinfo line to the bottom of ftrace
990		out = ''
991		for dev in sorted(props):
992			out += props[dev].out(dev)
993		footer += '# platform-devinfo: %s\n' % self.b64zip(out)
994
995		# add a line for each of these commands with their outputs
996		for name, cmdline, info in cmdafter:
997			footer += '# platform-%s: %s | %s\n' % (name, cmdline, self.b64zip(info))
998
999		with self.openlog(self.ftracefile, 'a') as fp:
1000			fp.write(footer)
1001		return True
1002	def commonPrefix(self, list):
1003		if len(list) < 2:
1004			return ''
1005		prefix = list[0]
1006		for s in list[1:]:
1007			while s[:len(prefix)] != prefix and prefix:
1008				prefix = prefix[:len(prefix)-1]
1009			if not prefix:
1010				break
1011		if '/' in prefix and prefix[-1] != '/':
1012			prefix = prefix[0:prefix.rfind('/')+1]
1013		return prefix
1014	def dictify(self, text, format):
1015		out = dict()
1016		header = True if format == 1 else False
1017		delim = ' ' if format == 1 else ':'
1018		for line in text.split('\n'):
1019			if header:
1020				header, out['@'] = False, line
1021				continue
1022			line = line.strip()
1023			if delim in line:
1024				data = line.split(delim, 1)
1025				num = re.search(r'[\d]+', data[1])
1026				if format == 2 and num:
1027					out[data[0].strip()] = num.group()
1028				else:
1029					out[data[0].strip()] = data[1]
1030		return out
1031	def cmdinfo(self, begin, debug=False):
1032		out = []
1033		if begin:
1034			self.cmd1 = dict()
1035		for cargs in self.infocmds:
1036			delta, name = cargs[0], cargs[1]
1037			cmdline, cmdpath = ' '.join(cargs[2:]), self.getExec(cargs[2])
1038			if not cmdpath or (begin and not delta):
1039				continue
1040			try:
1041				fp = Popen([cmdpath]+cargs[3:], stdout=PIPE, stderr=PIPE).stdout
1042				info = ascii(fp.read()).strip()
1043				fp.close()
1044			except:
1045				continue
1046			if not debug and begin:
1047				self.cmd1[name] = self.dictify(info, delta)
1048			elif not debug and delta and name in self.cmd1:
1049				before, after = self.cmd1[name], self.dictify(info, delta)
1050				dinfo = ('\t%s\n' % before['@']) if '@' in before else ''
1051				prefix = self.commonPrefix(list(before.keys()))
1052				for key in sorted(before):
1053					if key in after and before[key] != after[key]:
1054						title = key.replace(prefix, '')
1055						if delta == 2:
1056							dinfo += '\t%s : %s -> %s\n' % \
1057								(title, before[key].strip(), after[key].strip())
1058						else:
1059							dinfo += '%10s (start) : %s\n%10s (after) : %s\n' % \
1060								(title, before[key], title, after[key])
1061				dinfo = '\tnothing changed' if not dinfo else dinfo.rstrip()
1062				out.append((name, cmdline, dinfo))
1063			else:
1064				out.append((name, cmdline, '\tnothing' if not info else info))
1065		return out
1066	def haveTurbostat(self):
1067		if not self.tstat:
1068			return False
1069		cmd = self.getExec('turbostat')
1070		if not cmd:
1071			return False
1072		fp = Popen([cmd, '-v'], stdout=PIPE, stderr=PIPE).stderr
1073		out = ascii(fp.read()).strip()
1074		fp.close()
1075		if re.match('turbostat version .*', out):
1076			self.vprint(out)
1077			return True
1078		return False
1079	def turbostat(self):
1080		cmd = self.getExec('turbostat')
1081		rawout = keyline = valline = ''
1082		fullcmd = '%s -q -S echo freeze > %s' % (cmd, self.powerfile)
1083		fp = Popen(['sh', '-c', fullcmd], stdout=PIPE, stderr=PIPE).stderr
1084		for line in fp:
1085			line = ascii(line)
1086			rawout += line
1087			if keyline and valline:
1088				continue
1089			if re.match('(?i)Avg_MHz.*', line):
1090				keyline = line.strip().split()
1091			elif keyline:
1092				valline = line.strip().split()
1093		fp.close()
1094		if not keyline or not valline or len(keyline) != len(valline):
1095			errmsg = 'unrecognized turbostat output:\n'+rawout.strip()
1096			self.vprint(errmsg)
1097			if not self.verbose:
1098				pprint(errmsg)
1099			return ''
1100		if self.verbose:
1101			pprint(rawout.strip())
1102		out = []
1103		for key in keyline:
1104			idx = keyline.index(key)
1105			val = valline[idx]
1106			out.append('%s=%s' % (key, val))
1107		return '|'.join(out)
1108	def wifiDetails(self, dev):
1109		try:
1110			info = open('/sys/class/net/%s/device/uevent' % dev, 'r').read().strip()
1111		except:
1112			return dev
1113		vals = [dev]
1114		for prop in info.split('\n'):
1115			if prop.startswith('DRIVER=') or prop.startswith('PCI_ID='):
1116				vals.append(prop.split('=')[-1])
1117		return ':'.join(vals)
1118	def checkWifi(self, dev=''):
1119		try:
1120			w = open('/proc/net/wireless', 'r').read().strip()
1121		except:
1122			return ''
1123		for line in reversed(w.split('\n')):
1124			m = re.match(' *(?P<dev>.*): (?P<stat>[0-9a-f]*) .*', w.split('\n')[-1])
1125			if not m or (dev and dev != m.group('dev')):
1126				continue
1127			return m.group('dev')
1128		return ''
1129	def pollWifi(self, dev, timeout=60):
1130		start = time.time()
1131		while (time.time() - start) < timeout:
1132			w = self.checkWifi(dev)
1133			if w:
1134				return '%s reconnected %.2f' % \
1135					(self.wifiDetails(dev), max(0, time.time() - start))
1136			time.sleep(0.01)
1137		return '%s timeout %d' % (self.wifiDetails(dev), timeout)
1138	def errorSummary(self, errinfo, msg):
1139		found = False
1140		for entry in errinfo:
1141			if re.match(entry['match'], msg):
1142				entry['count'] += 1
1143				if self.hostname not in entry['urls']:
1144					entry['urls'][self.hostname] = [self.htmlfile]
1145				elif self.htmlfile not in entry['urls'][self.hostname]:
1146					entry['urls'][self.hostname].append(self.htmlfile)
1147				found = True
1148				break
1149		if found:
1150			return
1151		arr = msg.split()
1152		for j in range(len(arr)):
1153			if re.match('^[0-9,\-\.]*$', arr[j]):
1154				arr[j] = '[0-9,\-\.]*'
1155			else:
1156				arr[j] = arr[j]\
1157					.replace('\\', '\\\\').replace(']', '\]').replace('[', '\[')\
1158					.replace('.', '\.').replace('+', '\+').replace('*', '\*')\
1159					.replace('(', '\(').replace(')', '\)').replace('}', '\}')\
1160					.replace('{', '\{')
1161		mstr = ' *'.join(arr)
1162		entry = {
1163			'line': msg,
1164			'match': mstr,
1165			'count': 1,
1166			'urls': {self.hostname: [self.htmlfile]}
1167		}
1168		errinfo.append(entry)
1169	def multistat(self, start, idx, finish):
1170		if 'time' in self.multitest:
1171			id = '%d Duration=%dmin' % (idx+1, self.multitest['time'])
1172		else:
1173			id = '%d/%d' % (idx+1, self.multitest['count'])
1174		t = time.time()
1175		if 'start' not in self.multitest:
1176			self.multitest['start'] = self.multitest['last'] = t
1177			self.multitest['total'] = 0.0
1178			pprint('TEST (%s) START' % id)
1179			return
1180		dt = t - self.multitest['last']
1181		if not start:
1182			if idx == 0 and self.multitest['delay'] > 0:
1183				self.multitest['total'] += self.multitest['delay']
1184			pprint('TEST (%s) COMPLETE -- Duration %.1fs' % (id, dt))
1185			return
1186		self.multitest['total'] += dt
1187		self.multitest['last'] = t
1188		avg = self.multitest['total'] / idx
1189		if 'time' in self.multitest:
1190			left = finish - datetime.now()
1191			left -= timedelta(microseconds=left.microseconds)
1192		else:
1193			left = timedelta(seconds=((self.multitest['count'] - idx) * int(avg)))
1194		pprint('TEST (%s) START - Avg Duration %.1fs, Time left %s' % \
1195			(id, avg, str(left)))
1196	def multiinit(self, c, d):
1197		sz, unit = 'count', 'm'
1198		if c.endswith('d') or c.endswith('h') or c.endswith('m'):
1199			sz, unit, c = 'time', c[-1], c[:-1]
1200		self.multitest['run'] = True
1201		self.multitest[sz] = getArgInt('multi: n d (exec count)', c, 1, 1000000, False)
1202		self.multitest['delay'] = getArgInt('multi: n d (delay between tests)', d, 0, 3600, False)
1203		if unit == 'd':
1204			self.multitest[sz] *= 1440
1205		elif unit == 'h':
1206			self.multitest[sz] *= 60
1207
1208sysvals = SystemValues()
1209switchvalues = ['enable', 'disable', 'on', 'off', 'true', 'false', '1', '0']
1210switchoff = ['disable', 'off', 'false', '0']
1211suspendmodename = {
1212	'freeze': 'Freeze (S0)',
1213	'standby': 'Standby (S1)',
1214	'mem': 'Suspend (S3)',
1215	'disk': 'Hibernate (S4)'
1216}
1217
1218# Class: DevProps
1219# Description:
1220#	 Simple class which holds property values collected
1221#	 for all the devices used in the timeline.
1222class DevProps:
1223	def __init__(self):
1224		self.syspath = ''
1225		self.altname = ''
1226		self.isasync = True
1227		self.xtraclass = ''
1228		self.xtrainfo = ''
1229	def out(self, dev):
1230		return '%s,%s,%d;' % (dev, self.altname, self.isasync)
1231	def debug(self, dev):
1232		pprint('%s:\n\taltname = %s\n\t  async = %s' % (dev, self.altname, self.isasync))
1233	def altName(self, dev):
1234		if not self.altname or self.altname == dev:
1235			return dev
1236		return '%s [%s]' % (self.altname, dev)
1237	def xtraClass(self):
1238		if self.xtraclass:
1239			return ' '+self.xtraclass
1240		if not self.isasync:
1241			return ' sync'
1242		return ''
1243	def xtraInfo(self):
1244		if self.xtraclass:
1245			return ' '+self.xtraclass
1246		if self.isasync:
1247			return ' async_device'
1248		return ' sync_device'
1249
1250# Class: DeviceNode
1251# Description:
1252#	 A container used to create a device hierachy, with a single root node
1253#	 and a tree of child nodes. Used by Data.deviceTopology()
1254class DeviceNode:
1255	def __init__(self, nodename, nodedepth):
1256		self.name = nodename
1257		self.children = []
1258		self.depth = nodedepth
1259
1260# Class: Data
1261# Description:
1262#	 The primary container for suspend/resume test data. There is one for
1263#	 each test run. The data is organized into a cronological hierarchy:
1264#	 Data.dmesg {
1265#		phases {
1266#			10 sequential, non-overlapping phases of S/R
1267#			contents: times for phase start/end, order/color data for html
1268#			devlist {
1269#				device callback or action list for this phase
1270#				device {
1271#					a single device callback or generic action
1272#					contents: start/stop times, pid/cpu/driver info
1273#						parents/children, html id for timeline/callgraph
1274#						optionally includes an ftrace callgraph
1275#						optionally includes dev/ps data
1276#				}
1277#			}
1278#		}
1279#	}
1280#
1281class Data:
1282	phasedef = {
1283		'suspend_prepare': {'order': 0, 'color': '#CCFFCC'},
1284		        'suspend': {'order': 1, 'color': '#88FF88'},
1285		   'suspend_late': {'order': 2, 'color': '#00AA00'},
1286		  'suspend_noirq': {'order': 3, 'color': '#008888'},
1287		'suspend_machine': {'order': 4, 'color': '#0000FF'},
1288		 'resume_machine': {'order': 5, 'color': '#FF0000'},
1289		   'resume_noirq': {'order': 6, 'color': '#FF9900'},
1290		   'resume_early': {'order': 7, 'color': '#FFCC00'},
1291		         'resume': {'order': 8, 'color': '#FFFF88'},
1292		'resume_complete': {'order': 9, 'color': '#FFFFCC'},
1293	}
1294	errlist = {
1295		'HWERROR' : r'.*\[ *Hardware Error *\].*',
1296		'FWBUG'   : r'.*\[ *Firmware Bug *\].*',
1297		'BUG'     : r'(?i).*\bBUG\b.*',
1298		'ERROR'   : r'(?i).*\bERROR\b.*',
1299		'WARNING' : r'(?i).*\bWARNING\b.*',
1300		'FAULT'   : r'(?i).*\bFAULT\b.*',
1301		'FAIL'    : r'(?i).*\bFAILED\b.*',
1302		'INVALID' : r'(?i).*\bINVALID\b.*',
1303		'CRASH'   : r'(?i).*\bCRASHED\b.*',
1304		'IRQ'     : r'.*\bgenirq: .*',
1305		'TASKFAIL': r'.*Freezing of tasks *.*',
1306		'ACPI'    : r'.*\bACPI *(?P<b>[A-Za-z]*) *Error[: ].*',
1307		'DISKFULL': r'.*\bNo space left on device.*',
1308		'USBERR'  : r'.*usb .*device .*, error [0-9-]*',
1309		'ATAERR'  : r' *ata[0-9\.]*: .*failed.*',
1310		'MEIERR'  : r' *mei.*: .*failed.*',
1311		'TPMERR'  : r'(?i) *tpm *tpm[0-9]*: .*error.*',
1312	}
1313	def __init__(self, num):
1314		idchar = 'abcdefghij'
1315		self.start = 0.0 # test start
1316		self.end = 0.0   # test end
1317		self.hwstart = 0 # rtc test start
1318		self.hwend = 0   # rtc test end
1319		self.tSuspended = 0.0 # low-level suspend start
1320		self.tResumed = 0.0   # low-level resume start
1321		self.tKernSus = 0.0   # kernel level suspend start
1322		self.tKernRes = 0.0   # kernel level resume end
1323		self.fwValid = False  # is firmware data available
1324		self.fwSuspend = 0    # time spent in firmware suspend
1325		self.fwResume = 0     # time spent in firmware resume
1326		self.html_device_id = 0
1327		self.stamp = 0
1328		self.outfile = ''
1329		self.kerror = False
1330		self.wifi = dict()
1331		self.turbostat = 0
1332		self.enterfail = ''
1333		self.currphase = ''
1334		self.pstl = dict()    # process timeline
1335		self.testnumber = num
1336		self.idstr = idchar[num]
1337		self.dmesgtext = []   # dmesg text file in memory
1338		self.dmesg = dict()   # root data structure
1339		self.errorinfo = {'suspend':[],'resume':[]}
1340		self.tLow = []        # time spent in low-level suspends (standby/freeze)
1341		self.devpids = []
1342		self.devicegroups = 0
1343	def sortedPhases(self):
1344		return sorted(self.dmesg, key=lambda k:self.dmesg[k]['order'])
1345	def initDevicegroups(self):
1346		# called when phases are all finished being added
1347		for phase in sorted(self.dmesg.keys()):
1348			if '*' in phase:
1349				p = phase.split('*')
1350				pnew = '%s%d' % (p[0], len(p))
1351				self.dmesg[pnew] = self.dmesg.pop(phase)
1352		self.devicegroups = []
1353		for phase in self.sortedPhases():
1354			self.devicegroups.append([phase])
1355	def nextPhase(self, phase, offset):
1356		order = self.dmesg[phase]['order'] + offset
1357		for p in self.dmesg:
1358			if self.dmesg[p]['order'] == order:
1359				return p
1360		return ''
1361	def lastPhase(self):
1362		plist = self.sortedPhases()
1363		if len(plist) < 1:
1364			return ''
1365		return plist[-1]
1366	def turbostatInfo(self):
1367		tp = TestProps()
1368		out = {'syslpi':'N/A','pkgpc10':'N/A'}
1369		for line in self.dmesgtext:
1370			m = re.match(tp.tstatfmt, line)
1371			if not m:
1372				continue
1373			for i in m.group('t').split('|'):
1374				if 'SYS%LPI' in i:
1375					out['syslpi'] = i.split('=')[-1]+'%'
1376				elif 'pc10' in i:
1377					out['pkgpc10'] = i.split('=')[-1]+'%'
1378			break
1379		return out
1380	def extractErrorInfo(self):
1381		lf = self.dmesgtext
1382		if len(self.dmesgtext) < 1 and sysvals.dmesgfile:
1383			lf = sysvals.openlog(sysvals.dmesgfile, 'r')
1384		i = 0
1385		list = []
1386		for line in lf:
1387			i += 1
1388			m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
1389			if not m:
1390				continue
1391			t = float(m.group('ktime'))
1392			if t < self.start or t > self.end:
1393				continue
1394			dir = 'suspend' if t < self.tSuspended else 'resume'
1395			msg = m.group('msg')
1396			if re.match('capability: warning: .*', msg):
1397				continue
1398			for err in self.errlist:
1399				if re.match(self.errlist[err], msg):
1400					list.append((msg, err, dir, t, i, i))
1401					self.kerror = True
1402					break
1403		msglist = []
1404		for msg, type, dir, t, idx1, idx2 in list:
1405			msglist.append(msg)
1406			self.errorinfo[dir].append((type, t, idx1, idx2))
1407		if self.kerror:
1408			sysvals.dmesglog = True
1409		if len(self.dmesgtext) < 1 and sysvals.dmesgfile:
1410			lf.close()
1411		return msglist
1412	def setStart(self, time, msg=''):
1413		self.start = time
1414		if msg:
1415			try:
1416				self.hwstart = datetime.strptime(msg, sysvals.tmstart)
1417			except:
1418				self.hwstart = 0
1419	def setEnd(self, time, msg=''):
1420		self.end = time
1421		if msg:
1422			try:
1423				self.hwend = datetime.strptime(msg, sysvals.tmend)
1424			except:
1425				self.hwend = 0
1426	def isTraceEventOutsideDeviceCalls(self, pid, time):
1427		for phase in self.sortedPhases():
1428			list = self.dmesg[phase]['list']
1429			for dev in list:
1430				d = list[dev]
1431				if(d['pid'] == pid and time >= d['start'] and
1432					time < d['end']):
1433					return False
1434		return True
1435	def sourcePhase(self, start):
1436		for phase in self.sortedPhases():
1437			if 'machine' in phase:
1438				continue
1439			pend = self.dmesg[phase]['end']
1440			if start <= pend:
1441				return phase
1442		return 'resume_complete'
1443	def sourceDevice(self, phaselist, start, end, pid, type):
1444		tgtdev = ''
1445		for phase in phaselist:
1446			list = self.dmesg[phase]['list']
1447			for devname in list:
1448				dev = list[devname]
1449				# pid must match
1450				if dev['pid'] != pid:
1451					continue
1452				devS = dev['start']
1453				devE = dev['end']
1454				if type == 'device':
1455					# device target event is entirely inside the source boundary
1456					if(start < devS or start >= devE or end <= devS or end > devE):
1457						continue
1458				elif type == 'thread':
1459					# thread target event will expand the source boundary
1460					if start < devS:
1461						dev['start'] = start
1462					if end > devE:
1463						dev['end'] = end
1464				tgtdev = dev
1465				break
1466		return tgtdev
1467	def addDeviceFunctionCall(self, displayname, kprobename, proc, pid, start, end, cdata, rdata):
1468		# try to place the call in a device
1469		phases = self.sortedPhases()
1470		tgtdev = self.sourceDevice(phases, start, end, pid, 'device')
1471		# calls with device pids that occur outside device bounds are dropped
1472		# TODO: include these somehow
1473		if not tgtdev and pid in self.devpids:
1474			return False
1475		# try to place the call in a thread
1476		if not tgtdev:
1477			tgtdev = self.sourceDevice(phases, start, end, pid, 'thread')
1478		# create new thread blocks, expand as new calls are found
1479		if not tgtdev:
1480			if proc == '<...>':
1481				threadname = 'kthread-%d' % (pid)
1482			else:
1483				threadname = '%s-%d' % (proc, pid)
1484			tgtphase = self.sourcePhase(start)
1485			self.newAction(tgtphase, threadname, pid, '', start, end, '', ' kth', '')
1486			return self.addDeviceFunctionCall(displayname, kprobename, proc, pid, start, end, cdata, rdata)
1487		# this should not happen
1488		if not tgtdev:
1489			sysvals.vprint('[%f - %f] %s-%d %s %s %s' % \
1490				(start, end, proc, pid, kprobename, cdata, rdata))
1491			return False
1492		# place the call data inside the src element of the tgtdev
1493		if('src' not in tgtdev):
1494			tgtdev['src'] = []
1495		dtf = sysvals.dev_tracefuncs
1496		ubiquitous = False
1497		if kprobename in dtf and 'ub' in dtf[kprobename]:
1498			ubiquitous = True
1499		title = cdata+' '+rdata
1500		mstr = '\(.*\) *(?P<args>.*) *\((?P<caller>.*)\+.* arg1=(?P<ret>.*)'
1501		m = re.match(mstr, title)
1502		if m:
1503			c = m.group('caller')
1504			a = m.group('args').strip()
1505			r = m.group('ret')
1506			if len(r) > 6:
1507				r = ''
1508			else:
1509				r = 'ret=%s ' % r
1510			if ubiquitous and c in dtf and 'ub' in dtf[c]:
1511				return False
1512		color = sysvals.kprobeColor(kprobename)
1513		e = DevFunction(displayname, a, c, r, start, end, ubiquitous, proc, pid, color)
1514		tgtdev['src'].append(e)
1515		return True
1516	def overflowDevices(self):
1517		# get a list of devices that extend beyond the end of this test run
1518		devlist = []
1519		for phase in self.sortedPhases():
1520			list = self.dmesg[phase]['list']
1521			for devname in list:
1522				dev = list[devname]
1523				if dev['end'] > self.end:
1524					devlist.append(dev)
1525		return devlist
1526	def mergeOverlapDevices(self, devlist):
1527		# merge any devices that overlap devlist
1528		for dev in devlist:
1529			devname = dev['name']
1530			for phase in self.sortedPhases():
1531				list = self.dmesg[phase]['list']
1532				if devname not in list:
1533					continue
1534				tdev = list[devname]
1535				o = min(dev['end'], tdev['end']) - max(dev['start'], tdev['start'])
1536				if o <= 0:
1537					continue
1538				dev['end'] = tdev['end']
1539				if 'src' not in dev or 'src' not in tdev:
1540					continue
1541				dev['src'] += tdev['src']
1542				del list[devname]
1543	def usurpTouchingThread(self, name, dev):
1544		# the caller test has priority of this thread, give it to him
1545		for phase in self.sortedPhases():
1546			list = self.dmesg[phase]['list']
1547			if name in list:
1548				tdev = list[name]
1549				if tdev['start'] - dev['end'] < 0.1:
1550					dev['end'] = tdev['end']
1551					if 'src' not in dev:
1552						dev['src'] = []
1553					if 'src' in tdev:
1554						dev['src'] += tdev['src']
1555					del list[name]
1556				break
1557	def stitchTouchingThreads(self, testlist):
1558		# merge any threads between tests that touch
1559		for phase in self.sortedPhases():
1560			list = self.dmesg[phase]['list']
1561			for devname in list:
1562				dev = list[devname]
1563				if 'htmlclass' not in dev or 'kth' not in dev['htmlclass']:
1564					continue
1565				for data in testlist:
1566					data.usurpTouchingThread(devname, dev)
1567	def optimizeDevSrc(self):
1568		# merge any src call loops to reduce timeline size
1569		for phase in self.sortedPhases():
1570			list = self.dmesg[phase]['list']
1571			for dev in list:
1572				if 'src' not in list[dev]:
1573					continue
1574				src = list[dev]['src']
1575				p = 0
1576				for e in sorted(src, key=lambda event: event.time):
1577					if not p or not e.repeat(p):
1578						p = e
1579						continue
1580					# e is another iteration of p, move it into p
1581					p.end = e.end
1582					p.length = p.end - p.time
1583					p.count += 1
1584					src.remove(e)
1585	def trimTimeVal(self, t, t0, dT, left):
1586		if left:
1587			if(t > t0):
1588				if(t - dT < t0):
1589					return t0
1590				return t - dT
1591			else:
1592				return t
1593		else:
1594			if(t < t0 + dT):
1595				if(t > t0):
1596					return t0 + dT
1597				return t + dT
1598			else:
1599				return t
1600	def trimTime(self, t0, dT, left):
1601		self.tSuspended = self.trimTimeVal(self.tSuspended, t0, dT, left)
1602		self.tResumed = self.trimTimeVal(self.tResumed, t0, dT, left)
1603		self.start = self.trimTimeVal(self.start, t0, dT, left)
1604		self.tKernSus = self.trimTimeVal(self.tKernSus, t0, dT, left)
1605		self.tKernRes = self.trimTimeVal(self.tKernRes, t0, dT, left)
1606		self.end = self.trimTimeVal(self.end, t0, dT, left)
1607		for phase in self.sortedPhases():
1608			p = self.dmesg[phase]
1609			p['start'] = self.trimTimeVal(p['start'], t0, dT, left)
1610			p['end'] = self.trimTimeVal(p['end'], t0, dT, left)
1611			list = p['list']
1612			for name in list:
1613				d = list[name]
1614				d['start'] = self.trimTimeVal(d['start'], t0, dT, left)
1615				d['end'] = self.trimTimeVal(d['end'], t0, dT, left)
1616				d['length'] = d['end'] - d['start']
1617				if('ftrace' in d):
1618					cg = d['ftrace']
1619					cg.start = self.trimTimeVal(cg.start, t0, dT, left)
1620					cg.end = self.trimTimeVal(cg.end, t0, dT, left)
1621					for line in cg.list:
1622						line.time = self.trimTimeVal(line.time, t0, dT, left)
1623				if('src' in d):
1624					for e in d['src']:
1625						e.time = self.trimTimeVal(e.time, t0, dT, left)
1626		for dir in ['suspend', 'resume']:
1627			list = []
1628			for e in self.errorinfo[dir]:
1629				type, tm, idx1, idx2 = e
1630				tm = self.trimTimeVal(tm, t0, dT, left)
1631				list.append((type, tm, idx1, idx2))
1632			self.errorinfo[dir] = list
1633	def trimFreezeTime(self, tZero):
1634		# trim out any standby or freeze clock time
1635		lp = ''
1636		for phase in self.sortedPhases():
1637			if 'resume_machine' in phase and 'suspend_machine' in lp:
1638				tS, tR = self.dmesg[lp]['end'], self.dmesg[phase]['start']
1639				tL = tR - tS
1640				if tL > 0:
1641					left = True if tR > tZero else False
1642					self.trimTime(tS, tL, left)
1643					self.tLow.append('%.0f'%(tL*1000))
1644			lp = phase
1645	def getMemTime(self):
1646		if not self.hwstart or not self.hwend:
1647			return
1648		stime = (self.tSuspended - self.start) * 1000000
1649		rtime = (self.end - self.tResumed) * 1000000
1650		hws = self.hwstart + timedelta(microseconds=stime)
1651		hwr = self.hwend - timedelta(microseconds=rtime)
1652		self.tLow.append('%.0f'%((hwr - hws).total_seconds() * 1000))
1653	def getTimeValues(self):
1654		sktime = (self.tSuspended - self.tKernSus) * 1000
1655		rktime = (self.tKernRes - self.tResumed) * 1000
1656		return (sktime, rktime)
1657	def setPhase(self, phase, ktime, isbegin, order=-1):
1658		if(isbegin):
1659			# phase start over current phase
1660			if self.currphase:
1661				if 'resume_machine' not in self.currphase:
1662					sysvals.vprint('WARNING: phase %s failed to end' % self.currphase)
1663				self.dmesg[self.currphase]['end'] = ktime
1664			phases = self.dmesg.keys()
1665			color = self.phasedef[phase]['color']
1666			count = len(phases) if order < 0 else order
1667			# create unique name for every new phase
1668			while phase in phases:
1669				phase += '*'
1670			self.dmesg[phase] = {'list': dict(), 'start': -1.0, 'end': -1.0,
1671				'row': 0, 'color': color, 'order': count}
1672			self.dmesg[phase]['start'] = ktime
1673			self.currphase = phase
1674		else:
1675			# phase end without a start
1676			if phase not in self.currphase:
1677				if self.currphase:
1678					sysvals.vprint('WARNING: %s ended instead of %s, ftrace corruption?' % (phase, self.currphase))
1679				else:
1680					sysvals.vprint('WARNING: %s ended without a start, ftrace corruption?' % phase)
1681					return phase
1682			phase = self.currphase
1683			self.dmesg[phase]['end'] = ktime
1684			self.currphase = ''
1685		return phase
1686	def sortedDevices(self, phase):
1687		list = self.dmesg[phase]['list']
1688		return sorted(list, key=lambda k:list[k]['start'])
1689	def fixupInitcalls(self, phase):
1690		# if any calls never returned, clip them at system resume end
1691		phaselist = self.dmesg[phase]['list']
1692		for devname in phaselist:
1693			dev = phaselist[devname]
1694			if(dev['end'] < 0):
1695				for p in self.sortedPhases():
1696					if self.dmesg[p]['end'] > dev['start']:
1697						dev['end'] = self.dmesg[p]['end']
1698						break
1699				sysvals.vprint('%s (%s): callback didnt return' % (devname, phase))
1700	def deviceFilter(self, devicefilter):
1701		for phase in self.sortedPhases():
1702			list = self.dmesg[phase]['list']
1703			rmlist = []
1704			for name in list:
1705				keep = False
1706				for filter in devicefilter:
1707					if filter in name or \
1708						('drv' in list[name] and filter in list[name]['drv']):
1709						keep = True
1710				if not keep:
1711					rmlist.append(name)
1712			for name in rmlist:
1713				del list[name]
1714	def fixupInitcallsThatDidntReturn(self):
1715		# if any calls never returned, clip them at system resume end
1716		for phase in self.sortedPhases():
1717			self.fixupInitcalls(phase)
1718	def phaseOverlap(self, phases):
1719		rmgroups = []
1720		newgroup = []
1721		for group in self.devicegroups:
1722			for phase in phases:
1723				if phase not in group:
1724					continue
1725				for p in group:
1726					if p not in newgroup:
1727						newgroup.append(p)
1728				if group not in rmgroups:
1729					rmgroups.append(group)
1730		for group in rmgroups:
1731			self.devicegroups.remove(group)
1732		self.devicegroups.append(newgroup)
1733	def newActionGlobal(self, name, start, end, pid=-1, color=''):
1734		# which phase is this device callback or action in
1735		phases = self.sortedPhases()
1736		targetphase = 'none'
1737		htmlclass = ''
1738		overlap = 0.0
1739		myphases = []
1740		for phase in phases:
1741			pstart = self.dmesg[phase]['start']
1742			pend = self.dmesg[phase]['end']
1743			# see if the action overlaps this phase
1744			o = max(0, min(end, pend) - max(start, pstart))
1745			if o > 0:
1746				myphases.append(phase)
1747			# set the target phase to the one that overlaps most
1748			if o > overlap:
1749				if overlap > 0 and phase == 'post_resume':
1750					continue
1751				targetphase = phase
1752				overlap = o
1753		# if no target phase was found, pin it to the edge
1754		if targetphase == 'none':
1755			p0start = self.dmesg[phases[0]]['start']
1756			if start <= p0start:
1757				targetphase = phases[0]
1758			else:
1759				targetphase = phases[-1]
1760		if pid == -2:
1761			htmlclass = ' bg'
1762		elif pid == -3:
1763			htmlclass = ' ps'
1764		if len(myphases) > 1:
1765			htmlclass = ' bg'
1766			self.phaseOverlap(myphases)
1767		if targetphase in phases:
1768			newname = self.newAction(targetphase, name, pid, '', start, end, '', htmlclass, color)
1769			return (targetphase, newname)
1770		return False
1771	def newAction(self, phase, name, pid, parent, start, end, drv, htmlclass='', color=''):
1772		# new device callback for a specific phase
1773		self.html_device_id += 1
1774		devid = '%s%d' % (self.idstr, self.html_device_id)
1775		list = self.dmesg[phase]['list']
1776		length = -1.0
1777		if(start >= 0 and end >= 0):
1778			length = end - start
1779		if pid == -2:
1780			i = 2
1781			origname = name
1782			while(name in list):
1783				name = '%s[%d]' % (origname, i)
1784				i += 1
1785		list[name] = {'name': name, 'start': start, 'end': end, 'pid': pid,
1786			'par': parent, 'length': length, 'row': 0, 'id': devid, 'drv': drv }
1787		if htmlclass:
1788			list[name]['htmlclass'] = htmlclass
1789		if color:
1790			list[name]['color'] = color
1791		return name
1792	def deviceChildren(self, devname, phase):
1793		devlist = []
1794		list = self.dmesg[phase]['list']
1795		for child in list:
1796			if(list[child]['par'] == devname):
1797				devlist.append(child)
1798		return devlist
1799	def maxDeviceNameSize(self, phase):
1800		size = 0
1801		for name in self.dmesg[phase]['list']:
1802			if len(name) > size:
1803				size = len(name)
1804		return size
1805	def printDetails(self):
1806		sysvals.vprint('Timeline Details:')
1807		sysvals.vprint('          test start: %f' % self.start)
1808		sysvals.vprint('kernel suspend start: %f' % self.tKernSus)
1809		tS = tR = False
1810		for phase in self.sortedPhases():
1811			devlist = self.dmesg[phase]['list']
1812			dc, ps, pe = len(devlist), self.dmesg[phase]['start'], self.dmesg[phase]['end']
1813			if not tS and ps >= self.tSuspended:
1814				sysvals.vprint('   machine suspended: %f' % self.tSuspended)
1815				tS = True
1816			if not tR and ps >= self.tResumed:
1817				sysvals.vprint('     machine resumed: %f' % self.tResumed)
1818				tR = True
1819			sysvals.vprint('%20s: %f - %f (%d devices)' % (phase, ps, pe, dc))
1820			if sysvals.devdump:
1821				sysvals.vprint(''.join('-' for i in range(80)))
1822				maxname = '%d' % self.maxDeviceNameSize(phase)
1823				fmt = '%3d) %'+maxname+'s - %f - %f'
1824				c = 1
1825				for name in sorted(devlist):
1826					s = devlist[name]['start']
1827					e = devlist[name]['end']
1828					sysvals.vprint(fmt % (c, name, s, e))
1829					c += 1
1830				sysvals.vprint(''.join('-' for i in range(80)))
1831		sysvals.vprint('   kernel resume end: %f' % self.tKernRes)
1832		sysvals.vprint('            test end: %f' % self.end)
1833	def deviceChildrenAllPhases(self, devname):
1834		devlist = []
1835		for phase in self.sortedPhases():
1836			list = self.deviceChildren(devname, phase)
1837			for dev in sorted(list):
1838				if dev not in devlist:
1839					devlist.append(dev)
1840		return devlist
1841	def masterTopology(self, name, list, depth):
1842		node = DeviceNode(name, depth)
1843		for cname in list:
1844			# avoid recursions
1845			if name == cname:
1846				continue
1847			clist = self.deviceChildrenAllPhases(cname)
1848			cnode = self.masterTopology(cname, clist, depth+1)
1849			node.children.append(cnode)
1850		return node
1851	def printTopology(self, node):
1852		html = ''
1853		if node.name:
1854			info = ''
1855			drv = ''
1856			for phase in self.sortedPhases():
1857				list = self.dmesg[phase]['list']
1858				if node.name in list:
1859					s = list[node.name]['start']
1860					e = list[node.name]['end']
1861					if list[node.name]['drv']:
1862						drv = ' {'+list[node.name]['drv']+'}'
1863					info += ('<li>%s: %.3fms</li>' % (phase, (e-s)*1000))
1864			html += '<li><b>'+node.name+drv+'</b>'
1865			if info:
1866				html += '<ul>'+info+'</ul>'
1867			html += '</li>'
1868		if len(node.children) > 0:
1869			html += '<ul>'
1870			for cnode in node.children:
1871				html += self.printTopology(cnode)
1872			html += '</ul>'
1873		return html
1874	def rootDeviceList(self):
1875		# list of devices graphed
1876		real = []
1877		for phase in self.sortedPhases():
1878			list = self.dmesg[phase]['list']
1879			for dev in sorted(list):
1880				if list[dev]['pid'] >= 0 and dev not in real:
1881					real.append(dev)
1882		# list of top-most root devices
1883		rootlist = []
1884		for phase in self.sortedPhases():
1885			list = self.dmesg[phase]['list']
1886			for dev in sorted(list):
1887				pdev = list[dev]['par']
1888				pid = list[dev]['pid']
1889				if(pid < 0 or re.match('[0-9]*-[0-9]*\.[0-9]*[\.0-9]*\:[\.0-9]*$', pdev)):
1890					continue
1891				if pdev and pdev not in real and pdev not in rootlist:
1892					rootlist.append(pdev)
1893		return rootlist
1894	def deviceTopology(self):
1895		rootlist = self.rootDeviceList()
1896		master = self.masterTopology('', rootlist, 0)
1897		return self.printTopology(master)
1898	def selectTimelineDevices(self, widfmt, tTotal, mindevlen):
1899		# only select devices that will actually show up in html
1900		self.tdevlist = dict()
1901		for phase in self.dmesg:
1902			devlist = []
1903			list = self.dmesg[phase]['list']
1904			for dev in list:
1905				length = (list[dev]['end'] - list[dev]['start']) * 1000
1906				width = widfmt % (((list[dev]['end']-list[dev]['start'])*100)/tTotal)
1907				if width != '0.000000' and length >= mindevlen:
1908					devlist.append(dev)
1909			self.tdevlist[phase] = devlist
1910	def addHorizontalDivider(self, devname, devend):
1911		phase = 'suspend_prepare'
1912		self.newAction(phase, devname, -2, '', \
1913			self.start, devend, '', ' sec', '')
1914		if phase not in self.tdevlist:
1915			self.tdevlist[phase] = []
1916		self.tdevlist[phase].append(devname)
1917		d = DevItem(0, phase, self.dmesg[phase]['list'][devname])
1918		return d
1919	def addProcessUsageEvent(self, name, times):
1920		# get the start and end times for this process
1921		maxC = 0
1922		tlast = 0
1923		start = -1
1924		end = -1
1925		for t in sorted(times):
1926			if tlast == 0:
1927				tlast = t
1928				continue
1929			if name in self.pstl[t]:
1930				if start == -1 or tlast < start:
1931					start = tlast
1932				if end == -1 or t > end:
1933					end = t
1934			tlast = t
1935		if start == -1 or end == -1:
1936			return 0
1937		# add a new action for this process and get the object
1938		out = self.newActionGlobal(name, start, end, -3)
1939		if not out:
1940			return 0
1941		phase, devname = out
1942		dev = self.dmesg[phase]['list'][devname]
1943		# get the cpu exec data
1944		tlast = 0
1945		clast = 0
1946		cpuexec = dict()
1947		for t in sorted(times):
1948			if tlast == 0 or t <= start or t > end:
1949				tlast = t
1950				continue
1951			list = self.pstl[t]
1952			c = 0
1953			if name in list:
1954				c = list[name]
1955			if c > maxC:
1956				maxC = c
1957			if c != clast:
1958				key = (tlast, t)
1959				cpuexec[key] = c
1960				tlast = t
1961				clast = c
1962		dev['cpuexec'] = cpuexec
1963		return maxC
1964	def createProcessUsageEvents(self):
1965		# get an array of process names
1966		proclist = []
1967		for t in sorted(self.pstl):
1968			pslist = self.pstl[t]
1969			for ps in sorted(pslist):
1970				if ps not in proclist:
1971					proclist.append(ps)
1972		# get a list of data points for suspend and resume
1973		tsus = []
1974		tres = []
1975		for t in sorted(self.pstl):
1976			if t < self.tSuspended:
1977				tsus.append(t)
1978			else:
1979				tres.append(t)
1980		# process the events for suspend and resume
1981		if len(proclist) > 0:
1982			sysvals.vprint('Process Execution:')
1983		for ps in proclist:
1984			c = self.addProcessUsageEvent(ps, tsus)
1985			if c > 0:
1986				sysvals.vprint('%25s (sus): %d' % (ps, c))
1987			c = self.addProcessUsageEvent(ps, tres)
1988			if c > 0:
1989				sysvals.vprint('%25s (res): %d' % (ps, c))
1990	def handleEndMarker(self, time, msg=''):
1991		dm = self.dmesg
1992		self.setEnd(time, msg)
1993		self.initDevicegroups()
1994		# give suspend_prepare an end if needed
1995		if 'suspend_prepare' in dm and dm['suspend_prepare']['end'] < 0:
1996			dm['suspend_prepare']['end'] = time
1997		# assume resume machine ends at next phase start
1998		if 'resume_machine' in dm and dm['resume_machine']['end'] < 0:
1999			np = self.nextPhase('resume_machine', 1)
2000			if np:
2001				dm['resume_machine']['end'] = dm[np]['start']
2002		# if kernel resume end not found, assume its the end marker
2003		if self.tKernRes == 0.0:
2004			self.tKernRes = time
2005		# if kernel suspend start not found, assume its the end marker
2006		if self.tKernSus == 0.0:
2007			self.tKernSus = time
2008		# set resume complete to end at end marker
2009		if 'resume_complete' in dm:
2010			dm['resume_complete']['end'] = time
2011	def debugPrint(self):
2012		for p in self.sortedPhases():
2013			list = self.dmesg[p]['list']
2014			for devname in sorted(list):
2015				dev = list[devname]
2016				if 'ftrace' in dev:
2017					dev['ftrace'].debugPrint(' [%s]' % devname)
2018
2019# Class: DevFunction
2020# Description:
2021#	 A container for kprobe function data we want in the dev timeline
2022class DevFunction:
2023	def __init__(self, name, args, caller, ret, start, end, u, proc, pid, color):
2024		self.row = 0
2025		self.count = 1
2026		self.name = name
2027		self.args = args
2028		self.caller = caller
2029		self.ret = ret
2030		self.time = start
2031		self.length = end - start
2032		self.end = end
2033		self.ubiquitous = u
2034		self.proc = proc
2035		self.pid = pid
2036		self.color = color
2037	def title(self):
2038		cnt = ''
2039		if self.count > 1:
2040			cnt = '(x%d)' % self.count
2041		l = '%0.3fms' % (self.length * 1000)
2042		if self.ubiquitous:
2043			title = '%s(%s)%s <- %s, %s(%s)' % \
2044				(self.name, self.args, cnt, self.caller, self.ret, l)
2045		else:
2046			title = '%s(%s) %s%s(%s)' % (self.name, self.args, self.ret, cnt, l)
2047		return title.replace('"', '')
2048	def text(self):
2049		if self.count > 1:
2050			text = '%s(x%d)' % (self.name, self.count)
2051		else:
2052			text = self.name
2053		return text
2054	def repeat(self, tgt):
2055		# is the tgt call just a repeat of this call (e.g. are we in a loop)
2056		dt = self.time - tgt.end
2057		# only combine calls if -all- attributes are identical
2058		if tgt.caller == self.caller and \
2059			tgt.name == self.name and tgt.args == self.args and \
2060			tgt.proc == self.proc and tgt.pid == self.pid and \
2061			tgt.ret == self.ret and dt >= 0 and \
2062			dt <= sysvals.callloopmaxgap and \
2063			self.length < sysvals.callloopmaxlen:
2064			return True
2065		return False
2066
2067# Class: FTraceLine
2068# Description:
2069#	 A container for a single line of ftrace data. There are six basic types:
2070#		 callgraph line:
2071#			  call: "  dpm_run_callback() {"
2072#			return: "  }"
2073#			  leaf: " dpm_run_callback();"
2074#		 trace event:
2075#			 tracing_mark_write: SUSPEND START or RESUME COMPLETE
2076#			 suspend_resume: phase or custom exec block data
2077#			 device_pm_callback: device callback info
2078class FTraceLine:
2079	def __init__(self, t, m='', d=''):
2080		self.length = 0.0
2081		self.fcall = False
2082		self.freturn = False
2083		self.fevent = False
2084		self.fkprobe = False
2085		self.depth = 0
2086		self.name = ''
2087		self.type = ''
2088		self.time = float(t)
2089		if not m and not d:
2090			return
2091		# is this a trace event
2092		if(d == 'traceevent' or re.match('^ *\/\* *(?P<msg>.*) \*\/ *$', m)):
2093			if(d == 'traceevent'):
2094				# nop format trace event
2095				msg = m
2096			else:
2097				# function_graph format trace event
2098				em = re.match('^ *\/\* *(?P<msg>.*) \*\/ *$', m)
2099				msg = em.group('msg')
2100
2101			emm = re.match('^(?P<call>.*?): (?P<msg>.*)', msg)
2102			if(emm):
2103				self.name = emm.group('msg')
2104				self.type = emm.group('call')
2105			else:
2106				self.name = msg
2107			km = re.match('^(?P<n>.*)_cal$', self.type)
2108			if km:
2109				self.fcall = True
2110				self.fkprobe = True
2111				self.type = km.group('n')
2112				return
2113			km = re.match('^(?P<n>.*)_ret$', self.type)
2114			if km:
2115				self.freturn = True
2116				self.fkprobe = True
2117				self.type = km.group('n')
2118				return
2119			self.fevent = True
2120			return
2121		# convert the duration to seconds
2122		if(d):
2123			self.length = float(d)/1000000
2124		# the indentation determines the depth
2125		match = re.match('^(?P<d> *)(?P<o>.*)$', m)
2126		if(not match):
2127			return
2128		self.depth = self.getDepth(match.group('d'))
2129		m = match.group('o')
2130		# function return
2131		if(m[0] == '}'):
2132			self.freturn = True
2133			if(len(m) > 1):
2134				# includes comment with function name
2135				match = re.match('^} *\/\* *(?P<n>.*) *\*\/$', m)
2136				if(match):
2137					self.name = match.group('n').strip()
2138		# function call
2139		else:
2140			self.fcall = True
2141			# function call with children
2142			if(m[-1] == '{'):
2143				match = re.match('^(?P<n>.*) *\(.*', m)
2144				if(match):
2145					self.name = match.group('n').strip()
2146			# function call with no children (leaf)
2147			elif(m[-1] == ';'):
2148				self.freturn = True
2149				match = re.match('^(?P<n>.*) *\(.*', m)
2150				if(match):
2151					self.name = match.group('n').strip()
2152			# something else (possibly a trace marker)
2153			else:
2154				self.name = m
2155	def isCall(self):
2156		return self.fcall and not self.freturn
2157	def isReturn(self):
2158		return self.freturn and not self.fcall
2159	def isLeaf(self):
2160		return self.fcall and self.freturn
2161	def getDepth(self, str):
2162		return len(str)/2
2163	def debugPrint(self, info=''):
2164		if self.isLeaf():
2165			pprint(' -- %12.6f (depth=%02d): %s(); (%.3f us) %s' % (self.time, \
2166				self.depth, self.name, self.length*1000000, info))
2167		elif self.freturn:
2168			pprint(' -- %12.6f (depth=%02d): %s} (%.3f us) %s' % (self.time, \
2169				self.depth, self.name, self.length*1000000, info))
2170		else:
2171			pprint(' -- %12.6f (depth=%02d): %s() { (%.3f us) %s' % (self.time, \
2172				self.depth, self.name, self.length*1000000, info))
2173	def startMarker(self):
2174		# Is this the starting line of a suspend?
2175		if not self.fevent:
2176			return False
2177		if sysvals.usetracemarkers:
2178			if(self.name.startswith('SUSPEND START')):
2179				return True
2180			return False
2181		else:
2182			if(self.type == 'suspend_resume' and
2183				re.match('suspend_enter\[.*\] begin', self.name)):
2184				return True
2185			return False
2186	def endMarker(self):
2187		# Is this the ending line of a resume?
2188		if not self.fevent:
2189			return False
2190		if sysvals.usetracemarkers:
2191			if(self.name.startswith('RESUME COMPLETE')):
2192				return True
2193			return False
2194		else:
2195			if(self.type == 'suspend_resume' and
2196				re.match('thaw_processes\[.*\] end', self.name)):
2197				return True
2198			return False
2199
2200# Class: FTraceCallGraph
2201# Description:
2202#	 A container for the ftrace callgraph of a single recursive function.
2203#	 This can be a dpm_run_callback, dpm_prepare, or dpm_complete callgraph
2204#	 Each instance is tied to a single device in a single phase, and is
2205#	 comprised of an ordered list of FTraceLine objects
2206class FTraceCallGraph:
2207	vfname = 'missing_function_name'
2208	def __init__(self, pid, sv):
2209		self.id = ''
2210		self.invalid = False
2211		self.name = ''
2212		self.partial = False
2213		self.ignore = False
2214		self.start = -1.0
2215		self.end = -1.0
2216		self.list = []
2217		self.depth = 0
2218		self.pid = pid
2219		self.sv = sv
2220	def addLine(self, line):
2221		# if this is already invalid, just leave
2222		if(self.invalid):
2223			if(line.depth == 0 and line.freturn):
2224				return 1
2225			return 0
2226		# invalidate on bad depth
2227		if(self.depth < 0):
2228			self.invalidate(line)
2229			return 0
2230		# ignore data til we return to the current depth
2231		if self.ignore:
2232			if line.depth > self.depth:
2233				return 0
2234			else:
2235				self.list[-1].freturn = True
2236				self.list[-1].length = line.time - self.list[-1].time
2237				self.ignore = False
2238				# if this is a return at self.depth, no more work is needed
2239				if line.depth == self.depth and line.isReturn():
2240					if line.depth == 0:
2241						self.end = line.time
2242						return 1
2243					return 0
2244		# compare current depth with this lines pre-call depth
2245		prelinedep = line.depth
2246		if line.isReturn():
2247			prelinedep += 1
2248		last = 0
2249		lasttime = line.time
2250		if len(self.list) > 0:
2251			last = self.list[-1]
2252			lasttime = last.time
2253			if last.isLeaf():
2254				lasttime += last.length
2255		# handle low misalignments by inserting returns
2256		mismatch = prelinedep - self.depth
2257		warning = self.sv.verbose and abs(mismatch) > 1
2258		info = []
2259		if mismatch < 0:
2260			idx = 0
2261			# add return calls to get the depth down
2262			while prelinedep < self.depth:
2263				self.depth -= 1
2264				if idx == 0 and last and last.isCall():
2265					# special case, turn last call into a leaf
2266					last.depth = self.depth
2267					last.freturn = True
2268					last.length = line.time - last.time
2269					if warning:
2270						info.append(('[make leaf]', last))
2271				else:
2272					vline = FTraceLine(lasttime)
2273					vline.depth = self.depth
2274					vline.name = self.vfname
2275					vline.freturn = True
2276					self.list.append(vline)
2277					if warning:
2278						if idx == 0:
2279							info.append(('', last))
2280						info.append(('[add return]', vline))
2281				idx += 1
2282			if warning:
2283				info.append(('', line))
2284		# handle high misalignments by inserting calls
2285		elif mismatch > 0:
2286			idx = 0
2287			if warning:
2288				info.append(('', last))
2289			# add calls to get the depth up
2290			while prelinedep > self.depth:
2291				if idx == 0 and line.isReturn():
2292					# special case, turn this return into a leaf
2293					line.fcall = True
2294					prelinedep -= 1
2295					if warning:
2296						info.append(('[make leaf]', line))
2297				else:
2298					vline = FTraceLine(lasttime)
2299					vline.depth = self.depth
2300					vline.name = self.vfname
2301					vline.fcall = True
2302					self.list.append(vline)
2303					self.depth += 1
2304					if not last:
2305						self.start = vline.time
2306					if warning:
2307						info.append(('[add call]', vline))
2308				idx += 1
2309			if warning and ('[make leaf]', line) not in info:
2310				info.append(('', line))
2311		if warning:
2312			pprint('WARNING: ftrace data missing, corrections made:')
2313			for i in info:
2314				t, obj = i
2315				if obj:
2316					obj.debugPrint(t)
2317		# process the call and set the new depth
2318		skipadd = False
2319		md = self.sv.max_graph_depth
2320		if line.isCall():
2321			# ignore blacklisted/overdepth funcs
2322			if (md and self.depth >= md - 1) or (line.name in self.sv.cgblacklist):
2323				self.ignore = True
2324			else:
2325				self.depth += 1
2326		elif line.isReturn():
2327			self.depth -= 1
2328			# remove blacklisted/overdepth/empty funcs that slipped through
2329			if (last and last.isCall() and last.depth == line.depth) or \
2330				(md and last and last.depth >= md) or \
2331				(line.name in self.sv.cgblacklist):
2332				while len(self.list) > 0 and self.list[-1].depth > line.depth:
2333					self.list.pop(-1)
2334				if len(self.list) == 0:
2335					self.invalid = True
2336					return 1
2337				self.list[-1].freturn = True
2338				self.list[-1].length = line.time - self.list[-1].time
2339				self.list[-1].name = line.name
2340				skipadd = True
2341		if len(self.list) < 1:
2342			self.start = line.time
2343		# check for a mismatch that returned all the way to callgraph end
2344		res = 1
2345		if mismatch < 0 and self.list[-1].depth == 0 and self.list[-1].freturn:
2346			line = self.list[-1]
2347			skipadd = True
2348			res = -1
2349		if not skipadd:
2350			self.list.append(line)
2351		if(line.depth == 0 and line.freturn):
2352			if(self.start < 0):
2353				self.start = line.time
2354			self.end = line.time
2355			if line.fcall:
2356				self.end += line.length
2357			if self.list[0].name == self.vfname:
2358				self.invalid = True
2359			if res == -1:
2360				self.partial = True
2361			return res
2362		return 0
2363	def invalidate(self, line):
2364		if(len(self.list) > 0):
2365			first = self.list[0]
2366			self.list = []
2367			self.list.append(first)
2368		self.invalid = True
2369		id = 'task %s' % (self.pid)
2370		window = '(%f - %f)' % (self.start, line.time)
2371		if(self.depth < 0):
2372			pprint('Data misalignment for '+id+\
2373				' (buffer overflow), ignoring this callback')
2374		else:
2375			pprint('Too much data for '+id+\
2376				' '+window+', ignoring this callback')
2377	def slice(self, dev):
2378		minicg = FTraceCallGraph(dev['pid'], self.sv)
2379		minicg.name = self.name
2380		mydepth = -1
2381		good = False
2382		for l in self.list:
2383			if(l.time < dev['start'] or l.time > dev['end']):
2384				continue
2385			if mydepth < 0:
2386				if l.name == 'mutex_lock' and l.freturn:
2387					mydepth = l.depth
2388				continue
2389			elif l.depth == mydepth and l.name == 'mutex_unlock' and l.fcall:
2390				good = True
2391				break
2392			l.depth -= mydepth
2393			minicg.addLine(l)
2394		if not good or len(minicg.list) < 1:
2395			return 0
2396		return minicg
2397	def repair(self, enddepth):
2398		# bring the depth back to 0 with additional returns
2399		fixed = False
2400		last = self.list[-1]
2401		for i in reversed(range(enddepth)):
2402			t = FTraceLine(last.time)
2403			t.depth = i
2404			t.freturn = True
2405			fixed = self.addLine(t)
2406			if fixed != 0:
2407				self.end = last.time
2408				return True
2409		return False
2410	def postProcess(self):
2411		if len(self.list) > 0:
2412			self.name = self.list[0].name
2413		stack = dict()
2414		cnt = 0
2415		last = 0
2416		for l in self.list:
2417			# ftrace bug: reported duration is not reliable
2418			# check each leaf and clip it at max possible length
2419			if last and last.isLeaf():
2420				if last.length > l.time - last.time:
2421					last.length = l.time - last.time
2422			if l.isCall():
2423				stack[l.depth] = l
2424				cnt += 1
2425			elif l.isReturn():
2426				if(l.depth not in stack):
2427					if self.sv.verbose:
2428						pprint('Post Process Error: Depth missing')
2429						l.debugPrint()
2430					return False
2431				# calculate call length from call/return lines
2432				cl = stack[l.depth]
2433				cl.length = l.time - cl.time
2434				if cl.name == self.vfname:
2435					cl.name = l.name
2436				stack.pop(l.depth)
2437				l.length = 0
2438				cnt -= 1
2439			last = l
2440		if(cnt == 0):
2441			# trace caught the whole call tree
2442			return True
2443		elif(cnt < 0):
2444			if self.sv.verbose:
2445				pprint('Post Process Error: Depth is less than 0')
2446			return False
2447		# trace ended before call tree finished
2448		return self.repair(cnt)
2449	def deviceMatch(self, pid, data):
2450		found = ''
2451		# add the callgraph data to the device hierarchy
2452		borderphase = {
2453			'dpm_prepare': 'suspend_prepare',
2454			'dpm_complete': 'resume_complete'
2455		}
2456		if(self.name in borderphase):
2457			p = borderphase[self.name]
2458			list = data.dmesg[p]['list']
2459			for devname in list:
2460				dev = list[devname]
2461				if(pid == dev['pid'] and
2462					self.start <= dev['start'] and
2463					self.end >= dev['end']):
2464					cg = self.slice(dev)
2465					if cg:
2466						dev['ftrace'] = cg
2467					found = devname
2468			return found
2469		for p in data.sortedPhases():
2470			if(data.dmesg[p]['start'] <= self.start and
2471				self.start <= data.dmesg[p]['end']):
2472				list = data.dmesg[p]['list']
2473				for devname in sorted(list, key=lambda k:list[k]['start']):
2474					dev = list[devname]
2475					if(pid == dev['pid'] and
2476						self.start <= dev['start'] and
2477						self.end >= dev['end']):
2478						dev['ftrace'] = self
2479						found = devname
2480						break
2481				break
2482		return found
2483	def newActionFromFunction(self, data):
2484		name = self.name
2485		if name in ['dpm_run_callback', 'dpm_prepare', 'dpm_complete']:
2486			return
2487		fs = self.start
2488		fe = self.end
2489		if fs < data.start or fe > data.end:
2490			return
2491		phase = ''
2492		for p in data.sortedPhases():
2493			if(data.dmesg[p]['start'] <= self.start and
2494				self.start < data.dmesg[p]['end']):
2495				phase = p
2496				break
2497		if not phase:
2498			return
2499		out = data.newActionGlobal(name, fs, fe, -2)
2500		if out:
2501			phase, myname = out
2502			data.dmesg[phase]['list'][myname]['ftrace'] = self
2503	def debugPrint(self, info=''):
2504		pprint('%s pid=%d [%f - %f] %.3f us' % \
2505			(self.name, self.pid, self.start, self.end,
2506			(self.end - self.start)*1000000))
2507		for l in self.list:
2508			if l.isLeaf():
2509				pprint('%f (%02d): %s(); (%.3f us)%s' % (l.time, \
2510					l.depth, l.name, l.length*1000000, info))
2511			elif l.freturn:
2512				pprint('%f (%02d): %s} (%.3f us)%s' % (l.time, \
2513					l.depth, l.name, l.length*1000000, info))
2514			else:
2515				pprint('%f (%02d): %s() { (%.3f us)%s' % (l.time, \
2516					l.depth, l.name, l.length*1000000, info))
2517		pprint(' ')
2518
2519class DevItem:
2520	def __init__(self, test, phase, dev):
2521		self.test = test
2522		self.phase = phase
2523		self.dev = dev
2524	def isa(self, cls):
2525		if 'htmlclass' in self.dev and cls in self.dev['htmlclass']:
2526			return True
2527		return False
2528
2529# Class: Timeline
2530# Description:
2531#	 A container for a device timeline which calculates
2532#	 all the html properties to display it correctly
2533class Timeline:
2534	html_tblock = '<div id="block{0}" class="tblock" style="left:{1}%;width:{2}%;"><div class="tback" style="height:{3}px"></div>\n'
2535	html_device = '<div id="{0}" title="{1}" class="thread{7}" style="left:{2}%;top:{3}px;height:{4}px;width:{5}%;{8}">{6}</div>\n'
2536	html_phase = '<div class="phase" style="left:{0}%;width:{1}%;top:{2}px;height:{3}px;background:{4}">{5}</div>\n'
2537	html_phaselet = '<div id="{0}" class="phaselet" style="left:{1}%;width:{2}%;background:{3}"></div>\n'
2538	html_legend = '<div id="p{3}" class="square" style="left:{0}%;background:{1}">&nbsp;{2}</div>\n'
2539	def __init__(self, rowheight, scaleheight):
2540		self.html = ''
2541		self.height = 0  # total timeline height
2542		self.scaleH = scaleheight # timescale (top) row height
2543		self.rowH = rowheight     # device row height
2544		self.bodyH = 0   # body height
2545		self.rows = 0    # total timeline rows
2546		self.rowlines = dict()
2547		self.rowheight = dict()
2548	def createHeader(self, sv, stamp):
2549		if(not stamp['time']):
2550			return
2551		self.html += '<div class="version"><a href="https://01.org/pm-graph">%s v%s</a></div>' \
2552			% (sv.title, sv.version)
2553		if sv.logmsg and sv.testlog:
2554			self.html += '<button id="showtest" class="logbtn btnfmt">log</button>'
2555		if sv.dmesglog:
2556			self.html += '<button id="showdmesg" class="logbtn btnfmt">dmesg</button>'
2557		if sv.ftracelog:
2558			self.html += '<button id="showftrace" class="logbtn btnfmt">ftrace</button>'
2559		headline_stamp = '<div class="stamp">{0} {1} {2} {3}</div>\n'
2560		self.html += headline_stamp.format(stamp['host'], stamp['kernel'],
2561			stamp['mode'], stamp['time'])
2562		if 'man' in stamp and 'plat' in stamp and 'cpu' in stamp and \
2563			stamp['man'] and stamp['plat'] and stamp['cpu']:
2564			headline_sysinfo = '<div class="stamp sysinfo">{0} {1} <i>with</i> {2}</div>\n'
2565			self.html += headline_sysinfo.format(stamp['man'], stamp['plat'], stamp['cpu'])
2566
2567	# Function: getDeviceRows
2568	# Description:
2569	#    determine how may rows the device funcs will take
2570	# Arguments:
2571	#	 rawlist: the list of devices/actions for a single phase
2572	# Output:
2573	#	 The total number of rows needed to display this phase of the timeline
2574	def getDeviceRows(self, rawlist):
2575		# clear all rows and set them to undefined
2576		sortdict = dict()
2577		for item in rawlist:
2578			item.row = -1
2579			sortdict[item] = item.length
2580		sortlist = sorted(sortdict, key=sortdict.get, reverse=True)
2581		remaining = len(sortlist)
2582		rowdata = dict()
2583		row = 1
2584		# try to pack each row with as many ranges as possible
2585		while(remaining > 0):
2586			if(row not in rowdata):
2587				rowdata[row] = []
2588			for i in sortlist:
2589				if(i.row >= 0):
2590					continue
2591				s = i.time
2592				e = i.time + i.length
2593				valid = True
2594				for ritem in rowdata[row]:
2595					rs = ritem.time
2596					re = ritem.time + ritem.length
2597					if(not (((s <= rs) and (e <= rs)) or
2598						((s >= re) and (e >= re)))):
2599						valid = False
2600						break
2601				if(valid):
2602					rowdata[row].append(i)
2603					i.row = row
2604					remaining -= 1
2605			row += 1
2606		return row
2607	# Function: getPhaseRows
2608	# Description:
2609	#	 Organize the timeline entries into the smallest
2610	#	 number of rows possible, with no entry overlapping
2611	# Arguments:
2612	#	 devlist: the list of devices/actions in a group of contiguous phases
2613	# Output:
2614	#	 The total number of rows needed to display this phase of the timeline
2615	def getPhaseRows(self, devlist, row=0, sortby='length'):
2616		# clear all rows and set them to undefined
2617		remaining = len(devlist)
2618		rowdata = dict()
2619		sortdict = dict()
2620		myphases = []
2621		# initialize all device rows to -1 and calculate devrows
2622		for item in devlist:
2623			dev = item.dev
2624			tp = (item.test, item.phase)
2625			if tp not in myphases:
2626				myphases.append(tp)
2627			dev['row'] = -1
2628			if sortby == 'start':
2629				# sort by start 1st, then length 2nd
2630				sortdict[item] = (-1*float(dev['start']), float(dev['end']) - float(dev['start']))
2631			else:
2632				# sort by length 1st, then name 2nd
2633				sortdict[item] = (float(dev['end']) - float(dev['start']), item.dev['name'])
2634			if 'src' in dev:
2635				dev['devrows'] = self.getDeviceRows(dev['src'])
2636		# sort the devlist by length so that large items graph on top
2637		sortlist = sorted(sortdict, key=sortdict.get, reverse=True)
2638		orderedlist = []
2639		for item in sortlist:
2640			if item.dev['pid'] == -2:
2641				orderedlist.append(item)
2642		for item in sortlist:
2643			if item not in orderedlist:
2644				orderedlist.append(item)
2645		# try to pack each row with as many devices as possible
2646		while(remaining > 0):
2647			rowheight = 1
2648			if(row not in rowdata):
2649				rowdata[row] = []
2650			for item in orderedlist:
2651				dev = item.dev
2652				if(dev['row'] < 0):
2653					s = dev['start']
2654					e = dev['end']
2655					valid = True
2656					for ritem in rowdata[row]:
2657						rs = ritem.dev['start']
2658						re = ritem.dev['end']
2659						if(not (((s <= rs) and (e <= rs)) or
2660							((s >= re) and (e >= re)))):
2661							valid = False
2662							break
2663					if(valid):
2664						rowdata[row].append(item)
2665						dev['row'] = row
2666						remaining -= 1
2667						if 'devrows' in dev and dev['devrows'] > rowheight:
2668							rowheight = dev['devrows']
2669			for t, p in myphases:
2670				if t not in self.rowlines or t not in self.rowheight:
2671					self.rowlines[t] = dict()
2672					self.rowheight[t] = dict()
2673				if p not in self.rowlines[t] or p not in self.rowheight[t]:
2674					self.rowlines[t][p] = dict()
2675					self.rowheight[t][p] = dict()
2676				rh = self.rowH
2677				# section headers should use a different row height
2678				if len(rowdata[row]) == 1 and \
2679					'htmlclass' in rowdata[row][0].dev and \
2680					'sec' in rowdata[row][0].dev['htmlclass']:
2681					rh = 15
2682				self.rowlines[t][p][row] = rowheight
2683				self.rowheight[t][p][row] = rowheight * rh
2684			row += 1
2685		if(row > self.rows):
2686			self.rows = int(row)
2687		return row
2688	def phaseRowHeight(self, test, phase, row):
2689		return self.rowheight[test][phase][row]
2690	def phaseRowTop(self, test, phase, row):
2691		top = 0
2692		for i in sorted(self.rowheight[test][phase]):
2693			if i >= row:
2694				break
2695			top += self.rowheight[test][phase][i]
2696		return top
2697	def calcTotalRows(self):
2698		# Calculate the heights and offsets for the header and rows
2699		maxrows = 0
2700		standardphases = []
2701		for t in self.rowlines:
2702			for p in self.rowlines[t]:
2703				total = 0
2704				for i in sorted(self.rowlines[t][p]):
2705					total += self.rowlines[t][p][i]
2706				if total > maxrows:
2707					maxrows = total
2708				if total == len(self.rowlines[t][p]):
2709					standardphases.append((t, p))
2710		self.height = self.scaleH + (maxrows*self.rowH)
2711		self.bodyH = self.height - self.scaleH
2712		# if there is 1 line per row, draw them the standard way
2713		for t, p in standardphases:
2714			for i in sorted(self.rowheight[t][p]):
2715				self.rowheight[t][p][i] = float(self.bodyH)/len(self.rowlines[t][p])
2716	def createZoomBox(self, mode='command', testcount=1):
2717		# Create bounding box, add buttons
2718		html_zoombox = '<center><button id="zoomin">ZOOM IN +</button><button id="zoomout">ZOOM OUT -</button><button id="zoomdef">ZOOM 1:1</button></center>\n'
2719		html_timeline = '<div id="dmesgzoombox" class="zoombox">\n<div id="{0}" class="timeline" style="height:{1}px">\n'
2720		html_devlist1 = '<button id="devlist1" class="devlist" style="float:left;">Device Detail{0}</button>'
2721		html_devlist2 = '<button id="devlist2" class="devlist" style="float:right;">Device Detail2</button>\n'
2722		if mode != 'command':
2723			if testcount > 1:
2724				self.html += html_devlist2
2725				self.html += html_devlist1.format('1')
2726			else:
2727				self.html += html_devlist1.format('')
2728		self.html += html_zoombox
2729		self.html += html_timeline.format('dmesg', self.height)
2730	# Function: createTimeScale
2731	# Description:
2732	#	 Create the timescale for a timeline block
2733	# Arguments:
2734	#	 m0: start time (mode begin)
2735	#	 mMax: end time (mode end)
2736	#	 tTotal: total timeline time
2737	#	 mode: suspend or resume
2738	# Output:
2739	#	 The html code needed to display the time scale
2740	def createTimeScale(self, m0, mMax, tTotal, mode):
2741		timescale = '<div class="t" style="right:{0}%">{1}</div>\n'
2742		rline = '<div class="t" style="left:0;border-left:1px solid black;border-right:0;">{0}</div>\n'
2743		output = '<div class="timescale">\n'
2744		# set scale for timeline
2745		mTotal = mMax - m0
2746		tS = 0.1
2747		if(tTotal <= 0):
2748			return output+'</div>\n'
2749		if(tTotal > 4):
2750			tS = 1
2751		divTotal = int(mTotal/tS) + 1
2752		divEdge = (mTotal - tS*(divTotal-1))*100/mTotal
2753		for i in range(divTotal):
2754			htmlline = ''
2755			if(mode == 'suspend'):
2756				pos = '%0.3f' % (100 - ((float(i)*tS*100)/mTotal) - divEdge)
2757				val = '%0.fms' % (float(i-divTotal+1)*tS*1000)
2758				if(i == divTotal - 1):
2759					val = mode
2760				htmlline = timescale.format(pos, val)
2761			else:
2762				pos = '%0.3f' % (100 - ((float(i)*tS*100)/mTotal))
2763				val = '%0.fms' % (float(i)*tS*1000)
2764				htmlline = timescale.format(pos, val)
2765				if(i == 0):
2766					htmlline = rline.format(mode)
2767			output += htmlline
2768		self.html += output+'</div>\n'
2769
2770# Class: TestProps
2771# Description:
2772#	 A list of values describing the properties of these test runs
2773class TestProps:
2774	stampfmt = '# [a-z]*-(?P<m>[0-9]{2})(?P<d>[0-9]{2})(?P<y>[0-9]{2})-'+\
2775				'(?P<H>[0-9]{2})(?P<M>[0-9]{2})(?P<S>[0-9]{2})'+\
2776				' (?P<host>.*) (?P<mode>.*) (?P<kernel>.*)$'
2777	wififmt    = '^# wifi *(?P<d>\S*) *(?P<s>\S*) *(?P<t>[0-9\.]+).*'
2778	tstatfmt   = '^# turbostat (?P<t>\S*)'
2779	testerrfmt = '^# enter_sleep_error (?P<e>.*)'
2780	sysinfofmt = '^# sysinfo .*'
2781	cmdlinefmt = '^# command \| (?P<cmd>.*)'
2782	devpropfmt = '# Device Properties: .*'
2783	pinfofmt   = '# platform-(?P<val>[a-z,A-Z,0-9]*): (?P<info>.*)'
2784	tracertypefmt = '# tracer: (?P<t>.*)'
2785	firmwarefmt = '# fwsuspend (?P<s>[0-9]*) fwresume (?P<r>[0-9]*)$'
2786	procexecfmt = 'ps - (?P<ps>.*)$'
2787	ftrace_line_fmt_fg = \
2788		'^ *(?P<time>[0-9\.]*) *\| *(?P<cpu>[0-9]*)\)'+\
2789		' *(?P<proc>.*)-(?P<pid>[0-9]*) *\|'+\
2790		'[ +!#\*@$]*(?P<dur>[0-9\.]*) .*\|  (?P<msg>.*)'
2791	ftrace_line_fmt_nop = \
2792		' *(?P<proc>.*)-(?P<pid>[0-9]*) *\[(?P<cpu>[0-9]*)\] *'+\
2793		'(?P<flags>.{4}) *(?P<time>[0-9\.]*): *'+\
2794		'(?P<msg>.*)'
2795	def __init__(self):
2796		self.stamp = ''
2797		self.sysinfo = ''
2798		self.cmdline = ''
2799		self.testerror = []
2800		self.turbostat = []
2801		self.wifi = []
2802		self.fwdata = []
2803		self.ftrace_line_fmt = self.ftrace_line_fmt_nop
2804		self.cgformat = False
2805		self.data = 0
2806		self.ktemp = dict()
2807	def setTracerType(self, tracer):
2808		if(tracer == 'function_graph'):
2809			self.cgformat = True
2810			self.ftrace_line_fmt = self.ftrace_line_fmt_fg
2811		elif(tracer == 'nop'):
2812			self.ftrace_line_fmt = self.ftrace_line_fmt_nop
2813		else:
2814			doError('Invalid tracer format: [%s]' % tracer)
2815	def stampInfo(self, line):
2816		if re.match(self.stampfmt, line):
2817			self.stamp = line
2818			return True
2819		elif re.match(self.sysinfofmt, line):
2820			self.sysinfo = line
2821			return True
2822		elif re.match(self.cmdlinefmt, line):
2823			self.cmdline = line
2824			return True
2825		elif re.match(self.tstatfmt, line):
2826			self.turbostat.append(line)
2827			return True
2828		elif re.match(self.wififmt, line):
2829			self.wifi.append(line)
2830			return True
2831		elif re.match(self.testerrfmt, line):
2832			self.testerror.append(line)
2833			return True
2834		elif re.match(self.firmwarefmt, line):
2835			self.fwdata.append(line)
2836			return True
2837		return False
2838	def parseStamp(self, data, sv):
2839		# global test data
2840		m = re.match(self.stampfmt, self.stamp)
2841		if not self.stamp or not m:
2842			doError('data does not include the expected stamp')
2843		data.stamp = {'time': '', 'host': '', 'mode': ''}
2844		dt = datetime(int(m.group('y'))+2000, int(m.group('m')),
2845			int(m.group('d')), int(m.group('H')), int(m.group('M')),
2846			int(m.group('S')))
2847		data.stamp['time'] = dt.strftime('%B %d %Y, %I:%M:%S %p')
2848		data.stamp['host'] = m.group('host')
2849		data.stamp['mode'] = m.group('mode')
2850		data.stamp['kernel'] = m.group('kernel')
2851		if re.match(self.sysinfofmt, self.sysinfo):
2852			for f in self.sysinfo.split('|'):
2853				if '#' in f:
2854					continue
2855				tmp = f.strip().split(':', 1)
2856				key = tmp[0]
2857				val = tmp[1]
2858				data.stamp[key] = val
2859		sv.hostname = data.stamp['host']
2860		sv.suspendmode = data.stamp['mode']
2861		if sv.suspendmode == 'command' and sv.ftracefile != '':
2862			modes = ['on', 'freeze', 'standby', 'mem', 'disk']
2863			fp = sysvals.openlog(sv.ftracefile, 'r')
2864			for line in fp:
2865				m = re.match('.* machine_suspend\[(?P<mode>.*)\]', line)
2866				if m and m.group('mode') in ['1', '2', '3', '4']:
2867					sv.suspendmode = modes[int(m.group('mode'))]
2868					data.stamp['mode'] = sv.suspendmode
2869					break
2870			fp.close()
2871		m = re.match(self.cmdlinefmt, self.cmdline)
2872		if m:
2873			sv.cmdline = m.group('cmd')
2874		if not sv.stamp:
2875			sv.stamp = data.stamp
2876		# firmware data
2877		if sv.suspendmode == 'mem' and len(self.fwdata) > data.testnumber:
2878			m = re.match(self.firmwarefmt, self.fwdata[data.testnumber])
2879			if m:
2880				data.fwSuspend, data.fwResume = int(m.group('s')), int(m.group('r'))
2881				if(data.fwSuspend > 0 or data.fwResume > 0):
2882					data.fwValid = True
2883		# turbostat data
2884		if len(self.turbostat) > data.testnumber:
2885			m = re.match(self.tstatfmt, self.turbostat[data.testnumber])
2886			if m:
2887				data.turbostat = m.group('t')
2888		# wifi data
2889		if len(self.wifi) > data.testnumber:
2890			m = re.match(self.wififmt, self.wifi[data.testnumber])
2891			if m:
2892				data.wifi = {'dev': m.group('d'), 'stat': m.group('s'),
2893					'time': float(m.group('t'))}
2894				data.stamp['wifi'] = m.group('d')
2895		# sleep mode enter errors
2896		if len(self.testerror) > data.testnumber:
2897			m = re.match(self.testerrfmt, self.testerror[data.testnumber])
2898			if m:
2899				data.enterfail = m.group('e')
2900	def devprops(self, data):
2901		props = dict()
2902		devlist = data.split(';')
2903		for dev in devlist:
2904			f = dev.split(',')
2905			if len(f) < 3:
2906				continue
2907			dev = f[0]
2908			props[dev] = DevProps()
2909			props[dev].altname = f[1]
2910			if int(f[2]):
2911				props[dev].isasync = True
2912			else:
2913				props[dev].isasync = False
2914		return props
2915	def parseDevprops(self, line, sv):
2916		idx = line.index(': ') + 2
2917		if idx >= len(line):
2918			return
2919		props = self.devprops(line[idx:])
2920		if sv.suspendmode == 'command' and 'testcommandstring' in props:
2921			sv.testcommand = props['testcommandstring'].altname
2922		sv.devprops = props
2923	def parsePlatformInfo(self, line, sv):
2924		m = re.match(self.pinfofmt, line)
2925		if not m:
2926			return
2927		name, info = m.group('val'), m.group('info')
2928		if name == 'devinfo':
2929			sv.devprops = self.devprops(sv.b64unzip(info))
2930			return
2931		elif name == 'testcmd':
2932			sv.testcommand = info
2933			return
2934		field = info.split('|')
2935		if len(field) < 2:
2936			return
2937		cmdline = field[0].strip()
2938		output = sv.b64unzip(field[1].strip())
2939		sv.platinfo.append([name, cmdline, output])
2940
2941# Class: TestRun
2942# Description:
2943#	 A container for a suspend/resume test run. This is necessary as
2944#	 there could be more than one, and they need to be separate.
2945class TestRun:
2946	def __init__(self, dataobj):
2947		self.data = dataobj
2948		self.ftemp = dict()
2949		self.ttemp = dict()
2950
2951class ProcessMonitor:
2952	def __init__(self):
2953		self.proclist = dict()
2954		self.running = False
2955	def procstat(self):
2956		c = ['cat /proc/[1-9]*/stat 2>/dev/null']
2957		process = Popen(c, shell=True, stdout=PIPE)
2958		running = dict()
2959		for line in process.stdout:
2960			data = ascii(line).split()
2961			pid = data[0]
2962			name = re.sub('[()]', '', data[1])
2963			user = int(data[13])
2964			kern = int(data[14])
2965			kjiff = ujiff = 0
2966			if pid not in self.proclist:
2967				self.proclist[pid] = {'name' : name, 'user' : user, 'kern' : kern}
2968			else:
2969				val = self.proclist[pid]
2970				ujiff = user - val['user']
2971				kjiff = kern - val['kern']
2972				val['user'] = user
2973				val['kern'] = kern
2974			if ujiff > 0 or kjiff > 0:
2975				running[pid] = ujiff + kjiff
2976		process.wait()
2977		out = ''
2978		for pid in running:
2979			jiffies = running[pid]
2980			val = self.proclist[pid]
2981			if out:
2982				out += ','
2983			out += '%s-%s %d' % (val['name'], pid, jiffies)
2984		return 'ps - '+out
2985	def processMonitor(self, tid):
2986		while self.running:
2987			out = self.procstat()
2988			if out:
2989				sysvals.fsetVal(out, 'trace_marker')
2990	def start(self):
2991		self.thread = Thread(target=self.processMonitor, args=(0,))
2992		self.running = True
2993		self.thread.start()
2994	def stop(self):
2995		self.running = False
2996
2997# ----------------- FUNCTIONS --------------------
2998
2999# Function: doesTraceLogHaveTraceEvents
3000# Description:
3001#	 Quickly determine if the ftrace log has all of the trace events,
3002#	 markers, and/or kprobes required for primary parsing.
3003def doesTraceLogHaveTraceEvents():
3004	kpcheck = ['_cal: (', '_ret: (']
3005	techeck = ['suspend_resume', 'device_pm_callback']
3006	tmcheck = ['SUSPEND START', 'RESUME COMPLETE']
3007	sysvals.usekprobes = False
3008	fp = sysvals.openlog(sysvals.ftracefile, 'r')
3009	for line in fp:
3010		# check for kprobes
3011		if not sysvals.usekprobes:
3012			for i in kpcheck:
3013				if i in line:
3014					sysvals.usekprobes = True
3015		# check for all necessary trace events
3016		check = techeck[:]
3017		for i in techeck:
3018			if i in line:
3019				check.remove(i)
3020		techeck = check
3021		# check for all necessary trace markers
3022		check = tmcheck[:]
3023		for i in tmcheck:
3024			if i in line:
3025				check.remove(i)
3026		tmcheck = check
3027	fp.close()
3028	sysvals.usetraceevents = True if len(techeck) < 2 else False
3029	sysvals.usetracemarkers = True if len(tmcheck) == 0 else False
3030
3031# Function: appendIncompleteTraceLog
3032# Description:
3033#	 [deprecated for kernel 3.15 or newer]
3034#	 Adds callgraph data which lacks trace event data. This is only
3035#	 for timelines generated from 3.15 or older
3036# Arguments:
3037#	 testruns: the array of Data objects obtained from parseKernelLog
3038def appendIncompleteTraceLog(testruns):
3039	# create TestRun vessels for ftrace parsing
3040	testcnt = len(testruns)
3041	testidx = 0
3042	testrun = []
3043	for data in testruns:
3044		testrun.append(TestRun(data))
3045
3046	# extract the callgraph and traceevent data
3047	sysvals.vprint('Analyzing the ftrace data (%s)...' % \
3048		os.path.basename(sysvals.ftracefile))
3049	tp = TestProps()
3050	tf = sysvals.openlog(sysvals.ftracefile, 'r')
3051	data = 0
3052	for line in tf:
3053		# remove any latent carriage returns
3054		line = line.replace('\r\n', '')
3055		if tp.stampInfo(line):
3056			continue
3057		# determine the trace data type (required for further parsing)
3058		m = re.match(tp.tracertypefmt, line)
3059		if(m):
3060			tp.setTracerType(m.group('t'))
3061			continue
3062		# device properties line
3063		if(re.match(tp.devpropfmt, line)):
3064			tp.parseDevprops(line, sysvals)
3065			continue
3066		# platform info line
3067		if(re.match(tp.pinfofmt, line)):
3068			tp.parsePlatformInfo(line, sysvals)
3069			continue
3070		# parse only valid lines, if this is not one move on
3071		m = re.match(tp.ftrace_line_fmt, line)
3072		if(not m):
3073			continue
3074		# gather the basic message data from the line
3075		m_time = m.group('time')
3076		m_pid = m.group('pid')
3077		m_msg = m.group('msg')
3078		if(tp.cgformat):
3079			m_param3 = m.group('dur')
3080		else:
3081			m_param3 = 'traceevent'
3082		if(m_time and m_pid and m_msg):
3083			t = FTraceLine(m_time, m_msg, m_param3)
3084			pid = int(m_pid)
3085		else:
3086			continue
3087		# the line should be a call, return, or event
3088		if(not t.fcall and not t.freturn and not t.fevent):
3089			continue
3090		# look for the suspend start marker
3091		if(t.startMarker()):
3092			data = testrun[testidx].data
3093			tp.parseStamp(data, sysvals)
3094			data.setStart(t.time, t.name)
3095			continue
3096		if(not data):
3097			continue
3098		# find the end of resume
3099		if(t.endMarker()):
3100			data.setEnd(t.time, t.name)
3101			testidx += 1
3102			if(testidx >= testcnt):
3103				break
3104			continue
3105		# trace event processing
3106		if(t.fevent):
3107			continue
3108		# call/return processing
3109		elif sysvals.usecallgraph:
3110			# create a callgraph object for the data
3111			if(pid not in testrun[testidx].ftemp):
3112				testrun[testidx].ftemp[pid] = []
3113				testrun[testidx].ftemp[pid].append(FTraceCallGraph(pid, sysvals))
3114			# when the call is finished, see which device matches it
3115			cg = testrun[testidx].ftemp[pid][-1]
3116			res = cg.addLine(t)
3117			if(res != 0):
3118				testrun[testidx].ftemp[pid].append(FTraceCallGraph(pid, sysvals))
3119			if(res == -1):
3120				testrun[testidx].ftemp[pid][-1].addLine(t)
3121	tf.close()
3122
3123	for test in testrun:
3124		# add the callgraph data to the device hierarchy
3125		for pid in test.ftemp:
3126			for cg in test.ftemp[pid]:
3127				if len(cg.list) < 1 or cg.invalid or (cg.end - cg.start == 0):
3128					continue
3129				if(not cg.postProcess()):
3130					id = 'task %s cpu %s' % (pid, m.group('cpu'))
3131					sysvals.vprint('Sanity check failed for '+\
3132						id+', ignoring this callback')
3133					continue
3134				callstart = cg.start
3135				callend = cg.end
3136				for p in test.data.sortedPhases():
3137					if(test.data.dmesg[p]['start'] <= callstart and
3138						callstart <= test.data.dmesg[p]['end']):
3139						list = test.data.dmesg[p]['list']
3140						for devname in list:
3141							dev = list[devname]
3142							if(pid == dev['pid'] and
3143								callstart <= dev['start'] and
3144								callend >= dev['end']):
3145								dev['ftrace'] = cg
3146						break
3147
3148# Function: parseTraceLog
3149# Description:
3150#	 Analyze an ftrace log output file generated from this app during
3151#	 the execution phase. Used when the ftrace log is the primary data source
3152#	 and includes the suspend_resume and device_pm_callback trace events
3153#	 The ftrace filename is taken from sysvals
3154# Output:
3155#	 An array of Data objects
3156def parseTraceLog(live=False):
3157	sysvals.vprint('Analyzing the ftrace data (%s)...' % \
3158		os.path.basename(sysvals.ftracefile))
3159	if(os.path.exists(sysvals.ftracefile) == False):
3160		doError('%s does not exist' % sysvals.ftracefile)
3161	if not live:
3162		sysvals.setupAllKprobes()
3163	ksuscalls = ['ksys_sync', 'pm_prepare_console']
3164	krescalls = ['pm_restore_console']
3165	tracewatch = ['irq_wakeup']
3166	if sysvals.usekprobes:
3167		tracewatch += ['sync_filesystems', 'freeze_processes', 'syscore_suspend',
3168			'syscore_resume', 'resume_console', 'thaw_processes', 'CPU_ON',
3169			'CPU_OFF', 'timekeeping_freeze', 'acpi_suspend']
3170
3171	# extract the callgraph and traceevent data
3172	tp = TestProps()
3173	testruns = []
3174	testdata = []
3175	testrun = 0
3176	data, limbo = 0, True
3177	tf = sysvals.openlog(sysvals.ftracefile, 'r')
3178	phase = 'suspend_prepare'
3179	for line in tf:
3180		# remove any latent carriage returns
3181		line = line.replace('\r\n', '')
3182		if tp.stampInfo(line):
3183			continue
3184		# tracer type line: determine the trace data type
3185		m = re.match(tp.tracertypefmt, line)
3186		if(m):
3187			tp.setTracerType(m.group('t'))
3188			continue
3189		# device properties line
3190		if(re.match(tp.devpropfmt, line)):
3191			tp.parseDevprops(line, sysvals)
3192			continue
3193		# platform info line
3194		if(re.match(tp.pinfofmt, line)):
3195			tp.parsePlatformInfo(line, sysvals)
3196			continue
3197		# ignore all other commented lines
3198		if line[0] == '#':
3199			continue
3200		# ftrace line: parse only valid lines
3201		m = re.match(tp.ftrace_line_fmt, line)
3202		if(not m):
3203			continue
3204		# gather the basic message data from the line
3205		m_time = m.group('time')
3206		m_proc = m.group('proc')
3207		m_pid = m.group('pid')
3208		m_msg = m.group('msg')
3209		if(tp.cgformat):
3210			m_param3 = m.group('dur')
3211		else:
3212			m_param3 = 'traceevent'
3213		if(m_time and m_pid and m_msg):
3214			t = FTraceLine(m_time, m_msg, m_param3)
3215			pid = int(m_pid)
3216		else:
3217			continue
3218		# the line should be a call, return, or event
3219		if(not t.fcall and not t.freturn and not t.fevent):
3220			continue
3221		# find the start of suspend
3222		if(t.startMarker()):
3223			data, limbo = Data(len(testdata)), False
3224			testdata.append(data)
3225			testrun = TestRun(data)
3226			testruns.append(testrun)
3227			tp.parseStamp(data, sysvals)
3228			data.setStart(t.time, t.name)
3229			data.first_suspend_prepare = True
3230			phase = data.setPhase('suspend_prepare', t.time, True)
3231			continue
3232		if(not data or limbo):
3233			continue
3234		# process cpu exec line
3235		if t.type == 'tracing_mark_write':
3236			m = re.match(tp.procexecfmt, t.name)
3237			if(m):
3238				proclist = dict()
3239				for ps in m.group('ps').split(','):
3240					val = ps.split()
3241					if not val:
3242						continue
3243					name = val[0].replace('--', '-')
3244					proclist[name] = int(val[1])
3245				data.pstl[t.time] = proclist
3246				continue
3247		# find the end of resume
3248		if(t.endMarker()):
3249			if data.tKernRes == 0:
3250				data.tKernRes = t.time
3251			data.handleEndMarker(t.time, t.name)
3252			if(not sysvals.usetracemarkers):
3253				# no trace markers? then quit and be sure to finish recording
3254				# the event we used to trigger resume end
3255				if('thaw_processes' in testrun.ttemp and len(testrun.ttemp['thaw_processes']) > 0):
3256					# if an entry exists, assume this is its end
3257					testrun.ttemp['thaw_processes'][-1]['end'] = t.time
3258			limbo = True
3259			continue
3260		# trace event processing
3261		if(t.fevent):
3262			if(t.type == 'suspend_resume'):
3263				# suspend_resume trace events have two types, begin and end
3264				if(re.match('(?P<name>.*) begin$', t.name)):
3265					isbegin = True
3266				elif(re.match('(?P<name>.*) end$', t.name)):
3267					isbegin = False
3268				else:
3269					continue
3270				if '[' in t.name:
3271					m = re.match('(?P<name>.*)\[.*', t.name)
3272				else:
3273					m = re.match('(?P<name>.*) .*', t.name)
3274				name = m.group('name')
3275				# ignore these events
3276				if(name.split('[')[0] in tracewatch):
3277					continue
3278				# -- phase changes --
3279				# start of kernel suspend
3280				if(re.match('suspend_enter\[.*', t.name)):
3281					if(isbegin and data.tKernSus == 0):
3282						data.tKernSus = t.time
3283					continue
3284				# suspend_prepare start
3285				elif(re.match('dpm_prepare\[.*', t.name)):
3286					if isbegin and data.first_suspend_prepare:
3287						data.first_suspend_prepare = False
3288						if data.tKernSus == 0:
3289							data.tKernSus = t.time
3290						continue
3291					phase = data.setPhase('suspend_prepare', t.time, isbegin)
3292					continue
3293				# suspend start
3294				elif(re.match('dpm_suspend\[.*', t.name)):
3295					phase = data.setPhase('suspend', t.time, isbegin)
3296					continue
3297				# suspend_late start
3298				elif(re.match('dpm_suspend_late\[.*', t.name)):
3299					phase = data.setPhase('suspend_late', t.time, isbegin)
3300					continue
3301				# suspend_noirq start
3302				elif(re.match('dpm_suspend_noirq\[.*', t.name)):
3303					phase = data.setPhase('suspend_noirq', t.time, isbegin)
3304					continue
3305				# suspend_machine/resume_machine
3306				elif(re.match('machine_suspend\[.*', t.name)):
3307					if(isbegin):
3308						lp = data.lastPhase()
3309						if lp.startswith('resume_machine'):
3310							data.dmesg[lp]['end'] = t.time
3311						phase = data.setPhase('suspend_machine', data.dmesg[lp]['end'], True)
3312						data.setPhase(phase, t.time, False)
3313						if data.tSuspended == 0:
3314							data.tSuspended = t.time
3315					else:
3316						phase = data.setPhase('resume_machine', t.time, True)
3317						if(sysvals.suspendmode in ['mem', 'disk']):
3318							susp = phase.replace('resume', 'suspend')
3319							if susp in data.dmesg:
3320								data.dmesg[susp]['end'] = t.time
3321							data.tSuspended = t.time
3322						data.tResumed = t.time
3323					continue
3324				# resume_noirq start
3325				elif(re.match('dpm_resume_noirq\[.*', t.name)):
3326					phase = data.setPhase('resume_noirq', t.time, isbegin)
3327					continue
3328				# resume_early start
3329				elif(re.match('dpm_resume_early\[.*', t.name)):
3330					phase = data.setPhase('resume_early', t.time, isbegin)
3331					continue
3332				# resume start
3333				elif(re.match('dpm_resume\[.*', t.name)):
3334					phase = data.setPhase('resume', t.time, isbegin)
3335					continue
3336				# resume complete start
3337				elif(re.match('dpm_complete\[.*', t.name)):
3338					phase = data.setPhase('resume_complete', t.time, isbegin)
3339					continue
3340				# skip trace events inside devices calls
3341				if(not data.isTraceEventOutsideDeviceCalls(pid, t.time)):
3342					continue
3343				# global events (outside device calls) are graphed
3344				if(name not in testrun.ttemp):
3345					testrun.ttemp[name] = []
3346				if(isbegin):
3347					# create a new list entry
3348					testrun.ttemp[name].append(\
3349						{'begin': t.time, 'end': t.time, 'pid': pid})
3350				else:
3351					if(len(testrun.ttemp[name]) > 0):
3352						# if an entry exists, assume this is its end
3353						testrun.ttemp[name][-1]['end'] = t.time
3354			# device callback start
3355			elif(t.type == 'device_pm_callback_start'):
3356				if phase not in data.dmesg:
3357					continue
3358				m = re.match('(?P<drv>.*) (?P<d>.*), parent: *(?P<p>.*), .*',\
3359					t.name);
3360				if(not m):
3361					continue
3362				drv = m.group('drv')
3363				n = m.group('d')
3364				p = m.group('p')
3365				if(n and p):
3366					data.newAction(phase, n, pid, p, t.time, -1, drv)
3367					if pid not in data.devpids:
3368						data.devpids.append(pid)
3369			# device callback finish
3370			elif(t.type == 'device_pm_callback_end'):
3371				if phase not in data.dmesg:
3372					continue
3373				m = re.match('(?P<drv>.*) (?P<d>.*), err.*', t.name);
3374				if(not m):
3375					continue
3376				n = m.group('d')
3377				list = data.dmesg[phase]['list']
3378				if(n in list):
3379					dev = list[n]
3380					dev['length'] = t.time - dev['start']
3381					dev['end'] = t.time
3382		# kprobe event processing
3383		elif(t.fkprobe):
3384			kprobename = t.type
3385			kprobedata = t.name
3386			key = (kprobename, pid)
3387			# displayname is generated from kprobe data
3388			displayname = ''
3389			if(t.fcall):
3390				displayname = sysvals.kprobeDisplayName(kprobename, kprobedata)
3391				if not displayname:
3392					continue
3393				if(key not in tp.ktemp):
3394					tp.ktemp[key] = []
3395				tp.ktemp[key].append({
3396					'pid': pid,
3397					'begin': t.time,
3398					'end': -1,
3399					'name': displayname,
3400					'cdata': kprobedata,
3401					'proc': m_proc,
3402				})
3403				# start of kernel resume
3404				if(data.tKernSus == 0 and phase == 'suspend_prepare' \
3405					and kprobename in ksuscalls):
3406					data.tKernSus = t.time
3407			elif(t.freturn):
3408				if(key not in tp.ktemp) or len(tp.ktemp[key]) < 1:
3409					continue
3410				e = next((x for x in reversed(tp.ktemp[key]) if x['end'] < 0), 0)
3411				if not e:
3412					continue
3413				e['end'] = t.time
3414				e['rdata'] = kprobedata
3415				# end of kernel resume
3416				if(phase != 'suspend_prepare' and kprobename in krescalls):
3417					if phase in data.dmesg:
3418						data.dmesg[phase]['end'] = t.time
3419					data.tKernRes = t.time
3420
3421		# callgraph processing
3422		elif sysvals.usecallgraph:
3423			# create a callgraph object for the data
3424			key = (m_proc, pid)
3425			if(key not in testrun.ftemp):
3426				testrun.ftemp[key] = []
3427				testrun.ftemp[key].append(FTraceCallGraph(pid, sysvals))
3428			# when the call is finished, see which device matches it
3429			cg = testrun.ftemp[key][-1]
3430			res = cg.addLine(t)
3431			if(res != 0):
3432				testrun.ftemp[key].append(FTraceCallGraph(pid, sysvals))
3433			if(res == -1):
3434				testrun.ftemp[key][-1].addLine(t)
3435	tf.close()
3436	if len(testdata) < 1:
3437		sysvals.vprint('WARNING: ftrace start marker is missing')
3438	if data and not data.devicegroups:
3439		sysvals.vprint('WARNING: ftrace end marker is missing')
3440		data.handleEndMarker(t.time, t.name)
3441
3442	if sysvals.suspendmode == 'command':
3443		for test in testruns:
3444			for p in test.data.sortedPhases():
3445				if p == 'suspend_prepare':
3446					test.data.dmesg[p]['start'] = test.data.start
3447					test.data.dmesg[p]['end'] = test.data.end
3448				else:
3449					test.data.dmesg[p]['start'] = test.data.end
3450					test.data.dmesg[p]['end'] = test.data.end
3451			test.data.tSuspended = test.data.end
3452			test.data.tResumed = test.data.end
3453			test.data.fwValid = False
3454
3455	# dev source and procmon events can be unreadable with mixed phase height
3456	if sysvals.usedevsrc or sysvals.useprocmon:
3457		sysvals.mixedphaseheight = False
3458
3459	# expand phase boundaries so there are no gaps
3460	for data in testdata:
3461		lp = data.sortedPhases()[0]
3462		for p in data.sortedPhases():
3463			if(p != lp and not ('machine' in p and 'machine' in lp)):
3464				data.dmesg[lp]['end'] = data.dmesg[p]['start']
3465			lp = p
3466
3467	for i in range(len(testruns)):
3468		test = testruns[i]
3469		data = test.data
3470		# find the total time range for this test (begin, end)
3471		tlb, tle = data.start, data.end
3472		if i < len(testruns) - 1:
3473			tle = testruns[i+1].data.start
3474		# add the process usage data to the timeline
3475		if sysvals.useprocmon:
3476			data.createProcessUsageEvents()
3477		# add the traceevent data to the device hierarchy
3478		if(sysvals.usetraceevents):
3479			# add actual trace funcs
3480			for name in sorted(test.ttemp):
3481				for event in test.ttemp[name]:
3482					data.newActionGlobal(name, event['begin'], event['end'], event['pid'])
3483			# add the kprobe based virtual tracefuncs as actual devices
3484			for key in sorted(tp.ktemp):
3485				name, pid = key
3486				if name not in sysvals.tracefuncs:
3487					continue
3488				if pid not in data.devpids:
3489					data.devpids.append(pid)
3490				for e in tp.ktemp[key]:
3491					kb, ke = e['begin'], e['end']
3492					if ke - kb < 0.000001 or tlb > kb or tle <= kb:
3493						continue
3494					color = sysvals.kprobeColor(name)
3495					data.newActionGlobal(e['name'], kb, ke, pid, color)
3496			# add config base kprobes and dev kprobes
3497			if sysvals.usedevsrc:
3498				for key in sorted(tp.ktemp):
3499					name, pid = key
3500					if name in sysvals.tracefuncs or name not in sysvals.dev_tracefuncs:
3501						continue
3502					for e in tp.ktemp[key]:
3503						kb, ke = e['begin'], e['end']
3504						if ke - kb < 0.000001 or tlb > kb or tle <= kb:
3505							continue
3506						data.addDeviceFunctionCall(e['name'], name, e['proc'], pid, kb,
3507							ke, e['cdata'], e['rdata'])
3508		if sysvals.usecallgraph:
3509			# add the callgraph data to the device hierarchy
3510			sortlist = dict()
3511			for key in sorted(test.ftemp):
3512				proc, pid = key
3513				for cg in test.ftemp[key]:
3514					if len(cg.list) < 1 or cg.invalid or (cg.end - cg.start == 0):
3515						continue
3516					if(not cg.postProcess()):
3517						id = 'task %s' % (pid)
3518						sysvals.vprint('Sanity check failed for '+\
3519							id+', ignoring this callback')
3520						continue
3521					# match cg data to devices
3522					devname = ''
3523					if sysvals.suspendmode != 'command':
3524						devname = cg.deviceMatch(pid, data)
3525					if not devname:
3526						sortkey = '%f%f%d' % (cg.start, cg.end, pid)
3527						sortlist[sortkey] = cg
3528					elif len(cg.list) > 1000000 and cg.name != sysvals.ftopfunc:
3529						sysvals.vprint('WARNING: the callgraph for %s is massive (%d lines)' %\
3530							(devname, len(cg.list)))
3531			# create blocks for orphan cg data
3532			for sortkey in sorted(sortlist):
3533				cg = sortlist[sortkey]
3534				name = cg.name
3535				if sysvals.isCallgraphFunc(name):
3536					sysvals.vprint('Callgraph found for task %d: %.3fms, %s' % (cg.pid, (cg.end - cg.start)*1000, name))
3537					cg.newActionFromFunction(data)
3538	if sysvals.suspendmode == 'command':
3539		return (testdata, '')
3540
3541	# fill in any missing phases
3542	error = []
3543	for data in testdata:
3544		tn = '' if len(testdata) == 1 else ('%d' % (data.testnumber + 1))
3545		terr = ''
3546		phasedef = data.phasedef
3547		lp = 'suspend_prepare'
3548		for p in sorted(phasedef, key=lambda k:phasedef[k]['order']):
3549			if p not in data.dmesg:
3550				if not terr:
3551					pprint('TEST%s FAILED: %s failed in %s phase' % (tn, sysvals.suspendmode, lp))
3552					terr = '%s%s failed in %s phase' % (sysvals.suspendmode, tn, lp)
3553					error.append(terr)
3554					if data.tSuspended == 0:
3555						data.tSuspended = data.dmesg[lp]['end']
3556					if data.tResumed == 0:
3557						data.tResumed = data.dmesg[lp]['end']
3558					data.fwValid = False
3559				sysvals.vprint('WARNING: phase "%s" is missing!' % p)
3560			lp = p
3561		if not terr and 'dev' in data.wifi and data.wifi['stat'] == 'timeout':
3562			terr = '%s%s failed in wifi_resume <i>(%s %.0fs timeout)</i>' % \
3563				(sysvals.suspendmode, tn, data.wifi['dev'], data.wifi['time'])
3564			error.append(terr)
3565		if not terr and data.enterfail:
3566			pprint('test%s FAILED: enter %s failed with %s' % (tn, sysvals.suspendmode, data.enterfail))
3567			terr = 'test%s failed to enter %s mode' % (tn, sysvals.suspendmode)
3568			error.append(terr)
3569		if data.tSuspended == 0:
3570			data.tSuspended = data.tKernRes
3571		if data.tResumed == 0:
3572			data.tResumed = data.tSuspended
3573
3574		if(len(sysvals.devicefilter) > 0):
3575			data.deviceFilter(sysvals.devicefilter)
3576		data.fixupInitcallsThatDidntReturn()
3577		if sysvals.usedevsrc:
3578			data.optimizeDevSrc()
3579
3580	# x2: merge any overlapping devices between test runs
3581	if sysvals.usedevsrc and len(testdata) > 1:
3582		tc = len(testdata)
3583		for i in range(tc - 1):
3584			devlist = testdata[i].overflowDevices()
3585			for j in range(i + 1, tc):
3586				testdata[j].mergeOverlapDevices(devlist)
3587		testdata[0].stitchTouchingThreads(testdata[1:])
3588	return (testdata, ', '.join(error))
3589
3590# Function: loadKernelLog
3591# Description:
3592#	 [deprecated for kernel 3.15.0 or newer]
3593#	 load the dmesg file into memory and fix up any ordering issues
3594#	 The dmesg filename is taken from sysvals
3595# Output:
3596#	 An array of empty Data objects with only their dmesgtext attributes set
3597def loadKernelLog():
3598	sysvals.vprint('Analyzing the dmesg data (%s)...' % \
3599		os.path.basename(sysvals.dmesgfile))
3600	if(os.path.exists(sysvals.dmesgfile) == False):
3601		doError('%s does not exist' % sysvals.dmesgfile)
3602
3603	# there can be multiple test runs in a single file
3604	tp = TestProps()
3605	tp.stamp = datetime.now().strftime('# suspend-%m%d%y-%H%M%S localhost mem unknown')
3606	testruns = []
3607	data = 0
3608	lf = sysvals.openlog(sysvals.dmesgfile, 'r')
3609	for line in lf:
3610		line = line.replace('\r\n', '')
3611		idx = line.find('[')
3612		if idx > 1:
3613			line = line[idx:]
3614		if tp.stampInfo(line):
3615			continue
3616		m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
3617		if(not m):
3618			continue
3619		msg = m.group("msg")
3620		if(re.match('PM: Syncing filesystems.*', msg)):
3621			if(data):
3622				testruns.append(data)
3623			data = Data(len(testruns))
3624			tp.parseStamp(data, sysvals)
3625		if(not data):
3626			continue
3627		m = re.match('.* *(?P<k>[0-9]\.[0-9]{2}\.[0-9]-.*) .*', msg)
3628		if(m):
3629			sysvals.stamp['kernel'] = m.group('k')
3630		m = re.match('PM: Preparing system for (?P<m>.*) sleep', msg)
3631		if(m):
3632			sysvals.stamp['mode'] = sysvals.suspendmode = m.group('m')
3633		data.dmesgtext.append(line)
3634	lf.close()
3635
3636	if data:
3637		testruns.append(data)
3638	if len(testruns) < 1:
3639		doError('dmesg log has no suspend/resume data: %s' \
3640			% sysvals.dmesgfile)
3641
3642	# fix lines with same timestamp/function with the call and return swapped
3643	for data in testruns:
3644		last = ''
3645		for line in data.dmesgtext:
3646			mc = re.match('.*(\[ *)(?P<t>[0-9\.]*)(\]) calling  '+\
3647				'(?P<f>.*)\+ @ .*, parent: .*', line)
3648			mr = re.match('.*(\[ *)(?P<t>[0-9\.]*)(\]) call '+\
3649				'(?P<f>.*)\+ returned .* after (?P<dt>.*) usecs', last)
3650			if(mc and mr and (mc.group('t') == mr.group('t')) and
3651				(mc.group('f') == mr.group('f'))):
3652				i = data.dmesgtext.index(last)
3653				j = data.dmesgtext.index(line)
3654				data.dmesgtext[i] = line
3655				data.dmesgtext[j] = last
3656			last = line
3657	return testruns
3658
3659# Function: parseKernelLog
3660# Description:
3661#	 [deprecated for kernel 3.15.0 or newer]
3662#	 Analyse a dmesg log output file generated from this app during
3663#	 the execution phase. Create a set of device structures in memory
3664#	 for subsequent formatting in the html output file
3665#	 This call is only for legacy support on kernels where the ftrace
3666#	 data lacks the suspend_resume or device_pm_callbacks trace events.
3667# Arguments:
3668#	 data: an empty Data object (with dmesgtext) obtained from loadKernelLog
3669# Output:
3670#	 The filled Data object
3671def parseKernelLog(data):
3672	phase = 'suspend_runtime'
3673
3674	if(data.fwValid):
3675		sysvals.vprint('Firmware Suspend = %u ns, Firmware Resume = %u ns' % \
3676			(data.fwSuspend, data.fwResume))
3677
3678	# dmesg phase match table
3679	dm = {
3680		'suspend_prepare': ['PM: Syncing filesystems.*'],
3681		        'suspend': ['PM: Entering [a-z]* sleep.*', 'Suspending console.*'],
3682		   'suspend_late': ['PM: suspend of devices complete after.*'],
3683		  'suspend_noirq': ['PM: late suspend of devices complete after.*'],
3684		'suspend_machine': ['PM: noirq suspend of devices complete after.*'],
3685		 'resume_machine': ['ACPI: Low-level resume complete.*'],
3686		   'resume_noirq': ['ACPI: Waking up from system sleep state.*'],
3687		   'resume_early': ['PM: noirq resume of devices complete after.*'],
3688		         'resume': ['PM: early resume of devices complete after.*'],
3689		'resume_complete': ['PM: resume of devices complete after.*'],
3690		    'post_resume': ['.*Restarting tasks \.\.\..*'],
3691	}
3692	if(sysvals.suspendmode == 'standby'):
3693		dm['resume_machine'] = ['PM: Restoring platform NVS memory']
3694	elif(sysvals.suspendmode == 'disk'):
3695		dm['suspend_late'] = ['PM: freeze of devices complete after.*']
3696		dm['suspend_noirq'] = ['PM: late freeze of devices complete after.*']
3697		dm['suspend_machine'] = ['PM: noirq freeze of devices complete after.*']
3698		dm['resume_machine'] = ['PM: Restoring platform NVS memory']
3699		dm['resume_early'] = ['PM: noirq restore of devices complete after.*']
3700		dm['resume'] = ['PM: early restore of devices complete after.*']
3701		dm['resume_complete'] = ['PM: restore of devices complete after.*']
3702	elif(sysvals.suspendmode == 'freeze'):
3703		dm['resume_machine'] = ['ACPI: resume from mwait']
3704
3705	# action table (expected events that occur and show up in dmesg)
3706	at = {
3707		'sync_filesystems': {
3708			'smsg': 'PM: Syncing filesystems.*',
3709			'emsg': 'PM: Preparing system for mem sleep.*' },
3710		'freeze_user_processes': {
3711			'smsg': 'Freezing user space processes .*',
3712			'emsg': 'Freezing remaining freezable tasks.*' },
3713		'freeze_tasks': {
3714			'smsg': 'Freezing remaining freezable tasks.*',
3715			'emsg': 'PM: Entering (?P<mode>[a-z,A-Z]*) sleep.*' },
3716		'ACPI prepare': {
3717			'smsg': 'ACPI: Preparing to enter system sleep state.*',
3718			'emsg': 'PM: Saving platform NVS memory.*' },
3719		'PM vns': {
3720			'smsg': 'PM: Saving platform NVS memory.*',
3721			'emsg': 'Disabling non-boot CPUs .*' },
3722	}
3723
3724	t0 = -1.0
3725	cpu_start = -1.0
3726	prevktime = -1.0
3727	actions = dict()
3728	for line in data.dmesgtext:
3729		# parse each dmesg line into the time and message
3730		m = re.match('[ \t]*(\[ *)(?P<ktime>[0-9\.]*)(\]) (?P<msg>.*)', line)
3731		if(m):
3732			val = m.group('ktime')
3733			try:
3734				ktime = float(val)
3735			except:
3736				continue
3737			msg = m.group('msg')
3738			# initialize data start to first line time
3739			if t0 < 0:
3740				data.setStart(ktime)
3741				t0 = ktime
3742		else:
3743			continue
3744
3745		# check for a phase change line
3746		phasechange = False
3747		for p in dm:
3748			for s in dm[p]:
3749				if(re.match(s, msg)):
3750					phasechange, phase = True, p
3751					break
3752
3753		# hack for determining resume_machine end for freeze
3754		if(not sysvals.usetraceevents and sysvals.suspendmode == 'freeze' \
3755			and phase == 'resume_machine' and \
3756			re.match('calling  (?P<f>.*)\+ @ .*, parent: .*', msg)):
3757			data.setPhase(phase, ktime, False)
3758			phase = 'resume_noirq'
3759			data.setPhase(phase, ktime, True)
3760
3761		if phasechange:
3762			if phase == 'suspend_prepare':
3763				data.setPhase(phase, ktime, True)
3764				data.setStart(ktime)
3765				data.tKernSus = ktime
3766			elif phase == 'suspend':
3767				lp = data.lastPhase()
3768				if lp:
3769					data.setPhase(lp, ktime, False)
3770				data.setPhase(phase, ktime, True)
3771			elif phase == 'suspend_late':
3772				lp = data.lastPhase()
3773				if lp:
3774					data.setPhase(lp, ktime, False)
3775				data.setPhase(phase, ktime, True)
3776			elif phase == 'suspend_noirq':
3777				lp = data.lastPhase()
3778				if lp:
3779					data.setPhase(lp, ktime, False)
3780				data.setPhase(phase, ktime, True)
3781			elif phase == 'suspend_machine':
3782				lp = data.lastPhase()
3783				if lp:
3784					data.setPhase(lp, ktime, False)
3785				data.setPhase(phase, ktime, True)
3786			elif phase == 'resume_machine':
3787				lp = data.lastPhase()
3788				if(sysvals.suspendmode in ['freeze', 'standby']):
3789					data.tSuspended = prevktime
3790					if lp:
3791						data.setPhase(lp, prevktime, False)
3792				else:
3793					data.tSuspended = ktime
3794					if lp:
3795						data.setPhase(lp, prevktime, False)
3796				data.tResumed = ktime
3797				data.setPhase(phase, ktime, True)
3798			elif phase == 'resume_noirq':
3799				lp = data.lastPhase()
3800				if lp:
3801					data.setPhase(lp, ktime, False)
3802				data.setPhase(phase, ktime, True)
3803			elif phase == 'resume_early':
3804				lp = data.lastPhase()
3805				if lp:
3806					data.setPhase(lp, ktime, False)
3807				data.setPhase(phase, ktime, True)
3808			elif phase == 'resume':
3809				lp = data.lastPhase()
3810				if lp:
3811					data.setPhase(lp, ktime, False)
3812				data.setPhase(phase, ktime, True)
3813			elif phase == 'resume_complete':
3814				lp = data.lastPhase()
3815				if lp:
3816					data.setPhase(lp, ktime, False)
3817				data.setPhase(phase, ktime, True)
3818			elif phase == 'post_resume':
3819				lp = data.lastPhase()
3820				if lp:
3821					data.setPhase(lp, ktime, False)
3822				data.setEnd(ktime)
3823				data.tKernRes = ktime
3824				break
3825
3826		# -- device callbacks --
3827		if(phase in data.sortedPhases()):
3828			# device init call
3829			if(re.match('calling  (?P<f>.*)\+ @ .*, parent: .*', msg)):
3830				sm = re.match('calling  (?P<f>.*)\+ @ '+\
3831					'(?P<n>.*), parent: (?P<p>.*)', msg);
3832				f = sm.group('f')
3833				n = sm.group('n')
3834				p = sm.group('p')
3835				if(f and n and p):
3836					data.newAction(phase, f, int(n), p, ktime, -1, '')
3837			# device init return
3838			elif(re.match('call (?P<f>.*)\+ returned .* after '+\
3839				'(?P<t>.*) usecs', msg)):
3840				sm = re.match('call (?P<f>.*)\+ returned .* after '+\
3841					'(?P<t>.*) usecs(?P<a>.*)', msg);
3842				f = sm.group('f')
3843				t = sm.group('t')
3844				list = data.dmesg[phase]['list']
3845				if(f in list):
3846					dev = list[f]
3847					dev['length'] = int(t)
3848					dev['end'] = ktime
3849
3850		# if trace events are not available, these are better than nothing
3851		if(not sysvals.usetraceevents):
3852			# look for known actions
3853			for a in sorted(at):
3854				if(re.match(at[a]['smsg'], msg)):
3855					if(a not in actions):
3856						actions[a] = []
3857					actions[a].append({'begin': ktime, 'end': ktime})
3858				if(re.match(at[a]['emsg'], msg)):
3859					if(a in actions):
3860						actions[a][-1]['end'] = ktime
3861			# now look for CPU on/off events
3862			if(re.match('Disabling non-boot CPUs .*', msg)):
3863				# start of first cpu suspend
3864				cpu_start = ktime
3865			elif(re.match('Enabling non-boot CPUs .*', msg)):
3866				# start of first cpu resume
3867				cpu_start = ktime
3868			elif(re.match('smpboot: CPU (?P<cpu>[0-9]*) is now offline', msg)):
3869				# end of a cpu suspend, start of the next
3870				m = re.match('smpboot: CPU (?P<cpu>[0-9]*) is now offline', msg)
3871				cpu = 'CPU'+m.group('cpu')
3872				if(cpu not in actions):
3873					actions[cpu] = []
3874				actions[cpu].append({'begin': cpu_start, 'end': ktime})
3875				cpu_start = ktime
3876			elif(re.match('CPU(?P<cpu>[0-9]*) is up', msg)):
3877				# end of a cpu resume, start of the next
3878				m = re.match('CPU(?P<cpu>[0-9]*) is up', msg)
3879				cpu = 'CPU'+m.group('cpu')
3880				if(cpu not in actions):
3881					actions[cpu] = []
3882				actions[cpu].append({'begin': cpu_start, 'end': ktime})
3883				cpu_start = ktime
3884		prevktime = ktime
3885	data.initDevicegroups()
3886
3887	# fill in any missing phases
3888	phasedef = data.phasedef
3889	terr, lp = '', 'suspend_prepare'
3890	for p in sorted(phasedef, key=lambda k:phasedef[k]['order']):
3891		if p not in data.dmesg:
3892			if not terr:
3893				pprint('TEST FAILED: %s failed in %s phase' % (sysvals.suspendmode, lp))
3894				terr = '%s failed in %s phase' % (sysvals.suspendmode, lp)
3895				if data.tSuspended == 0:
3896					data.tSuspended = data.dmesg[lp]['end']
3897				if data.tResumed == 0:
3898					data.tResumed = data.dmesg[lp]['end']
3899			sysvals.vprint('WARNING: phase "%s" is missing!' % p)
3900		lp = p
3901	lp = data.sortedPhases()[0]
3902	for p in data.sortedPhases():
3903		if(p != lp and not ('machine' in p and 'machine' in lp)):
3904			data.dmesg[lp]['end'] = data.dmesg[p]['start']
3905		lp = p
3906	if data.tSuspended == 0:
3907		data.tSuspended = data.tKernRes
3908	if data.tResumed == 0:
3909		data.tResumed = data.tSuspended
3910
3911	# fill in any actions we've found
3912	for name in sorted(actions):
3913		for event in actions[name]:
3914			data.newActionGlobal(name, event['begin'], event['end'])
3915
3916	if(len(sysvals.devicefilter) > 0):
3917		data.deviceFilter(sysvals.devicefilter)
3918	data.fixupInitcallsThatDidntReturn()
3919	return True
3920
3921def callgraphHTML(sv, hf, num, cg, title, color, devid):
3922	html_func_top = '<article id="{0}" class="atop" style="background:{1}">\n<input type="checkbox" class="pf" id="f{2}" checked/><label for="f{2}">{3} {4}</label>\n'
3923	html_func_start = '<article>\n<input type="checkbox" class="pf" id="f{0}" checked/><label for="f{0}">{1} {2}</label>\n'
3924	html_func_end = '</article>\n'
3925	html_func_leaf = '<article>{0} {1}</article>\n'
3926
3927	cgid = devid
3928	if cg.id:
3929		cgid += cg.id
3930	cglen = (cg.end - cg.start) * 1000
3931	if cglen < sv.mincglen:
3932		return num
3933
3934	fmt = '<r>(%.3f ms @ '+sv.timeformat+' to '+sv.timeformat+')</r>'
3935	flen = fmt % (cglen, cg.start, cg.end)
3936	hf.write(html_func_top.format(cgid, color, num, title, flen))
3937	num += 1
3938	for line in cg.list:
3939		if(line.length < 0.000000001):
3940			flen = ''
3941		else:
3942			fmt = '<n>(%.3f ms @ '+sv.timeformat+')</n>'
3943			flen = fmt % (line.length*1000, line.time)
3944		if line.isLeaf():
3945			hf.write(html_func_leaf.format(line.name, flen))
3946		elif line.freturn:
3947			hf.write(html_func_end)
3948		else:
3949			hf.write(html_func_start.format(num, line.name, flen))
3950			num += 1
3951	hf.write(html_func_end)
3952	return num
3953
3954def addCallgraphs(sv, hf, data):
3955	hf.write('<section id="callgraphs" class="callgraph">\n')
3956	# write out the ftrace data converted to html
3957	num = 0
3958	for p in data.sortedPhases():
3959		if sv.cgphase and p != sv.cgphase:
3960			continue
3961		list = data.dmesg[p]['list']
3962		for devname in data.sortedDevices(p):
3963			if len(sv.cgfilter) > 0 and devname not in sv.cgfilter:
3964				continue
3965			dev = list[devname]
3966			color = 'white'
3967			if 'color' in data.dmesg[p]:
3968				color = data.dmesg[p]['color']
3969			if 'color' in dev:
3970				color = dev['color']
3971			name = devname
3972			if(devname in sv.devprops):
3973				name = sv.devprops[devname].altName(devname)
3974			if sv.suspendmode in suspendmodename:
3975				name += ' '+p
3976			if('ftrace' in dev):
3977				cg = dev['ftrace']
3978				if cg.name == sv.ftopfunc:
3979					name = 'top level suspend/resume call'
3980				num = callgraphHTML(sv, hf, num, cg,
3981					name, color, dev['id'])
3982			if('ftraces' in dev):
3983				for cg in dev['ftraces']:
3984					num = callgraphHTML(sv, hf, num, cg,
3985						name+' &rarr; '+cg.name, color, dev['id'])
3986	hf.write('\n\n    </section>\n')
3987
3988def summaryCSS(title, center=True):
3989	tdcenter = 'text-align:center;' if center else ''
3990	out = '<!DOCTYPE html>\n<html>\n<head>\n\
3991	<meta http-equiv="content-type" content="text/html; charset=UTF-8">\n\
3992	<title>'+title+'</title>\n\
3993	<style type=\'text/css\'>\n\
3994		.stamp {width: 100%;text-align:center;background:#888;line-height:30px;color:white;font: 25px Arial;}\n\
3995		table {width:100%;border-collapse: collapse;border:1px solid;}\n\
3996		th {border: 1px solid black;background:#222;color:white;}\n\
3997		td {font: 14px "Times New Roman";'+tdcenter+'}\n\
3998		tr.head td {border: 1px solid black;background:#aaa;}\n\
3999		tr.alt {background-color:#ddd;}\n\
4000		tr.notice {color:red;}\n\
4001		.minval {background-color:#BBFFBB;}\n\
4002		.medval {background-color:#BBBBFF;}\n\
4003		.maxval {background-color:#FFBBBB;}\n\
4004		.head a {color:#000;text-decoration: none;}\n\
4005	</style>\n</head>\n<body>\n'
4006	return out
4007
4008# Function: createHTMLSummarySimple
4009# Description:
4010#	 Create summary html file for a series of tests
4011# Arguments:
4012#	 testruns: array of Data objects from parseTraceLog
4013def createHTMLSummarySimple(testruns, htmlfile, title):
4014	# write the html header first (html head, css code, up to body start)
4015	html = summaryCSS('Summary - SleepGraph')
4016
4017	# extract the test data into list
4018	list = dict()
4019	tAvg, tMin, tMax, tMed = [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [dict(), dict()]
4020	iMin, iMed, iMax = [0, 0], [0, 0], [0, 0]
4021	num = 0
4022	useturbo = usewifi = False
4023	lastmode = ''
4024	cnt = dict()
4025	for data in sorted(testruns, key=lambda v:(v['mode'], v['host'], v['kernel'], v['time'])):
4026		mode = data['mode']
4027		if mode not in list:
4028			list[mode] = {'data': [], 'avg': [0,0], 'min': [0,0], 'max': [0,0], 'med': [0,0]}
4029		if lastmode and lastmode != mode and num > 0:
4030			for i in range(2):
4031				s = sorted(tMed[i])
4032				list[lastmode]['med'][i] = s[int(len(s)//2)]
4033				iMed[i] = tMed[i][list[lastmode]['med'][i]]
4034			list[lastmode]['avg'] = [tAvg[0] / num, tAvg[1] / num]
4035			list[lastmode]['min'] = tMin
4036			list[lastmode]['max'] = tMax
4037			list[lastmode]['idx'] = (iMin, iMed, iMax)
4038			tAvg, tMin, tMax, tMed = [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [dict(), dict()]
4039			iMin, iMed, iMax = [0, 0], [0, 0], [0, 0]
4040			num = 0
4041		pkgpc10 = syslpi = wifi = ''
4042		if 'pkgpc10' in data and 'syslpi' in data:
4043			pkgpc10, syslpi, useturbo = data['pkgpc10'], data['syslpi'], True
4044		if 'wifi' in data:
4045			wifi, usewifi = data['wifi'], True
4046		res = data['result']
4047		tVal = [float(data['suspend']), float(data['resume'])]
4048		list[mode]['data'].append([data['host'], data['kernel'],
4049			data['time'], tVal[0], tVal[1], data['url'], res,
4050			data['issues'], data['sus_worst'], data['sus_worsttime'],
4051			data['res_worst'], data['res_worsttime'], pkgpc10, syslpi, wifi])
4052		idx = len(list[mode]['data']) - 1
4053		if res.startswith('fail in'):
4054			res = 'fail'
4055		if res not in cnt:
4056			cnt[res] = 1
4057		else:
4058			cnt[res] += 1
4059		if res == 'pass':
4060			for i in range(2):
4061				tMed[i][tVal[i]] = idx
4062				tAvg[i] += tVal[i]
4063				if tMin[i] == 0 or tVal[i] < tMin[i]:
4064					iMin[i] = idx
4065					tMin[i] = tVal[i]
4066				if tMax[i] == 0 or tVal[i] > tMax[i]:
4067					iMax[i] = idx
4068					tMax[i] = tVal[i]
4069			num += 1
4070		lastmode = mode
4071	if lastmode and num > 0:
4072		for i in range(2):
4073			s = sorted(tMed[i])
4074			list[lastmode]['med'][i] = s[int(len(s)//2)]
4075			iMed[i] = tMed[i][list[lastmode]['med'][i]]
4076		list[lastmode]['avg'] = [tAvg[0] / num, tAvg[1] / num]
4077		list[lastmode]['min'] = tMin
4078		list[lastmode]['max'] = tMax
4079		list[lastmode]['idx'] = (iMin, iMed, iMax)
4080
4081	# group test header
4082	desc = []
4083	for ilk in sorted(cnt, reverse=True):
4084		if cnt[ilk] > 0:
4085			desc.append('%d %s' % (cnt[ilk], ilk))
4086	html += '<div class="stamp">%s (%d tests: %s)</div>\n' % (title, len(testruns), ', '.join(desc))
4087	th = '\t<th>{0}</th>\n'
4088	td = '\t<td>{0}</td>\n'
4089	tdh = '\t<td{1}>{0}</td>\n'
4090	tdlink = '\t<td><a href="{0}">html</a></td>\n'
4091	cols = 12
4092	if useturbo:
4093		cols += 2
4094	if usewifi:
4095		cols += 1
4096	colspan = '%d' % cols
4097
4098	# table header
4099	html += '<table>\n<tr>\n' + th.format('#') +\
4100		th.format('Mode') + th.format('Host') + th.format('Kernel') +\
4101		th.format('Test Time') + th.format('Result') + th.format('Issues') +\
4102		th.format('Suspend') + th.format('Resume') +\
4103		th.format('Worst Suspend Device') + th.format('SD Time') +\
4104		th.format('Worst Resume Device') + th.format('RD Time')
4105	if useturbo:
4106		html += th.format('PkgPC10') + th.format('SysLPI')
4107	if usewifi:
4108		html += th.format('Wifi')
4109	html += th.format('Detail')+'</tr>\n'
4110	# export list into html
4111	head = '<tr class="head"><td>{0}</td><td>{1}</td>'+\
4112		'<td colspan='+colspan+' class="sus">Suspend Avg={2} '+\
4113		'<span class=minval><a href="#s{10}min">Min={3}</a></span> '+\
4114		'<span class=medval><a href="#s{10}med">Med={4}</a></span> '+\
4115		'<span class=maxval><a href="#s{10}max">Max={5}</a></span> '+\
4116		'Resume Avg={6} '+\
4117		'<span class=minval><a href="#r{10}min">Min={7}</a></span> '+\
4118		'<span class=medval><a href="#r{10}med">Med={8}</a></span> '+\
4119		'<span class=maxval><a href="#r{10}max">Max={9}</a></span></td>'+\
4120		'</tr>\n'
4121	headnone = '<tr class="head"><td>{0}</td><td>{1}</td><td colspan='+\
4122		colspan+'></td></tr>\n'
4123	for mode in sorted(list):
4124		# header line for each suspend mode
4125		num = 0
4126		tAvg, tMin, tMax, tMed = list[mode]['avg'], list[mode]['min'],\
4127			list[mode]['max'], list[mode]['med']
4128		count = len(list[mode]['data'])
4129		if 'idx' in list[mode]:
4130			iMin, iMed, iMax = list[mode]['idx']
4131			html += head.format('%d' % count, mode.upper(),
4132				'%.3f' % tAvg[0], '%.3f' % tMin[0], '%.3f' % tMed[0], '%.3f' % tMax[0],
4133				'%.3f' % tAvg[1], '%.3f' % tMin[1], '%.3f' % tMed[1], '%.3f' % tMax[1],
4134				mode.lower()
4135			)
4136		else:
4137			iMin = iMed = iMax = [-1, -1, -1]
4138			html += headnone.format('%d' % count, mode.upper())
4139		for d in list[mode]['data']:
4140			# row classes - alternate row color
4141			rcls = ['alt'] if num % 2 == 1 else []
4142			if d[6] != 'pass':
4143				rcls.append('notice')
4144			html += '<tr class="'+(' '.join(rcls))+'">\n' if len(rcls) > 0 else '<tr>\n'
4145			# figure out if the line has sus or res highlighted
4146			idx = list[mode]['data'].index(d)
4147			tHigh = ['', '']
4148			for i in range(2):
4149				tag = 's%s' % mode if i == 0 else 'r%s' % mode
4150				if idx == iMin[i]:
4151					tHigh[i] = ' id="%smin" class=minval title="Minimum"' % tag
4152				elif idx == iMax[i]:
4153					tHigh[i] = ' id="%smax" class=maxval title="Maximum"' % tag
4154				elif idx == iMed[i]:
4155					tHigh[i] = ' id="%smed" class=medval title="Median"' % tag
4156			html += td.format("%d" % (list[mode]['data'].index(d) + 1)) # row
4157			html += td.format(mode)										# mode
4158			html += td.format(d[0])										# host
4159			html += td.format(d[1])										# kernel
4160			html += td.format(d[2])										# time
4161			html += td.format(d[6])										# result
4162			html += td.format(d[7])										# issues
4163			html += tdh.format('%.3f ms' % d[3], tHigh[0]) if d[3] else td.format('')	# suspend
4164			html += tdh.format('%.3f ms' % d[4], tHigh[1]) if d[4] else td.format('')	# resume
4165			html += td.format(d[8])										# sus_worst
4166			html += td.format('%.3f ms' % d[9])	if d[9] else td.format('')		# sus_worst time
4167			html += td.format(d[10])									# res_worst
4168			html += td.format('%.3f ms' % d[11]) if d[11] else td.format('')	# res_worst time
4169			if useturbo:
4170				html += td.format(d[12])								# pkg_pc10
4171				html += td.format(d[13])								# syslpi
4172			if usewifi:
4173				html += td.format(d[14])								# wifi
4174			html += tdlink.format(d[5]) if d[5] else td.format('')		# url
4175			html += '</tr>\n'
4176			num += 1
4177
4178	# flush the data to file
4179	hf = open(htmlfile, 'w')
4180	hf.write(html+'</table>\n</body>\n</html>\n')
4181	hf.close()
4182
4183def createHTMLDeviceSummary(testruns, htmlfile, title):
4184	html = summaryCSS('Device Summary - SleepGraph', False)
4185
4186	# create global device list from all tests
4187	devall = dict()
4188	for data in testruns:
4189		host, url, devlist = data['host'], data['url'], data['devlist']
4190		for type in devlist:
4191			if type not in devall:
4192				devall[type] = dict()
4193			mdevlist, devlist = devall[type], data['devlist'][type]
4194			for name in devlist:
4195				length = devlist[name]
4196				if name not in mdevlist:
4197					mdevlist[name] = {'name': name, 'host': host,
4198						'worst': length, 'total': length, 'count': 1,
4199						'url': url}
4200				else:
4201					if length > mdevlist[name]['worst']:
4202						mdevlist[name]['worst'] = length
4203						mdevlist[name]['url'] = url
4204						mdevlist[name]['host'] = host
4205					mdevlist[name]['total'] += length
4206					mdevlist[name]['count'] += 1
4207
4208	# generate the html
4209	th = '\t<th>{0}</th>\n'
4210	td = '\t<td align=center>{0}</td>\n'
4211	tdr = '\t<td align=right>{0}</td>\n'
4212	tdlink = '\t<td align=center><a href="{0}">html</a></td>\n'
4213	limit = 1
4214	for type in sorted(devall, reverse=True):
4215		num = 0
4216		devlist = devall[type]
4217		# table header
4218		html += '<div class="stamp">%s (%s devices > %d ms)</div><table>\n' % \
4219			(title, type.upper(), limit)
4220		html += '<tr>\n' + '<th align=right>Device Name</th>' +\
4221			th.format('Average Time') + th.format('Count') +\
4222			th.format('Worst Time') + th.format('Host (worst time)') +\
4223			th.format('Link (worst time)') + '</tr>\n'
4224		for name in sorted(devlist, key=lambda k:(devlist[k]['worst'], \
4225			devlist[k]['total'], devlist[k]['name']), reverse=True):
4226			data = devall[type][name]
4227			data['average'] = data['total'] / data['count']
4228			if data['average'] < limit:
4229				continue
4230			# row classes - alternate row color
4231			rcls = ['alt'] if num % 2 == 1 else []
4232			html += '<tr class="'+(' '.join(rcls))+'">\n' if len(rcls) > 0 else '<tr>\n'
4233			html += tdr.format(data['name'])				# name
4234			html += td.format('%.3f ms' % data['average'])	# average
4235			html += td.format(data['count'])				# count
4236			html += td.format('%.3f ms' % data['worst'])	# worst
4237			html += td.format(data['host'])					# host
4238			html += tdlink.format(data['url'])				# url
4239			html += '</tr>\n'
4240			num += 1
4241		html += '</table>\n'
4242
4243	# flush the data to file
4244	hf = open(htmlfile, 'w')
4245	hf.write(html+'</body>\n</html>\n')
4246	hf.close()
4247	return devall
4248
4249def createHTMLIssuesSummary(testruns, issues, htmlfile, title, extra=''):
4250	multihost = len([e for e in issues if len(e['urls']) > 1]) > 0
4251	html = summaryCSS('Issues Summary - SleepGraph', False)
4252	total = len(testruns)
4253
4254	# generate the html
4255	th = '\t<th>{0}</th>\n'
4256	td = '\t<td align={0}>{1}</td>\n'
4257	tdlink = '<a href="{1}">{0}</a>'
4258	subtitle = '%d issues' % len(issues) if len(issues) > 0 else 'no issues'
4259	html += '<div class="stamp">%s (%s)</div><table>\n' % (title, subtitle)
4260	html += '<tr>\n' + th.format('Issue') + th.format('Count')
4261	if multihost:
4262		html += th.format('Hosts')
4263	html += th.format('Tests') + th.format('Fail Rate') +\
4264		th.format('First Instance') + '</tr>\n'
4265
4266	num = 0
4267	for e in sorted(issues, key=lambda v:v['count'], reverse=True):
4268		testtotal = 0
4269		links = []
4270		for host in sorted(e['urls']):
4271			links.append(tdlink.format(host, e['urls'][host][0]))
4272			testtotal += len(e['urls'][host])
4273		rate = '%d/%d (%.2f%%)' % (testtotal, total, 100*float(testtotal)/float(total))
4274		# row classes - alternate row color
4275		rcls = ['alt'] if num % 2 == 1 else []
4276		html += '<tr class="'+(' '.join(rcls))+'">\n' if len(rcls) > 0 else '<tr>\n'
4277		html += td.format('left', e['line'])		# issue
4278		html += td.format('center', e['count'])		# count
4279		if multihost:
4280			html += td.format('center', len(e['urls']))	# hosts
4281		html += td.format('center', testtotal)		# test count
4282		html += td.format('center', rate)			# test rate
4283		html += td.format('center nowrap', '<br>'.join(links))	# links
4284		html += '</tr>\n'
4285		num += 1
4286
4287	# flush the data to file
4288	hf = open(htmlfile, 'w')
4289	hf.write(html+'</table>\n'+extra+'</body>\n</html>\n')
4290	hf.close()
4291	return issues
4292
4293def ordinal(value):
4294	suffix = 'th'
4295	if value < 10 or value > 19:
4296		if value % 10 == 1:
4297			suffix = 'st'
4298		elif value % 10 == 2:
4299			suffix = 'nd'
4300		elif value % 10 == 3:
4301			suffix = 'rd'
4302	return '%d%s' % (value, suffix)
4303
4304# Function: createHTML
4305# Description:
4306#	 Create the output html file from the resident test data
4307# Arguments:
4308#	 testruns: array of Data objects from parseKernelLog or parseTraceLog
4309# Output:
4310#	 True if the html file was created, false if it failed
4311def createHTML(testruns, testfail):
4312	if len(testruns) < 1:
4313		pprint('ERROR: Not enough test data to build a timeline')
4314		return
4315
4316	kerror = False
4317	for data in testruns:
4318		if data.kerror:
4319			kerror = True
4320		if(sysvals.suspendmode in ['freeze', 'standby']):
4321			data.trimFreezeTime(testruns[-1].tSuspended)
4322		else:
4323			data.getMemTime()
4324
4325	# html function templates
4326	html_error = '<div id="{1}" title="kernel error/warning" class="err" style="right:{0}%">{2}&rarr;</div>\n'
4327	html_traceevent = '<div title="{0}" class="traceevent{6}" style="left:{1}%;top:{2}px;height:{3}px;width:{4}%;line-height:{3}px;{7}">{5}</div>\n'
4328	html_cpuexec = '<div class="jiffie" style="left:{0}%;top:{1}px;height:{2}px;width:{3}%;background:{4};"></div>\n'
4329	html_timetotal = '<table class="time1">\n<tr>'\
4330		'<td class="green" title="{3}">{2} Suspend Time: <b>{0} ms</b></td>'\
4331		'<td class="yellow" title="{4}">{2} Resume Time: <b>{1} ms</b></td>'\
4332		'</tr>\n</table>\n'
4333	html_timetotal2 = '<table class="time1">\n<tr>'\
4334		'<td class="green" title="{4}">{3} Suspend Time: <b>{0} ms</b></td>'\
4335		'<td class="gray" title="time spent in low-power mode with clock running">'+sysvals.suspendmode+' time: <b>{1} ms</b></td>'\
4336		'<td class="yellow" title="{5}">{3} Resume Time: <b>{2} ms</b></td>'\
4337		'</tr>\n</table>\n'
4338	html_timetotal3 = '<table class="time1">\n<tr>'\
4339		'<td class="green">Execution Time: <b>{0} ms</b></td>'\
4340		'<td class="yellow">Command: <b>{1}</b></td>'\
4341		'</tr>\n</table>\n'
4342	html_fail = '<table class="testfail"><tr><td>{0}</td></tr></table>\n'
4343	html_kdesc = '<td class="{3}" title="time spent in kernel execution">{0}Kernel {2}: {1} ms</td>'
4344	html_fwdesc = '<td class="{3}" title="time spent in firmware">{0}Firmware {2}: {1} ms</td>'
4345	html_wifdesc = '<td class="yellow" title="time for wifi to reconnect after resume complete ({2})">{0}Wifi Resume: {1}</td>'
4346
4347	# html format variables
4348	scaleH = 20
4349	if kerror:
4350		scaleH = 40
4351
4352	# device timeline
4353	devtl = Timeline(30, scaleH)
4354
4355	# write the test title and general info header
4356	devtl.createHeader(sysvals, testruns[0].stamp)
4357
4358	# Generate the header for this timeline
4359	for data in testruns:
4360		tTotal = data.end - data.start
4361		if(tTotal == 0):
4362			doError('No timeline data')
4363		if sysvals.suspendmode == 'command':
4364			run_time = '%.0f' % (tTotal * 1000)
4365			if sysvals.testcommand:
4366				testdesc = sysvals.testcommand
4367			else:
4368				testdesc = 'unknown'
4369			if(len(testruns) > 1):
4370				testdesc = ordinal(data.testnumber+1)+' '+testdesc
4371			thtml = html_timetotal3.format(run_time, testdesc)
4372			devtl.html += thtml
4373			continue
4374		# typical full suspend/resume header
4375		stot, rtot = sktime, rktime = data.getTimeValues()
4376		ssrc, rsrc, testdesc, testdesc2 = ['kernel'], ['kernel'], 'Kernel', ''
4377		if data.fwValid:
4378			stot += (data.fwSuspend/1000000.0)
4379			rtot += (data.fwResume/1000000.0)
4380			ssrc.append('firmware')
4381			rsrc.append('firmware')
4382			testdesc = 'Total'
4383		if 'time' in data.wifi and data.wifi['stat'] != 'timeout':
4384			rtot += data.end - data.tKernRes + (data.wifi['time'] * 1000.0)
4385			rsrc.append('wifi')
4386			testdesc = 'Total'
4387		suspend_time, resume_time = '%.3f' % stot, '%.3f' % rtot
4388		stitle = 'time from kernel suspend start to %s mode [%s time]' % \
4389			(sysvals.suspendmode, ' & '.join(ssrc))
4390		rtitle = 'time from %s mode to kernel resume complete [%s time]' % \
4391			(sysvals.suspendmode, ' & '.join(rsrc))
4392		if(len(testruns) > 1):
4393			testdesc = testdesc2 = ordinal(data.testnumber+1)
4394			testdesc2 += ' '
4395		if(len(data.tLow) == 0):
4396			thtml = html_timetotal.format(suspend_time, \
4397				resume_time, testdesc, stitle, rtitle)
4398		else:
4399			low_time = '+'.join(data.tLow)
4400			thtml = html_timetotal2.format(suspend_time, low_time, \
4401				resume_time, testdesc, stitle, rtitle)
4402		devtl.html += thtml
4403		if not data.fwValid and 'dev' not in data.wifi:
4404			continue
4405		# extra detail when the times come from multiple sources
4406		thtml = '<table class="time2">\n<tr>'
4407		thtml += html_kdesc.format(testdesc2, '%.3f'%sktime, 'Suspend', 'green')
4408		if data.fwValid:
4409			sftime = '%.3f'%(data.fwSuspend / 1000000.0)
4410			rftime = '%.3f'%(data.fwResume / 1000000.0)
4411			thtml += html_fwdesc.format(testdesc2, sftime, 'Suspend', 'green')
4412			thtml += html_fwdesc.format(testdesc2, rftime, 'Resume', 'yellow')
4413		thtml += html_kdesc.format(testdesc2, '%.3f'%rktime, 'Resume', 'yellow')
4414		if 'time' in data.wifi:
4415			if data.wifi['stat'] != 'timeout':
4416				wtime = '%.0f ms'%(data.end - data.tKernRes + (data.wifi['time'] * 1000.0))
4417			else:
4418				wtime = 'TIMEOUT'
4419			thtml += html_wifdesc.format(testdesc2, wtime, data.wifi['dev'])
4420		thtml += '</tr>\n</table>\n'
4421		devtl.html += thtml
4422	if testfail:
4423		devtl.html += html_fail.format(testfail)
4424
4425	# time scale for potentially multiple datasets
4426	t0 = testruns[0].start
4427	tMax = testruns[-1].end
4428	tTotal = tMax - t0
4429
4430	# determine the maximum number of rows we need to draw
4431	fulllist = []
4432	threadlist = []
4433	pscnt = 0
4434	devcnt = 0
4435	for data in testruns:
4436		data.selectTimelineDevices('%f', tTotal, sysvals.mindevlen)
4437		for group in data.devicegroups:
4438			devlist = []
4439			for phase in group:
4440				for devname in sorted(data.tdevlist[phase]):
4441					d = DevItem(data.testnumber, phase, data.dmesg[phase]['list'][devname])
4442					devlist.append(d)
4443					if d.isa('kth'):
4444						threadlist.append(d)
4445					else:
4446						if d.isa('ps'):
4447							pscnt += 1
4448						else:
4449							devcnt += 1
4450						fulllist.append(d)
4451			if sysvals.mixedphaseheight:
4452				devtl.getPhaseRows(devlist)
4453	if not sysvals.mixedphaseheight:
4454		if len(threadlist) > 0 and len(fulllist) > 0:
4455			if pscnt > 0 and devcnt > 0:
4456				msg = 'user processes & device pm callbacks'
4457			elif pscnt > 0:
4458				msg = 'user processes'
4459			else:
4460				msg = 'device pm callbacks'
4461			d = testruns[0].addHorizontalDivider(msg, testruns[-1].end)
4462			fulllist.insert(0, d)
4463		devtl.getPhaseRows(fulllist)
4464		if len(threadlist) > 0:
4465			d = testruns[0].addHorizontalDivider('asynchronous kernel threads', testruns[-1].end)
4466			threadlist.insert(0, d)
4467			devtl.getPhaseRows(threadlist, devtl.rows)
4468	devtl.calcTotalRows()
4469
4470	# draw the full timeline
4471	devtl.createZoomBox(sysvals.suspendmode, len(testruns))
4472	for data in testruns:
4473		# draw each test run and block chronologically
4474		phases = {'suspend':[],'resume':[]}
4475		for phase in data.sortedPhases():
4476			if data.dmesg[phase]['start'] >= data.tSuspended:
4477				phases['resume'].append(phase)
4478			else:
4479				phases['suspend'].append(phase)
4480		# now draw the actual timeline blocks
4481		for dir in phases:
4482			# draw suspend and resume blocks separately
4483			bname = '%s%d' % (dir[0], data.testnumber)
4484			if dir == 'suspend':
4485				m0 = data.start
4486				mMax = data.tSuspended
4487				left = '%f' % (((m0-t0)*100.0)/tTotal)
4488			else:
4489				m0 = data.tSuspended
4490				mMax = data.end
4491				# in an x2 run, remove any gap between blocks
4492				if len(testruns) > 1 and data.testnumber == 0:
4493					mMax = testruns[1].start
4494				left = '%f' % ((((m0-t0)*100.0)+sysvals.srgap/2)/tTotal)
4495			mTotal = mMax - m0
4496			# if a timeline block is 0 length, skip altogether
4497			if mTotal == 0:
4498				continue
4499			width = '%f' % (((mTotal*100.0)-sysvals.srgap/2)/tTotal)
4500			devtl.html += devtl.html_tblock.format(bname, left, width, devtl.scaleH)
4501			for b in phases[dir]:
4502				# draw the phase color background
4503				phase = data.dmesg[b]
4504				length = phase['end']-phase['start']
4505				left = '%f' % (((phase['start']-m0)*100.0)/mTotal)
4506				width = '%f' % ((length*100.0)/mTotal)
4507				devtl.html += devtl.html_phase.format(left, width, \
4508					'%.3f'%devtl.scaleH, '%.3f'%devtl.bodyH, \
4509					data.dmesg[b]['color'], '')
4510			for e in data.errorinfo[dir]:
4511				# draw red lines for any kernel errors found
4512				type, t, idx1, idx2 = e
4513				id = '%d_%d' % (idx1, idx2)
4514				right = '%f' % (((mMax-t)*100.0)/mTotal)
4515				devtl.html += html_error.format(right, id, type)
4516			for b in phases[dir]:
4517				# draw the devices for this phase
4518				phaselist = data.dmesg[b]['list']
4519				for d in sorted(data.tdevlist[b]):
4520					name = d
4521					drv = ''
4522					dev = phaselist[d]
4523					xtraclass = ''
4524					xtrainfo = ''
4525					xtrastyle = ''
4526					if 'htmlclass' in dev:
4527						xtraclass = dev['htmlclass']
4528					if 'color' in dev:
4529						xtrastyle = 'background:%s;' % dev['color']
4530					if(d in sysvals.devprops):
4531						name = sysvals.devprops[d].altName(d)
4532						xtraclass = sysvals.devprops[d].xtraClass()
4533						xtrainfo = sysvals.devprops[d].xtraInfo()
4534					elif xtraclass == ' kth':
4535						xtrainfo = ' kernel_thread'
4536					if('drv' in dev and dev['drv']):
4537						drv = ' {%s}' % dev['drv']
4538					rowheight = devtl.phaseRowHeight(data.testnumber, b, dev['row'])
4539					rowtop = devtl.phaseRowTop(data.testnumber, b, dev['row'])
4540					top = '%.3f' % (rowtop + devtl.scaleH)
4541					left = '%f' % (((dev['start']-m0)*100)/mTotal)
4542					width = '%f' % (((dev['end']-dev['start'])*100)/mTotal)
4543					length = ' (%0.3f ms) ' % ((dev['end']-dev['start'])*1000)
4544					title = name+drv+xtrainfo+length
4545					if sysvals.suspendmode == 'command':
4546						title += sysvals.testcommand
4547					elif xtraclass == ' ps':
4548						if 'suspend' in b:
4549							title += 'pre_suspend_process'
4550						else:
4551							title += 'post_resume_process'
4552					else:
4553						title += b
4554					devtl.html += devtl.html_device.format(dev['id'], \
4555						title, left, top, '%.3f'%rowheight, width, \
4556						d+drv, xtraclass, xtrastyle)
4557					if('cpuexec' in dev):
4558						for t in sorted(dev['cpuexec']):
4559							start, end = t
4560							j = float(dev['cpuexec'][t]) / 5
4561							if j > 1.0:
4562								j = 1.0
4563							height = '%.3f' % (rowheight/3)
4564							top = '%.3f' % (rowtop + devtl.scaleH + 2*rowheight/3)
4565							left = '%f' % (((start-m0)*100)/mTotal)
4566							width = '%f' % ((end-start)*100/mTotal)
4567							color = 'rgba(255, 0, 0, %f)' % j
4568							devtl.html += \
4569								html_cpuexec.format(left, top, height, width, color)
4570					if('src' not in dev):
4571						continue
4572					# draw any trace events for this device
4573					for e in dev['src']:
4574						height = '%.3f' % devtl.rowH
4575						top = '%.3f' % (rowtop + devtl.scaleH + (e.row*devtl.rowH))
4576						left = '%f' % (((e.time-m0)*100)/mTotal)
4577						width = '%f' % (e.length*100/mTotal)
4578						xtrastyle = ''
4579						if e.color:
4580							xtrastyle = 'background:%s;' % e.color
4581						devtl.html += \
4582							html_traceevent.format(e.title(), \
4583								left, top, height, width, e.text(), '', xtrastyle)
4584			# draw the time scale, try to make the number of labels readable
4585			devtl.createTimeScale(m0, mMax, tTotal, dir)
4586			devtl.html += '</div>\n'
4587
4588	# timeline is finished
4589	devtl.html += '</div>\n</div>\n'
4590
4591	# draw a legend which describes the phases by color
4592	if sysvals.suspendmode != 'command':
4593		phasedef = testruns[-1].phasedef
4594		devtl.html += '<div class="legend">\n'
4595		pdelta = 100.0/len(phasedef.keys())
4596		pmargin = pdelta / 4.0
4597		for phase in sorted(phasedef, key=lambda k:phasedef[k]['order']):
4598			id, p = '', phasedef[phase]
4599			for word in phase.split('_'):
4600				id += word[0]
4601			order = '%.2f' % ((p['order'] * pdelta) + pmargin)
4602			name = phase.replace('_', ' &nbsp;')
4603			devtl.html += devtl.html_legend.format(order, p['color'], name, id)
4604		devtl.html += '</div>\n'
4605
4606	hf = open(sysvals.htmlfile, 'w')
4607	addCSS(hf, sysvals, len(testruns), kerror)
4608
4609	# write the device timeline
4610	hf.write(devtl.html)
4611	hf.write('<div id="devicedetailtitle"></div>\n')
4612	hf.write('<div id="devicedetail" style="display:none;">\n')
4613	# draw the colored boxes for the device detail section
4614	for data in testruns:
4615		hf.write('<div id="devicedetail%d">\n' % data.testnumber)
4616		pscolor = 'linear-gradient(to top left, #ccc, #eee)'
4617		hf.write(devtl.html_phaselet.format('pre_suspend_process', \
4618			'0', '0', pscolor))
4619		for b in data.sortedPhases():
4620			phase = data.dmesg[b]
4621			length = phase['end']-phase['start']
4622			left = '%.3f' % (((phase['start']-t0)*100.0)/tTotal)
4623			width = '%.3f' % ((length*100.0)/tTotal)
4624			hf.write(devtl.html_phaselet.format(b, left, width, \
4625				data.dmesg[b]['color']))
4626		hf.write(devtl.html_phaselet.format('post_resume_process', \
4627			'0', '0', pscolor))
4628		if sysvals.suspendmode == 'command':
4629			hf.write(devtl.html_phaselet.format('cmdexec', '0', '0', pscolor))
4630		hf.write('</div>\n')
4631	hf.write('</div>\n')
4632
4633	# write the ftrace data (callgraph)
4634	if sysvals.cgtest >= 0 and len(testruns) > sysvals.cgtest:
4635		data = testruns[sysvals.cgtest]
4636	else:
4637		data = testruns[-1]
4638	if sysvals.usecallgraph:
4639		addCallgraphs(sysvals, hf, data)
4640
4641	# add the test log as a hidden div
4642	if sysvals.testlog and sysvals.logmsg:
4643		hf.write('<div id="testlog" style="display:none;">\n'+sysvals.logmsg+'</div>\n')
4644	# add the dmesg log as a hidden div
4645	if sysvals.dmesglog and sysvals.dmesgfile:
4646		hf.write('<div id="dmesglog" style="display:none;">\n')
4647		lf = sysvals.openlog(sysvals.dmesgfile, 'r')
4648		for line in lf:
4649			line = line.replace('<', '&lt').replace('>', '&gt')
4650			hf.write(line)
4651		lf.close()
4652		hf.write('</div>\n')
4653	# add the ftrace log as a hidden div
4654	if sysvals.ftracelog and sysvals.ftracefile:
4655		hf.write('<div id="ftracelog" style="display:none;">\n')
4656		lf = sysvals.openlog(sysvals.ftracefile, 'r')
4657		for line in lf:
4658			hf.write(line)
4659		lf.close()
4660		hf.write('</div>\n')
4661
4662	# write the footer and close
4663	addScriptCode(hf, testruns)
4664	hf.write('</body>\n</html>\n')
4665	hf.close()
4666	return True
4667
4668def addCSS(hf, sv, testcount=1, kerror=False, extra=''):
4669	kernel = sv.stamp['kernel']
4670	host = sv.hostname[0].upper()+sv.hostname[1:]
4671	mode = sv.suspendmode
4672	if sv.suspendmode in suspendmodename:
4673		mode = suspendmodename[sv.suspendmode]
4674	title = host+' '+mode+' '+kernel
4675
4676	# various format changes by flags
4677	cgchk = 'checked'
4678	cgnchk = 'not(:checked)'
4679	if sv.cgexp:
4680		cgchk = 'not(:checked)'
4681		cgnchk = 'checked'
4682
4683	hoverZ = 'z-index:8;'
4684	if sv.usedevsrc:
4685		hoverZ = ''
4686
4687	devlistpos = 'absolute'
4688	if testcount > 1:
4689		devlistpos = 'relative'
4690
4691	scaleTH = 20
4692	if kerror:
4693		scaleTH = 60
4694
4695	# write the html header first (html head, css code, up to body start)
4696	html_header = '<!DOCTYPE html>\n<html>\n<head>\n\
4697	<meta http-equiv="content-type" content="text/html; charset=UTF-8">\n\
4698	<title>'+title+'</title>\n\
4699	<style type=\'text/css\'>\n\
4700		body {overflow-y:scroll;}\n\
4701		.stamp {width:100%;text-align:center;background:gray;line-height:30px;color:white;font:25px Arial;}\n\
4702		.stamp.sysinfo {font:10px Arial;}\n\
4703		.callgraph {margin-top:30px;box-shadow:5px 5px 20px black;}\n\
4704		.callgraph article * {padding-left:28px;}\n\
4705		h1 {color:black;font:bold 30px Times;}\n\
4706		t0 {color:black;font:bold 30px Times;}\n\
4707		t1 {color:black;font:30px Times;}\n\
4708		t2 {color:black;font:25px Times;}\n\
4709		t3 {color:black;font:20px Times;white-space:nowrap;}\n\
4710		t4 {color:black;font:bold 30px Times;line-height:60px;white-space:nowrap;}\n\
4711		cS {font:bold 13px Times;}\n\
4712		table {width:100%;}\n\
4713		.gray {background:rgba(80,80,80,0.1);}\n\
4714		.green {background:rgba(204,255,204,0.4);}\n\
4715		.purple {background:rgba(128,0,128,0.2);}\n\
4716		.yellow {background:rgba(255,255,204,0.4);}\n\
4717		.blue {background:rgba(169,208,245,0.4);}\n\
4718		.time1 {font:22px Arial;border:1px solid;}\n\
4719		.time2 {font:15px Arial;border-bottom:1px solid;border-left:1px solid;border-right:1px solid;}\n\
4720		.testfail {font:bold 22px Arial;color:red;border:1px dashed;}\n\
4721		td {text-align:center;}\n\
4722		r {color:#500000;font:15px Tahoma;}\n\
4723		n {color:#505050;font:15px Tahoma;}\n\
4724		.tdhl {color:red;}\n\
4725		.hide {display:none;}\n\
4726		.pf {display:none;}\n\
4727		.pf:'+cgchk+' + label {background:url(\'data:image/svg+xml;utf,<?xml version="1.0" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" height="18" width="18" version="1.1"><circle cx="9" cy="9" r="8" stroke="black" stroke-width="1" fill="white"/><rect x="4" y="8" width="10" height="2" style="fill:black;stroke-width:0"/><rect x="8" y="4" width="2" height="10" style="fill:black;stroke-width:0"/></svg>\') no-repeat left center;}\n\
4728		.pf:'+cgnchk+' ~ label {background:url(\'data:image/svg+xml;utf,<?xml version="1.0" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" height="18" width="18" version="1.1"><circle cx="9" cy="9" r="8" stroke="black" stroke-width="1" fill="white"/><rect x="4" y="8" width="10" height="2" style="fill:black;stroke-width:0"/></svg>\') no-repeat left center;}\n\
4729		.pf:'+cgchk+' ~ *:not(:nth-child(2)) {display:none;}\n\
4730		.zoombox {position:relative;width:100%;overflow-x:scroll;-webkit-user-select:none;-moz-user-select:none;user-select:none;}\n\
4731		.timeline {position:relative;font-size:14px;cursor:pointer;width:100%; overflow:hidden;background:linear-gradient(#cccccc, white);}\n\
4732		.thread {position:absolute;height:0%;overflow:hidden;z-index:7;line-height:30px;font-size:14px;border:1px solid;text-align:center;white-space:nowrap;}\n\
4733		.thread.ps {border-radius:3px;background:linear-gradient(to top, #ccc, #eee);}\n\
4734		.thread:hover {background:white;border:1px solid red;'+hoverZ+'}\n\
4735		.thread.sec,.thread.sec:hover {background:black;border:0;color:white;line-height:15px;font-size:10px;}\n\
4736		.hover {background:white;border:1px solid red;'+hoverZ+'}\n\
4737		.hover.sync {background:white;}\n\
4738		.hover.bg,.hover.kth,.hover.sync,.hover.ps {background:white;}\n\
4739		.jiffie {position:absolute;pointer-events: none;z-index:8;}\n\
4740		.traceevent {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\
4741		.traceevent:hover {color:white;font-weight:bold;border:1px solid white;}\n\
4742		.phase {position:absolute;overflow:hidden;border:0px;text-align:center;}\n\
4743		.phaselet {float:left;overflow:hidden;border:0px;text-align:center;min-height:100px;font-size:24px;}\n\
4744		.t {position:absolute;line-height:'+('%d'%scaleTH)+'px;pointer-events:none;top:0;height:100%;border-right:1px solid black;z-index:6;}\n\
4745		.err {position:absolute;top:0%;height:100%;border-right:3px solid red;color:red;font:bold 14px Times;line-height:18px;}\n\
4746		.legend {position:relative; width:100%; height:40px; text-align:center;margin-bottom:20px}\n\
4747		.legend .square {position:absolute;cursor:pointer;top:10px; width:0px;height:20px;border:1px solid;padding-left:20px;}\n\
4748		button {height:40px;width:200px;margin-bottom:20px;margin-top:20px;font-size:24px;}\n\
4749		.btnfmt {position:relative;float:right;height:25px;width:auto;margin-top:3px;margin-bottom:0;font-size:10px;text-align:center;}\n\
4750		.devlist {position:'+devlistpos+';width:190px;}\n\
4751		a:link {color:white;text-decoration:none;}\n\
4752		a:visited {color:white;}\n\
4753		a:hover {color:white;}\n\
4754		a:active {color:white;}\n\
4755		.version {position:relative;float:left;color:white;font-size:10px;line-height:30px;margin-left:10px;}\n\
4756		#devicedetail {min-height:100px;box-shadow:5px 5px 20px black;}\n\
4757		.tblock {position:absolute;height:100%;background:#ddd;}\n\
4758		.tback {position:absolute;width:100%;background:linear-gradient(#ccc, #ddd);}\n\
4759		.bg {z-index:1;}\n\
4760'+extra+'\
4761	</style>\n</head>\n<body>\n'
4762	hf.write(html_header)
4763
4764# Function: addScriptCode
4765# Description:
4766#	 Adds the javascript code to the output html
4767# Arguments:
4768#	 hf: the open html file pointer
4769#	 testruns: array of Data objects from parseKernelLog or parseTraceLog
4770def addScriptCode(hf, testruns):
4771	t0 = testruns[0].start * 1000
4772	tMax = testruns[-1].end * 1000
4773	# create an array in javascript memory with the device details
4774	detail = '	var devtable = [];\n'
4775	for data in testruns:
4776		topo = data.deviceTopology()
4777		detail += '	devtable[%d] = "%s";\n' % (data.testnumber, topo)
4778	detail += '	var bounds = [%f,%f];\n' % (t0, tMax)
4779	# add the code which will manipulate the data in the browser
4780	script_code = \
4781	'<script type="text/javascript">\n'+detail+\
4782	'	var resolution = -1;\n'\
4783	'	var dragval = [0, 0];\n'\
4784	'	function redrawTimescale(t0, tMax, tS) {\n'\
4785	'		var rline = \'<div class="t" style="left:0;border-left:1px solid black;border-right:0;">\';\n'\
4786	'		var tTotal = tMax - t0;\n'\
4787	'		var list = document.getElementsByClassName("tblock");\n'\
4788	'		for (var i = 0; i < list.length; i++) {\n'\
4789	'			var timescale = list[i].getElementsByClassName("timescale")[0];\n'\
4790	'			var m0 = t0 + (tTotal*parseFloat(list[i].style.left)/100);\n'\
4791	'			var mTotal = tTotal*parseFloat(list[i].style.width)/100;\n'\
4792	'			var mMax = m0 + mTotal;\n'\
4793	'			var html = "";\n'\
4794	'			var divTotal = Math.floor(mTotal/tS) + 1;\n'\
4795	'			if(divTotal > 1000) continue;\n'\
4796	'			var divEdge = (mTotal - tS*(divTotal-1))*100/mTotal;\n'\
4797	'			var pos = 0.0, val = 0.0;\n'\
4798	'			for (var j = 0; j < divTotal; j++) {\n'\
4799	'				var htmlline = "";\n'\
4800	'				var mode = list[i].id[5];\n'\
4801	'				if(mode == "s") {\n'\
4802	'					pos = 100 - (((j)*tS*100)/mTotal) - divEdge;\n'\
4803	'					val = (j-divTotal+1)*tS;\n'\
4804	'					if(j == divTotal - 1)\n'\
4805	'						htmlline = \'<div class="t" style="right:\'+pos+\'%"><cS>S&rarr;</cS></div>\';\n'\
4806	'					else\n'\
4807	'						htmlline = \'<div class="t" style="right:\'+pos+\'%">\'+val+\'ms</div>\';\n'\
4808	'				} else {\n'\
4809	'					pos = 100 - (((j)*tS*100)/mTotal);\n'\
4810	'					val = (j)*tS;\n'\
4811	'					htmlline = \'<div class="t" style="right:\'+pos+\'%">\'+val+\'ms</div>\';\n'\
4812	'					if(j == 0)\n'\
4813	'						if(mode == "r")\n'\
4814	'							htmlline = rline+"<cS>&larr;R</cS></div>";\n'\
4815	'						else\n'\
4816	'							htmlline = rline+"<cS>0ms</div>";\n'\
4817	'				}\n'\
4818	'				html += htmlline;\n'\
4819	'			}\n'\
4820	'			timescale.innerHTML = html;\n'\
4821	'		}\n'\
4822	'	}\n'\
4823	'	function zoomTimeline() {\n'\
4824	'		var dmesg = document.getElementById("dmesg");\n'\
4825	'		var zoombox = document.getElementById("dmesgzoombox");\n'\
4826	'		var left = zoombox.scrollLeft;\n'\
4827	'		var val = parseFloat(dmesg.style.width);\n'\
4828	'		var newval = 100;\n'\
4829	'		var sh = window.outerWidth / 2;\n'\
4830	'		if(this.id == "zoomin") {\n'\
4831	'			newval = val * 1.2;\n'\
4832	'			if(newval > 910034) newval = 910034;\n'\
4833	'			dmesg.style.width = newval+"%";\n'\
4834	'			zoombox.scrollLeft = ((left + sh) * newval / val) - sh;\n'\
4835	'		} else if (this.id == "zoomout") {\n'\
4836	'			newval = val / 1.2;\n'\
4837	'			if(newval < 100) newval = 100;\n'\
4838	'			dmesg.style.width = newval+"%";\n'\
4839	'			zoombox.scrollLeft = ((left + sh) * newval / val) - sh;\n'\
4840	'		} else {\n'\
4841	'			zoombox.scrollLeft = 0;\n'\
4842	'			dmesg.style.width = "100%";\n'\
4843	'		}\n'\
4844	'		var tS = [10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1];\n'\
4845	'		var t0 = bounds[0];\n'\
4846	'		var tMax = bounds[1];\n'\
4847	'		var tTotal = tMax - t0;\n'\
4848	'		var wTotal = tTotal * 100.0 / newval;\n'\
4849	'		var idx = 7*window.innerWidth/1100;\n'\
4850	'		for(var i = 0; (i < tS.length)&&((wTotal / tS[i]) < idx); i++);\n'\
4851	'		if(i >= tS.length) i = tS.length - 1;\n'\
4852	'		if(tS[i] == resolution) return;\n'\
4853	'		resolution = tS[i];\n'\
4854	'		redrawTimescale(t0, tMax, tS[i]);\n'\
4855	'	}\n'\
4856	'	function deviceName(title) {\n'\
4857	'		var name = title.slice(0, title.indexOf(" ("));\n'\
4858	'		return name;\n'\
4859	'	}\n'\
4860	'	function deviceHover() {\n'\
4861	'		var name = deviceName(this.title);\n'\
4862	'		var dmesg = document.getElementById("dmesg");\n'\
4863	'		var dev = dmesg.getElementsByClassName("thread");\n'\
4864	'		var cpu = -1;\n'\
4865	'		if(name.match("CPU_ON\[[0-9]*\]"))\n'\
4866	'			cpu = parseInt(name.slice(7));\n'\
4867	'		else if(name.match("CPU_OFF\[[0-9]*\]"))\n'\
4868	'			cpu = parseInt(name.slice(8));\n'\
4869	'		for (var i = 0; i < dev.length; i++) {\n'\
4870	'			dname = deviceName(dev[i].title);\n'\
4871	'			var cname = dev[i].className.slice(dev[i].className.indexOf("thread"));\n'\
4872	'			if((cpu >= 0 && dname.match("CPU_O[NF]*\\\[*"+cpu+"\\\]")) ||\n'\
4873	'				(name == dname))\n'\
4874	'			{\n'\
4875	'				dev[i].className = "hover "+cname;\n'\
4876	'			} else {\n'\
4877	'				dev[i].className = cname;\n'\
4878	'			}\n'\
4879	'		}\n'\
4880	'	}\n'\
4881	'	function deviceUnhover() {\n'\
4882	'		var dmesg = document.getElementById("dmesg");\n'\
4883	'		var dev = dmesg.getElementsByClassName("thread");\n'\
4884	'		for (var i = 0; i < dev.length; i++) {\n'\
4885	'			dev[i].className = dev[i].className.slice(dev[i].className.indexOf("thread"));\n'\
4886	'		}\n'\
4887	'	}\n'\
4888	'	function deviceTitle(title, total, cpu) {\n'\
4889	'		var prefix = "Total";\n'\
4890	'		if(total.length > 3) {\n'\
4891	'			prefix = "Average";\n'\
4892	'			total[1] = (total[1]+total[3])/2;\n'\
4893	'			total[2] = (total[2]+total[4])/2;\n'\
4894	'		}\n'\
4895	'		var devtitle = document.getElementById("devicedetailtitle");\n'\
4896	'		var name = deviceName(title);\n'\
4897	'		if(cpu >= 0) name = "CPU"+cpu;\n'\
4898	'		var driver = "";\n'\
4899	'		var tS = "<t2>(</t2>";\n'\
4900	'		var tR = "<t2>)</t2>";\n'\
4901	'		if(total[1] > 0)\n'\
4902	'			tS = "<t2>("+prefix+" Suspend:</t2><t0> "+total[1].toFixed(3)+" ms</t0> ";\n'\
4903	'		if(total[2] > 0)\n'\
4904	'			tR = " <t2>"+prefix+" Resume:</t2><t0> "+total[2].toFixed(3)+" ms<t2>)</t2></t0>";\n'\
4905	'		var s = title.indexOf("{");\n'\
4906	'		var e = title.indexOf("}");\n'\
4907	'		if((s >= 0) && (e >= 0))\n'\
4908	'			driver = title.slice(s+1, e) + " <t1>@</t1> ";\n'\
4909	'		if(total[1] > 0 && total[2] > 0)\n'\
4910	'			devtitle.innerHTML = "<t0>"+driver+name+"</t0> "+tS+tR;\n'\
4911	'		else\n'\
4912	'			devtitle.innerHTML = "<t0>"+title+"</t0>";\n'\
4913	'		return name;\n'\
4914	'	}\n'\
4915	'	function deviceDetail() {\n'\
4916	'		var devinfo = document.getElementById("devicedetail");\n'\
4917	'		devinfo.style.display = "block";\n'\
4918	'		var name = deviceName(this.title);\n'\
4919	'		var cpu = -1;\n'\
4920	'		if(name.match("CPU_ON\[[0-9]*\]"))\n'\
4921	'			cpu = parseInt(name.slice(7));\n'\
4922	'		else if(name.match("CPU_OFF\[[0-9]*\]"))\n'\
4923	'			cpu = parseInt(name.slice(8));\n'\
4924	'		var dmesg = document.getElementById("dmesg");\n'\
4925	'		var dev = dmesg.getElementsByClassName("thread");\n'\
4926	'		var idlist = [];\n'\
4927	'		var pdata = [[]];\n'\
4928	'		if(document.getElementById("devicedetail1"))\n'\
4929	'			pdata = [[], []];\n'\
4930	'		var pd = pdata[0];\n'\
4931	'		var total = [0.0, 0.0, 0.0];\n'\
4932	'		for (var i = 0; i < dev.length; i++) {\n'\
4933	'			dname = deviceName(dev[i].title);\n'\
4934	'			if((cpu >= 0 && dname.match("CPU_O[NF]*\\\[*"+cpu+"\\\]")) ||\n'\
4935	'				(name == dname))\n'\
4936	'			{\n'\
4937	'				idlist[idlist.length] = dev[i].id;\n'\
4938	'				var tidx = 1;\n'\
4939	'				if(dev[i].id[0] == "a") {\n'\
4940	'					pd = pdata[0];\n'\
4941	'				} else {\n'\
4942	'					if(pdata.length == 1) pdata[1] = [];\n'\
4943	'					if(total.length == 3) total[3]=total[4]=0.0;\n'\
4944	'					pd = pdata[1];\n'\
4945	'					tidx = 3;\n'\
4946	'				}\n'\
4947	'				var info = dev[i].title.split(" ");\n'\
4948	'				var pname = info[info.length-1];\n'\
4949	'				pd[pname] = parseFloat(info[info.length-3].slice(1));\n'\
4950	'				total[0] += pd[pname];\n'\
4951	'				if(pname.indexOf("suspend") >= 0)\n'\
4952	'					total[tidx] += pd[pname];\n'\
4953	'				else\n'\
4954	'					total[tidx+1] += pd[pname];\n'\
4955	'			}\n'\
4956	'		}\n'\
4957	'		var devname = deviceTitle(this.title, total, cpu);\n'\
4958	'		var left = 0.0;\n'\
4959	'		for (var t = 0; t < pdata.length; t++) {\n'\
4960	'			pd = pdata[t];\n'\
4961	'			devinfo = document.getElementById("devicedetail"+t);\n'\
4962	'			var phases = devinfo.getElementsByClassName("phaselet");\n'\
4963	'			for (var i = 0; i < phases.length; i++) {\n'\
4964	'				if(phases[i].id in pd) {\n'\
4965	'					var w = 100.0*pd[phases[i].id]/total[0];\n'\
4966	'					var fs = 32;\n'\
4967	'					if(w < 8) fs = 4*w | 0;\n'\
4968	'					var fs2 = fs*3/4;\n'\
4969	'					phases[i].style.width = w+"%";\n'\
4970	'					phases[i].style.left = left+"%";\n'\
4971	'					phases[i].title = phases[i].id+" "+pd[phases[i].id]+" ms";\n'\
4972	'					left += w;\n'\
4973	'					var time = "<t4 style=\\"font-size:"+fs+"px\\">"+pd[phases[i].id]+" ms<br></t4>";\n'\
4974	'					var pname = "<t3 style=\\"font-size:"+fs2+"px\\">"+phases[i].id.replace(new RegExp("_", "g"), " ")+"</t3>";\n'\
4975	'					phases[i].innerHTML = time+pname;\n'\
4976	'				} else {\n'\
4977	'					phases[i].style.width = "0%";\n'\
4978	'					phases[i].style.left = left+"%";\n'\
4979	'				}\n'\
4980	'			}\n'\
4981	'		}\n'\
4982	'		if(typeof devstats !== \'undefined\')\n'\
4983	'			callDetail(this.id, this.title);\n'\
4984	'		var cglist = document.getElementById("callgraphs");\n'\
4985	'		if(!cglist) return;\n'\
4986	'		var cg = cglist.getElementsByClassName("atop");\n'\
4987	'		if(cg.length < 10) return;\n'\
4988	'		for (var i = 0; i < cg.length; i++) {\n'\
4989	'			cgid = cg[i].id.split("x")[0]\n'\
4990	'			if(idlist.indexOf(cgid) >= 0) {\n'\
4991	'				cg[i].style.display = "block";\n'\
4992	'			} else {\n'\
4993	'				cg[i].style.display = "none";\n'\
4994	'			}\n'\
4995	'		}\n'\
4996	'	}\n'\
4997	'	function callDetail(devid, devtitle) {\n'\
4998	'		if(!(devid in devstats) || devstats[devid].length < 1)\n'\
4999	'			return;\n'\
5000	'		var list = devstats[devid];\n'\
5001	'		var tmp = devtitle.split(" ");\n'\
5002	'		var name = tmp[0], phase = tmp[tmp.length-1];\n'\
5003	'		var dd = document.getElementById(phase);\n'\
5004	'		var total = parseFloat(tmp[1].slice(1));\n'\
5005	'		var mlist = [];\n'\
5006	'		var maxlen = 0;\n'\
5007	'		var info = []\n'\
5008	'		for(var i in list) {\n'\
5009	'			if(list[i][0] == "@") {\n'\
5010	'				info = list[i].split("|");\n'\
5011	'				continue;\n'\
5012	'			}\n'\
5013	'			var tmp = list[i].split("|");\n'\
5014	'			var t = parseFloat(tmp[0]), f = tmp[1], c = parseInt(tmp[2]);\n'\
5015	'			var p = (t*100.0/total).toFixed(2);\n'\
5016	'			mlist[mlist.length] = [f, c, t.toFixed(2), p+"%"];\n'\
5017	'			if(f.length > maxlen)\n'\
5018	'				maxlen = f.length;\n'\
5019	'		}\n'\
5020	'		var pad = 5;\n'\
5021	'		if(mlist.length == 0) pad = 30;\n'\
5022	'		var html = \'<div style="padding-top:\'+pad+\'px"><t3> <b>\'+name+\':</b>\';\n'\
5023	'		if(info.length > 2)\n'\
5024	'			html += " start=<b>"+info[1]+"</b>, end=<b>"+info[2]+"</b>";\n'\
5025	'		if(info.length > 3)\n'\
5026	'			html += ", length<i>(w/o overhead)</i>=<b>"+info[3]+" ms</b>";\n'\
5027	'		if(info.length > 4)\n'\
5028	'			html += ", return=<b>"+info[4]+"</b>";\n'\
5029	'		html += "</t3></div>";\n'\
5030	'		if(mlist.length > 0) {\n'\
5031	'			html += \'<table class=fstat style="padding-top:\'+(maxlen*5)+\'px;"><tr><th>Function</th>\';\n'\
5032	'			for(var i in mlist)\n'\
5033	'				html += "<td class=vt>"+mlist[i][0]+"</td>";\n'\
5034	'			html += "</tr><tr><th>Calls</th>";\n'\
5035	'			for(var i in mlist)\n'\
5036	'				html += "<td>"+mlist[i][1]+"</td>";\n'\
5037	'			html += "</tr><tr><th>Time(ms)</th>";\n'\
5038	'			for(var i in mlist)\n'\
5039	'				html += "<td>"+mlist[i][2]+"</td>";\n'\
5040	'			html += "</tr><tr><th>Percent</th>";\n'\
5041	'			for(var i in mlist)\n'\
5042	'				html += "<td>"+mlist[i][3]+"</td>";\n'\
5043	'			html += "</tr></table>";\n'\
5044	'		}\n'\
5045	'		dd.innerHTML = html;\n'\
5046	'		var height = (maxlen*5)+100;\n'\
5047	'		dd.style.height = height+"px";\n'\
5048	'		document.getElementById("devicedetail").style.height = height+"px";\n'\
5049	'	}\n'\
5050	'	function callSelect() {\n'\
5051	'		var cglist = document.getElementById("callgraphs");\n'\
5052	'		if(!cglist) return;\n'\
5053	'		var cg = cglist.getElementsByClassName("atop");\n'\
5054	'		for (var i = 0; i < cg.length; i++) {\n'\
5055	'			if(this.id == cg[i].id) {\n'\
5056	'				cg[i].style.display = "block";\n'\
5057	'			} else {\n'\
5058	'				cg[i].style.display = "none";\n'\
5059	'			}\n'\
5060	'		}\n'\
5061	'	}\n'\
5062	'	function devListWindow(e) {\n'\
5063	'		var win = window.open();\n'\
5064	'		var html = "<title>"+e.target.innerHTML+"</title>"+\n'\
5065	'			"<style type=\\"text/css\\">"+\n'\
5066	'			"   ul {list-style-type:circle;padding-left:10px;margin-left:10px;}"+\n'\
5067	'			"</style>"\n'\
5068	'		var dt = devtable[0];\n'\
5069	'		if(e.target.id != "devlist1")\n'\
5070	'			dt = devtable[1];\n'\
5071	'		win.document.write(html+dt);\n'\
5072	'	}\n'\
5073	'	function errWindow() {\n'\
5074	'		var range = this.id.split("_");\n'\
5075	'		var idx1 = parseInt(range[0]);\n'\
5076	'		var idx2 = parseInt(range[1]);\n'\
5077	'		var win = window.open();\n'\
5078	'		var log = document.getElementById("dmesglog");\n'\
5079	'		var title = "<title>dmesg log</title>";\n'\
5080	'		var text = log.innerHTML.split("\\n");\n'\
5081	'		var html = "";\n'\
5082	'		for(var i = 0; i < text.length; i++) {\n'\
5083	'			if(i == idx1) {\n'\
5084	'				html += "<e id=target>"+text[i]+"</e>\\n";\n'\
5085	'			} else if(i > idx1 && i <= idx2) {\n'\
5086	'				html += "<e>"+text[i]+"</e>\\n";\n'\
5087	'			} else {\n'\
5088	'				html += text[i]+"\\n";\n'\
5089	'			}\n'\
5090	'		}\n'\
5091	'		win.document.write("<style>e{color:red}</style>"+title+"<pre>"+html+"</pre>");\n'\
5092	'		win.location.hash = "#target";\n'\
5093	'		win.document.close();\n'\
5094	'	}\n'\
5095	'	function logWindow(e) {\n'\
5096	'		var name = e.target.id.slice(4);\n'\
5097	'		var win = window.open();\n'\
5098	'		var log = document.getElementById(name+"log");\n'\
5099	'		var title = "<title>"+document.title.split(" ")[0]+" "+name+" log</title>";\n'\
5100	'		win.document.write(title+"<pre>"+log.innerHTML+"</pre>");\n'\
5101	'		win.document.close();\n'\
5102	'	}\n'\
5103	'	function onMouseDown(e) {\n'\
5104	'		dragval[0] = e.clientX;\n'\
5105	'		dragval[1] = document.getElementById("dmesgzoombox").scrollLeft;\n'\
5106	'		document.onmousemove = onMouseMove;\n'\
5107	'	}\n'\
5108	'	function onMouseMove(e) {\n'\
5109	'		var zoombox = document.getElementById("dmesgzoombox");\n'\
5110	'		zoombox.scrollLeft = dragval[1] + dragval[0] - e.clientX;\n'\
5111	'	}\n'\
5112	'	function onMouseUp(e) {\n'\
5113	'		document.onmousemove = null;\n'\
5114	'	}\n'\
5115	'	function onKeyPress(e) {\n'\
5116	'		var c = e.charCode;\n'\
5117	'		if(c != 42 && c != 43 && c != 45) return;\n'\
5118	'		var click = document.createEvent("Events");\n'\
5119	'		click.initEvent("click", true, false);\n'\
5120	'		if(c == 43)  \n'\
5121	'			document.getElementById("zoomin").dispatchEvent(click);\n'\
5122	'		else if(c == 45)\n'\
5123	'			document.getElementById("zoomout").dispatchEvent(click);\n'\
5124	'		else if(c == 42)\n'\
5125	'			document.getElementById("zoomdef").dispatchEvent(click);\n'\
5126	'	}\n'\
5127	'	window.addEventListener("resize", function () {zoomTimeline();});\n'\
5128	'	window.addEventListener("load", function () {\n'\
5129	'		var dmesg = document.getElementById("dmesg");\n'\
5130	'		dmesg.style.width = "100%"\n'\
5131	'		dmesg.onmousedown = onMouseDown;\n'\
5132	'		document.onmouseup = onMouseUp;\n'\
5133	'		document.onkeypress = onKeyPress;\n'\
5134	'		document.getElementById("zoomin").onclick = zoomTimeline;\n'\
5135	'		document.getElementById("zoomout").onclick = zoomTimeline;\n'\
5136	'		document.getElementById("zoomdef").onclick = zoomTimeline;\n'\
5137	'		var list = document.getElementsByClassName("err");\n'\
5138	'		for (var i = 0; i < list.length; i++)\n'\
5139	'			list[i].onclick = errWindow;\n'\
5140	'		var list = document.getElementsByClassName("logbtn");\n'\
5141	'		for (var i = 0; i < list.length; i++)\n'\
5142	'			list[i].onclick = logWindow;\n'\
5143	'		list = document.getElementsByClassName("devlist");\n'\
5144	'		for (var i = 0; i < list.length; i++)\n'\
5145	'			list[i].onclick = devListWindow;\n'\
5146	'		var dev = dmesg.getElementsByClassName("thread");\n'\
5147	'		for (var i = 0; i < dev.length; i++) {\n'\
5148	'			dev[i].onclick = deviceDetail;\n'\
5149	'			dev[i].onmouseover = deviceHover;\n'\
5150	'			dev[i].onmouseout = deviceUnhover;\n'\
5151	'		}\n'\
5152	'		var dev = dmesg.getElementsByClassName("srccall");\n'\
5153	'		for (var i = 0; i < dev.length; i++)\n'\
5154	'			dev[i].onclick = callSelect;\n'\
5155	'		zoomTimeline();\n'\
5156	'	});\n'\
5157	'</script>\n'
5158	hf.write(script_code);
5159
5160def setRuntimeSuspend(before=True):
5161	global sysvals
5162	sv = sysvals
5163	if sv.rs == 0:
5164		return
5165	if before:
5166		# runtime suspend disable or enable
5167		if sv.rs > 0:
5168			sv.rstgt, sv.rsval, sv.rsdir = 'on', 'auto', 'enabled'
5169		else:
5170			sv.rstgt, sv.rsval, sv.rsdir = 'auto', 'on', 'disabled'
5171		pprint('CONFIGURING RUNTIME SUSPEND...')
5172		sv.rslist = deviceInfo(sv.rstgt)
5173		for i in sv.rslist:
5174			sv.setVal(sv.rsval, i)
5175		pprint('runtime suspend %s on all devices (%d changed)' % (sv.rsdir, len(sv.rslist)))
5176		pprint('waiting 5 seconds...')
5177		time.sleep(5)
5178	else:
5179		# runtime suspend re-enable or re-disable
5180		for i in sv.rslist:
5181			sv.setVal(sv.rstgt, i)
5182		pprint('runtime suspend settings restored on %d devices' % len(sv.rslist))
5183
5184# Function: executeSuspend
5185# Description:
5186#	 Execute system suspend through the sysfs interface, then copy the output
5187#	 dmesg and ftrace files to the test output directory.
5188def executeSuspend(quiet=False):
5189	pm = ProcessMonitor()
5190	tp = sysvals.tpath
5191	if sysvals.wifi:
5192		wifi = sysvals.checkWifi()
5193	testdata = []
5194	# run these commands to prepare the system for suspend
5195	if sysvals.display:
5196		if not quiet:
5197			pprint('SET DISPLAY TO %s' % sysvals.display.upper())
5198		displayControl(sysvals.display)
5199		time.sleep(1)
5200	if sysvals.sync:
5201		if not quiet:
5202			pprint('SYNCING FILESYSTEMS')
5203		call('sync', shell=True)
5204	# mark the start point in the kernel ring buffer just as we start
5205	sysvals.initdmesg()
5206	# start ftrace
5207	if(sysvals.usecallgraph or sysvals.usetraceevents):
5208		if not quiet:
5209			pprint('START TRACING')
5210		sysvals.fsetVal('1', 'tracing_on')
5211		if sysvals.useprocmon:
5212			pm.start()
5213	sysvals.cmdinfo(True)
5214	# execute however many s/r runs requested
5215	for count in range(1,sysvals.execcount+1):
5216		# x2delay in between test runs
5217		if(count > 1 and sysvals.x2delay > 0):
5218			sysvals.fsetVal('WAIT %d' % sysvals.x2delay, 'trace_marker')
5219			time.sleep(sysvals.x2delay/1000.0)
5220			sysvals.fsetVal('WAIT END', 'trace_marker')
5221		# start message
5222		if sysvals.testcommand != '':
5223			pprint('COMMAND START')
5224		else:
5225			if(sysvals.rtcwake):
5226				pprint('SUSPEND START')
5227			else:
5228				pprint('SUSPEND START (press a key to resume)')
5229		# set rtcwake
5230		if(sysvals.rtcwake):
5231			if not quiet:
5232				pprint('will issue an rtcwake in %d seconds' % sysvals.rtcwaketime)
5233			sysvals.rtcWakeAlarmOn()
5234		# start of suspend trace marker
5235		if(sysvals.usecallgraph or sysvals.usetraceevents):
5236			sysvals.fsetVal(datetime.now().strftime(sysvals.tmstart), 'trace_marker')
5237		# predelay delay
5238		if(count == 1 and sysvals.predelay > 0):
5239			sysvals.fsetVal('WAIT %d' % sysvals.predelay, 'trace_marker')
5240			time.sleep(sysvals.predelay/1000.0)
5241			sysvals.fsetVal('WAIT END', 'trace_marker')
5242		# initiate suspend or command
5243		tdata = {'error': ''}
5244		if sysvals.testcommand != '':
5245			res = call(sysvals.testcommand+' 2>&1', shell=True);
5246			if res != 0:
5247				tdata['error'] = 'cmd returned %d' % res
5248		else:
5249			mode = sysvals.suspendmode
5250			if sysvals.memmode and os.path.exists(sysvals.mempowerfile):
5251				mode = 'mem'
5252				pf = open(sysvals.mempowerfile, 'w')
5253				pf.write(sysvals.memmode)
5254				pf.close()
5255			if sysvals.diskmode and os.path.exists(sysvals.diskpowerfile):
5256				mode = 'disk'
5257				pf = open(sysvals.diskpowerfile, 'w')
5258				pf.write(sysvals.diskmode)
5259				pf.close()
5260			if mode == 'freeze' and sysvals.haveTurbostat():
5261				# execution will pause here
5262				turbo = sysvals.turbostat()
5263				if turbo:
5264					tdata['turbo'] = turbo
5265			else:
5266				pf = open(sysvals.powerfile, 'w')
5267				pf.write(mode)
5268				# execution will pause here
5269				try:
5270					pf.close()
5271				except Exception as e:
5272					tdata['error'] = str(e)
5273		if(sysvals.rtcwake):
5274			sysvals.rtcWakeAlarmOff()
5275		# postdelay delay
5276		if(count == sysvals.execcount and sysvals.postdelay > 0):
5277			sysvals.fsetVal('WAIT %d' % sysvals.postdelay, 'trace_marker')
5278			time.sleep(sysvals.postdelay/1000.0)
5279			sysvals.fsetVal('WAIT END', 'trace_marker')
5280		# return from suspend
5281		pprint('RESUME COMPLETE')
5282		if(sysvals.usecallgraph or sysvals.usetraceevents):
5283			sysvals.fsetVal(datetime.now().strftime(sysvals.tmend), 'trace_marker')
5284		if sysvals.wifi and wifi:
5285			tdata['wifi'] = sysvals.pollWifi(wifi)
5286		if(sysvals.suspendmode == 'mem' or sysvals.suspendmode == 'command'):
5287			tdata['fw'] = getFPDT(False)
5288		testdata.append(tdata)
5289	cmdafter = sysvals.cmdinfo(False)
5290	# stop ftrace
5291	if(sysvals.usecallgraph or sysvals.usetraceevents):
5292		if sysvals.useprocmon:
5293			pm.stop()
5294		sysvals.fsetVal('0', 'tracing_on')
5295	# grab a copy of the dmesg output
5296	if not quiet:
5297		pprint('CAPTURING DMESG')
5298	sysvals.getdmesg(testdata)
5299	# grab a copy of the ftrace output
5300	if(sysvals.usecallgraph or sysvals.usetraceevents):
5301		if not quiet:
5302			pprint('CAPTURING TRACE')
5303		op = sysvals.writeDatafileHeader(sysvals.ftracefile, testdata)
5304		fp = open(tp+'trace', 'r')
5305		for line in fp:
5306			op.write(line)
5307		op.close()
5308		sysvals.fsetVal('', 'trace')
5309		sysvals.platforminfo(cmdafter)
5310
5311def readFile(file):
5312	if os.path.islink(file):
5313		return os.readlink(file).split('/')[-1]
5314	else:
5315		return sysvals.getVal(file).strip()
5316
5317# Function: ms2nice
5318# Description:
5319#	 Print out a very concise time string in minutes and seconds
5320# Output:
5321#	 The time string, e.g. "1901m16s"
5322def ms2nice(val):
5323	val = int(val)
5324	h = val // 3600000
5325	m = (val // 60000) % 60
5326	s = (val // 1000) % 60
5327	if h > 0:
5328		return '%d:%02d:%02d' % (h, m, s)
5329	if m > 0:
5330		return '%02d:%02d' % (m, s)
5331	return '%ds' % s
5332
5333def yesno(val):
5334	list = {'enabled':'A', 'disabled':'S', 'auto':'E', 'on':'D',
5335		'active':'A', 'suspended':'S', 'suspending':'S'}
5336	if val not in list:
5337		return ' '
5338	return list[val]
5339
5340# Function: deviceInfo
5341# Description:
5342#	 Detect all the USB hosts and devices currently connected and add
5343#	 a list of USB device names to sysvals for better timeline readability
5344def deviceInfo(output=''):
5345	if not output:
5346		pprint('LEGEND\n'\
5347		'---------------------------------------------------------------------------------------------\n'\
5348		'  A = async/sync PM queue (A/S)               C = runtime active children\n'\
5349		'  R = runtime suspend enabled/disabled (E/D)  rACTIVE = runtime active (min/sec)\n'\
5350		'  S = runtime status active/suspended (A/S)   rSUSPEND = runtime suspend (min/sec)\n'\
5351		'  U = runtime usage count\n'\
5352		'---------------------------------------------------------------------------------------------\n'\
5353		'DEVICE                     NAME                       A R S U C    rACTIVE   rSUSPEND\n'\
5354		'---------------------------------------------------------------------------------------------')
5355
5356	res = []
5357	tgtval = 'runtime_status'
5358	lines = dict()
5359	for dirname, dirnames, filenames in os.walk('/sys/devices'):
5360		if(not re.match('.*/power', dirname) or
5361			'control' not in filenames or
5362			tgtval not in filenames):
5363			continue
5364		name = ''
5365		dirname = dirname[:-6]
5366		device = dirname.split('/')[-1]
5367		power = dict()
5368		power[tgtval] = readFile('%s/power/%s' % (dirname, tgtval))
5369		# only list devices which support runtime suspend
5370		if power[tgtval] not in ['active', 'suspended', 'suspending']:
5371			continue
5372		for i in ['product', 'driver', 'subsystem']:
5373			file = '%s/%s' % (dirname, i)
5374			if os.path.exists(file):
5375				name = readFile(file)
5376				break
5377		for i in ['async', 'control', 'runtime_status', 'runtime_usage',
5378			'runtime_active_kids', 'runtime_active_time',
5379			'runtime_suspended_time']:
5380			if i in filenames:
5381				power[i] = readFile('%s/power/%s' % (dirname, i))
5382		if output:
5383			if power['control'] == output:
5384				res.append('%s/power/control' % dirname)
5385			continue
5386		lines[dirname] = '%-26s %-26s %1s %1s %1s %1s %1s %10s %10s' % \
5387			(device[:26], name[:26],
5388			yesno(power['async']), \
5389			yesno(power['control']), \
5390			yesno(power['runtime_status']), \
5391			power['runtime_usage'], \
5392			power['runtime_active_kids'], \
5393			ms2nice(power['runtime_active_time']), \
5394			ms2nice(power['runtime_suspended_time']))
5395	for i in sorted(lines):
5396		print(lines[i])
5397	return res
5398
5399# Function: getModes
5400# Description:
5401#	 Determine the supported power modes on this system
5402# Output:
5403#	 A string list of the available modes
5404def getModes():
5405	modes = []
5406	if(os.path.exists(sysvals.powerfile)):
5407		fp = open(sysvals.powerfile, 'r')
5408		modes = fp.read().split()
5409		fp.close()
5410	if(os.path.exists(sysvals.mempowerfile)):
5411		deep = False
5412		fp = open(sysvals.mempowerfile, 'r')
5413		for m in fp.read().split():
5414			memmode = m.strip('[]')
5415			if memmode == 'deep':
5416				deep = True
5417			else:
5418				modes.append('mem-%s' % memmode)
5419		fp.close()
5420		if 'mem' in modes and not deep:
5421			modes.remove('mem')
5422	if('disk' in modes and os.path.exists(sysvals.diskpowerfile)):
5423		fp = open(sysvals.diskpowerfile, 'r')
5424		for m in fp.read().split():
5425			modes.append('disk-%s' % m.strip('[]'))
5426		fp.close()
5427	return modes
5428
5429# Function: dmidecode
5430# Description:
5431#	 Read the bios tables and pull out system info
5432# Arguments:
5433#	 mempath: /dev/mem or custom mem path
5434#	 fatal: True to exit on error, False to return empty dict
5435# Output:
5436#	 A dict object with all available key/values
5437def dmidecode(mempath, fatal=False):
5438	out = dict()
5439
5440	# the list of values to retrieve, with hardcoded (type, idx)
5441	info = {
5442		'bios-vendor': (0, 4),
5443		'bios-version': (0, 5),
5444		'bios-release-date': (0, 8),
5445		'system-manufacturer': (1, 4),
5446		'system-product-name': (1, 5),
5447		'system-version': (1, 6),
5448		'system-serial-number': (1, 7),
5449		'baseboard-manufacturer': (2, 4),
5450		'baseboard-product-name': (2, 5),
5451		'baseboard-version': (2, 6),
5452		'baseboard-serial-number': (2, 7),
5453		'chassis-manufacturer': (3, 4),
5454		'chassis-type': (3, 5),
5455		'chassis-version': (3, 6),
5456		'chassis-serial-number': (3, 7),
5457		'processor-manufacturer': (4, 7),
5458		'processor-version': (4, 16),
5459	}
5460	if(not os.path.exists(mempath)):
5461		if(fatal):
5462			doError('file does not exist: %s' % mempath)
5463		return out
5464	if(not os.access(mempath, os.R_OK)):
5465		if(fatal):
5466			doError('file is not readable: %s' % mempath)
5467		return out
5468
5469	# by default use legacy scan, but try to use EFI first
5470	memaddr = 0xf0000
5471	memsize = 0x10000
5472	for ep in ['/sys/firmware/efi/systab', '/proc/efi/systab']:
5473		if not os.path.exists(ep) or not os.access(ep, os.R_OK):
5474			continue
5475		fp = open(ep, 'r')
5476		buf = fp.read()
5477		fp.close()
5478		i = buf.find('SMBIOS=')
5479		if i >= 0:
5480			try:
5481				memaddr = int(buf[i+7:], 16)
5482				memsize = 0x20
5483			except:
5484				continue
5485
5486	# read in the memory for scanning
5487	try:
5488		fp = open(mempath, 'rb')
5489		fp.seek(memaddr)
5490		buf = fp.read(memsize)
5491	except:
5492		if(fatal):
5493			doError('DMI table is unreachable, sorry')
5494		else:
5495			pprint('WARNING: /dev/mem is not readable, ignoring DMI data')
5496			return out
5497	fp.close()
5498
5499	# search for either an SM table or DMI table
5500	i = base = length = num = 0
5501	while(i < memsize):
5502		if buf[i:i+4] == b'_SM_' and i < memsize - 16:
5503			length = struct.unpack('H', buf[i+22:i+24])[0]
5504			base, num = struct.unpack('IH', buf[i+24:i+30])
5505			break
5506		elif buf[i:i+5] == b'_DMI_':
5507			length = struct.unpack('H', buf[i+6:i+8])[0]
5508			base, num = struct.unpack('IH', buf[i+8:i+14])
5509			break
5510		i += 16
5511	if base == 0 and length == 0 and num == 0:
5512		if(fatal):
5513			doError('Neither SMBIOS nor DMI were found')
5514		else:
5515			return out
5516
5517	# read in the SM or DMI table
5518	try:
5519		fp = open(mempath, 'rb')
5520		fp.seek(base)
5521		buf = fp.read(length)
5522	except:
5523		if(fatal):
5524			doError('DMI table is unreachable, sorry')
5525		else:
5526			pprint('WARNING: /dev/mem is not readable, ignoring DMI data')
5527			return out
5528	fp.close()
5529
5530	# scan the table for the values we want
5531	count = i = 0
5532	while(count < num and i <= len(buf) - 4):
5533		type, size, handle = struct.unpack('BBH', buf[i:i+4])
5534		n = i + size
5535		while n < len(buf) - 1:
5536			if 0 == struct.unpack('H', buf[n:n+2])[0]:
5537				break
5538			n += 1
5539		data = buf[i+size:n+2].split(b'\0')
5540		for name in info:
5541			itype, idxadr = info[name]
5542			if itype == type:
5543				idx = struct.unpack('B', buf[i+idxadr:i+idxadr+1])[0]
5544				if idx > 0 and idx < len(data) - 1:
5545					s = data[idx-1].decode('utf-8')
5546					if s.strip() and s.strip().lower() != 'to be filled by o.e.m.':
5547						out[name] = s
5548		i = n + 2
5549		count += 1
5550	return out
5551
5552def displayControl(cmd):
5553	xset, ret = 'timeout 10 xset -d :0.0 {0}', 0
5554	if sysvals.sudouser:
5555		xset = 'sudo -u %s %s' % (sysvals.sudouser, xset)
5556	if cmd == 'init':
5557		ret = call(xset.format('dpms 0 0 0'), shell=True)
5558		if not ret:
5559			ret = call(xset.format('s off'), shell=True)
5560	elif cmd == 'reset':
5561		ret = call(xset.format('s reset'), shell=True)
5562	elif cmd in ['on', 'off', 'standby', 'suspend']:
5563		b4 = displayControl('stat')
5564		ret = call(xset.format('dpms force %s' % cmd), shell=True)
5565		if not ret:
5566			curr = displayControl('stat')
5567			sysvals.vprint('Display Switched: %s -> %s' % (b4, curr))
5568			if curr != cmd:
5569				sysvals.vprint('WARNING: Display failed to change to %s' % cmd)
5570		if ret:
5571			sysvals.vprint('WARNING: Display failed to change to %s with xset' % cmd)
5572			return ret
5573	elif cmd == 'stat':
5574		fp = Popen(xset.format('q').split(' '), stdout=PIPE).stdout
5575		ret = 'unknown'
5576		for line in fp:
5577			m = re.match('[\s]*Monitor is (?P<m>.*)', ascii(line))
5578			if(m and len(m.group('m')) >= 2):
5579				out = m.group('m').lower()
5580				ret = out[3:] if out[0:2] == 'in' else out
5581				break
5582		fp.close()
5583	return ret
5584
5585# Function: getFPDT
5586# Description:
5587#	 Read the acpi bios tables and pull out FPDT, the firmware data
5588# Arguments:
5589#	 output: True to output the info to stdout, False otherwise
5590def getFPDT(output):
5591	rectype = {}
5592	rectype[0] = 'Firmware Basic Boot Performance Record'
5593	rectype[1] = 'S3 Performance Table Record'
5594	prectype = {}
5595	prectype[0] = 'Basic S3 Resume Performance Record'
5596	prectype[1] = 'Basic S3 Suspend Performance Record'
5597
5598	sysvals.rootCheck(True)
5599	if(not os.path.exists(sysvals.fpdtpath)):
5600		if(output):
5601			doError('file does not exist: %s' % sysvals.fpdtpath)
5602		return False
5603	if(not os.access(sysvals.fpdtpath, os.R_OK)):
5604		if(output):
5605			doError('file is not readable: %s' % sysvals.fpdtpath)
5606		return False
5607	if(not os.path.exists(sysvals.mempath)):
5608		if(output):
5609			doError('file does not exist: %s' % sysvals.mempath)
5610		return False
5611	if(not os.access(sysvals.mempath, os.R_OK)):
5612		if(output):
5613			doError('file is not readable: %s' % sysvals.mempath)
5614		return False
5615
5616	fp = open(sysvals.fpdtpath, 'rb')
5617	buf = fp.read()
5618	fp.close()
5619
5620	if(len(buf) < 36):
5621		if(output):
5622			doError('Invalid FPDT table data, should '+\
5623				'be at least 36 bytes')
5624		return False
5625
5626	table = struct.unpack('4sIBB6s8sI4sI', buf[0:36])
5627	if(output):
5628		pprint('\n'\
5629		'Firmware Performance Data Table (%s)\n'\
5630		'                  Signature : %s\n'\
5631		'               Table Length : %u\n'\
5632		'                   Revision : %u\n'\
5633		'                   Checksum : 0x%x\n'\
5634		'                     OEM ID : %s\n'\
5635		'               OEM Table ID : %s\n'\
5636		'               OEM Revision : %u\n'\
5637		'                 Creator ID : %s\n'\
5638		'           Creator Revision : 0x%x\n'\
5639		'' % (ascii(table[0]), ascii(table[0]), table[1], table[2],
5640			table[3], ascii(table[4]), ascii(table[5]), table[6],
5641			ascii(table[7]), table[8]))
5642
5643	if(table[0] != b'FPDT'):
5644		if(output):
5645			doError('Invalid FPDT table')
5646		return False
5647	if(len(buf) <= 36):
5648		return False
5649	i = 0
5650	fwData = [0, 0]
5651	records = buf[36:]
5652	try:
5653		fp = open(sysvals.mempath, 'rb')
5654	except:
5655		pprint('WARNING: /dev/mem is not readable, ignoring the FPDT data')
5656		return False
5657	while(i < len(records)):
5658		header = struct.unpack('HBB', records[i:i+4])
5659		if(header[0] not in rectype):
5660			i += header[1]
5661			continue
5662		if(header[1] != 16):
5663			i += header[1]
5664			continue
5665		addr = struct.unpack('Q', records[i+8:i+16])[0]
5666		try:
5667			fp.seek(addr)
5668			first = fp.read(8)
5669		except:
5670			if(output):
5671				pprint('Bad address 0x%x in %s' % (addr, sysvals.mempath))
5672			return [0, 0]
5673		rechead = struct.unpack('4sI', first)
5674		recdata = fp.read(rechead[1]-8)
5675		if(rechead[0] == b'FBPT'):
5676			record = struct.unpack('HBBIQQQQQ', recdata[:48])
5677			if(output):
5678				pprint('%s (%s)\n'\
5679				'                  Reset END : %u ns\n'\
5680				'  OS Loader LoadImage Start : %u ns\n'\
5681				' OS Loader StartImage Start : %u ns\n'\
5682				'     ExitBootServices Entry : %u ns\n'\
5683				'      ExitBootServices Exit : %u ns'\
5684				'' % (rectype[header[0]], ascii(rechead[0]), record[4], record[5],
5685					record[6], record[7], record[8]))
5686		elif(rechead[0] == b'S3PT'):
5687			if(output):
5688				pprint('%s (%s)' % (rectype[header[0]], ascii(rechead[0])))
5689			j = 0
5690			while(j < len(recdata)):
5691				prechead = struct.unpack('HBB', recdata[j:j+4])
5692				if(prechead[0] not in prectype):
5693					continue
5694				if(prechead[0] == 0):
5695					record = struct.unpack('IIQQ', recdata[j:j+prechead[1]])
5696					fwData[1] = record[2]
5697					if(output):
5698						pprint('    %s\n'\
5699						'               Resume Count : %u\n'\
5700						'                 FullResume : %u ns\n'\
5701						'              AverageResume : %u ns'\
5702						'' % (prectype[prechead[0]], record[1],
5703								record[2], record[3]))
5704				elif(prechead[0] == 1):
5705					record = struct.unpack('QQ', recdata[j+4:j+prechead[1]])
5706					fwData[0] = record[1] - record[0]
5707					if(output):
5708						pprint('    %s\n'\
5709						'               SuspendStart : %u ns\n'\
5710						'                 SuspendEnd : %u ns\n'\
5711						'                SuspendTime : %u ns'\
5712						'' % (prectype[prechead[0]], record[0],
5713								record[1], fwData[0]))
5714
5715				j += prechead[1]
5716		if(output):
5717			pprint('')
5718		i += header[1]
5719	fp.close()
5720	return fwData
5721
5722# Function: statusCheck
5723# Description:
5724#	 Verify that the requested command and options will work, and
5725#	 print the results to the terminal
5726# Output:
5727#	 True if the test will work, False if not
5728def statusCheck(probecheck=False):
5729	status = ''
5730
5731	pprint('Checking this system (%s)...' % platform.node())
5732
5733	# check we have root access
5734	res = sysvals.colorText('NO (No features of this tool will work!)')
5735	if(sysvals.rootCheck(False)):
5736		res = 'YES'
5737	pprint('    have root access: %s' % res)
5738	if(res != 'YES'):
5739		pprint('    Try running this script with sudo')
5740		return 'missing root access'
5741
5742	# check sysfs is mounted
5743	res = sysvals.colorText('NO (No features of this tool will work!)')
5744	if(os.path.exists(sysvals.powerfile)):
5745		res = 'YES'
5746	pprint('    is sysfs mounted: %s' % res)
5747	if(res != 'YES'):
5748		return 'sysfs is missing'
5749
5750	# check target mode is a valid mode
5751	if sysvals.suspendmode != 'command':
5752		res = sysvals.colorText('NO')
5753		modes = getModes()
5754		if(sysvals.suspendmode in modes):
5755			res = 'YES'
5756		else:
5757			status = '%s mode is not supported' % sysvals.suspendmode
5758		pprint('    is "%s" a valid power mode: %s' % (sysvals.suspendmode, res))
5759		if(res == 'NO'):
5760			pprint('      valid power modes are: %s' % modes)
5761			pprint('      please choose one with -m')
5762
5763	# check if ftrace is available
5764	res = sysvals.colorText('NO')
5765	ftgood = sysvals.verifyFtrace()
5766	if(ftgood):
5767		res = 'YES'
5768	elif(sysvals.usecallgraph):
5769		status = 'ftrace is not properly supported'
5770	pprint('    is ftrace supported: %s' % res)
5771
5772	# check if kprobes are available
5773	if sysvals.usekprobes:
5774		res = sysvals.colorText('NO')
5775		sysvals.usekprobes = sysvals.verifyKprobes()
5776		if(sysvals.usekprobes):
5777			res = 'YES'
5778		else:
5779			sysvals.usedevsrc = False
5780		pprint('    are kprobes supported: %s' % res)
5781
5782	# what data source are we using
5783	res = 'DMESG'
5784	if(ftgood):
5785		sysvals.usetraceevents = True
5786		for e in sysvals.traceevents:
5787			if not os.path.exists(sysvals.epath+e):
5788				sysvals.usetraceevents = False
5789		if(sysvals.usetraceevents):
5790			res = 'FTRACE (all trace events found)'
5791	pprint('    timeline data source: %s' % res)
5792
5793	# check if rtcwake
5794	res = sysvals.colorText('NO')
5795	if(sysvals.rtcpath != ''):
5796		res = 'YES'
5797	elif(sysvals.rtcwake):
5798		status = 'rtcwake is not properly supported'
5799	pprint('    is rtcwake supported: %s' % res)
5800
5801	# check info commands
5802	pprint('    optional commands this tool may use for info:')
5803	no = sysvals.colorText('MISSING')
5804	yes = sysvals.colorText('FOUND', 32)
5805	for c in ['turbostat', 'mcelog', 'lspci', 'lsusb']:
5806		if c == 'turbostat':
5807			res = yes if sysvals.haveTurbostat() else no
5808		else:
5809			res = yes if sysvals.getExec(c) else no
5810		pprint('        %s: %s' % (c, res))
5811
5812	if not probecheck:
5813		return status
5814
5815	# verify kprobes
5816	if sysvals.usekprobes:
5817		for name in sysvals.tracefuncs:
5818			sysvals.defaultKprobe(name, sysvals.tracefuncs[name])
5819		if sysvals.usedevsrc:
5820			for name in sysvals.dev_tracefuncs:
5821				sysvals.defaultKprobe(name, sysvals.dev_tracefuncs[name])
5822		sysvals.addKprobes(True)
5823
5824	return status
5825
5826# Function: doError
5827# Description:
5828#	 generic error function for catastrphic failures
5829# Arguments:
5830#	 msg: the error message to print
5831#	 help: True if printHelp should be called after, False otherwise
5832def doError(msg, help=False):
5833	if(help == True):
5834		printHelp()
5835	pprint('ERROR: %s\n' % msg)
5836	sysvals.outputResult({'error':msg})
5837	sys.exit(1)
5838
5839# Function: getArgInt
5840# Description:
5841#	 pull out an integer argument from the command line with checks
5842def getArgInt(name, args, min, max, main=True):
5843	if main:
5844		try:
5845			arg = next(args)
5846		except:
5847			doError(name+': no argument supplied', True)
5848	else:
5849		arg = args
5850	try:
5851		val = int(arg)
5852	except:
5853		doError(name+': non-integer value given', True)
5854	if(val < min or val > max):
5855		doError(name+': value should be between %d and %d' % (min, max), True)
5856	return val
5857
5858# Function: getArgFloat
5859# Description:
5860#	 pull out a float argument from the command line with checks
5861def getArgFloat(name, args, min, max, main=True):
5862	if main:
5863		try:
5864			arg = next(args)
5865		except:
5866			doError(name+': no argument supplied', True)
5867	else:
5868		arg = args
5869	try:
5870		val = float(arg)
5871	except:
5872		doError(name+': non-numerical value given', True)
5873	if(val < min or val > max):
5874		doError(name+': value should be between %f and %f' % (min, max), True)
5875	return val
5876
5877def processData(live=False, quiet=False):
5878	if not quiet:
5879		pprint('PROCESSING DATA')
5880	sysvals.vprint('usetraceevents=%s, usetracemarkers=%s, usekprobes=%s' % \
5881		(sysvals.usetraceevents, sysvals.usetracemarkers, sysvals.usekprobes))
5882	error = ''
5883	if(sysvals.usetraceevents):
5884		testruns, error = parseTraceLog(live)
5885		if sysvals.dmesgfile:
5886			for data in testruns:
5887				data.extractErrorInfo()
5888	else:
5889		testruns = loadKernelLog()
5890		for data in testruns:
5891			parseKernelLog(data)
5892		if(sysvals.ftracefile and (sysvals.usecallgraph or sysvals.usetraceevents)):
5893			appendIncompleteTraceLog(testruns)
5894	if not sysvals.stamp:
5895		pprint('ERROR: data does not include the expected stamp')
5896		return (testruns, {'error': 'timeline generation failed'})
5897	shown = ['bios', 'biosdate', 'cpu', 'host', 'kernel', 'man', 'memfr',
5898			'memsz', 'mode', 'numcpu', 'plat', 'time', 'wifi']
5899	sysvals.vprint('System Info:')
5900	for key in sorted(sysvals.stamp):
5901		if key in shown:
5902			sysvals.vprint('    %-8s : %s' % (key.upper(), sysvals.stamp[key]))
5903	sysvals.vprint('Command:\n    %s' % sysvals.cmdline)
5904	for data in testruns:
5905		if data.turbostat:
5906			idx, s = 0, 'Turbostat:\n    '
5907			for val in data.turbostat.split('|'):
5908				idx += len(val) + 1
5909				if idx >= 80:
5910					idx = 0
5911					s += '\n    '
5912				s += val + ' '
5913			sysvals.vprint(s)
5914		data.printDetails()
5915	if len(sysvals.platinfo) > 0:
5916		sysvals.vprint('\nPlatform Info:')
5917		for info in sysvals.platinfo:
5918			sysvals.vprint('[%s - %s]' % (info[0], info[1]))
5919			sysvals.vprint(info[2])
5920		sysvals.vprint('')
5921	if sysvals.cgdump:
5922		for data in testruns:
5923			data.debugPrint()
5924		sys.exit(0)
5925	if len(testruns) < 1:
5926		pprint('ERROR: Not enough test data to build a timeline')
5927		return (testruns, {'error': 'timeline generation failed'})
5928	sysvals.vprint('Creating the html timeline (%s)...' % sysvals.htmlfile)
5929	createHTML(testruns, error)
5930	if not quiet:
5931		pprint('DONE')
5932	data = testruns[0]
5933	stamp = data.stamp
5934	stamp['suspend'], stamp['resume'] = data.getTimeValues()
5935	if data.fwValid:
5936		stamp['fwsuspend'], stamp['fwresume'] = data.fwSuspend, data.fwResume
5937	if error:
5938		stamp['error'] = error
5939	return (testruns, stamp)
5940
5941# Function: rerunTest
5942# Description:
5943#	 generate an output from an existing set of ftrace/dmesg logs
5944def rerunTest(htmlfile=''):
5945	if sysvals.ftracefile:
5946		doesTraceLogHaveTraceEvents()
5947	if not sysvals.dmesgfile and not sysvals.usetraceevents:
5948		doError('recreating this html output requires a dmesg file')
5949	if htmlfile:
5950		sysvals.htmlfile = htmlfile
5951	else:
5952		sysvals.setOutputFile()
5953	if os.path.exists(sysvals.htmlfile):
5954		if not os.path.isfile(sysvals.htmlfile):
5955			doError('a directory already exists with this name: %s' % sysvals.htmlfile)
5956		elif not os.access(sysvals.htmlfile, os.W_OK):
5957			doError('missing permission to write to %s' % sysvals.htmlfile)
5958	testruns, stamp = processData()
5959	sysvals.resetlog()
5960	return stamp
5961
5962# Function: runTest
5963# Description:
5964#	 execute a suspend/resume, gather the logs, and generate the output
5965def runTest(n=0, quiet=False):
5966	# prepare for the test
5967	sysvals.initFtrace(quiet)
5968	sysvals.initTestOutput('suspend')
5969
5970	# execute the test
5971	executeSuspend(quiet)
5972	sysvals.cleanupFtrace()
5973	if sysvals.skiphtml:
5974		sysvals.outputResult({}, n)
5975		sysvals.sudoUserchown(sysvals.testdir)
5976		return
5977	testruns, stamp = processData(True, quiet)
5978	for data in testruns:
5979		del data
5980	sysvals.sudoUserchown(sysvals.testdir)
5981	sysvals.outputResult(stamp, n)
5982	if 'error' in stamp:
5983		return 2
5984	return 0
5985
5986def find_in_html(html, start, end, firstonly=True):
5987	n, cnt, out = 0, len(html), []
5988	while n < cnt:
5989		e = cnt if (n + 10000 > cnt or n == 0) else n + 10000
5990		m = re.search(start, html[n:e])
5991		if not m:
5992			break
5993		i = m.end()
5994		m = re.search(end, html[n+i:e])
5995		if not m:
5996			break
5997		j = m.start()
5998		str = html[n+i:n+i+j]
5999		if end == 'ms':
6000			num = re.search(r'[-+]?\d*\.\d+|\d+', str)
6001			str = num.group() if num else 'NaN'
6002		if firstonly:
6003			return str
6004		out.append(str)
6005		n += i+j
6006	if firstonly:
6007		return ''
6008	return out
6009
6010def data_from_html(file, outpath, issues, fulldetail=False):
6011	html = open(file, 'r').read()
6012	sysvals.htmlfile = os.path.relpath(file, outpath)
6013	# extract general info
6014	suspend = find_in_html(html, 'Kernel Suspend', 'ms')
6015	resume = find_in_html(html, 'Kernel Resume', 'ms')
6016	sysinfo = find_in_html(html, '<div class="stamp sysinfo">', '</div>')
6017	line = find_in_html(html, '<div class="stamp">', '</div>')
6018	stmp = line.split()
6019	if not suspend or not resume or len(stmp) != 8:
6020		return False
6021	try:
6022		dt = datetime.strptime(' '.join(stmp[3:]), '%B %d %Y, %I:%M:%S %p')
6023	except:
6024		return False
6025	sysvals.hostname = stmp[0]
6026	tstr = dt.strftime('%Y/%m/%d %H:%M:%S')
6027	error = find_in_html(html, '<table class="testfail"><tr><td>', '</td>')
6028	if error:
6029		m = re.match('[a-z0-9]* failed in (?P<p>\S*).*', error)
6030		if m:
6031			result = 'fail in %s' % m.group('p')
6032		else:
6033			result = 'fail'
6034	else:
6035		result = 'pass'
6036	# extract error info
6037	ilist = []
6038	extra = dict()
6039	log = find_in_html(html, '<div id="dmesglog" style="display:none;">',
6040		'</div>').strip()
6041	if log:
6042		d = Data(0)
6043		d.end = 999999999
6044		d.dmesgtext = log.split('\n')
6045		msglist = d.extractErrorInfo()
6046		for msg in msglist:
6047			sysvals.errorSummary(issues, msg)
6048		if stmp[2] == 'freeze':
6049			extra = d.turbostatInfo()
6050		elist = dict()
6051		for dir in d.errorinfo:
6052			for err in d.errorinfo[dir]:
6053				if err[0] not in elist:
6054					elist[err[0]] = 0
6055				elist[err[0]] += 1
6056		for i in elist:
6057			ilist.append('%sx%d' % (i, elist[i]) if elist[i] > 1 else i)
6058	wifi = find_in_html(html, 'Wifi Resume: ', '</td>')
6059	if wifi:
6060		extra['wifi'] = wifi
6061	low = find_in_html(html, 'freeze time: <b>', ' ms</b>')
6062	if low and '|' in low:
6063		issue = 'FREEZEx%d' % len(low.split('|'))
6064		match = [i for i in issues if i['match'] == issue]
6065		if len(match) > 0:
6066			match[0]['count'] += 1
6067			if sysvals.hostname not in match[0]['urls']:
6068				match[0]['urls'][sysvals.hostname] = [sysvals.htmlfile]
6069			elif sysvals.htmlfile not in match[0]['urls'][sysvals.hostname]:
6070				match[0]['urls'][sysvals.hostname].append(sysvals.htmlfile)
6071		else:
6072			issues.append({
6073				'match': issue, 'count': 1, 'line': issue,
6074				'urls': {sysvals.hostname: [sysvals.htmlfile]},
6075			})
6076		ilist.append(issue)
6077	# extract device info
6078	devices = dict()
6079	for line in html.split('\n'):
6080		m = re.match(' *<div id=\"[a,0-9]*\" *title=\"(?P<title>.*)\" class=\"thread.*', line)
6081		if not m or 'thread kth' in line or 'thread sec' in line:
6082			continue
6083		m = re.match('(?P<n>.*) \((?P<t>[0-9,\.]*) ms\) (?P<p>.*)', m.group('title'))
6084		if not m:
6085			continue
6086		name, time, phase = m.group('n'), m.group('t'), m.group('p')
6087		if ' async' in name or ' sync' in name:
6088			name = ' '.join(name.split(' ')[:-1])
6089		if phase.startswith('suspend'):
6090			d = 'suspend'
6091		elif phase.startswith('resume'):
6092			d = 'resume'
6093		else:
6094			continue
6095		if d not in devices:
6096			devices[d] = dict()
6097		if name not in devices[d]:
6098			devices[d][name] = 0.0
6099		devices[d][name] += float(time)
6100	# create worst device info
6101	worst = dict()
6102	for d in ['suspend', 'resume']:
6103		worst[d] = {'name':'', 'time': 0.0}
6104		dev = devices[d] if d in devices else 0
6105		if dev and len(dev.keys()) > 0:
6106			n = sorted(dev, key=lambda k:(dev[k], k), reverse=True)[0]
6107			worst[d]['name'], worst[d]['time'] = n, dev[n]
6108	data = {
6109		'mode': stmp[2],
6110		'host': stmp[0],
6111		'kernel': stmp[1],
6112		'sysinfo': sysinfo,
6113		'time': tstr,
6114		'result': result,
6115		'issues': ' '.join(ilist),
6116		'suspend': suspend,
6117		'resume': resume,
6118		'devlist': devices,
6119		'sus_worst': worst['suspend']['name'],
6120		'sus_worsttime': worst['suspend']['time'],
6121		'res_worst': worst['resume']['name'],
6122		'res_worsttime': worst['resume']['time'],
6123		'url': sysvals.htmlfile,
6124	}
6125	for key in extra:
6126		data[key] = extra[key]
6127	if fulldetail:
6128		data['funclist'] = find_in_html(html, '<div title="', '" class="traceevent"', False)
6129	return data
6130
6131def genHtml(subdir, force=False):
6132	for dirname, dirnames, filenames in os.walk(subdir):
6133		sysvals.dmesgfile = sysvals.ftracefile = sysvals.htmlfile = ''
6134		for filename in filenames:
6135			file = os.path.join(dirname, filename)
6136			if sysvals.usable(file):
6137				if(re.match('.*_dmesg.txt', filename)):
6138					sysvals.dmesgfile = file
6139				elif(re.match('.*_ftrace.txt', filename)):
6140					sysvals.ftracefile = file
6141		sysvals.setOutputFile()
6142		if (sysvals.dmesgfile or sysvals.ftracefile) and sysvals.htmlfile and \
6143			(force or not sysvals.usable(sysvals.htmlfile)):
6144			pprint('FTRACE: %s' % sysvals.ftracefile)
6145			if sysvals.dmesgfile:
6146				pprint('DMESG : %s' % sysvals.dmesgfile)
6147			rerunTest()
6148
6149# Function: runSummary
6150# Description:
6151#	 create a summary of tests in a sub-directory
6152def runSummary(subdir, local=True, genhtml=False):
6153	inpath = os.path.abspath(subdir)
6154	outpath = os.path.abspath('.') if local else inpath
6155	pprint('Generating a summary of folder:\n   %s' % inpath)
6156	if genhtml:
6157		genHtml(subdir)
6158	issues = []
6159	testruns = []
6160	desc = {'host':[],'mode':[],'kernel':[]}
6161	for dirname, dirnames, filenames in os.walk(subdir):
6162		for filename in filenames:
6163			if(not re.match('.*.html', filename)):
6164				continue
6165			data = data_from_html(os.path.join(dirname, filename), outpath, issues)
6166			if(not data):
6167				continue
6168			testruns.append(data)
6169			for key in desc:
6170				if data[key] not in desc[key]:
6171					desc[key].append(data[key])
6172	pprint('Summary files:')
6173	if len(desc['host']) == len(desc['mode']) == len(desc['kernel']) == 1:
6174		title = '%s %s %s' % (desc['host'][0], desc['kernel'][0], desc['mode'][0])
6175	else:
6176		title = inpath
6177	createHTMLSummarySimple(testruns, os.path.join(outpath, 'summary.html'), title)
6178	pprint('   summary.html         - tabular list of test data found')
6179	createHTMLDeviceSummary(testruns, os.path.join(outpath, 'summary-devices.html'), title)
6180	pprint('   summary-devices.html - kernel device list sorted by total execution time')
6181	createHTMLIssuesSummary(testruns, issues, os.path.join(outpath, 'summary-issues.html'), title)
6182	pprint('   summary-issues.html  - kernel issues found sorted by frequency')
6183
6184# Function: checkArgBool
6185# Description:
6186#	 check if a boolean string value is true or false
6187def checkArgBool(name, value):
6188	if value in switchvalues:
6189		if value in switchoff:
6190			return False
6191		return True
6192	doError('invalid boolean --> (%s: %s), use "true/false" or "1/0"' % (name, value), True)
6193	return False
6194
6195# Function: configFromFile
6196# Description:
6197#	 Configure the script via the info in a config file
6198def configFromFile(file):
6199	Config = configparser.ConfigParser()
6200
6201	Config.read(file)
6202	sections = Config.sections()
6203	overridekprobes = False
6204	overridedevkprobes = False
6205	if 'Settings' in sections:
6206		for opt in Config.options('Settings'):
6207			value = Config.get('Settings', opt).lower()
6208			option = opt.lower()
6209			if(option == 'verbose'):
6210				sysvals.verbose = checkArgBool(option, value)
6211			elif(option == 'addlogs'):
6212				sysvals.dmesglog = sysvals.ftracelog = checkArgBool(option, value)
6213			elif(option == 'dev'):
6214				sysvals.usedevsrc = checkArgBool(option, value)
6215			elif(option == 'proc'):
6216				sysvals.useprocmon = checkArgBool(option, value)
6217			elif(option == 'x2'):
6218				if checkArgBool(option, value):
6219					sysvals.execcount = 2
6220			elif(option == 'callgraph'):
6221				sysvals.usecallgraph = checkArgBool(option, value)
6222			elif(option == 'override-timeline-functions'):
6223				overridekprobes = checkArgBool(option, value)
6224			elif(option == 'override-dev-timeline-functions'):
6225				overridedevkprobes = checkArgBool(option, value)
6226			elif(option == 'skiphtml'):
6227				sysvals.skiphtml = checkArgBool(option, value)
6228			elif(option == 'sync'):
6229				sysvals.sync = checkArgBool(option, value)
6230			elif(option == 'rs' or option == 'runtimesuspend'):
6231				if value in switchvalues:
6232					if value in switchoff:
6233						sysvals.rs = -1
6234					else:
6235						sysvals.rs = 1
6236				else:
6237					doError('invalid value --> (%s: %s), use "enable/disable"' % (option, value), True)
6238			elif(option == 'display'):
6239				disopt = ['on', 'off', 'standby', 'suspend']
6240				if value not in disopt:
6241					doError('invalid value --> (%s: %s), use %s' % (option, value, disopt), True)
6242				sysvals.display = value
6243			elif(option == 'gzip'):
6244				sysvals.gzip = checkArgBool(option, value)
6245			elif(option == 'cgfilter'):
6246				sysvals.setCallgraphFilter(value)
6247			elif(option == 'cgskip'):
6248				if value in switchoff:
6249					sysvals.cgskip = ''
6250				else:
6251					sysvals.cgskip = sysvals.configFile(val)
6252					if(not sysvals.cgskip):
6253						doError('%s does not exist' % sysvals.cgskip)
6254			elif(option == 'cgtest'):
6255				sysvals.cgtest = getArgInt('cgtest', value, 0, 1, False)
6256			elif(option == 'cgphase'):
6257				d = Data(0)
6258				if value not in d.phasedef:
6259					doError('invalid phase --> (%s: %s), valid phases are %s'\
6260						% (option, value, d.phasedef.keys()), True)
6261				sysvals.cgphase = value
6262			elif(option == 'fadd'):
6263				file = sysvals.configFile(value)
6264				if(not file):
6265					doError('%s does not exist' % value)
6266				sysvals.addFtraceFilterFunctions(file)
6267			elif(option == 'result'):
6268				sysvals.result = value
6269			elif(option == 'multi'):
6270				nums = value.split()
6271				if len(nums) != 2:
6272					doError('multi requires 2 integers (exec_count and delay)', True)
6273				sysvals.multiinit(nums[0], nums[1])
6274			elif(option == 'devicefilter'):
6275				sysvals.setDeviceFilter(value)
6276			elif(option == 'expandcg'):
6277				sysvals.cgexp = checkArgBool(option, value)
6278			elif(option == 'srgap'):
6279				if checkArgBool(option, value):
6280					sysvals.srgap = 5
6281			elif(option == 'mode'):
6282				sysvals.suspendmode = value
6283			elif(option == 'command' or option == 'cmd'):
6284				sysvals.testcommand = value
6285			elif(option == 'x2delay'):
6286				sysvals.x2delay = getArgInt('x2delay', value, 0, 60000, False)
6287			elif(option == 'predelay'):
6288				sysvals.predelay = getArgInt('predelay', value, 0, 60000, False)
6289			elif(option == 'postdelay'):
6290				sysvals.postdelay = getArgInt('postdelay', value, 0, 60000, False)
6291			elif(option == 'maxdepth'):
6292				sysvals.max_graph_depth = getArgInt('maxdepth', value, 0, 1000, False)
6293			elif(option == 'rtcwake'):
6294				if value in switchoff:
6295					sysvals.rtcwake = False
6296				else:
6297					sysvals.rtcwake = True
6298					sysvals.rtcwaketime = getArgInt('rtcwake', value, 0, 3600, False)
6299			elif(option == 'timeprec'):
6300				sysvals.setPrecision(getArgInt('timeprec', value, 0, 6, False))
6301			elif(option == 'mindev'):
6302				sysvals.mindevlen = getArgFloat('mindev', value, 0.0, 10000.0, False)
6303			elif(option == 'callloop-maxgap'):
6304				sysvals.callloopmaxgap = getArgFloat('callloop-maxgap', value, 0.0, 1.0, False)
6305			elif(option == 'callloop-maxlen'):
6306				sysvals.callloopmaxgap = getArgFloat('callloop-maxlen', value, 0.0, 1.0, False)
6307			elif(option == 'mincg'):
6308				sysvals.mincglen = getArgFloat('mincg', value, 0.0, 10000.0, False)
6309			elif(option == 'bufsize'):
6310				sysvals.bufsize = getArgInt('bufsize', value, 1, 1024*1024*8, False)
6311			elif(option == 'output-dir'):
6312				sysvals.outdir = sysvals.setOutputFolder(value)
6313
6314	if sysvals.suspendmode == 'command' and not sysvals.testcommand:
6315		doError('No command supplied for mode "command"')
6316
6317	# compatibility errors
6318	if sysvals.usedevsrc and sysvals.usecallgraph:
6319		doError('-dev is not compatible with -f')
6320	if sysvals.usecallgraph and sysvals.useprocmon:
6321		doError('-proc is not compatible with -f')
6322
6323	if overridekprobes:
6324		sysvals.tracefuncs = dict()
6325	if overridedevkprobes:
6326		sysvals.dev_tracefuncs = dict()
6327
6328	kprobes = dict()
6329	kprobesec = 'dev_timeline_functions_'+platform.machine()
6330	if kprobesec in sections:
6331		for name in Config.options(kprobesec):
6332			text = Config.get(kprobesec, name)
6333			kprobes[name] = (text, True)
6334	kprobesec = 'timeline_functions_'+platform.machine()
6335	if kprobesec in sections:
6336		for name in Config.options(kprobesec):
6337			if name in kprobes:
6338				doError('Duplicate timeline function found "%s"' % (name))
6339			text = Config.get(kprobesec, name)
6340			kprobes[name] = (text, False)
6341
6342	for name in kprobes:
6343		function = name
6344		format = name
6345		color = ''
6346		args = dict()
6347		text, dev = kprobes[name]
6348		data = text.split()
6349		i = 0
6350		for val in data:
6351			# bracketted strings are special formatting, read them separately
6352			if val[0] == '[' and val[-1] == ']':
6353				for prop in val[1:-1].split(','):
6354					p = prop.split('=')
6355					if p[0] == 'color':
6356						try:
6357							color = int(p[1], 16)
6358							color = '#'+p[1]
6359						except:
6360							color = p[1]
6361				continue
6362			# first real arg should be the format string
6363			if i == 0:
6364				format = val
6365			# all other args are actual function args
6366			else:
6367				d = val.split('=')
6368				args[d[0]] = d[1]
6369			i += 1
6370		if not function or not format:
6371			doError('Invalid kprobe: %s' % name)
6372		for arg in re.findall('{(?P<n>[a-z,A-Z,0-9]*)}', format):
6373			if arg not in args:
6374				doError('Kprobe "%s" is missing argument "%s"' % (name, arg))
6375		if (dev and name in sysvals.dev_tracefuncs) or (not dev and name in sysvals.tracefuncs):
6376			doError('Duplicate timeline function found "%s"' % (name))
6377
6378		kp = {
6379			'name': name,
6380			'func': function,
6381			'format': format,
6382			sysvals.archargs: args
6383		}
6384		if color:
6385			kp['color'] = color
6386		if dev:
6387			sysvals.dev_tracefuncs[name] = kp
6388		else:
6389			sysvals.tracefuncs[name] = kp
6390
6391# Function: printHelp
6392# Description:
6393#	 print out the help text
6394def printHelp():
6395	pprint('\n%s v%s\n'\
6396	'Usage: sudo sleepgraph <options> <commands>\n'\
6397	'\n'\
6398	'Description:\n'\
6399	'  This tool is designed to assist kernel and OS developers in optimizing\n'\
6400	'  their linux stack\'s suspend/resume time. Using a kernel image built\n'\
6401	'  with a few extra options enabled, the tool will execute a suspend and\n'\
6402	'  capture dmesg and ftrace data until resume is complete. This data is\n'\
6403	'  transformed into a device timeline and an optional callgraph to give\n'\
6404	'  a detailed view of which devices/subsystems are taking the most\n'\
6405	'  time in suspend/resume.\n'\
6406	'\n'\
6407	'  If no specific command is given, the default behavior is to initiate\n'\
6408	'  a suspend/resume and capture the dmesg/ftrace output as an html timeline.\n'\
6409	'\n'\
6410	'  Generates output files in subdirectory: suspend-yymmdd-HHMMSS\n'\
6411	'   HTML output:                    <hostname>_<mode>.html\n'\
6412	'   raw dmesg output:               <hostname>_<mode>_dmesg.txt\n'\
6413	'   raw ftrace output:              <hostname>_<mode>_ftrace.txt\n'\
6414	'\n'\
6415	'Options:\n'\
6416	'   -h           Print this help text\n'\
6417	'   -v           Print the current tool version\n'\
6418	'   -config fn   Pull arguments and config options from file fn\n'\
6419	'   -verbose     Print extra information during execution and analysis\n'\
6420	'   -m mode      Mode to initiate for suspend (default: %s)\n'\
6421	'   -o name      Overrides the output subdirectory name when running a new test\n'\
6422	'                default: suspend-{date}-{time}\n'\
6423	'   -rtcwake t   Wakeup t seconds after suspend, set t to "off" to disable (default: 15)\n'\
6424	'   -addlogs     Add the dmesg and ftrace logs to the html output\n'\
6425	'   -noturbostat Dont use turbostat in freeze mode (default: disabled)\n'\
6426	'   -srgap       Add a visible gap in the timeline between sus/res (default: disabled)\n'\
6427	'   -skiphtml    Run the test and capture the trace logs, but skip the timeline (default: disabled)\n'\
6428	'   -result fn   Export a results table to a text file for parsing.\n'\
6429	'   -wifi        If a wifi connection is available, check that it reconnects after resume.\n'\
6430	'  [testprep]\n'\
6431	'   -sync        Sync the filesystems before starting the test\n'\
6432	'   -rs on/off   Enable/disable runtime suspend for all devices, restore all after test\n'\
6433	'   -display m   Change the display mode to m for the test (on/off/standby/suspend)\n'\
6434	'  [advanced]\n'\
6435	'   -gzip        Gzip the trace and dmesg logs to save space\n'\
6436	'   -cmd {s}     Run the timeline over a custom command, e.g. "sync -d"\n'\
6437	'   -proc        Add usermode process info into the timeline (default: disabled)\n'\
6438	'   -dev         Add kernel function calls and threads to the timeline (default: disabled)\n'\
6439	'   -x2          Run two suspend/resumes back to back (default: disabled)\n'\
6440	'   -x2delay t   Include t ms delay between multiple test runs (default: 0 ms)\n'\
6441	'   -predelay t  Include t ms delay before 1st suspend (default: 0 ms)\n'\
6442	'   -postdelay t Include t ms delay after last resume (default: 0 ms)\n'\
6443	'   -mindev ms   Discard all device blocks shorter than ms milliseconds (e.g. 0.001 for us)\n'\
6444	'   -multi n d   Execute <n> consecutive tests at <d> seconds intervals. If <n> is followed\n'\
6445	'                by a "d", "h", or "m" execute for <n> days, hours, or mins instead.\n'\
6446	'                The outputs will be created in a new subdirectory with a summary page.\n'\
6447	'   -maxfail n   Abort a -multi run after n consecutive fails (default is 0 = never abort)\n'\
6448	'  [debug]\n'\
6449	'   -f           Use ftrace to create device callgraphs (default: disabled)\n'\
6450	'   -ftop        Use ftrace on the top level call: "%s" (default: disabled)\n'\
6451	'   -maxdepth N  limit the callgraph data to N call levels (default: 0=all)\n'\
6452	'   -expandcg    pre-expand the callgraph data in the html output (default: disabled)\n'\
6453	'   -fadd file   Add functions to be graphed in the timeline from a list in a text file\n'\
6454	'   -filter "d1,d2,..." Filter out all but this comma-delimited list of device names\n'\
6455	'   -mincg  ms   Discard all callgraphs shorter than ms milliseconds (e.g. 0.001 for us)\n'\
6456	'   -cgphase P   Only show callgraph data for phase P (e.g. suspend_late)\n'\
6457	'   -cgtest N    Only show callgraph data for test N (e.g. 0 or 1 in an x2 run)\n'\
6458	'   -timeprec N  Number of significant digits in timestamps (0:S, [3:ms], 6:us)\n'\
6459	'   -cgfilter S  Filter the callgraph output in the timeline\n'\
6460	'   -cgskip file Callgraph functions to skip, off to disable (default: cgskip.txt)\n'\
6461	'   -bufsize N   Set trace buffer size to N kilo-bytes (default: all of free memory)\n'\
6462	'   -devdump     Print out all the raw device data for each phase\n'\
6463	'   -cgdump      Print out all the raw callgraph data\n'\
6464	'\n'\
6465	'Other commands:\n'\
6466	'   -modes       List available suspend modes\n'\
6467	'   -status      Test to see if the system is enabled to run this tool\n'\
6468	'   -fpdt        Print out the contents of the ACPI Firmware Performance Data Table\n'\
6469	'   -wificheck   Print out wifi connection info\n'\
6470	'   -x<mode>     Test xset by toggling the given mode (on/off/standby/suspend)\n'\
6471	'   -sysinfo     Print out system info extracted from BIOS\n'\
6472	'   -devinfo     Print out the pm settings of all devices which support runtime suspend\n'\
6473	'   -cmdinfo     Print out all the platform info collected before and after suspend/resume\n'\
6474	'   -flist       Print the list of functions currently being captured in ftrace\n'\
6475	'   -flistall    Print all functions capable of being captured in ftrace\n'\
6476	'   -summary dir Create a summary of tests in this dir [-genhtml builds missing html]\n'\
6477	'  [redo]\n'\
6478	'   -ftrace ftracefile  Create HTML output using ftrace input (used with -dmesg)\n'\
6479	'   -dmesg dmesgfile    Create HTML output using dmesg (used with -ftrace)\n'\
6480	'' % (sysvals.title, sysvals.version, sysvals.suspendmode, sysvals.ftopfunc))
6481	return True
6482
6483# ----------------- MAIN --------------------
6484# exec start (skipped if script is loaded as library)
6485if __name__ == '__main__':
6486	genhtml = False
6487	cmd = ''
6488	simplecmds = ['-sysinfo', '-modes', '-fpdt', '-flist', '-flistall',
6489		'-devinfo', '-status', '-xon', '-xoff', '-xstandby', '-xsuspend',
6490		'-xinit', '-xreset', '-xstat', '-wificheck', '-cmdinfo']
6491	if '-f' in sys.argv:
6492		sysvals.cgskip = sysvals.configFile('cgskip.txt')
6493	# loop through the command line arguments
6494	args = iter(sys.argv[1:])
6495	for arg in args:
6496		if(arg == '-m'):
6497			try:
6498				val = next(args)
6499			except:
6500				doError('No mode supplied', True)
6501			if val == 'command' and not sysvals.testcommand:
6502				doError('No command supplied for mode "command"', True)
6503			sysvals.suspendmode = val
6504		elif(arg in simplecmds):
6505			cmd = arg[1:]
6506		elif(arg == '-h'):
6507			printHelp()
6508			sys.exit(0)
6509		elif(arg == '-v'):
6510			pprint("Version %s" % sysvals.version)
6511			sys.exit(0)
6512		elif(arg == '-x2'):
6513			sysvals.execcount = 2
6514		elif(arg == '-x2delay'):
6515			sysvals.x2delay = getArgInt('-x2delay', args, 0, 60000)
6516		elif(arg == '-predelay'):
6517			sysvals.predelay = getArgInt('-predelay', args, 0, 60000)
6518		elif(arg == '-postdelay'):
6519			sysvals.postdelay = getArgInt('-postdelay', args, 0, 60000)
6520		elif(arg == '-f'):
6521			sysvals.usecallgraph = True
6522		elif(arg == '-ftop'):
6523			sysvals.usecallgraph = True
6524			sysvals.ftop = True
6525			sysvals.usekprobes = False
6526		elif(arg == '-skiphtml'):
6527			sysvals.skiphtml = True
6528		elif(arg == '-cgdump'):
6529			sysvals.cgdump = True
6530		elif(arg == '-devdump'):
6531			sysvals.devdump = True
6532		elif(arg == '-genhtml'):
6533			genhtml = True
6534		elif(arg == '-addlogs'):
6535			sysvals.dmesglog = sysvals.ftracelog = True
6536		elif(arg == '-nologs'):
6537			sysvals.dmesglog = sysvals.ftracelog = False
6538		elif(arg == '-addlogdmesg'):
6539			sysvals.dmesglog = True
6540		elif(arg == '-addlogftrace'):
6541			sysvals.ftracelog = True
6542		elif(arg == '-noturbostat'):
6543			sysvals.tstat = False
6544		elif(arg == '-verbose'):
6545			sysvals.verbose = True
6546		elif(arg == '-proc'):
6547			sysvals.useprocmon = True
6548		elif(arg == '-dev'):
6549			sysvals.usedevsrc = True
6550		elif(arg == '-sync'):
6551			sysvals.sync = True
6552		elif(arg == '-wifi'):
6553			sysvals.wifi = True
6554		elif(arg == '-gzip'):
6555			sysvals.gzip = True
6556		elif(arg == '-info'):
6557			try:
6558				val = next(args)
6559			except:
6560				doError('-info requires one string argument', True)
6561		elif(arg == '-rs'):
6562			try:
6563				val = next(args)
6564			except:
6565				doError('-rs requires "enable" or "disable"', True)
6566			if val.lower() in switchvalues:
6567				if val.lower() in switchoff:
6568					sysvals.rs = -1
6569				else:
6570					sysvals.rs = 1
6571			else:
6572				doError('invalid option: %s, use "enable/disable" or "on/off"' % val, True)
6573		elif(arg == '-display'):
6574			try:
6575				val = next(args)
6576			except:
6577				doError('-display requires an mode value', True)
6578			disopt = ['on', 'off', 'standby', 'suspend']
6579			if val.lower() not in disopt:
6580				doError('valid display mode values are %s' % disopt, True)
6581			sysvals.display = val.lower()
6582		elif(arg == '-maxdepth'):
6583			sysvals.max_graph_depth = getArgInt('-maxdepth', args, 0, 1000)
6584		elif(arg == '-rtcwake'):
6585			try:
6586				val = next(args)
6587			except:
6588				doError('No rtcwake time supplied', True)
6589			if val.lower() in switchoff:
6590				sysvals.rtcwake = False
6591			else:
6592				sysvals.rtcwake = True
6593				sysvals.rtcwaketime = getArgInt('-rtcwake', val, 0, 3600, False)
6594		elif(arg == '-timeprec'):
6595			sysvals.setPrecision(getArgInt('-timeprec', args, 0, 6))
6596		elif(arg == '-mindev'):
6597			sysvals.mindevlen = getArgFloat('-mindev', args, 0.0, 10000.0)
6598		elif(arg == '-mincg'):
6599			sysvals.mincglen = getArgFloat('-mincg', args, 0.0, 10000.0)
6600		elif(arg == '-bufsize'):
6601			sysvals.bufsize = getArgInt('-bufsize', args, 1, 1024*1024*8)
6602		elif(arg == '-cgtest'):
6603			sysvals.cgtest = getArgInt('-cgtest', args, 0, 1)
6604		elif(arg == '-cgphase'):
6605			try:
6606				val = next(args)
6607			except:
6608				doError('No phase name supplied', True)
6609			d = Data(0)
6610			if val not in d.phasedef:
6611				doError('invalid phase --> (%s: %s), valid phases are %s'\
6612					% (arg, val, d.phasedef.keys()), True)
6613			sysvals.cgphase = val
6614		elif(arg == '-cgfilter'):
6615			try:
6616				val = next(args)
6617			except:
6618				doError('No callgraph functions supplied', True)
6619			sysvals.setCallgraphFilter(val)
6620		elif(arg == '-skipkprobe'):
6621			try:
6622				val = next(args)
6623			except:
6624				doError('No kprobe functions supplied', True)
6625			sysvals.skipKprobes(val)
6626		elif(arg == '-cgskip'):
6627			try:
6628				val = next(args)
6629			except:
6630				doError('No file supplied', True)
6631			if val.lower() in switchoff:
6632				sysvals.cgskip = ''
6633			else:
6634				sysvals.cgskip = sysvals.configFile(val)
6635				if(not sysvals.cgskip):
6636					doError('%s does not exist' % sysvals.cgskip)
6637		elif(arg == '-callloop-maxgap'):
6638			sysvals.callloopmaxgap = getArgFloat('-callloop-maxgap', args, 0.0, 1.0)
6639		elif(arg == '-callloop-maxlen'):
6640			sysvals.callloopmaxlen = getArgFloat('-callloop-maxlen', args, 0.0, 1.0)
6641		elif(arg == '-cmd'):
6642			try:
6643				val = next(args)
6644			except:
6645				doError('No command string supplied', True)
6646			sysvals.testcommand = val
6647			sysvals.suspendmode = 'command'
6648		elif(arg == '-expandcg'):
6649			sysvals.cgexp = True
6650		elif(arg == '-srgap'):
6651			sysvals.srgap = 5
6652		elif(arg == '-maxfail'):
6653			sysvals.maxfail = getArgInt('-maxfail', args, 0, 1000000)
6654		elif(arg == '-multi'):
6655			try:
6656				c, d = next(args), next(args)
6657			except:
6658				doError('-multi requires two values', True)
6659			sysvals.multiinit(c, d)
6660		elif(arg == '-o'):
6661			try:
6662				val = next(args)
6663			except:
6664				doError('No subdirectory name supplied', True)
6665			sysvals.outdir = sysvals.setOutputFolder(val)
6666		elif(arg == '-config'):
6667			try:
6668				val = next(args)
6669			except:
6670				doError('No text file supplied', True)
6671			file = sysvals.configFile(val)
6672			if(not file):
6673				doError('%s does not exist' % val)
6674			configFromFile(file)
6675		elif(arg == '-fadd'):
6676			try:
6677				val = next(args)
6678			except:
6679				doError('No text file supplied', True)
6680			file = sysvals.configFile(val)
6681			if(not file):
6682				doError('%s does not exist' % val)
6683			sysvals.addFtraceFilterFunctions(file)
6684		elif(arg == '-dmesg'):
6685			try:
6686				val = next(args)
6687			except:
6688				doError('No dmesg file supplied', True)
6689			sysvals.notestrun = True
6690			sysvals.dmesgfile = val
6691			if(os.path.exists(sysvals.dmesgfile) == False):
6692				doError('%s does not exist' % sysvals.dmesgfile)
6693		elif(arg == '-ftrace'):
6694			try:
6695				val = next(args)
6696			except:
6697				doError('No ftrace file supplied', True)
6698			sysvals.notestrun = True
6699			sysvals.ftracefile = val
6700			if(os.path.exists(sysvals.ftracefile) == False):
6701				doError('%s does not exist' % sysvals.ftracefile)
6702		elif(arg == '-summary'):
6703			try:
6704				val = next(args)
6705			except:
6706				doError('No directory supplied', True)
6707			cmd = 'summary'
6708			sysvals.outdir = val
6709			sysvals.notestrun = True
6710			if(os.path.isdir(val) == False):
6711				doError('%s is not accesible' % val)
6712		elif(arg == '-filter'):
6713			try:
6714				val = next(args)
6715			except:
6716				doError('No devnames supplied', True)
6717			sysvals.setDeviceFilter(val)
6718		elif(arg == '-result'):
6719			try:
6720				val = next(args)
6721			except:
6722				doError('No result file supplied', True)
6723			sysvals.result = val
6724			sysvals.signalHandlerInit()
6725		else:
6726			doError('Invalid argument: '+arg, True)
6727
6728	# compatibility errors
6729	if(sysvals.usecallgraph and sysvals.usedevsrc):
6730		doError('-dev is not compatible with -f')
6731	if(sysvals.usecallgraph and sysvals.useprocmon):
6732		doError('-proc is not compatible with -f')
6733
6734	if sysvals.usecallgraph and sysvals.cgskip:
6735		sysvals.vprint('Using cgskip file: %s' % sysvals.cgskip)
6736		sysvals.setCallgraphBlacklist(sysvals.cgskip)
6737
6738	# callgraph size cannot exceed device size
6739	if sysvals.mincglen < sysvals.mindevlen:
6740		sysvals.mincglen = sysvals.mindevlen
6741
6742	# remove existing buffers before calculating memory
6743	if(sysvals.usecallgraph or sysvals.usedevsrc):
6744		sysvals.fsetVal('16', 'buffer_size_kb')
6745	sysvals.cpuInfo()
6746
6747	# just run a utility command and exit
6748	if(cmd != ''):
6749		ret = 0
6750		if(cmd == 'status'):
6751			if not statusCheck(True):
6752				ret = 1
6753		elif(cmd == 'fpdt'):
6754			if not getFPDT(True):
6755				ret = 1
6756		elif(cmd == 'sysinfo'):
6757			sysvals.printSystemInfo(True)
6758		elif(cmd == 'devinfo'):
6759			deviceInfo()
6760		elif(cmd == 'modes'):
6761			pprint(getModes())
6762		elif(cmd == 'flist'):
6763			sysvals.getFtraceFilterFunctions(True)
6764		elif(cmd == 'flistall'):
6765			sysvals.getFtraceFilterFunctions(False)
6766		elif(cmd == 'summary'):
6767			runSummary(sysvals.outdir, True, genhtml)
6768		elif(cmd in ['xon', 'xoff', 'xstandby', 'xsuspend', 'xinit', 'xreset']):
6769			sysvals.verbose = True
6770			ret = displayControl(cmd[1:])
6771		elif(cmd == 'xstat'):
6772			pprint('Display Status: %s' % displayControl('stat').upper())
6773		elif(cmd == 'wificheck'):
6774			dev = sysvals.checkWifi()
6775			if dev:
6776				print('%s is connected' % sysvals.wifiDetails(dev))
6777			else:
6778				print('No wifi connection found')
6779		elif(cmd == 'cmdinfo'):
6780			for out in sysvals.cmdinfo(False, True):
6781				print('[%s - %s]\n%s\n' % out)
6782		sys.exit(ret)
6783
6784	# if instructed, re-analyze existing data files
6785	if(sysvals.notestrun):
6786		stamp = rerunTest(sysvals.outdir)
6787		sysvals.outputResult(stamp)
6788		sys.exit(0)
6789
6790	# verify that we can run a test
6791	error = statusCheck()
6792	if(error):
6793		doError(error)
6794
6795	# extract mem/disk extra modes and convert
6796	mode = sysvals.suspendmode
6797	if mode.startswith('mem'):
6798		memmode = mode.split('-', 1)[-1] if '-' in mode else 'deep'
6799		if memmode == 'shallow':
6800			mode = 'standby'
6801		elif memmode ==  's2idle':
6802			mode = 'freeze'
6803		else:
6804			mode = 'mem'
6805		sysvals.memmode = memmode
6806		sysvals.suspendmode = mode
6807	if mode.startswith('disk-'):
6808		sysvals.diskmode = mode.split('-', 1)[-1]
6809		sysvals.suspendmode = 'disk'
6810
6811	sysvals.systemInfo(dmidecode(sysvals.mempath))
6812
6813	setRuntimeSuspend(True)
6814	if sysvals.display:
6815		displayControl('init')
6816	failcnt, ret = 0, 0
6817	if sysvals.multitest['run']:
6818		# run multiple tests in a separate subdirectory
6819		if not sysvals.outdir:
6820			if 'time' in sysvals.multitest:
6821				s = '-%dm' % sysvals.multitest['time']
6822			else:
6823				s = '-x%d' % sysvals.multitest['count']
6824			sysvals.outdir = datetime.now().strftime('suspend-%y%m%d-%H%M%S'+s)
6825		if not os.path.isdir(sysvals.outdir):
6826			os.makedirs(sysvals.outdir)
6827		sysvals.sudoUserchown(sysvals.outdir)
6828		finish = datetime.now()
6829		if 'time' in sysvals.multitest:
6830			finish += timedelta(minutes=sysvals.multitest['time'])
6831		for i in range(sysvals.multitest['count']):
6832			sysvals.multistat(True, i, finish)
6833			if i != 0 and sysvals.multitest['delay'] > 0:
6834				pprint('Waiting %d seconds...' % (sysvals.multitest['delay']))
6835				time.sleep(sysvals.multitest['delay'])
6836			fmt = 'suspend-%y%m%d-%H%M%S'
6837			sysvals.testdir = os.path.join(sysvals.outdir, datetime.now().strftime(fmt))
6838			ret = runTest(i+1, True)
6839			failcnt = 0 if not ret else failcnt + 1
6840			if sysvals.maxfail > 0 and failcnt >= sysvals.maxfail:
6841				pprint('Maximum fail count of %d reached, aborting multitest' % (sysvals.maxfail))
6842				break
6843			time.sleep(5)
6844			sysvals.resetlog()
6845			sysvals.multistat(False, i, finish)
6846			if 'time' in sysvals.multitest and datetime.now() >= finish:
6847				break
6848		if not sysvals.skiphtml:
6849			runSummary(sysvals.outdir, False, False)
6850		sysvals.sudoUserchown(sysvals.outdir)
6851	else:
6852		if sysvals.outdir:
6853			sysvals.testdir = sysvals.outdir
6854		# run the test in the current directory
6855		ret = runTest()
6856	if sysvals.display:
6857		displayControl('reset')
6858	setRuntimeSuspend(False)
6859	sys.exit(ret)
6860