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