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