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