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