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