Back to home page

OSCL-LXR

 
 

    


0001 #!/usr/bin/env python
0002 # SPDX-License-Identifier: GPL-2.0
0003 # exported-sql-viewer.py: view data from sql database
0004 # Copyright (c) 2014-2018, Intel Corporation.
0005 
0006 # To use this script you will need to have exported data using either the
0007 # export-to-sqlite.py or the export-to-postgresql.py script.  Refer to those
0008 # scripts for details.
0009 #
0010 # Following on from the example in the export scripts, a
0011 # call-graph can be displayed for the pt_example database like this:
0012 #
0013 #   python tools/perf/scripts/python/exported-sql-viewer.py pt_example
0014 #
0015 # Note that for PostgreSQL, this script supports connecting to remote databases
0016 # by setting hostname, port, username, password, and dbname e.g.
0017 #
0018 #   python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
0019 #
0020 # The result is a GUI window with a tree representing a context-sensitive
0021 # call-graph.  Expanding a couple of levels of the tree and adjusting column
0022 # widths to suit will display something like:
0023 #
0024 #                                         Call Graph: pt_example
0025 # Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
0026 # v- ls
0027 #     v- 2638:2638
0028 #         v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
0029 #           |- unknown               unknown       1        13198     0.1              1              0.0
0030 #           >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
0031 #           >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
0032 #           v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
0033 #              >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
0034 #              >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
0035 #              >- __libc_csu_init    ls            1        10354     0.1             10              0.0
0036 #              |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
0037 #              v- main               ls            1      8182043    99.6         180254             99.9
0038 #
0039 # Points to note:
0040 #   The top level is a command name (comm)
0041 #   The next level is a thread (pid:tid)
0042 #   Subsequent levels are functions
0043 #   'Count' is the number of calls
0044 #   'Time' is the elapsed time until the function returns
0045 #   Percentages are relative to the level above
0046 #   'Branch Count' is the total number of branches for that function and all
0047 #       functions that it calls
0048 
0049 # There is also a "All branches" report, which displays branches and
0050 # possibly disassembly.  However, presently, the only supported disassembler is
0051 # Intel XED, and additionally the object code must be present in perf build ID
0052 # cache. To use Intel XED, libxed.so must be present. To build and install
0053 # libxed.so:
0054 #            git clone https://github.com/intelxed/mbuild.git mbuild
0055 #            git clone https://github.com/intelxed/xed
0056 #            cd xed
0057 #            ./mfile.py --share
0058 #            sudo ./mfile.py --prefix=/usr/local install
0059 #            sudo ldconfig
0060 #
0061 # Example report:
0062 #
0063 # Time           CPU  Command  PID    TID    Branch Type            In Tx  Branch
0064 # 8107675239590  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
0065 #                                                                              7fab593ea260 48 89 e7                                        mov %rsp, %rdi
0066 # 8107675239899  2    ls       22011  22011  hardware interrupt     No         7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
0067 # 8107675241900  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
0068 #                                                                              7fab593ea260 48 89 e7                                        mov %rsp, %rdi
0069 #                                                                              7fab593ea263 e8 c8 06 00 00                                  callq  0x7fab593ea930
0070 # 8107675241900  2    ls       22011  22011  call                   No         7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
0071 #                                                                              7fab593ea930 55                                              pushq  %rbp
0072 #                                                                              7fab593ea931 48 89 e5                                        mov %rsp, %rbp
0073 #                                                                              7fab593ea934 41 57                                           pushq  %r15
0074 #                                                                              7fab593ea936 41 56                                           pushq  %r14
0075 #                                                                              7fab593ea938 41 55                                           pushq  %r13
0076 #                                                                              7fab593ea93a 41 54                                           pushq  %r12
0077 #                                                                              7fab593ea93c 53                                              pushq  %rbx
0078 #                                                                              7fab593ea93d 48 89 fb                                        mov %rdi, %rbx
0079 #                                                                              7fab593ea940 48 83 ec 68                                     sub $0x68, %rsp
0080 #                                                                              7fab593ea944 0f 31                                           rdtsc
0081 #                                                                              7fab593ea946 48 c1 e2 20                                     shl $0x20, %rdx
0082 #                                                                              7fab593ea94a 89 c0                                           mov %eax, %eax
0083 #                                                                              7fab593ea94c 48 09 c2                                        or %rax, %rdx
0084 #                                                                              7fab593ea94f 48 8b 05 1a 15 22 00                            movq  0x22151a(%rip), %rax
0085 # 8107675242232  2    ls       22011  22011  hardware interrupt     No         7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
0086 # 8107675242900  2    ls       22011  22011  return from interrupt  No     ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
0087 #                                                                              7fab593ea94f 48 8b 05 1a 15 22 00                            movq  0x22151a(%rip), %rax
0088 #                                                                              7fab593ea956 48 89 15 3b 13 22 00                            movq  %rdx, 0x22133b(%rip)
0089 # 8107675243232  2    ls       22011  22011  hardware interrupt     No         7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
0090 
0091 from __future__ import print_function
0092 
0093 import sys
0094 # Only change warnings if the python -W option was not used
0095 if not sys.warnoptions:
0096     import warnings
0097     # PySide2 causes deprecation warnings, ignore them.
0098     warnings.filterwarnings("ignore", category=DeprecationWarning)
0099 import argparse
0100 import weakref
0101 import threading
0102 import string
0103 try:
0104     # Python2
0105     import cPickle as pickle
0106     # size of pickled integer big enough for record size
0107     glb_nsz = 8
0108 except ImportError:
0109     import pickle
0110     glb_nsz = 16
0111 import re
0112 import os
0113 import random
0114 import copy
0115 import math
0116 from libxed import LibXED
0117 
0118 pyside_version_1 = True
0119 if not "--pyside-version-1" in sys.argv:
0120     try:
0121         from PySide2.QtCore import *
0122         from PySide2.QtGui import *
0123         from PySide2.QtSql import *
0124         from PySide2.QtWidgets import *
0125         pyside_version_1 = False
0126     except:
0127         pass
0128 
0129 if pyside_version_1:
0130     from PySide.QtCore import *
0131     from PySide.QtGui import *
0132     from PySide.QtSql import *
0133 
0134 from decimal import Decimal, ROUND_HALF_UP
0135 from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \
0136            c_void_p, c_bool, c_byte, c_char, c_int, c_uint, c_longlong, c_ulonglong
0137 from multiprocessing import Process, Array, Value, Event
0138 
0139 # xrange is range in Python3
0140 try:
0141     xrange
0142 except NameError:
0143     xrange = range
0144 
0145 def printerr(*args, **keyword_args):
0146     print(*args, file=sys.stderr, **keyword_args)
0147 
0148 # Data formatting helpers
0149 
0150 def tohex(ip):
0151     if ip < 0:
0152         ip += 1 << 64
0153     return "%x" % ip
0154 
0155 def offstr(offset):
0156     if offset:
0157         return "+0x%x" % offset
0158     return ""
0159 
0160 def dsoname(name):
0161     if name == "[kernel.kallsyms]":
0162         return "[kernel]"
0163     return name
0164 
0165 def findnth(s, sub, n, offs=0):
0166     pos = s.find(sub)
0167     if pos < 0:
0168         return pos
0169     if n <= 1:
0170         return offs + pos
0171     return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
0172 
0173 # Percent to one decimal place
0174 
0175 def PercentToOneDP(n, d):
0176     if not d:
0177         return "0.0"
0178     x = (n * Decimal(100)) / d
0179     return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
0180 
0181 # Helper for queries that must not fail
0182 
0183 def QueryExec(query, stmt):
0184     ret = query.exec_(stmt)
0185     if not ret:
0186         raise Exception("Query failed: " + query.lastError().text())
0187 
0188 # Background thread
0189 
0190 class Thread(QThread):
0191 
0192     done = Signal(object)
0193 
0194     def __init__(self, task, param=None, parent=None):
0195         super(Thread, self).__init__(parent)
0196         self.task = task
0197         self.param = param
0198 
0199     def run(self):
0200         while True:
0201             if self.param is None:
0202                 done, result = self.task()
0203             else:
0204                 done, result = self.task(self.param)
0205             self.done.emit(result)
0206             if done:
0207                 break
0208 
0209 # Tree data model
0210 
0211 class TreeModel(QAbstractItemModel):
0212 
0213     def __init__(self, glb, params, parent=None):
0214         super(TreeModel, self).__init__(parent)
0215         self.glb = glb
0216         self.params = params
0217         self.root = self.GetRoot()
0218         self.last_row_read = 0
0219 
0220     def Item(self, parent):
0221         if parent.isValid():
0222             return parent.internalPointer()
0223         else:
0224             return self.root
0225 
0226     def rowCount(self, parent):
0227         result = self.Item(parent).childCount()
0228         if result < 0:
0229             result = 0
0230             self.dataChanged.emit(parent, parent)
0231         return result
0232 
0233     def hasChildren(self, parent):
0234         return self.Item(parent).hasChildren()
0235 
0236     def headerData(self, section, orientation, role):
0237         if role == Qt.TextAlignmentRole:
0238             return self.columnAlignment(section)
0239         if role != Qt.DisplayRole:
0240             return None
0241         if orientation != Qt.Horizontal:
0242             return None
0243         return self.columnHeader(section)
0244 
0245     def parent(self, child):
0246         child_item = child.internalPointer()
0247         if child_item is self.root:
0248             return QModelIndex()
0249         parent_item = child_item.getParentItem()
0250         return self.createIndex(parent_item.getRow(), 0, parent_item)
0251 
0252     def index(self, row, column, parent):
0253         child_item = self.Item(parent).getChildItem(row)
0254         return self.createIndex(row, column, child_item)
0255 
0256     def DisplayData(self, item, index):
0257         return item.getData(index.column())
0258 
0259     def FetchIfNeeded(self, row):
0260         if row > self.last_row_read:
0261             self.last_row_read = row
0262             if row + 10 >= self.root.child_count:
0263                 self.fetcher.Fetch(glb_chunk_sz)
0264 
0265     def columnAlignment(self, column):
0266         return Qt.AlignLeft
0267 
0268     def columnFont(self, column):
0269         return None
0270 
0271     def data(self, index, role):
0272         if role == Qt.TextAlignmentRole:
0273             return self.columnAlignment(index.column())
0274         if role == Qt.FontRole:
0275             return self.columnFont(index.column())
0276         if role != Qt.DisplayRole:
0277             return None
0278         item = index.internalPointer()
0279         return self.DisplayData(item, index)
0280 
0281 # Table data model
0282 
0283 class TableModel(QAbstractTableModel):
0284 
0285     def __init__(self, parent=None):
0286         super(TableModel, self).__init__(parent)
0287         self.child_count = 0
0288         self.child_items = []
0289         self.last_row_read = 0
0290 
0291     def Item(self, parent):
0292         if parent.isValid():
0293             return parent.internalPointer()
0294         else:
0295             return self
0296 
0297     def rowCount(self, parent):
0298         return self.child_count
0299 
0300     def headerData(self, section, orientation, role):
0301         if role == Qt.TextAlignmentRole:
0302             return self.columnAlignment(section)
0303         if role != Qt.DisplayRole:
0304             return None
0305         if orientation != Qt.Horizontal:
0306             return None
0307         return self.columnHeader(section)
0308 
0309     def index(self, row, column, parent):
0310         return self.createIndex(row, column, self.child_items[row])
0311 
0312     def DisplayData(self, item, index):
0313         return item.getData(index.column())
0314 
0315     def FetchIfNeeded(self, row):
0316         if row > self.last_row_read:
0317             self.last_row_read = row
0318             if row + 10 >= self.child_count:
0319                 self.fetcher.Fetch(glb_chunk_sz)
0320 
0321     def columnAlignment(self, column):
0322         return Qt.AlignLeft
0323 
0324     def columnFont(self, column):
0325         return None
0326 
0327     def data(self, index, role):
0328         if role == Qt.TextAlignmentRole:
0329             return self.columnAlignment(index.column())
0330         if role == Qt.FontRole:
0331             return self.columnFont(index.column())
0332         if role != Qt.DisplayRole:
0333             return None
0334         item = index.internalPointer()
0335         return self.DisplayData(item, index)
0336 
0337 # Model cache
0338 
0339 model_cache = weakref.WeakValueDictionary()
0340 model_cache_lock = threading.Lock()
0341 
0342 def LookupCreateModel(model_name, create_fn):
0343     model_cache_lock.acquire()
0344     try:
0345         model = model_cache[model_name]
0346     except:
0347         model = None
0348     if model is None:
0349         model = create_fn()
0350         model_cache[model_name] = model
0351     model_cache_lock.release()
0352     return model
0353 
0354 def LookupModel(model_name):
0355     model_cache_lock.acquire()
0356     try:
0357         model = model_cache[model_name]
0358     except:
0359         model = None
0360     model_cache_lock.release()
0361     return model
0362 
0363 # Find bar
0364 
0365 class FindBar():
0366 
0367     def __init__(self, parent, finder, is_reg_expr=False):
0368         self.finder = finder
0369         self.context = []
0370         self.last_value = None
0371         self.last_pattern = None
0372 
0373         label = QLabel("Find:")
0374         label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
0375 
0376         self.textbox = QComboBox()
0377         self.textbox.setEditable(True)
0378         self.textbox.currentIndexChanged.connect(self.ValueChanged)
0379 
0380         self.progress = QProgressBar()
0381         self.progress.setRange(0, 0)
0382         self.progress.hide()
0383 
0384         if is_reg_expr:
0385             self.pattern = QCheckBox("Regular Expression")
0386         else:
0387             self.pattern = QCheckBox("Pattern")
0388         self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
0389 
0390         self.next_button = QToolButton()
0391         self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
0392         self.next_button.released.connect(lambda: self.NextPrev(1))
0393 
0394         self.prev_button = QToolButton()
0395         self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
0396         self.prev_button.released.connect(lambda: self.NextPrev(-1))
0397 
0398         self.close_button = QToolButton()
0399         self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
0400         self.close_button.released.connect(self.Deactivate)
0401 
0402         self.hbox = QHBoxLayout()
0403         self.hbox.setContentsMargins(0, 0, 0, 0)
0404 
0405         self.hbox.addWidget(label)
0406         self.hbox.addWidget(self.textbox)
0407         self.hbox.addWidget(self.progress)
0408         self.hbox.addWidget(self.pattern)
0409         self.hbox.addWidget(self.next_button)
0410         self.hbox.addWidget(self.prev_button)
0411         self.hbox.addWidget(self.close_button)
0412 
0413         self.bar = QWidget()
0414         self.bar.setLayout(self.hbox)
0415         self.bar.hide()
0416 
0417     def Widget(self):
0418         return self.bar
0419 
0420     def Activate(self):
0421         self.bar.show()
0422         self.textbox.lineEdit().selectAll()
0423         self.textbox.setFocus()
0424 
0425     def Deactivate(self):
0426         self.bar.hide()
0427 
0428     def Busy(self):
0429         self.textbox.setEnabled(False)
0430         self.pattern.hide()
0431         self.next_button.hide()
0432         self.prev_button.hide()
0433         self.progress.show()
0434 
0435     def Idle(self):
0436         self.textbox.setEnabled(True)
0437         self.progress.hide()
0438         self.pattern.show()
0439         self.next_button.show()
0440         self.prev_button.show()
0441 
0442     def Find(self, direction):
0443         value = self.textbox.currentText()
0444         pattern = self.pattern.isChecked()
0445         self.last_value = value
0446         self.last_pattern = pattern
0447         self.finder.Find(value, direction, pattern, self.context)
0448 
0449     def ValueChanged(self):
0450         value = self.textbox.currentText()
0451         pattern = self.pattern.isChecked()
0452         index = self.textbox.currentIndex()
0453         data = self.textbox.itemData(index)
0454         # Store the pattern in the combo box to keep it with the text value
0455         if data == None:
0456             self.textbox.setItemData(index, pattern)
0457         else:
0458             self.pattern.setChecked(data)
0459         self.Find(0)
0460 
0461     def NextPrev(self, direction):
0462         value = self.textbox.currentText()
0463         pattern = self.pattern.isChecked()
0464         if value != self.last_value:
0465             index = self.textbox.findText(value)
0466             # Allow for a button press before the value has been added to the combo box
0467             if index < 0:
0468                 index = self.textbox.count()
0469                 self.textbox.addItem(value, pattern)
0470                 self.textbox.setCurrentIndex(index)
0471                 return
0472             else:
0473                 self.textbox.setItemData(index, pattern)
0474         elif pattern != self.last_pattern:
0475             # Keep the pattern recorded in the combo box up to date
0476             index = self.textbox.currentIndex()
0477             self.textbox.setItemData(index, pattern)
0478         self.Find(direction)
0479 
0480     def NotFound(self):
0481         QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
0482 
0483 # Context-sensitive call graph data model item base
0484 
0485 class CallGraphLevelItemBase(object):
0486 
0487     def __init__(self, glb, params, row, parent_item):
0488         self.glb = glb
0489         self.params = params
0490         self.row = row
0491         self.parent_item = parent_item
0492         self.query_done = False
0493         self.child_count = 0
0494         self.child_items = []
0495         if parent_item:
0496             self.level = parent_item.level + 1
0497         else:
0498             self.level = 0
0499 
0500     def getChildItem(self, row):
0501         return self.child_items[row]
0502 
0503     def getParentItem(self):
0504         return self.parent_item
0505 
0506     def getRow(self):
0507         return self.row
0508 
0509     def childCount(self):
0510         if not self.query_done:
0511             self.Select()
0512             if not self.child_count:
0513                 return -1
0514         return self.child_count
0515 
0516     def hasChildren(self):
0517         if not self.query_done:
0518             return True
0519         return self.child_count > 0
0520 
0521     def getData(self, column):
0522         return self.data[column]
0523 
0524 # Context-sensitive call graph data model level 2+ item base
0525 
0526 class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
0527 
0528     def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
0529         super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
0530         self.comm_id = comm_id
0531         self.thread_id = thread_id
0532         self.call_path_id = call_path_id
0533         self.insn_cnt = insn_cnt
0534         self.cyc_cnt = cyc_cnt
0535         self.branch_count = branch_count
0536         self.time = time
0537 
0538     def Select(self):
0539         self.query_done = True
0540         query = QSqlQuery(self.glb.db)
0541         if self.params.have_ipc:
0542             ipc_str = ", SUM(insn_count), SUM(cyc_count)"
0543         else:
0544             ipc_str = ""
0545         QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
0546                     " FROM calls"
0547                     " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
0548                     " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
0549                     " INNER JOIN dsos ON symbols.dso_id = dsos.id"
0550                     " WHERE parent_call_path_id = " + str(self.call_path_id) +
0551                     " AND comm_id = " + str(self.comm_id) +
0552                     " AND thread_id = " + str(self.thread_id) +
0553                     " GROUP BY call_path_id, name, short_name"
0554                     " ORDER BY call_path_id")
0555         while query.next():
0556             if self.params.have_ipc:
0557                 insn_cnt = int(query.value(5))
0558                 cyc_cnt = int(query.value(6))
0559                 branch_count = int(query.value(7))
0560             else:
0561                 insn_cnt = 0
0562                 cyc_cnt = 0
0563                 branch_count = int(query.value(5))
0564             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)
0565             self.child_items.append(child_item)
0566             self.child_count += 1
0567 
0568 # Context-sensitive call graph data model level three item
0569 
0570 class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
0571 
0572     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):
0573         super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
0574         dso = dsoname(dso)
0575         if self.params.have_ipc:
0576             insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
0577             cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
0578             br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
0579             ipc = CalcIPC(cyc_cnt, insn_cnt)
0580             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 ]
0581         else:
0582             self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
0583         self.dbid = call_path_id
0584 
0585 # Context-sensitive call graph data model level two item
0586 
0587 class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
0588 
0589     def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
0590         super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)
0591         if self.params.have_ipc:
0592             self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
0593         else:
0594             self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
0595         self.dbid = thread_id
0596 
0597     def Select(self):
0598         super(CallGraphLevelTwoItem, self).Select()
0599         for child_item in self.child_items:
0600             self.time += child_item.time
0601             self.insn_cnt += child_item.insn_cnt
0602             self.cyc_cnt += child_item.cyc_cnt
0603             self.branch_count += child_item.branch_count
0604         for child_item in self.child_items:
0605             child_item.data[4] = PercentToOneDP(child_item.time, self.time)
0606             if self.params.have_ipc:
0607                 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
0608                 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
0609                 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
0610             else:
0611                 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
0612 
0613 # Context-sensitive call graph data model level one item
0614 
0615 class CallGraphLevelOneItem(CallGraphLevelItemBase):
0616 
0617     def __init__(self, glb, params, row, comm_id, comm, parent_item):
0618         super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)
0619         if self.params.have_ipc:
0620             self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
0621         else:
0622             self.data = [comm, "", "", "", "", "", ""]
0623         self.dbid = comm_id
0624 
0625     def Select(self):
0626         self.query_done = True
0627         query = QSqlQuery(self.glb.db)
0628         QueryExec(query, "SELECT thread_id, pid, tid"
0629                     " FROM comm_threads"
0630                     " INNER JOIN threads ON thread_id = threads.id"
0631                     " WHERE comm_id = " + str(self.dbid))
0632         while query.next():
0633             child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
0634             self.child_items.append(child_item)
0635             self.child_count += 1
0636 
0637 # Context-sensitive call graph data model root item
0638 
0639 class CallGraphRootItem(CallGraphLevelItemBase):
0640 
0641     def __init__(self, glb, params):
0642         super(CallGraphRootItem, self).__init__(glb, params, 0, None)
0643         self.dbid = 0
0644         self.query_done = True
0645         if_has_calls = ""
0646         if IsSelectable(glb.db, "comms", columns = "has_calls"):
0647             if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
0648         query = QSqlQuery(glb.db)
0649         QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
0650         while query.next():
0651             if not query.value(0):
0652                 continue
0653             child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
0654             self.child_items.append(child_item)
0655             self.child_count += 1
0656 
0657 # Call graph model parameters
0658 
0659 class CallGraphModelParams():
0660 
0661     def __init__(self, glb, parent=None):
0662         self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")
0663 
0664 # Context-sensitive call graph data model base
0665 
0666 class CallGraphModelBase(TreeModel):
0667 
0668     def __init__(self, glb, parent=None):
0669         super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)
0670 
0671     def FindSelect(self, value, pattern, query):
0672         if pattern:
0673             # postgresql and sqlite pattern patching differences:
0674             #   postgresql LIKE is case sensitive but sqlite LIKE is not
0675             #   postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
0676             #   postgresql supports ILIKE which is case insensitive
0677             #   sqlite supports GLOB (text only) which uses * and ? and is case sensitive
0678             if not self.glb.dbref.is_sqlite3:
0679                 # Escape % and _
0680                 s = value.replace("%", "\%")
0681                 s = s.replace("_", "\_")
0682                 # Translate * and ? into SQL LIKE pattern characters % and _
0683                 trans = string.maketrans("*?", "%_")
0684                 match = " LIKE '" + str(s).translate(trans) + "'"
0685             else:
0686                 match = " GLOB '" + str(value) + "'"
0687         else:
0688             match = " = '" + str(value) + "'"
0689         self.DoFindSelect(query, match)
0690 
0691     def Found(self, query, found):
0692         if found:
0693             return self.FindPath(query)
0694         return []
0695 
0696     def FindValue(self, value, pattern, query, last_value, last_pattern):
0697         if last_value == value and pattern == last_pattern:
0698             found = query.first()
0699         else:
0700             self.FindSelect(value, pattern, query)
0701             found = query.next()
0702         return self.Found(query, found)
0703 
0704     def FindNext(self, query):
0705         found = query.next()
0706         if not found:
0707             found = query.first()
0708         return self.Found(query, found)
0709 
0710     def FindPrev(self, query):
0711         found = query.previous()
0712         if not found:
0713             found = query.last()
0714         return self.Found(query, found)
0715 
0716     def FindThread(self, c):
0717         if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
0718             ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
0719         elif c.direction > 0:
0720             ids = self.FindNext(c.query)
0721         else:
0722             ids = self.FindPrev(c.query)
0723         return (True, ids)
0724 
0725     def Find(self, value, direction, pattern, context, callback):
0726         class Context():
0727             def __init__(self, *x):
0728                 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
0729             def Update(self, *x):
0730                 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
0731         if len(context):
0732             context[0].Update(value, direction, pattern)
0733         else:
0734             context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
0735         # Use a thread so the UI is not blocked during the SELECT
0736         thread = Thread(self.FindThread, context[0])
0737         thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
0738         thread.start()
0739 
0740     def FindDone(self, thread, callback, ids):
0741         callback(ids)
0742 
0743 # Context-sensitive call graph data model
0744 
0745 class CallGraphModel(CallGraphModelBase):
0746 
0747     def __init__(self, glb, parent=None):
0748         super(CallGraphModel, self).__init__(glb, parent)
0749 
0750     def GetRoot(self):
0751         return CallGraphRootItem(self.glb, self.params)
0752 
0753     def columnCount(self, parent=None):
0754         if self.params.have_ipc:
0755             return 12
0756         else:
0757             return 7
0758 
0759     def columnHeader(self, column):
0760         if self.params.have_ipc:
0761             headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
0762         else:
0763             headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
0764         return headers[column]
0765 
0766     def columnAlignment(self, column):
0767         if self.params.have_ipc:
0768             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 ]
0769         else:
0770             alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
0771         return alignment[column]
0772 
0773     def DoFindSelect(self, query, match):
0774         QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
0775                         " FROM calls"
0776                         " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
0777                         " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
0778                         " WHERE calls.id <> 0"
0779                         " AND symbols.name" + match +
0780                         " GROUP BY comm_id, thread_id, call_path_id"
0781                         " ORDER BY comm_id, thread_id, call_path_id")
0782 
0783     def FindPath(self, query):
0784         # Turn the query result into a list of ids that the tree view can walk
0785         # to open the tree at the right place.
0786         ids = []
0787         parent_id = query.value(0)
0788         while parent_id:
0789             ids.insert(0, parent_id)
0790             q2 = QSqlQuery(self.glb.db)
0791             QueryExec(q2, "SELECT parent_id"
0792                     " FROM call_paths"
0793                     " WHERE id = " + str(parent_id))
0794             if not q2.next():
0795                 break
0796             parent_id = q2.value(0)
0797         # The call path root is not used
0798         if ids[0] == 1:
0799             del ids[0]
0800         ids.insert(0, query.value(2))
0801         ids.insert(0, query.value(1))
0802         return ids
0803 
0804 # Call tree data model level 2+ item base
0805 
0806 class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
0807 
0808     def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
0809         super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
0810         self.comm_id = comm_id
0811         self.thread_id = thread_id
0812         self.calls_id = calls_id
0813         self.call_time = call_time
0814         self.time = time
0815         self.insn_cnt = insn_cnt
0816         self.cyc_cnt = cyc_cnt
0817         self.branch_count = branch_count
0818 
0819     def Select(self):
0820         self.query_done = True
0821         if self.calls_id == 0:
0822             comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
0823         else:
0824             comm_thread = ""
0825         if self.params.have_ipc:
0826             ipc_str = ", insn_count, cyc_count"
0827         else:
0828             ipc_str = ""
0829         query = QSqlQuery(self.glb.db)
0830         QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"
0831                     " FROM calls"
0832                     " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
0833                     " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
0834                     " INNER JOIN dsos ON symbols.dso_id = dsos.id"
0835                     " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
0836                     " ORDER BY call_time, calls.id")
0837         while query.next():
0838             if self.params.have_ipc:
0839                 insn_cnt = int(query.value(5))
0840                 cyc_cnt = int(query.value(6))
0841                 branch_count = int(query.value(7))
0842             else:
0843                 insn_cnt = 0
0844                 cyc_cnt = 0
0845                 branch_count = int(query.value(5))
0846             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)
0847             self.child_items.append(child_item)
0848             self.child_count += 1
0849 
0850 # Call tree data model level three item
0851 
0852 class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
0853 
0854     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):
0855         super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item)
0856         dso = dsoname(dso)
0857         if self.params.have_ipc:
0858             insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
0859             cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
0860             br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
0861             ipc = CalcIPC(cyc_cnt, insn_cnt)
0862             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 ]
0863         else:
0864             self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
0865         self.dbid = calls_id
0866 
0867 # Call tree data model level two item
0868 
0869 class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
0870 
0871     def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
0872         super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item)
0873         if self.params.have_ipc:
0874             self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
0875         else:
0876             self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
0877         self.dbid = thread_id
0878 
0879     def Select(self):
0880         super(CallTreeLevelTwoItem, self).Select()
0881         for child_item in self.child_items:
0882             self.time += child_item.time
0883             self.insn_cnt += child_item.insn_cnt
0884             self.cyc_cnt += child_item.cyc_cnt
0885             self.branch_count += child_item.branch_count
0886         for child_item in self.child_items:
0887             child_item.data[4] = PercentToOneDP(child_item.time, self.time)
0888             if self.params.have_ipc:
0889                 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
0890                 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
0891                 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
0892             else:
0893                 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
0894 
0895 # Call tree data model level one item
0896 
0897 class CallTreeLevelOneItem(CallGraphLevelItemBase):
0898 
0899     def __init__(self, glb, params, row, comm_id, comm, parent_item):
0900         super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
0901         if self.params.have_ipc:
0902             self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
0903         else:
0904             self.data = [comm, "", "", "", "", "", ""]
0905         self.dbid = comm_id
0906 
0907     def Select(self):
0908         self.query_done = True
0909         query = QSqlQuery(self.glb.db)
0910         QueryExec(query, "SELECT thread_id, pid, tid"
0911                     " FROM comm_threads"
0912                     " INNER JOIN threads ON thread_id = threads.id"
0913                     " WHERE comm_id = " + str(self.dbid))
0914         while query.next():
0915             child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
0916             self.child_items.append(child_item)
0917             self.child_count += 1
0918 
0919 # Call tree data model root item
0920 
0921 class CallTreeRootItem(CallGraphLevelItemBase):
0922 
0923     def __init__(self, glb, params):
0924         super(CallTreeRootItem, self).__init__(glb, params, 0, None)
0925         self.dbid = 0
0926         self.query_done = True
0927         if_has_calls = ""
0928         if IsSelectable(glb.db, "comms", columns = "has_calls"):
0929             if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
0930         query = QSqlQuery(glb.db)
0931         QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
0932         while query.next():
0933             if not query.value(0):
0934                 continue
0935             child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
0936             self.child_items.append(child_item)
0937             self.child_count += 1
0938 
0939 # Call Tree data model
0940 
0941 class CallTreeModel(CallGraphModelBase):
0942 
0943     def __init__(self, glb, parent=None):
0944         super(CallTreeModel, self).__init__(glb, parent)
0945 
0946     def GetRoot(self):
0947         return CallTreeRootItem(self.glb, self.params)
0948 
0949     def columnCount(self, parent=None):
0950         if self.params.have_ipc:
0951             return 12
0952         else:
0953             return 7
0954 
0955     def columnHeader(self, column):
0956         if self.params.have_ipc:
0957             headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
0958         else:
0959             headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
0960         return headers[column]
0961 
0962     def columnAlignment(self, column):
0963         if self.params.have_ipc:
0964             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 ]
0965         else:
0966             alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
0967         return alignment[column]
0968 
0969     def DoFindSelect(self, query, match):
0970         QueryExec(query, "SELECT calls.id, comm_id, thread_id"
0971                         " FROM calls"
0972                         " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
0973                         " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
0974                         " WHERE calls.id <> 0"
0975                         " AND symbols.name" + match +
0976                         " ORDER BY comm_id, thread_id, call_time, calls.id")
0977 
0978     def FindPath(self, query):
0979         # Turn the query result into a list of ids that the tree view can walk
0980         # to open the tree at the right place.
0981         ids = []
0982         parent_id = query.value(0)
0983         while parent_id:
0984             ids.insert(0, parent_id)
0985             q2 = QSqlQuery(self.glb.db)
0986             QueryExec(q2, "SELECT parent_id"
0987                     " FROM calls"
0988                     " WHERE id = " + str(parent_id))
0989             if not q2.next():
0990                 break
0991             parent_id = q2.value(0)
0992         ids.insert(0, query.value(2))
0993         ids.insert(0, query.value(1))
0994         return ids
0995 
0996 # Vertical layout
0997 
0998 class HBoxLayout(QHBoxLayout):
0999 
1000     def __init__(self, *children):
1001         super(HBoxLayout, self).__init__()
1002 
1003         self.layout().setContentsMargins(0, 0, 0, 0)
1004         for child in children:
1005             if child.isWidgetType():
1006                 self.layout().addWidget(child)
1007             else:
1008                 self.layout().addLayout(child)
1009 
1010 # Horizontal layout
1011 
1012 class VBoxLayout(QVBoxLayout):
1013 
1014     def __init__(self, *children):
1015         super(VBoxLayout, self).__init__()
1016 
1017         self.layout().setContentsMargins(0, 0, 0, 0)
1018         for child in children:
1019             if child.isWidgetType():
1020                 self.layout().addWidget(child)
1021             else:
1022                 self.layout().addLayout(child)
1023 
1024 # Vertical layout widget
1025 
1026 class VBox():
1027 
1028     def __init__(self, *children):
1029         self.vbox = QWidget()
1030         self.vbox.setLayout(VBoxLayout(*children))
1031 
1032     def Widget(self):
1033         return self.vbox
1034 
1035 # Tree window base
1036 
1037 class TreeWindowBase(QMdiSubWindow):
1038 
1039     def __init__(self, parent=None):
1040         super(TreeWindowBase, self).__init__(parent)
1041 
1042         self.model = None
1043         self.find_bar = None
1044 
1045         self.view = QTreeView()
1046         self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1047         self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1048 
1049         self.context_menu = TreeContextMenu(self.view)
1050 
1051     def DisplayFound(self, ids):
1052         if not len(ids):
1053             return False
1054         parent = QModelIndex()
1055         for dbid in ids:
1056             found = False
1057             n = self.model.rowCount(parent)
1058             for row in xrange(n):
1059                 child = self.model.index(row, 0, parent)
1060                 if child.internalPointer().dbid == dbid:
1061                     found = True
1062                     self.view.setExpanded(parent, True)
1063                     self.view.setCurrentIndex(child)
1064                     parent = child
1065                     break
1066             if not found:
1067                 break
1068         return found
1069 
1070     def Find(self, value, direction, pattern, context):
1071         self.view.setFocus()
1072         self.find_bar.Busy()
1073         self.model.Find(value, direction, pattern, context, self.FindDone)
1074 
1075     def FindDone(self, ids):
1076         found = True
1077         if not self.DisplayFound(ids):
1078             found = False
1079         self.find_bar.Idle()
1080         if not found:
1081             self.find_bar.NotFound()
1082 
1083 
1084 # Context-sensitive call graph window
1085 
1086 class CallGraphWindow(TreeWindowBase):
1087 
1088     def __init__(self, glb, parent=None):
1089         super(CallGraphWindow, self).__init__(parent)
1090 
1091         self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
1092 
1093         self.view.setModel(self.model)
1094 
1095         for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
1096             self.view.setColumnWidth(c, w)
1097 
1098         self.find_bar = FindBar(self, self)
1099 
1100         self.vbox = VBox(self.view, self.find_bar.Widget())
1101 
1102         self.setWidget(self.vbox.Widget())
1103 
1104         AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
1105 
1106 # Call tree window
1107 
1108 class CallTreeWindow(TreeWindowBase):
1109 
1110     def __init__(self, glb, parent=None, thread_at_time=None):
1111         super(CallTreeWindow, self).__init__(parent)
1112 
1113         self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
1114 
1115         self.view.setModel(self.model)
1116 
1117         for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
1118             self.view.setColumnWidth(c, w)
1119 
1120         self.find_bar = FindBar(self, self)
1121 
1122         self.vbox = VBox(self.view, self.find_bar.Widget())
1123 
1124         self.setWidget(self.vbox.Widget())
1125 
1126         AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
1127 
1128         if thread_at_time:
1129             self.DisplayThreadAtTime(*thread_at_time)
1130 
1131     def DisplayThreadAtTime(self, comm_id, thread_id, time):
1132         parent = QModelIndex()
1133         for dbid in (comm_id, thread_id):
1134             found = False
1135             n = self.model.rowCount(parent)
1136             for row in xrange(n):
1137                 child = self.model.index(row, 0, parent)
1138                 if child.internalPointer().dbid == dbid:
1139                     found = True
1140                     self.view.setExpanded(parent, True)
1141                     self.view.setCurrentIndex(child)
1142                     parent = child
1143                     break
1144             if not found:
1145                 return
1146         found = False
1147         while True:
1148             n = self.model.rowCount(parent)
1149             if not n:
1150                 return
1151             last_child = None
1152             for row in xrange(n):
1153                 self.view.setExpanded(parent, True)
1154                 child = self.model.index(row, 0, parent)
1155                 child_call_time = child.internalPointer().call_time
1156                 if child_call_time < time:
1157                     last_child = child
1158                 elif child_call_time == time:
1159                     self.view.setCurrentIndex(child)
1160                     return
1161                 elif child_call_time > time:
1162                     break
1163             if not last_child:
1164                 if not found:
1165                     child = self.model.index(0, 0, parent)
1166                     self.view.setExpanded(parent, True)
1167                     self.view.setCurrentIndex(child)
1168                 return
1169             found = True
1170             self.view.setExpanded(parent, True)
1171             self.view.setCurrentIndex(last_child)
1172             parent = last_child
1173 
1174 # ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
1175 
1176 def ExecComm(db, thread_id, time):
1177     query = QSqlQuery(db)
1178     QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
1179                 " FROM comm_threads"
1180                 " INNER JOIN comms ON comms.id = comm_threads.comm_id"
1181                 " WHERE comm_threads.thread_id = " + str(thread_id) +
1182                 " ORDER BY comms.c_time, comms.id")
1183     first = None
1184     last = None
1185     while query.next():
1186         if first is None:
1187             first = query.value(0)
1188         if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
1189             last = query.value(0)
1190     if not(last is None):
1191         return last
1192     return first
1193 
1194 # Container for (x, y) data
1195 
1196 class XY():
1197     def __init__(self, x=0, y=0):
1198         self.x = x
1199         self.y = y
1200 
1201     def __str__(self):
1202         return "XY({}, {})".format(str(self.x), str(self.y))
1203 
1204 # Container for sub-range data
1205 
1206 class Subrange():
1207     def __init__(self, lo=0, hi=0):
1208         self.lo = lo
1209         self.hi = hi
1210 
1211     def __str__(self):
1212         return "Subrange({}, {})".format(str(self.lo), str(self.hi))
1213 
1214 # Graph data region base class
1215 
1216 class GraphDataRegion(object):
1217 
1218     def __init__(self, key, title = "", ordinal = ""):
1219         self.key = key
1220         self.title = title
1221         self.ordinal = ordinal
1222 
1223 # Function to sort GraphDataRegion
1224 
1225 def GraphDataRegionOrdinal(data_region):
1226     return data_region.ordinal
1227 
1228 # Attributes for a graph region
1229 
1230 class GraphRegionAttribute():
1231 
1232     def __init__(self, colour):
1233         self.colour = colour
1234 
1235 # Switch graph data region represents a task
1236 
1237 class SwitchGraphDataRegion(GraphDataRegion):
1238 
1239     def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
1240         super(SwitchGraphDataRegion, self).__init__(key)
1241 
1242         self.title = str(pid) + " / " + str(tid) + " " + comm
1243         # Order graph legend within exec comm by pid / tid / time
1244         self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
1245         self.exec_comm_id = exec_comm_id
1246         self.pid = pid
1247         self.tid = tid
1248         self.comm = comm
1249         self.thread_id = thread_id
1250         self.comm_id = comm_id
1251 
1252 # Graph data point
1253 
1254 class GraphDataPoint():
1255 
1256     def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
1257         self.data = data
1258         self.index = index
1259         self.x = x
1260         self.y = y
1261         self.altx = altx
1262         self.alty = alty
1263         self.hregion = hregion
1264         self.vregion = vregion
1265 
1266 # Graph data (single graph) base class
1267 
1268 class GraphData(object):
1269 
1270     def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
1271         self.collection = collection
1272         self.points = []
1273         self.xbase = xbase
1274         self.ybase = ybase
1275         self.title = ""
1276 
1277     def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
1278         index = len(self.points)
1279 
1280         x = float(Decimal(x) - self.xbase)
1281         y = float(Decimal(y) - self.ybase)
1282 
1283         self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
1284 
1285     def XToData(self, x):
1286         return Decimal(x) + self.xbase
1287 
1288     def YToData(self, y):
1289         return Decimal(y) + self.ybase
1290 
1291 # Switch graph data (for one CPU)
1292 
1293 class SwitchGraphData(GraphData):
1294 
1295     def __init__(self, db, collection, cpu, xbase):
1296         super(SwitchGraphData, self).__init__(collection, xbase)
1297 
1298         self.cpu = cpu
1299         self.title = "CPU " + str(cpu)
1300         self.SelectSwitches(db)
1301 
1302     def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
1303         query = QSqlQuery(db)
1304         QueryExec(query, "SELECT id, c_time"
1305                     " FROM comms"
1306                     " WHERE c_thread_id = " + str(thread_id) +
1307                     "   AND exec_flag = " + self.collection.glb.dbref.TRUE +
1308                     "   AND c_time >= " + str(start_time) +
1309                     "   AND c_time <= " + str(end_time) +
1310                     " ORDER BY c_time, id")
1311         while query.next():
1312             comm_id = query.value(0)
1313             if comm_id == last_comm_id:
1314                 continue
1315             time = query.value(1)
1316             hregion = self.HRegion(db, thread_id, comm_id, time)
1317             self.AddPoint(time, 1000, None, None, hregion)
1318 
1319     def SelectSwitches(self, db):
1320         last_time = None
1321         last_comm_id = None
1322         last_thread_id = None
1323         query = QSqlQuery(db)
1324         QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
1325                     " FROM context_switches"
1326                     " WHERE machine_id = " + str(self.collection.machine_id) +
1327                     "   AND cpu = " + str(self.cpu) +
1328                     " ORDER BY time, id")
1329         while query.next():
1330             flags = int(query.value(5))
1331             if flags & 1:
1332                 # Schedule-out: detect and add exec's
1333                 if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
1334                     self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
1335                 continue
1336             # Schedule-in: add data point
1337             if len(self.points) == 0:
1338                 start_time = self.collection.glb.StartTime(self.collection.machine_id)
1339                 hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
1340                 self.AddPoint(start_time, 1000, None, None, hregion)
1341             time = query.value(0)
1342             comm_id = query.value(4)
1343             thread_id = query.value(2)
1344             hregion = self.HRegion(db, thread_id, comm_id, time)
1345             self.AddPoint(time, 1000, None, None, hregion)
1346             last_time = time
1347             last_comm_id = comm_id
1348             last_thread_id = thread_id
1349 
1350     def NewHRegion(self, db, key, thread_id, comm_id, time):
1351         exec_comm_id = ExecComm(db, thread_id, time)
1352         query = QSqlQuery(db)
1353         QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
1354         if query.next():
1355             pid = query.value(0)
1356             tid = query.value(1)
1357         else:
1358             pid = -1
1359             tid = -1
1360         query = QSqlQuery(db)
1361         QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
1362         if query.next():
1363             comm = query.value(0)
1364         else:
1365             comm = ""
1366         return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
1367 
1368     def HRegion(self, db, thread_id, comm_id, time):
1369         key = str(thread_id) + ":" + str(comm_id)
1370         hregion = self.collection.LookupHRegion(key)
1371         if hregion is None:
1372             hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
1373             self.collection.AddHRegion(key, hregion)
1374         return hregion
1375 
1376 # Graph data collection (multiple related graphs) base class
1377 
1378 class GraphDataCollection(object):
1379 
1380     def __init__(self, glb):
1381         self.glb = glb
1382         self.data = []
1383         self.hregions = {}
1384         self.xrangelo = None
1385         self.xrangehi = None
1386         self.yrangelo = None
1387         self.yrangehi = None
1388         self.dp = XY(0, 0)
1389 
1390     def AddGraphData(self, data):
1391         self.data.append(data)
1392 
1393     def LookupHRegion(self, key):
1394         if key in self.hregions:
1395             return self.hregions[key]
1396         return None
1397 
1398     def AddHRegion(self, key, hregion):
1399         self.hregions[key] = hregion
1400 
1401 # Switch graph data collection (SwitchGraphData for each CPU)
1402 
1403 class SwitchGraphDataCollection(GraphDataCollection):
1404 
1405     def __init__(self, glb, db, machine_id):
1406         super(SwitchGraphDataCollection, self).__init__(glb)
1407 
1408         self.machine_id = machine_id
1409         self.cpus = self.SelectCPUs(db)
1410 
1411         self.xrangelo = glb.StartTime(machine_id)
1412         self.xrangehi = glb.FinishTime(machine_id)
1413 
1414         self.yrangelo = Decimal(0)
1415         self.yrangehi = Decimal(1000)
1416 
1417         for cpu in self.cpus:
1418             self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
1419 
1420     def SelectCPUs(self, db):
1421         cpus = []
1422         query = QSqlQuery(db)
1423         QueryExec(query, "SELECT DISTINCT cpu"
1424                     " FROM context_switches"
1425                     " WHERE machine_id = " + str(self.machine_id))
1426         while query.next():
1427             cpus.append(int(query.value(0)))
1428         return sorted(cpus)
1429 
1430 # Switch graph data graphics item displays the graphed data
1431 
1432 class SwitchGraphDataGraphicsItem(QGraphicsItem):
1433 
1434     def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
1435         super(SwitchGraphDataGraphicsItem, self).__init__(parent)
1436 
1437         self.data = data
1438         self.graph_width = graph_width
1439         self.graph_height = graph_height
1440         self.attrs = attrs
1441         self.event_handler = event_handler
1442         self.setAcceptHoverEvents(True)
1443 
1444     def boundingRect(self):
1445         return QRectF(0, 0, self.graph_width, self.graph_height)
1446 
1447     def PaintPoint(self, painter, last, x):
1448         if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
1449             if last.x < self.attrs.subrange.x.lo:
1450                 x0 = self.attrs.subrange.x.lo
1451             else:
1452                 x0 = last.x
1453             if x > self.attrs.subrange.x.hi:
1454                 x1 = self.attrs.subrange.x.hi
1455             else:
1456                 x1 = x - 1
1457             x0 = self.attrs.XToPixel(x0)
1458             x1 = self.attrs.XToPixel(x1)
1459 
1460             y0 = self.attrs.YToPixel(last.y)
1461 
1462             colour = self.attrs.region_attributes[last.hregion.key].colour
1463 
1464             width = x1 - x0 + 1
1465             if width < 2:
1466                 painter.setPen(colour)
1467                 painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
1468             else:
1469                 painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
1470 
1471     def paint(self, painter, option, widget):
1472         last = None
1473         for point in self.data.points:
1474             self.PaintPoint(painter, last, point.x)
1475             if point.x > self.attrs.subrange.x.hi:
1476                 break;
1477             last = point
1478         self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
1479 
1480     def BinarySearchPoint(self, target):
1481         lower_pos = 0
1482         higher_pos = len(self.data.points)
1483         while True:
1484             pos = int((lower_pos + higher_pos) / 2)
1485             val = self.data.points[pos].x
1486             if target >= val:
1487                 lower_pos = pos
1488             else:
1489                 higher_pos = pos
1490             if higher_pos <= lower_pos + 1:
1491                 return lower_pos
1492 
1493     def XPixelToData(self, x):
1494         x = self.attrs.PixelToX(x)
1495         if x < self.data.points[0].x:
1496             x = 0
1497             pos = 0
1498             low = True
1499         else:
1500             pos = self.BinarySearchPoint(x)
1501             low = False
1502         return (low, pos, self.data.XToData(x))
1503 
1504     def EventToData(self, event):
1505         no_data = (None,) * 4
1506         if len(self.data.points) < 1:
1507             return no_data
1508         x = event.pos().x()
1509         if x < 0:
1510             return no_data
1511         low0, pos0, time_from = self.XPixelToData(x)
1512         low1, pos1, time_to = self.XPixelToData(x + 1)
1513         hregions = set()
1514         hregion_times = []
1515         if not low1:
1516             for i in xrange(pos0, pos1 + 1):
1517                 hregion = self.data.points[i].hregion
1518                 hregions.add(hregion)
1519                 if i == pos0:
1520                     time = time_from
1521                 else:
1522                     time = self.data.XToData(self.data.points[i].x)
1523                 hregion_times.append((hregion, time))
1524         return (time_from, time_to, hregions, hregion_times)
1525 
1526     def hoverMoveEvent(self, event):
1527         time_from, time_to, hregions, hregion_times = self.EventToData(event)
1528         if time_from is not None:
1529             self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
1530 
1531     def hoverLeaveEvent(self, event):
1532         self.event_handler.NoPointEvent()
1533 
1534     def mousePressEvent(self, event):
1535         if event.button() != Qt.RightButton:
1536             super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
1537             return
1538         time_from, time_to, hregions, hregion_times = self.EventToData(event)
1539         if hregion_times:
1540             self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
1541 
1542 # X-axis graphics item
1543 
1544 class XAxisGraphicsItem(QGraphicsItem):
1545 
1546     def __init__(self, width, parent=None):
1547         super(XAxisGraphicsItem, self).__init__(parent)
1548 
1549         self.width = width
1550         self.max_mark_sz = 4
1551         self.height = self.max_mark_sz + 1
1552 
1553     def boundingRect(self):
1554         return QRectF(0, 0, self.width, self.height)
1555 
1556     def Step(self):
1557         attrs = self.parentItem().attrs
1558         subrange = attrs.subrange.x
1559         t = subrange.hi - subrange.lo
1560         s = (3.0 * t) / self.width
1561         n = 1.0
1562         while s > n:
1563             n = n * 10.0
1564         return n
1565 
1566     def PaintMarks(self, painter, at_y, lo, hi, step, i):
1567         attrs = self.parentItem().attrs
1568         x = lo
1569         while x <= hi:
1570             xp = attrs.XToPixel(x)
1571             if i % 10:
1572                 if i % 5:
1573                     sz = 1
1574                 else:
1575                     sz = 2
1576             else:
1577                 sz = self.max_mark_sz
1578                 i = 0
1579             painter.drawLine(xp, at_y, xp, at_y + sz)
1580             x += step
1581             i += 1
1582 
1583     def paint(self, painter, option, widget):
1584         # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
1585         painter.drawLine(0, 0, self.width - 1, 0)
1586         n = self.Step()
1587         attrs = self.parentItem().attrs
1588         subrange = attrs.subrange.x
1589         if subrange.lo:
1590             x_offset = n - (subrange.lo % n)
1591         else:
1592             x_offset = 0.0
1593         x = subrange.lo + x_offset
1594         i = (x / n) % 10
1595         self.PaintMarks(painter, 0, x, subrange.hi, n, i)
1596 
1597     def ScaleDimensions(self):
1598         n = self.Step()
1599         attrs = self.parentItem().attrs
1600         lo = attrs.subrange.x.lo
1601         hi = (n * 10.0) + lo
1602         width = attrs.XToPixel(hi)
1603         if width > 500:
1604             width = 0
1605         return (n, lo, hi, width)
1606 
1607     def PaintScale(self, painter, at_x, at_y):
1608         n, lo, hi, width = self.ScaleDimensions()
1609         if not width:
1610             return
1611         painter.drawLine(at_x, at_y, at_x + width, at_y)
1612         self.PaintMarks(painter, at_y, lo, hi, n, 0)
1613 
1614     def ScaleWidth(self):
1615         n, lo, hi, width = self.ScaleDimensions()
1616         return width
1617 
1618     def ScaleHeight(self):
1619         return self.height
1620 
1621     def ScaleUnit(self):
1622         return self.Step() * 10
1623 
1624 # Scale graphics item base class
1625 
1626 class ScaleGraphicsItem(QGraphicsItem):
1627 
1628     def __init__(self, axis, parent=None):
1629         super(ScaleGraphicsItem, self).__init__(parent)
1630         self.axis = axis
1631 
1632     def boundingRect(self):
1633         scale_width = self.axis.ScaleWidth()
1634         if not scale_width:
1635             return QRectF()
1636         return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
1637 
1638     def paint(self, painter, option, widget):
1639         scale_width = self.axis.ScaleWidth()
1640         if not scale_width:
1641             return
1642         self.axis.PaintScale(painter, 0, 5)
1643         x = scale_width + 4
1644         painter.drawText(QPointF(x, 10), self.Text())
1645 
1646     def Unit(self):
1647         return self.axis.ScaleUnit()
1648 
1649     def Text(self):
1650         return ""
1651 
1652 # Switch graph scale graphics item
1653 
1654 class SwitchScaleGraphicsItem(ScaleGraphicsItem):
1655 
1656     def __init__(self, axis, parent=None):
1657         super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
1658 
1659     def Text(self):
1660         unit = self.Unit()
1661         if unit >= 1000000000:
1662             unit = int(unit / 1000000000)
1663             us = "s"
1664         elif unit >= 1000000:
1665             unit = int(unit / 1000000)
1666             us = "ms"
1667         elif unit >= 1000:
1668             unit = int(unit / 1000)
1669             us = "us"
1670         else:
1671             unit = int(unit)
1672             us = "ns"
1673         return " = " + str(unit) + " " + us
1674 
1675 # Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
1676 
1677 class SwitchGraphGraphicsItem(QGraphicsItem):
1678 
1679     def __init__(self, collection, data, attrs, event_handler, first, parent=None):
1680         super(SwitchGraphGraphicsItem, self).__init__(parent)
1681         self.collection = collection
1682         self.data = data
1683         self.attrs = attrs
1684         self.event_handler = event_handler
1685 
1686         margin = 20
1687         title_width = 50
1688 
1689         self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
1690 
1691         self.title_graphics.setPos(margin, margin)
1692         graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
1693         graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
1694 
1695         self.graph_origin_x = margin + title_width + margin
1696         self.graph_origin_y = graph_height + margin
1697 
1698         x_axis_size = 1
1699         y_axis_size = 1
1700         self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
1701 
1702         self.x_axis = XAxisGraphicsItem(graph_width, self)
1703         self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
1704 
1705         if first:
1706             self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
1707             self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
1708 
1709         self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
1710 
1711         self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
1712         self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
1713 
1714         self.width = self.graph_origin_x + graph_width + margin
1715         self.height = self.graph_origin_y + margin
1716 
1717         self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
1718         self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
1719 
1720         if parent and 'EnableRubberBand' in dir(parent):
1721             parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
1722 
1723     def boundingRect(self):
1724         return QRectF(0, 0, self.width, self.height)
1725 
1726     def paint(self, painter, option, widget):
1727         pass
1728 
1729     def RBXToPixel(self, x):
1730         return self.attrs.PixelToX(x - self.graph_origin_x)
1731 
1732     def RBXRangeToPixel(self, x0, x1):
1733         return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
1734 
1735     def RBPixelToTime(self, x):
1736         if x < self.data.points[0].x:
1737             return self.data.XToData(0)
1738         return self.data.XToData(x)
1739 
1740     def RBEventTimes(self, x0, x1):
1741         x0, x1 = self.RBXRangeToPixel(x0, x1)
1742         time_from = self.RBPixelToTime(x0)
1743         time_to = self.RBPixelToTime(x1)
1744         return (time_from, time_to)
1745 
1746     def RBEvent(self, x0, x1):
1747         time_from, time_to = self.RBEventTimes(x0, x1)
1748         self.event_handler.RangeEvent(time_from, time_to)
1749 
1750     def RBMoveEvent(self, x0, x1):
1751         if x1 < x0:
1752             x0, x1 = x1, x0
1753         self.RBEvent(x0, x1)
1754 
1755     def RBReleaseEvent(self, x0, x1, selection_state):
1756         if x1 < x0:
1757             x0, x1 = x1, x0
1758         x0, x1 = self.RBXRangeToPixel(x0, x1)
1759         self.event_handler.SelectEvent(x0, x1, selection_state)
1760 
1761 # Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
1762 
1763 class VerticalBracketGraphicsItem(QGraphicsItem):
1764 
1765     def __init__(self, parent=None):
1766         super(VerticalBracketGraphicsItem, self).__init__(parent)
1767 
1768         self.width = 0
1769         self.height = 0
1770         self.hide()
1771 
1772     def SetSize(self, width, height):
1773         self.width = width + 1
1774         self.height = height + 1
1775 
1776     def boundingRect(self):
1777         return QRectF(0, 0, self.width, self.height)
1778 
1779     def paint(self, painter, option, widget):
1780         colour = QColor(255, 255, 0, 32)
1781         painter.fillRect(0, 0, self.width, self.height, colour)
1782         x1 = self.width - 1
1783         y1 = self.height - 1
1784         painter.drawLine(0, 0, x1, 0)
1785         painter.drawLine(0, 0, 0, 3)
1786         painter.drawLine(x1, 0, x1, 3)
1787         painter.drawLine(0, y1, x1, y1)
1788         painter.drawLine(0, y1, 0, y1 - 3)
1789         painter.drawLine(x1, y1, x1, y1 - 3)
1790 
1791 # Graphics item to contain graphs arranged vertically
1792 
1793 class VertcalGraphSetGraphicsItem(QGraphicsItem):
1794 
1795     def __init__(self, collection, attrs, event_handler, child_class, parent=None):
1796         super(VertcalGraphSetGraphicsItem, self).__init__(parent)
1797 
1798         self.collection = collection
1799 
1800         self.top = 10
1801 
1802         self.width = 0
1803         self.height = self.top
1804 
1805         self.rubber_band = None
1806         self.rb_enabled = False
1807 
1808         first = True
1809         for data in collection.data:
1810             child = child_class(collection, data, attrs, event_handler, first, self)
1811             child.setPos(0, self.height + 1)
1812             rect = child.boundingRect()
1813             if rect.right() > self.width:
1814                 self.width = rect.right()
1815             self.height = self.height + rect.bottom() + 1
1816             first = False
1817 
1818         self.bracket = VerticalBracketGraphicsItem(self)
1819 
1820     def EnableRubberBand(self, xlo, xhi, rb_event_handler):
1821         if self.rb_enabled:
1822             return
1823         self.rb_enabled = True
1824         self.rb_in_view = False
1825         self.setAcceptedMouseButtons(Qt.LeftButton)
1826         self.rb_xlo = xlo
1827         self.rb_xhi = xhi
1828         self.rb_event_handler = rb_event_handler
1829         self.mousePressEvent = self.MousePressEvent
1830         self.mouseMoveEvent = self.MouseMoveEvent
1831         self.mouseReleaseEvent = self.MouseReleaseEvent
1832 
1833     def boundingRect(self):
1834         return QRectF(0, 0, self.width, self.height)
1835 
1836     def paint(self, painter, option, widget):
1837         pass
1838 
1839     def RubberBandParent(self):
1840         scene = self.scene()
1841         view = scene.views()[0]
1842         viewport = view.viewport()
1843         return viewport
1844 
1845     def RubberBandSetGeometry(self, rect):
1846         scene_rectf = self.mapRectToScene(QRectF(rect))
1847         scene = self.scene()
1848         view = scene.views()[0]
1849         poly = view.mapFromScene(scene_rectf)
1850         self.rubber_band.setGeometry(poly.boundingRect())
1851 
1852     def SetSelection(self, selection_state):
1853         if self.rubber_band:
1854             if selection_state:
1855                 self.RubberBandSetGeometry(selection_state)
1856                 self.rubber_band.show()
1857             else:
1858                 self.rubber_band.hide()
1859 
1860     def SetBracket(self, rect):
1861         if rect:
1862             x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
1863             self.bracket.setPos(x, y)
1864             self.bracket.SetSize(width, height)
1865             self.bracket.show()
1866         else:
1867             self.bracket.hide()
1868 
1869     def RubberBandX(self, event):
1870         x = event.pos().toPoint().x()
1871         if x < self.rb_xlo:
1872             x = self.rb_xlo
1873         elif x > self.rb_xhi:
1874             x = self.rb_xhi
1875         else:
1876             self.rb_in_view = True
1877         return x
1878 
1879     def RubberBandRect(self, x):
1880         if self.rb_origin.x() <= x:
1881             width = x - self.rb_origin.x()
1882             rect = QRect(self.rb_origin, QSize(width, self.height))
1883         else:
1884             width = self.rb_origin.x() - x
1885             top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
1886             rect = QRect(top_left, QSize(width, self.height))
1887         return rect
1888 
1889     def MousePressEvent(self, event):
1890         self.rb_in_view = False
1891         x = self.RubberBandX(event)
1892         self.rb_origin = QPoint(x, self.top)
1893         if self.rubber_band is None:
1894             self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
1895         self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
1896         if self.rb_in_view:
1897             self.rubber_band.show()
1898             self.rb_event_handler.RBMoveEvent(x, x)
1899         else:
1900             self.rubber_band.hide()
1901 
1902     def MouseMoveEvent(self, event):
1903         x = self.RubberBandX(event)
1904         rect = self.RubberBandRect(x)
1905         self.RubberBandSetGeometry(rect)
1906         if self.rb_in_view:
1907             self.rubber_band.show()
1908             self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
1909 
1910     def MouseReleaseEvent(self, event):
1911         x = self.RubberBandX(event)
1912         if self.rb_in_view:
1913             selection_state = self.RubberBandRect(x)
1914         else:
1915             selection_state = None
1916         self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
1917 
1918 # Switch graph legend data model
1919 
1920 class SwitchGraphLegendModel(QAbstractTableModel):
1921 
1922     def __init__(self, collection, region_attributes, parent=None):
1923         super(SwitchGraphLegendModel, self).__init__(parent)
1924 
1925         self.region_attributes = region_attributes
1926 
1927         self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
1928         self.child_count = len(self.child_items)
1929 
1930         self.highlight_set = set()
1931 
1932         self.column_headers = ("pid", "tid", "comm")
1933 
1934     def rowCount(self, parent):
1935         return self.child_count
1936 
1937     def headerData(self, section, orientation, role):
1938         if role != Qt.DisplayRole:
1939             return None
1940         if orientation != Qt.Horizontal:
1941             return None
1942         return self.columnHeader(section)
1943 
1944     def index(self, row, column, parent):
1945         return self.createIndex(row, column, self.child_items[row])
1946 
1947     def columnCount(self, parent=None):
1948         return len(self.column_headers)
1949 
1950     def columnHeader(self, column):
1951         return self.column_headers[column]
1952 
1953     def data(self, index, role):
1954         if role == Qt.BackgroundRole:
1955             child = self.child_items[index.row()]
1956             if child in self.highlight_set:
1957                 return self.region_attributes[child.key].colour
1958             return None
1959         if role == Qt.ForegroundRole:
1960             child = self.child_items[index.row()]
1961             if child in self.highlight_set:
1962                 return QColor(255, 255, 255)
1963             return self.region_attributes[child.key].colour
1964         if role != Qt.DisplayRole:
1965             return None
1966         hregion = self.child_items[index.row()]
1967         col = index.column()
1968         if col == 0:
1969             return hregion.pid
1970         if col == 1:
1971             return hregion.tid
1972         if col == 2:
1973             return hregion.comm
1974         return None
1975 
1976     def SetHighlight(self, row, set_highlight):
1977         child = self.child_items[row]
1978         top_left = self.createIndex(row, 0, child)
1979         bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
1980         self.dataChanged.emit(top_left, bottom_right)
1981 
1982     def Highlight(self, highlight_set):
1983         for row in xrange(self.child_count):
1984             child = self.child_items[row]
1985             if child in self.highlight_set:
1986                 if child not in highlight_set:
1987                     self.SetHighlight(row, False)
1988             elif child in highlight_set:
1989                 self.SetHighlight(row, True)
1990         self.highlight_set = highlight_set
1991 
1992 # Switch graph legend is a table
1993 
1994 class SwitchGraphLegend(QWidget):
1995 
1996     def __init__(self, collection, region_attributes, parent=None):
1997         super(SwitchGraphLegend, self).__init__(parent)
1998 
1999         self.data_model = SwitchGraphLegendModel(collection, region_attributes)
2000 
2001         self.model = QSortFilterProxyModel()
2002         self.model.setSourceModel(self.data_model)
2003 
2004         self.view = QTableView()
2005         self.view.setModel(self.model)
2006         self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2007         self.view.verticalHeader().setVisible(False)
2008         self.view.sortByColumn(-1, Qt.AscendingOrder)
2009         self.view.setSortingEnabled(True)
2010         self.view.resizeColumnsToContents()
2011         self.view.resizeRowsToContents()
2012 
2013         self.vbox = VBoxLayout(self.view)
2014         self.setLayout(self.vbox)
2015 
2016         sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
2017         sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
2018         self.saved_size = sz1
2019 
2020     def resizeEvent(self, event):
2021         self.saved_size = self.size().width()
2022         super(SwitchGraphLegend, self).resizeEvent(event)
2023 
2024     def Highlight(self, highlight_set):
2025         self.data_model.Highlight(highlight_set)
2026         self.update()
2027 
2028     def changeEvent(self, event):
2029         if event.type() == QEvent.FontChange:
2030             self.view.resizeRowsToContents()
2031             self.view.resizeColumnsToContents()
2032             # Need to resize rows again after column resize
2033             self.view.resizeRowsToContents()
2034         super(SwitchGraphLegend, self).changeEvent(event)
2035 
2036 # Random colour generation
2037 
2038 def RGBColourTooLight(r, g, b):
2039     if g > 230:
2040         return True
2041     if g <= 160:
2042         return False
2043     if r <= 180 and g <= 180:
2044         return False
2045     if r < 60:
2046         return False
2047     return True
2048 
2049 def GenerateColours(x):
2050     cs = [0]
2051     for i in xrange(1, x):
2052         cs.append(int((255.0 / i) + 0.5))
2053     colours = []
2054     for r in cs:
2055         for g in cs:
2056             for b in cs:
2057                 # Exclude black and colours that look too light against a white background
2058                 if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
2059                     continue
2060                 colours.append(QColor(r, g, b))
2061     return colours
2062 
2063 def GenerateNColours(n):
2064     for x in xrange(2, n + 2):
2065         colours = GenerateColours(x)
2066         if len(colours) >= n:
2067             return colours
2068     return []
2069 
2070 def GenerateNRandomColours(n, seed):
2071     colours = GenerateNColours(n)
2072     random.seed(seed)
2073     random.shuffle(colours)
2074     return colours
2075 
2076 # Graph attributes, in particular the scale and subrange that change when zooming
2077 
2078 class GraphAttributes():
2079 
2080     def __init__(self, scale, subrange, region_attributes, dp):
2081         self.scale = scale
2082         self.subrange = subrange
2083         self.region_attributes = region_attributes
2084         # Rounding avoids errors due to finite floating point precision
2085         self.dp = dp    # data decimal places
2086         self.Update()
2087 
2088     def XToPixel(self, x):
2089         return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
2090 
2091     def YToPixel(self, y):
2092         return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
2093 
2094     def PixelToXRounded(self, px):
2095         return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
2096 
2097     def PixelToYRounded(self, py):
2098         return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
2099 
2100     def PixelToX(self, px):
2101         x = self.PixelToXRounded(px)
2102         if self.pdp.x == 0:
2103             rt = self.XToPixel(x)
2104             if rt > px:
2105                 return x - 1
2106         return x
2107 
2108     def PixelToY(self, py):
2109         y = self.PixelToYRounded(py)
2110         if self.pdp.y == 0:
2111             rt = self.YToPixel(y)
2112             if rt > py:
2113                 return y - 1
2114         return y
2115 
2116     def ToPDP(self, dp, scale):
2117         # Calculate pixel decimal places:
2118         #    (10 ** dp) is the minimum delta in the data
2119         #    scale it to get the minimum delta in pixels
2120         #    log10 gives the number of decimals places negatively
2121         #    subtrace 1 to divide by 10
2122         #    round to the lower negative number
2123         #    change the sign to get the number of decimals positively
2124         x = math.log10((10 ** dp) * scale)
2125         if x < 0:
2126             x -= 1
2127             x = -int(math.floor(x) - 0.1)
2128         else:
2129             x = 0
2130         return x
2131 
2132     def Update(self):
2133         x = self.ToPDP(self.dp.x, self.scale.x)
2134         y = self.ToPDP(self.dp.y, self.scale.y)
2135         self.pdp = XY(x, y) # pixel decimal places
2136 
2137 # Switch graph splitter which divides the CPU graphs from the legend
2138 
2139 class SwitchGraphSplitter(QSplitter):
2140 
2141     def __init__(self, parent=None):
2142         super(SwitchGraphSplitter, self).__init__(parent)
2143 
2144         self.first_time = False
2145 
2146     def resizeEvent(self, ev):
2147         if self.first_time:
2148             self.first_time = False
2149             sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
2150             sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
2151             sz0 = self.size().width() - self.handleWidth() - sz1
2152             self.setSizes([sz0, sz1])
2153         elif not(self.widget(1).saved_size is None):
2154             sz1 = self.widget(1).saved_size
2155             sz0 = self.size().width() - self.handleWidth() - sz1
2156             self.setSizes([sz0, sz1])
2157         super(SwitchGraphSplitter, self).resizeEvent(ev)
2158 
2159 # Graph widget base class
2160 
2161 class GraphWidget(QWidget):
2162 
2163     graph_title_changed = Signal(object)
2164 
2165     def __init__(self, parent=None):
2166         super(GraphWidget, self).__init__(parent)
2167 
2168     def GraphTitleChanged(self, title):
2169         self.graph_title_changed.emit(title)
2170 
2171     def Title(self):
2172         return ""
2173 
2174 # Display time in s, ms, us or ns
2175 
2176 def ToTimeStr(val):
2177     val = Decimal(val)
2178     if val >= 1000000000:
2179         return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
2180     if val >= 1000000:
2181         return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
2182     if val >= 1000:
2183         return "{} us".format((val / 1000).quantize(Decimal("0.001")))
2184     return "{} ns".format(val.quantize(Decimal("1")))
2185 
2186 # Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
2187 
2188 class SwitchGraphWidget(GraphWidget):
2189 
2190     def __init__(self, glb, collection, parent=None):
2191         super(SwitchGraphWidget, self).__init__(parent)
2192 
2193         self.glb = glb
2194         self.collection = collection
2195 
2196         self.back_state = []
2197         self.forward_state = []
2198         self.selection_state = (None, None)
2199         self.fwd_rect = None
2200         self.start_time = self.glb.StartTime(collection.machine_id)
2201 
2202         i = 0
2203         hregions = collection.hregions.values()
2204         colours = GenerateNRandomColours(len(hregions), 1013)
2205         region_attributes = {}
2206         for hregion in hregions:
2207             if hregion.pid == 0 and hregion.tid == 0:
2208                 region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
2209             else:
2210                 region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
2211                 i = i + 1
2212 
2213         # Default to entire range
2214         xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
2215         ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
2216         subrange = XY(xsubrange, ysubrange)
2217 
2218         scale = self.GetScaleForRange(subrange)
2219 
2220         self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
2221 
2222         self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
2223 
2224         self.scene = QGraphicsScene()
2225         self.scene.addItem(self.item)
2226 
2227         self.view = QGraphicsView(self.scene)
2228         self.view.centerOn(0, 0)
2229         self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
2230 
2231         self.legend = SwitchGraphLegend(collection, region_attributes)
2232 
2233         self.splitter = SwitchGraphSplitter()
2234         self.splitter.addWidget(self.view)
2235         self.splitter.addWidget(self.legend)
2236 
2237         self.point_label = QLabel("")
2238         self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
2239 
2240         self.back_button = QToolButton()
2241         self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
2242         self.back_button.setDisabled(True)
2243         self.back_button.released.connect(lambda: self.Back())
2244 
2245         self.forward_button = QToolButton()
2246         self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
2247         self.forward_button.setDisabled(True)
2248         self.forward_button.released.connect(lambda: self.Forward())
2249 
2250         self.zoom_button = QToolButton()
2251         self.zoom_button.setText("Zoom")
2252         self.zoom_button.setDisabled(True)
2253         self.zoom_button.released.connect(lambda: self.Zoom())
2254 
2255         self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
2256 
2257         self.vbox = VBoxLayout(self.splitter, self.hbox)
2258 
2259         self.setLayout(self.vbox)
2260 
2261     def GetScaleForRangeX(self, xsubrange):
2262         # Default graph 1000 pixels wide
2263         dflt = 1000.0
2264         r = xsubrange.hi - xsubrange.lo
2265         return dflt / r
2266 
2267     def GetScaleForRangeY(self, ysubrange):
2268         # Default graph 50 pixels high
2269         dflt = 50.0
2270         r = ysubrange.hi - ysubrange.lo
2271         return dflt / r
2272 
2273     def GetScaleForRange(self, subrange):
2274         # Default graph 1000 pixels wide, 50 pixels high
2275         xscale = self.GetScaleForRangeX(subrange.x)
2276         yscale = self.GetScaleForRangeY(subrange.y)
2277         return XY(xscale, yscale)
2278 
2279     def PointEvent(self, cpu, time_from, time_to, hregions):
2280         text = "CPU: " + str(cpu)
2281         time_from = time_from.quantize(Decimal(1))
2282         rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
2283         text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
2284         self.point_label.setText(text)
2285         self.legend.Highlight(hregions)
2286 
2287     def RightClickEvent(self, cpu, hregion_times, pos):
2288         if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
2289             return
2290         menu = QMenu(self.view)
2291         for hregion, time in hregion_times:
2292             thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
2293             menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
2294             menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
2295         menu.exec_(pos)
2296 
2297     def RightClickSelect(self, args):
2298         CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
2299 
2300     def NoPointEvent(self):
2301         self.point_label.setText("")
2302         self.legend.Highlight({})
2303 
2304     def RangeEvent(self, time_from, time_to):
2305         time_from = time_from.quantize(Decimal(1))
2306         time_to = time_to.quantize(Decimal(1))
2307         if time_to <= time_from:
2308             self.point_label.setText("")
2309             return
2310         rel_time_from = time_from - self.start_time
2311         rel_time_to = time_to - self.start_time
2312         text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
2313         text = text + " duration: " + ToTimeStr(time_to - time_from)
2314         self.point_label.setText(text)
2315 
2316     def BackState(self):
2317         return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
2318 
2319     def PushBackState(self):
2320         state = copy.deepcopy(self.BackState())
2321         self.back_state.append(state)
2322         self.back_button.setEnabled(True)
2323 
2324     def PopBackState(self):
2325         self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
2326         self.attrs.Update()
2327         if not self.back_state:
2328             self.back_button.setDisabled(True)
2329 
2330     def PushForwardState(self):
2331         state = copy.deepcopy(self.BackState())
2332         self.forward_state.append(state)
2333         self.forward_button.setEnabled(True)
2334 
2335     def PopForwardState(self):
2336         self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
2337         self.attrs.Update()
2338         if not self.forward_state:
2339             self.forward_button.setDisabled(True)
2340 
2341     def Title(self):
2342         time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
2343         time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
2344         rel_time_from = time_from - self.start_time
2345         rel_time_to = time_to - self.start_time
2346         title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
2347         title = title + " (" + ToTimeStr(time_to - time_from) + ")"
2348         return title
2349 
2350     def Update(self):
2351         selected_subrange, selection_state = self.selection_state
2352         self.item.SetSelection(selection_state)
2353         self.item.SetBracket(self.fwd_rect)
2354         self.zoom_button.setDisabled(selected_subrange is None)
2355         self.GraphTitleChanged(self.Title())
2356         self.item.update(self.item.boundingRect())
2357 
2358     def Back(self):
2359         if not self.back_state:
2360             return
2361         self.PushForwardState()
2362         self.PopBackState()
2363         self.Update()
2364 
2365     def Forward(self):
2366         if not self.forward_state:
2367             return
2368         self.PushBackState()
2369         self.PopForwardState()
2370         self.Update()
2371 
2372     def SelectEvent(self, x0, x1, selection_state):
2373         if selection_state is None:
2374             selected_subrange = None
2375         else:
2376             if x1 - x0 < 1.0:
2377                 x1 += 1.0
2378             selected_subrange = Subrange(x0, x1)
2379         self.selection_state = (selected_subrange, selection_state)
2380         self.zoom_button.setDisabled(selected_subrange is None)
2381 
2382     def Zoom(self):
2383         selected_subrange, selection_state = self.selection_state
2384         if selected_subrange is None:
2385             return
2386         self.fwd_rect = selection_state
2387         self.item.SetSelection(None)
2388         self.PushBackState()
2389         self.attrs.subrange.x = selected_subrange
2390         self.forward_state = []
2391         self.forward_button.setDisabled(True)
2392         self.selection_state = (None, None)
2393         self.fwd_rect = None
2394         self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
2395         self.attrs.Update()
2396         self.Update()
2397 
2398 # Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
2399 
2400 class SlowInitClass():
2401 
2402     def __init__(self, glb, title, init_fn):
2403         self.init_fn = init_fn
2404         self.done = False
2405         self.result = None
2406 
2407         self.msg_box = QMessageBox(glb.mainwindow)
2408         self.msg_box.setText("Initializing " + title + ". Please wait.")
2409         self.msg_box.setWindowTitle("Initializing " + title)
2410         self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
2411 
2412         self.init_thread = Thread(self.ThreadFn, glb)
2413         self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
2414 
2415         self.init_thread.start()
2416 
2417     def Done(self):
2418         self.msg_box.done(0)
2419 
2420     def ThreadFn(self, glb):
2421         conn_name = "SlowInitClass" + str(os.getpid())
2422         db, dbname = glb.dbref.Open(conn_name)
2423         self.result = self.init_fn(db)
2424         self.done = True
2425         return (True, 0)
2426 
2427     def Result(self):
2428         while not self.done:
2429             self.msg_box.exec_()
2430         self.init_thread.wait()
2431         return self.result
2432 
2433 def SlowInit(glb, title, init_fn):
2434     init = SlowInitClass(glb, title, init_fn)
2435     return init.Result()
2436 
2437 # Time chart by CPU window
2438 
2439 class TimeChartByCPUWindow(QMdiSubWindow):
2440 
2441     def __init__(self, glb, parent=None):
2442         super(TimeChartByCPUWindow, self).__init__(parent)
2443 
2444         self.glb = glb
2445         self.machine_id = glb.HostMachineId()
2446         self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
2447 
2448         collection = LookupModel(self.collection_name)
2449         if collection is None:
2450             collection = SlowInit(glb, "Time Chart", self.Init)
2451 
2452         self.widget = SwitchGraphWidget(glb, collection, self)
2453         self.view = self.widget
2454 
2455         self.base_title = "Time Chart by CPU"
2456         self.setWindowTitle(self.base_title + self.widget.Title())
2457         self.widget.graph_title_changed.connect(self.GraphTitleChanged)
2458 
2459         self.setWidget(self.widget)
2460 
2461         AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
2462 
2463     def Init(self, db):
2464         return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
2465 
2466     def GraphTitleChanged(self, title):
2467         self.setWindowTitle(self.base_title + " : " + title)
2468 
2469 # Child data item  finder
2470 
2471 class ChildDataItemFinder():
2472 
2473     def __init__(self, root):
2474         self.root = root
2475         self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
2476         self.rows = []
2477         self.pos = 0
2478 
2479     def FindSelect(self):
2480         self.rows = []
2481         if self.pattern:
2482             pattern = re.compile(self.value)
2483             for child in self.root.child_items:
2484                 for column_data in child.data:
2485                     if re.search(pattern, str(column_data)) is not None:
2486                         self.rows.append(child.row)
2487                         break
2488         else:
2489             for child in self.root.child_items:
2490                 for column_data in child.data:
2491                     if self.value in str(column_data):
2492                         self.rows.append(child.row)
2493                         break
2494 
2495     def FindValue(self):
2496         self.pos = 0
2497         if self.last_value != self.value or self.pattern != self.last_pattern:
2498             self.FindSelect()
2499         if not len(self.rows):
2500             return -1
2501         return self.rows[self.pos]
2502 
2503     def FindThread(self):
2504         if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
2505             row = self.FindValue()
2506         elif len(self.rows):
2507             if self.direction > 0:
2508                 self.pos += 1
2509                 if self.pos >= len(self.rows):
2510                     self.pos = 0
2511             else:
2512                 self.pos -= 1
2513                 if self.pos < 0:
2514                     self.pos = len(self.rows) - 1
2515             row = self.rows[self.pos]
2516         else:
2517             row = -1
2518         return (True, row)
2519 
2520     def Find(self, value, direction, pattern, context, callback):
2521         self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
2522         # Use a thread so the UI is not blocked
2523         thread = Thread(self.FindThread)
2524         thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
2525         thread.start()
2526 
2527     def FindDone(self, thread, callback, row):
2528         callback(row)
2529 
2530 # Number of database records to fetch in one go
2531 
2532 glb_chunk_sz = 10000
2533 
2534 # Background process for SQL data fetcher
2535 
2536 class SQLFetcherProcess():
2537 
2538     def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
2539         # Need a unique connection name
2540         conn_name = "SQLFetcher" + str(os.getpid())
2541         self.db, dbname = dbref.Open(conn_name)
2542         self.sql = sql
2543         self.buffer = buffer
2544         self.head = head
2545         self.tail = tail
2546         self.fetch_count = fetch_count
2547         self.fetching_done = fetching_done
2548         self.process_target = process_target
2549         self.wait_event = wait_event
2550         self.fetched_event = fetched_event
2551         self.prep = prep
2552         self.query = QSqlQuery(self.db)
2553         self.query_limit = 0 if "$$last_id$$" in sql else 2
2554         self.last_id = -1
2555         self.fetched = 0
2556         self.more = True
2557         self.local_head = self.head.value
2558         self.local_tail = self.tail.value
2559 
2560     def Select(self):
2561         if self.query_limit:
2562             if self.query_limit == 1:
2563                 return
2564             self.query_limit -= 1
2565         stmt = self.sql.replace("$$last_id$$", str(self.last_id))
2566         QueryExec(self.query, stmt)
2567 
2568     def Next(self):
2569         if not self.query.next():
2570             self.Select()
2571             if not self.query.next():
2572                 return None
2573         self.last_id = self.query.value(0)
2574         return self.prep(self.query)
2575 
2576     def WaitForTarget(self):
2577         while True:
2578             self.wait_event.clear()
2579             target = self.process_target.value
2580             if target > self.fetched or target < 0:
2581                 break
2582             self.wait_event.wait()
2583         return target
2584 
2585     def HasSpace(self, sz):
2586         if self.local_tail <= self.local_head:
2587             space = len(self.buffer) - self.local_head
2588             if space > sz:
2589                 return True
2590             if space >= glb_nsz:
2591                 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
2592                 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
2593                 self.buffer[self.local_head : self.local_head + len(nd)] = nd
2594             self.local_head = 0
2595         if self.local_tail - self.local_head > sz:
2596             return True
2597         return False
2598 
2599     def WaitForSpace(self, sz):
2600         if self.HasSpace(sz):
2601             return
2602         while True:
2603             self.wait_event.clear()
2604             self.local_tail = self.tail.value
2605             if self.HasSpace(sz):
2606                 return
2607             self.wait_event.wait()
2608 
2609     def AddToBuffer(self, obj):
2610         d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
2611         n = len(d)
2612         nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
2613         sz = n + glb_nsz
2614         self.WaitForSpace(sz)
2615         pos = self.local_head
2616         self.buffer[pos : pos + len(nd)] = nd
2617         self.buffer[pos + glb_nsz : pos + sz] = d
2618         self.local_head += sz
2619 
2620     def FetchBatch(self, batch_size):
2621         fetched = 0
2622         while batch_size > fetched:
2623             obj = self.Next()
2624             if obj is None:
2625                 self.more = False
2626                 break
2627             self.AddToBuffer(obj)
2628             fetched += 1
2629         if fetched:
2630             self.fetched += fetched
2631             with self.fetch_count.get_lock():
2632                 self.fetch_count.value += fetched
2633             self.head.value = self.local_head
2634             self.fetched_event.set()
2635 
2636     def Run(self):
2637         while self.more:
2638             target = self.WaitForTarget()
2639             if target < 0:
2640                 break
2641             batch_size = min(glb_chunk_sz, target - self.fetched)
2642             self.FetchBatch(batch_size)
2643         self.fetching_done.value = True
2644         self.fetched_event.set()
2645 
2646 def SQLFetcherFn(*x):
2647     process = SQLFetcherProcess(*x)
2648     process.Run()
2649 
2650 # SQL data fetcher
2651 
2652 class SQLFetcher(QObject):
2653 
2654     done = Signal(object)
2655 
2656     def __init__(self, glb, sql, prep, process_data, parent=None):
2657         super(SQLFetcher, self).__init__(parent)
2658         self.process_data = process_data
2659         self.more = True
2660         self.target = 0
2661         self.last_target = 0
2662         self.fetched = 0
2663         self.buffer_size = 16 * 1024 * 1024
2664         self.buffer = Array(c_char, self.buffer_size, lock=False)
2665         self.head = Value(c_longlong)
2666         self.tail = Value(c_longlong)
2667         self.local_tail = 0
2668         self.fetch_count = Value(c_longlong)
2669         self.fetching_done = Value(c_bool)
2670         self.last_count = 0
2671         self.process_target = Value(c_longlong)
2672         self.wait_event = Event()
2673         self.fetched_event = Event()
2674         glb.AddInstanceToShutdownOnExit(self)
2675         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))
2676         self.process.start()
2677         self.thread = Thread(self.Thread)
2678         self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
2679         self.thread.start()
2680 
2681     def Shutdown(self):
2682         # Tell the thread and process to exit
2683         self.process_target.value = -1
2684         self.wait_event.set()
2685         self.more = False
2686         self.fetching_done.value = True
2687         self.fetched_event.set()
2688 
2689     def Thread(self):
2690         if not self.more:
2691             return True, 0
2692         while True:
2693             self.fetched_event.clear()
2694             fetch_count = self.fetch_count.value
2695             if fetch_count != self.last_count:
2696                 break
2697             if self.fetching_done.value:
2698                 self.more = False
2699                 return True, 0
2700             self.fetched_event.wait()
2701         count = fetch_count - self.last_count
2702         self.last_count = fetch_count
2703         self.fetched += count
2704         return False, count
2705 
2706     def Fetch(self, nr):
2707         if not self.more:
2708             # -1 inidcates there are no more
2709             return -1
2710         result = self.fetched
2711         extra = result + nr - self.target
2712         if extra > 0:
2713             self.target += extra
2714             # process_target < 0 indicates shutting down
2715             if self.process_target.value >= 0:
2716                 self.process_target.value = self.target
2717             self.wait_event.set()
2718         return result
2719 
2720     def RemoveFromBuffer(self):
2721         pos = self.local_tail
2722         if len(self.buffer) - pos < glb_nsz:
2723             pos = 0
2724         n = pickle.loads(self.buffer[pos : pos + glb_nsz])
2725         if n == 0:
2726             pos = 0
2727             n = pickle.loads(self.buffer[0 : glb_nsz])
2728         pos += glb_nsz
2729         obj = pickle.loads(self.buffer[pos : pos + n])
2730         self.local_tail = pos + n
2731         return obj
2732 
2733     def ProcessData(self, count):
2734         for i in xrange(count):
2735             obj = self.RemoveFromBuffer()
2736             self.process_data(obj)
2737         self.tail.value = self.local_tail
2738         self.wait_event.set()
2739         self.done.emit(count)
2740 
2741 # Fetch more records bar
2742 
2743 class FetchMoreRecordsBar():
2744 
2745     def __init__(self, model, parent):
2746         self.model = model
2747 
2748         self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
2749         self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2750 
2751         self.fetch_count = QSpinBox()
2752         self.fetch_count.setRange(1, 1000000)
2753         self.fetch_count.setValue(10)
2754         self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2755 
2756         self.fetch = QPushButton("Go!")
2757         self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2758         self.fetch.released.connect(self.FetchMoreRecords)
2759 
2760         self.progress = QProgressBar()
2761         self.progress.setRange(0, 100)
2762         self.progress.hide()
2763 
2764         self.done_label = QLabel("All records fetched")
2765         self.done_label.hide()
2766 
2767         self.spacer = QLabel("")
2768 
2769         self.close_button = QToolButton()
2770         self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
2771         self.close_button.released.connect(self.Deactivate)
2772 
2773         self.hbox = QHBoxLayout()
2774         self.hbox.setContentsMargins(0, 0, 0, 0)
2775 
2776         self.hbox.addWidget(self.label)
2777         self.hbox.addWidget(self.fetch_count)
2778         self.hbox.addWidget(self.fetch)
2779         self.hbox.addWidget(self.spacer)
2780         self.hbox.addWidget(self.progress)
2781         self.hbox.addWidget(self.done_label)
2782         self.hbox.addWidget(self.close_button)
2783 
2784         self.bar = QWidget()
2785         self.bar.setLayout(self.hbox)
2786         self.bar.show()
2787 
2788         self.in_progress = False
2789         self.model.progress.connect(self.Progress)
2790 
2791         self.done = False
2792 
2793         if not model.HasMoreRecords():
2794             self.Done()
2795 
2796     def Widget(self):
2797         return self.bar
2798 
2799     def Activate(self):
2800         self.bar.show()
2801         self.fetch.setFocus()
2802 
2803     def Deactivate(self):
2804         self.bar.hide()
2805 
2806     def Enable(self, enable):
2807         self.fetch.setEnabled(enable)
2808         self.fetch_count.setEnabled(enable)
2809 
2810     def Busy(self):
2811         self.Enable(False)
2812         self.fetch.hide()
2813         self.spacer.hide()
2814         self.progress.show()
2815 
2816     def Idle(self):
2817         self.in_progress = False
2818         self.Enable(True)
2819         self.progress.hide()
2820         self.fetch.show()
2821         self.spacer.show()
2822 
2823     def Target(self):
2824         return self.fetch_count.value() * glb_chunk_sz
2825 
2826     def Done(self):
2827         self.done = True
2828         self.Idle()
2829         self.label.hide()
2830         self.fetch_count.hide()
2831         self.fetch.hide()
2832         self.spacer.hide()
2833         self.done_label.show()
2834 
2835     def Progress(self, count):
2836         if self.in_progress:
2837             if count:
2838                 percent = ((count - self.start) * 100) / self.Target()
2839                 if percent >= 100:
2840                     self.Idle()
2841                 else:
2842                     self.progress.setValue(percent)
2843         if not count:
2844             # Count value of zero means no more records
2845             self.Done()
2846 
2847     def FetchMoreRecords(self):
2848         if self.done:
2849             return
2850         self.progress.setValue(0)
2851         self.Busy()
2852         self.in_progress = True
2853         self.start = self.model.FetchMoreRecords(self.Target())
2854 
2855 # Brance data model level two item
2856 
2857 class BranchLevelTwoItem():
2858 
2859     def __init__(self, row, col, text, parent_item):
2860         self.row = row
2861         self.parent_item = parent_item
2862         self.data = [""] * (col + 1)
2863         self.data[col] = text
2864         self.level = 2
2865 
2866     def getParentItem(self):
2867         return self.parent_item
2868 
2869     def getRow(self):
2870         return self.row
2871 
2872     def childCount(self):
2873         return 0
2874 
2875     def hasChildren(self):
2876         return False
2877 
2878     def getData(self, column):
2879         return self.data[column]
2880 
2881 # Brance data model level one item
2882 
2883 class BranchLevelOneItem():
2884 
2885     def __init__(self, glb, row, data, parent_item):
2886         self.glb = glb
2887         self.row = row
2888         self.parent_item = parent_item
2889         self.child_count = 0
2890         self.child_items = []
2891         self.data = data[1:]
2892         self.dbid = data[0]
2893         self.level = 1
2894         self.query_done = False
2895         self.br_col = len(self.data) - 1
2896 
2897     def getChildItem(self, row):
2898         return self.child_items[row]
2899 
2900     def getParentItem(self):
2901         return self.parent_item
2902 
2903     def getRow(self):
2904         return self.row
2905 
2906     def Select(self):
2907         self.query_done = True
2908 
2909         if not self.glb.have_disassembler:
2910             return
2911 
2912         query = QSqlQuery(self.glb.db)
2913 
2914         QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
2915                   " FROM samples"
2916                   " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
2917                   " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
2918                   " WHERE samples.id = " + str(self.dbid))
2919         if not query.next():
2920             return
2921         cpu = query.value(0)
2922         dso = query.value(1)
2923         sym = query.value(2)
2924         if dso == 0 or sym == 0:
2925             return
2926         off = query.value(3)
2927         short_name = query.value(4)
2928         long_name = query.value(5)
2929         build_id = query.value(6)
2930         sym_start = query.value(7)
2931         ip = query.value(8)
2932 
2933         QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
2934                   " FROM samples"
2935                   " INNER JOIN symbols ON samples.symbol_id = symbols.id"
2936                   " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
2937                   " ORDER BY samples.id"
2938                   " LIMIT 1")
2939         if not query.next():
2940             return
2941         if query.value(0) != dso:
2942             # Cannot disassemble from one dso to another
2943             return
2944         bsym = query.value(1)
2945         boff = query.value(2)
2946         bsym_start = query.value(3)
2947         if bsym == 0:
2948             return
2949         tot = bsym_start + boff + 1 - sym_start - off
2950         if tot <= 0 or tot > 16384:
2951             return
2952 
2953         inst = self.glb.disassembler.Instruction()
2954         f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
2955         if not f:
2956             return
2957         mode = 0 if Is64Bit(f) else 1
2958         self.glb.disassembler.SetMode(inst, mode)
2959 
2960         buf_sz = tot + 16
2961         buf = create_string_buffer(tot + 16)
2962         f.seek(sym_start + off)
2963         buf.value = f.read(buf_sz)
2964         buf_ptr = addressof(buf)
2965         i = 0
2966         while tot > 0:
2967             cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
2968             if cnt:
2969                 byte_str = tohex(ip).rjust(16)
2970                 for k in xrange(cnt):
2971                     byte_str += " %02x" % ord(buf[i])
2972                     i += 1
2973                 while k < 15:
2974                     byte_str += "   "
2975                     k += 1
2976                 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
2977                 self.child_count += 1
2978             else:
2979                 return
2980             buf_ptr += cnt
2981             tot -= cnt
2982             buf_sz -= cnt
2983             ip += cnt
2984 
2985     def childCount(self):
2986         if not self.query_done:
2987             self.Select()
2988             if not self.child_count:
2989                 return -1
2990         return self.child_count
2991 
2992     def hasChildren(self):
2993         if not self.query_done:
2994             return True
2995         return self.child_count > 0
2996 
2997     def getData(self, column):
2998         return self.data[column]
2999 
3000 # Brance data model root item
3001 
3002 class BranchRootItem():
3003 
3004     def __init__(self):
3005         self.child_count = 0
3006         self.child_items = []
3007         self.level = 0
3008 
3009     def getChildItem(self, row):
3010         return self.child_items[row]
3011 
3012     def getParentItem(self):
3013         return None
3014 
3015     def getRow(self):
3016         return 0
3017 
3018     def childCount(self):
3019         return self.child_count
3020 
3021     def hasChildren(self):
3022         return self.child_count > 0
3023 
3024     def getData(self, column):
3025         return ""
3026 
3027 # Calculate instructions per cycle
3028 
3029 def CalcIPC(cyc_cnt, insn_cnt):
3030     if cyc_cnt and insn_cnt:
3031         ipc = Decimal(float(insn_cnt) / cyc_cnt)
3032         ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
3033     else:
3034         ipc = "0"
3035     return ipc
3036 
3037 # Branch data preparation
3038 
3039 def BranchDataPrepBr(query, data):
3040     data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
3041             " (" + dsoname(query.value(11)) + ")" + " -> " +
3042             tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
3043             " (" + dsoname(query.value(15)) + ")")
3044 
3045 def BranchDataPrepIPC(query, data):
3046     insn_cnt = query.value(16)
3047     cyc_cnt = query.value(17)
3048     ipc = CalcIPC(cyc_cnt, insn_cnt)
3049     data.append(insn_cnt)
3050     data.append(cyc_cnt)
3051     data.append(ipc)
3052 
3053 def BranchDataPrep(query):
3054     data = []
3055     for i in xrange(0, 8):
3056         data.append(query.value(i))
3057     BranchDataPrepBr(query, data)
3058     return data
3059 
3060 def BranchDataPrepWA(query):
3061     data = []
3062     data.append(query.value(0))
3063     # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3064     data.append("{:>19}".format(query.value(1)))
3065     for i in xrange(2, 8):
3066         data.append(query.value(i))
3067     BranchDataPrepBr(query, data)
3068     return data
3069 
3070 def BranchDataWithIPCPrep(query):
3071     data = []
3072     for i in xrange(0, 8):
3073         data.append(query.value(i))
3074     BranchDataPrepIPC(query, data)
3075     BranchDataPrepBr(query, data)
3076     return data
3077 
3078 def BranchDataWithIPCPrepWA(query):
3079     data = []
3080     data.append(query.value(0))
3081     # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3082     data.append("{:>19}".format(query.value(1)))
3083     for i in xrange(2, 8):
3084         data.append(query.value(i))
3085     BranchDataPrepIPC(query, data)
3086     BranchDataPrepBr(query, data)
3087     return data
3088 
3089 # Branch data model
3090 
3091 class BranchModel(TreeModel):
3092 
3093     progress = Signal(object)
3094 
3095     def __init__(self, glb, event_id, where_clause, parent=None):
3096         super(BranchModel, self).__init__(glb, None, parent)
3097         self.event_id = event_id
3098         self.more = True
3099         self.populated = 0
3100         self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
3101         if self.have_ipc:
3102             select_ipc = ", insn_count, cyc_count"
3103             prep_fn = BranchDataWithIPCPrep
3104             prep_wa_fn = BranchDataWithIPCPrepWA
3105         else:
3106             select_ipc = ""
3107             prep_fn = BranchDataPrep
3108             prep_wa_fn = BranchDataPrepWA
3109         sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
3110             " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
3111             " ip, symbols.name, sym_offset, dsos.short_name,"
3112             " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
3113             + select_ipc +
3114             " FROM samples"
3115             " INNER JOIN comms ON comm_id = comms.id"
3116             " INNER JOIN threads ON thread_id = threads.id"
3117             " INNER JOIN branch_types ON branch_type = branch_types.id"
3118             " INNER JOIN symbols ON symbol_id = symbols.id"
3119             " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
3120             " INNER JOIN dsos ON samples.dso_id = dsos.id"
3121             " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
3122             " WHERE samples.id > $$last_id$$" + where_clause +
3123             " AND evsel_id = " + str(self.event_id) +
3124             " ORDER BY samples.id"
3125             " LIMIT " + str(glb_chunk_sz))
3126         if pyside_version_1 and sys.version_info[0] == 3:
3127             prep = prep_fn
3128         else:
3129             prep = prep_wa_fn
3130         self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
3131         self.fetcher.done.connect(self.Update)
3132         self.fetcher.Fetch(glb_chunk_sz)
3133 
3134     def GetRoot(self):
3135         return BranchRootItem()
3136 
3137     def columnCount(self, parent=None):
3138         if self.have_ipc:
3139             return 11
3140         else:
3141             return 8
3142 
3143     def columnHeader(self, column):
3144         if self.have_ipc:
3145             return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
3146         else:
3147             return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
3148 
3149     def columnFont(self, column):
3150         if self.have_ipc:
3151             br_col = 10
3152         else:
3153             br_col = 7
3154         if column != br_col:
3155             return None
3156         return QFont("Monospace")
3157 
3158     def DisplayData(self, item, index):
3159         if item.level == 1:
3160             self.FetchIfNeeded(item.row)
3161         return item.getData(index.column())
3162 
3163     def AddSample(self, data):
3164         child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
3165         self.root.child_items.append(child)
3166         self.populated += 1
3167 
3168     def Update(self, fetched):
3169         if not fetched:
3170             self.more = False
3171             self.progress.emit(0)
3172         child_count = self.root.child_count
3173         count = self.populated - child_count
3174         if count > 0:
3175             parent = QModelIndex()
3176             self.beginInsertRows(parent, child_count, child_count + count - 1)
3177             self.insertRows(child_count, count, parent)
3178             self.root.child_count += count
3179             self.endInsertRows()
3180             self.progress.emit(self.root.child_count)
3181 
3182     def FetchMoreRecords(self, count):
3183         current = self.root.child_count
3184         if self.more:
3185             self.fetcher.Fetch(count)
3186         else:
3187             self.progress.emit(0)
3188         return current
3189 
3190     def HasMoreRecords(self):
3191         return self.more
3192 
3193 # Report Variables
3194 
3195 class ReportVars():
3196 
3197     def __init__(self, name = "", where_clause = "", limit = ""):
3198         self.name = name
3199         self.where_clause = where_clause
3200         self.limit = limit
3201 
3202     def UniqueId(self):
3203         return str(self.where_clause + ";" + self.limit)
3204 
3205 # Branch window
3206 
3207 class BranchWindow(QMdiSubWindow):
3208 
3209     def __init__(self, glb, event_id, report_vars, parent=None):
3210         super(BranchWindow, self).__init__(parent)
3211 
3212         model_name = "Branch Events " + str(event_id) +  " " + report_vars.UniqueId()
3213 
3214         self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
3215 
3216         self.view = QTreeView()
3217         self.view.setUniformRowHeights(True)
3218         self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
3219         self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
3220         self.view.setModel(self.model)
3221 
3222         self.ResizeColumnsToContents()
3223 
3224         self.context_menu = TreeContextMenu(self.view)
3225 
3226         self.find_bar = FindBar(self, self, True)
3227 
3228         self.finder = ChildDataItemFinder(self.model.root)
3229 
3230         self.fetch_bar = FetchMoreRecordsBar(self.model, self)
3231 
3232         self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
3233 
3234         self.setWidget(self.vbox.Widget())
3235 
3236         AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
3237 
3238     def ResizeColumnToContents(self, column, n):
3239         # Using the view's resizeColumnToContents() here is extrememly slow
3240         # so implement a crude alternative
3241         mm = "MM" if column else "MMMM"
3242         font = self.view.font()
3243         metrics = QFontMetrics(font)
3244         max = 0
3245         for row in xrange(n):
3246             val = self.model.root.child_items[row].data[column]
3247             len = metrics.width(str(val) + mm)
3248             max = len if len > max else max
3249         val = self.model.columnHeader(column)
3250         len = metrics.width(str(val) + mm)
3251         max = len if len > max else max
3252         self.view.setColumnWidth(column, max)
3253 
3254     def ResizeColumnsToContents(self):
3255         n = min(self.model.root.child_count, 100)
3256         if n < 1:
3257             # No data yet, so connect a signal to notify when there is
3258             self.model.rowsInserted.connect(self.UpdateColumnWidths)
3259             return
3260         columns = self.model.columnCount()
3261         for i in xrange(columns):
3262             self.ResizeColumnToContents(i, n)
3263 
3264     def UpdateColumnWidths(self, *x):
3265         # This only needs to be done once, so disconnect the signal now
3266         self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
3267         self.ResizeColumnsToContents()
3268 
3269     def Find(self, value, direction, pattern, context):
3270         self.view.setFocus()
3271         self.find_bar.Busy()
3272         self.finder.Find(value, direction, pattern, context, self.FindDone)
3273 
3274     def FindDone(self, row):
3275         self.find_bar.Idle()
3276         if row >= 0:
3277             self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
3278         else:
3279             self.find_bar.NotFound()
3280 
3281 # Line edit data item
3282 
3283 class LineEditDataItem(object):
3284 
3285     def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3286         self.glb = glb
3287         self.label = label
3288         self.placeholder_text = placeholder_text
3289         self.parent = parent
3290         self.id = id
3291 
3292         self.value = default
3293 
3294         self.widget = QLineEdit(default)
3295         self.widget.editingFinished.connect(self.Validate)
3296         self.widget.textChanged.connect(self.Invalidate)
3297         self.red = False
3298         self.error = ""
3299         self.validated = True
3300 
3301         if placeholder_text:
3302             self.widget.setPlaceholderText(placeholder_text)
3303 
3304     def TurnTextRed(self):
3305         if not self.red:
3306             palette = QPalette()
3307             palette.setColor(QPalette.Text,Qt.red)
3308             self.widget.setPalette(palette)
3309             self.red = True
3310 
3311     def TurnTextNormal(self):
3312         if self.red:
3313             palette = QPalette()
3314             self.widget.setPalette(palette)
3315             self.red = False
3316 
3317     def InvalidValue(self, value):
3318         self.value = ""
3319         self.TurnTextRed()
3320         self.error = self.label + " invalid value '" + value + "'"
3321         self.parent.ShowMessage(self.error)
3322 
3323     def Invalidate(self):
3324         self.validated = False
3325 
3326     def DoValidate(self, input_string):
3327         self.value = input_string.strip()
3328 
3329     def Validate(self):
3330         self.validated = True
3331         self.error = ""
3332         self.TurnTextNormal()
3333         self.parent.ClearMessage()
3334         input_string = self.widget.text()
3335         if not len(input_string.strip()):
3336             self.value = ""
3337             return
3338         self.DoValidate(input_string)
3339 
3340     def IsValid(self):
3341         if not self.validated:
3342             self.Validate()
3343         if len(self.error):
3344             self.parent.ShowMessage(self.error)
3345             return False
3346         return True
3347 
3348     def IsNumber(self, value):
3349         try:
3350             x = int(value)
3351         except:
3352             x = 0
3353         return str(x) == value
3354 
3355 # Non-negative integer ranges dialog data item
3356 
3357 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
3358 
3359     def __init__(self, glb, label, placeholder_text, column_name, parent):
3360         super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3361 
3362         self.column_name = column_name
3363 
3364     def DoValidate(self, input_string):
3365         singles = []
3366         ranges = []
3367         for value in [x.strip() for x in input_string.split(",")]:
3368             if "-" in value:
3369                 vrange = value.split("-")
3370                 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3371                     return self.InvalidValue(value)
3372                 ranges.append(vrange)
3373             else:
3374                 if not self.IsNumber(value):
3375                     return self.InvalidValue(value)
3376                 singles.append(value)
3377         ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3378         if len(singles):
3379             ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
3380         self.value = " OR ".join(ranges)
3381 
3382 # Positive integer dialog data item
3383 
3384 class PositiveIntegerDataItem(LineEditDataItem):
3385 
3386     def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3387         super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
3388 
3389     def DoValidate(self, input_string):
3390         if not self.IsNumber(input_string.strip()):
3391             return self.InvalidValue(input_string)
3392         value = int(input_string.strip())
3393         if value <= 0:
3394             return self.InvalidValue(input_string)
3395         self.value = str(value)
3396 
3397 # Dialog data item converted and validated using a SQL table
3398 
3399 class SQLTableDataItem(LineEditDataItem):
3400 
3401     def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
3402         super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
3403 
3404         self.table_name = table_name
3405         self.match_column = match_column
3406         self.column_name1 = column_name1
3407         self.column_name2 = column_name2
3408 
3409     def ValueToIds(self, value):
3410         ids = []
3411         query = QSqlQuery(self.glb.db)
3412         stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
3413         ret = query.exec_(stmt)
3414         if ret:
3415             while query.next():
3416                 ids.append(str(query.value(0)))
3417         return ids
3418 
3419     def DoValidate(self, input_string):
3420         all_ids = []
3421         for value in [x.strip() for x in input_string.split(",")]:
3422             ids = self.ValueToIds(value)
3423             if len(ids):
3424                 all_ids.extend(ids)
3425             else:
3426                 return self.InvalidValue(value)
3427         self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
3428         if self.column_name2:
3429             self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
3430 
3431 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
3432 
3433 class SampleTimeRangesDataItem(LineEditDataItem):
3434 
3435     def __init__(self, glb, label, placeholder_text, column_name, parent):
3436         self.column_name = column_name
3437 
3438         self.last_id = 0
3439         self.first_time = 0
3440         self.last_time = 2 ** 64
3441 
3442         query = QSqlQuery(glb.db)
3443         QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
3444         if query.next():
3445             self.last_id = int(query.value(0))
3446         self.first_time = int(glb.HostStartTime())
3447         self.last_time = int(glb.HostFinishTime())
3448         if placeholder_text:
3449             placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
3450 
3451         super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3452 
3453     def IdBetween(self, query, lower_id, higher_id, order):
3454         QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
3455         if query.next():
3456             return True, int(query.value(0))
3457         else:
3458             return False, 0
3459 
3460     def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
3461         query = QSqlQuery(self.glb.db)
3462         while True:
3463             next_id = int((lower_id + higher_id) / 2)
3464             QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3465             if not query.next():
3466                 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
3467                 if not ok:
3468                     ok, dbid = self.IdBetween(query, next_id, higher_id, "")
3469                     if not ok:
3470                         return str(higher_id)
3471                 next_id = dbid
3472                 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3473             next_time = int(query.value(0))
3474             if get_floor:
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(higher_id)
3481             else:
3482                 if target_time >= next_time:
3483                     lower_id = next_id
3484                 else:
3485                     higher_id = next_id
3486                 if higher_id <= lower_id + 1:
3487                     return str(lower_id)
3488 
3489     def ConvertRelativeTime(self, val):
3490         mult = 1
3491         suffix = val[-2:]
3492         if suffix == "ms":
3493             mult = 1000000
3494         elif suffix == "us":
3495             mult = 1000
3496         elif suffix == "ns":
3497             mult = 1
3498         else:
3499             return val
3500         val = val[:-2].strip()
3501         if not self.IsNumber(val):
3502             return val
3503         val = int(val) * mult
3504         if val >= 0:
3505             val += self.first_time
3506         else:
3507             val += self.last_time
3508         return str(val)
3509 
3510     def ConvertTimeRange(self, vrange):
3511         if vrange[0] == "":
3512             vrange[0] = str(self.first_time)
3513         if vrange[1] == "":
3514             vrange[1] = str(self.last_time)
3515         vrange[0] = self.ConvertRelativeTime(vrange[0])
3516         vrange[1] = self.ConvertRelativeTime(vrange[1])
3517         if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3518             return False
3519         beg_range = max(int(vrange[0]), self.first_time)
3520         end_range = min(int(vrange[1]), self.last_time)
3521         if beg_range > self.last_time or end_range < self.first_time:
3522             return False
3523         vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
3524         vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
3525         return True
3526 
3527     def AddTimeRange(self, value, ranges):
3528         n = value.count("-")
3529         if n == 1:
3530             pass
3531         elif n == 2:
3532             if value.split("-")[1].strip() == "":
3533                 n = 1
3534         elif n == 3:
3535             n = 2
3536         else:
3537             return False
3538         pos = findnth(value, "-", n)
3539         vrange = [value[:pos].strip() ,value[pos+1:].strip()]
3540         if self.ConvertTimeRange(vrange):
3541             ranges.append(vrange)
3542             return True
3543         return False
3544 
3545     def DoValidate(self, input_string):
3546         ranges = []
3547         for value in [x.strip() for x in input_string.split(",")]:
3548             if not self.AddTimeRange(value, ranges):
3549                 return self.InvalidValue(value)
3550         ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3551         self.value = " OR ".join(ranges)
3552 
3553 # Report Dialog Base
3554 
3555 class ReportDialogBase(QDialog):
3556 
3557     def __init__(self, glb, title, items, partial, parent=None):
3558         super(ReportDialogBase, self).__init__(parent)
3559 
3560         self.glb = glb
3561 
3562         self.report_vars = ReportVars()
3563 
3564         self.setWindowTitle(title)
3565         self.setMinimumWidth(600)
3566 
3567         self.data_items = [x(glb, self) for x in items]
3568 
3569         self.partial = partial
3570 
3571         self.grid = QGridLayout()
3572 
3573         for row in xrange(len(self.data_items)):
3574             self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
3575             self.grid.addWidget(self.data_items[row].widget, row, 1)
3576 
3577         self.status = QLabel()
3578 
3579         self.ok_button = QPushButton("Ok", self)
3580         self.ok_button.setDefault(True)
3581         self.ok_button.released.connect(self.Ok)
3582         self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3583 
3584         self.cancel_button = QPushButton("Cancel", self)
3585         self.cancel_button.released.connect(self.reject)
3586         self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3587 
3588         self.hbox = QHBoxLayout()
3589         #self.hbox.addStretch()
3590         self.hbox.addWidget(self.status)
3591         self.hbox.addWidget(self.ok_button)
3592         self.hbox.addWidget(self.cancel_button)
3593 
3594         self.vbox = QVBoxLayout()
3595         self.vbox.addLayout(self.grid)
3596         self.vbox.addLayout(self.hbox)
3597 
3598         self.setLayout(self.vbox)
3599 
3600     def Ok(self):
3601         vars = self.report_vars
3602         for d in self.data_items:
3603             if d.id == "REPORTNAME":
3604                 vars.name = d.value
3605         if not vars.name:
3606             self.ShowMessage("Report name is required")
3607             return
3608         for d in self.data_items:
3609             if not d.IsValid():
3610                 return
3611         for d in self.data_items[1:]:
3612             if d.id == "LIMIT":
3613                 vars.limit = d.value
3614             elif len(d.value):
3615                 if len(vars.where_clause):
3616                     vars.where_clause += " AND "
3617                 vars.where_clause += d.value
3618         if len(vars.where_clause):
3619             if self.partial:
3620                 vars.where_clause = " AND ( " + vars.where_clause + " ) "
3621             else:
3622                 vars.where_clause = " WHERE " + vars.where_clause + " "
3623         self.accept()
3624 
3625     def ShowMessage(self, msg):
3626         self.status.setText("<font color=#FF0000>" + msg)
3627 
3628     def ClearMessage(self):
3629         self.status.setText("")
3630 
3631 # Selected branch report creation dialog
3632 
3633 class SelectedBranchDialog(ReportDialogBase):
3634 
3635     def __init__(self, glb, parent=None):
3636         title = "Selected Branches"
3637         items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
3638              lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
3639              lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
3640              lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
3641              lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
3642              lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
3643              lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
3644              lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
3645              lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
3646         super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
3647 
3648 # Event list
3649 
3650 def GetEventList(db):
3651     events = []
3652     query = QSqlQuery(db)
3653     QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
3654     while query.next():
3655         events.append(query.value(0))
3656     return events
3657 
3658 # Is a table selectable
3659 
3660 def IsSelectable(db, table, sql = "", columns = "*"):
3661     query = QSqlQuery(db)
3662     try:
3663         QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
3664     except:
3665         return False
3666     return True
3667 
3668 # SQL table data model item
3669 
3670 class SQLTableItem():
3671 
3672     def __init__(self, row, data):
3673         self.row = row
3674         self.data = data
3675 
3676     def getData(self, column):
3677         return self.data[column]
3678 
3679 # SQL table data model
3680 
3681 class SQLTableModel(TableModel):
3682 
3683     progress = Signal(object)
3684 
3685     def __init__(self, glb, sql, column_headers, parent=None):
3686         super(SQLTableModel, self).__init__(parent)
3687         self.glb = glb
3688         self.more = True
3689         self.populated = 0
3690         self.column_headers = column_headers
3691         self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
3692         self.fetcher.done.connect(self.Update)
3693         self.fetcher.Fetch(glb_chunk_sz)
3694 
3695     def DisplayData(self, item, index):
3696         self.FetchIfNeeded(item.row)
3697         return item.getData(index.column())
3698 
3699     def AddSample(self, data):
3700         child = SQLTableItem(self.populated, data)
3701         self.child_items.append(child)
3702         self.populated += 1
3703 
3704     def Update(self, fetched):
3705         if not fetched:
3706             self.more = False
3707             self.progress.emit(0)
3708         child_count = self.child_count
3709         count = self.populated - child_count
3710         if count > 0:
3711             parent = QModelIndex()
3712             self.beginInsertRows(parent, child_count, child_count + count - 1)
3713             self.insertRows(child_count, count, parent)
3714             self.child_count += count
3715             self.endInsertRows()
3716             self.progress.emit(self.child_count)
3717 
3718     def FetchMoreRecords(self, count):
3719         current = self.child_count
3720         if self.more:
3721             self.fetcher.Fetch(count)
3722         else:
3723             self.progress.emit(0)
3724         return current
3725 
3726     def HasMoreRecords(self):
3727         return self.more
3728 
3729     def columnCount(self, parent=None):
3730         return len(self.column_headers)
3731 
3732     def columnHeader(self, column):
3733         return self.column_headers[column]
3734 
3735     def SQLTableDataPrep(self, query, count):
3736         data = []
3737         for i in xrange(count):
3738             data.append(query.value(i))
3739         return data
3740 
3741 # SQL automatic table data model
3742 
3743 class SQLAutoTableModel(SQLTableModel):
3744 
3745     def __init__(self, glb, table_name, parent=None):
3746         sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
3747         if table_name == "comm_threads_view":
3748             # For now, comm_threads_view has no id column
3749             sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
3750         column_headers = []
3751         query = QSqlQuery(glb.db)
3752         if glb.dbref.is_sqlite3:
3753             QueryExec(query, "PRAGMA table_info(" + table_name + ")")
3754             while query.next():
3755                 column_headers.append(query.value(1))
3756             if table_name == "sqlite_master":
3757                 sql = "SELECT * FROM " + table_name
3758         else:
3759             if table_name[:19] == "information_schema.":
3760                 sql = "SELECT * FROM " + table_name
3761                 select_table_name = table_name[19:]
3762                 schema = "information_schema"
3763             else:
3764                 select_table_name = table_name
3765                 schema = "public"
3766             QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
3767             while query.next():
3768                 column_headers.append(query.value(0))
3769         if pyside_version_1 and sys.version_info[0] == 3:
3770             if table_name == "samples_view":
3771                 self.SQLTableDataPrep = self.samples_view_DataPrep
3772             if table_name == "samples":
3773                 self.SQLTableDataPrep = self.samples_DataPrep
3774         super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
3775 
3776     def samples_view_DataPrep(self, query, count):
3777         data = []
3778         data.append(query.value(0))
3779         # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3780         data.append("{:>19}".format(query.value(1)))
3781         for i in xrange(2, count):
3782             data.append(query.value(i))
3783         return data
3784 
3785     def samples_DataPrep(self, query, count):
3786         data = []
3787         for i in xrange(9):
3788             data.append(query.value(i))
3789         # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3790         data.append("{:>19}".format(query.value(9)))
3791         for i in xrange(10, count):
3792             data.append(query.value(i))
3793         return data
3794 
3795 # Base class for custom ResizeColumnsToContents
3796 
3797 class ResizeColumnsToContentsBase(QObject):
3798 
3799     def __init__(self, parent=None):
3800         super(ResizeColumnsToContentsBase, self).__init__(parent)
3801 
3802     def ResizeColumnToContents(self, column, n):
3803         # Using the view's resizeColumnToContents() here is extrememly slow
3804         # so implement a crude alternative
3805         font = self.view.font()
3806         metrics = QFontMetrics(font)
3807         max = 0
3808         for row in xrange(n):
3809             val = self.data_model.child_items[row].data[column]
3810             len = metrics.width(str(val) + "MM")
3811             max = len if len > max else max
3812         val = self.data_model.columnHeader(column)
3813         len = metrics.width(str(val) + "MM")
3814         max = len if len > max else max
3815         self.view.setColumnWidth(column, max)
3816 
3817     def ResizeColumnsToContents(self):
3818         n = min(self.data_model.child_count, 100)
3819         if n < 1:
3820             # No data yet, so connect a signal to notify when there is
3821             self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
3822             return
3823         columns = self.data_model.columnCount()
3824         for i in xrange(columns):
3825             self.ResizeColumnToContents(i, n)
3826 
3827     def UpdateColumnWidths(self, *x):
3828         # This only needs to be done once, so disconnect the signal now
3829         self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
3830         self.ResizeColumnsToContents()
3831 
3832 # Convert value to CSV
3833 
3834 def ToCSValue(val):
3835     if '"' in val:
3836         val = val.replace('"', '""')
3837     if "," in val or '"' in val:
3838         val = '"' + val + '"'
3839     return val
3840 
3841 # Key to sort table model indexes by row / column, assuming fewer than 1000 columns
3842 
3843 glb_max_cols = 1000
3844 
3845 def RowColumnKey(a):
3846     return a.row() * glb_max_cols + a.column()
3847 
3848 # Copy selected table cells to clipboard
3849 
3850 def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
3851     indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
3852     idx_cnt = len(indexes)
3853     if not idx_cnt:
3854         return
3855     if idx_cnt == 1:
3856         with_hdr=False
3857     min_row = indexes[0].row()
3858     max_row = indexes[0].row()
3859     min_col = indexes[0].column()
3860     max_col = indexes[0].column()
3861     for i in indexes:
3862         min_row = min(min_row, i.row())
3863         max_row = max(max_row, i.row())
3864         min_col = min(min_col, i.column())
3865         max_col = max(max_col, i.column())
3866     if max_col > glb_max_cols:
3867         raise RuntimeError("glb_max_cols is too low")
3868     max_width = [0] * (1 + max_col - min_col)
3869     for i in indexes:
3870         c = i.column() - min_col
3871         max_width[c] = max(max_width[c], len(str(i.data())))
3872     text = ""
3873     pad = ""
3874     sep = ""
3875     if with_hdr:
3876         model = indexes[0].model()
3877         for col in range(min_col, max_col + 1):
3878             val = model.headerData(col, Qt.Horizontal, Qt.DisplayRole)
3879             if as_csv:
3880                 text += sep + ToCSValue(val)
3881                 sep = ","
3882             else:
3883                 c = col - min_col
3884                 max_width[c] = max(max_width[c], len(val))
3885                 width = max_width[c]
3886                 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
3887                 if align & Qt.AlignRight:
3888                     val = val.rjust(width)
3889                 text += pad + sep + val
3890                 pad = " " * (width - len(val))
3891                 sep = "  "
3892         text += "\n"
3893         pad = ""
3894         sep = ""
3895     last_row = min_row
3896     for i in indexes:
3897         if i.row() > last_row:
3898             last_row = i.row()
3899             text += "\n"
3900             pad = ""
3901             sep = ""
3902         if as_csv:
3903             text += sep + ToCSValue(str(i.data()))
3904             sep = ","
3905         else:
3906             width = max_width[i.column() - min_col]
3907             if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3908                 val = str(i.data()).rjust(width)
3909             else:
3910                 val = str(i.data())
3911             text += pad + sep + val
3912             pad = " " * (width - len(val))
3913             sep = "  "
3914     QApplication.clipboard().setText(text)
3915 
3916 def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
3917     indexes = view.selectedIndexes()
3918     if not len(indexes):
3919         return
3920 
3921     selection = view.selectionModel()
3922 
3923     first = None
3924     for i in indexes:
3925         above = view.indexAbove(i)
3926         if not selection.isSelected(above):
3927             first = i
3928             break
3929 
3930     if first is None:
3931         raise RuntimeError("CopyTreeCellsToClipboard internal error")
3932 
3933     model = first.model()
3934     row_cnt = 0
3935     col_cnt = model.columnCount(first)
3936     max_width = [0] * col_cnt
3937 
3938     indent_sz = 2
3939     indent_str = " " * indent_sz
3940 
3941     expanded_mark_sz = 2
3942     if sys.version_info[0] == 3:
3943         expanded_mark = "\u25BC "
3944         not_expanded_mark = "\u25B6 "
3945     else:
3946         expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
3947         not_expanded_mark =  unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
3948     leaf_mark = "  "
3949 
3950     if not as_csv:
3951         pos = first
3952         while True:
3953             row_cnt += 1
3954             row = pos.row()
3955             for c in range(col_cnt):
3956                 i = pos.sibling(row, c)
3957                 if c:
3958                     n = len(str(i.data()))
3959                 else:
3960                     n = len(str(i.data()).strip())
3961                     n += (i.internalPointer().level - 1) * indent_sz
3962                     n += expanded_mark_sz
3963                 max_width[c] = max(max_width[c], n)
3964             pos = view.indexBelow(pos)
3965             if not selection.isSelected(pos):
3966                 break
3967 
3968     text = ""
3969     pad = ""
3970     sep = ""
3971     if with_hdr:
3972         for c in range(col_cnt):
3973             val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
3974             if as_csv:
3975                 text += sep + ToCSValue(val)
3976                 sep = ","
3977             else:
3978                 max_width[c] = max(max_width[c], len(val))
3979                 width = max_width[c]
3980                 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
3981                 if align & Qt.AlignRight:
3982                     val = val.rjust(width)
3983                 text += pad + sep + val
3984                 pad = " " * (width - len(val))
3985                 sep = "   "
3986         text += "\n"
3987         pad = ""
3988         sep = ""
3989 
3990     pos = first
3991     while True:
3992         row = pos.row()
3993         for c in range(col_cnt):
3994             i = pos.sibling(row, c)
3995             val = str(i.data())
3996             if not c:
3997                 if model.hasChildren(i):
3998                     if view.isExpanded(i):
3999                         mark = expanded_mark
4000                     else:
4001                         mark = not_expanded_mark
4002                 else:
4003                     mark = leaf_mark
4004                 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
4005             if as_csv:
4006                 text += sep + ToCSValue(val)
4007                 sep = ","
4008             else:
4009                 width = max_width[c]
4010                 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
4011                     val = val.rjust(width)
4012                 text += pad + sep + val
4013                 pad = " " * (width - len(val))
4014                 sep = "   "
4015         pos = view.indexBelow(pos)
4016         if not selection.isSelected(pos):
4017             break
4018         text = text.rstrip() + "\n"
4019         pad = ""
4020         sep = ""
4021 
4022     QApplication.clipboard().setText(text)
4023 
4024 def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
4025     view.CopyCellsToClipboard(view, as_csv, with_hdr)
4026 
4027 def CopyCellsToClipboardHdr(view):
4028     CopyCellsToClipboard(view, False, True)
4029 
4030 def CopyCellsToClipboardCSV(view):
4031     CopyCellsToClipboard(view, True, True)
4032 
4033 # Context menu
4034 
4035 class ContextMenu(object):
4036 
4037     def __init__(self, view):
4038         self.view = view
4039         self.view.setContextMenuPolicy(Qt.CustomContextMenu)
4040         self.view.customContextMenuRequested.connect(self.ShowContextMenu)
4041 
4042     def ShowContextMenu(self, pos):
4043         menu = QMenu(self.view)
4044         self.AddActions(menu)
4045         menu.exec_(self.view.mapToGlobal(pos))
4046 
4047     def AddCopy(self, menu):
4048         menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
4049         menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
4050 
4051     def AddActions(self, menu):
4052         self.AddCopy(menu)
4053 
4054 class TreeContextMenu(ContextMenu):
4055 
4056     def __init__(self, view):
4057         super(TreeContextMenu, self).__init__(view)
4058 
4059     def AddActions(self, menu):
4060         i = self.view.currentIndex()
4061         text = str(i.data()).strip()
4062         if len(text):
4063             menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
4064         self.AddCopy(menu)
4065 
4066 # Table window
4067 
4068 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4069 
4070     def __init__(self, glb, table_name, parent=None):
4071         super(TableWindow, self).__init__(parent)
4072 
4073         self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
4074 
4075         self.model = QSortFilterProxyModel()
4076         self.model.setSourceModel(self.data_model)
4077 
4078         self.view = QTableView()
4079         self.view.setModel(self.model)
4080         self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4081         self.view.verticalHeader().setVisible(False)
4082         self.view.sortByColumn(-1, Qt.AscendingOrder)
4083         self.view.setSortingEnabled(True)
4084         self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4085         self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4086 
4087         self.ResizeColumnsToContents()
4088 
4089         self.context_menu = ContextMenu(self.view)
4090 
4091         self.find_bar = FindBar(self, self, True)
4092 
4093         self.finder = ChildDataItemFinder(self.data_model)
4094 
4095         self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4096 
4097         self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4098 
4099         self.setWidget(self.vbox.Widget())
4100 
4101         AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
4102 
4103     def Find(self, value, direction, pattern, context):
4104         self.view.setFocus()
4105         self.find_bar.Busy()
4106         self.finder.Find(value, direction, pattern, context, self.FindDone)
4107 
4108     def FindDone(self, row):
4109         self.find_bar.Idle()
4110         if row >= 0:
4111             self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
4112         else:
4113             self.find_bar.NotFound()
4114 
4115 # Table list
4116 
4117 def GetTableList(glb):
4118     tables = []
4119     query = QSqlQuery(glb.db)
4120     if glb.dbref.is_sqlite3:
4121         QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
4122     else:
4123         QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
4124     while query.next():
4125         tables.append(query.value(0))
4126     if glb.dbref.is_sqlite3:
4127         tables.append("sqlite_master")
4128     else:
4129         tables.append("information_schema.tables")
4130         tables.append("information_schema.views")
4131         tables.append("information_schema.columns")
4132     return tables
4133 
4134 # Top Calls data model
4135 
4136 class TopCallsModel(SQLTableModel):
4137 
4138     def __init__(self, glb, report_vars, parent=None):
4139         text = ""
4140         if not glb.dbref.is_sqlite3:
4141             text = "::text"
4142         limit = ""
4143         if len(report_vars.limit):
4144             limit = " LIMIT " + report_vars.limit
4145         sql = ("SELECT comm, pid, tid, name,"
4146             " CASE"
4147             " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
4148             " ELSE short_name"
4149             " END AS dso,"
4150             " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
4151             " CASE"
4152             " WHEN (calls.flags = 1) THEN 'no call'" + text +
4153             " WHEN (calls.flags = 2) THEN 'no return'" + text +
4154             " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
4155             " ELSE ''" + text +
4156             " END AS flags"
4157             " FROM calls"
4158             " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
4159             " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
4160             " INNER JOIN dsos ON symbols.dso_id = dsos.id"
4161             " INNER JOIN comms ON calls.comm_id = comms.id"
4162             " INNER JOIN threads ON calls.thread_id = threads.id" +
4163             report_vars.where_clause +
4164             " ORDER BY elapsed_time DESC" +
4165             limit
4166             )
4167         column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
4168         self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
4169         super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
4170 
4171     def columnAlignment(self, column):
4172         return self.alignment[column]
4173 
4174 # Top Calls report creation dialog
4175 
4176 class TopCallsDialog(ReportDialogBase):
4177 
4178     def __init__(self, glb, parent=None):
4179         title = "Top Calls by Elapsed Time"
4180         items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
4181              lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
4182              lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
4183              lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
4184              lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
4185              lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
4186              lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
4187              lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
4188         super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
4189 
4190 # Top Calls window
4191 
4192 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4193 
4194     def __init__(self, glb, report_vars, parent=None):
4195         super(TopCallsWindow, self).__init__(parent)
4196 
4197         self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
4198         self.model = self.data_model
4199 
4200         self.view = QTableView()
4201         self.view.setModel(self.model)
4202         self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4203         self.view.verticalHeader().setVisible(False)
4204         self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4205         self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4206 
4207         self.context_menu = ContextMenu(self.view)
4208 
4209         self.ResizeColumnsToContents()
4210 
4211         self.find_bar = FindBar(self, self, True)
4212 
4213         self.finder = ChildDataItemFinder(self.model)
4214 
4215         self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4216 
4217         self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4218 
4219         self.setWidget(self.vbox.Widget())
4220 
4221         AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
4222 
4223     def Find(self, value, direction, pattern, context):
4224         self.view.setFocus()
4225         self.find_bar.Busy()
4226         self.finder.Find(value, direction, pattern, context, self.FindDone)
4227 
4228     def FindDone(self, row):
4229         self.find_bar.Idle()
4230         if row >= 0:
4231             self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
4232         else:
4233             self.find_bar.NotFound()
4234 
4235 # Action Definition
4236 
4237 def CreateAction(label, tip, callback, parent=None, shortcut=None):
4238     action = QAction(label, parent)
4239     if shortcut != None:
4240         action.setShortcuts(shortcut)
4241     action.setStatusTip(tip)
4242     action.triggered.connect(callback)
4243     return action
4244 
4245 # Typical application actions
4246 
4247 def CreateExitAction(app, parent=None):
4248     return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
4249 
4250 # Typical MDI actions
4251 
4252 def CreateCloseActiveWindowAction(mdi_area):
4253     return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
4254 
4255 def CreateCloseAllWindowsAction(mdi_area):
4256     return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
4257 
4258 def CreateTileWindowsAction(mdi_area):
4259     return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
4260 
4261 def CreateCascadeWindowsAction(mdi_area):
4262     return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
4263 
4264 def CreateNextWindowAction(mdi_area):
4265     return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
4266 
4267 def CreatePreviousWindowAction(mdi_area):
4268     return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
4269 
4270 # Typical MDI window menu
4271 
4272 class WindowMenu():
4273 
4274     def __init__(self, mdi_area, menu):
4275         self.mdi_area = mdi_area
4276         self.window_menu = menu.addMenu("&Windows")
4277         self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
4278         self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
4279         self.tile_windows = CreateTileWindowsAction(mdi_area)
4280         self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
4281         self.next_window = CreateNextWindowAction(mdi_area)
4282         self.previous_window = CreatePreviousWindowAction(mdi_area)
4283         self.window_menu.aboutToShow.connect(self.Update)
4284 
4285     def Update(self):
4286         self.window_menu.clear()
4287         sub_window_count = len(self.mdi_area.subWindowList())
4288         have_sub_windows = sub_window_count != 0
4289         self.close_active_window.setEnabled(have_sub_windows)
4290         self.close_all_windows.setEnabled(have_sub_windows)
4291         self.tile_windows.setEnabled(have_sub_windows)
4292         self.cascade_windows.setEnabled(have_sub_windows)
4293         self.next_window.setEnabled(have_sub_windows)
4294         self.previous_window.setEnabled(have_sub_windows)
4295         self.window_menu.addAction(self.close_active_window)
4296         self.window_menu.addAction(self.close_all_windows)
4297         self.window_menu.addSeparator()
4298         self.window_menu.addAction(self.tile_windows)
4299         self.window_menu.addAction(self.cascade_windows)
4300         self.window_menu.addSeparator()
4301         self.window_menu.addAction(self.next_window)
4302         self.window_menu.addAction(self.previous_window)
4303         if sub_window_count == 0:
4304             return
4305         self.window_menu.addSeparator()
4306         nr = 1
4307         for sub_window in self.mdi_area.subWindowList():
4308             label = str(nr) + " " + sub_window.name
4309             if nr < 10:
4310                 label = "&" + label
4311             action = self.window_menu.addAction(label)
4312             action.setCheckable(True)
4313             action.setChecked(sub_window == self.mdi_area.activeSubWindow())
4314             action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
4315             self.window_menu.addAction(action)
4316             nr += 1
4317 
4318     def setActiveSubWindow(self, nr):
4319         self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
4320 
4321 # Help text
4322 
4323 glb_help_text = """
4324 <h1>Contents</h1>
4325 <style>
4326 p.c1 {
4327     text-indent: 40px;
4328 }
4329 p.c2 {
4330     text-indent: 80px;
4331 }
4332 }
4333 </style>
4334 <p class=c1><a href=#reports>1. Reports</a></p>
4335 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
4336 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
4337 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
4338 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
4339 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
4340 <p class=c1><a href=#charts>2. Charts</a></p>
4341 <p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
4342 <p class=c1><a href=#tables>3. Tables</a></p>
4343 <h1 id=reports>1. Reports</h1>
4344 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
4345 The result is a GUI window with a tree representing a context-sensitive
4346 call-graph. Expanding a couple of levels of the tree and adjusting column
4347 widths to suit will display something like:
4348 <pre>
4349                                          Call Graph: pt_example
4350 Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%)
4351 v- ls
4352     v- 2638:2638
4353         v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0
4354           |- unknown               unknown       1        13198     0.1              1              0.0
4355           >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3
4356           >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3
4357           v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4
4358              >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1
4359              >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0
4360              >- __libc_csu_init    ls            1        10354     0.1             10              0.0
4361              |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0
4362              v- main               ls            1      8182043    99.6         180254             99.9
4363 </pre>
4364 <h3>Points to note:</h3>
4365 <ul>
4366 <li>The top level is a command name (comm)</li>
4367 <li>The next level is a thread (pid:tid)</li>
4368 <li>Subsequent levels are functions</li>
4369 <li>'Count' is the number of calls</li>
4370 <li>'Time' is the elapsed time until the function returns</li>
4371 <li>Percentages are relative to the level above</li>
4372 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
4373 </ul>
4374 <h3>Find</h3>
4375 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
4376 The pattern matching symbols are ? for any character and * for zero or more characters.
4377 <h2 id=calltree>1.2 Call Tree</h2>
4378 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
4379 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
4380 <h2 id=allbranches>1.3 All branches</h2>
4381 The All branches report displays all branches in chronological order.
4382 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
4383 <h3>Disassembly</h3>
4384 Open a branch to display disassembly. This only works if:
4385 <ol>
4386 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
4387 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
4388 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
4389 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
4390 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
4391 </ol>
4392 <h4 id=xed>Intel XED Setup</h4>
4393 To use Intel XED, libxed.so must be present.  To build and install libxed.so:
4394 <pre>
4395 git clone https://github.com/intelxed/mbuild.git mbuild
4396 git clone https://github.com/intelxed/xed
4397 cd xed
4398 ./mfile.py --share
4399 sudo ./mfile.py --prefix=/usr/local install
4400 sudo ldconfig
4401 </pre>
4402 <h3>Instructions per Cycle (IPC)</h3>
4403 If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
4404 <p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
4405 Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
4406 In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
4407 since the previous displayed 'IPC'.
4408 <h3>Find</h3>
4409 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4410 Refer to Python documentation for the regular expression syntax.
4411 All columns are searched, but only currently fetched rows are searched.
4412 <h2 id=selectedbranches>1.4 Selected branches</h2>
4413 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
4414 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4415 <h3>1.4.1 Time ranges</h3>
4416 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
4417 ms, us or ns. Also, negative values are relative to the end of trace.  Examples:
4418 <pre>
4419     81073085947329-81073085958238   From 81073085947329 to 81073085958238
4420     100us-200us     From 100us to 200us
4421     10ms-           From 10ms to the end
4422     -100ns          The first 100ns
4423     -10ms-          The last 10ms
4424 </pre>
4425 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
4426 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
4427 The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
4428 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4429 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
4430 <h1 id=charts>2. Charts</h1>
4431 <h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
4432 This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
4433 <h3>Features</h3>
4434 <ol>
4435 <li>Mouse over to highight the task and show the time</li>
4436 <li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
4437 <li>Go back and forward by pressing the arrow buttons</li>
4438 <li>If call information is available, right-click to show a call tree opened to that task and time.
4439 Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
4440 </li>
4441 </ol>
4442 <h3>Important</h3>
4443 The graph can be misleading in the following respects:
4444 <ol>
4445 <li>The graph shows the first task on each CPU as running from the beginning of the time range.
4446 Because tracing might start on different CPUs at different times, that is not necessarily the case.
4447 Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4448 <li>Similarly, the last task on each CPU can be showing running longer than it really was.
4449 Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4450 <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>
4451 </ol>
4452 <h1 id=tables>3. Tables</h1>
4453 The Tables menu shows all tables and views in the database. Most tables have an associated view
4454 which displays the information in a more friendly way. Not all data for large tables is fetched
4455 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
4456 but that can be slow for large tables.
4457 <p>There are also tables of database meta-information.
4458 For SQLite3 databases, the sqlite_master table is included.
4459 For PostgreSQL databases, information_schema.tables/views/columns are included.
4460 <h3>Find</h3>
4461 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4462 Refer to Python documentation for the regular expression syntax.
4463 All columns are searched, but only currently fetched rows are searched.
4464 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
4465 will go to the next/previous result in id order, instead of display order.
4466 """
4467 
4468 # Help window
4469 
4470 class HelpWindow(QMdiSubWindow):
4471 
4472     def __init__(self, glb, parent=None):
4473         super(HelpWindow, self).__init__(parent)
4474 
4475         self.text = QTextBrowser()
4476         self.text.setHtml(glb_help_text)
4477         self.text.setReadOnly(True)
4478         self.text.setOpenExternalLinks(True)
4479 
4480         self.setWidget(self.text)
4481 
4482         AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
4483 
4484 # Main window that only displays the help text
4485 
4486 class HelpOnlyWindow(QMainWindow):
4487 
4488     def __init__(self, parent=None):
4489         super(HelpOnlyWindow, self).__init__(parent)
4490 
4491         self.setMinimumSize(200, 100)
4492         self.resize(800, 600)
4493         self.setWindowTitle("Exported SQL Viewer Help")
4494         self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
4495 
4496         self.text = QTextBrowser()
4497         self.text.setHtml(glb_help_text)
4498         self.text.setReadOnly(True)
4499         self.text.setOpenExternalLinks(True)
4500 
4501         self.setCentralWidget(self.text)
4502 
4503 # PostqreSQL server version
4504 
4505 def PostqreSQLServerVersion(db):
4506     query = QSqlQuery(db)
4507     QueryExec(query, "SELECT VERSION()")
4508     if query.next():
4509         v_str = query.value(0)
4510         v_list = v_str.strip().split(" ")
4511         if v_list[0] == "PostgreSQL" and v_list[2] == "on":
4512             return v_list[1]
4513         return v_str
4514     return "Unknown"
4515 
4516 # SQLite version
4517 
4518 def SQLiteVersion(db):
4519     query = QSqlQuery(db)
4520     QueryExec(query, "SELECT sqlite_version()")
4521     if query.next():
4522         return query.value(0)
4523     return "Unknown"
4524 
4525 # About dialog
4526 
4527 class AboutDialog(QDialog):
4528 
4529     def __init__(self, glb, parent=None):
4530         super(AboutDialog, self).__init__(parent)
4531 
4532         self.setWindowTitle("About Exported SQL Viewer")
4533         self.setMinimumWidth(300)
4534 
4535         pyside_version = "1" if pyside_version_1 else "2"
4536 
4537         text = "<pre>"
4538         text += "Python version:     " + sys.version.split(" ")[0] + "\n"
4539         text += "PySide version:     " + pyside_version + "\n"
4540         text += "Qt version:         " + qVersion() + "\n"
4541         if glb.dbref.is_sqlite3:
4542             text += "SQLite version:     " + SQLiteVersion(glb.db) + "\n"
4543         else:
4544             text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
4545         text += "</pre>"
4546 
4547         self.text = QTextBrowser()
4548         self.text.setHtml(text)
4549         self.text.setReadOnly(True)
4550         self.text.setOpenExternalLinks(True)
4551 
4552         self.vbox = QVBoxLayout()
4553         self.vbox.addWidget(self.text)
4554 
4555         self.setLayout(self.vbox)
4556 
4557 # Font resize
4558 
4559 def ResizeFont(widget, diff):
4560     font = widget.font()
4561     sz = font.pointSize()
4562     font.setPointSize(sz + diff)
4563     widget.setFont(font)
4564 
4565 def ShrinkFont(widget):
4566     ResizeFont(widget, -1)
4567 
4568 def EnlargeFont(widget):
4569     ResizeFont(widget, 1)
4570 
4571 # Unique name for sub-windows
4572 
4573 def NumberedWindowName(name, nr):
4574     if nr > 1:
4575         name += " <" + str(nr) + ">"
4576     return name
4577 
4578 def UniqueSubWindowName(mdi_area, name):
4579     nr = 1
4580     while True:
4581         unique_name = NumberedWindowName(name, nr)
4582         ok = True
4583         for sub_window in mdi_area.subWindowList():
4584             if sub_window.name == unique_name:
4585                 ok = False
4586                 break
4587         if ok:
4588             return unique_name
4589         nr += 1
4590 
4591 # Add a sub-window
4592 
4593 def AddSubWindow(mdi_area, sub_window, name):
4594     unique_name = UniqueSubWindowName(mdi_area, name)
4595     sub_window.setMinimumSize(200, 100)
4596     sub_window.resize(800, 600)
4597     sub_window.setWindowTitle(unique_name)
4598     sub_window.setAttribute(Qt.WA_DeleteOnClose)
4599     sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
4600     sub_window.name = unique_name
4601     mdi_area.addSubWindow(sub_window)
4602     sub_window.show()
4603 
4604 # Main window
4605 
4606 class MainWindow(QMainWindow):
4607 
4608     def __init__(self, glb, parent=None):
4609         super(MainWindow, self).__init__(parent)
4610 
4611         self.glb = glb
4612 
4613         self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
4614         self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
4615         self.setMinimumSize(200, 100)
4616 
4617         self.mdi_area = QMdiArea()
4618         self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4619         self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4620 
4621         self.setCentralWidget(self.mdi_area)
4622 
4623         menu = self.menuBar()
4624 
4625         file_menu = menu.addMenu("&File")
4626         file_menu.addAction(CreateExitAction(glb.app, self))
4627 
4628         edit_menu = menu.addMenu("&Edit")
4629         edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
4630         edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
4631         edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
4632         edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
4633         edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
4634         edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
4635 
4636         reports_menu = menu.addMenu("&Reports")
4637         if IsSelectable(glb.db, "calls"):
4638             reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
4639 
4640         if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
4641             reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
4642 
4643         self.EventMenu(GetEventList(glb.db), reports_menu)
4644 
4645         if IsSelectable(glb.db, "calls"):
4646             reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
4647 
4648         if IsSelectable(glb.db, "context_switches"):
4649             charts_menu = menu.addMenu("&Charts")
4650             charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
4651 
4652         self.TableMenu(GetTableList(glb), menu)
4653 
4654         self.window_menu = WindowMenu(self.mdi_area, menu)
4655 
4656         help_menu = menu.addMenu("&Help")
4657         help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
4658         help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
4659 
4660     def Try(self, fn):
4661         win = self.mdi_area.activeSubWindow()
4662         if win:
4663             try:
4664                 fn(win.view)
4665             except:
4666                 pass
4667 
4668     def CopyToClipboard(self):
4669         self.Try(CopyCellsToClipboardHdr)
4670 
4671     def CopyToClipboardCSV(self):
4672         self.Try(CopyCellsToClipboardCSV)
4673 
4674     def Find(self):
4675         win = self.mdi_area.activeSubWindow()
4676         if win:
4677             try:
4678                 win.find_bar.Activate()
4679             except:
4680                 pass
4681 
4682     def FetchMoreRecords(self):
4683         win = self.mdi_area.activeSubWindow()
4684         if win:
4685             try:
4686                 win.fetch_bar.Activate()
4687             except:
4688                 pass
4689 
4690     def ShrinkFont(self):
4691         self.Try(ShrinkFont)
4692 
4693     def EnlargeFont(self):
4694         self.Try(EnlargeFont)
4695 
4696     def EventMenu(self, events, reports_menu):
4697         branches_events = 0
4698         for event in events:
4699             event = event.split(":")[0]
4700             if event == "branches":
4701                 branches_events += 1
4702         dbid = 0
4703         for event in events:
4704             dbid += 1
4705             event = event.split(":")[0]
4706             if event == "branches":
4707                 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
4708                 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
4709                 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
4710                 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
4711 
4712     def TimeChartByCPU(self):
4713         TimeChartByCPUWindow(self.glb, self)
4714 
4715     def TableMenu(self, tables, menu):
4716         table_menu = menu.addMenu("&Tables")
4717         for table in tables:
4718             table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
4719 
4720     def NewCallGraph(self):
4721         CallGraphWindow(self.glb, self)
4722 
4723     def NewCallTree(self):
4724         CallTreeWindow(self.glb, self)
4725 
4726     def NewTopCalls(self):
4727         dialog = TopCallsDialog(self.glb, self)
4728         ret = dialog.exec_()
4729         if ret:
4730             TopCallsWindow(self.glb, dialog.report_vars, self)
4731 
4732     def NewBranchView(self, event_id):
4733         BranchWindow(self.glb, event_id, ReportVars(), self)
4734 
4735     def NewSelectedBranchView(self, event_id):
4736         dialog = SelectedBranchDialog(self.glb, self)
4737         ret = dialog.exec_()
4738         if ret:
4739             BranchWindow(self.glb, event_id, dialog.report_vars, self)
4740 
4741     def NewTableView(self, table_name):
4742         TableWindow(self.glb, table_name, self)
4743 
4744     def Help(self):
4745         HelpWindow(self.glb, self)
4746 
4747     def About(self):
4748         dialog = AboutDialog(self.glb, self)
4749         dialog.exec_()
4750 
4751 def TryOpen(file_name):
4752     try:
4753         return open(file_name, "rb")
4754     except:
4755         return None
4756 
4757 def Is64Bit(f):
4758     result = sizeof(c_void_p)
4759     # ELF support only
4760     pos = f.tell()
4761     f.seek(0)
4762     header = f.read(7)
4763     f.seek(pos)
4764     magic = header[0:4]
4765     if sys.version_info[0] == 2:
4766         eclass = ord(header[4])
4767         encoding = ord(header[5])
4768         version = ord(header[6])
4769     else:
4770         eclass = header[4]
4771         encoding = header[5]
4772         version = header[6]
4773     if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
4774         result = True if eclass == 2 else False
4775     return result
4776 
4777 # Global data
4778 
4779 class Glb():
4780 
4781     def __init__(self, dbref, db, dbname):
4782         self.dbref = dbref
4783         self.db = db
4784         self.dbname = dbname
4785         self.home_dir = os.path.expanduser("~")
4786         self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
4787         if self.buildid_dir:
4788             self.buildid_dir += "/.build-id/"
4789         else:
4790             self.buildid_dir = self.home_dir + "/.debug/.build-id/"
4791         self.app = None
4792         self.mainwindow = None
4793         self.instances_to_shutdown_on_exit = weakref.WeakSet()
4794         try:
4795             self.disassembler = LibXED()
4796             self.have_disassembler = True
4797         except:
4798             self.have_disassembler = False
4799         self.host_machine_id = 0
4800         self.host_start_time = 0
4801         self.host_finish_time = 0
4802 
4803     def FileFromBuildId(self, build_id):
4804         file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
4805         return TryOpen(file_name)
4806 
4807     def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
4808         # Assume current machine i.e. no support for virtualization
4809         if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
4810             file_name = os.getenv("PERF_KCORE")
4811             f = TryOpen(file_name) if file_name else None
4812             if f:
4813                 return f
4814             # For now, no special handling if long_name is /proc/kcore
4815             f = TryOpen(long_name)
4816             if f:
4817                 return f
4818         f = self.FileFromBuildId(build_id)
4819         if f:
4820             return f
4821         return None
4822 
4823     def AddInstanceToShutdownOnExit(self, instance):
4824         self.instances_to_shutdown_on_exit.add(instance)
4825 
4826     # Shutdown any background processes or threads
4827     def ShutdownInstances(self):
4828         for x in self.instances_to_shutdown_on_exit:
4829             try:
4830                 x.Shutdown()
4831             except:
4832                 pass
4833 
4834     def GetHostMachineId(self):
4835         query = QSqlQuery(self.db)
4836         QueryExec(query, "SELECT id FROM machines WHERE pid = -1")
4837         if query.next():
4838             self.host_machine_id = query.value(0)
4839         else:
4840             self.host_machine_id = 0
4841         return self.host_machine_id
4842 
4843     def HostMachineId(self):
4844         if self.host_machine_id:
4845             return self.host_machine_id
4846         return self.GetHostMachineId()
4847 
4848     def SelectValue(self, sql):
4849         query = QSqlQuery(self.db)
4850         try:
4851             QueryExec(query, sql)
4852         except:
4853             return None
4854         if query.next():
4855             return Decimal(query.value(0))
4856         return None
4857 
4858     def SwitchesMinTime(self, machine_id):
4859         return self.SelectValue("SELECT time"
4860                     " FROM context_switches"
4861                     " WHERE time != 0 AND machine_id = " + str(machine_id) +
4862                     " ORDER BY id LIMIT 1")
4863 
4864     def SwitchesMaxTime(self, machine_id):
4865         return self.SelectValue("SELECT time"
4866                     " FROM context_switches"
4867                     " WHERE time != 0 AND machine_id = " + str(machine_id) +
4868                     " ORDER BY id DESC LIMIT 1")
4869 
4870     def SamplesMinTime(self, machine_id):
4871         return self.SelectValue("SELECT time"
4872                     " FROM samples"
4873                     " WHERE time != 0 AND machine_id = " + str(machine_id) +
4874                     " ORDER BY id LIMIT 1")
4875 
4876     def SamplesMaxTime(self, machine_id):
4877         return self.SelectValue("SELECT time"
4878                     " FROM samples"
4879                     " WHERE time != 0 AND machine_id = " + str(machine_id) +
4880                     " ORDER BY id DESC LIMIT 1")
4881 
4882     def CallsMinTime(self, machine_id):
4883         return self.SelectValue("SELECT calls.call_time"
4884                     " FROM calls"
4885                     " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4886                     " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +
4887                     " ORDER BY calls.id LIMIT 1")
4888 
4889     def CallsMaxTime(self, machine_id):
4890         return self.SelectValue("SELECT calls.return_time"
4891                     " FROM calls"
4892                     " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4893                     " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +
4894                     " ORDER BY calls.return_time DESC LIMIT 1")
4895 
4896     def GetStartTime(self, machine_id):
4897         t0 = self.SwitchesMinTime(machine_id)
4898         t1 = self.SamplesMinTime(machine_id)
4899         t2 = self.CallsMinTime(machine_id)
4900         if t0 is None or (not(t1 is None) and t1 < t0):
4901             t0 = t1
4902         if t0 is None or (not(t2 is None) and t2 < t0):
4903             t0 = t2
4904         return t0
4905 
4906     def GetFinishTime(self, machine_id):
4907         t0 = self.SwitchesMaxTime(machine_id)
4908         t1 = self.SamplesMaxTime(machine_id)
4909         t2 = self.CallsMaxTime(machine_id)
4910         if t0 is None or (not(t1 is None) and t1 > t0):
4911             t0 = t1
4912         if t0 is None or (not(t2 is None) and t2 > t0):
4913             t0 = t2
4914         return t0
4915 
4916     def HostStartTime(self):
4917         if self.host_start_time:
4918             return self.host_start_time
4919         self.host_start_time = self.GetStartTime(self.HostMachineId())
4920         return self.host_start_time
4921 
4922     def HostFinishTime(self):
4923         if self.host_finish_time:
4924             return self.host_finish_time
4925         self.host_finish_time = self.GetFinishTime(self.HostMachineId())
4926         return self.host_finish_time
4927 
4928     def StartTime(self, machine_id):
4929         if machine_id == self.HostMachineId():
4930             return self.HostStartTime()
4931         return self.GetStartTime(machine_id)
4932 
4933     def FinishTime(self, machine_id):
4934         if machine_id == self.HostMachineId():
4935             return self.HostFinishTime()
4936         return self.GetFinishTime(machine_id)
4937 
4938 # Database reference
4939 
4940 class DBRef():
4941 
4942     def __init__(self, is_sqlite3, dbname):
4943         self.is_sqlite3 = is_sqlite3
4944         self.dbname = dbname
4945         self.TRUE = "TRUE"
4946         self.FALSE = "FALSE"
4947         # SQLite prior to version 3.23 does not support TRUE and FALSE
4948         if self.is_sqlite3:
4949             self.TRUE = "1"
4950             self.FALSE = "0"
4951 
4952     def Open(self, connection_name):
4953         dbname = self.dbname
4954         if self.is_sqlite3:
4955             db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
4956         else:
4957             db = QSqlDatabase.addDatabase("QPSQL", connection_name)
4958             opts = dbname.split()
4959             for opt in opts:
4960                 if "=" in opt:
4961                     opt = opt.split("=")
4962                     if opt[0] == "hostname":
4963                         db.setHostName(opt[1])
4964                     elif opt[0] == "port":
4965                         db.setPort(int(opt[1]))
4966                     elif opt[0] == "username":
4967                         db.setUserName(opt[1])
4968                     elif opt[0] == "password":
4969                         db.setPassword(opt[1])
4970                     elif opt[0] == "dbname":
4971                         dbname = opt[1]
4972                 else:
4973                     dbname = opt
4974 
4975         db.setDatabaseName(dbname)
4976         if not db.open():
4977             raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
4978         return db, dbname
4979 
4980 # Main
4981 
4982 def Main():
4983     usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
4984             "   or: exported-sql-viewer.py --help-only"
4985     ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
4986     ap.add_argument("--pyside-version-1", action='store_true')
4987     ap.add_argument("dbname", nargs="?")
4988     ap.add_argument("--help-only", action='store_true')
4989     args = ap.parse_args()
4990 
4991     if args.help_only:
4992         app = QApplication(sys.argv)
4993         mainwindow = HelpOnlyWindow()
4994         mainwindow.show()
4995         err = app.exec_()
4996         sys.exit(err)
4997 
4998     dbname = args.dbname
4999     if dbname is None:
5000         ap.print_usage()
5001         print("Too few arguments")
5002         sys.exit(1)
5003 
5004     is_sqlite3 = False
5005     try:
5006         f = open(dbname, "rb")
5007         if f.read(15) == b'SQLite format 3':
5008             is_sqlite3 = True
5009         f.close()
5010     except:
5011         pass
5012 
5013     dbref = DBRef(is_sqlite3, dbname)
5014     db, dbname = dbref.Open("main")
5015     glb = Glb(dbref, db, dbname)
5016     app = QApplication(sys.argv)
5017     glb.app = app
5018     mainwindow = MainWindow(glb)
5019     glb.mainwindow = mainwindow
5020     mainwindow.show()
5021     err = app.exec_()
5022     glb.ShutdownInstances()
5023     db.close()
5024     sys.exit(err)
5025 
5026 if __name__ == "__main__":
5027     Main()