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