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