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