1#!/usr/bin/env python
2# SPDX-License-Identifier: GPL-2.0
3# exported-sql-viewer.py: view data from sql database
4# Copyright (c) 2014-2018, Intel Corporation.
5
6# To use this script you will need to have exported data using either the
7# export-to-sqlite.py or the export-to-postgresql.py script.  Refer to those
8# scripts for details.
9#
10# Following on from the example in the export scripts, a
11# call-graph can be displayed for the pt_example database like this:
12#
13#	python tools/perf/scripts/python/exported-sql-viewer.py pt_example
14#
15# Note that for PostgreSQL, this script supports connecting to remote databases
16# by setting hostname, port, username, password, and dbname e.g.
17#
18#	python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
19#
20# The result is a GUI window with a tree representing a context-sensitive
21# call-graph.  Expanding a couple of levels of the tree and adjusting column
22# widths to suit will display something like:
23#
24#                                         Call Graph: pt_example
25# Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
26# v- ls
27#     v- 2638:2638
28#         v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
29#           |- unknown               unknown       1        13198     0.1              1              0.0
30#           >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
31#           >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
32#           v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
33#              >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
34#              >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
35#              >- __libc_csu_init    ls            1        10354     0.1             10              0.0
36#              |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
37#              v- main               ls            1      8182043    99.6         180254             99.9
38#
39# Points to note:
40#	The top level is a command name (comm)
41#	The next level is a thread (pid:tid)
42#	Subsequent levels are functions
43#	'Count' is the number of calls
44#	'Time' is the elapsed time until the function returns
45#	Percentages are relative to the level above
46#	'Branch Count' is the total number of branches for that function and all
47#       functions that it calls
48
49# There is also a "All branches" report, which displays branches and
50# possibly disassembly.  However, presently, the only supported disassembler is
51# Intel XED, and additionally the object code must be present in perf build ID
52# cache. To use Intel XED, libxed.so must be present. To build and install
53# libxed.so:
54#            git clone https://github.com/intelxed/mbuild.git mbuild
55#            git clone https://github.com/intelxed/xed
56#            cd xed
57#            ./mfile.py --share
58#            sudo ./mfile.py --prefix=/usr/local install
59#            sudo ldconfig
60#
61# Example report:
62#
63# Time           CPU  Command  PID    TID    Branch Type            In Tx  Branch
64# 8107675239590  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
65#                                                                              7fab593ea260 48 89 e7                                        mov %rsp, %rdi
66# 8107675239899  2    ls       22011  22011  hardware interrupt     No         7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
67# 8107675241900  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
68#                                                                              7fab593ea260 48 89 e7                                        mov %rsp, %rdi
69#                                                                              7fab593ea263 e8 c8 06 00 00                                  callq  0x7fab593ea930
70# 8107675241900  2    ls       22011  22011  call                   No         7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
71#                                                                              7fab593ea930 55                                              pushq  %rbp
72#                                                                              7fab593ea931 48 89 e5                                        mov %rsp, %rbp
73#                                                                              7fab593ea934 41 57                                           pushq  %r15
74#                                                                              7fab593ea936 41 56                                           pushq  %r14
75#                                                                              7fab593ea938 41 55                                           pushq  %r13
76#                                                                              7fab593ea93a 41 54                                           pushq  %r12
77#                                                                              7fab593ea93c 53                                              pushq  %rbx
78#                                                                              7fab593ea93d 48 89 fb                                        mov %rdi, %rbx
79#                                                                              7fab593ea940 48 83 ec 68                                     sub $0x68, %rsp
80#                                                                              7fab593ea944 0f 31                                           rdtsc
81#                                                                              7fab593ea946 48 c1 e2 20                                     shl $0x20, %rdx
82#                                                                              7fab593ea94a 89 c0                                           mov %eax, %eax
83#                                                                              7fab593ea94c 48 09 c2                                        or %rax, %rdx
84#                                                                              7fab593ea94f 48 8b 05 1a 15 22 00                            movq  0x22151a(%rip), %rax
85# 8107675242232  2    ls       22011  22011  hardware interrupt     No         7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
86# 8107675242900  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
87#                                                                              7fab593ea94f 48 8b 05 1a 15 22 00                            movq  0x22151a(%rip), %rax
88#                                                                              7fab593ea956 48 89 15 3b 13 22 00                            movq  %rdx, 0x22133b(%rip)
89# 8107675243232  2    ls       22011  22011  hardware interrupt     No         7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
90
91from __future__ import print_function
92
93import sys
94import argparse
95import weakref
96import threading
97import string
98try:
99	# Python2
100	import cPickle as pickle
101	# size of pickled integer big enough for record size
102	glb_nsz = 8
103except ImportError:
104	import pickle
105	glb_nsz = 16
106import re
107import os
108import random
109import copy
110import math
111
112pyside_version_1 = True
113if not "--pyside-version-1" in sys.argv:
114	try:
115		from PySide2.QtCore import *
116		from PySide2.QtGui import *
117		from PySide2.QtSql import *
118		from PySide2.QtWidgets import *
119		pyside_version_1 = False
120	except:
121		pass
122
123if pyside_version_1:
124	from PySide.QtCore import *
125	from PySide.QtGui import *
126	from PySide.QtSql import *
127
128from decimal import *
129from ctypes import *
130from multiprocessing import Process, Array, Value, Event
131
132# xrange is range in Python3
133try:
134	xrange
135except NameError:
136	xrange = range
137
138def printerr(*args, **keyword_args):
139	print(*args, file=sys.stderr, **keyword_args)
140
141# Data formatting helpers
142
143def tohex(ip):
144	if ip < 0:
145		ip += 1 << 64
146	return "%x" % ip
147
148def offstr(offset):
149	if offset:
150		return "+0x%x" % offset
151	return ""
152
153def dsoname(name):
154	if name == "[kernel.kallsyms]":
155		return "[kernel]"
156	return name
157
158def findnth(s, sub, n, offs=0):
159	pos = s.find(sub)
160	if pos < 0:
161		return pos
162	if n <= 1:
163		return offs + pos
164	return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
165
166# Percent to one decimal place
167
168def PercentToOneDP(n, d):
169	if not d:
170		return "0.0"
171	x = (n * Decimal(100)) / d
172	return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
173
174# Helper for queries that must not fail
175
176def QueryExec(query, stmt):
177	ret = query.exec_(stmt)
178	if not ret:
179		raise Exception("Query failed: " + query.lastError().text())
180
181# Background thread
182
183class Thread(QThread):
184
185	done = Signal(object)
186
187	def __init__(self, task, param=None, parent=None):
188		super(Thread, self).__init__(parent)
189		self.task = task
190		self.param = param
191
192	def run(self):
193		while True:
194			if self.param is None:
195				done, result = self.task()
196			else:
197				done, result = self.task(self.param)
198			self.done.emit(result)
199			if done:
200				break
201
202# Tree data model
203
204class TreeModel(QAbstractItemModel):
205
206	def __init__(self, glb, params, parent=None):
207		super(TreeModel, self).__init__(parent)
208		self.glb = glb
209		self.params = params
210		self.root = self.GetRoot()
211		self.last_row_read = 0
212
213	def Item(self, parent):
214		if parent.isValid():
215			return parent.internalPointer()
216		else:
217			return self.root
218
219	def rowCount(self, parent):
220		result = self.Item(parent).childCount()
221		if result < 0:
222			result = 0
223			self.dataChanged.emit(parent, parent)
224		return result
225
226	def hasChildren(self, parent):
227		return self.Item(parent).hasChildren()
228
229	def headerData(self, section, orientation, role):
230		if role == Qt.TextAlignmentRole:
231			return self.columnAlignment(section)
232		if role != Qt.DisplayRole:
233			return None
234		if orientation != Qt.Horizontal:
235			return None
236		return self.columnHeader(section)
237
238	def parent(self, child):
239		child_item = child.internalPointer()
240		if child_item is self.root:
241			return QModelIndex()
242		parent_item = child_item.getParentItem()
243		return self.createIndex(parent_item.getRow(), 0, parent_item)
244
245	def index(self, row, column, parent):
246		child_item = self.Item(parent).getChildItem(row)
247		return self.createIndex(row, column, child_item)
248
249	def DisplayData(self, item, index):
250		return item.getData(index.column())
251
252	def FetchIfNeeded(self, row):
253		if row > self.last_row_read:
254			self.last_row_read = row
255			if row + 10 >= self.root.child_count:
256				self.fetcher.Fetch(glb_chunk_sz)
257
258	def columnAlignment(self, column):
259		return Qt.AlignLeft
260
261	def columnFont(self, column):
262		return None
263
264	def data(self, index, role):
265		if role == Qt.TextAlignmentRole:
266			return self.columnAlignment(index.column())
267		if role == Qt.FontRole:
268			return self.columnFont(index.column())
269		if role != Qt.DisplayRole:
270			return None
271		item = index.internalPointer()
272		return self.DisplayData(item, index)
273
274# Table data model
275
276class TableModel(QAbstractTableModel):
277
278	def __init__(self, parent=None):
279		super(TableModel, self).__init__(parent)
280		self.child_count = 0
281		self.child_items = []
282		self.last_row_read = 0
283
284	def Item(self, parent):
285		if parent.isValid():
286			return parent.internalPointer()
287		else:
288			return self
289
290	def rowCount(self, parent):
291		return self.child_count
292
293	def headerData(self, section, orientation, role):
294		if role == Qt.TextAlignmentRole:
295			return self.columnAlignment(section)
296		if role != Qt.DisplayRole:
297			return None
298		if orientation != Qt.Horizontal:
299			return None
300		return self.columnHeader(section)
301
302	def index(self, row, column, parent):
303		return self.createIndex(row, column, self.child_items[row])
304
305	def DisplayData(self, item, index):
306		return item.getData(index.column())
307
308	def FetchIfNeeded(self, row):
309		if row > self.last_row_read:
310			self.last_row_read = row
311			if row + 10 >= self.child_count:
312				self.fetcher.Fetch(glb_chunk_sz)
313
314	def columnAlignment(self, column):
315		return Qt.AlignLeft
316
317	def columnFont(self, column):
318		return None
319
320	def data(self, index, role):
321		if role == Qt.TextAlignmentRole:
322			return self.columnAlignment(index.column())
323		if role == Qt.FontRole:
324			return self.columnFont(index.column())
325		if role != Qt.DisplayRole:
326			return None
327		item = index.internalPointer()
328		return self.DisplayData(item, index)
329
330# Model cache
331
332model_cache = weakref.WeakValueDictionary()
333model_cache_lock = threading.Lock()
334
335def LookupCreateModel(model_name, create_fn):
336	model_cache_lock.acquire()
337	try:
338		model = model_cache[model_name]
339	except:
340		model = None
341	if model is None:
342		model = create_fn()
343		model_cache[model_name] = model
344	model_cache_lock.release()
345	return model
346
347def LookupModel(model_name):
348	model_cache_lock.acquire()
349	try:
350		model = model_cache[model_name]
351	except:
352		model = None
353	model_cache_lock.release()
354	return model
355
356# Find bar
357
358class FindBar():
359
360	def __init__(self, parent, finder, is_reg_expr=False):
361		self.finder = finder
362		self.context = []
363		self.last_value = None
364		self.last_pattern = None
365
366		label = QLabel("Find:")
367		label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
368
369		self.textbox = QComboBox()
370		self.textbox.setEditable(True)
371		self.textbox.currentIndexChanged.connect(self.ValueChanged)
372
373		self.progress = QProgressBar()
374		self.progress.setRange(0, 0)
375		self.progress.hide()
376
377		if is_reg_expr:
378			self.pattern = QCheckBox("Regular Expression")
379		else:
380			self.pattern = QCheckBox("Pattern")
381		self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
382
383		self.next_button = QToolButton()
384		self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
385		self.next_button.released.connect(lambda: self.NextPrev(1))
386
387		self.prev_button = QToolButton()
388		self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
389		self.prev_button.released.connect(lambda: self.NextPrev(-1))
390
391		self.close_button = QToolButton()
392		self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
393		self.close_button.released.connect(self.Deactivate)
394
395		self.hbox = QHBoxLayout()
396		self.hbox.setContentsMargins(0, 0, 0, 0)
397
398		self.hbox.addWidget(label)
399		self.hbox.addWidget(self.textbox)
400		self.hbox.addWidget(self.progress)
401		self.hbox.addWidget(self.pattern)
402		self.hbox.addWidget(self.next_button)
403		self.hbox.addWidget(self.prev_button)
404		self.hbox.addWidget(self.close_button)
405
406		self.bar = QWidget()
407		self.bar.setLayout(self.hbox)
408		self.bar.hide()
409
410	def Widget(self):
411		return self.bar
412
413	def Activate(self):
414		self.bar.show()
415		self.textbox.lineEdit().selectAll()
416		self.textbox.setFocus()
417
418	def Deactivate(self):
419		self.bar.hide()
420
421	def Busy(self):
422		self.textbox.setEnabled(False)
423		self.pattern.hide()
424		self.next_button.hide()
425		self.prev_button.hide()
426		self.progress.show()
427
428	def Idle(self):
429		self.textbox.setEnabled(True)
430		self.progress.hide()
431		self.pattern.show()
432		self.next_button.show()
433		self.prev_button.show()
434
435	def Find(self, direction):
436		value = self.textbox.currentText()
437		pattern = self.pattern.isChecked()
438		self.last_value = value
439		self.last_pattern = pattern
440		self.finder.Find(value, direction, pattern, self.context)
441
442	def ValueChanged(self):
443		value = self.textbox.currentText()
444		pattern = self.pattern.isChecked()
445		index = self.textbox.currentIndex()
446		data = self.textbox.itemData(index)
447		# Store the pattern in the combo box to keep it with the text value
448		if data == None:
449			self.textbox.setItemData(index, pattern)
450		else:
451			self.pattern.setChecked(data)
452		self.Find(0)
453
454	def NextPrev(self, direction):
455		value = self.textbox.currentText()
456		pattern = self.pattern.isChecked()
457		if value != self.last_value:
458			index = self.textbox.findText(value)
459			# Allow for a button press before the value has been added to the combo box
460			if index < 0:
461				index = self.textbox.count()
462				self.textbox.addItem(value, pattern)
463				self.textbox.setCurrentIndex(index)
464				return
465			else:
466				self.textbox.setItemData(index, pattern)
467		elif pattern != self.last_pattern:
468			# Keep the pattern recorded in the combo box up to date
469			index = self.textbox.currentIndex()
470			self.textbox.setItemData(index, pattern)
471		self.Find(direction)
472
473	def NotFound(self):
474		QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
475
476# Context-sensitive call graph data model item base
477
478class CallGraphLevelItemBase(object):
479
480	def __init__(self, glb, params, row, parent_item):
481		self.glb = glb
482		self.params = params
483		self.row = row
484		self.parent_item = parent_item
485		self.query_done = False
486		self.child_count = 0
487		self.child_items = []
488		if parent_item:
489			self.level = parent_item.level + 1
490		else:
491			self.level = 0
492
493	def getChildItem(self, row):
494		return self.child_items[row]
495
496	def getParentItem(self):
497		return self.parent_item
498
499	def getRow(self):
500		return self.row
501
502	def childCount(self):
503		if not self.query_done:
504			self.Select()
505			if not self.child_count:
506				return -1
507		return self.child_count
508
509	def hasChildren(self):
510		if not self.query_done:
511			return True
512		return self.child_count > 0
513
514	def getData(self, column):
515		return self.data[column]
516
517# Context-sensitive call graph data model level 2+ item base
518
519class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
520
521	def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
522		super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
523		self.comm_id = comm_id
524		self.thread_id = thread_id
525		self.call_path_id = call_path_id
526		self.insn_cnt = insn_cnt
527		self.cyc_cnt = cyc_cnt
528		self.branch_count = branch_count
529		self.time = time
530
531	def Select(self):
532		self.query_done = True
533		query = QSqlQuery(self.glb.db)
534		if self.params.have_ipc:
535			ipc_str = ", SUM(insn_count), SUM(cyc_count)"
536		else:
537			ipc_str = ""
538		QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
539					" FROM calls"
540					" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
541					" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
542					" INNER JOIN dsos ON symbols.dso_id = dsos.id"
543					" WHERE parent_call_path_id = " + str(self.call_path_id) +
544					" AND comm_id = " + str(self.comm_id) +
545					" AND thread_id = " + str(self.thread_id) +
546					" GROUP BY call_path_id, name, short_name"
547					" ORDER BY call_path_id")
548		while query.next():
549			if self.params.have_ipc:
550				insn_cnt = int(query.value(5))
551				cyc_cnt = int(query.value(6))
552				branch_count = int(query.value(7))
553			else:
554				insn_cnt = 0
555				cyc_cnt = 0
556				branch_count = int(query.value(5))
557			child_item = CallGraphLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)
558			self.child_items.append(child_item)
559			self.child_count += 1
560
561# Context-sensitive call graph data model level three item
562
563class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
564
565	def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item):
566		super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
567		dso = dsoname(dso)
568		if self.params.have_ipc:
569			insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
570			cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
571			br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
572			ipc = CalcIPC(cyc_cnt, insn_cnt)
573			self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
574		else:
575			self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
576		self.dbid = call_path_id
577
578# Context-sensitive call graph data model level two item
579
580class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
581
582	def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
583		super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)
584		if self.params.have_ipc:
585			self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
586		else:
587			self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
588		self.dbid = thread_id
589
590	def Select(self):
591		super(CallGraphLevelTwoItem, self).Select()
592		for child_item in self.child_items:
593			self.time += child_item.time
594			self.insn_cnt += child_item.insn_cnt
595			self.cyc_cnt += child_item.cyc_cnt
596			self.branch_count += child_item.branch_count
597		for child_item in self.child_items:
598			child_item.data[4] = PercentToOneDP(child_item.time, self.time)
599			if self.params.have_ipc:
600				child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
601				child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
602				child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
603			else:
604				child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
605
606# Context-sensitive call graph data model level one item
607
608class CallGraphLevelOneItem(CallGraphLevelItemBase):
609
610	def __init__(self, glb, params, row, comm_id, comm, parent_item):
611		super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)
612		if self.params.have_ipc:
613			self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
614		else:
615			self.data = [comm, "", "", "", "", "", ""]
616		self.dbid = comm_id
617
618	def Select(self):
619		self.query_done = True
620		query = QSqlQuery(self.glb.db)
621		QueryExec(query, "SELECT thread_id, pid, tid"
622					" FROM comm_threads"
623					" INNER JOIN threads ON thread_id = threads.id"
624					" WHERE comm_id = " + str(self.dbid))
625		while query.next():
626			child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
627			self.child_items.append(child_item)
628			self.child_count += 1
629
630# Context-sensitive call graph data model root item
631
632class CallGraphRootItem(CallGraphLevelItemBase):
633
634	def __init__(self, glb, params):
635		super(CallGraphRootItem, self).__init__(glb, params, 0, None)
636		self.dbid = 0
637		self.query_done = True
638		if_has_calls = ""
639		if IsSelectable(glb.db, "comms", columns = "has_calls"):
640			if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
641		query = QSqlQuery(glb.db)
642		QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
643		while query.next():
644			if not query.value(0):
645				continue
646			child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
647			self.child_items.append(child_item)
648			self.child_count += 1
649
650# Call graph model parameters
651
652class CallGraphModelParams():
653
654	def __init__(self, glb, parent=None):
655		self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")
656
657# Context-sensitive call graph data model base
658
659class CallGraphModelBase(TreeModel):
660
661	def __init__(self, glb, parent=None):
662		super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)
663
664	def FindSelect(self, value, pattern, query):
665		if pattern:
666			# postgresql and sqlite pattern patching differences:
667			#   postgresql LIKE is case sensitive but sqlite LIKE is not
668			#   postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
669			#   postgresql supports ILIKE which is case insensitive
670			#   sqlite supports GLOB (text only) which uses * and ? and is case sensitive
671			if not self.glb.dbref.is_sqlite3:
672				# Escape % and _
673				s = value.replace("%", "\%")
674				s = s.replace("_", "\_")
675				# Translate * and ? into SQL LIKE pattern characters % and _
676				trans = string.maketrans("*?", "%_")
677				match = " LIKE '" + str(s).translate(trans) + "'"
678			else:
679				match = " GLOB '" + str(value) + "'"
680		else:
681			match = " = '" + str(value) + "'"
682		self.DoFindSelect(query, match)
683
684	def Found(self, query, found):
685		if found:
686			return self.FindPath(query)
687		return []
688
689	def FindValue(self, value, pattern, query, last_value, last_pattern):
690		if last_value == value and pattern == last_pattern:
691			found = query.first()
692		else:
693			self.FindSelect(value, pattern, query)
694			found = query.next()
695		return self.Found(query, found)
696
697	def FindNext(self, query):
698		found = query.next()
699		if not found:
700			found = query.first()
701		return self.Found(query, found)
702
703	def FindPrev(self, query):
704		found = query.previous()
705		if not found:
706			found = query.last()
707		return self.Found(query, found)
708
709	def FindThread(self, c):
710		if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
711			ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
712		elif c.direction > 0:
713			ids = self.FindNext(c.query)
714		else:
715			ids = self.FindPrev(c.query)
716		return (True, ids)
717
718	def Find(self, value, direction, pattern, context, callback):
719		class Context():
720			def __init__(self, *x):
721				self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
722			def Update(self, *x):
723				self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
724		if len(context):
725			context[0].Update(value, direction, pattern)
726		else:
727			context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
728		# Use a thread so the UI is not blocked during the SELECT
729		thread = Thread(self.FindThread, context[0])
730		thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
731		thread.start()
732
733	def FindDone(self, thread, callback, ids):
734		callback(ids)
735
736# Context-sensitive call graph data model
737
738class CallGraphModel(CallGraphModelBase):
739
740	def __init__(self, glb, parent=None):
741		super(CallGraphModel, self).__init__(glb, parent)
742
743	def GetRoot(self):
744		return CallGraphRootItem(self.glb, self.params)
745
746	def columnCount(self, parent=None):
747		if self.params.have_ipc:
748			return 12
749		else:
750			return 7
751
752	def columnHeader(self, column):
753		if self.params.have_ipc:
754			headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
755		else:
756			headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
757		return headers[column]
758
759	def columnAlignment(self, column):
760		if self.params.have_ipc:
761			alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
762		else:
763			alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
764		return alignment[column]
765
766	def DoFindSelect(self, query, match):
767		QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
768						" FROM calls"
769						" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
770						" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
771						" WHERE symbols.name" + match +
772						" GROUP BY comm_id, thread_id, call_path_id"
773						" ORDER BY comm_id, thread_id, call_path_id")
774
775	def FindPath(self, query):
776		# Turn the query result into a list of ids that the tree view can walk
777		# to open the tree at the right place.
778		ids = []
779		parent_id = query.value(0)
780		while parent_id:
781			ids.insert(0, parent_id)
782			q2 = QSqlQuery(self.glb.db)
783			QueryExec(q2, "SELECT parent_id"
784					" FROM call_paths"
785					" WHERE id = " + str(parent_id))
786			if not q2.next():
787				break
788			parent_id = q2.value(0)
789		# The call path root is not used
790		if ids[0] == 1:
791			del ids[0]
792		ids.insert(0, query.value(2))
793		ids.insert(0, query.value(1))
794		return ids
795
796# Call tree data model level 2+ item base
797
798class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
799
800	def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
801		super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
802		self.comm_id = comm_id
803		self.thread_id = thread_id
804		self.calls_id = calls_id
805		self.call_time = call_time
806		self.time = time
807		self.insn_cnt = insn_cnt
808		self.cyc_cnt = cyc_cnt
809		self.branch_count = branch_count
810
811	def Select(self):
812		self.query_done = True
813		if self.calls_id == 0:
814			comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
815		else:
816			comm_thread = ""
817		if self.params.have_ipc:
818			ipc_str = ", insn_count, cyc_count"
819		else:
820			ipc_str = ""
821		query = QSqlQuery(self.glb.db)
822		QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"
823					" FROM calls"
824					" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
825					" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
826					" INNER JOIN dsos ON symbols.dso_id = dsos.id"
827					" WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
828					" ORDER BY call_time, calls.id")
829		while query.next():
830			if self.params.have_ipc:
831				insn_cnt = int(query.value(5))
832				cyc_cnt = int(query.value(6))
833				branch_count = int(query.value(7))
834			else:
835				insn_cnt = 0
836				cyc_cnt = 0
837				branch_count = int(query.value(5))
838			child_item = CallTreeLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)
839			self.child_items.append(child_item)
840			self.child_count += 1
841
842# Call tree data model level three item
843
844class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
845
846	def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
847		super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item)
848		dso = dsoname(dso)
849		if self.params.have_ipc:
850			insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
851			cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
852			br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
853			ipc = CalcIPC(cyc_cnt, insn_cnt)
854			self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
855		else:
856			self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
857		self.dbid = calls_id
858
859# Call tree data model level two item
860
861class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
862
863	def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
864		super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item)
865		if self.params.have_ipc:
866			self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
867		else:
868			self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
869		self.dbid = thread_id
870
871	def Select(self):
872		super(CallTreeLevelTwoItem, self).Select()
873		for child_item in self.child_items:
874			self.time += child_item.time
875			self.insn_cnt += child_item.insn_cnt
876			self.cyc_cnt += child_item.cyc_cnt
877			self.branch_count += child_item.branch_count
878		for child_item in self.child_items:
879			child_item.data[4] = PercentToOneDP(child_item.time, self.time)
880			if self.params.have_ipc:
881				child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
882				child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
883				child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
884			else:
885				child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
886
887# Call tree data model level one item
888
889class CallTreeLevelOneItem(CallGraphLevelItemBase):
890
891	def __init__(self, glb, params, row, comm_id, comm, parent_item):
892		super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
893		if self.params.have_ipc:
894			self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
895		else:
896			self.data = [comm, "", "", "", "", "", ""]
897		self.dbid = comm_id
898
899	def Select(self):
900		self.query_done = True
901		query = QSqlQuery(self.glb.db)
902		QueryExec(query, "SELECT thread_id, pid, tid"
903					" FROM comm_threads"
904					" INNER JOIN threads ON thread_id = threads.id"
905					" WHERE comm_id = " + str(self.dbid))
906		while query.next():
907			child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
908			self.child_items.append(child_item)
909			self.child_count += 1
910
911# Call tree data model root item
912
913class CallTreeRootItem(CallGraphLevelItemBase):
914
915	def __init__(self, glb, params):
916		super(CallTreeRootItem, self).__init__(glb, params, 0, None)
917		self.dbid = 0
918		self.query_done = True
919		if_has_calls = ""
920		if IsSelectable(glb.db, "comms", columns = "has_calls"):
921			if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
922		query = QSqlQuery(glb.db)
923		QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
924		while query.next():
925			if not query.value(0):
926				continue
927			child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
928			self.child_items.append(child_item)
929			self.child_count += 1
930
931# Call Tree data model
932
933class CallTreeModel(CallGraphModelBase):
934
935	def __init__(self, glb, parent=None):
936		super(CallTreeModel, self).__init__(glb, parent)
937
938	def GetRoot(self):
939		return CallTreeRootItem(self.glb, self.params)
940
941	def columnCount(self, parent=None):
942		if self.params.have_ipc:
943			return 12
944		else:
945			return 7
946
947	def columnHeader(self, column):
948		if self.params.have_ipc:
949			headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
950		else:
951			headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
952		return headers[column]
953
954	def columnAlignment(self, column):
955		if self.params.have_ipc:
956			alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
957		else:
958			alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
959		return alignment[column]
960
961	def DoFindSelect(self, query, match):
962		QueryExec(query, "SELECT calls.id, comm_id, thread_id"
963						" FROM calls"
964						" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
965						" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
966						" WHERE symbols.name" + match +
967						" ORDER BY comm_id, thread_id, call_time, calls.id")
968
969	def FindPath(self, query):
970		# Turn the query result into a list of ids that the tree view can walk
971		# to open the tree at the right place.
972		ids = []
973		parent_id = query.value(0)
974		while parent_id:
975			ids.insert(0, parent_id)
976			q2 = QSqlQuery(self.glb.db)
977			QueryExec(q2, "SELECT parent_id"
978					" FROM calls"
979					" WHERE id = " + str(parent_id))
980			if not q2.next():
981				break
982			parent_id = q2.value(0)
983		ids.insert(0, query.value(2))
984		ids.insert(0, query.value(1))
985		return ids
986
987# Vertical layout
988
989class HBoxLayout(QHBoxLayout):
990
991	def __init__(self, *children):
992		super(HBoxLayout, self).__init__()
993
994		self.layout().setContentsMargins(0, 0, 0, 0)
995		for child in children:
996			if child.isWidgetType():
997				self.layout().addWidget(child)
998			else:
999				self.layout().addLayout(child)
1000
1001# Horizontal layout
1002
1003class VBoxLayout(QVBoxLayout):
1004
1005	def __init__(self, *children):
1006		super(VBoxLayout, self).__init__()
1007
1008		self.layout().setContentsMargins(0, 0, 0, 0)
1009		for child in children:
1010			if child.isWidgetType():
1011				self.layout().addWidget(child)
1012			else:
1013				self.layout().addLayout(child)
1014
1015# Vertical layout widget
1016
1017class VBox():
1018
1019	def __init__(self, *children):
1020		self.vbox = QWidget()
1021		self.vbox.setLayout(VBoxLayout(*children))
1022
1023	def Widget(self):
1024		return self.vbox
1025
1026# Tree window base
1027
1028class TreeWindowBase(QMdiSubWindow):
1029
1030	def __init__(self, parent=None):
1031		super(TreeWindowBase, self).__init__(parent)
1032
1033		self.model = None
1034		self.find_bar = None
1035
1036		self.view = QTreeView()
1037		self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1038		self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1039
1040		self.context_menu = TreeContextMenu(self.view)
1041
1042	def DisplayFound(self, ids):
1043		if not len(ids):
1044			return False
1045		parent = QModelIndex()
1046		for dbid in ids:
1047			found = False
1048			n = self.model.rowCount(parent)
1049			for row in xrange(n):
1050				child = self.model.index(row, 0, parent)
1051				if child.internalPointer().dbid == dbid:
1052					found = True
1053					self.view.setCurrentIndex(child)
1054					parent = child
1055					break
1056			if not found:
1057				break
1058		return found
1059
1060	def Find(self, value, direction, pattern, context):
1061		self.view.setFocus()
1062		self.find_bar.Busy()
1063		self.model.Find(value, direction, pattern, context, self.FindDone)
1064
1065	def FindDone(self, ids):
1066		found = True
1067		if not self.DisplayFound(ids):
1068			found = False
1069		self.find_bar.Idle()
1070		if not found:
1071			self.find_bar.NotFound()
1072
1073
1074# Context-sensitive call graph window
1075
1076class CallGraphWindow(TreeWindowBase):
1077
1078	def __init__(self, glb, parent=None):
1079		super(CallGraphWindow, self).__init__(parent)
1080
1081		self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
1082
1083		self.view.setModel(self.model)
1084
1085		for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
1086			self.view.setColumnWidth(c, w)
1087
1088		self.find_bar = FindBar(self, self)
1089
1090		self.vbox = VBox(self.view, self.find_bar.Widget())
1091
1092		self.setWidget(self.vbox.Widget())
1093
1094		AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
1095
1096# Call tree window
1097
1098class CallTreeWindow(TreeWindowBase):
1099
1100	def __init__(self, glb, parent=None, thread_at_time=None):
1101		super(CallTreeWindow, self).__init__(parent)
1102
1103		self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
1104
1105		self.view.setModel(self.model)
1106
1107		for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
1108			self.view.setColumnWidth(c, w)
1109
1110		self.find_bar = FindBar(self, self)
1111
1112		self.vbox = VBox(self.view, self.find_bar.Widget())
1113
1114		self.setWidget(self.vbox.Widget())
1115
1116		AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
1117
1118		if thread_at_time:
1119			self.DisplayThreadAtTime(*thread_at_time)
1120
1121	def DisplayThreadAtTime(self, comm_id, thread_id, time):
1122		parent = QModelIndex()
1123		for dbid in (comm_id, thread_id):
1124			found = False
1125			n = self.model.rowCount(parent)
1126			for row in xrange(n):
1127				child = self.model.index(row, 0, parent)
1128				if child.internalPointer().dbid == dbid:
1129					found = True
1130					self.view.setCurrentIndex(child)
1131					parent = child
1132					break
1133			if not found:
1134				return
1135		found = False
1136		while True:
1137			n = self.model.rowCount(parent)
1138			if not n:
1139				return
1140			last_child = None
1141			for row in xrange(n):
1142				child = self.model.index(row, 0, parent)
1143				child_call_time = child.internalPointer().call_time
1144				if child_call_time < time:
1145					last_child = child
1146				elif child_call_time == time:
1147					self.view.setCurrentIndex(child)
1148					return
1149				elif child_call_time > time:
1150					break
1151			if not last_child:
1152				if not found:
1153					child = self.model.index(0, 0, parent)
1154					self.view.setCurrentIndex(child)
1155				return
1156			found = True
1157			self.view.setCurrentIndex(last_child)
1158			parent = last_child
1159
1160# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
1161
1162def ExecComm(db, thread_id, time):
1163	query = QSqlQuery(db)
1164	QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
1165				" FROM comm_threads"
1166				" INNER JOIN comms ON comms.id = comm_threads.comm_id"
1167				" WHERE comm_threads.thread_id = " + str(thread_id) +
1168				" ORDER BY comms.c_time, comms.id")
1169	first = None
1170	last = None
1171	while query.next():
1172		if first is None:
1173			first = query.value(0)
1174		if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
1175			last = query.value(0)
1176	if not(last is None):
1177		return last
1178	return first
1179
1180# Container for (x, y) data
1181
1182class XY():
1183	def __init__(self, x=0, y=0):
1184		self.x = x
1185		self.y = y
1186
1187	def __str__(self):
1188		return "XY({}, {})".format(str(self.x), str(self.y))
1189
1190# Container for sub-range data
1191
1192class Subrange():
1193	def __init__(self, lo=0, hi=0):
1194		self.lo = lo
1195		self.hi = hi
1196
1197	def __str__(self):
1198		return "Subrange({}, {})".format(str(self.lo), str(self.hi))
1199
1200# Graph data region base class
1201
1202class GraphDataRegion(object):
1203
1204	def __init__(self, key, title = "", ordinal = ""):
1205		self.key = key
1206		self.title = title
1207		self.ordinal = ordinal
1208
1209# Function to sort GraphDataRegion
1210
1211def GraphDataRegionOrdinal(data_region):
1212	return data_region.ordinal
1213
1214# Attributes for a graph region
1215
1216class GraphRegionAttribute():
1217
1218	def __init__(self, colour):
1219		self.colour = colour
1220
1221# Switch graph data region represents a task
1222
1223class SwitchGraphDataRegion(GraphDataRegion):
1224
1225	def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
1226		super(SwitchGraphDataRegion, self).__init__(key)
1227
1228		self.title = str(pid) + " / " + str(tid) + " " + comm
1229		# Order graph legend within exec comm by pid / tid / time
1230		self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
1231		self.exec_comm_id = exec_comm_id
1232		self.pid = pid
1233		self.tid = tid
1234		self.comm = comm
1235		self.thread_id = thread_id
1236		self.comm_id = comm_id
1237
1238# Graph data point
1239
1240class GraphDataPoint():
1241
1242	def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
1243		self.data = data
1244		self.index = index
1245		self.x = x
1246		self.y = y
1247		self.altx = altx
1248		self.alty = alty
1249		self.hregion = hregion
1250		self.vregion = vregion
1251
1252# Graph data (single graph) base class
1253
1254class GraphData(object):
1255
1256	def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
1257		self.collection = collection
1258		self.points = []
1259		self.xbase = xbase
1260		self.ybase = ybase
1261		self.title = ""
1262
1263	def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
1264		index = len(self.points)
1265
1266		x = float(Decimal(x) - self.xbase)
1267		y = float(Decimal(y) - self.ybase)
1268
1269		self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
1270
1271	def XToData(self, x):
1272		return Decimal(x) + self.xbase
1273
1274	def YToData(self, y):
1275		return Decimal(y) + self.ybase
1276
1277# Switch graph data (for one CPU)
1278
1279class SwitchGraphData(GraphData):
1280
1281	def __init__(self, db, collection, cpu, xbase):
1282		super(SwitchGraphData, self).__init__(collection, xbase)
1283
1284		self.cpu = cpu
1285		self.title = "CPU " + str(cpu)
1286		self.SelectSwitches(db)
1287
1288	def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
1289		query = QSqlQuery(db)
1290		QueryExec(query, "SELECT id, c_time"
1291					" FROM comms"
1292					" WHERE c_thread_id = " + str(thread_id) +
1293					"   AND exec_flag = " + self.collection.glb.dbref.TRUE +
1294					"   AND c_time >= " + str(start_time) +
1295					"   AND c_time <= " + str(end_time) +
1296					" ORDER BY c_time, id")
1297		while query.next():
1298			comm_id = query.value(0)
1299			if comm_id == last_comm_id:
1300				continue
1301			time = query.value(1)
1302			hregion = self.HRegion(db, thread_id, comm_id, time)
1303			self.AddPoint(time, 1000, None, None, hregion)
1304
1305	def SelectSwitches(self, db):
1306		last_time = None
1307		last_comm_id = None
1308		last_thread_id = None
1309		query = QSqlQuery(db)
1310		QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
1311					" FROM context_switches"
1312					" WHERE machine_id = " + str(self.collection.machine_id) +
1313					"   AND cpu = " + str(self.cpu) +
1314					" ORDER BY time, id")
1315		while query.next():
1316			flags = int(query.value(5))
1317			if flags & 1:
1318				# Schedule-out: detect and add exec's
1319				if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
1320					self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
1321				continue
1322			# Schedule-in: add data point
1323			if len(self.points) == 0:
1324				start_time = self.collection.glb.StartTime(self.collection.machine_id)
1325				hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
1326				self.AddPoint(start_time, 1000, None, None, hregion)
1327			time = query.value(0)
1328			comm_id = query.value(4)
1329			thread_id = query.value(2)
1330			hregion = self.HRegion(db, thread_id, comm_id, time)
1331			self.AddPoint(time, 1000, None, None, hregion)
1332			last_time = time
1333			last_comm_id = comm_id
1334			last_thread_id = thread_id
1335
1336	def NewHRegion(self, db, key, thread_id, comm_id, time):
1337		exec_comm_id = ExecComm(db, thread_id, time)
1338		query = QSqlQuery(db)
1339		QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
1340		if query.next():
1341			pid = query.value(0)
1342			tid = query.value(1)
1343		else:
1344			pid = -1
1345			tid = -1
1346		query = QSqlQuery(db)
1347		QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
1348		if query.next():
1349			comm = query.value(0)
1350		else:
1351			comm = ""
1352		return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
1353
1354	def HRegion(self, db, thread_id, comm_id, time):
1355		key = str(thread_id) + ":" + str(comm_id)
1356		hregion = self.collection.LookupHRegion(key)
1357		if hregion is None:
1358			hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
1359			self.collection.AddHRegion(key, hregion)
1360		return hregion
1361
1362# Graph data collection (multiple related graphs) base class
1363
1364class GraphDataCollection(object):
1365
1366	def __init__(self, glb):
1367		self.glb = glb
1368		self.data = []
1369		self.hregions = {}
1370		self.xrangelo = None
1371		self.xrangehi = None
1372		self.yrangelo = None
1373		self.yrangehi = None
1374		self.dp = XY(0, 0)
1375
1376	def AddGraphData(self, data):
1377		self.data.append(data)
1378
1379	def LookupHRegion(self, key):
1380		if key in self.hregions:
1381			return self.hregions[key]
1382		return None
1383
1384	def AddHRegion(self, key, hregion):
1385		self.hregions[key] = hregion
1386
1387# Switch graph data collection (SwitchGraphData for each CPU)
1388
1389class SwitchGraphDataCollection(GraphDataCollection):
1390
1391	def __init__(self, glb, db, machine_id):
1392		super(SwitchGraphDataCollection, self).__init__(glb)
1393
1394		self.machine_id = machine_id
1395		self.cpus = self.SelectCPUs(db)
1396
1397		self.xrangelo = glb.StartTime(machine_id)
1398		self.xrangehi = glb.FinishTime(machine_id)
1399
1400		self.yrangelo = Decimal(0)
1401		self.yrangehi = Decimal(1000)
1402
1403		for cpu in self.cpus:
1404			self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
1405
1406	def SelectCPUs(self, db):
1407		cpus = []
1408		query = QSqlQuery(db)
1409		QueryExec(query, "SELECT DISTINCT cpu"
1410					" FROM context_switches"
1411					" WHERE machine_id = " + str(self.machine_id))
1412		while query.next():
1413			cpus.append(int(query.value(0)))
1414		return sorted(cpus)
1415
1416# Switch graph data graphics item displays the graphed data
1417
1418class SwitchGraphDataGraphicsItem(QGraphicsItem):
1419
1420	def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
1421		super(SwitchGraphDataGraphicsItem, self).__init__(parent)
1422
1423		self.data = data
1424		self.graph_width = graph_width
1425		self.graph_height = graph_height
1426		self.attrs = attrs
1427		self.event_handler = event_handler
1428		self.setAcceptHoverEvents(True)
1429
1430	def boundingRect(self):
1431		return QRectF(0, 0, self.graph_width, self.graph_height)
1432
1433	def PaintPoint(self, painter, last, x):
1434		if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
1435			if last.x < self.attrs.subrange.x.lo:
1436				x0 = self.attrs.subrange.x.lo
1437			else:
1438				x0 = last.x
1439			if x > self.attrs.subrange.x.hi:
1440				x1 = self.attrs.subrange.x.hi
1441			else:
1442				x1 = x - 1
1443			x0 = self.attrs.XToPixel(x0)
1444			x1 = self.attrs.XToPixel(x1)
1445
1446			y0 = self.attrs.YToPixel(last.y)
1447
1448			colour = self.attrs.region_attributes[last.hregion.key].colour
1449
1450			width = x1 - x0 + 1
1451			if width < 2:
1452				painter.setPen(colour)
1453				painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
1454			else:
1455				painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
1456
1457	def paint(self, painter, option, widget):
1458		last = None
1459		for point in self.data.points:
1460			self.PaintPoint(painter, last, point.x)
1461			if point.x > self.attrs.subrange.x.hi:
1462				break;
1463			last = point
1464		self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
1465
1466	def BinarySearchPoint(self, target):
1467		lower_pos = 0
1468		higher_pos = len(self.data.points)
1469		while True:
1470			pos = int((lower_pos + higher_pos) / 2)
1471			val = self.data.points[pos].x
1472			if target >= val:
1473				lower_pos = pos
1474			else:
1475				higher_pos = pos
1476			if higher_pos <= lower_pos + 1:
1477				return lower_pos
1478
1479	def XPixelToData(self, x):
1480		x = self.attrs.PixelToX(x)
1481		if x < self.data.points[0].x:
1482			x = 0
1483			pos = 0
1484			low = True
1485		else:
1486			pos = self.BinarySearchPoint(x)
1487			low = False
1488		return (low, pos, self.data.XToData(x))
1489
1490	def EventToData(self, event):
1491		no_data = (None,) * 4
1492		if len(self.data.points) < 1:
1493			return no_data
1494		x = event.pos().x()
1495		if x < 0:
1496			return no_data
1497		low0, pos0, time_from = self.XPixelToData(x)
1498		low1, pos1, time_to = self.XPixelToData(x + 1)
1499		hregions = set()
1500		hregion_times = []
1501		if not low1:
1502			for i in xrange(pos0, pos1 + 1):
1503				hregion = self.data.points[i].hregion
1504				hregions.add(hregion)
1505				if i == pos0:
1506					time = time_from
1507				else:
1508					time = self.data.XToData(self.data.points[i].x)
1509				hregion_times.append((hregion, time))
1510		return (time_from, time_to, hregions, hregion_times)
1511
1512	def hoverMoveEvent(self, event):
1513		time_from, time_to, hregions, hregion_times = self.EventToData(event)
1514		if time_from is not None:
1515			self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
1516
1517	def hoverLeaveEvent(self, event):
1518		self.event_handler.NoPointEvent()
1519
1520	def mousePressEvent(self, event):
1521		if event.button() != Qt.RightButton:
1522			super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
1523			return
1524		time_from, time_to, hregions, hregion_times = self.EventToData(event)
1525		if hregion_times:
1526			self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
1527
1528# X-axis graphics item
1529
1530class XAxisGraphicsItem(QGraphicsItem):
1531
1532	def __init__(self, width, parent=None):
1533		super(XAxisGraphicsItem, self).__init__(parent)
1534
1535		self.width = width
1536		self.max_mark_sz = 4
1537		self.height = self.max_mark_sz + 1
1538
1539	def boundingRect(self):
1540		return QRectF(0, 0, self.width, self.height)
1541
1542	def Step(self):
1543		attrs = self.parentItem().attrs
1544		subrange = attrs.subrange.x
1545		t = subrange.hi - subrange.lo
1546		s = (3.0 * t) / self.width
1547		n = 1.0
1548		while s > n:
1549			n = n * 10.0
1550		return n
1551
1552	def PaintMarks(self, painter, at_y, lo, hi, step, i):
1553		attrs = self.parentItem().attrs
1554		x = lo
1555		while x <= hi:
1556			xp = attrs.XToPixel(x)
1557			if i % 10:
1558				if i % 5:
1559					sz = 1
1560				else:
1561					sz = 2
1562			else:
1563				sz = self.max_mark_sz
1564				i = 0
1565			painter.drawLine(xp, at_y, xp, at_y + sz)
1566			x += step
1567			i += 1
1568
1569	def paint(self, painter, option, widget):
1570		# Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
1571		painter.drawLine(0, 0, self.width - 1, 0)
1572		n = self.Step()
1573		attrs = self.parentItem().attrs
1574		subrange = attrs.subrange.x
1575		if subrange.lo:
1576			x_offset = n - (subrange.lo % n)
1577		else:
1578			x_offset = 0.0
1579		x = subrange.lo + x_offset
1580		i = (x / n) % 10
1581		self.PaintMarks(painter, 0, x, subrange.hi, n, i)
1582
1583	def ScaleDimensions(self):
1584		n = self.Step()
1585		attrs = self.parentItem().attrs
1586		lo = attrs.subrange.x.lo
1587		hi = (n * 10.0) + lo
1588		width = attrs.XToPixel(hi)
1589		if width > 500:
1590			width = 0
1591		return (n, lo, hi, width)
1592
1593	def PaintScale(self, painter, at_x, at_y):
1594		n, lo, hi, width = self.ScaleDimensions()
1595		if not width:
1596			return
1597		painter.drawLine(at_x, at_y, at_x + width, at_y)
1598		self.PaintMarks(painter, at_y, lo, hi, n, 0)
1599
1600	def ScaleWidth(self):
1601		n, lo, hi, width = self.ScaleDimensions()
1602		return width
1603
1604	def ScaleHeight(self):
1605		return self.height
1606
1607	def ScaleUnit(self):
1608		return self.Step() * 10
1609
1610# Scale graphics item base class
1611
1612class ScaleGraphicsItem(QGraphicsItem):
1613
1614	def __init__(self, axis, parent=None):
1615		super(ScaleGraphicsItem, self).__init__(parent)
1616		self.axis = axis
1617
1618	def boundingRect(self):
1619		scale_width = self.axis.ScaleWidth()
1620		if not scale_width:
1621			return QRectF()
1622		return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
1623
1624	def paint(self, painter, option, widget):
1625		scale_width = self.axis.ScaleWidth()
1626		if not scale_width:
1627			return
1628		self.axis.PaintScale(painter, 0, 5)
1629		x = scale_width + 4
1630		painter.drawText(QPointF(x, 10), self.Text())
1631
1632	def Unit(self):
1633		return self.axis.ScaleUnit()
1634
1635	def Text(self):
1636		return ""
1637
1638# Switch graph scale graphics item
1639
1640class SwitchScaleGraphicsItem(ScaleGraphicsItem):
1641
1642	def __init__(self, axis, parent=None):
1643		super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
1644
1645	def Text(self):
1646		unit = self.Unit()
1647		if unit >= 1000000000:
1648			unit = int(unit / 1000000000)
1649			us = "s"
1650		elif unit >= 1000000:
1651			unit = int(unit / 1000000)
1652			us = "ms"
1653		elif unit >= 1000:
1654			unit = int(unit / 1000)
1655			us = "us"
1656		else:
1657			unit = int(unit)
1658			us = "ns"
1659		return " = " + str(unit) + " " + us
1660
1661# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
1662
1663class SwitchGraphGraphicsItem(QGraphicsItem):
1664
1665	def __init__(self, collection, data, attrs, event_handler, first, parent=None):
1666		super(SwitchGraphGraphicsItem, self).__init__(parent)
1667		self.collection = collection
1668		self.data = data
1669		self.attrs = attrs
1670		self.event_handler = event_handler
1671
1672		margin = 20
1673		title_width = 50
1674
1675		self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
1676
1677		self.title_graphics.setPos(margin, margin)
1678		graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
1679		graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
1680
1681		self.graph_origin_x = margin + title_width + margin
1682		self.graph_origin_y = graph_height + margin
1683
1684		x_axis_size = 1
1685		y_axis_size = 1
1686		self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
1687
1688		self.x_axis = XAxisGraphicsItem(graph_width, self)
1689		self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
1690
1691		if first:
1692			self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
1693			self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
1694
1695		self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
1696
1697		self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
1698		self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
1699
1700		self.width = self.graph_origin_x + graph_width + margin
1701		self.height = self.graph_origin_y + margin
1702
1703		self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
1704		self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
1705
1706		if parent and 'EnableRubberBand' in dir(parent):
1707			parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
1708
1709	def boundingRect(self):
1710		return QRectF(0, 0, self.width, self.height)
1711
1712	def paint(self, painter, option, widget):
1713		pass
1714
1715	def RBXToPixel(self, x):
1716		return self.attrs.PixelToX(x - self.graph_origin_x)
1717
1718	def RBXRangeToPixel(self, x0, x1):
1719		return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
1720
1721	def RBPixelToTime(self, x):
1722		if x < self.data.points[0].x:
1723			return self.data.XToData(0)
1724		return self.data.XToData(x)
1725
1726	def RBEventTimes(self, x0, x1):
1727		x0, x1 = self.RBXRangeToPixel(x0, x1)
1728		time_from = self.RBPixelToTime(x0)
1729		time_to = self.RBPixelToTime(x1)
1730		return (time_from, time_to)
1731
1732	def RBEvent(self, x0, x1):
1733		time_from, time_to = self.RBEventTimes(x0, x1)
1734		self.event_handler.RangeEvent(time_from, time_to)
1735
1736	def RBMoveEvent(self, x0, x1):
1737		if x1 < x0:
1738			x0, x1 = x1, x0
1739		self.RBEvent(x0, x1)
1740
1741	def RBReleaseEvent(self, x0, x1, selection_state):
1742		if x1 < x0:
1743			x0, x1 = x1, x0
1744		x0, x1 = self.RBXRangeToPixel(x0, x1)
1745		self.event_handler.SelectEvent(x0, x1, selection_state)
1746
1747# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
1748
1749class VerticalBracketGraphicsItem(QGraphicsItem):
1750
1751	def __init__(self, parent=None):
1752		super(VerticalBracketGraphicsItem, self).__init__(parent)
1753
1754		self.width = 0
1755		self.height = 0
1756		self.hide()
1757
1758	def SetSize(self, width, height):
1759		self.width = width + 1
1760		self.height = height + 1
1761
1762	def boundingRect(self):
1763		return QRectF(0, 0, self.width, self.height)
1764
1765	def paint(self, painter, option, widget):
1766		colour = QColor(255, 255, 0, 32)
1767		painter.fillRect(0, 0, self.width, self.height, colour)
1768		x1 = self.width - 1
1769		y1 = self.height - 1
1770		painter.drawLine(0, 0, x1, 0)
1771		painter.drawLine(0, 0, 0, 3)
1772		painter.drawLine(x1, 0, x1, 3)
1773		painter.drawLine(0, y1, x1, y1)
1774		painter.drawLine(0, y1, 0, y1 - 3)
1775		painter.drawLine(x1, y1, x1, y1 - 3)
1776
1777# Graphics item to contain graphs arranged vertically
1778
1779class VertcalGraphSetGraphicsItem(QGraphicsItem):
1780
1781	def __init__(self, collection, attrs, event_handler, child_class, parent=None):
1782		super(VertcalGraphSetGraphicsItem, self).__init__(parent)
1783
1784		self.collection = collection
1785
1786		self.top = 10
1787
1788		self.width = 0
1789		self.height = self.top
1790
1791		self.rubber_band = None
1792		self.rb_enabled = False
1793
1794		first = True
1795		for data in collection.data:
1796			child = child_class(collection, data, attrs, event_handler, first, self)
1797			child.setPos(0, self.height + 1)
1798			rect = child.boundingRect()
1799			if rect.right() > self.width:
1800				self.width = rect.right()
1801			self.height = self.height + rect.bottom() + 1
1802			first = False
1803
1804		self.bracket = VerticalBracketGraphicsItem(self)
1805
1806	def EnableRubberBand(self, xlo, xhi, rb_event_handler):
1807		if self.rb_enabled:
1808			return
1809		self.rb_enabled = True
1810		self.rb_in_view = False
1811		self.setAcceptedMouseButtons(Qt.LeftButton)
1812		self.rb_xlo = xlo
1813		self.rb_xhi = xhi
1814		self.rb_event_handler = rb_event_handler
1815		self.mousePressEvent = self.MousePressEvent
1816		self.mouseMoveEvent = self.MouseMoveEvent
1817		self.mouseReleaseEvent = self.MouseReleaseEvent
1818
1819	def boundingRect(self):
1820		return QRectF(0, 0, self.width, self.height)
1821
1822	def paint(self, painter, option, widget):
1823		pass
1824
1825	def RubberBandParent(self):
1826		scene = self.scene()
1827		view = scene.views()[0]
1828		viewport = view.viewport()
1829		return viewport
1830
1831	def RubberBandSetGeometry(self, rect):
1832		scene_rectf = self.mapRectToScene(QRectF(rect))
1833		scene = self.scene()
1834		view = scene.views()[0]
1835		poly = view.mapFromScene(scene_rectf)
1836		self.rubber_band.setGeometry(poly.boundingRect())
1837
1838	def SetSelection(self, selection_state):
1839		if self.rubber_band:
1840			if selection_state:
1841				self.RubberBandSetGeometry(selection_state)
1842				self.rubber_band.show()
1843			else:
1844				self.rubber_band.hide()
1845
1846	def SetBracket(self, rect):
1847		if rect:
1848			x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
1849			self.bracket.setPos(x, y)
1850			self.bracket.SetSize(width, height)
1851			self.bracket.show()
1852		else:
1853			self.bracket.hide()
1854
1855	def RubberBandX(self, event):
1856		x = event.pos().toPoint().x()
1857		if x < self.rb_xlo:
1858			x = self.rb_xlo
1859		elif x > self.rb_xhi:
1860			x = self.rb_xhi
1861		else:
1862			self.rb_in_view = True
1863		return x
1864
1865	def RubberBandRect(self, x):
1866		if self.rb_origin.x() <= x:
1867			width = x - self.rb_origin.x()
1868			rect = QRect(self.rb_origin, QSize(width, self.height))
1869		else:
1870			width = self.rb_origin.x() - x
1871			top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
1872			rect = QRect(top_left, QSize(width, self.height))
1873		return rect
1874
1875	def MousePressEvent(self, event):
1876		self.rb_in_view = False
1877		x = self.RubberBandX(event)
1878		self.rb_origin = QPoint(x, self.top)
1879		if self.rubber_band is None:
1880			self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
1881		self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
1882		if self.rb_in_view:
1883			self.rubber_band.show()
1884			self.rb_event_handler.RBMoveEvent(x, x)
1885		else:
1886			self.rubber_band.hide()
1887
1888	def MouseMoveEvent(self, event):
1889		x = self.RubberBandX(event)
1890		rect = self.RubberBandRect(x)
1891		self.RubberBandSetGeometry(rect)
1892		if self.rb_in_view:
1893			self.rubber_band.show()
1894			self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
1895
1896	def MouseReleaseEvent(self, event):
1897		x = self.RubberBandX(event)
1898		if self.rb_in_view:
1899			selection_state = self.RubberBandRect(x)
1900		else:
1901			selection_state = None
1902		self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
1903
1904# Switch graph legend data model
1905
1906class SwitchGraphLegendModel(QAbstractTableModel):
1907
1908	def __init__(self, collection, region_attributes, parent=None):
1909		super(SwitchGraphLegendModel, self).__init__(parent)
1910
1911		self.region_attributes = region_attributes
1912
1913		self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
1914		self.child_count = len(self.child_items)
1915
1916		self.highlight_set = set()
1917
1918		self.column_headers = ("pid", "tid", "comm")
1919
1920	def rowCount(self, parent):
1921		return self.child_count
1922
1923	def headerData(self, section, orientation, role):
1924		if role != Qt.DisplayRole:
1925			return None
1926		if orientation != Qt.Horizontal:
1927			return None
1928		return self.columnHeader(section)
1929
1930	def index(self, row, column, parent):
1931		return self.createIndex(row, column, self.child_items[row])
1932
1933	def columnCount(self, parent=None):
1934		return len(self.column_headers)
1935
1936	def columnHeader(self, column):
1937		return self.column_headers[column]
1938
1939	def data(self, index, role):
1940		if role == Qt.BackgroundRole:
1941			child = self.child_items[index.row()]
1942			if child in self.highlight_set:
1943				return self.region_attributes[child.key].colour
1944			return None
1945		if role == Qt.ForegroundRole:
1946			child = self.child_items[index.row()]
1947			if child in self.highlight_set:
1948				return QColor(255, 255, 255)
1949			return self.region_attributes[child.key].colour
1950		if role != Qt.DisplayRole:
1951			return None
1952		hregion = self.child_items[index.row()]
1953		col = index.column()
1954		if col == 0:
1955			return hregion.pid
1956		if col == 1:
1957			return hregion.tid
1958		if col == 2:
1959			return hregion.comm
1960		return None
1961
1962	def SetHighlight(self, row, set_highlight):
1963		child = self.child_items[row]
1964		top_left = self.createIndex(row, 0, child)
1965		bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
1966		self.dataChanged.emit(top_left, bottom_right)
1967
1968	def Highlight(self, highlight_set):
1969		for row in xrange(self.child_count):
1970			child = self.child_items[row]
1971			if child in self.highlight_set:
1972				if child not in highlight_set:
1973					self.SetHighlight(row, False)
1974			elif child in highlight_set:
1975				self.SetHighlight(row, True)
1976		self.highlight_set = highlight_set
1977
1978# Switch graph legend is a table
1979
1980class SwitchGraphLegend(QWidget):
1981
1982	def __init__(self, collection, region_attributes, parent=None):
1983		super(SwitchGraphLegend, self).__init__(parent)
1984
1985		self.data_model = SwitchGraphLegendModel(collection, region_attributes)
1986
1987		self.model = QSortFilterProxyModel()
1988		self.model.setSourceModel(self.data_model)
1989
1990		self.view = QTableView()
1991		self.view.setModel(self.model)
1992		self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
1993		self.view.verticalHeader().setVisible(False)
1994		self.view.sortByColumn(-1, Qt.AscendingOrder)
1995		self.view.setSortingEnabled(True)
1996		self.view.resizeColumnsToContents()
1997		self.view.resizeRowsToContents()
1998
1999		self.vbox = VBoxLayout(self.view)
2000		self.setLayout(self.vbox)
2001
2002		sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
2003		sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
2004		self.saved_size = sz1
2005
2006	def resizeEvent(self, event):
2007		self.saved_size = self.size().width()
2008		super(SwitchGraphLegend, self).resizeEvent(event)
2009
2010	def Highlight(self, highlight_set):
2011		self.data_model.Highlight(highlight_set)
2012		self.update()
2013
2014	def changeEvent(self, event):
2015		if event.type() == QEvent.FontChange:
2016			self.view.resizeRowsToContents()
2017			self.view.resizeColumnsToContents()
2018			# Need to resize rows again after column resize
2019			self.view.resizeRowsToContents()
2020		super(SwitchGraphLegend, self).changeEvent(event)
2021
2022# Random colour generation
2023
2024def RGBColourTooLight(r, g, b):
2025	if g > 230:
2026		return True
2027	if g <= 160:
2028		return False
2029	if r <= 180 and g <= 180:
2030		return False
2031	if r < 60:
2032		return False
2033	return True
2034
2035def GenerateColours(x):
2036	cs = [0]
2037	for i in xrange(1, x):
2038		cs.append(int((255.0 / i) + 0.5))
2039	colours = []
2040	for r in cs:
2041		for g in cs:
2042			for b in cs:
2043				# Exclude black and colours that look too light against a white background
2044				if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
2045					continue
2046				colours.append(QColor(r, g, b))
2047	return colours
2048
2049def GenerateNColours(n):
2050	for x in xrange(2, n + 2):
2051		colours = GenerateColours(x)
2052		if len(colours) >= n:
2053			return colours
2054	return []
2055
2056def GenerateNRandomColours(n, seed):
2057	colours = GenerateNColours(n)
2058	random.seed(seed)
2059	random.shuffle(colours)
2060	return colours
2061
2062# Graph attributes, in particular the scale and subrange that change when zooming
2063
2064class GraphAttributes():
2065
2066	def __init__(self, scale, subrange, region_attributes, dp):
2067		self.scale = scale
2068		self.subrange = subrange
2069		self.region_attributes = region_attributes
2070		# Rounding avoids errors due to finite floating point precision
2071		self.dp = dp	# data decimal places
2072		self.Update()
2073
2074	def XToPixel(self, x):
2075		return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
2076
2077	def YToPixel(self, y):
2078		return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
2079
2080	def PixelToXRounded(self, px):
2081		return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
2082
2083	def PixelToYRounded(self, py):
2084		return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
2085
2086	def PixelToX(self, px):
2087		x = self.PixelToXRounded(px)
2088		if self.pdp.x == 0:
2089			rt = self.XToPixel(x)
2090			if rt > px:
2091				return x - 1
2092		return x
2093
2094	def PixelToY(self, py):
2095		y = self.PixelToYRounded(py)
2096		if self.pdp.y == 0:
2097			rt = self.YToPixel(y)
2098			if rt > py:
2099				return y - 1
2100		return y
2101
2102	def ToPDP(self, dp, scale):
2103		# Calculate pixel decimal places:
2104		#    (10 ** dp) is the minimum delta in the data
2105		#    scale it to get the minimum delta in pixels
2106		#    log10 gives the number of decimals places negatively
2107		#    subtrace 1 to divide by 10
2108		#    round to the lower negative number
2109		#    change the sign to get the number of decimals positively
2110		x = math.log10((10 ** dp) * scale)
2111		if x < 0:
2112			x -= 1
2113			x = -int(math.floor(x) - 0.1)
2114		else:
2115			x = 0
2116		return x
2117
2118	def Update(self):
2119		x = self.ToPDP(self.dp.x, self.scale.x)
2120		y = self.ToPDP(self.dp.y, self.scale.y)
2121		self.pdp = XY(x, y) # pixel decimal places
2122
2123# Switch graph splitter which divides the CPU graphs from the legend
2124
2125class SwitchGraphSplitter(QSplitter):
2126
2127	def __init__(self, parent=None):
2128		super(SwitchGraphSplitter, self).__init__(parent)
2129
2130		self.first_time = False
2131
2132	def resizeEvent(self, ev):
2133		if self.first_time:
2134			self.first_time = False
2135			sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
2136			sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
2137			sz0 = self.size().width() - self.handleWidth() - sz1
2138			self.setSizes([sz0, sz1])
2139		elif not(self.widget(1).saved_size is None):
2140			sz1 = self.widget(1).saved_size
2141			sz0 = self.size().width() - self.handleWidth() - sz1
2142			self.setSizes([sz0, sz1])
2143		super(SwitchGraphSplitter, self).resizeEvent(ev)
2144
2145# Graph widget base class
2146
2147class GraphWidget(QWidget):
2148
2149	graph_title_changed = Signal(object)
2150
2151	def __init__(self, parent=None):
2152		super(GraphWidget, self).__init__(parent)
2153
2154	def GraphTitleChanged(self, title):
2155		self.graph_title_changed.emit(title)
2156
2157	def Title(self):
2158		return ""
2159
2160# Display time in s, ms, us or ns
2161
2162def ToTimeStr(val):
2163	val = Decimal(val)
2164	if val >= 1000000000:
2165		return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
2166	if val >= 1000000:
2167		return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
2168	if val >= 1000:
2169		return "{} us".format((val / 1000).quantize(Decimal("0.001")))
2170	return "{} ns".format(val.quantize(Decimal("1")))
2171
2172# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
2173
2174class SwitchGraphWidget(GraphWidget):
2175
2176	def __init__(self, glb, collection, parent=None):
2177		super(SwitchGraphWidget, self).__init__(parent)
2178
2179		self.glb = glb
2180		self.collection = collection
2181
2182		self.back_state = []
2183		self.forward_state = []
2184		self.selection_state = (None, None)
2185		self.fwd_rect = None
2186		self.start_time = self.glb.StartTime(collection.machine_id)
2187
2188		i = 0
2189		hregions = collection.hregions.values()
2190		colours = GenerateNRandomColours(len(hregions), 1013)
2191		region_attributes = {}
2192		for hregion in hregions:
2193			if hregion.pid == 0 and hregion.tid == 0:
2194				region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
2195			else:
2196				region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
2197				i = i + 1
2198
2199		# Default to entire range
2200		xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
2201		ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
2202		subrange = XY(xsubrange, ysubrange)
2203
2204		scale = self.GetScaleForRange(subrange)
2205
2206		self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
2207
2208		self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
2209
2210		self.scene = QGraphicsScene()
2211		self.scene.addItem(self.item)
2212
2213		self.view = QGraphicsView(self.scene)
2214		self.view.centerOn(0, 0)
2215		self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
2216
2217		self.legend = SwitchGraphLegend(collection, region_attributes)
2218
2219		self.splitter = SwitchGraphSplitter()
2220		self.splitter.addWidget(self.view)
2221		self.splitter.addWidget(self.legend)
2222
2223		self.point_label = QLabel("")
2224		self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
2225
2226		self.back_button = QToolButton()
2227		self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
2228		self.back_button.setDisabled(True)
2229		self.back_button.released.connect(lambda: self.Back())
2230
2231		self.forward_button = QToolButton()
2232		self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
2233		self.forward_button.setDisabled(True)
2234		self.forward_button.released.connect(lambda: self.Forward())
2235
2236		self.zoom_button = QToolButton()
2237		self.zoom_button.setText("Zoom")
2238		self.zoom_button.setDisabled(True)
2239		self.zoom_button.released.connect(lambda: self.Zoom())
2240
2241		self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
2242
2243		self.vbox = VBoxLayout(self.splitter, self.hbox)
2244
2245		self.setLayout(self.vbox)
2246
2247	def GetScaleForRangeX(self, xsubrange):
2248		# Default graph 1000 pixels wide
2249		dflt = 1000.0
2250		r = xsubrange.hi - xsubrange.lo
2251		return dflt / r
2252
2253	def GetScaleForRangeY(self, ysubrange):
2254		# Default graph 50 pixels high
2255		dflt = 50.0
2256		r = ysubrange.hi - ysubrange.lo
2257		return dflt / r
2258
2259	def GetScaleForRange(self, subrange):
2260		# Default graph 1000 pixels wide, 50 pixels high
2261		xscale = self.GetScaleForRangeX(subrange.x)
2262		yscale = self.GetScaleForRangeY(subrange.y)
2263		return XY(xscale, yscale)
2264
2265	def PointEvent(self, cpu, time_from, time_to, hregions):
2266		text = "CPU: " + str(cpu)
2267		time_from = time_from.quantize(Decimal(1))
2268		rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
2269		text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
2270		self.point_label.setText(text)
2271		self.legend.Highlight(hregions)
2272
2273	def RightClickEvent(self, cpu, hregion_times, pos):
2274		if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
2275			return
2276		menu = QMenu(self.view)
2277		for hregion, time in hregion_times:
2278			thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
2279			menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
2280			menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
2281		menu.exec_(pos)
2282
2283	def RightClickSelect(self, args):
2284		CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
2285
2286	def NoPointEvent(self):
2287		self.point_label.setText("")
2288		self.legend.Highlight({})
2289
2290	def RangeEvent(self, time_from, time_to):
2291		time_from = time_from.quantize(Decimal(1))
2292		time_to = time_to.quantize(Decimal(1))
2293		if time_to <= time_from:
2294			self.point_label.setText("")
2295			return
2296		rel_time_from = time_from - self.start_time
2297		rel_time_to = time_to - self.start_time
2298		text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
2299		text = text + " duration: " + ToTimeStr(time_to - time_from)
2300		self.point_label.setText(text)
2301
2302	def BackState(self):
2303		return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
2304
2305	def PushBackState(self):
2306		state = copy.deepcopy(self.BackState())
2307		self.back_state.append(state)
2308		self.back_button.setEnabled(True)
2309
2310	def PopBackState(self):
2311		self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
2312		self.attrs.Update()
2313		if not self.back_state:
2314			self.back_button.setDisabled(True)
2315
2316	def PushForwardState(self):
2317		state = copy.deepcopy(self.BackState())
2318		self.forward_state.append(state)
2319		self.forward_button.setEnabled(True)
2320
2321	def PopForwardState(self):
2322		self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
2323		self.attrs.Update()
2324		if not self.forward_state:
2325			self.forward_button.setDisabled(True)
2326
2327	def Title(self):
2328		time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
2329		time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
2330		rel_time_from = time_from - self.start_time
2331		rel_time_to = time_to - self.start_time
2332		title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
2333		title = title + " (" + ToTimeStr(time_to - time_from) + ")"
2334		return title
2335
2336	def Update(self):
2337		selected_subrange, selection_state = self.selection_state
2338		self.item.SetSelection(selection_state)
2339		self.item.SetBracket(self.fwd_rect)
2340		self.zoom_button.setDisabled(selected_subrange is None)
2341		self.GraphTitleChanged(self.Title())
2342		self.item.update(self.item.boundingRect())
2343
2344	def Back(self):
2345		if not self.back_state:
2346			return
2347		self.PushForwardState()
2348		self.PopBackState()
2349		self.Update()
2350
2351	def Forward(self):
2352		if not self.forward_state:
2353			return
2354		self.PushBackState()
2355		self.PopForwardState()
2356		self.Update()
2357
2358	def SelectEvent(self, x0, x1, selection_state):
2359		if selection_state is None:
2360			selected_subrange = None
2361		else:
2362			if x1 - x0 < 1.0:
2363				x1 += 1.0
2364			selected_subrange = Subrange(x0, x1)
2365		self.selection_state = (selected_subrange, selection_state)
2366		self.zoom_button.setDisabled(selected_subrange is None)
2367
2368	def Zoom(self):
2369		selected_subrange, selection_state = self.selection_state
2370		if selected_subrange is None:
2371			return
2372		self.fwd_rect = selection_state
2373		self.item.SetSelection(None)
2374		self.PushBackState()
2375		self.attrs.subrange.x = selected_subrange
2376		self.forward_state = []
2377		self.forward_button.setDisabled(True)
2378		self.selection_state = (None, None)
2379		self.fwd_rect = None
2380		self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
2381		self.attrs.Update()
2382		self.Update()
2383
2384# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
2385
2386class SlowInitClass():
2387
2388	def __init__(self, glb, title, init_fn):
2389		self.init_fn = init_fn
2390		self.done = False
2391		self.result = None
2392
2393		self.msg_box = QMessageBox(glb.mainwindow)
2394		self.msg_box.setText("Initializing " + title + ". Please wait.")
2395		self.msg_box.setWindowTitle("Initializing " + title)
2396		self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
2397
2398		self.init_thread = Thread(self.ThreadFn, glb)
2399		self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
2400
2401		self.init_thread.start()
2402
2403	def Done(self):
2404		self.msg_box.done(0)
2405
2406	def ThreadFn(self, glb):
2407		conn_name = "SlowInitClass" + str(os.getpid())
2408		db, dbname = glb.dbref.Open(conn_name)
2409		self.result = self.init_fn(db)
2410		self.done = True
2411		return (True, 0)
2412
2413	def Result(self):
2414		while not self.done:
2415			self.msg_box.exec_()
2416		self.init_thread.wait()
2417		return self.result
2418
2419def SlowInit(glb, title, init_fn):
2420	init = SlowInitClass(glb, title, init_fn)
2421	return init.Result()
2422
2423# Time chart by CPU window
2424
2425class TimeChartByCPUWindow(QMdiSubWindow):
2426
2427	def __init__(self, glb, parent=None):
2428		super(TimeChartByCPUWindow, self).__init__(parent)
2429
2430		self.glb = glb
2431		self.machine_id = glb.HostMachineId()
2432		self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
2433
2434		collection = LookupModel(self.collection_name)
2435		if collection is None:
2436			collection = SlowInit(glb, "Time Chart", self.Init)
2437
2438		self.widget = SwitchGraphWidget(glb, collection, self)
2439		self.view = self.widget
2440
2441		self.base_title = "Time Chart by CPU"
2442		self.setWindowTitle(self.base_title + self.widget.Title())
2443		self.widget.graph_title_changed.connect(self.GraphTitleChanged)
2444
2445		self.setWidget(self.widget)
2446
2447		AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
2448
2449	def Init(self, db):
2450		return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
2451
2452	def GraphTitleChanged(self, title):
2453		self.setWindowTitle(self.base_title + " : " + title)
2454
2455# Child data item  finder
2456
2457class ChildDataItemFinder():
2458
2459	def __init__(self, root):
2460		self.root = root
2461		self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
2462		self.rows = []
2463		self.pos = 0
2464
2465	def FindSelect(self):
2466		self.rows = []
2467		if self.pattern:
2468			pattern = re.compile(self.value)
2469			for child in self.root.child_items:
2470				for column_data in child.data:
2471					if re.search(pattern, str(column_data)) is not None:
2472						self.rows.append(child.row)
2473						break
2474		else:
2475			for child in self.root.child_items:
2476				for column_data in child.data:
2477					if self.value in str(column_data):
2478						self.rows.append(child.row)
2479						break
2480
2481	def FindValue(self):
2482		self.pos = 0
2483		if self.last_value != self.value or self.pattern != self.last_pattern:
2484			self.FindSelect()
2485		if not len(self.rows):
2486			return -1
2487		return self.rows[self.pos]
2488
2489	def FindThread(self):
2490		if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
2491			row = self.FindValue()
2492		elif len(self.rows):
2493			if self.direction > 0:
2494				self.pos += 1
2495				if self.pos >= len(self.rows):
2496					self.pos = 0
2497			else:
2498				self.pos -= 1
2499				if self.pos < 0:
2500					self.pos = len(self.rows) - 1
2501			row = self.rows[self.pos]
2502		else:
2503			row = -1
2504		return (True, row)
2505
2506	def Find(self, value, direction, pattern, context, callback):
2507		self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
2508		# Use a thread so the UI is not blocked
2509		thread = Thread(self.FindThread)
2510		thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
2511		thread.start()
2512
2513	def FindDone(self, thread, callback, row):
2514		callback(row)
2515
2516# Number of database records to fetch in one go
2517
2518glb_chunk_sz = 10000
2519
2520# Background process for SQL data fetcher
2521
2522class SQLFetcherProcess():
2523
2524	def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
2525		# Need a unique connection name
2526		conn_name = "SQLFetcher" + str(os.getpid())
2527		self.db, dbname = dbref.Open(conn_name)
2528		self.sql = sql
2529		self.buffer = buffer
2530		self.head = head
2531		self.tail = tail
2532		self.fetch_count = fetch_count
2533		self.fetching_done = fetching_done
2534		self.process_target = process_target
2535		self.wait_event = wait_event
2536		self.fetched_event = fetched_event
2537		self.prep = prep
2538		self.query = QSqlQuery(self.db)
2539		self.query_limit = 0 if "$$last_id$$" in sql else 2
2540		self.last_id = -1
2541		self.fetched = 0
2542		self.more = True
2543		self.local_head = self.head.value
2544		self.local_tail = self.tail.value
2545
2546	def Select(self):
2547		if self.query_limit:
2548			if self.query_limit == 1:
2549				return
2550			self.query_limit -= 1
2551		stmt = self.sql.replace("$$last_id$$", str(self.last_id))
2552		QueryExec(self.query, stmt)
2553
2554	def Next(self):
2555		if not self.query.next():
2556			self.Select()
2557			if not self.query.next():
2558				return None
2559		self.last_id = self.query.value(0)
2560		return self.prep(self.query)
2561
2562	def WaitForTarget(self):
2563		while True:
2564			self.wait_event.clear()
2565			target = self.process_target.value
2566			if target > self.fetched or target < 0:
2567				break
2568			self.wait_event.wait()
2569		return target
2570
2571	def HasSpace(self, sz):
2572		if self.local_tail <= self.local_head:
2573			space = len(self.buffer) - self.local_head
2574			if space > sz:
2575				return True
2576			if space >= glb_nsz:
2577				# Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
2578				nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
2579				self.buffer[self.local_head : self.local_head + len(nd)] = nd
2580			self.local_head = 0
2581		if self.local_tail - self.local_head > sz:
2582			return True
2583		return False
2584
2585	def WaitForSpace(self, sz):
2586		if self.HasSpace(sz):
2587			return
2588		while True:
2589			self.wait_event.clear()
2590			self.local_tail = self.tail.value
2591			if self.HasSpace(sz):
2592				return
2593			self.wait_event.wait()
2594
2595	def AddToBuffer(self, obj):
2596		d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
2597		n = len(d)
2598		nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
2599		sz = n + glb_nsz
2600		self.WaitForSpace(sz)
2601		pos = self.local_head
2602		self.buffer[pos : pos + len(nd)] = nd
2603		self.buffer[pos + glb_nsz : pos + sz] = d
2604		self.local_head += sz
2605
2606	def FetchBatch(self, batch_size):
2607		fetched = 0
2608		while batch_size > fetched:
2609			obj = self.Next()
2610			if obj is None:
2611				self.more = False
2612				break
2613			self.AddToBuffer(obj)
2614			fetched += 1
2615		if fetched:
2616			self.fetched += fetched
2617			with self.fetch_count.get_lock():
2618				self.fetch_count.value += fetched
2619			self.head.value = self.local_head
2620			self.fetched_event.set()
2621
2622	def Run(self):
2623		while self.more:
2624			target = self.WaitForTarget()
2625			if target < 0:
2626				break
2627			batch_size = min(glb_chunk_sz, target - self.fetched)
2628			self.FetchBatch(batch_size)
2629		self.fetching_done.value = True
2630		self.fetched_event.set()
2631
2632def SQLFetcherFn(*x):
2633	process = SQLFetcherProcess(*x)
2634	process.Run()
2635
2636# SQL data fetcher
2637
2638class SQLFetcher(QObject):
2639
2640	done = Signal(object)
2641
2642	def __init__(self, glb, sql, prep, process_data, parent=None):
2643		super(SQLFetcher, self).__init__(parent)
2644		self.process_data = process_data
2645		self.more = True
2646		self.target = 0
2647		self.last_target = 0
2648		self.fetched = 0
2649		self.buffer_size = 16 * 1024 * 1024
2650		self.buffer = Array(c_char, self.buffer_size, lock=False)
2651		self.head = Value(c_longlong)
2652		self.tail = Value(c_longlong)
2653		self.local_tail = 0
2654		self.fetch_count = Value(c_longlong)
2655		self.fetching_done = Value(c_bool)
2656		self.last_count = 0
2657		self.process_target = Value(c_longlong)
2658		self.wait_event = Event()
2659		self.fetched_event = Event()
2660		glb.AddInstanceToShutdownOnExit(self)
2661		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))
2662		self.process.start()
2663		self.thread = Thread(self.Thread)
2664		self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
2665		self.thread.start()
2666
2667	def Shutdown(self):
2668		# Tell the thread and process to exit
2669		self.process_target.value = -1
2670		self.wait_event.set()
2671		self.more = False
2672		self.fetching_done.value = True
2673		self.fetched_event.set()
2674
2675	def Thread(self):
2676		if not self.more:
2677			return True, 0
2678		while True:
2679			self.fetched_event.clear()
2680			fetch_count = self.fetch_count.value
2681			if fetch_count != self.last_count:
2682				break
2683			if self.fetching_done.value:
2684				self.more = False
2685				return True, 0
2686			self.fetched_event.wait()
2687		count = fetch_count - self.last_count
2688		self.last_count = fetch_count
2689		self.fetched += count
2690		return False, count
2691
2692	def Fetch(self, nr):
2693		if not self.more:
2694			# -1 inidcates there are no more
2695			return -1
2696		result = self.fetched
2697		extra = result + nr - self.target
2698		if extra > 0:
2699			self.target += extra
2700			# process_target < 0 indicates shutting down
2701			if self.process_target.value >= 0:
2702				self.process_target.value = self.target
2703			self.wait_event.set()
2704		return result
2705
2706	def RemoveFromBuffer(self):
2707		pos = self.local_tail
2708		if len(self.buffer) - pos < glb_nsz:
2709			pos = 0
2710		n = pickle.loads(self.buffer[pos : pos + glb_nsz])
2711		if n == 0:
2712			pos = 0
2713			n = pickle.loads(self.buffer[0 : glb_nsz])
2714		pos += glb_nsz
2715		obj = pickle.loads(self.buffer[pos : pos + n])
2716		self.local_tail = pos + n
2717		return obj
2718
2719	def ProcessData(self, count):
2720		for i in xrange(count):
2721			obj = self.RemoveFromBuffer()
2722			self.process_data(obj)
2723		self.tail.value = self.local_tail
2724		self.wait_event.set()
2725		self.done.emit(count)
2726
2727# Fetch more records bar
2728
2729class FetchMoreRecordsBar():
2730
2731	def __init__(self, model, parent):
2732		self.model = model
2733
2734		self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
2735		self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2736
2737		self.fetch_count = QSpinBox()
2738		self.fetch_count.setRange(1, 1000000)
2739		self.fetch_count.setValue(10)
2740		self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2741
2742		self.fetch = QPushButton("Go!")
2743		self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2744		self.fetch.released.connect(self.FetchMoreRecords)
2745
2746		self.progress = QProgressBar()
2747		self.progress.setRange(0, 100)
2748		self.progress.hide()
2749
2750		self.done_label = QLabel("All records fetched")
2751		self.done_label.hide()
2752
2753		self.spacer = QLabel("")
2754
2755		self.close_button = QToolButton()
2756		self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
2757		self.close_button.released.connect(self.Deactivate)
2758
2759		self.hbox = QHBoxLayout()
2760		self.hbox.setContentsMargins(0, 0, 0, 0)
2761
2762		self.hbox.addWidget(self.label)
2763		self.hbox.addWidget(self.fetch_count)
2764		self.hbox.addWidget(self.fetch)
2765		self.hbox.addWidget(self.spacer)
2766		self.hbox.addWidget(self.progress)
2767		self.hbox.addWidget(self.done_label)
2768		self.hbox.addWidget(self.close_button)
2769
2770		self.bar = QWidget()
2771		self.bar.setLayout(self.hbox)
2772		self.bar.show()
2773
2774		self.in_progress = False
2775		self.model.progress.connect(self.Progress)
2776
2777		self.done = False
2778
2779		if not model.HasMoreRecords():
2780			self.Done()
2781
2782	def Widget(self):
2783		return self.bar
2784
2785	def Activate(self):
2786		self.bar.show()
2787		self.fetch.setFocus()
2788
2789	def Deactivate(self):
2790		self.bar.hide()
2791
2792	def Enable(self, enable):
2793		self.fetch.setEnabled(enable)
2794		self.fetch_count.setEnabled(enable)
2795
2796	def Busy(self):
2797		self.Enable(False)
2798		self.fetch.hide()
2799		self.spacer.hide()
2800		self.progress.show()
2801
2802	def Idle(self):
2803		self.in_progress = False
2804		self.Enable(True)
2805		self.progress.hide()
2806		self.fetch.show()
2807		self.spacer.show()
2808
2809	def Target(self):
2810		return self.fetch_count.value() * glb_chunk_sz
2811
2812	def Done(self):
2813		self.done = True
2814		self.Idle()
2815		self.label.hide()
2816		self.fetch_count.hide()
2817		self.fetch.hide()
2818		self.spacer.hide()
2819		self.done_label.show()
2820
2821	def Progress(self, count):
2822		if self.in_progress:
2823			if count:
2824				percent = ((count - self.start) * 100) / self.Target()
2825				if percent >= 100:
2826					self.Idle()
2827				else:
2828					self.progress.setValue(percent)
2829		if not count:
2830			# Count value of zero means no more records
2831			self.Done()
2832
2833	def FetchMoreRecords(self):
2834		if self.done:
2835			return
2836		self.progress.setValue(0)
2837		self.Busy()
2838		self.in_progress = True
2839		self.start = self.model.FetchMoreRecords(self.Target())
2840
2841# Brance data model level two item
2842
2843class BranchLevelTwoItem():
2844
2845	def __init__(self, row, col, text, parent_item):
2846		self.row = row
2847		self.parent_item = parent_item
2848		self.data = [""] * (col + 1)
2849		self.data[col] = text
2850		self.level = 2
2851
2852	def getParentItem(self):
2853		return self.parent_item
2854
2855	def getRow(self):
2856		return self.row
2857
2858	def childCount(self):
2859		return 0
2860
2861	def hasChildren(self):
2862		return False
2863
2864	def getData(self, column):
2865		return self.data[column]
2866
2867# Brance data model level one item
2868
2869class BranchLevelOneItem():
2870
2871	def __init__(self, glb, row, data, parent_item):
2872		self.glb = glb
2873		self.row = row
2874		self.parent_item = parent_item
2875		self.child_count = 0
2876		self.child_items = []
2877		self.data = data[1:]
2878		self.dbid = data[0]
2879		self.level = 1
2880		self.query_done = False
2881		self.br_col = len(self.data) - 1
2882
2883	def getChildItem(self, row):
2884		return self.child_items[row]
2885
2886	def getParentItem(self):
2887		return self.parent_item
2888
2889	def getRow(self):
2890		return self.row
2891
2892	def Select(self):
2893		self.query_done = True
2894
2895		if not self.glb.have_disassembler:
2896			return
2897
2898		query = QSqlQuery(self.glb.db)
2899
2900		QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
2901				  " FROM samples"
2902				  " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
2903				  " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
2904				  " WHERE samples.id = " + str(self.dbid))
2905		if not query.next():
2906			return
2907		cpu = query.value(0)
2908		dso = query.value(1)
2909		sym = query.value(2)
2910		if dso == 0 or sym == 0:
2911			return
2912		off = query.value(3)
2913		short_name = query.value(4)
2914		long_name = query.value(5)
2915		build_id = query.value(6)
2916		sym_start = query.value(7)
2917		ip = query.value(8)
2918
2919		QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
2920				  " FROM samples"
2921				  " INNER JOIN symbols ON samples.symbol_id = symbols.id"
2922				  " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
2923				  " ORDER BY samples.id"
2924				  " LIMIT 1")
2925		if not query.next():
2926			return
2927		if query.value(0) != dso:
2928			# Cannot disassemble from one dso to another
2929			return
2930		bsym = query.value(1)
2931		boff = query.value(2)
2932		bsym_start = query.value(3)
2933		if bsym == 0:
2934			return
2935		tot = bsym_start + boff + 1 - sym_start - off
2936		if tot <= 0 or tot > 16384:
2937			return
2938
2939		inst = self.glb.disassembler.Instruction()
2940		f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
2941		if not f:
2942			return
2943		mode = 0 if Is64Bit(f) else 1
2944		self.glb.disassembler.SetMode(inst, mode)
2945
2946		buf_sz = tot + 16
2947		buf = create_string_buffer(tot + 16)
2948		f.seek(sym_start + off)
2949		buf.value = f.read(buf_sz)
2950		buf_ptr = addressof(buf)
2951		i = 0
2952		while tot > 0:
2953			cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
2954			if cnt:
2955				byte_str = tohex(ip).rjust(16)
2956				for k in xrange(cnt):
2957					byte_str += " %02x" % ord(buf[i])
2958					i += 1
2959				while k < 15:
2960					byte_str += "   "
2961					k += 1
2962				self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
2963				self.child_count += 1
2964			else:
2965				return
2966			buf_ptr += cnt
2967			tot -= cnt
2968			buf_sz -= cnt
2969			ip += cnt
2970
2971	def childCount(self):
2972		if not self.query_done:
2973			self.Select()
2974			if not self.child_count:
2975				return -1
2976		return self.child_count
2977
2978	def hasChildren(self):
2979		if not self.query_done:
2980			return True
2981		return self.child_count > 0
2982
2983	def getData(self, column):
2984		return self.data[column]
2985
2986# Brance data model root item
2987
2988class BranchRootItem():
2989
2990	def __init__(self):
2991		self.child_count = 0
2992		self.child_items = []
2993		self.level = 0
2994
2995	def getChildItem(self, row):
2996		return self.child_items[row]
2997
2998	def getParentItem(self):
2999		return None
3000
3001	def getRow(self):
3002		return 0
3003
3004	def childCount(self):
3005		return self.child_count
3006
3007	def hasChildren(self):
3008		return self.child_count > 0
3009
3010	def getData(self, column):
3011		return ""
3012
3013# Calculate instructions per cycle
3014
3015def CalcIPC(cyc_cnt, insn_cnt):
3016	if cyc_cnt and insn_cnt:
3017		ipc = Decimal(float(insn_cnt) / cyc_cnt)
3018		ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
3019	else:
3020		ipc = "0"
3021	return ipc
3022
3023# Branch data preparation
3024
3025def BranchDataPrepBr(query, data):
3026	data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
3027			" (" + dsoname(query.value(11)) + ")" + " -> " +
3028			tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
3029			" (" + dsoname(query.value(15)) + ")")
3030
3031def BranchDataPrepIPC(query, data):
3032	insn_cnt = query.value(16)
3033	cyc_cnt = query.value(17)
3034	ipc = CalcIPC(cyc_cnt, insn_cnt)
3035	data.append(insn_cnt)
3036	data.append(cyc_cnt)
3037	data.append(ipc)
3038
3039def BranchDataPrep(query):
3040	data = []
3041	for i in xrange(0, 8):
3042		data.append(query.value(i))
3043	BranchDataPrepBr(query, data)
3044	return data
3045
3046def BranchDataPrepWA(query):
3047	data = []
3048	data.append(query.value(0))
3049	# Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3050	data.append("{:>19}".format(query.value(1)))
3051	for i in xrange(2, 8):
3052		data.append(query.value(i))
3053	BranchDataPrepBr(query, data)
3054	return data
3055
3056def BranchDataWithIPCPrep(query):
3057	data = []
3058	for i in xrange(0, 8):
3059		data.append(query.value(i))
3060	BranchDataPrepIPC(query, data)
3061	BranchDataPrepBr(query, data)
3062	return data
3063
3064def BranchDataWithIPCPrepWA(query):
3065	data = []
3066	data.append(query.value(0))
3067	# Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3068	data.append("{:>19}".format(query.value(1)))
3069	for i in xrange(2, 8):
3070		data.append(query.value(i))
3071	BranchDataPrepIPC(query, data)
3072	BranchDataPrepBr(query, data)
3073	return data
3074
3075# Branch data model
3076
3077class BranchModel(TreeModel):
3078
3079	progress = Signal(object)
3080
3081	def __init__(self, glb, event_id, where_clause, parent=None):
3082		super(BranchModel, self).__init__(glb, None, parent)
3083		self.event_id = event_id
3084		self.more = True
3085		self.populated = 0
3086		self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
3087		if self.have_ipc:
3088			select_ipc = ", insn_count, cyc_count"
3089			prep_fn = BranchDataWithIPCPrep
3090			prep_wa_fn = BranchDataWithIPCPrepWA
3091		else:
3092			select_ipc = ""
3093			prep_fn = BranchDataPrep
3094			prep_wa_fn = BranchDataPrepWA
3095		sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
3096			" CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
3097			" ip, symbols.name, sym_offset, dsos.short_name,"
3098			" to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
3099			+ select_ipc +
3100			" FROM samples"
3101			" INNER JOIN comms ON comm_id = comms.id"
3102			" INNER JOIN threads ON thread_id = threads.id"
3103			" INNER JOIN branch_types ON branch_type = branch_types.id"
3104			" INNER JOIN symbols ON symbol_id = symbols.id"
3105			" INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
3106			" INNER JOIN dsos ON samples.dso_id = dsos.id"
3107			" INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
3108			" WHERE samples.id > $$last_id$$" + where_clause +
3109			" AND evsel_id = " + str(self.event_id) +
3110			" ORDER BY samples.id"
3111			" LIMIT " + str(glb_chunk_sz))
3112		if pyside_version_1 and sys.version_info[0] == 3:
3113			prep = prep_fn
3114		else:
3115			prep = prep_wa_fn
3116		self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
3117		self.fetcher.done.connect(self.Update)
3118		self.fetcher.Fetch(glb_chunk_sz)
3119
3120	def GetRoot(self):
3121		return BranchRootItem()
3122
3123	def columnCount(self, parent=None):
3124		if self.have_ipc:
3125			return 11
3126		else:
3127			return 8
3128
3129	def columnHeader(self, column):
3130		if self.have_ipc:
3131			return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
3132		else:
3133			return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
3134
3135	def columnFont(self, column):
3136		if self.have_ipc:
3137			br_col = 10
3138		else:
3139			br_col = 7
3140		if column != br_col:
3141			return None
3142		return QFont("Monospace")
3143
3144	def DisplayData(self, item, index):
3145		if item.level == 1:
3146			self.FetchIfNeeded(item.row)
3147		return item.getData(index.column())
3148
3149	def AddSample(self, data):
3150		child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
3151		self.root.child_items.append(child)
3152		self.populated += 1
3153
3154	def Update(self, fetched):
3155		if not fetched:
3156			self.more = False
3157			self.progress.emit(0)
3158		child_count = self.root.child_count
3159		count = self.populated - child_count
3160		if count > 0:
3161			parent = QModelIndex()
3162			self.beginInsertRows(parent, child_count, child_count + count - 1)
3163			self.insertRows(child_count, count, parent)
3164			self.root.child_count += count
3165			self.endInsertRows()
3166			self.progress.emit(self.root.child_count)
3167
3168	def FetchMoreRecords(self, count):
3169		current = self.root.child_count
3170		if self.more:
3171			self.fetcher.Fetch(count)
3172		else:
3173			self.progress.emit(0)
3174		return current
3175
3176	def HasMoreRecords(self):
3177		return self.more
3178
3179# Report Variables
3180
3181class ReportVars():
3182
3183	def __init__(self, name = "", where_clause = "", limit = ""):
3184		self.name = name
3185		self.where_clause = where_clause
3186		self.limit = limit
3187
3188	def UniqueId(self):
3189		return str(self.where_clause + ";" + self.limit)
3190
3191# Branch window
3192
3193class BranchWindow(QMdiSubWindow):
3194
3195	def __init__(self, glb, event_id, report_vars, parent=None):
3196		super(BranchWindow, self).__init__(parent)
3197
3198		model_name = "Branch Events " + str(event_id) +  " " + report_vars.UniqueId()
3199
3200		self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
3201
3202		self.view = QTreeView()
3203		self.view.setUniformRowHeights(True)
3204		self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
3205		self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
3206		self.view.setModel(self.model)
3207
3208		self.ResizeColumnsToContents()
3209
3210		self.context_menu = TreeContextMenu(self.view)
3211
3212		self.find_bar = FindBar(self, self, True)
3213
3214		self.finder = ChildDataItemFinder(self.model.root)
3215
3216		self.fetch_bar = FetchMoreRecordsBar(self.model, self)
3217
3218		self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
3219
3220		self.setWidget(self.vbox.Widget())
3221
3222		AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
3223
3224	def ResizeColumnToContents(self, column, n):
3225		# Using the view's resizeColumnToContents() here is extrememly slow
3226		# so implement a crude alternative
3227		mm = "MM" if column else "MMMM"
3228		font = self.view.font()
3229		metrics = QFontMetrics(font)
3230		max = 0
3231		for row in xrange(n):
3232			val = self.model.root.child_items[row].data[column]
3233			len = metrics.width(str(val) + mm)
3234			max = len if len > max else max
3235		val = self.model.columnHeader(column)
3236		len = metrics.width(str(val) + mm)
3237		max = len if len > max else max
3238		self.view.setColumnWidth(column, max)
3239
3240	def ResizeColumnsToContents(self):
3241		n = min(self.model.root.child_count, 100)
3242		if n < 1:
3243			# No data yet, so connect a signal to notify when there is
3244			self.model.rowsInserted.connect(self.UpdateColumnWidths)
3245			return
3246		columns = self.model.columnCount()
3247		for i in xrange(columns):
3248			self.ResizeColumnToContents(i, n)
3249
3250	def UpdateColumnWidths(self, *x):
3251		# This only needs to be done once, so disconnect the signal now
3252		self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
3253		self.ResizeColumnsToContents()
3254
3255	def Find(self, value, direction, pattern, context):
3256		self.view.setFocus()
3257		self.find_bar.Busy()
3258		self.finder.Find(value, direction, pattern, context, self.FindDone)
3259
3260	def FindDone(self, row):
3261		self.find_bar.Idle()
3262		if row >= 0:
3263			self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
3264		else:
3265			self.find_bar.NotFound()
3266
3267# Line edit data item
3268
3269class LineEditDataItem(object):
3270
3271	def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3272		self.glb = glb
3273		self.label = label
3274		self.placeholder_text = placeholder_text
3275		self.parent = parent
3276		self.id = id
3277
3278		self.value = default
3279
3280		self.widget = QLineEdit(default)
3281		self.widget.editingFinished.connect(self.Validate)
3282		self.widget.textChanged.connect(self.Invalidate)
3283		self.red = False
3284		self.error = ""
3285		self.validated = True
3286
3287		if placeholder_text:
3288			self.widget.setPlaceholderText(placeholder_text)
3289
3290	def TurnTextRed(self):
3291		if not self.red:
3292			palette = QPalette()
3293			palette.setColor(QPalette.Text,Qt.red)
3294			self.widget.setPalette(palette)
3295			self.red = True
3296
3297	def TurnTextNormal(self):
3298		if self.red:
3299			palette = QPalette()
3300			self.widget.setPalette(palette)
3301			self.red = False
3302
3303	def InvalidValue(self, value):
3304		self.value = ""
3305		self.TurnTextRed()
3306		self.error = self.label + " invalid value '" + value + "'"
3307		self.parent.ShowMessage(self.error)
3308
3309	def Invalidate(self):
3310		self.validated = False
3311
3312	def DoValidate(self, input_string):
3313		self.value = input_string.strip()
3314
3315	def Validate(self):
3316		self.validated = True
3317		self.error = ""
3318		self.TurnTextNormal()
3319		self.parent.ClearMessage()
3320		input_string = self.widget.text()
3321		if not len(input_string.strip()):
3322			self.value = ""
3323			return
3324		self.DoValidate(input_string)
3325
3326	def IsValid(self):
3327		if not self.validated:
3328			self.Validate()
3329		if len(self.error):
3330			self.parent.ShowMessage(self.error)
3331			return False
3332		return True
3333
3334	def IsNumber(self, value):
3335		try:
3336			x = int(value)
3337		except:
3338			x = 0
3339		return str(x) == value
3340
3341# Non-negative integer ranges dialog data item
3342
3343class NonNegativeIntegerRangesDataItem(LineEditDataItem):
3344
3345	def __init__(self, glb, label, placeholder_text, column_name, parent):
3346		super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3347
3348		self.column_name = column_name
3349
3350	def DoValidate(self, input_string):
3351		singles = []
3352		ranges = []
3353		for value in [x.strip() for x in input_string.split(",")]:
3354			if "-" in value:
3355				vrange = value.split("-")
3356				if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3357					return self.InvalidValue(value)
3358				ranges.append(vrange)
3359			else:
3360				if not self.IsNumber(value):
3361					return self.InvalidValue(value)
3362				singles.append(value)
3363		ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3364		if len(singles):
3365			ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
3366		self.value = " OR ".join(ranges)
3367
3368# Positive integer dialog data item
3369
3370class PositiveIntegerDataItem(LineEditDataItem):
3371
3372	def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3373		super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
3374
3375	def DoValidate(self, input_string):
3376		if not self.IsNumber(input_string.strip()):
3377			return self.InvalidValue(input_string)
3378		value = int(input_string.strip())
3379		if value <= 0:
3380			return self.InvalidValue(input_string)
3381		self.value = str(value)
3382
3383# Dialog data item converted and validated using a SQL table
3384
3385class SQLTableDataItem(LineEditDataItem):
3386
3387	def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
3388		super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
3389
3390		self.table_name = table_name
3391		self.match_column = match_column
3392		self.column_name1 = column_name1
3393		self.column_name2 = column_name2
3394
3395	def ValueToIds(self, value):
3396		ids = []
3397		query = QSqlQuery(self.glb.db)
3398		stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
3399		ret = query.exec_(stmt)
3400		if ret:
3401			while query.next():
3402				ids.append(str(query.value(0)))
3403		return ids
3404
3405	def DoValidate(self, input_string):
3406		all_ids = []
3407		for value in [x.strip() for x in input_string.split(",")]:
3408			ids = self.ValueToIds(value)
3409			if len(ids):
3410				all_ids.extend(ids)
3411			else:
3412				return self.InvalidValue(value)
3413		self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
3414		if self.column_name2:
3415			self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
3416
3417# Sample time ranges dialog data item converted and validated using 'samples' SQL table
3418
3419class SampleTimeRangesDataItem(LineEditDataItem):
3420
3421	def __init__(self, glb, label, placeholder_text, column_name, parent):
3422		self.column_name = column_name
3423
3424		self.last_id = 0
3425		self.first_time = 0
3426		self.last_time = 2 ** 64
3427
3428		query = QSqlQuery(glb.db)
3429		QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
3430		if query.next():
3431			self.last_id = int(query.value(0))
3432		self.first_time = int(glb.HostStartTime())
3433		self.last_time = int(glb.HostFinishTime())
3434		if placeholder_text:
3435			placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
3436
3437		super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3438
3439	def IdBetween(self, query, lower_id, higher_id, order):
3440		QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
3441		if query.next():
3442			return True, int(query.value(0))
3443		else:
3444			return False, 0
3445
3446	def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
3447		query = QSqlQuery(self.glb.db)
3448		while True:
3449			next_id = int((lower_id + higher_id) / 2)
3450			QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3451			if not query.next():
3452				ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
3453				if not ok:
3454					ok, dbid = self.IdBetween(query, next_id, higher_id, "")
3455					if not ok:
3456						return str(higher_id)
3457				next_id = dbid
3458				QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3459			next_time = int(query.value(0))
3460			if get_floor:
3461				if target_time > next_time:
3462					lower_id = next_id
3463				else:
3464					higher_id = next_id
3465				if higher_id <= lower_id + 1:
3466					return str(higher_id)
3467			else:
3468				if target_time >= next_time:
3469					lower_id = next_id
3470				else:
3471					higher_id = next_id
3472				if higher_id <= lower_id + 1:
3473					return str(lower_id)
3474
3475	def ConvertRelativeTime(self, val):
3476		mult = 1
3477		suffix = val[-2:]
3478		if suffix == "ms":
3479			mult = 1000000
3480		elif suffix == "us":
3481			mult = 1000
3482		elif suffix == "ns":
3483			mult = 1
3484		else:
3485			return val
3486		val = val[:-2].strip()
3487		if not self.IsNumber(val):
3488			return val
3489		val = int(val) * mult
3490		if val >= 0:
3491			val += self.first_time
3492		else:
3493			val += self.last_time
3494		return str(val)
3495
3496	def ConvertTimeRange(self, vrange):
3497		if vrange[0] == "":
3498			vrange[0] = str(self.first_time)
3499		if vrange[1] == "":
3500			vrange[1] = str(self.last_time)
3501		vrange[0] = self.ConvertRelativeTime(vrange[0])
3502		vrange[1] = self.ConvertRelativeTime(vrange[1])
3503		if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3504			return False
3505		beg_range = max(int(vrange[0]), self.first_time)
3506		end_range = min(int(vrange[1]), self.last_time)
3507		if beg_range > self.last_time or end_range < self.first_time:
3508			return False
3509		vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
3510		vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
3511		return True
3512
3513	def AddTimeRange(self, value, ranges):
3514		n = value.count("-")
3515		if n == 1:
3516			pass
3517		elif n == 2:
3518			if value.split("-")[1].strip() == "":
3519				n = 1
3520		elif n == 3:
3521			n = 2
3522		else:
3523			return False
3524		pos = findnth(value, "-", n)
3525		vrange = [value[:pos].strip() ,value[pos+1:].strip()]
3526		if self.ConvertTimeRange(vrange):
3527			ranges.append(vrange)
3528			return True
3529		return False
3530
3531	def DoValidate(self, input_string):
3532		ranges = []
3533		for value in [x.strip() for x in input_string.split(",")]:
3534			if not self.AddTimeRange(value, ranges):
3535				return self.InvalidValue(value)
3536		ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3537		self.value = " OR ".join(ranges)
3538
3539# Report Dialog Base
3540
3541class ReportDialogBase(QDialog):
3542
3543	def __init__(self, glb, title, items, partial, parent=None):
3544		super(ReportDialogBase, self).__init__(parent)
3545
3546		self.glb = glb
3547
3548		self.report_vars = ReportVars()
3549
3550		self.setWindowTitle(title)
3551		self.setMinimumWidth(600)
3552
3553		self.data_items = [x(glb, self) for x in items]
3554
3555		self.partial = partial
3556
3557		self.grid = QGridLayout()
3558
3559		for row in xrange(len(self.data_items)):
3560			self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
3561			self.grid.addWidget(self.data_items[row].widget, row, 1)
3562
3563		self.status = QLabel()
3564
3565		self.ok_button = QPushButton("Ok", self)
3566		self.ok_button.setDefault(True)
3567		self.ok_button.released.connect(self.Ok)
3568		self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3569
3570		self.cancel_button = QPushButton("Cancel", self)
3571		self.cancel_button.released.connect(self.reject)
3572		self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3573
3574		self.hbox = QHBoxLayout()
3575		#self.hbox.addStretch()
3576		self.hbox.addWidget(self.status)
3577		self.hbox.addWidget(self.ok_button)
3578		self.hbox.addWidget(self.cancel_button)
3579
3580		self.vbox = QVBoxLayout()
3581		self.vbox.addLayout(self.grid)
3582		self.vbox.addLayout(self.hbox)
3583
3584		self.setLayout(self.vbox)
3585
3586	def Ok(self):
3587		vars = self.report_vars
3588		for d in self.data_items:
3589			if d.id == "REPORTNAME":
3590				vars.name = d.value
3591		if not vars.name:
3592			self.ShowMessage("Report name is required")
3593			return
3594		for d in self.data_items:
3595			if not d.IsValid():
3596				return
3597		for d in self.data_items[1:]:
3598			if d.id == "LIMIT":
3599				vars.limit = d.value
3600			elif len(d.value):
3601				if len(vars.where_clause):
3602					vars.where_clause += " AND "
3603				vars.where_clause += d.value
3604		if len(vars.where_clause):
3605			if self.partial:
3606				vars.where_clause = " AND ( " + vars.where_clause + " ) "
3607			else:
3608				vars.where_clause = " WHERE " + vars.where_clause + " "
3609		self.accept()
3610
3611	def ShowMessage(self, msg):
3612		self.status.setText("<font color=#FF0000>" + msg)
3613
3614	def ClearMessage(self):
3615		self.status.setText("")
3616
3617# Selected branch report creation dialog
3618
3619class SelectedBranchDialog(ReportDialogBase):
3620
3621	def __init__(self, glb, parent=None):
3622		title = "Selected Branches"
3623		items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
3624			 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
3625			 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
3626			 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
3627			 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
3628			 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
3629			 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
3630			 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
3631			 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
3632		super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
3633
3634# Event list
3635
3636def GetEventList(db):
3637	events = []
3638	query = QSqlQuery(db)
3639	QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
3640	while query.next():
3641		events.append(query.value(0))
3642	return events
3643
3644# Is a table selectable
3645
3646def IsSelectable(db, table, sql = "", columns = "*"):
3647	query = QSqlQuery(db)
3648	try:
3649		QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
3650	except:
3651		return False
3652	return True
3653
3654# SQL table data model item
3655
3656class SQLTableItem():
3657
3658	def __init__(self, row, data):
3659		self.row = row
3660		self.data = data
3661
3662	def getData(self, column):
3663		return self.data[column]
3664
3665# SQL table data model
3666
3667class SQLTableModel(TableModel):
3668
3669	progress = Signal(object)
3670
3671	def __init__(self, glb, sql, column_headers, parent=None):
3672		super(SQLTableModel, self).__init__(parent)
3673		self.glb = glb
3674		self.more = True
3675		self.populated = 0
3676		self.column_headers = column_headers
3677		self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
3678		self.fetcher.done.connect(self.Update)
3679		self.fetcher.Fetch(glb_chunk_sz)
3680
3681	def DisplayData(self, item, index):
3682		self.FetchIfNeeded(item.row)
3683		return item.getData(index.column())
3684
3685	def AddSample(self, data):
3686		child = SQLTableItem(self.populated, data)
3687		self.child_items.append(child)
3688		self.populated += 1
3689
3690	def Update(self, fetched):
3691		if not fetched:
3692			self.more = False
3693			self.progress.emit(0)
3694		child_count = self.child_count
3695		count = self.populated - child_count
3696		if count > 0:
3697			parent = QModelIndex()
3698			self.beginInsertRows(parent, child_count, child_count + count - 1)
3699			self.insertRows(child_count, count, parent)
3700			self.child_count += count
3701			self.endInsertRows()
3702			self.progress.emit(self.child_count)
3703
3704	def FetchMoreRecords(self, count):
3705		current = self.child_count
3706		if self.more:
3707			self.fetcher.Fetch(count)
3708		else:
3709			self.progress.emit(0)
3710		return current
3711
3712	def HasMoreRecords(self):
3713		return self.more
3714
3715	def columnCount(self, parent=None):
3716		return len(self.column_headers)
3717
3718	def columnHeader(self, column):
3719		return self.column_headers[column]
3720
3721	def SQLTableDataPrep(self, query, count):
3722		data = []
3723		for i in xrange(count):
3724			data.append(query.value(i))
3725		return data
3726
3727# SQL automatic table data model
3728
3729class SQLAutoTableModel(SQLTableModel):
3730
3731	def __init__(self, glb, table_name, parent=None):
3732		sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
3733		if table_name == "comm_threads_view":
3734			# For now, comm_threads_view has no id column
3735			sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
3736		column_headers = []
3737		query = QSqlQuery(glb.db)
3738		if glb.dbref.is_sqlite3:
3739			QueryExec(query, "PRAGMA table_info(" + table_name + ")")
3740			while query.next():
3741				column_headers.append(query.value(1))
3742			if table_name == "sqlite_master":
3743				sql = "SELECT * FROM " + table_name
3744		else:
3745			if table_name[:19] == "information_schema.":
3746				sql = "SELECT * FROM " + table_name
3747				select_table_name = table_name[19:]
3748				schema = "information_schema"
3749			else:
3750				select_table_name = table_name
3751				schema = "public"
3752			QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
3753			while query.next():
3754				column_headers.append(query.value(0))
3755		if pyside_version_1 and sys.version_info[0] == 3:
3756			if table_name == "samples_view":
3757				self.SQLTableDataPrep = self.samples_view_DataPrep
3758			if table_name == "samples":
3759				self.SQLTableDataPrep = self.samples_DataPrep
3760		super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
3761
3762	def samples_view_DataPrep(self, query, count):
3763		data = []
3764		data.append(query.value(0))
3765		# Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3766		data.append("{:>19}".format(query.value(1)))
3767		for i in xrange(2, count):
3768			data.append(query.value(i))
3769		return data
3770
3771	def samples_DataPrep(self, query, count):
3772		data = []
3773		for i in xrange(9):
3774			data.append(query.value(i))
3775		# Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3776		data.append("{:>19}".format(query.value(9)))
3777		for i in xrange(10, count):
3778			data.append(query.value(i))
3779		return data
3780
3781# Base class for custom ResizeColumnsToContents
3782
3783class ResizeColumnsToContentsBase(QObject):
3784
3785	def __init__(self, parent=None):
3786		super(ResizeColumnsToContentsBase, self).__init__(parent)
3787
3788	def ResizeColumnToContents(self, column, n):
3789		# Using the view's resizeColumnToContents() here is extrememly slow
3790		# so implement a crude alternative
3791		font = self.view.font()
3792		metrics = QFontMetrics(font)
3793		max = 0
3794		for row in xrange(n):
3795			val = self.data_model.child_items[row].data[column]
3796			len = metrics.width(str(val) + "MM")
3797			max = len if len > max else max
3798		val = self.data_model.columnHeader(column)
3799		len = metrics.width(str(val) + "MM")
3800		max = len if len > max else max
3801		self.view.setColumnWidth(column, max)
3802
3803	def ResizeColumnsToContents(self):
3804		n = min(self.data_model.child_count, 100)
3805		if n < 1:
3806			# No data yet, so connect a signal to notify when there is
3807			self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
3808			return
3809		columns = self.data_model.columnCount()
3810		for i in xrange(columns):
3811			self.ResizeColumnToContents(i, n)
3812
3813	def UpdateColumnWidths(self, *x):
3814		# This only needs to be done once, so disconnect the signal now
3815		self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
3816		self.ResizeColumnsToContents()
3817
3818# Convert value to CSV
3819
3820def ToCSValue(val):
3821	if '"' in val:
3822		val = val.replace('"', '""')
3823	if "," in val or '"' in val:
3824		val = '"' + val + '"'
3825	return val
3826
3827# Key to sort table model indexes by row / column, assuming fewer than 1000 columns
3828
3829glb_max_cols = 1000
3830
3831def RowColumnKey(a):
3832	return a.row() * glb_max_cols + a.column()
3833
3834# Copy selected table cells to clipboard
3835
3836def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
3837	indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
3838	idx_cnt = len(indexes)
3839	if not idx_cnt:
3840		return
3841	if idx_cnt == 1:
3842		with_hdr=False
3843	min_row = indexes[0].row()
3844	max_row = indexes[0].row()
3845	min_col = indexes[0].column()
3846	max_col = indexes[0].column()
3847	for i in indexes:
3848		min_row = min(min_row, i.row())
3849		max_row = max(max_row, i.row())
3850		min_col = min(min_col, i.column())
3851		max_col = max(max_col, i.column())
3852	if max_col > glb_max_cols:
3853		raise RuntimeError("glb_max_cols is too low")
3854	max_width = [0] * (1 + max_col - min_col)
3855	for i in indexes:
3856		c = i.column() - min_col
3857		max_width[c] = max(max_width[c], len(str(i.data())))
3858	text = ""
3859	pad = ""
3860	sep = ""
3861	if with_hdr:
3862		model = indexes[0].model()
3863		for col in range(min_col, max_col + 1):
3864			val = model.headerData(col, Qt.Horizontal)
3865			if as_csv:
3866				text += sep + ToCSValue(val)
3867				sep = ","
3868			else:
3869				c = col - min_col
3870				max_width[c] = max(max_width[c], len(val))
3871				width = max_width[c]
3872				align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
3873				if align & Qt.AlignRight:
3874					val = val.rjust(width)
3875				text += pad + sep + val
3876				pad = " " * (width - len(val))
3877				sep = "  "
3878		text += "\n"
3879		pad = ""
3880		sep = ""
3881	last_row = min_row
3882	for i in indexes:
3883		if i.row() > last_row:
3884			last_row = i.row()
3885			text += "\n"
3886			pad = ""
3887			sep = ""
3888		if as_csv:
3889			text += sep + ToCSValue(str(i.data()))
3890			sep = ","
3891		else:
3892			width = max_width[i.column() - min_col]
3893			if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3894				val = str(i.data()).rjust(width)
3895			else:
3896				val = str(i.data())
3897			text += pad + sep + val
3898			pad = " " * (width - len(val))
3899			sep = "  "
3900	QApplication.clipboard().setText(text)
3901
3902def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
3903	indexes = view.selectedIndexes()
3904	if not len(indexes):
3905		return
3906
3907	selection = view.selectionModel()
3908
3909	first = None
3910	for i in indexes:
3911		above = view.indexAbove(i)
3912		if not selection.isSelected(above):
3913			first = i
3914			break
3915
3916	if first is None:
3917		raise RuntimeError("CopyTreeCellsToClipboard internal error")
3918
3919	model = first.model()
3920	row_cnt = 0
3921	col_cnt = model.columnCount(first)
3922	max_width = [0] * col_cnt
3923
3924	indent_sz = 2
3925	indent_str = " " * indent_sz
3926
3927	expanded_mark_sz = 2
3928	if sys.version_info[0] == 3:
3929		expanded_mark = "\u25BC "
3930		not_expanded_mark = "\u25B6 "
3931	else:
3932		expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
3933		not_expanded_mark =  unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
3934	leaf_mark = "  "
3935
3936	if not as_csv:
3937		pos = first
3938		while True:
3939			row_cnt += 1
3940			row = pos.row()
3941			for c in range(col_cnt):
3942				i = pos.sibling(row, c)
3943				if c:
3944					n = len(str(i.data()))
3945				else:
3946					n = len(str(i.data()).strip())
3947					n += (i.internalPointer().level - 1) * indent_sz
3948					n += expanded_mark_sz
3949				max_width[c] = max(max_width[c], n)
3950			pos = view.indexBelow(pos)
3951			if not selection.isSelected(pos):
3952				break
3953
3954	text = ""
3955	pad = ""
3956	sep = ""
3957	if with_hdr:
3958		for c in range(col_cnt):
3959			val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
3960			if as_csv:
3961				text += sep + ToCSValue(val)
3962				sep = ","
3963			else:
3964				max_width[c] = max(max_width[c], len(val))
3965				width = max_width[c]
3966				align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
3967				if align & Qt.AlignRight:
3968					val = val.rjust(width)
3969				text += pad + sep + val
3970				pad = " " * (width - len(val))
3971				sep = "   "
3972		text += "\n"
3973		pad = ""
3974		sep = ""
3975
3976	pos = first
3977	while True:
3978		row = pos.row()
3979		for c in range(col_cnt):
3980			i = pos.sibling(row, c)
3981			val = str(i.data())
3982			if not c:
3983				if model.hasChildren(i):
3984					if view.isExpanded(i):
3985						mark = expanded_mark
3986					else:
3987						mark = not_expanded_mark
3988				else:
3989					mark = leaf_mark
3990				val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
3991			if as_csv:
3992				text += sep + ToCSValue(val)
3993				sep = ","
3994			else:
3995				width = max_width[c]
3996				if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3997					val = val.rjust(width)
3998				text += pad + sep + val
3999				pad = " " * (width - len(val))
4000				sep = "   "
4001		pos = view.indexBelow(pos)
4002		if not selection.isSelected(pos):
4003			break
4004		text = text.rstrip() + "\n"
4005		pad = ""
4006		sep = ""
4007
4008	QApplication.clipboard().setText(text)
4009
4010def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
4011	view.CopyCellsToClipboard(view, as_csv, with_hdr)
4012
4013def CopyCellsToClipboardHdr(view):
4014	CopyCellsToClipboard(view, False, True)
4015
4016def CopyCellsToClipboardCSV(view):
4017	CopyCellsToClipboard(view, True, True)
4018
4019# Context menu
4020
4021class ContextMenu(object):
4022
4023	def __init__(self, view):
4024		self.view = view
4025		self.view.setContextMenuPolicy(Qt.CustomContextMenu)
4026		self.view.customContextMenuRequested.connect(self.ShowContextMenu)
4027
4028	def ShowContextMenu(self, pos):
4029		menu = QMenu(self.view)
4030		self.AddActions(menu)
4031		menu.exec_(self.view.mapToGlobal(pos))
4032
4033	def AddCopy(self, menu):
4034		menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
4035		menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
4036
4037	def AddActions(self, menu):
4038		self.AddCopy(menu)
4039
4040class TreeContextMenu(ContextMenu):
4041
4042	def __init__(self, view):
4043		super(TreeContextMenu, self).__init__(view)
4044
4045	def AddActions(self, menu):
4046		i = self.view.currentIndex()
4047		text = str(i.data()).strip()
4048		if len(text):
4049			menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
4050		self.AddCopy(menu)
4051
4052# Table window
4053
4054class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4055
4056	def __init__(self, glb, table_name, parent=None):
4057		super(TableWindow, self).__init__(parent)
4058
4059		self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
4060
4061		self.model = QSortFilterProxyModel()
4062		self.model.setSourceModel(self.data_model)
4063
4064		self.view = QTableView()
4065		self.view.setModel(self.model)
4066		self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4067		self.view.verticalHeader().setVisible(False)
4068		self.view.sortByColumn(-1, Qt.AscendingOrder)
4069		self.view.setSortingEnabled(True)
4070		self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4071		self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4072
4073		self.ResizeColumnsToContents()
4074
4075		self.context_menu = ContextMenu(self.view)
4076
4077		self.find_bar = FindBar(self, self, True)
4078
4079		self.finder = ChildDataItemFinder(self.data_model)
4080
4081		self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4082
4083		self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4084
4085		self.setWidget(self.vbox.Widget())
4086
4087		AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
4088
4089	def Find(self, value, direction, pattern, context):
4090		self.view.setFocus()
4091		self.find_bar.Busy()
4092		self.finder.Find(value, direction, pattern, context, self.FindDone)
4093
4094	def FindDone(self, row):
4095		self.find_bar.Idle()
4096		if row >= 0:
4097			self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
4098		else:
4099			self.find_bar.NotFound()
4100
4101# Table list
4102
4103def GetTableList(glb):
4104	tables = []
4105	query = QSqlQuery(glb.db)
4106	if glb.dbref.is_sqlite3:
4107		QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
4108	else:
4109		QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
4110	while query.next():
4111		tables.append(query.value(0))
4112	if glb.dbref.is_sqlite3:
4113		tables.append("sqlite_master")
4114	else:
4115		tables.append("information_schema.tables")
4116		tables.append("information_schema.views")
4117		tables.append("information_schema.columns")
4118	return tables
4119
4120# Top Calls data model
4121
4122class TopCallsModel(SQLTableModel):
4123
4124	def __init__(self, glb, report_vars, parent=None):
4125		text = ""
4126		if not glb.dbref.is_sqlite3:
4127			text = "::text"
4128		limit = ""
4129		if len(report_vars.limit):
4130			limit = " LIMIT " + report_vars.limit
4131		sql = ("SELECT comm, pid, tid, name,"
4132			" CASE"
4133			" WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
4134			" ELSE short_name"
4135			" END AS dso,"
4136			" call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
4137			" CASE"
4138			" WHEN (calls.flags = 1) THEN 'no call'" + text +
4139			" WHEN (calls.flags = 2) THEN 'no return'" + text +
4140			" WHEN (calls.flags = 3) THEN 'no call/return'" + text +
4141			" ELSE ''" + text +
4142			" END AS flags"
4143			" FROM calls"
4144			" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
4145			" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
4146			" INNER JOIN dsos ON symbols.dso_id = dsos.id"
4147			" INNER JOIN comms ON calls.comm_id = comms.id"
4148			" INNER JOIN threads ON calls.thread_id = threads.id" +
4149			report_vars.where_clause +
4150			" ORDER BY elapsed_time DESC" +
4151			limit
4152			)
4153		column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
4154		self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
4155		super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
4156
4157	def columnAlignment(self, column):
4158		return self.alignment[column]
4159
4160# Top Calls report creation dialog
4161
4162class TopCallsDialog(ReportDialogBase):
4163
4164	def __init__(self, glb, parent=None):
4165		title = "Top Calls by Elapsed Time"
4166		items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
4167			 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
4168			 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
4169			 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
4170			 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
4171			 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
4172			 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
4173			 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
4174		super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
4175
4176# Top Calls window
4177
4178class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4179
4180	def __init__(self, glb, report_vars, parent=None):
4181		super(TopCallsWindow, self).__init__(parent)
4182
4183		self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
4184		self.model = self.data_model
4185
4186		self.view = QTableView()
4187		self.view.setModel(self.model)
4188		self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4189		self.view.verticalHeader().setVisible(False)
4190		self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4191		self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4192
4193		self.context_menu = ContextMenu(self.view)
4194
4195		self.ResizeColumnsToContents()
4196
4197		self.find_bar = FindBar(self, self, True)
4198
4199		self.finder = ChildDataItemFinder(self.model)
4200
4201		self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4202
4203		self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4204
4205		self.setWidget(self.vbox.Widget())
4206
4207		AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
4208
4209	def Find(self, value, direction, pattern, context):
4210		self.view.setFocus()
4211		self.find_bar.Busy()
4212		self.finder.Find(value, direction, pattern, context, self.FindDone)
4213
4214	def FindDone(self, row):
4215		self.find_bar.Idle()
4216		if row >= 0:
4217			self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
4218		else:
4219			self.find_bar.NotFound()
4220
4221# Action Definition
4222
4223def CreateAction(label, tip, callback, parent=None, shortcut=None):
4224	action = QAction(label, parent)
4225	if shortcut != None:
4226		action.setShortcuts(shortcut)
4227	action.setStatusTip(tip)
4228	action.triggered.connect(callback)
4229	return action
4230
4231# Typical application actions
4232
4233def CreateExitAction(app, parent=None):
4234	return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
4235
4236# Typical MDI actions
4237
4238def CreateCloseActiveWindowAction(mdi_area):
4239	return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
4240
4241def CreateCloseAllWindowsAction(mdi_area):
4242	return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
4243
4244def CreateTileWindowsAction(mdi_area):
4245	return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
4246
4247def CreateCascadeWindowsAction(mdi_area):
4248	return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
4249
4250def CreateNextWindowAction(mdi_area):
4251	return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
4252
4253def CreatePreviousWindowAction(mdi_area):
4254	return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
4255
4256# Typical MDI window menu
4257
4258class WindowMenu():
4259
4260	def __init__(self, mdi_area, menu):
4261		self.mdi_area = mdi_area
4262		self.window_menu = menu.addMenu("&Windows")
4263		self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
4264		self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
4265		self.tile_windows = CreateTileWindowsAction(mdi_area)
4266		self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
4267		self.next_window = CreateNextWindowAction(mdi_area)
4268		self.previous_window = CreatePreviousWindowAction(mdi_area)
4269		self.window_menu.aboutToShow.connect(self.Update)
4270
4271	def Update(self):
4272		self.window_menu.clear()
4273		sub_window_count = len(self.mdi_area.subWindowList())
4274		have_sub_windows = sub_window_count != 0
4275		self.close_active_window.setEnabled(have_sub_windows)
4276		self.close_all_windows.setEnabled(have_sub_windows)
4277		self.tile_windows.setEnabled(have_sub_windows)
4278		self.cascade_windows.setEnabled(have_sub_windows)
4279		self.next_window.setEnabled(have_sub_windows)
4280		self.previous_window.setEnabled(have_sub_windows)
4281		self.window_menu.addAction(self.close_active_window)
4282		self.window_menu.addAction(self.close_all_windows)
4283		self.window_menu.addSeparator()
4284		self.window_menu.addAction(self.tile_windows)
4285		self.window_menu.addAction(self.cascade_windows)
4286		self.window_menu.addSeparator()
4287		self.window_menu.addAction(self.next_window)
4288		self.window_menu.addAction(self.previous_window)
4289		if sub_window_count == 0:
4290			return
4291		self.window_menu.addSeparator()
4292		nr = 1
4293		for sub_window in self.mdi_area.subWindowList():
4294			label = str(nr) + " " + sub_window.name
4295			if nr < 10:
4296				label = "&" + label
4297			action = self.window_menu.addAction(label)
4298			action.setCheckable(True)
4299			action.setChecked(sub_window == self.mdi_area.activeSubWindow())
4300			action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
4301			self.window_menu.addAction(action)
4302			nr += 1
4303
4304	def setActiveSubWindow(self, nr):
4305		self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
4306
4307# Help text
4308
4309glb_help_text = """
4310<h1>Contents</h1>
4311<style>
4312p.c1 {
4313    text-indent: 40px;
4314}
4315p.c2 {
4316    text-indent: 80px;
4317}
4318}
4319</style>
4320<p class=c1><a href=#reports>1. Reports</a></p>
4321<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
4322<p class=c2><a href=#calltree>1.2 Call Tree</a></p>
4323<p class=c2><a href=#allbranches>1.3 All branches</a></p>
4324<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
4325<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
4326<p class=c1><a href=#charts>2. Charts</a></p>
4327<p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
4328<p class=c1><a href=#tables>3. Tables</a></p>
4329<h1 id=reports>1. Reports</h1>
4330<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
4331The result is a GUI window with a tree representing a context-sensitive
4332call-graph. Expanding a couple of levels of the tree and adjusting column
4333widths to suit will display something like:
4334<pre>
4335                                         Call Graph: pt_example
4336Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
4337v- ls
4338    v- 2638:2638
4339        v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
4340          |- unknown               unknown       1        13198     0.1              1              0.0
4341          >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
4342          >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
4343          v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
4344             >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
4345             >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
4346             >- __libc_csu_init    ls            1        10354     0.1             10              0.0
4347             |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
4348             v- main               ls            1      8182043    99.6         180254             99.9
4349</pre>
4350<h3>Points to note:</h3>
4351<ul>
4352<li>The top level is a command name (comm)</li>
4353<li>The next level is a thread (pid:tid)</li>
4354<li>Subsequent levels are functions</li>
4355<li>'Count' is the number of calls</li>
4356<li>'Time' is the elapsed time until the function returns</li>
4357<li>Percentages are relative to the level above</li>
4358<li>'Branch Count' is the total number of branches for that function and all functions that it calls
4359</ul>
4360<h3>Find</h3>
4361Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
4362The pattern matching symbols are ? for any character and * for zero or more characters.
4363<h2 id=calltree>1.2 Call Tree</h2>
4364The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
4365Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
4366<h2 id=allbranches>1.3 All branches</h2>
4367The All branches report displays all branches in chronological order.
4368Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
4369<h3>Disassembly</h3>
4370Open a branch to display disassembly. This only works if:
4371<ol>
4372<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
4373<li>The object code is available. Currently, only the perf build ID cache is searched for object code.
4374The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
4375One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
4376or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
4377</ol>
4378<h4 id=xed>Intel XED Setup</h4>
4379To use Intel XED, libxed.so must be present.  To build and install libxed.so:
4380<pre>
4381git clone https://github.com/intelxed/mbuild.git mbuild
4382git clone https://github.com/intelxed/xed
4383cd xed
4384./mfile.py --share
4385sudo ./mfile.py --prefix=/usr/local install
4386sudo ldconfig
4387</pre>
4388<h3>Instructions per Cycle (IPC)</h3>
4389If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
4390<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
4391Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
4392In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
4393since the previous displayed 'IPC'.
4394<h3>Find</h3>
4395Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4396Refer to Python documentation for the regular expression syntax.
4397All columns are searched, but only currently fetched rows are searched.
4398<h2 id=selectedbranches>1.4 Selected branches</h2>
4399This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
4400by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4401<h3>1.4.1 Time ranges</h3>
4402The time ranges hint text shows the total time range. Relative time ranges can also be entered in
4403ms, us or ns. Also, negative values are relative to the end of trace.  Examples:
4404<pre>
4405	81073085947329-81073085958238	From 81073085947329 to 81073085958238
4406	100us-200us		From 100us to 200us
4407	10ms-			From 10ms to the end
4408	-100ns			The first 100ns
4409	-10ms-			The last 10ms
4410</pre>
4411N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
4412<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
4413The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
4414The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4415If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
4416<h1 id=charts>2. Charts</h1>
4417<h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
4418This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
4419<h3>Features</h3>
4420<ol>
4421<li>Mouse over to highight the task and show the time</li>
4422<li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
4423<li>Go back and forward by pressing the arrow buttons</li>
4424<li>If call information is available, right-click to show a call tree opened to that task and time.
4425Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
4426</li>
4427</ol>
4428<h3>Important</h3>
4429The graph can be misleading in the following respects:
4430<ol>
4431<li>The graph shows the first task on each CPU as running from the beginning of the time range.
4432Because tracing might start on different CPUs at different times, that is not necessarily the case.
4433Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4434<li>Similarly, the last task on each CPU can be showing running longer than it really was.
4435Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4436<li>When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window</li>
4437</ol>
4438<h1 id=tables>3. Tables</h1>
4439The Tables menu shows all tables and views in the database. Most tables have an associated view
4440which displays the information in a more friendly way. Not all data for large tables is fetched
4441immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
4442but that can be slow for large tables.
4443<p>There are also tables of database meta-information.
4444For SQLite3 databases, the sqlite_master table is included.
4445For PostgreSQL databases, information_schema.tables/views/columns are included.
4446<h3>Find</h3>
4447Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4448Refer to Python documentation for the regular expression syntax.
4449All columns are searched, but only currently fetched rows are searched.
4450<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
4451will go to the next/previous result in id order, instead of display order.
4452"""
4453
4454# Help window
4455
4456class HelpWindow(QMdiSubWindow):
4457
4458	def __init__(self, glb, parent=None):
4459		super(HelpWindow, self).__init__(parent)
4460
4461		self.text = QTextBrowser()
4462		self.text.setHtml(glb_help_text)
4463		self.text.setReadOnly(True)
4464		self.text.setOpenExternalLinks(True)
4465
4466		self.setWidget(self.text)
4467
4468		AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
4469
4470# Main window that only displays the help text
4471
4472class HelpOnlyWindow(QMainWindow):
4473
4474	def __init__(self, parent=None):
4475		super(HelpOnlyWindow, self).__init__(parent)
4476
4477		self.setMinimumSize(200, 100)
4478		self.resize(800, 600)
4479		self.setWindowTitle("Exported SQL Viewer Help")
4480		self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
4481
4482		self.text = QTextBrowser()
4483		self.text.setHtml(glb_help_text)
4484		self.text.setReadOnly(True)
4485		self.text.setOpenExternalLinks(True)
4486
4487		self.setCentralWidget(self.text)
4488
4489# PostqreSQL server version
4490
4491def PostqreSQLServerVersion(db):
4492	query = QSqlQuery(db)
4493	QueryExec(query, "SELECT VERSION()")
4494	if query.next():
4495		v_str = query.value(0)
4496		v_list = v_str.strip().split(" ")
4497		if v_list[0] == "PostgreSQL" and v_list[2] == "on":
4498			return v_list[1]
4499		return v_str
4500	return "Unknown"
4501
4502# SQLite version
4503
4504def SQLiteVersion(db):
4505	query = QSqlQuery(db)
4506	QueryExec(query, "SELECT sqlite_version()")
4507	if query.next():
4508		return query.value(0)
4509	return "Unknown"
4510
4511# About dialog
4512
4513class AboutDialog(QDialog):
4514
4515	def __init__(self, glb, parent=None):
4516		super(AboutDialog, self).__init__(parent)
4517
4518		self.setWindowTitle("About Exported SQL Viewer")
4519		self.setMinimumWidth(300)
4520
4521		pyside_version = "1" if pyside_version_1 else "2"
4522
4523		text = "<pre>"
4524		text += "Python version:     " + sys.version.split(" ")[0] + "\n"
4525		text += "PySide version:     " + pyside_version + "\n"
4526		text += "Qt version:         " + qVersion() + "\n"
4527		if glb.dbref.is_sqlite3:
4528			text += "SQLite version:     " + SQLiteVersion(glb.db) + "\n"
4529		else:
4530			text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
4531		text += "</pre>"
4532
4533		self.text = QTextBrowser()
4534		self.text.setHtml(text)
4535		self.text.setReadOnly(True)
4536		self.text.setOpenExternalLinks(True)
4537
4538		self.vbox = QVBoxLayout()
4539		self.vbox.addWidget(self.text)
4540
4541		self.setLayout(self.vbox)
4542
4543# Font resize
4544
4545def ResizeFont(widget, diff):
4546	font = widget.font()
4547	sz = font.pointSize()
4548	font.setPointSize(sz + diff)
4549	widget.setFont(font)
4550
4551def ShrinkFont(widget):
4552	ResizeFont(widget, -1)
4553
4554def EnlargeFont(widget):
4555	ResizeFont(widget, 1)
4556
4557# Unique name for sub-windows
4558
4559def NumberedWindowName(name, nr):
4560	if nr > 1:
4561		name += " <" + str(nr) + ">"
4562	return name
4563
4564def UniqueSubWindowName(mdi_area, name):
4565	nr = 1
4566	while True:
4567		unique_name = NumberedWindowName(name, nr)
4568		ok = True
4569		for sub_window in mdi_area.subWindowList():
4570			if sub_window.name == unique_name:
4571				ok = False
4572				break
4573		if ok:
4574			return unique_name
4575		nr += 1
4576
4577# Add a sub-window
4578
4579def AddSubWindow(mdi_area, sub_window, name):
4580	unique_name = UniqueSubWindowName(mdi_area, name)
4581	sub_window.setMinimumSize(200, 100)
4582	sub_window.resize(800, 600)
4583	sub_window.setWindowTitle(unique_name)
4584	sub_window.setAttribute(Qt.WA_DeleteOnClose)
4585	sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
4586	sub_window.name = unique_name
4587	mdi_area.addSubWindow(sub_window)
4588	sub_window.show()
4589
4590# Main window
4591
4592class MainWindow(QMainWindow):
4593
4594	def __init__(self, glb, parent=None):
4595		super(MainWindow, self).__init__(parent)
4596
4597		self.glb = glb
4598
4599		self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
4600		self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
4601		self.setMinimumSize(200, 100)
4602
4603		self.mdi_area = QMdiArea()
4604		self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4605		self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4606
4607		self.setCentralWidget(self.mdi_area)
4608
4609		menu = self.menuBar()
4610
4611		file_menu = menu.addMenu("&File")
4612		file_menu.addAction(CreateExitAction(glb.app, self))
4613
4614		edit_menu = menu.addMenu("&Edit")
4615		edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
4616		edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
4617		edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
4618		edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
4619		edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
4620		edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
4621
4622		reports_menu = menu.addMenu("&Reports")
4623		if IsSelectable(glb.db, "calls"):
4624			reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
4625
4626		if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
4627			reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
4628
4629		self.EventMenu(GetEventList(glb.db), reports_menu)
4630
4631		if IsSelectable(glb.db, "calls"):
4632			reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
4633
4634		if IsSelectable(glb.db, "context_switches"):
4635			charts_menu = menu.addMenu("&Charts")
4636			charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
4637
4638		self.TableMenu(GetTableList(glb), menu)
4639
4640		self.window_menu = WindowMenu(self.mdi_area, menu)
4641
4642		help_menu = menu.addMenu("&Help")
4643		help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
4644		help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
4645
4646	def Try(self, fn):
4647		win = self.mdi_area.activeSubWindow()
4648		if win:
4649			try:
4650				fn(win.view)
4651			except:
4652				pass
4653
4654	def CopyToClipboard(self):
4655		self.Try(CopyCellsToClipboardHdr)
4656
4657	def CopyToClipboardCSV(self):
4658		self.Try(CopyCellsToClipboardCSV)
4659
4660	def Find(self):
4661		win = self.mdi_area.activeSubWindow()
4662		if win:
4663			try:
4664				win.find_bar.Activate()
4665			except:
4666				pass
4667
4668	def FetchMoreRecords(self):
4669		win = self.mdi_area.activeSubWindow()
4670		if win:
4671			try:
4672				win.fetch_bar.Activate()
4673			except:
4674				pass
4675
4676	def ShrinkFont(self):
4677		self.Try(ShrinkFont)
4678
4679	def EnlargeFont(self):
4680		self.Try(EnlargeFont)
4681
4682	def EventMenu(self, events, reports_menu):
4683		branches_events = 0
4684		for event in events:
4685			event = event.split(":")[0]
4686			if event == "branches":
4687				branches_events += 1
4688		dbid = 0
4689		for event in events:
4690			dbid += 1
4691			event = event.split(":")[0]
4692			if event == "branches":
4693				label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
4694				reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
4695				label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
4696				reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
4697
4698	def TimeChartByCPU(self):
4699		TimeChartByCPUWindow(self.glb, self)
4700
4701	def TableMenu(self, tables, menu):
4702		table_menu = menu.addMenu("&Tables")
4703		for table in tables:
4704			table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
4705
4706	def NewCallGraph(self):
4707		CallGraphWindow(self.glb, self)
4708
4709	def NewCallTree(self):
4710		CallTreeWindow(self.glb, self)
4711
4712	def NewTopCalls(self):
4713		dialog = TopCallsDialog(self.glb, self)
4714		ret = dialog.exec_()
4715		if ret:
4716			TopCallsWindow(self.glb, dialog.report_vars, self)
4717
4718	def NewBranchView(self, event_id):
4719		BranchWindow(self.glb, event_id, ReportVars(), self)
4720
4721	def NewSelectedBranchView(self, event_id):
4722		dialog = SelectedBranchDialog(self.glb, self)
4723		ret = dialog.exec_()
4724		if ret:
4725			BranchWindow(self.glb, event_id, dialog.report_vars, self)
4726
4727	def NewTableView(self, table_name):
4728		TableWindow(self.glb, table_name, self)
4729
4730	def Help(self):
4731		HelpWindow(self.glb, self)
4732
4733	def About(self):
4734		dialog = AboutDialog(self.glb, self)
4735		dialog.exec_()
4736
4737# XED Disassembler
4738
4739class xed_state_t(Structure):
4740
4741	_fields_ = [
4742		("mode", c_int),
4743		("width", c_int)
4744	]
4745
4746class XEDInstruction():
4747
4748	def __init__(self, libxed):
4749		# Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
4750		xedd_t = c_byte * 512
4751		self.xedd = xedd_t()
4752		self.xedp = addressof(self.xedd)
4753		libxed.xed_decoded_inst_zero(self.xedp)
4754		self.state = xed_state_t()
4755		self.statep = addressof(self.state)
4756		# Buffer for disassembled instruction text
4757		self.buffer = create_string_buffer(256)
4758		self.bufferp = addressof(self.buffer)
4759
4760class LibXED():
4761
4762	def __init__(self):
4763		try:
4764			self.libxed = CDLL("libxed.so")
4765		except:
4766			self.libxed = None
4767		if not self.libxed:
4768			self.libxed = CDLL("/usr/local/lib/libxed.so")
4769
4770		self.xed_tables_init = self.libxed.xed_tables_init
4771		self.xed_tables_init.restype = None
4772		self.xed_tables_init.argtypes = []
4773
4774		self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
4775		self.xed_decoded_inst_zero.restype = None
4776		self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
4777
4778		self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
4779		self.xed_operand_values_set_mode.restype = None
4780		self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
4781
4782		self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
4783		self.xed_decoded_inst_zero_keep_mode.restype = None
4784		self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
4785
4786		self.xed_decode = self.libxed.xed_decode
4787		self.xed_decode.restype = c_int
4788		self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
4789
4790		self.xed_format_context = self.libxed.xed_format_context
4791		self.xed_format_context.restype = c_uint
4792		self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
4793
4794		self.xed_tables_init()
4795
4796	def Instruction(self):
4797		return XEDInstruction(self)
4798
4799	def SetMode(self, inst, mode):
4800		if mode:
4801			inst.state.mode = 4 # 32-bit
4802			inst.state.width = 4 # 4 bytes
4803		else:
4804			inst.state.mode = 1 # 64-bit
4805			inst.state.width = 8 # 8 bytes
4806		self.xed_operand_values_set_mode(inst.xedp, inst.statep)
4807
4808	def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
4809		self.xed_decoded_inst_zero_keep_mode(inst.xedp)
4810		err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
4811		if err:
4812			return 0, ""
4813		# Use AT&T mode (2), alternative is Intel (3)
4814		ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
4815		if not ok:
4816			return 0, ""
4817		if sys.version_info[0] == 2:
4818			result = inst.buffer.value
4819		else:
4820			result = inst.buffer.value.decode()
4821		# Return instruction length and the disassembled instruction text
4822		# For now, assume the length is in byte 166
4823		return inst.xedd[166], result
4824
4825def TryOpen(file_name):
4826	try:
4827		return open(file_name, "rb")
4828	except:
4829		return None
4830
4831def Is64Bit(f):
4832	result = sizeof(c_void_p)
4833	# ELF support only
4834	pos = f.tell()
4835	f.seek(0)
4836	header = f.read(7)
4837	f.seek(pos)
4838	magic = header[0:4]
4839	if sys.version_info[0] == 2:
4840		eclass = ord(header[4])
4841		encoding = ord(header[5])
4842		version = ord(header[6])
4843	else:
4844		eclass = header[4]
4845		encoding = header[5]
4846		version = header[6]
4847	if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
4848		result = True if eclass == 2 else False
4849	return result
4850
4851# Global data
4852
4853class Glb():
4854
4855	def __init__(self, dbref, db, dbname):
4856		self.dbref = dbref
4857		self.db = db
4858		self.dbname = dbname
4859		self.home_dir = os.path.expanduser("~")
4860		self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
4861		if self.buildid_dir:
4862			self.buildid_dir += "/.build-id/"
4863		else:
4864			self.buildid_dir = self.home_dir + "/.debug/.build-id/"
4865		self.app = None
4866		self.mainwindow = None
4867		self.instances_to_shutdown_on_exit = weakref.WeakSet()
4868		try:
4869			self.disassembler = LibXED()
4870			self.have_disassembler = True
4871		except:
4872			self.have_disassembler = False
4873		self.host_machine_id = 0
4874		self.host_start_time = 0
4875		self.host_finish_time = 0
4876
4877	def FileFromBuildId(self, build_id):
4878		file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
4879		return TryOpen(file_name)
4880
4881	def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
4882		# Assume current machine i.e. no support for virtualization
4883		if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
4884			file_name = os.getenv("PERF_KCORE")
4885			f = TryOpen(file_name) if file_name else None
4886			if f:
4887				return f
4888			# For now, no special handling if long_name is /proc/kcore
4889			f = TryOpen(long_name)
4890			if f:
4891				return f
4892		f = self.FileFromBuildId(build_id)
4893		if f:
4894			return f
4895		return None
4896
4897	def AddInstanceToShutdownOnExit(self, instance):
4898		self.instances_to_shutdown_on_exit.add(instance)
4899
4900	# Shutdown any background processes or threads
4901	def ShutdownInstances(self):
4902		for x in self.instances_to_shutdown_on_exit:
4903			try:
4904				x.Shutdown()
4905			except:
4906				pass
4907
4908	def GetHostMachineId(self):
4909		query = QSqlQuery(self.db)
4910		QueryExec(query, "SELECT id FROM machines WHERE pid = -1")
4911		if query.next():
4912			self.host_machine_id = query.value(0)
4913		else:
4914			self.host_machine_id = 0
4915		return self.host_machine_id
4916
4917	def HostMachineId(self):
4918		if self.host_machine_id:
4919			return self.host_machine_id
4920		return self.GetHostMachineId()
4921
4922	def SelectValue(self, sql):
4923		query = QSqlQuery(self.db)
4924		try:
4925			QueryExec(query, sql)
4926		except:
4927			return None
4928		if query.next():
4929			return Decimal(query.value(0))
4930		return None
4931
4932	def SwitchesMinTime(self, machine_id):
4933		return self.SelectValue("SELECT time"
4934					" FROM context_switches"
4935					" WHERE time != 0 AND machine_id = " + str(machine_id) +
4936					" ORDER BY id LIMIT 1")
4937
4938	def SwitchesMaxTime(self, machine_id):
4939		return self.SelectValue("SELECT time"
4940					" FROM context_switches"
4941					" WHERE time != 0 AND machine_id = " + str(machine_id) +
4942					" ORDER BY id DESC LIMIT 1")
4943
4944	def SamplesMinTime(self, machine_id):
4945		return self.SelectValue("SELECT time"
4946					" FROM samples"
4947					" WHERE time != 0 AND machine_id = " + str(machine_id) +
4948					" ORDER BY id LIMIT 1")
4949
4950	def SamplesMaxTime(self, machine_id):
4951		return self.SelectValue("SELECT time"
4952					" FROM samples"
4953					" WHERE time != 0 AND machine_id = " + str(machine_id) +
4954					" ORDER BY id DESC LIMIT 1")
4955
4956	def CallsMinTime(self, machine_id):
4957		return self.SelectValue("SELECT calls.call_time"
4958					" FROM calls"
4959					" INNER JOIN threads ON threads.thread_id = calls.thread_id"
4960					" WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +
4961					" ORDER BY calls.id LIMIT 1")
4962
4963	def CallsMaxTime(self, machine_id):
4964		return self.SelectValue("SELECT calls.return_time"
4965					" FROM calls"
4966					" INNER JOIN threads ON threads.thread_id = calls.thread_id"
4967					" WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +
4968					" ORDER BY calls.return_time DESC LIMIT 1")
4969
4970	def GetStartTime(self, machine_id):
4971		t0 = self.SwitchesMinTime(machine_id)
4972		t1 = self.SamplesMinTime(machine_id)
4973		t2 = self.CallsMinTime(machine_id)
4974		if t0 is None or (not(t1 is None) and t1 < t0):
4975			t0 = t1
4976		if t0 is None or (not(t2 is None) and t2 < t0):
4977			t0 = t2
4978		return t0
4979
4980	def GetFinishTime(self, machine_id):
4981		t0 = self.SwitchesMaxTime(machine_id)
4982		t1 = self.SamplesMaxTime(machine_id)
4983		t2 = self.CallsMaxTime(machine_id)
4984		if t0 is None or (not(t1 is None) and t1 > t0):
4985			t0 = t1
4986		if t0 is None or (not(t2 is None) and t2 > t0):
4987			t0 = t2
4988		return t0
4989
4990	def HostStartTime(self):
4991		if self.host_start_time:
4992			return self.host_start_time
4993		self.host_start_time = self.GetStartTime(self.HostMachineId())
4994		return self.host_start_time
4995
4996	def HostFinishTime(self):
4997		if self.host_finish_time:
4998			return self.host_finish_time
4999		self.host_finish_time = self.GetFinishTime(self.HostMachineId())
5000		return self.host_finish_time
5001
5002	def StartTime(self, machine_id):
5003		if machine_id == self.HostMachineId():
5004			return self.HostStartTime()
5005		return self.GetStartTime(machine_id)
5006
5007	def FinishTime(self, machine_id):
5008		if machine_id == self.HostMachineId():
5009			return self.HostFinishTime()
5010		return self.GetFinishTime(machine_id)
5011
5012# Database reference
5013
5014class DBRef():
5015
5016	def __init__(self, is_sqlite3, dbname):
5017		self.is_sqlite3 = is_sqlite3
5018		self.dbname = dbname
5019		self.TRUE = "TRUE"
5020		self.FALSE = "FALSE"
5021		# SQLite prior to version 3.23 does not support TRUE and FALSE
5022		if self.is_sqlite3:
5023			self.TRUE = "1"
5024			self.FALSE = "0"
5025
5026	def Open(self, connection_name):
5027		dbname = self.dbname
5028		if self.is_sqlite3:
5029			db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
5030		else:
5031			db = QSqlDatabase.addDatabase("QPSQL", connection_name)
5032			opts = dbname.split()
5033			for opt in opts:
5034				if "=" in opt:
5035					opt = opt.split("=")
5036					if opt[0] == "hostname":
5037						db.setHostName(opt[1])
5038					elif opt[0] == "port":
5039						db.setPort(int(opt[1]))
5040					elif opt[0] == "username":
5041						db.setUserName(opt[1])
5042					elif opt[0] == "password":
5043						db.setPassword(opt[1])
5044					elif opt[0] == "dbname":
5045						dbname = opt[1]
5046				else:
5047					dbname = opt
5048
5049		db.setDatabaseName(dbname)
5050		if not db.open():
5051			raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
5052		return db, dbname
5053
5054# Main
5055
5056def Main():
5057	usage_str =	"exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
5058			"   or: exported-sql-viewer.py --help-only"
5059	ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
5060	ap.add_argument("--pyside-version-1", action='store_true')
5061	ap.add_argument("dbname", nargs="?")
5062	ap.add_argument("--help-only", action='store_true')
5063	args = ap.parse_args()
5064
5065	if args.help_only:
5066		app = QApplication(sys.argv)
5067		mainwindow = HelpOnlyWindow()
5068		mainwindow.show()
5069		err = app.exec_()
5070		sys.exit(err)
5071
5072	dbname = args.dbname
5073	if dbname is None:
5074		ap.print_usage()
5075		print("Too few arguments")
5076		sys.exit(1)
5077
5078	is_sqlite3 = False
5079	try:
5080		f = open(dbname, "rb")
5081		if f.read(15) == b'SQLite format 3':
5082			is_sqlite3 = True
5083		f.close()
5084	except:
5085		pass
5086
5087	dbref = DBRef(is_sqlite3, dbname)
5088	db, dbname = dbref.Open("main")
5089	glb = Glb(dbref, db, dbname)
5090	app = QApplication(sys.argv)
5091	glb.app = app
5092	mainwindow = MainWindow(glb)
5093	glb.mainwindow = mainwindow
5094	mainwindow.show()
5095	err = app.exec_()
5096	glb.ShutdownInstances()
5097	db.close()
5098	sys.exit(err)
5099
5100if __name__ == "__main__":
5101	Main()
5102