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