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