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