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