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