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