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