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