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