Linux Audio

Check our new training course

Linux debugging, profiling, tracing and performance analysis training

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