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