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