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