1#!/usr/bin/env python2
2# SPDX-License-Identifier: GPL-2.0
3# exported-sql-viewer.py: view data from sql database
4# Copyright (c) 2014-2018, Intel Corporation.
5
6# To use this script you will need to have exported data using either the
7# export-to-sqlite.py or the export-to-postgresql.py script.  Refer to those
8# scripts for details.
9#
10# Following on from the example in the export scripts, a
11# call-graph can be displayed for the pt_example database like this:
12#
13#	python tools/perf/scripts/python/exported-sql-viewer.py pt_example
14#
15# Note that for PostgreSQL, this script supports connecting to remote databases
16# by setting hostname, port, username, password, and dbname e.g.
17#
18#	python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
19#
20# The result is a GUI window with a tree representing a context-sensitive
21# call-graph.  Expanding a couple of levels of the tree and adjusting column
22# widths to suit will display something like:
23#
24#                                         Call Graph: pt_example
25# Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
26# v- ls
27#     v- 2638:2638
28#         v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
29#           |- unknown               unknown       1        13198     0.1              1              0.0
30#           >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
31#           >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
32#           v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
33#              >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
34#              >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
35#              >- __libc_csu_init    ls            1        10354     0.1             10              0.0
36#              |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
37#              v- main               ls            1      8182043    99.6         180254             99.9
38#
39# Points to note:
40#	The top level is a command name (comm)
41#	The next level is a thread (pid:tid)
42#	Subsequent levels are functions
43#	'Count' is the number of calls
44#	'Time' is the elapsed time until the function returns
45#	Percentages are relative to the level above
46#	'Branch Count' is the total number of branches for that function and all
47#       functions that it calls
48
49# There is also a "All branches" report, which displays branches and
50# possibly disassembly.  However, presently, the only supported disassembler is
51# Intel XED, and additionally the object code must be present in perf build ID
52# cache. To use Intel XED, libxed.so must be present. To build and install
53# libxed.so:
54#            git clone https://github.com/intelxed/mbuild.git mbuild
55#            git clone https://github.com/intelxed/xed
56#            cd xed
57#            ./mfile.py --share
58#            sudo ./mfile.py --prefix=/usr/local install
59#            sudo ldconfig
60#
61# Example report:
62#
63# Time           CPU  Command  PID    TID    Branch Type            In Tx  Branch
64# 8107675239590  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
65#                                                                              7fab593ea260 48 89 e7                                        mov %rsp, %rdi
66# 8107675239899  2    ls       22011  22011  hardware interrupt     No         7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
67# 8107675241900  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
68#                                                                              7fab593ea260 48 89 e7                                        mov %rsp, %rdi
69#                                                                              7fab593ea263 e8 c8 06 00 00                                  callq  0x7fab593ea930
70# 8107675241900  2    ls       22011  22011  call                   No         7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
71#                                                                              7fab593ea930 55                                              pushq  %rbp
72#                                                                              7fab593ea931 48 89 e5                                        mov %rsp, %rbp
73#                                                                              7fab593ea934 41 57                                           pushq  %r15
74#                                                                              7fab593ea936 41 56                                           pushq  %r14
75#                                                                              7fab593ea938 41 55                                           pushq  %r13
76#                                                                              7fab593ea93a 41 54                                           pushq  %r12
77#                                                                              7fab593ea93c 53                                              pushq  %rbx
78#                                                                              7fab593ea93d 48 89 fb                                        mov %rdi, %rbx
79#                                                                              7fab593ea940 48 83 ec 68                                     sub $0x68, %rsp
80#                                                                              7fab593ea944 0f 31                                           rdtsc
81#                                                                              7fab593ea946 48 c1 e2 20                                     shl $0x20, %rdx
82#                                                                              7fab593ea94a 89 c0                                           mov %eax, %eax
83#                                                                              7fab593ea94c 48 09 c2                                        or %rax, %rdx
84#                                                                              7fab593ea94f 48 8b 05 1a 15 22 00                            movq  0x22151a(%rip), %rax
85# 8107675242232  2    ls       22011  22011  hardware interrupt     No         7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
86# 8107675242900  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
87#                                                                              7fab593ea94f 48 8b 05 1a 15 22 00                            movq  0x22151a(%rip), %rax
88#                                                                              7fab593ea956 48 89 15 3b 13 22 00                            movq  %rdx, 0x22133b(%rip)
89# 8107675243232  2    ls       22011  22011  hardware interrupt     No         7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
90
91from __future__ import print_function
92
93import sys
94import weakref
95import threading
96import string
97try:
98	# Python2
99	import cPickle as pickle
100	# size of pickled integer big enough for record size
101	glb_nsz = 8
102except ImportError:
103	import pickle
104	glb_nsz = 16
105import re
106import os
107from PySide.QtCore import *
108from PySide.QtGui import *
109from PySide.QtSql import *
110pyside_version_1 = True
111from decimal import *
112from ctypes import *
113from multiprocessing import Process, Array, Value, Event
114
115# xrange is range in Python3
116try:
117	xrange
118except NameError:
119	xrange = range
120
121def printerr(*args, **keyword_args):
122	print(*args, file=sys.stderr, **keyword_args)
123
124# Data formatting helpers
125
126def tohex(ip):
127	if ip < 0:
128		ip += 1 << 64
129	return "%x" % ip
130
131def offstr(offset):
132	if offset:
133		return "+0x%x" % offset
134	return ""
135
136def dsoname(name):
137	if name == "[kernel.kallsyms]":
138		return "[kernel]"
139	return name
140
141def findnth(s, sub, n, offs=0):
142	pos = s.find(sub)
143	if pos < 0:
144		return pos
145	if n <= 1:
146		return offs + pos
147	return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
148
149# Percent to one decimal place
150
151def PercentToOneDP(n, d):
152	if not d:
153		return "0.0"
154	x = (n * Decimal(100)) / d
155	return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
156
157# Helper for queries that must not fail
158
159def QueryExec(query, stmt):
160	ret = query.exec_(stmt)
161	if not ret:
162		raise Exception("Query failed: " + query.lastError().text())
163
164# Background thread
165
166class Thread(QThread):
167
168	done = Signal(object)
169
170	def __init__(self, task, param=None, parent=None):
171		super(Thread, self).__init__(parent)
172		self.task = task
173		self.param = param
174
175	def run(self):
176		while True:
177			if self.param is None:
178				done, result = self.task()
179			else:
180				done, result = self.task(self.param)
181			self.done.emit(result)
182			if done:
183				break
184
185# Tree data model
186
187class TreeModel(QAbstractItemModel):
188
189	def __init__(self, glb, parent=None):
190		super(TreeModel, self).__init__(parent)
191		self.glb = glb
192		self.root = self.GetRoot()
193		self.last_row_read = 0
194
195	def Item(self, parent):
196		if parent.isValid():
197			return parent.internalPointer()
198		else:
199			return self.root
200
201	def rowCount(self, parent):
202		result = self.Item(parent).childCount()
203		if result < 0:
204			result = 0
205			self.dataChanged.emit(parent, parent)
206		return result
207
208	def hasChildren(self, parent):
209		return self.Item(parent).hasChildren()
210
211	def headerData(self, section, orientation, role):
212		if role == Qt.TextAlignmentRole:
213			return self.columnAlignment(section)
214		if role != Qt.DisplayRole:
215			return None
216		if orientation != Qt.Horizontal:
217			return None
218		return self.columnHeader(section)
219
220	def parent(self, child):
221		child_item = child.internalPointer()
222		if child_item is self.root:
223			return QModelIndex()
224		parent_item = child_item.getParentItem()
225		return self.createIndex(parent_item.getRow(), 0, parent_item)
226
227	def index(self, row, column, parent):
228		child_item = self.Item(parent).getChildItem(row)
229		return self.createIndex(row, column, child_item)
230
231	def DisplayData(self, item, index):
232		return item.getData(index.column())
233
234	def FetchIfNeeded(self, row):
235		if row > self.last_row_read:
236			self.last_row_read = row
237			if row + 10 >= self.root.child_count:
238				self.fetcher.Fetch(glb_chunk_sz)
239
240	def columnAlignment(self, column):
241		return Qt.AlignLeft
242
243	def columnFont(self, column):
244		return None
245
246	def data(self, index, role):
247		if role == Qt.TextAlignmentRole:
248			return self.columnAlignment(index.column())
249		if role == Qt.FontRole:
250			return self.columnFont(index.column())
251		if role != Qt.DisplayRole:
252			return None
253		item = index.internalPointer()
254		return self.DisplayData(item, index)
255
256# Table data model
257
258class TableModel(QAbstractTableModel):
259
260	def __init__(self, parent=None):
261		super(TableModel, self).__init__(parent)
262		self.child_count = 0
263		self.child_items = []
264		self.last_row_read = 0
265
266	def Item(self, parent):
267		if parent.isValid():
268			return parent.internalPointer()
269		else:
270			return self
271
272	def rowCount(self, parent):
273		return self.child_count
274
275	def headerData(self, section, orientation, role):
276		if role == Qt.TextAlignmentRole:
277			return self.columnAlignment(section)
278		if role != Qt.DisplayRole:
279			return None
280		if orientation != Qt.Horizontal:
281			return None
282		return self.columnHeader(section)
283
284	def index(self, row, column, parent):
285		return self.createIndex(row, column, self.child_items[row])
286
287	def DisplayData(self, item, index):
288		return item.getData(index.column())
289
290	def FetchIfNeeded(self, row):
291		if row > self.last_row_read:
292			self.last_row_read = row
293			if row + 10 >= self.child_count:
294				self.fetcher.Fetch(glb_chunk_sz)
295
296	def columnAlignment(self, column):
297		return Qt.AlignLeft
298
299	def columnFont(self, column):
300		return None
301
302	def data(self, index, role):
303		if role == Qt.TextAlignmentRole:
304			return self.columnAlignment(index.column())
305		if role == Qt.FontRole:
306			return self.columnFont(index.column())
307		if role != Qt.DisplayRole:
308			return None
309		item = index.internalPointer()
310		return self.DisplayData(item, index)
311
312# Model cache
313
314model_cache = weakref.WeakValueDictionary()
315model_cache_lock = threading.Lock()
316
317def LookupCreateModel(model_name, create_fn):
318	model_cache_lock.acquire()
319	try:
320		model = model_cache[model_name]
321	except:
322		model = None
323	if model is None:
324		model = create_fn()
325		model_cache[model_name] = model
326	model_cache_lock.release()
327	return model
328
329# Find bar
330
331class FindBar():
332
333	def __init__(self, parent, finder, is_reg_expr=False):
334		self.finder = finder
335		self.context = []
336		self.last_value = None
337		self.last_pattern = None
338
339		label = QLabel("Find:")
340		label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
341
342		self.textbox = QComboBox()
343		self.textbox.setEditable(True)
344		self.textbox.currentIndexChanged.connect(self.ValueChanged)
345
346		self.progress = QProgressBar()
347		self.progress.setRange(0, 0)
348		self.progress.hide()
349
350		if is_reg_expr:
351			self.pattern = QCheckBox("Regular Expression")
352		else:
353			self.pattern = QCheckBox("Pattern")
354		self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
355
356		self.next_button = QToolButton()
357		self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
358		self.next_button.released.connect(lambda: self.NextPrev(1))
359
360		self.prev_button = QToolButton()
361		self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
362		self.prev_button.released.connect(lambda: self.NextPrev(-1))
363
364		self.close_button = QToolButton()
365		self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
366		self.close_button.released.connect(self.Deactivate)
367
368		self.hbox = QHBoxLayout()
369		self.hbox.setContentsMargins(0, 0, 0, 0)
370
371		self.hbox.addWidget(label)
372		self.hbox.addWidget(self.textbox)
373		self.hbox.addWidget(self.progress)
374		self.hbox.addWidget(self.pattern)
375		self.hbox.addWidget(self.next_button)
376		self.hbox.addWidget(self.prev_button)
377		self.hbox.addWidget(self.close_button)
378
379		self.bar = QWidget()
380		self.bar.setLayout(self.hbox);
381		self.bar.hide()
382
383	def Widget(self):
384		return self.bar
385
386	def Activate(self):
387		self.bar.show()
388		self.textbox.setFocus()
389
390	def Deactivate(self):
391		self.bar.hide()
392
393	def Busy(self):
394		self.textbox.setEnabled(False)
395		self.pattern.hide()
396		self.next_button.hide()
397		self.prev_button.hide()
398		self.progress.show()
399
400	def Idle(self):
401		self.textbox.setEnabled(True)
402		self.progress.hide()
403		self.pattern.show()
404		self.next_button.show()
405		self.prev_button.show()
406
407	def Find(self, direction):
408		value = self.textbox.currentText()
409		pattern = self.pattern.isChecked()
410		self.last_value = value
411		self.last_pattern = pattern
412		self.finder.Find(value, direction, pattern, self.context)
413
414	def ValueChanged(self):
415		value = self.textbox.currentText()
416		pattern = self.pattern.isChecked()
417		index = self.textbox.currentIndex()
418		data = self.textbox.itemData(index)
419		# Store the pattern in the combo box to keep it with the text value
420		if data == None:
421			self.textbox.setItemData(index, pattern)
422		else:
423			self.pattern.setChecked(data)
424		self.Find(0)
425
426	def NextPrev(self, direction):
427		value = self.textbox.currentText()
428		pattern = self.pattern.isChecked()
429		if value != self.last_value:
430			index = self.textbox.findText(value)
431			# Allow for a button press before the value has been added to the combo box
432			if index < 0:
433				index = self.textbox.count()
434				self.textbox.addItem(value, pattern)
435				self.textbox.setCurrentIndex(index)
436				return
437			else:
438				self.textbox.setItemData(index, pattern)
439		elif pattern != self.last_pattern:
440			# Keep the pattern recorded in the combo box up to date
441			index = self.textbox.currentIndex()
442			self.textbox.setItemData(index, pattern)
443		self.Find(direction)
444
445	def NotFound(self):
446		QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
447
448# Context-sensitive call graph data model item base
449
450class CallGraphLevelItemBase(object):
451
452	def __init__(self, glb, row, parent_item):
453		self.glb = glb
454		self.row = row
455		self.parent_item = parent_item
456		self.query_done = False;
457		self.child_count = 0
458		self.child_items = []
459		if parent_item:
460			self.level = parent_item.level + 1
461		else:
462			self.level = 0
463
464	def getChildItem(self, row):
465		return self.child_items[row]
466
467	def getParentItem(self):
468		return self.parent_item
469
470	def getRow(self):
471		return self.row
472
473	def childCount(self):
474		if not self.query_done:
475			self.Select()
476			if not self.child_count:
477				return -1
478		return self.child_count
479
480	def hasChildren(self):
481		if not self.query_done:
482			return True
483		return self.child_count > 0
484
485	def getData(self, column):
486		return self.data[column]
487
488# Context-sensitive call graph data model level 2+ item base
489
490class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
491
492	def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
493		super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
494		self.comm_id = comm_id
495		self.thread_id = thread_id
496		self.call_path_id = call_path_id
497		self.branch_count = branch_count
498		self.time = time
499
500	def Select(self):
501		self.query_done = True;
502		query = QSqlQuery(self.glb.db)
503		QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
504					" FROM calls"
505					" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
506					" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
507					" INNER JOIN dsos ON symbols.dso_id = dsos.id"
508					" WHERE parent_call_path_id = " + str(self.call_path_id) +
509					" AND comm_id = " + str(self.comm_id) +
510					" AND thread_id = " + str(self.thread_id) +
511					" GROUP BY call_path_id, name, short_name"
512					" ORDER BY call_path_id")
513		while query.next():
514			child_item = CallGraphLevelThreeItem(self.glb, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), int(query.value(5)), self)
515			self.child_items.append(child_item)
516			self.child_count += 1
517
518# Context-sensitive call graph data model level three item
519
520class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
521
522	def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
523		super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
524		dso = dsoname(dso)
525		self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
526		self.dbid = call_path_id
527
528# Context-sensitive call graph data model level two item
529
530class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
531
532	def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
533		super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
534		self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
535		self.dbid = thread_id
536
537	def Select(self):
538		super(CallGraphLevelTwoItem, self).Select()
539		for child_item in self.child_items:
540			self.time += child_item.time
541			self.branch_count += child_item.branch_count
542		for child_item in self.child_items:
543			child_item.data[4] = PercentToOneDP(child_item.time, self.time)
544			child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
545
546# Context-sensitive call graph data model level one item
547
548class CallGraphLevelOneItem(CallGraphLevelItemBase):
549
550	def __init__(self, glb, row, comm_id, comm, parent_item):
551		super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
552		self.data = [comm, "", "", "", "", "", ""]
553		self.dbid = comm_id
554
555	def Select(self):
556		self.query_done = True;
557		query = QSqlQuery(self.glb.db)
558		QueryExec(query, "SELECT thread_id, pid, tid"
559					" FROM comm_threads"
560					" INNER JOIN threads ON thread_id = threads.id"
561					" WHERE comm_id = " + str(self.dbid))
562		while query.next():
563			child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
564			self.child_items.append(child_item)
565			self.child_count += 1
566
567# Context-sensitive call graph data model root item
568
569class CallGraphRootItem(CallGraphLevelItemBase):
570
571	def __init__(self, glb):
572		super(CallGraphRootItem, self).__init__(glb, 0, None)
573		self.dbid = 0
574		self.query_done = True;
575		query = QSqlQuery(glb.db)
576		QueryExec(query, "SELECT id, comm FROM comms")
577		while query.next():
578			if not query.value(0):
579				continue
580			child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
581			self.child_items.append(child_item)
582			self.child_count += 1
583
584# Context-sensitive call graph data model base
585
586class CallGraphModelBase(TreeModel):
587
588	def __init__(self, glb, parent=None):
589		super(CallGraphModelBase, self).__init__(glb, parent)
590
591	def FindSelect(self, value, pattern, query):
592		if pattern:
593			# postgresql and sqlite pattern patching differences:
594			#   postgresql LIKE is case sensitive but sqlite LIKE is not
595			#   postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
596			#   postgresql supports ILIKE which is case insensitive
597			#   sqlite supports GLOB (text only) which uses * and ? and is case sensitive
598			if not self.glb.dbref.is_sqlite3:
599				# Escape % and _
600				s = value.replace("%", "\%")
601				s = s.replace("_", "\_")
602				# Translate * and ? into SQL LIKE pattern characters % and _
603				trans = string.maketrans("*?", "%_")
604				match = " LIKE '" + str(s).translate(trans) + "'"
605			else:
606				match = " GLOB '" + str(value) + "'"
607		else:
608			match = " = '" + str(value) + "'"
609		self.DoFindSelect(query, match)
610
611	def Found(self, query, found):
612		if found:
613			return self.FindPath(query)
614		return []
615
616	def FindValue(self, value, pattern, query, last_value, last_pattern):
617		if last_value == value and pattern == last_pattern:
618			found = query.first()
619		else:
620			self.FindSelect(value, pattern, query)
621			found = query.next()
622		return self.Found(query, found)
623
624	def FindNext(self, query):
625		found = query.next()
626		if not found:
627			found = query.first()
628		return self.Found(query, found)
629
630	def FindPrev(self, query):
631		found = query.previous()
632		if not found:
633			found = query.last()
634		return self.Found(query, found)
635
636	def FindThread(self, c):
637		if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
638			ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
639		elif c.direction > 0:
640			ids = self.FindNext(c.query)
641		else:
642			ids = self.FindPrev(c.query)
643		return (True, ids)
644
645	def Find(self, value, direction, pattern, context, callback):
646		class Context():
647			def __init__(self, *x):
648				self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
649			def Update(self, *x):
650				self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
651		if len(context):
652			context[0].Update(value, direction, pattern)
653		else:
654			context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
655		# Use a thread so the UI is not blocked during the SELECT
656		thread = Thread(self.FindThread, context[0])
657		thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
658		thread.start()
659
660	def FindDone(self, thread, callback, ids):
661		callback(ids)
662
663# Context-sensitive call graph data model
664
665class CallGraphModel(CallGraphModelBase):
666
667	def __init__(self, glb, parent=None):
668		super(CallGraphModel, self).__init__(glb, parent)
669
670	def GetRoot(self):
671		return CallGraphRootItem(self.glb)
672
673	def columnCount(self, parent=None):
674		return 7
675
676	def columnHeader(self, column):
677		headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
678		return headers[column]
679
680	def columnAlignment(self, column):
681		alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
682		return alignment[column]
683
684	def DoFindSelect(self, query, match):
685		QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
686						" FROM calls"
687						" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
688						" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
689						" WHERE symbols.name" + match +
690						" GROUP BY comm_id, thread_id, call_path_id"
691						" ORDER BY comm_id, thread_id, call_path_id")
692
693	def FindPath(self, query):
694		# Turn the query result into a list of ids that the tree view can walk
695		# to open the tree at the right place.
696		ids = []
697		parent_id = query.value(0)
698		while parent_id:
699			ids.insert(0, parent_id)
700			q2 = QSqlQuery(self.glb.db)
701			QueryExec(q2, "SELECT parent_id"
702					" FROM call_paths"
703					" WHERE id = " + str(parent_id))
704			if not q2.next():
705				break
706			parent_id = q2.value(0)
707		# The call path root is not used
708		if ids[0] == 1:
709			del ids[0]
710		ids.insert(0, query.value(2))
711		ids.insert(0, query.value(1))
712		return ids
713
714# Call tree data model level 2+ item base
715
716class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
717
718	def __init__(self, glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item):
719		super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
720		self.comm_id = comm_id
721		self.thread_id = thread_id
722		self.calls_id = calls_id
723		self.branch_count = branch_count
724		self.time = time
725
726	def Select(self):
727		self.query_done = True;
728		if self.calls_id == 0:
729			comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
730		else:
731			comm_thread = ""
732		query = QSqlQuery(self.glb.db)
733		QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time, branch_count"
734					" FROM calls"
735					" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
736					" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
737					" INNER JOIN dsos ON symbols.dso_id = dsos.id"
738					" WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
739					" ORDER BY call_time, calls.id")
740		while query.next():
741			child_item = CallTreeLevelThreeItem(self.glb, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), int(query.value(5)), self)
742			self.child_items.append(child_item)
743			self.child_count += 1
744
745# Call tree data model level three item
746
747class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
748
749	def __init__(self, glb, row, comm_id, thread_id, calls_id, name, dso, count, time, branch_count, parent_item):
750		super(CallTreeLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item)
751		dso = dsoname(dso)
752		self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
753		self.dbid = calls_id
754
755# Call tree data model level two item
756
757class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
758
759	def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
760		super(CallTreeLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 0, 0, 0, parent_item)
761		self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
762		self.dbid = thread_id
763
764	def Select(self):
765		super(CallTreeLevelTwoItem, self).Select()
766		for child_item in self.child_items:
767			self.time += child_item.time
768			self.branch_count += child_item.branch_count
769		for child_item in self.child_items:
770			child_item.data[4] = PercentToOneDP(child_item.time, self.time)
771			child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
772
773# Call tree data model level one item
774
775class CallTreeLevelOneItem(CallGraphLevelItemBase):
776
777	def __init__(self, glb, row, comm_id, comm, parent_item):
778		super(CallTreeLevelOneItem, self).__init__(glb, row, parent_item)
779		self.data = [comm, "", "", "", "", "", ""]
780		self.dbid = comm_id
781
782	def Select(self):
783		self.query_done = True;
784		query = QSqlQuery(self.glb.db)
785		QueryExec(query, "SELECT thread_id, pid, tid"
786					" FROM comm_threads"
787					" INNER JOIN threads ON thread_id = threads.id"
788					" WHERE comm_id = " + str(self.dbid))
789		while query.next():
790			child_item = CallTreeLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
791			self.child_items.append(child_item)
792			self.child_count += 1
793
794# Call tree data model root item
795
796class CallTreeRootItem(CallGraphLevelItemBase):
797
798	def __init__(self, glb):
799		super(CallTreeRootItem, self).__init__(glb, 0, None)
800		self.dbid = 0
801		self.query_done = True;
802		query = QSqlQuery(glb.db)
803		QueryExec(query, "SELECT id, comm FROM comms")
804		while query.next():
805			if not query.value(0):
806				continue
807			child_item = CallTreeLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
808			self.child_items.append(child_item)
809			self.child_count += 1
810
811# Call Tree data model
812
813class CallTreeModel(CallGraphModelBase):
814
815	def __init__(self, glb, parent=None):
816		super(CallTreeModel, self).__init__(glb, parent)
817
818	def GetRoot(self):
819		return CallTreeRootItem(self.glb)
820
821	def columnCount(self, parent=None):
822		return 7
823
824	def columnHeader(self, column):
825		headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
826		return headers[column]
827
828	def columnAlignment(self, column):
829		alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
830		return alignment[column]
831
832	def DoFindSelect(self, query, match):
833		QueryExec(query, "SELECT calls.id, comm_id, thread_id"
834						" FROM calls"
835						" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
836						" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
837						" WHERE symbols.name" + match +
838						" ORDER BY comm_id, thread_id, call_time, calls.id")
839
840	def FindPath(self, query):
841		# Turn the query result into a list of ids that the tree view can walk
842		# to open the tree at the right place.
843		ids = []
844		parent_id = query.value(0)
845		while parent_id:
846			ids.insert(0, parent_id)
847			q2 = QSqlQuery(self.glb.db)
848			QueryExec(q2, "SELECT parent_id"
849					" FROM calls"
850					" WHERE id = " + str(parent_id))
851			if not q2.next():
852				break
853			parent_id = q2.value(0)
854		ids.insert(0, query.value(2))
855		ids.insert(0, query.value(1))
856		return ids
857
858# Vertical widget layout
859
860class VBox():
861
862	def __init__(self, w1, w2, w3=None):
863		self.vbox = QWidget()
864		self.vbox.setLayout(QVBoxLayout());
865
866		self.vbox.layout().setContentsMargins(0, 0, 0, 0)
867
868		self.vbox.layout().addWidget(w1)
869		self.vbox.layout().addWidget(w2)
870		if w3:
871			self.vbox.layout().addWidget(w3)
872
873	def Widget(self):
874		return self.vbox
875
876# Tree window base
877
878class TreeWindowBase(QMdiSubWindow):
879
880	def __init__(self, parent=None):
881		super(TreeWindowBase, self).__init__(parent)
882
883		self.model = None
884		self.find_bar = None
885
886		self.view = QTreeView()
887
888	def DisplayFound(self, ids):
889		if not len(ids):
890			return False
891		parent = QModelIndex()
892		for dbid in ids:
893			found = False
894			n = self.model.rowCount(parent)
895			for row in xrange(n):
896				child = self.model.index(row, 0, parent)
897				if child.internalPointer().dbid == dbid:
898					found = True
899					self.view.setCurrentIndex(child)
900					parent = child
901					break
902			if not found:
903				break
904		return found
905
906	def Find(self, value, direction, pattern, context):
907		self.view.setFocus()
908		self.find_bar.Busy()
909		self.model.Find(value, direction, pattern, context, self.FindDone)
910
911	def FindDone(self, ids):
912		found = True
913		if not self.DisplayFound(ids):
914			found = False
915		self.find_bar.Idle()
916		if not found:
917			self.find_bar.NotFound()
918
919
920# Context-sensitive call graph window
921
922class CallGraphWindow(TreeWindowBase):
923
924	def __init__(self, glb, parent=None):
925		super(CallGraphWindow, self).__init__(parent)
926
927		self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
928
929		self.view.setModel(self.model)
930
931		for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
932			self.view.setColumnWidth(c, w)
933
934		self.find_bar = FindBar(self, self)
935
936		self.vbox = VBox(self.view, self.find_bar.Widget())
937
938		self.setWidget(self.vbox.Widget())
939
940		AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
941
942# Call tree window
943
944class CallTreeWindow(TreeWindowBase):
945
946	def __init__(self, glb, parent=None):
947		super(CallTreeWindow, self).__init__(parent)
948
949		self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
950
951		self.view.setModel(self.model)
952
953		for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
954			self.view.setColumnWidth(c, w)
955
956		self.find_bar = FindBar(self, self)
957
958		self.vbox = VBox(self.view, self.find_bar.Widget())
959
960		self.setWidget(self.vbox.Widget())
961
962		AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
963
964# Child data item  finder
965
966class ChildDataItemFinder():
967
968	def __init__(self, root):
969		self.root = root
970		self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
971		self.rows = []
972		self.pos = 0
973
974	def FindSelect(self):
975		self.rows = []
976		if self.pattern:
977			pattern = re.compile(self.value)
978			for child in self.root.child_items:
979				for column_data in child.data:
980					if re.search(pattern, str(column_data)) is not None:
981						self.rows.append(child.row)
982						break
983		else:
984			for child in self.root.child_items:
985				for column_data in child.data:
986					if self.value in str(column_data):
987						self.rows.append(child.row)
988						break
989
990	def FindValue(self):
991		self.pos = 0
992		if self.last_value != self.value or self.pattern != self.last_pattern:
993			self.FindSelect()
994		if not len(self.rows):
995			return -1
996		return self.rows[self.pos]
997
998	def FindThread(self):
999		if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
1000			row = self.FindValue()
1001		elif len(self.rows):
1002			if self.direction > 0:
1003				self.pos += 1
1004				if self.pos >= len(self.rows):
1005					self.pos = 0
1006			else:
1007				self.pos -= 1
1008				if self.pos < 0:
1009					self.pos = len(self.rows) - 1
1010			row = self.rows[self.pos]
1011		else:
1012			row = -1
1013		return (True, row)
1014
1015	def Find(self, value, direction, pattern, context, callback):
1016		self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
1017		# Use a thread so the UI is not blocked
1018		thread = Thread(self.FindThread)
1019		thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
1020		thread.start()
1021
1022	def FindDone(self, thread, callback, row):
1023		callback(row)
1024
1025# Number of database records to fetch in one go
1026
1027glb_chunk_sz = 10000
1028
1029# Background process for SQL data fetcher
1030
1031class SQLFetcherProcess():
1032
1033	def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
1034		# Need a unique connection name
1035		conn_name = "SQLFetcher" + str(os.getpid())
1036		self.db, dbname = dbref.Open(conn_name)
1037		self.sql = sql
1038		self.buffer = buffer
1039		self.head = head
1040		self.tail = tail
1041		self.fetch_count = fetch_count
1042		self.fetching_done = fetching_done
1043		self.process_target = process_target
1044		self.wait_event = wait_event
1045		self.fetched_event = fetched_event
1046		self.prep = prep
1047		self.query = QSqlQuery(self.db)
1048		self.query_limit = 0 if "$$last_id$$" in sql else 2
1049		self.last_id = -1
1050		self.fetched = 0
1051		self.more = True
1052		self.local_head = self.head.value
1053		self.local_tail = self.tail.value
1054
1055	def Select(self):
1056		if self.query_limit:
1057			if self.query_limit == 1:
1058				return
1059			self.query_limit -= 1
1060		stmt = self.sql.replace("$$last_id$$", str(self.last_id))
1061		QueryExec(self.query, stmt)
1062
1063	def Next(self):
1064		if not self.query.next():
1065			self.Select()
1066			if not self.query.next():
1067				return None
1068		self.last_id = self.query.value(0)
1069		return self.prep(self.query)
1070
1071	def WaitForTarget(self):
1072		while True:
1073			self.wait_event.clear()
1074			target = self.process_target.value
1075			if target > self.fetched or target < 0:
1076				break
1077			self.wait_event.wait()
1078		return target
1079
1080	def HasSpace(self, sz):
1081		if self.local_tail <= self.local_head:
1082			space = len(self.buffer) - self.local_head
1083			if space > sz:
1084				return True
1085			if space >= glb_nsz:
1086				# Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
1087				nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
1088				self.buffer[self.local_head : self.local_head + len(nd)] = nd
1089			self.local_head = 0
1090		if self.local_tail - self.local_head > sz:
1091			return True
1092		return False
1093
1094	def WaitForSpace(self, sz):
1095		if self.HasSpace(sz):
1096			return
1097		while True:
1098			self.wait_event.clear()
1099			self.local_tail = self.tail.value
1100			if self.HasSpace(sz):
1101				return
1102			self.wait_event.wait()
1103
1104	def AddToBuffer(self, obj):
1105		d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
1106		n = len(d)
1107		nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
1108		sz = n + glb_nsz
1109		self.WaitForSpace(sz)
1110		pos = self.local_head
1111		self.buffer[pos : pos + len(nd)] = nd
1112		self.buffer[pos + glb_nsz : pos + sz] = d
1113		self.local_head += sz
1114
1115	def FetchBatch(self, batch_size):
1116		fetched = 0
1117		while batch_size > fetched:
1118			obj = self.Next()
1119			if obj is None:
1120				self.more = False
1121				break
1122			self.AddToBuffer(obj)
1123			fetched += 1
1124		if fetched:
1125			self.fetched += fetched
1126			with self.fetch_count.get_lock():
1127				self.fetch_count.value += fetched
1128			self.head.value = self.local_head
1129			self.fetched_event.set()
1130
1131	def Run(self):
1132		while self.more:
1133			target = self.WaitForTarget()
1134			if target < 0:
1135				break
1136			batch_size = min(glb_chunk_sz, target - self.fetched)
1137			self.FetchBatch(batch_size)
1138		self.fetching_done.value = True
1139		self.fetched_event.set()
1140
1141def SQLFetcherFn(*x):
1142	process = SQLFetcherProcess(*x)
1143	process.Run()
1144
1145# SQL data fetcher
1146
1147class SQLFetcher(QObject):
1148
1149	done = Signal(object)
1150
1151	def __init__(self, glb, sql, prep, process_data, parent=None):
1152		super(SQLFetcher, self).__init__(parent)
1153		self.process_data = process_data
1154		self.more = True
1155		self.target = 0
1156		self.last_target = 0
1157		self.fetched = 0
1158		self.buffer_size = 16 * 1024 * 1024
1159		self.buffer = Array(c_char, self.buffer_size, lock=False)
1160		self.head = Value(c_longlong)
1161		self.tail = Value(c_longlong)
1162		self.local_tail = 0
1163		self.fetch_count = Value(c_longlong)
1164		self.fetching_done = Value(c_bool)
1165		self.last_count = 0
1166		self.process_target = Value(c_longlong)
1167		self.wait_event = Event()
1168		self.fetched_event = Event()
1169		glb.AddInstanceToShutdownOnExit(self)
1170		self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep))
1171		self.process.start()
1172		self.thread = Thread(self.Thread)
1173		self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
1174		self.thread.start()
1175
1176	def Shutdown(self):
1177		# Tell the thread and process to exit
1178		self.process_target.value = -1
1179		self.wait_event.set()
1180		self.more = False
1181		self.fetching_done.value = True
1182		self.fetched_event.set()
1183
1184	def Thread(self):
1185		if not self.more:
1186			return True, 0
1187		while True:
1188			self.fetched_event.clear()
1189			fetch_count = self.fetch_count.value
1190			if fetch_count != self.last_count:
1191				break
1192			if self.fetching_done.value:
1193				self.more = False
1194				return True, 0
1195			self.fetched_event.wait()
1196		count = fetch_count - self.last_count
1197		self.last_count = fetch_count
1198		self.fetched += count
1199		return False, count
1200
1201	def Fetch(self, nr):
1202		if not self.more:
1203			# -1 inidcates there are no more
1204			return -1
1205		result = self.fetched
1206		extra = result + nr - self.target
1207		if extra > 0:
1208			self.target += extra
1209			# process_target < 0 indicates shutting down
1210			if self.process_target.value >= 0:
1211				self.process_target.value = self.target
1212			self.wait_event.set()
1213		return result
1214
1215	def RemoveFromBuffer(self):
1216		pos = self.local_tail
1217		if len(self.buffer) - pos < glb_nsz:
1218			pos = 0
1219		n = pickle.loads(self.buffer[pos : pos + glb_nsz])
1220		if n == 0:
1221			pos = 0
1222			n = pickle.loads(self.buffer[0 : glb_nsz])
1223		pos += glb_nsz
1224		obj = pickle.loads(self.buffer[pos : pos + n])
1225		self.local_tail = pos + n
1226		return obj
1227
1228	def ProcessData(self, count):
1229		for i in xrange(count):
1230			obj = self.RemoveFromBuffer()
1231			self.process_data(obj)
1232		self.tail.value = self.local_tail
1233		self.wait_event.set()
1234		self.done.emit(count)
1235
1236# Fetch more records bar
1237
1238class FetchMoreRecordsBar():
1239
1240	def __init__(self, model, parent):
1241		self.model = model
1242
1243		self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
1244		self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1245
1246		self.fetch_count = QSpinBox()
1247		self.fetch_count.setRange(1, 1000000)
1248		self.fetch_count.setValue(10)
1249		self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1250
1251		self.fetch = QPushButton("Go!")
1252		self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1253		self.fetch.released.connect(self.FetchMoreRecords)
1254
1255		self.progress = QProgressBar()
1256		self.progress.setRange(0, 100)
1257		self.progress.hide()
1258
1259		self.done_label = QLabel("All records fetched")
1260		self.done_label.hide()
1261
1262		self.spacer = QLabel("")
1263
1264		self.close_button = QToolButton()
1265		self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
1266		self.close_button.released.connect(self.Deactivate)
1267
1268		self.hbox = QHBoxLayout()
1269		self.hbox.setContentsMargins(0, 0, 0, 0)
1270
1271		self.hbox.addWidget(self.label)
1272		self.hbox.addWidget(self.fetch_count)
1273		self.hbox.addWidget(self.fetch)
1274		self.hbox.addWidget(self.spacer)
1275		self.hbox.addWidget(self.progress)
1276		self.hbox.addWidget(self.done_label)
1277		self.hbox.addWidget(self.close_button)
1278
1279		self.bar = QWidget()
1280		self.bar.setLayout(self.hbox);
1281		self.bar.show()
1282
1283		self.in_progress = False
1284		self.model.progress.connect(self.Progress)
1285
1286		self.done = False
1287
1288		if not model.HasMoreRecords():
1289			self.Done()
1290
1291	def Widget(self):
1292		return self.bar
1293
1294	def Activate(self):
1295		self.bar.show()
1296		self.fetch.setFocus()
1297
1298	def Deactivate(self):
1299		self.bar.hide()
1300
1301	def Enable(self, enable):
1302		self.fetch.setEnabled(enable)
1303		self.fetch_count.setEnabled(enable)
1304
1305	def Busy(self):
1306		self.Enable(False)
1307		self.fetch.hide()
1308		self.spacer.hide()
1309		self.progress.show()
1310
1311	def Idle(self):
1312		self.in_progress = False
1313		self.Enable(True)
1314		self.progress.hide()
1315		self.fetch.show()
1316		self.spacer.show()
1317
1318	def Target(self):
1319		return self.fetch_count.value() * glb_chunk_sz
1320
1321	def Done(self):
1322		self.done = True
1323		self.Idle()
1324		self.label.hide()
1325		self.fetch_count.hide()
1326		self.fetch.hide()
1327		self.spacer.hide()
1328		self.done_label.show()
1329
1330	def Progress(self, count):
1331		if self.in_progress:
1332			if count:
1333				percent = ((count - self.start) * 100) / self.Target()
1334				if percent >= 100:
1335					self.Idle()
1336				else:
1337					self.progress.setValue(percent)
1338		if not count:
1339			# Count value of zero means no more records
1340			self.Done()
1341
1342	def FetchMoreRecords(self):
1343		if self.done:
1344			return
1345		self.progress.setValue(0)
1346		self.Busy()
1347		self.in_progress = True
1348		self.start = self.model.FetchMoreRecords(self.Target())
1349
1350# Brance data model level two item
1351
1352class BranchLevelTwoItem():
1353
1354	def __init__(self, row, text, parent_item):
1355		self.row = row
1356		self.parent_item = parent_item
1357		self.data = [""] * 8
1358		self.data[7] = text
1359		self.level = 2
1360
1361	def getParentItem(self):
1362		return self.parent_item
1363
1364	def getRow(self):
1365		return self.row
1366
1367	def childCount(self):
1368		return 0
1369
1370	def hasChildren(self):
1371		return False
1372
1373	def getData(self, column):
1374		return self.data[column]
1375
1376# Brance data model level one item
1377
1378class BranchLevelOneItem():
1379
1380	def __init__(self, glb, row, data, parent_item):
1381		self.glb = glb
1382		self.row = row
1383		self.parent_item = parent_item
1384		self.child_count = 0
1385		self.child_items = []
1386		self.data = data[1:]
1387		self.dbid = data[0]
1388		self.level = 1
1389		self.query_done = False
1390
1391	def getChildItem(self, row):
1392		return self.child_items[row]
1393
1394	def getParentItem(self):
1395		return self.parent_item
1396
1397	def getRow(self):
1398		return self.row
1399
1400	def Select(self):
1401		self.query_done = True
1402
1403		if not self.glb.have_disassembler:
1404			return
1405
1406		query = QSqlQuery(self.glb.db)
1407
1408		QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
1409				  " FROM samples"
1410				  " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
1411				  " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
1412				  " WHERE samples.id = " + str(self.dbid))
1413		if not query.next():
1414			return
1415		cpu = query.value(0)
1416		dso = query.value(1)
1417		sym = query.value(2)
1418		if dso == 0 or sym == 0:
1419			return
1420		off = query.value(3)
1421		short_name = query.value(4)
1422		long_name = query.value(5)
1423		build_id = query.value(6)
1424		sym_start = query.value(7)
1425		ip = query.value(8)
1426
1427		QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
1428				  " FROM samples"
1429				  " INNER JOIN symbols ON samples.symbol_id = symbols.id"
1430				  " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
1431				  " ORDER BY samples.id"
1432				  " LIMIT 1")
1433		if not query.next():
1434			return
1435		if query.value(0) != dso:
1436			# Cannot disassemble from one dso to another
1437			return
1438		bsym = query.value(1)
1439		boff = query.value(2)
1440		bsym_start = query.value(3)
1441		if bsym == 0:
1442			return
1443		tot = bsym_start + boff + 1 - sym_start - off
1444		if tot <= 0 or tot > 16384:
1445			return
1446
1447		inst = self.glb.disassembler.Instruction()
1448		f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
1449		if not f:
1450			return
1451		mode = 0 if Is64Bit(f) else 1
1452		self.glb.disassembler.SetMode(inst, mode)
1453
1454		buf_sz = tot + 16
1455		buf = create_string_buffer(tot + 16)
1456		f.seek(sym_start + off)
1457		buf.value = f.read(buf_sz)
1458		buf_ptr = addressof(buf)
1459		i = 0
1460		while tot > 0:
1461			cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
1462			if cnt:
1463				byte_str = tohex(ip).rjust(16)
1464				for k in xrange(cnt):
1465					byte_str += " %02x" % ord(buf[i])
1466					i += 1
1467				while k < 15:
1468					byte_str += "   "
1469					k += 1
1470				self.child_items.append(BranchLevelTwoItem(0, byte_str + " " + text, self))
1471				self.child_count += 1
1472			else:
1473				return
1474			buf_ptr += cnt
1475			tot -= cnt
1476			buf_sz -= cnt
1477			ip += cnt
1478
1479	def childCount(self):
1480		if not self.query_done:
1481			self.Select()
1482			if not self.child_count:
1483				return -1
1484		return self.child_count
1485
1486	def hasChildren(self):
1487		if not self.query_done:
1488			return True
1489		return self.child_count > 0
1490
1491	def getData(self, column):
1492		return self.data[column]
1493
1494# Brance data model root item
1495
1496class BranchRootItem():
1497
1498	def __init__(self):
1499		self.child_count = 0
1500		self.child_items = []
1501		self.level = 0
1502
1503	def getChildItem(self, row):
1504		return self.child_items[row]
1505
1506	def getParentItem(self):
1507		return None
1508
1509	def getRow(self):
1510		return 0
1511
1512	def childCount(self):
1513		return self.child_count
1514
1515	def hasChildren(self):
1516		return self.child_count > 0
1517
1518	def getData(self, column):
1519		return ""
1520
1521# Branch data preparation
1522
1523def BranchDataPrep(query):
1524	data = []
1525	for i in xrange(0, 8):
1526		data.append(query.value(i))
1527	data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1528			" (" + dsoname(query.value(11)) + ")" + " -> " +
1529			tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1530			" (" + dsoname(query.value(15)) + ")")
1531	return data
1532
1533def BranchDataPrepWA(query):
1534	data = []
1535	data.append(query.value(0))
1536	# Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
1537	data.append("{:>19}".format(query.value(1)))
1538	for i in xrange(2, 8):
1539		data.append(query.value(i))
1540	data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1541			" (" + dsoname(query.value(11)) + ")" + " -> " +
1542			tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1543			" (" + dsoname(query.value(15)) + ")")
1544	return data
1545
1546# Branch data model
1547
1548class BranchModel(TreeModel):
1549
1550	progress = Signal(object)
1551
1552	def __init__(self, glb, event_id, where_clause, parent=None):
1553		super(BranchModel, self).__init__(glb, parent)
1554		self.event_id = event_id
1555		self.more = True
1556		self.populated = 0
1557		sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
1558			" CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
1559			" ip, symbols.name, sym_offset, dsos.short_name,"
1560			" to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
1561			" FROM samples"
1562			" INNER JOIN comms ON comm_id = comms.id"
1563			" INNER JOIN threads ON thread_id = threads.id"
1564			" INNER JOIN branch_types ON branch_type = branch_types.id"
1565			" INNER JOIN symbols ON symbol_id = symbols.id"
1566			" INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
1567			" INNER JOIN dsos ON samples.dso_id = dsos.id"
1568			" INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
1569			" WHERE samples.id > $$last_id$$" + where_clause +
1570			" AND evsel_id = " + str(self.event_id) +
1571			" ORDER BY samples.id"
1572			" LIMIT " + str(glb_chunk_sz))
1573		if pyside_version_1 and sys.version_info[0] == 3:
1574			prep = BranchDataPrepWA
1575		else:
1576			prep = BranchDataPrep
1577		self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
1578		self.fetcher.done.connect(self.Update)
1579		self.fetcher.Fetch(glb_chunk_sz)
1580
1581	def GetRoot(self):
1582		return BranchRootItem()
1583
1584	def columnCount(self, parent=None):
1585		return 8
1586
1587	def columnHeader(self, column):
1588		return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
1589
1590	def columnFont(self, column):
1591		if column != 7:
1592			return None
1593		return QFont("Monospace")
1594
1595	def DisplayData(self, item, index):
1596		if item.level == 1:
1597			self.FetchIfNeeded(item.row)
1598		return item.getData(index.column())
1599
1600	def AddSample(self, data):
1601		child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
1602		self.root.child_items.append(child)
1603		self.populated += 1
1604
1605	def Update(self, fetched):
1606		if not fetched:
1607			self.more = False
1608			self.progress.emit(0)
1609		child_count = self.root.child_count
1610		count = self.populated - child_count
1611		if count > 0:
1612			parent = QModelIndex()
1613			self.beginInsertRows(parent, child_count, child_count + count - 1)
1614			self.insertRows(child_count, count, parent)
1615			self.root.child_count += count
1616			self.endInsertRows()
1617			self.progress.emit(self.root.child_count)
1618
1619	def FetchMoreRecords(self, count):
1620		current = self.root.child_count
1621		if self.more:
1622			self.fetcher.Fetch(count)
1623		else:
1624			self.progress.emit(0)
1625		return current
1626
1627	def HasMoreRecords(self):
1628		return self.more
1629
1630# Report Variables
1631
1632class ReportVars():
1633
1634	def __init__(self, name = "", where_clause = "", limit = ""):
1635		self.name = name
1636		self.where_clause = where_clause
1637		self.limit = limit
1638
1639	def UniqueId(self):
1640		return str(self.where_clause + ";" + self.limit)
1641
1642# Branch window
1643
1644class BranchWindow(QMdiSubWindow):
1645
1646	def __init__(self, glb, event_id, report_vars, parent=None):
1647		super(BranchWindow, self).__init__(parent)
1648
1649		model_name = "Branch Events " + str(event_id) +  " " + report_vars.UniqueId()
1650
1651		self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
1652
1653		self.view = QTreeView()
1654		self.view.setUniformRowHeights(True)
1655		self.view.setModel(self.model)
1656
1657		self.ResizeColumnsToContents()
1658
1659		self.find_bar = FindBar(self, self, True)
1660
1661		self.finder = ChildDataItemFinder(self.model.root)
1662
1663		self.fetch_bar = FetchMoreRecordsBar(self.model, self)
1664
1665		self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
1666
1667		self.setWidget(self.vbox.Widget())
1668
1669		AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
1670
1671	def ResizeColumnToContents(self, column, n):
1672		# Using the view's resizeColumnToContents() here is extrememly slow
1673		# so implement a crude alternative
1674		mm = "MM" if column else "MMMM"
1675		font = self.view.font()
1676		metrics = QFontMetrics(font)
1677		max = 0
1678		for row in xrange(n):
1679			val = self.model.root.child_items[row].data[column]
1680			len = metrics.width(str(val) + mm)
1681			max = len if len > max else max
1682		val = self.model.columnHeader(column)
1683		len = metrics.width(str(val) + mm)
1684		max = len if len > max else max
1685		self.view.setColumnWidth(column, max)
1686
1687	def ResizeColumnsToContents(self):
1688		n = min(self.model.root.child_count, 100)
1689		if n < 1:
1690			# No data yet, so connect a signal to notify when there is
1691			self.model.rowsInserted.connect(self.UpdateColumnWidths)
1692			return
1693		columns = self.model.columnCount()
1694		for i in xrange(columns):
1695			self.ResizeColumnToContents(i, n)
1696
1697	def UpdateColumnWidths(self, *x):
1698		# This only needs to be done once, so disconnect the signal now
1699		self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
1700		self.ResizeColumnsToContents()
1701
1702	def Find(self, value, direction, pattern, context):
1703		self.view.setFocus()
1704		self.find_bar.Busy()
1705		self.finder.Find(value, direction, pattern, context, self.FindDone)
1706
1707	def FindDone(self, row):
1708		self.find_bar.Idle()
1709		if row >= 0:
1710			self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
1711		else:
1712			self.find_bar.NotFound()
1713
1714# Line edit data item
1715
1716class LineEditDataItem(object):
1717
1718	def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1719		self.glb = glb
1720		self.label = label
1721		self.placeholder_text = placeholder_text
1722		self.parent = parent
1723		self.id = id
1724
1725		self.value = default
1726
1727		self.widget = QLineEdit(default)
1728		self.widget.editingFinished.connect(self.Validate)
1729		self.widget.textChanged.connect(self.Invalidate)
1730		self.red = False
1731		self.error = ""
1732		self.validated = True
1733
1734		if placeholder_text:
1735			self.widget.setPlaceholderText(placeholder_text)
1736
1737	def TurnTextRed(self):
1738		if not self.red:
1739			palette = QPalette()
1740			palette.setColor(QPalette.Text,Qt.red)
1741			self.widget.setPalette(palette)
1742			self.red = True
1743
1744	def TurnTextNormal(self):
1745		if self.red:
1746			palette = QPalette()
1747			self.widget.setPalette(palette)
1748			self.red = False
1749
1750	def InvalidValue(self, value):
1751		self.value = ""
1752		self.TurnTextRed()
1753		self.error = self.label + " invalid value '" + value + "'"
1754		self.parent.ShowMessage(self.error)
1755
1756	def Invalidate(self):
1757		self.validated = False
1758
1759	def DoValidate(self, input_string):
1760		self.value = input_string.strip()
1761
1762	def Validate(self):
1763		self.validated = True
1764		self.error = ""
1765		self.TurnTextNormal()
1766		self.parent.ClearMessage()
1767		input_string = self.widget.text()
1768		if not len(input_string.strip()):
1769			self.value = ""
1770			return
1771		self.DoValidate(input_string)
1772
1773	def IsValid(self):
1774		if not self.validated:
1775			self.Validate()
1776		if len(self.error):
1777			self.parent.ShowMessage(self.error)
1778			return False
1779		return True
1780
1781	def IsNumber(self, value):
1782		try:
1783			x = int(value)
1784		except:
1785			x = 0
1786		return str(x) == value
1787
1788# Non-negative integer ranges dialog data item
1789
1790class NonNegativeIntegerRangesDataItem(LineEditDataItem):
1791
1792	def __init__(self, glb, label, placeholder_text, column_name, parent):
1793		super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1794
1795		self.column_name = column_name
1796
1797	def DoValidate(self, input_string):
1798		singles = []
1799		ranges = []
1800		for value in [x.strip() for x in input_string.split(",")]:
1801			if "-" in value:
1802				vrange = value.split("-")
1803				if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1804					return self.InvalidValue(value)
1805				ranges.append(vrange)
1806			else:
1807				if not self.IsNumber(value):
1808					return self.InvalidValue(value)
1809				singles.append(value)
1810		ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1811		if len(singles):
1812			ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
1813		self.value = " OR ".join(ranges)
1814
1815# Positive integer dialog data item
1816
1817class PositiveIntegerDataItem(LineEditDataItem):
1818
1819	def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1820		super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
1821
1822	def DoValidate(self, input_string):
1823		if not self.IsNumber(input_string.strip()):
1824			return self.InvalidValue(input_string)
1825		value = int(input_string.strip())
1826		if value <= 0:
1827			return self.InvalidValue(input_string)
1828		self.value = str(value)
1829
1830# Dialog data item converted and validated using a SQL table
1831
1832class SQLTableDataItem(LineEditDataItem):
1833
1834	def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
1835		super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
1836
1837		self.table_name = table_name
1838		self.match_column = match_column
1839		self.column_name1 = column_name1
1840		self.column_name2 = column_name2
1841
1842	def ValueToIds(self, value):
1843		ids = []
1844		query = QSqlQuery(self.glb.db)
1845		stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
1846		ret = query.exec_(stmt)
1847		if ret:
1848			while query.next():
1849				ids.append(str(query.value(0)))
1850		return ids
1851
1852	def DoValidate(self, input_string):
1853		all_ids = []
1854		for value in [x.strip() for x in input_string.split(",")]:
1855			ids = self.ValueToIds(value)
1856			if len(ids):
1857				all_ids.extend(ids)
1858			else:
1859				return self.InvalidValue(value)
1860		self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
1861		if self.column_name2:
1862			self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
1863
1864# Sample time ranges dialog data item converted and validated using 'samples' SQL table
1865
1866class SampleTimeRangesDataItem(LineEditDataItem):
1867
1868	def __init__(self, glb, label, placeholder_text, column_name, parent):
1869		self.column_name = column_name
1870
1871		self.last_id = 0
1872		self.first_time = 0
1873		self.last_time = 2 ** 64
1874
1875		query = QSqlQuery(glb.db)
1876		QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
1877		if query.next():
1878			self.last_id = int(query.value(0))
1879			self.last_time = int(query.value(1))
1880		QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1")
1881		if query.next():
1882			self.first_time = int(query.value(0))
1883		if placeholder_text:
1884			placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
1885
1886		super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1887
1888	def IdBetween(self, query, lower_id, higher_id, order):
1889		QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
1890		if query.next():
1891			return True, int(query.value(0))
1892		else:
1893			return False, 0
1894
1895	def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
1896		query = QSqlQuery(self.glb.db)
1897		while True:
1898			next_id = int((lower_id + higher_id) / 2)
1899			QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1900			if not query.next():
1901				ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
1902				if not ok:
1903					ok, dbid = self.IdBetween(query, next_id, higher_id, "")
1904					if not ok:
1905						return str(higher_id)
1906				next_id = dbid
1907				QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1908			next_time = int(query.value(0))
1909			if get_floor:
1910				if target_time > next_time:
1911					lower_id = next_id
1912				else:
1913					higher_id = next_id
1914				if higher_id <= lower_id + 1:
1915					return str(higher_id)
1916			else:
1917				if target_time >= next_time:
1918					lower_id = next_id
1919				else:
1920					higher_id = next_id
1921				if higher_id <= lower_id + 1:
1922					return str(lower_id)
1923
1924	def ConvertRelativeTime(self, val):
1925		mult = 1
1926		suffix = val[-2:]
1927		if suffix == "ms":
1928			mult = 1000000
1929		elif suffix == "us":
1930			mult = 1000
1931		elif suffix == "ns":
1932			mult = 1
1933		else:
1934			return val
1935		val = val[:-2].strip()
1936		if not self.IsNumber(val):
1937			return val
1938		val = int(val) * mult
1939		if val >= 0:
1940			val += self.first_time
1941		else:
1942			val += self.last_time
1943		return str(val)
1944
1945	def ConvertTimeRange(self, vrange):
1946		if vrange[0] == "":
1947			vrange[0] = str(self.first_time)
1948		if vrange[1] == "":
1949			vrange[1] = str(self.last_time)
1950		vrange[0] = self.ConvertRelativeTime(vrange[0])
1951		vrange[1] = self.ConvertRelativeTime(vrange[1])
1952		if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1953			return False
1954		beg_range = max(int(vrange[0]), self.first_time)
1955		end_range = min(int(vrange[1]), self.last_time)
1956		if beg_range > self.last_time or end_range < self.first_time:
1957			return False
1958		vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
1959		vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
1960		return True
1961
1962	def AddTimeRange(self, value, ranges):
1963		n = value.count("-")
1964		if n == 1:
1965			pass
1966		elif n == 2:
1967			if value.split("-")[1].strip() == "":
1968				n = 1
1969		elif n == 3:
1970			n = 2
1971		else:
1972			return False
1973		pos = findnth(value, "-", n)
1974		vrange = [value[:pos].strip() ,value[pos+1:].strip()]
1975		if self.ConvertTimeRange(vrange):
1976			ranges.append(vrange)
1977			return True
1978		return False
1979
1980	def DoValidate(self, input_string):
1981		ranges = []
1982		for value in [x.strip() for x in input_string.split(",")]:
1983			if not self.AddTimeRange(value, ranges):
1984				return self.InvalidValue(value)
1985		ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1986		self.value = " OR ".join(ranges)
1987
1988# Report Dialog Base
1989
1990class ReportDialogBase(QDialog):
1991
1992	def __init__(self, glb, title, items, partial, parent=None):
1993		super(ReportDialogBase, self).__init__(parent)
1994
1995		self.glb = glb
1996
1997		self.report_vars = ReportVars()
1998
1999		self.setWindowTitle(title)
2000		self.setMinimumWidth(600)
2001
2002		self.data_items = [x(glb, self) for x in items]
2003
2004		self.partial = partial
2005
2006		self.grid = QGridLayout()
2007
2008		for row in xrange(len(self.data_items)):
2009			self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
2010			self.grid.addWidget(self.data_items[row].widget, row, 1)
2011
2012		self.status = QLabel()
2013
2014		self.ok_button = QPushButton("Ok", self)
2015		self.ok_button.setDefault(True)
2016		self.ok_button.released.connect(self.Ok)
2017		self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2018
2019		self.cancel_button = QPushButton("Cancel", self)
2020		self.cancel_button.released.connect(self.reject)
2021		self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2022
2023		self.hbox = QHBoxLayout()
2024		#self.hbox.addStretch()
2025		self.hbox.addWidget(self.status)
2026		self.hbox.addWidget(self.ok_button)
2027		self.hbox.addWidget(self.cancel_button)
2028
2029		self.vbox = QVBoxLayout()
2030		self.vbox.addLayout(self.grid)
2031		self.vbox.addLayout(self.hbox)
2032
2033		self.setLayout(self.vbox);
2034
2035	def Ok(self):
2036		vars = self.report_vars
2037		for d in self.data_items:
2038			if d.id == "REPORTNAME":
2039				vars.name = d.value
2040		if not vars.name:
2041			self.ShowMessage("Report name is required")
2042			return
2043		for d in self.data_items:
2044			if not d.IsValid():
2045				return
2046		for d in self.data_items[1:]:
2047			if d.id == "LIMIT":
2048				vars.limit = d.value
2049			elif len(d.value):
2050				if len(vars.where_clause):
2051					vars.where_clause += " AND "
2052				vars.where_clause += d.value
2053		if len(vars.where_clause):
2054			if self.partial:
2055				vars.where_clause = " AND ( " + vars.where_clause + " ) "
2056			else:
2057				vars.where_clause = " WHERE " + vars.where_clause + " "
2058		self.accept()
2059
2060	def ShowMessage(self, msg):
2061		self.status.setText("<font color=#FF0000>" + msg)
2062
2063	def ClearMessage(self):
2064		self.status.setText("")
2065
2066# Selected branch report creation dialog
2067
2068class SelectedBranchDialog(ReportDialogBase):
2069
2070	def __init__(self, glb, parent=None):
2071		title = "Selected Branches"
2072		items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2073			 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
2074			 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
2075			 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
2076			 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2077			 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2078			 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
2079			 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
2080			 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
2081		super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
2082
2083# Event list
2084
2085def GetEventList(db):
2086	events = []
2087	query = QSqlQuery(db)
2088	QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
2089	while query.next():
2090		events.append(query.value(0))
2091	return events
2092
2093# Is a table selectable
2094
2095def IsSelectable(db, table, sql = ""):
2096	query = QSqlQuery(db)
2097	try:
2098		QueryExec(query, "SELECT * FROM " + table + " " + sql + " LIMIT 1")
2099	except:
2100		return False
2101	return True
2102
2103# SQL table data model item
2104
2105class SQLTableItem():
2106
2107	def __init__(self, row, data):
2108		self.row = row
2109		self.data = data
2110
2111	def getData(self, column):
2112		return self.data[column]
2113
2114# SQL table data model
2115
2116class SQLTableModel(TableModel):
2117
2118	progress = Signal(object)
2119
2120	def __init__(self, glb, sql, column_headers, parent=None):
2121		super(SQLTableModel, self).__init__(parent)
2122		self.glb = glb
2123		self.more = True
2124		self.populated = 0
2125		self.column_headers = column_headers
2126		self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
2127		self.fetcher.done.connect(self.Update)
2128		self.fetcher.Fetch(glb_chunk_sz)
2129
2130	def DisplayData(self, item, index):
2131		self.FetchIfNeeded(item.row)
2132		return item.getData(index.column())
2133
2134	def AddSample(self, data):
2135		child = SQLTableItem(self.populated, data)
2136		self.child_items.append(child)
2137		self.populated += 1
2138
2139	def Update(self, fetched):
2140		if not fetched:
2141			self.more = False
2142			self.progress.emit(0)
2143		child_count = self.child_count
2144		count = self.populated - child_count
2145		if count > 0:
2146			parent = QModelIndex()
2147			self.beginInsertRows(parent, child_count, child_count + count - 1)
2148			self.insertRows(child_count, count, parent)
2149			self.child_count += count
2150			self.endInsertRows()
2151			self.progress.emit(self.child_count)
2152
2153	def FetchMoreRecords(self, count):
2154		current = self.child_count
2155		if self.more:
2156			self.fetcher.Fetch(count)
2157		else:
2158			self.progress.emit(0)
2159		return current
2160
2161	def HasMoreRecords(self):
2162		return self.more
2163
2164	def columnCount(self, parent=None):
2165		return len(self.column_headers)
2166
2167	def columnHeader(self, column):
2168		return self.column_headers[column]
2169
2170	def SQLTableDataPrep(self, query, count):
2171		data = []
2172		for i in xrange(count):
2173			data.append(query.value(i))
2174		return data
2175
2176# SQL automatic table data model
2177
2178class SQLAutoTableModel(SQLTableModel):
2179
2180	def __init__(self, glb, table_name, parent=None):
2181		sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
2182		if table_name == "comm_threads_view":
2183			# For now, comm_threads_view has no id column
2184			sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
2185		column_headers = []
2186		query = QSqlQuery(glb.db)
2187		if glb.dbref.is_sqlite3:
2188			QueryExec(query, "PRAGMA table_info(" + table_name + ")")
2189			while query.next():
2190				column_headers.append(query.value(1))
2191			if table_name == "sqlite_master":
2192				sql = "SELECT * FROM " + table_name
2193		else:
2194			if table_name[:19] == "information_schema.":
2195				sql = "SELECT * FROM " + table_name
2196				select_table_name = table_name[19:]
2197				schema = "information_schema"
2198			else:
2199				select_table_name = table_name
2200				schema = "public"
2201			QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
2202			while query.next():
2203				column_headers.append(query.value(0))
2204		if pyside_version_1 and sys.version_info[0] == 3:
2205			if table_name == "samples_view":
2206				self.SQLTableDataPrep = self.samples_view_DataPrep
2207			if table_name == "samples":
2208				self.SQLTableDataPrep = self.samples_DataPrep
2209		super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
2210
2211	def samples_view_DataPrep(self, query, count):
2212		data = []
2213		data.append(query.value(0))
2214		# Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2215		data.append("{:>19}".format(query.value(1)))
2216		for i in xrange(2, count):
2217			data.append(query.value(i))
2218		return data
2219
2220	def samples_DataPrep(self, query, count):
2221		data = []
2222		for i in xrange(9):
2223			data.append(query.value(i))
2224		# Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
2225		data.append("{:>19}".format(query.value(9)))
2226		for i in xrange(10, count):
2227			data.append(query.value(i))
2228		return data
2229
2230# Base class for custom ResizeColumnsToContents
2231
2232class ResizeColumnsToContentsBase(QObject):
2233
2234	def __init__(self, parent=None):
2235		super(ResizeColumnsToContentsBase, self).__init__(parent)
2236
2237	def ResizeColumnToContents(self, column, n):
2238		# Using the view's resizeColumnToContents() here is extrememly slow
2239		# so implement a crude alternative
2240		font = self.view.font()
2241		metrics = QFontMetrics(font)
2242		max = 0
2243		for row in xrange(n):
2244			val = self.data_model.child_items[row].data[column]
2245			len = metrics.width(str(val) + "MM")
2246			max = len if len > max else max
2247		val = self.data_model.columnHeader(column)
2248		len = metrics.width(str(val) + "MM")
2249		max = len if len > max else max
2250		self.view.setColumnWidth(column, max)
2251
2252	def ResizeColumnsToContents(self):
2253		n = min(self.data_model.child_count, 100)
2254		if n < 1:
2255			# No data yet, so connect a signal to notify when there is
2256			self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
2257			return
2258		columns = self.data_model.columnCount()
2259		for i in xrange(columns):
2260			self.ResizeColumnToContents(i, n)
2261
2262	def UpdateColumnWidths(self, *x):
2263		# This only needs to be done once, so disconnect the signal now
2264		self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
2265		self.ResizeColumnsToContents()
2266
2267# Table window
2268
2269class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2270
2271	def __init__(self, glb, table_name, parent=None):
2272		super(TableWindow, self).__init__(parent)
2273
2274		self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
2275
2276		self.model = QSortFilterProxyModel()
2277		self.model.setSourceModel(self.data_model)
2278
2279		self.view = QTableView()
2280		self.view.setModel(self.model)
2281		self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2282		self.view.verticalHeader().setVisible(False)
2283		self.view.sortByColumn(-1, Qt.AscendingOrder)
2284		self.view.setSortingEnabled(True)
2285
2286		self.ResizeColumnsToContents()
2287
2288		self.find_bar = FindBar(self, self, True)
2289
2290		self.finder = ChildDataItemFinder(self.data_model)
2291
2292		self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2293
2294		self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2295
2296		self.setWidget(self.vbox.Widget())
2297
2298		AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
2299
2300	def Find(self, value, direction, pattern, context):
2301		self.view.setFocus()
2302		self.find_bar.Busy()
2303		self.finder.Find(value, direction, pattern, context, self.FindDone)
2304
2305	def FindDone(self, row):
2306		self.find_bar.Idle()
2307		if row >= 0:
2308			self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
2309		else:
2310			self.find_bar.NotFound()
2311
2312# Table list
2313
2314def GetTableList(glb):
2315	tables = []
2316	query = QSqlQuery(glb.db)
2317	if glb.dbref.is_sqlite3:
2318		QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
2319	else:
2320		QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
2321	while query.next():
2322		tables.append(query.value(0))
2323	if glb.dbref.is_sqlite3:
2324		tables.append("sqlite_master")
2325	else:
2326		tables.append("information_schema.tables")
2327		tables.append("information_schema.views")
2328		tables.append("information_schema.columns")
2329	return tables
2330
2331# Top Calls data model
2332
2333class TopCallsModel(SQLTableModel):
2334
2335	def __init__(self, glb, report_vars, parent=None):
2336		text = ""
2337		if not glb.dbref.is_sqlite3:
2338			text = "::text"
2339		limit = ""
2340		if len(report_vars.limit):
2341			limit = " LIMIT " + report_vars.limit
2342		sql = ("SELECT comm, pid, tid, name,"
2343			" CASE"
2344			" WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
2345			" ELSE short_name"
2346			" END AS dso,"
2347			" call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
2348			" CASE"
2349			" WHEN (calls.flags = 1) THEN 'no call'" + text +
2350			" WHEN (calls.flags = 2) THEN 'no return'" + text +
2351			" WHEN (calls.flags = 3) THEN 'no call/return'" + text +
2352			" ELSE ''" + text +
2353			" END AS flags"
2354			" FROM calls"
2355			" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
2356			" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
2357			" INNER JOIN dsos ON symbols.dso_id = dsos.id"
2358			" INNER JOIN comms ON calls.comm_id = comms.id"
2359			" INNER JOIN threads ON calls.thread_id = threads.id" +
2360			report_vars.where_clause +
2361			" ORDER BY elapsed_time DESC" +
2362			limit
2363			)
2364		column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
2365		self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
2366		super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
2367
2368	def columnAlignment(self, column):
2369		return self.alignment[column]
2370
2371# Top Calls report creation dialog
2372
2373class TopCallsDialog(ReportDialogBase):
2374
2375	def __init__(self, glb, parent=None):
2376		title = "Top Calls by Elapsed Time"
2377		items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2378			 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
2379			 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2380			 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2381			 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
2382			 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
2383			 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
2384			 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
2385		super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
2386
2387# Top Calls window
2388
2389class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2390
2391	def __init__(self, glb, report_vars, parent=None):
2392		super(TopCallsWindow, self).__init__(parent)
2393
2394		self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
2395		self.model = self.data_model
2396
2397		self.view = QTableView()
2398		self.view.setModel(self.model)
2399		self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2400		self.view.verticalHeader().setVisible(False)
2401
2402		self.ResizeColumnsToContents()
2403
2404		self.find_bar = FindBar(self, self, True)
2405
2406		self.finder = ChildDataItemFinder(self.model)
2407
2408		self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2409
2410		self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2411
2412		self.setWidget(self.vbox.Widget())
2413
2414		AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
2415
2416	def Find(self, value, direction, pattern, context):
2417		self.view.setFocus()
2418		self.find_bar.Busy()
2419		self.finder.Find(value, direction, pattern, context, self.FindDone)
2420
2421	def FindDone(self, row):
2422		self.find_bar.Idle()
2423		if row >= 0:
2424			self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
2425		else:
2426			self.find_bar.NotFound()
2427
2428# Action Definition
2429
2430def CreateAction(label, tip, callback, parent=None, shortcut=None):
2431	action = QAction(label, parent)
2432	if shortcut != None:
2433		action.setShortcuts(shortcut)
2434	action.setStatusTip(tip)
2435	action.triggered.connect(callback)
2436	return action
2437
2438# Typical application actions
2439
2440def CreateExitAction(app, parent=None):
2441	return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
2442
2443# Typical MDI actions
2444
2445def CreateCloseActiveWindowAction(mdi_area):
2446	return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
2447
2448def CreateCloseAllWindowsAction(mdi_area):
2449	return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
2450
2451def CreateTileWindowsAction(mdi_area):
2452	return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
2453
2454def CreateCascadeWindowsAction(mdi_area):
2455	return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
2456
2457def CreateNextWindowAction(mdi_area):
2458	return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
2459
2460def CreatePreviousWindowAction(mdi_area):
2461	return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
2462
2463# Typical MDI window menu
2464
2465class WindowMenu():
2466
2467	def __init__(self, mdi_area, menu):
2468		self.mdi_area = mdi_area
2469		self.window_menu = menu.addMenu("&Windows")
2470		self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
2471		self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
2472		self.tile_windows = CreateTileWindowsAction(mdi_area)
2473		self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
2474		self.next_window = CreateNextWindowAction(mdi_area)
2475		self.previous_window = CreatePreviousWindowAction(mdi_area)
2476		self.window_menu.aboutToShow.connect(self.Update)
2477
2478	def Update(self):
2479		self.window_menu.clear()
2480		sub_window_count = len(self.mdi_area.subWindowList())
2481		have_sub_windows = sub_window_count != 0
2482		self.close_active_window.setEnabled(have_sub_windows)
2483		self.close_all_windows.setEnabled(have_sub_windows)
2484		self.tile_windows.setEnabled(have_sub_windows)
2485		self.cascade_windows.setEnabled(have_sub_windows)
2486		self.next_window.setEnabled(have_sub_windows)
2487		self.previous_window.setEnabled(have_sub_windows)
2488		self.window_menu.addAction(self.close_active_window)
2489		self.window_menu.addAction(self.close_all_windows)
2490		self.window_menu.addSeparator()
2491		self.window_menu.addAction(self.tile_windows)
2492		self.window_menu.addAction(self.cascade_windows)
2493		self.window_menu.addSeparator()
2494		self.window_menu.addAction(self.next_window)
2495		self.window_menu.addAction(self.previous_window)
2496		if sub_window_count == 0:
2497			return
2498		self.window_menu.addSeparator()
2499		nr = 1
2500		for sub_window in self.mdi_area.subWindowList():
2501			label = str(nr) + " " + sub_window.name
2502			if nr < 10:
2503				label = "&" + label
2504			action = self.window_menu.addAction(label)
2505			action.setCheckable(True)
2506			action.setChecked(sub_window == self.mdi_area.activeSubWindow())
2507			action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
2508			self.window_menu.addAction(action)
2509			nr += 1
2510
2511	def setActiveSubWindow(self, nr):
2512		self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
2513
2514# Help text
2515
2516glb_help_text = """
2517<h1>Contents</h1>
2518<style>
2519p.c1 {
2520    text-indent: 40px;
2521}
2522p.c2 {
2523    text-indent: 80px;
2524}
2525}
2526</style>
2527<p class=c1><a href=#reports>1. Reports</a></p>
2528<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
2529<p class=c2><a href=#calltree>1.2 Call Tree</a></p>
2530<p class=c2><a href=#allbranches>1.3 All branches</a></p>
2531<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
2532<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
2533<p class=c1><a href=#tables>2. Tables</a></p>
2534<h1 id=reports>1. Reports</h1>
2535<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
2536The result is a GUI window with a tree representing a context-sensitive
2537call-graph. Expanding a couple of levels of the tree and adjusting column
2538widths to suit will display something like:
2539<pre>
2540                                         Call Graph: pt_example
2541Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
2542v- ls
2543    v- 2638:2638
2544        v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
2545          |- unknown               unknown       1        13198     0.1              1              0.0
2546          >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
2547          >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
2548          v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
2549             >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
2550             >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
2551             >- __libc_csu_init    ls            1        10354     0.1             10              0.0
2552             |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
2553             v- main               ls            1      8182043    99.6         180254             99.9
2554</pre>
2555<h3>Points to note:</h3>
2556<ul>
2557<li>The top level is a command name (comm)</li>
2558<li>The next level is a thread (pid:tid)</li>
2559<li>Subsequent levels are functions</li>
2560<li>'Count' is the number of calls</li>
2561<li>'Time' is the elapsed time until the function returns</li>
2562<li>Percentages are relative to the level above</li>
2563<li>'Branch Count' is the total number of branches for that function and all functions that it calls
2564</ul>
2565<h3>Find</h3>
2566Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
2567The pattern matching symbols are ? for any character and * for zero or more characters.
2568<h2 id=calltree>1.2 Call Tree</h2>
2569The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
2570Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
2571<h2 id=allbranches>1.3 All branches</h2>
2572The All branches report displays all branches in chronological order.
2573Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
2574<h3>Disassembly</h3>
2575Open a branch to display disassembly. This only works if:
2576<ol>
2577<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
2578<li>The object code is available. Currently, only the perf build ID cache is searched for object code.
2579The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
2580One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
2581or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
2582</ol>
2583<h4 id=xed>Intel XED Setup</h4>
2584To use Intel XED, libxed.so must be present.  To build and install libxed.so:
2585<pre>
2586git clone https://github.com/intelxed/mbuild.git mbuild
2587git clone https://github.com/intelxed/xed
2588cd xed
2589./mfile.py --share
2590sudo ./mfile.py --prefix=/usr/local install
2591sudo ldconfig
2592</pre>
2593<h3>Find</h3>
2594Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2595Refer to Python documentation for the regular expression syntax.
2596All columns are searched, but only currently fetched rows are searched.
2597<h2 id=selectedbranches>1.4 Selected branches</h2>
2598This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
2599by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2600<h3>1.4.1 Time ranges</h3>
2601The time ranges hint text shows the total time range. Relative time ranges can also be entered in
2602ms, us or ns. Also, negative values are relative to the end of trace.  Examples:
2603<pre>
2604	81073085947329-81073085958238	From 81073085947329 to 81073085958238
2605	100us-200us		From 100us to 200us
2606	10ms-			From 10ms to the end
2607	-100ns			The first 100ns
2608	-10ms-			The last 10ms
2609</pre>
2610N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
2611<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
2612The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
2613The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2614If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
2615<h1 id=tables>2. Tables</h1>
2616The Tables menu shows all tables and views in the database. Most tables have an associated view
2617which displays the information in a more friendly way. Not all data for large tables is fetched
2618immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
2619but that can be slow for large tables.
2620<p>There are also tables of database meta-information.
2621For SQLite3 databases, the sqlite_master table is included.
2622For PostgreSQL databases, information_schema.tables/views/columns are included.
2623<h3>Find</h3>
2624Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2625Refer to Python documentation for the regular expression syntax.
2626All columns are searched, but only currently fetched rows are searched.
2627<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
2628will go to the next/previous result in id order, instead of display order.
2629"""
2630
2631# Help window
2632
2633class HelpWindow(QMdiSubWindow):
2634
2635	def __init__(self, glb, parent=None):
2636		super(HelpWindow, self).__init__(parent)
2637
2638		self.text = QTextBrowser()
2639		self.text.setHtml(glb_help_text)
2640		self.text.setReadOnly(True)
2641		self.text.setOpenExternalLinks(True)
2642
2643		self.setWidget(self.text)
2644
2645		AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
2646
2647# Main window that only displays the help text
2648
2649class HelpOnlyWindow(QMainWindow):
2650
2651	def __init__(self, parent=None):
2652		super(HelpOnlyWindow, self).__init__(parent)
2653
2654		self.setMinimumSize(200, 100)
2655		self.resize(800, 600)
2656		self.setWindowTitle("Exported SQL Viewer Help")
2657		self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
2658
2659		self.text = QTextBrowser()
2660		self.text.setHtml(glb_help_text)
2661		self.text.setReadOnly(True)
2662		self.text.setOpenExternalLinks(True)
2663
2664		self.setCentralWidget(self.text)
2665
2666# Font resize
2667
2668def ResizeFont(widget, diff):
2669	font = widget.font()
2670	sz = font.pointSize()
2671	font.setPointSize(sz + diff)
2672	widget.setFont(font)
2673
2674def ShrinkFont(widget):
2675	ResizeFont(widget, -1)
2676
2677def EnlargeFont(widget):
2678	ResizeFont(widget, 1)
2679
2680# Unique name for sub-windows
2681
2682def NumberedWindowName(name, nr):
2683	if nr > 1:
2684		name += " <" + str(nr) + ">"
2685	return name
2686
2687def UniqueSubWindowName(mdi_area, name):
2688	nr = 1
2689	while True:
2690		unique_name = NumberedWindowName(name, nr)
2691		ok = True
2692		for sub_window in mdi_area.subWindowList():
2693			if sub_window.name == unique_name:
2694				ok = False
2695				break
2696		if ok:
2697			return unique_name
2698		nr += 1
2699
2700# Add a sub-window
2701
2702def AddSubWindow(mdi_area, sub_window, name):
2703	unique_name = UniqueSubWindowName(mdi_area, name)
2704	sub_window.setMinimumSize(200, 100)
2705	sub_window.resize(800, 600)
2706	sub_window.setWindowTitle(unique_name)
2707	sub_window.setAttribute(Qt.WA_DeleteOnClose)
2708	sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
2709	sub_window.name = unique_name
2710	mdi_area.addSubWindow(sub_window)
2711	sub_window.show()
2712
2713# Main window
2714
2715class MainWindow(QMainWindow):
2716
2717	def __init__(self, glb, parent=None):
2718		super(MainWindow, self).__init__(parent)
2719
2720		self.glb = glb
2721
2722		self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
2723		self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
2724		self.setMinimumSize(200, 100)
2725
2726		self.mdi_area = QMdiArea()
2727		self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2728		self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2729
2730		self.setCentralWidget(self.mdi_area)
2731
2732		menu = self.menuBar()
2733
2734		file_menu = menu.addMenu("&File")
2735		file_menu.addAction(CreateExitAction(glb.app, self))
2736
2737		edit_menu = menu.addMenu("&Edit")
2738		edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
2739		edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
2740		edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
2741		edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
2742
2743		reports_menu = menu.addMenu("&Reports")
2744		if IsSelectable(glb.db, "calls"):
2745			reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
2746
2747		if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
2748			reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
2749
2750		self.EventMenu(GetEventList(glb.db), reports_menu)
2751
2752		if IsSelectable(glb.db, "calls"):
2753			reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
2754
2755		self.TableMenu(GetTableList(glb), menu)
2756
2757		self.window_menu = WindowMenu(self.mdi_area, menu)
2758
2759		help_menu = menu.addMenu("&Help")
2760		help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
2761
2762	def Try(self, fn):
2763		win = self.mdi_area.activeSubWindow()
2764		if win:
2765			try:
2766				fn(win.view)
2767			except:
2768				pass
2769
2770	def Find(self):
2771		win = self.mdi_area.activeSubWindow()
2772		if win:
2773			try:
2774				win.find_bar.Activate()
2775			except:
2776				pass
2777
2778	def FetchMoreRecords(self):
2779		win = self.mdi_area.activeSubWindow()
2780		if win:
2781			try:
2782				win.fetch_bar.Activate()
2783			except:
2784				pass
2785
2786	def ShrinkFont(self):
2787		self.Try(ShrinkFont)
2788
2789	def EnlargeFont(self):
2790		self.Try(EnlargeFont)
2791
2792	def EventMenu(self, events, reports_menu):
2793		branches_events = 0
2794		for event in events:
2795			event = event.split(":")[0]
2796			if event == "branches":
2797				branches_events += 1
2798		dbid = 0
2799		for event in events:
2800			dbid += 1
2801			event = event.split(":")[0]
2802			if event == "branches":
2803				label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
2804				reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewBranchView(x), self))
2805				label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
2806				reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewSelectedBranchView(x), self))
2807
2808	def TableMenu(self, tables, menu):
2809		table_menu = menu.addMenu("&Tables")
2810		for table in tables:
2811			table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda t=table: self.NewTableView(t), self))
2812
2813	def NewCallGraph(self):
2814		CallGraphWindow(self.glb, self)
2815
2816	def NewCallTree(self):
2817		CallTreeWindow(self.glb, self)
2818
2819	def NewTopCalls(self):
2820		dialog = TopCallsDialog(self.glb, self)
2821		ret = dialog.exec_()
2822		if ret:
2823			TopCallsWindow(self.glb, dialog.report_vars, self)
2824
2825	def NewBranchView(self, event_id):
2826		BranchWindow(self.glb, event_id, ReportVars(), self)
2827
2828	def NewSelectedBranchView(self, event_id):
2829		dialog = SelectedBranchDialog(self.glb, self)
2830		ret = dialog.exec_()
2831		if ret:
2832			BranchWindow(self.glb, event_id, dialog.report_vars, self)
2833
2834	def NewTableView(self, table_name):
2835		TableWindow(self.glb, table_name, self)
2836
2837	def Help(self):
2838		HelpWindow(self.glb, self)
2839
2840# XED Disassembler
2841
2842class xed_state_t(Structure):
2843
2844	_fields_ = [
2845		("mode", c_int),
2846		("width", c_int)
2847	]
2848
2849class XEDInstruction():
2850
2851	def __init__(self, libxed):
2852		# Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
2853		xedd_t = c_byte * 512
2854		self.xedd = xedd_t()
2855		self.xedp = addressof(self.xedd)
2856		libxed.xed_decoded_inst_zero(self.xedp)
2857		self.state = xed_state_t()
2858		self.statep = addressof(self.state)
2859		# Buffer for disassembled instruction text
2860		self.buffer = create_string_buffer(256)
2861		self.bufferp = addressof(self.buffer)
2862
2863class LibXED():
2864
2865	def __init__(self):
2866		try:
2867			self.libxed = CDLL("libxed.so")
2868		except:
2869			self.libxed = None
2870		if not self.libxed:
2871			self.libxed = CDLL("/usr/local/lib/libxed.so")
2872
2873		self.xed_tables_init = self.libxed.xed_tables_init
2874		self.xed_tables_init.restype = None
2875		self.xed_tables_init.argtypes = []
2876
2877		self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
2878		self.xed_decoded_inst_zero.restype = None
2879		self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
2880
2881		self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
2882		self.xed_operand_values_set_mode.restype = None
2883		self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
2884
2885		self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
2886		self.xed_decoded_inst_zero_keep_mode.restype = None
2887		self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
2888
2889		self.xed_decode = self.libxed.xed_decode
2890		self.xed_decode.restype = c_int
2891		self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
2892
2893		self.xed_format_context = self.libxed.xed_format_context
2894		self.xed_format_context.restype = c_uint
2895		self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
2896
2897		self.xed_tables_init()
2898
2899	def Instruction(self):
2900		return XEDInstruction(self)
2901
2902	def SetMode(self, inst, mode):
2903		if mode:
2904			inst.state.mode = 4 # 32-bit
2905			inst.state.width = 4 # 4 bytes
2906		else:
2907			inst.state.mode = 1 # 64-bit
2908			inst.state.width = 8 # 8 bytes
2909		self.xed_operand_values_set_mode(inst.xedp, inst.statep)
2910
2911	def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
2912		self.xed_decoded_inst_zero_keep_mode(inst.xedp)
2913		err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
2914		if err:
2915			return 0, ""
2916		# Use AT&T mode (2), alternative is Intel (3)
2917		ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
2918		if not ok:
2919			return 0, ""
2920		if sys.version_info[0] == 2:
2921			result = inst.buffer.value
2922		else:
2923			result = inst.buffer.value.decode()
2924		# Return instruction length and the disassembled instruction text
2925		# For now, assume the length is in byte 166
2926		return inst.xedd[166], result
2927
2928def TryOpen(file_name):
2929	try:
2930		return open(file_name, "rb")
2931	except:
2932		return None
2933
2934def Is64Bit(f):
2935	result = sizeof(c_void_p)
2936	# ELF support only
2937	pos = f.tell()
2938	f.seek(0)
2939	header = f.read(7)
2940	f.seek(pos)
2941	magic = header[0:4]
2942	if sys.version_info[0] == 2:
2943		eclass = ord(header[4])
2944		encoding = ord(header[5])
2945		version = ord(header[6])
2946	else:
2947		eclass = header[4]
2948		encoding = header[5]
2949		version = header[6]
2950	if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
2951		result = True if eclass == 2 else False
2952	return result
2953
2954# Global data
2955
2956class Glb():
2957
2958	def __init__(self, dbref, db, dbname):
2959		self.dbref = dbref
2960		self.db = db
2961		self.dbname = dbname
2962		self.home_dir = os.path.expanduser("~")
2963		self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
2964		if self.buildid_dir:
2965			self.buildid_dir += "/.build-id/"
2966		else:
2967			self.buildid_dir = self.home_dir + "/.debug/.build-id/"
2968		self.app = None
2969		self.mainwindow = None
2970		self.instances_to_shutdown_on_exit = weakref.WeakSet()
2971		try:
2972			self.disassembler = LibXED()
2973			self.have_disassembler = True
2974		except:
2975			self.have_disassembler = False
2976
2977	def FileFromBuildId(self, build_id):
2978		file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
2979		return TryOpen(file_name)
2980
2981	def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
2982		# Assume current machine i.e. no support for virtualization
2983		if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
2984			file_name = os.getenv("PERF_KCORE")
2985			f = TryOpen(file_name) if file_name else None
2986			if f:
2987				return f
2988			# For now, no special handling if long_name is /proc/kcore
2989			f = TryOpen(long_name)
2990			if f:
2991				return f
2992		f = self.FileFromBuildId(build_id)
2993		if f:
2994			return f
2995		return None
2996
2997	def AddInstanceToShutdownOnExit(self, instance):
2998		self.instances_to_shutdown_on_exit.add(instance)
2999
3000	# Shutdown any background processes or threads
3001	def ShutdownInstances(self):
3002		for x in self.instances_to_shutdown_on_exit:
3003			try:
3004				x.Shutdown()
3005			except:
3006				pass
3007
3008# Database reference
3009
3010class DBRef():
3011
3012	def __init__(self, is_sqlite3, dbname):
3013		self.is_sqlite3 = is_sqlite3
3014		self.dbname = dbname
3015
3016	def Open(self, connection_name):
3017		dbname = self.dbname
3018		if self.is_sqlite3:
3019			db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
3020		else:
3021			db = QSqlDatabase.addDatabase("QPSQL", connection_name)
3022			opts = dbname.split()
3023			for opt in opts:
3024				if "=" in opt:
3025					opt = opt.split("=")
3026					if opt[0] == "hostname":
3027						db.setHostName(opt[1])
3028					elif opt[0] == "port":
3029						db.setPort(int(opt[1]))
3030					elif opt[0] == "username":
3031						db.setUserName(opt[1])
3032					elif opt[0] == "password":
3033						db.setPassword(opt[1])
3034					elif opt[0] == "dbname":
3035						dbname = opt[1]
3036				else:
3037					dbname = opt
3038
3039		db.setDatabaseName(dbname)
3040		if not db.open():
3041			raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
3042		return db, dbname
3043
3044# Main
3045
3046def Main():
3047	if (len(sys.argv) < 2):
3048		printerr("Usage is: exported-sql-viewer.py {<database name> | --help-only}");
3049		raise Exception("Too few arguments")
3050
3051	dbname = sys.argv[1]
3052	if dbname == "--help-only":
3053		app = QApplication(sys.argv)
3054		mainwindow = HelpOnlyWindow()
3055		mainwindow.show()
3056		err = app.exec_()
3057		sys.exit(err)
3058
3059	is_sqlite3 = False
3060	try:
3061		f = open(dbname, "rb")
3062		if f.read(15) == b'SQLite format 3':
3063			is_sqlite3 = True
3064		f.close()
3065	except:
3066		pass
3067
3068	dbref = DBRef(is_sqlite3, dbname)
3069	db, dbname = dbref.Open("main")
3070	glb = Glb(dbref, db, dbname)
3071	app = QApplication(sys.argv)
3072	glb.app = app
3073	mainwindow = MainWindow(glb)
3074	glb.mainwindow = mainwindow
3075	mainwindow.show()
3076	err = app.exec_()
3077	glb.ShutdownInstances()
3078	db.close()
3079	sys.exit(err)
3080
3081if __name__ == "__main__":
3082	Main()
3083