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