diff options
Diffstat (limited to 'tools/perf/scripts/python')
| -rwxr-xr-x | tools/perf/scripts/python/exported-sql-viewer.py | 5030 |
1 files changed, 0 insertions, 5030 deletions
diff --git a/tools/perf/scripts/python/exported-sql-viewer.py b/tools/perf/scripts/python/exported-sql-viewer.py deleted file mode 100755 index e0b2e7268ef6..000000000000 --- a/tools/perf/scripts/python/exported-sql-viewer.py +++ /dev/null @@ -1,5030 +0,0 @@ -#!/usr/bin/env python -# SPDX-License-Identifier: GPL-2.0 -# exported-sql-viewer.py: view data from sql database -# Copyright (c) 2014-2018, Intel Corporation. - -# To use this script you will need to have exported data using either the -# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those -# scripts for details. -# -# Following on from the example in the export scripts, a -# call-graph can be displayed for the pt_example database like this: -# -# python tools/perf/scripts/python/exported-sql-viewer.py pt_example -# -# Note that for PostgreSQL, this script supports connecting to remote databases -# by setting hostname, port, username, password, and dbname e.g. -# -# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example" -# -# The result is a GUI window with a tree representing a context-sensitive -# call-graph. Expanding a couple of levels of the tree and adjusting column -# widths to suit will display something like: -# -# Call Graph: pt_example -# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) -# v- ls -# v- 2638:2638 -# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 -# |- unknown unknown 1 13198 0.1 1 0.0 -# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 -# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 -# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 -# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 -# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 -# >- __libc_csu_init ls 1 10354 0.1 10 0.0 -# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 -# v- main ls 1 8182043 99.6 180254 99.9 -# -# Points to note: -# The top level is a command name (comm) -# The next level is a thread (pid:tid) -# Subsequent levels are functions -# 'Count' is the number of calls -# 'Time' is the elapsed time until the function returns -# Percentages are relative to the level above -# 'Branch Count' is the total number of branches for that function and all -# functions that it calls - -# There is also a "All branches" report, which displays branches and -# possibly disassembly. However, presently, the only supported disassembler is -# Intel XED, and additionally the object code must be present in perf build ID -# cache. To use Intel XED, libxed.so must be present. To build and install -# libxed.so: -# git clone https://github.com/intelxed/mbuild.git mbuild -# git clone https://github.com/intelxed/xed -# cd xed -# ./mfile.py --share -# sudo ./mfile.py --prefix=/usr/local install -# sudo ldconfig -# -# Example report: -# -# Time CPU Command PID TID Branch Type In Tx Branch -# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) -# 7fab593ea260 48 89 e7 mov %rsp, %rdi -# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) -# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) -# 7fab593ea260 48 89 e7 mov %rsp, %rdi -# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930 -# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so) -# 7fab593ea930 55 pushq %rbp -# 7fab593ea931 48 89 e5 mov %rsp, %rbp -# 7fab593ea934 41 57 pushq %r15 -# 7fab593ea936 41 56 pushq %r14 -# 7fab593ea938 41 55 pushq %r13 -# 7fab593ea93a 41 54 pushq %r12 -# 7fab593ea93c 53 pushq %rbx -# 7fab593ea93d 48 89 fb mov %rdi, %rbx -# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp -# 7fab593ea944 0f 31 rdtsc -# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx -# 7fab593ea94a 89 c0 mov %eax, %eax -# 7fab593ea94c 48 09 c2 or %rax, %rdx -# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax -# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) -# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax -# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip) -# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) - -from __future__ import print_function - -import sys -# Only change warnings if the python -W option was not used -if not sys.warnoptions: - import warnings - # PySide2 causes deprecation warnings, ignore them. - warnings.filterwarnings("ignore", category=DeprecationWarning) -import argparse -import weakref -import threading -import string -try: - # Python2 - import cPickle as pickle - # size of pickled integer big enough for record size - glb_nsz = 8 -except ImportError: - import pickle - glb_nsz = 16 -import re -import os -import random -import copy -import math -from libxed import LibXED - -pyside_version_1 = True -if not "--pyside-version-1" in sys.argv: - try: - from PySide2.QtCore import * - from PySide2.QtGui import * - from PySide2.QtSql import * - from PySide2.QtWidgets import * - pyside_version_1 = False - except: - pass - -if pyside_version_1: - from PySide.QtCore import * - from PySide.QtGui import * - from PySide.QtSql import * - -from decimal import Decimal, ROUND_HALF_UP -from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \ - c_void_p, c_bool, c_byte, c_char, c_int, c_uint, c_longlong, c_ulonglong -from multiprocessing import Process, Array, Value, Event - -# xrange is range in Python3 -try: - xrange -except NameError: - xrange = range - -def printerr(*args, **keyword_args): - print(*args, file=sys.stderr, **keyword_args) - -# Data formatting helpers - -def tohex(ip): - if ip < 0: - ip += 1 << 64 - return "%x" % ip - -def offstr(offset): - if offset: - return "+0x%x" % offset - return "" - -def dsoname(name): - if name == "[kernel.kallsyms]": - return "[kernel]" - return name - -def findnth(s, sub, n, offs=0): - pos = s.find(sub) - if pos < 0: - return pos - if n <= 1: - return offs + pos - return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1) - -# Percent to one decimal place - -def PercentToOneDP(n, d): - if not d: - return "0.0" - x = (n * Decimal(100)) / d - return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) - -# Helper for queries that must not fail - -def QueryExec(query, stmt): - ret = query.exec_(stmt) - if not ret: - raise Exception("Query failed: " + query.lastError().text()) - -# Background thread - -class Thread(QThread): - - done = Signal(object) - - def __init__(self, task, param=None, parent=None): - super(Thread, self).__init__(parent) - self.task = task - self.param = param - - def run(self): - while True: - if self.param is None: - done, result = self.task() - else: - done, result = self.task(self.param) - self.done.emit(result) - if done: - break - -# Tree data model - -class TreeModel(QAbstractItemModel): - - def __init__(self, glb, params, parent=None): - super(TreeModel, self).__init__(parent) - self.glb = glb - self.params = params - self.root = self.GetRoot() - self.last_row_read = 0 - - def Item(self, parent): - if parent.isValid(): - return parent.internalPointer() - else: - return self.root - - def rowCount(self, parent): - result = self.Item(parent).childCount() - if result < 0: - result = 0 - self.dataChanged.emit(parent, parent) - return result - - def hasChildren(self, parent): - return self.Item(parent).hasChildren() - - def headerData(self, section, orientation, role): - if role == Qt.TextAlignmentRole: - return self.columnAlignment(section) - if role != Qt.DisplayRole: - return None - if orientation != Qt.Horizontal: - return None - return self.columnHeader(section) - - def parent(self, child): - child_item = child.internalPointer() - if child_item is self.root: - return QModelIndex() - parent_item = child_item.getParentItem() - return self.createIndex(parent_item.getRow(), 0, parent_item) - - def index(self, row, column, parent): - child_item = self.Item(parent).getChildItem(row) - return self.createIndex(row, column, child_item) - - def DisplayData(self, item, index): - return item.getData(index.column()) - - def FetchIfNeeded(self, row): - if row > self.last_row_read: - self.last_row_read = row - if row + 10 >= self.root.child_count: - self.fetcher.Fetch(glb_chunk_sz) - - def columnAlignment(self, column): - return Qt.AlignLeft - - def columnFont(self, column): - return None - - def data(self, index, role): - if role == Qt.TextAlignmentRole: - return self.columnAlignment(index.column()) - if role == Qt.FontRole: - return self.columnFont(index.column()) - if role != Qt.DisplayRole: - return None - item = index.internalPointer() - return self.DisplayData(item, index) - -# Table data model - -class TableModel(QAbstractTableModel): - - def __init__(self, parent=None): - super(TableModel, self).__init__(parent) - self.child_count = 0 - self.child_items = [] - self.last_row_read = 0 - - def Item(self, parent): - if parent.isValid(): - return parent.internalPointer() - else: - return self - - def rowCount(self, parent): - return self.child_count - - def headerData(self, section, orientation, role): - if role == Qt.TextAlignmentRole: - return self.columnAlignment(section) - if role != Qt.DisplayRole: - return None - if orientation != Qt.Horizontal: - return None - return self.columnHeader(section) - - def index(self, row, column, parent): - return self.createIndex(row, column, self.child_items[row]) - - def DisplayData(self, item, index): - return item.getData(index.column()) - - def FetchIfNeeded(self, row): - if row > self.last_row_read: - self.last_row_read = row - if row + 10 >= self.child_count: - self.fetcher.Fetch(glb_chunk_sz) - - def columnAlignment(self, column): - return Qt.AlignLeft - - def columnFont(self, column): - return None - - def data(self, index, role): - if role == Qt.TextAlignmentRole: - return self.columnAlignment(index.column()) - if role == Qt.FontRole: - return self.columnFont(index.column()) - if role != Qt.DisplayRole: - return None - item = index.internalPointer() - return self.DisplayData(item, index) - -# Model cache - -model_cache = weakref.WeakValueDictionary() -model_cache_lock = threading.Lock() - -def LookupCreateModel(model_name, create_fn): - model_cache_lock.acquire() - try: - model = model_cache[model_name] - except: - model = None - if model is None: - model = create_fn() - model_cache[model_name] = model - model_cache_lock.release() - return model - -def LookupModel(model_name): - model_cache_lock.acquire() - try: - model = model_cache[model_name] - except: - model = None - model_cache_lock.release() - return model - -# Find bar - -class FindBar(): - - def __init__(self, parent, finder, is_reg_expr=False): - self.finder = finder - self.context = [] - self.last_value = None - self.last_pattern = None - - label = QLabel("Find:") - label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.textbox = QComboBox() - self.textbox.setEditable(True) - self.textbox.currentIndexChanged.connect(self.ValueChanged) - - self.progress = QProgressBar() - self.progress.setRange(0, 0) - self.progress.hide() - - if is_reg_expr: - self.pattern = QCheckBox("Regular Expression") - else: - self.pattern = QCheckBox("Pattern") - self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.next_button = QToolButton() - self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown)) - self.next_button.released.connect(lambda: self.NextPrev(1)) - - self.prev_button = QToolButton() - self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp)) - self.prev_button.released.connect(lambda: self.NextPrev(-1)) - - self.close_button = QToolButton() - self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) - self.close_button.released.connect(self.Deactivate) - - self.hbox = QHBoxLayout() - self.hbox.setContentsMargins(0, 0, 0, 0) - - self.hbox.addWidget(label) - self.hbox.addWidget(self.textbox) - self.hbox.addWidget(self.progress) - self.hbox.addWidget(self.pattern) - self.hbox.addWidget(self.next_button) - self.hbox.addWidget(self.prev_button) - self.hbox.addWidget(self.close_button) - - self.bar = QWidget() - self.bar.setLayout(self.hbox) - self.bar.hide() - - def Widget(self): - return self.bar - - def Activate(self): - self.bar.show() - self.textbox.lineEdit().selectAll() - self.textbox.setFocus() - - def Deactivate(self): - self.bar.hide() - - def Busy(self): - self.textbox.setEnabled(False) - self.pattern.hide() - self.next_button.hide() - self.prev_button.hide() - self.progress.show() - - def Idle(self): - self.textbox.setEnabled(True) - self.progress.hide() - self.pattern.show() - self.next_button.show() - self.prev_button.show() - - def Find(self, direction): - value = self.textbox.currentText() - pattern = self.pattern.isChecked() - self.last_value = value - self.last_pattern = pattern - self.finder.Find(value, direction, pattern, self.context) - - def ValueChanged(self): - value = self.textbox.currentText() - pattern = self.pattern.isChecked() - index = self.textbox.currentIndex() - data = self.textbox.itemData(index) - # Store the pattern in the combo box to keep it with the text value - if data == None: - self.textbox.setItemData(index, pattern) - else: - self.pattern.setChecked(data) - self.Find(0) - - def NextPrev(self, direction): - value = self.textbox.currentText() - pattern = self.pattern.isChecked() - if value != self.last_value: - index = self.textbox.findText(value) - # Allow for a button press before the value has been added to the combo box - if index < 0: - index = self.textbox.count() - self.textbox.addItem(value, pattern) - self.textbox.setCurrentIndex(index) - return - else: - self.textbox.setItemData(index, pattern) - elif pattern != self.last_pattern: - # Keep the pattern recorded in the combo box up to date - index = self.textbox.currentIndex() - self.textbox.setItemData(index, pattern) - self.Find(direction) - - def NotFound(self): - QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found") - -# Context-sensitive call graph data model item base - -class CallGraphLevelItemBase(object): - - def __init__(self, glb, params, row, parent_item): - self.glb = glb - self.params = params - self.row = row - self.parent_item = parent_item - self.query_done = False - self.child_count = 0 - self.child_items = [] - if parent_item: - self.level = parent_item.level + 1 - else: - self.level = 0 - - def getChildItem(self, row): - return self.child_items[row] - - def getParentItem(self): - return self.parent_item - - def getRow(self): - return self.row - - def childCount(self): - if not self.query_done: - self.Select() - if not self.child_count: - return -1 - return self.child_count - - def hasChildren(self): - if not self.query_done: - return True - return self.child_count > 0 - - def getData(self, column): - return self.data[column] - -# Context-sensitive call graph data model level 2+ item base - -class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item): - super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) - self.comm_id = comm_id - self.thread_id = thread_id - self.call_path_id = call_path_id - self.insn_cnt = insn_cnt - self.cyc_cnt = cyc_cnt - self.branch_count = branch_count - self.time = time - - def Select(self): - self.query_done = True - query = QSqlQuery(self.glb.db) - if self.params.have_ipc: - ipc_str = ", SUM(insn_count), SUM(cyc_count)" - else: - ipc_str = "" - QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " INNER JOIN dsos ON symbols.dso_id = dsos.id" - " WHERE parent_call_path_id = " + str(self.call_path_id) + - " AND comm_id = " + str(self.comm_id) + - " AND thread_id = " + str(self.thread_id) + - " GROUP BY call_path_id, name, short_name" - " ORDER BY call_path_id") - while query.next(): - if self.params.have_ipc: - insn_cnt = int(query.value(5)) - cyc_cnt = int(query.value(6)) - branch_count = int(query.value(7)) - else: - insn_cnt = 0 - cyc_cnt = 0 - branch_count = int(query.value(5)) - 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) - self.child_items.append(child_item) - self.child_count += 1 - -# Context-sensitive call graph data model level three item - -class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): - - 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): - super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item) - dso = dsoname(dso) - if self.params.have_ipc: - insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) - cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) - br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) - ipc = CalcIPC(cyc_cnt, insn_cnt) - 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 ] - else: - self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] - self.dbid = call_path_id - -# Context-sensitive call graph data model level two item - -class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): - super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item) - if self.params.have_ipc: - self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] - else: - self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] - self.dbid = thread_id - - def Select(self): - super(CallGraphLevelTwoItem, self).Select() - for child_item in self.child_items: - self.time += child_item.time - self.insn_cnt += child_item.insn_cnt - self.cyc_cnt += child_item.cyc_cnt - self.branch_count += child_item.branch_count - for child_item in self.child_items: - child_item.data[4] = PercentToOneDP(child_item.time, self.time) - if self.params.have_ipc: - child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) - child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) - child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) - else: - child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) - -# Context-sensitive call graph data model level one item - -class CallGraphLevelOneItem(CallGraphLevelItemBase): - - def __init__(self, glb, params, row, comm_id, comm, parent_item): - super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item) - if self.params.have_ipc: - self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] - else: - self.data = [comm, "", "", "", "", "", ""] - self.dbid = comm_id - - def Select(self): - self.query_done = True - query = QSqlQuery(self.glb.db) - QueryExec(query, "SELECT thread_id, pid, tid" - " FROM comm_threads" - " INNER JOIN threads ON thread_id = threads.id" - " WHERE comm_id = " + str(self.dbid)) - while query.next(): - child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) - self.child_items.append(child_item) - self.child_count += 1 - -# Context-sensitive call graph data model root item - -class CallGraphRootItem(CallGraphLevelItemBase): - - def __init__(self, glb, params): - super(CallGraphRootItem, self).__init__(glb, params, 0, None) - self.dbid = 0 - self.query_done = True - if_has_calls = "" - if IsSelectable(glb.db, "comms", columns = "has_calls"): - if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE - query = QSqlQuery(glb.db) - QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) - while query.next(): - if not query.value(0): - continue - child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) - self.child_items.append(child_item) - self.child_count += 1 - -# Call graph model parameters - -class CallGraphModelParams(): - - def __init__(self, glb, parent=None): - self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count") - -# Context-sensitive call graph data model base - -class CallGraphModelBase(TreeModel): - - def __init__(self, glb, parent=None): - super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent) - - def FindSelect(self, value, pattern, query): - if pattern: - # postgresql and sqlite pattern patching differences: - # postgresql LIKE is case sensitive but sqlite LIKE is not - # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not - # postgresql supports ILIKE which is case insensitive - # sqlite supports GLOB (text only) which uses * and ? and is case sensitive - if not self.glb.dbref.is_sqlite3: - # Escape % and _ - s = value.replace("%", "\\%") - s = s.replace("_", "\\_") - # Translate * and ? into SQL LIKE pattern characters % and _ - if sys.version_info[0] == 3: - trans = str.maketrans("*?", "%_") - else: - trans = string.maketrans("*?", "%_") - match = " LIKE '" + str(s).translate(trans) + "'" - else: - match = " GLOB '" + str(value) + "'" - else: - match = " = '" + str(value) + "'" - self.DoFindSelect(query, match) - - def Found(self, query, found): - if found: - return self.FindPath(query) - return [] - - def FindValue(self, value, pattern, query, last_value, last_pattern): - if last_value == value and pattern == last_pattern: - found = query.first() - else: - self.FindSelect(value, pattern, query) - found = query.next() - return self.Found(query, found) - - def FindNext(self, query): - found = query.next() - if not found: - found = query.first() - return self.Found(query, found) - - def FindPrev(self, query): - found = query.previous() - if not found: - found = query.last() - return self.Found(query, found) - - def FindThread(self, c): - if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern: - ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern) - elif c.direction > 0: - ids = self.FindNext(c.query) - else: - ids = self.FindPrev(c.query) - return (True, ids) - - def Find(self, value, direction, pattern, context, callback): - class Context(): - def __init__(self, *x): - self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x - def Update(self, *x): - self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern) - if len(context): - context[0].Update(value, direction, pattern) - else: - context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None)) - # Use a thread so the UI is not blocked during the SELECT - thread = Thread(self.FindThread, context[0]) - thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection) - thread.start() - - def FindDone(self, thread, callback, ids): - callback(ids) - -# Context-sensitive call graph data model - -class CallGraphModel(CallGraphModelBase): - - def __init__(self, glb, parent=None): - super(CallGraphModel, self).__init__(glb, parent) - - def GetRoot(self): - return CallGraphRootItem(self.glb, self.params) - - def columnCount(self, parent=None): - if self.params.have_ipc: - return 12 - else: - return 7 - - def columnHeader(self, column): - if self.params.have_ipc: - headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] - else: - headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] - return headers[column] - - def columnAlignment(self, column): - if self.params.have_ipc: - 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 ] - else: - alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] - return alignment[column] - - def DoFindSelect(self, query, match): - QueryExec(query, "SELECT call_path_id, comm_id, thread_id" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " WHERE calls.id <> 0" - " AND symbols.name" + match + - " GROUP BY comm_id, thread_id, call_path_id" - " ORDER BY comm_id, thread_id, call_path_id") - - def FindPath(self, query): - # Turn the query result into a list of ids that the tree view can walk - # to open the tree at the right place. - ids = [] - parent_id = query.value(0) - while parent_id: - ids.insert(0, parent_id) - q2 = QSqlQuery(self.glb.db) - QueryExec(q2, "SELECT parent_id" - " FROM call_paths" - " WHERE id = " + str(parent_id)) - if not q2.next(): - break - parent_id = q2.value(0) - # The call path root is not used - if ids[0] == 1: - del ids[0] - ids.insert(0, query.value(2)) - ids.insert(0, query.value(1)) - return ids - -# Call tree data model level 2+ item base - -class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item): - super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) - self.comm_id = comm_id - self.thread_id = thread_id - self.calls_id = calls_id - self.call_time = call_time - self.time = time - self.insn_cnt = insn_cnt - self.cyc_cnt = cyc_cnt - self.branch_count = branch_count - - def Select(self): - self.query_done = True - if self.calls_id == 0: - comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id) - else: - comm_thread = "" - if self.params.have_ipc: - ipc_str = ", insn_count, cyc_count" - else: - ipc_str = "" - query = QSqlQuery(self.glb.db) - QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " INNER JOIN dsos ON symbols.dso_id = dsos.id" - " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread + - " ORDER BY call_time, calls.id") - while query.next(): - if self.params.have_ipc: - insn_cnt = int(query.value(5)) - cyc_cnt = int(query.value(6)) - branch_count = int(query.value(7)) - else: - insn_cnt = 0 - cyc_cnt = 0 - branch_count = int(query.value(5)) - 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) - self.child_items.append(child_item) - self.child_count += 1 - -# Call tree data model level three item - -class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase): - - 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): - super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item) - dso = dsoname(dso) - if self.params.have_ipc: - insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) - cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) - br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) - ipc = CalcIPC(cyc_cnt, insn_cnt) - 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 ] - else: - self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] - self.dbid = calls_id - -# Call tree data model level two item - -class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase): - - def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): - super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item) - if self.params.have_ipc: - self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] - else: - self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] - self.dbid = thread_id - - def Select(self): - super(CallTreeLevelTwoItem, self).Select() - for child_item in self.child_items: - self.time += child_item.time - self.insn_cnt += child_item.insn_cnt - self.cyc_cnt += child_item.cyc_cnt - self.branch_count += child_item.branch_count - for child_item in self.child_items: - child_item.data[4] = PercentToOneDP(child_item.time, self.time) - if self.params.have_ipc: - child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) - child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) - child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) - else: - child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) - -# Call tree data model level one item - -class CallTreeLevelOneItem(CallGraphLevelItemBase): - - def __init__(self, glb, params, row, comm_id, comm, parent_item): - super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item) - if self.params.have_ipc: - self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] - else: - self.data = [comm, "", "", "", "", "", ""] - self.dbid = comm_id - - def Select(self): - self.query_done = True - query = QSqlQuery(self.glb.db) - QueryExec(query, "SELECT thread_id, pid, tid" - " FROM comm_threads" - " INNER JOIN threads ON thread_id = threads.id" - " WHERE comm_id = " + str(self.dbid)) - while query.next(): - child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) - self.child_items.append(child_item) - self.child_count += 1 - -# Call tree data model root item - -class CallTreeRootItem(CallGraphLevelItemBase): - - def __init__(self, glb, params): - super(CallTreeRootItem, self).__init__(glb, params, 0, None) - self.dbid = 0 - self.query_done = True - if_has_calls = "" - if IsSelectable(glb.db, "comms", columns = "has_calls"): - if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE - query = QSqlQuery(glb.db) - QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) - while query.next(): - if not query.value(0): - continue - child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) - self.child_items.append(child_item) - self.child_count += 1 - -# Call Tree data model - -class CallTreeModel(CallGraphModelBase): - - def __init__(self, glb, parent=None): - super(CallTreeModel, self).__init__(glb, parent) - - def GetRoot(self): - return CallTreeRootItem(self.glb, self.params) - - def columnCount(self, parent=None): - if self.params.have_ipc: - return 12 - else: - return 7 - - def columnHeader(self, column): - if self.params.have_ipc: - headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] - else: - headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] - return headers[column] - - def columnAlignment(self, column): - if self.params.have_ipc: - 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 ] - else: - alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] - return alignment[column] - - def DoFindSelect(self, query, match): - QueryExec(query, "SELECT calls.id, comm_id, thread_id" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " WHERE calls.id <> 0" - " AND symbols.name" + match + - " ORDER BY comm_id, thread_id, call_time, calls.id") - - def FindPath(self, query): - # Turn the query result into a list of ids that the tree view can walk - # to open the tree at the right place. - ids = [] - parent_id = query.value(0) - while parent_id: - ids.insert(0, parent_id) - q2 = QSqlQuery(self.glb.db) - QueryExec(q2, "SELECT parent_id" - " FROM calls" - " WHERE id = " + str(parent_id)) - if not q2.next(): - break - parent_id = q2.value(0) - ids.insert(0, query.value(2)) - ids.insert(0, query.value(1)) - return ids - -# Vertical layout - -class HBoxLayout(QHBoxLayout): - - def __init__(self, *children): - super(HBoxLayout, self).__init__() - - self.layout().setContentsMargins(0, 0, 0, 0) - for child in children: - if child.isWidgetType(): - self.layout().addWidget(child) - else: - self.layout().addLayout(child) - -# Horizontal layout - -class VBoxLayout(QVBoxLayout): - - def __init__(self, *children): - super(VBoxLayout, self).__init__() - - self.layout().setContentsMargins(0, 0, 0, 0) - for child in children: - if child.isWidgetType(): - self.layout().addWidget(child) - else: - self.layout().addLayout(child) - -# Vertical layout widget - -class VBox(): - - def __init__(self, *children): - self.vbox = QWidget() - self.vbox.setLayout(VBoxLayout(*children)) - - def Widget(self): - return self.vbox - -# Tree window base - -class TreeWindowBase(QMdiSubWindow): - - def __init__(self, parent=None): - super(TreeWindowBase, self).__init__(parent) - - self.model = None - self.find_bar = None - - self.view = QTreeView() - self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) - self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard - - self.context_menu = TreeContextMenu(self.view) - - def DisplayFound(self, ids): - if not len(ids): - return False - parent = QModelIndex() - for dbid in ids: - found = False - n = self.model.rowCount(parent) - for row in xrange(n): - child = self.model.index(row, 0, parent) - if child.internalPointer().dbid == dbid: - found = True - self.view.setExpanded(parent, True) - self.view.setCurrentIndex(child) - parent = child - break - if not found: - break - return found - - def Find(self, value, direction, pattern, context): - self.view.setFocus() - self.find_bar.Busy() - self.model.Find(value, direction, pattern, context, self.FindDone) - - def FindDone(self, ids): - found = True - if not self.DisplayFound(ids): - found = False - self.find_bar.Idle() - if not found: - self.find_bar.NotFound() - - -# Context-sensitive call graph window - -class CallGraphWindow(TreeWindowBase): - - def __init__(self, glb, parent=None): - super(CallGraphWindow, self).__init__(parent) - - self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) - - self.view.setModel(self.model) - - for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): - self.view.setColumnWidth(c, w) - - self.find_bar = FindBar(self, self) - - self.vbox = VBox(self.view, self.find_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") - -# Call tree window - -class CallTreeWindow(TreeWindowBase): - - def __init__(self, glb, parent=None, thread_at_time=None): - super(CallTreeWindow, self).__init__(parent) - - self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x)) - - self.view.setModel(self.model) - - for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)): - self.view.setColumnWidth(c, w) - - self.find_bar = FindBar(self, self) - - self.vbox = VBox(self.view, self.find_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree") - - if thread_at_time: - self.DisplayThreadAtTime(*thread_at_time) - - def DisplayThreadAtTime(self, comm_id, thread_id, time): - parent = QModelIndex() - for dbid in (comm_id, thread_id): - found = False - n = self.model.rowCount(parent) - for row in xrange(n): - child = self.model.index(row, 0, parent) - if child.internalPointer().dbid == dbid: - found = True - self.view.setExpanded(parent, True) - self.view.setCurrentIndex(child) - parent = child - break - if not found: - return - found = False - while True: - n = self.model.rowCount(parent) - if not n: - return - last_child = None - for row in xrange(n): - self.view.setExpanded(parent, True) - child = self.model.index(row, 0, parent) - child_call_time = child.internalPointer().call_time - if child_call_time < time: - last_child = child - elif child_call_time == time: - self.view.setCurrentIndex(child) - return - elif child_call_time > time: - break - if not last_child: - if not found: - child = self.model.index(0, 0, parent) - self.view.setExpanded(parent, True) - self.view.setCurrentIndex(child) - return - found = True - self.view.setExpanded(parent, True) - self.view.setCurrentIndex(last_child) - parent = last_child - -# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name - -def ExecComm(db, thread_id, time): - query = QSqlQuery(db) - QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag" - " FROM comm_threads" - " INNER JOIN comms ON comms.id = comm_threads.comm_id" - " WHERE comm_threads.thread_id = " + str(thread_id) + - " ORDER BY comms.c_time, comms.id") - first = None - last = None - while query.next(): - if first is None: - first = query.value(0) - if query.value(2) and Decimal(query.value(1)) <= Decimal(time): - last = query.value(0) - if not(last is None): - return last - return first - -# Container for (x, y) data - -class XY(): - def __init__(self, x=0, y=0): - self.x = x - self.y = y - - def __str__(self): - return "XY({}, {})".format(str(self.x), str(self.y)) - -# Container for sub-range data - -class Subrange(): - def __init__(self, lo=0, hi=0): - self.lo = lo - self.hi = hi - - def __str__(self): - return "Subrange({}, {})".format(str(self.lo), str(self.hi)) - -# Graph data region base class - -class GraphDataRegion(object): - - def __init__(self, key, title = "", ordinal = ""): - self.key = key - self.title = title - self.ordinal = ordinal - -# Function to sort GraphDataRegion - -def GraphDataRegionOrdinal(data_region): - return data_region.ordinal - -# Attributes for a graph region - -class GraphRegionAttribute(): - - def __init__(self, colour): - self.colour = colour - -# Switch graph data region represents a task - -class SwitchGraphDataRegion(GraphDataRegion): - - def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id): - super(SwitchGraphDataRegion, self).__init__(key) - - self.title = str(pid) + " / " + str(tid) + " " + comm - # Order graph legend within exec comm by pid / tid / time - self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16) - self.exec_comm_id = exec_comm_id - self.pid = pid - self.tid = tid - self.comm = comm - self.thread_id = thread_id - self.comm_id = comm_id - -# Graph data point - -class GraphDataPoint(): - - def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None): - self.data = data - self.index = index - self.x = x - self.y = y - self.altx = altx - self.alty = alty - self.hregion = hregion - self.vregion = vregion - -# Graph data (single graph) base class - -class GraphData(object): - - def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)): - self.collection = collection - self.points = [] - self.xbase = xbase - self.ybase = ybase - self.title = "" - - def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None): - index = len(self.points) - - x = float(Decimal(x) - self.xbase) - y = float(Decimal(y) - self.ybase) - - self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion)) - - def XToData(self, x): - return Decimal(x) + self.xbase - - def YToData(self, y): - return Decimal(y) + self.ybase - -# Switch graph data (for one CPU) - -class SwitchGraphData(GraphData): - - def __init__(self, db, collection, cpu, xbase): - super(SwitchGraphData, self).__init__(collection, xbase) - - self.cpu = cpu - self.title = "CPU " + str(cpu) - self.SelectSwitches(db) - - def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time): - query = QSqlQuery(db) - QueryExec(query, "SELECT id, c_time" - " FROM comms" - " WHERE c_thread_id = " + str(thread_id) + - " AND exec_flag = " + self.collection.glb.dbref.TRUE + - " AND c_time >= " + str(start_time) + - " AND c_time <= " + str(end_time) + - " ORDER BY c_time, id") - while query.next(): - comm_id = query.value(0) - if comm_id == last_comm_id: - continue - time = query.value(1) - hregion = self.HRegion(db, thread_id, comm_id, time) - self.AddPoint(time, 1000, None, None, hregion) - - def SelectSwitches(self, db): - last_time = None - last_comm_id = None - last_thread_id = None - query = QSqlQuery(db) - QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags" - " FROM context_switches" - " WHERE machine_id = " + str(self.collection.machine_id) + - " AND cpu = " + str(self.cpu) + - " ORDER BY time, id") - while query.next(): - flags = int(query.value(5)) - if flags & 1: - # Schedule-out: detect and add exec's - if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3): - self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0)) - continue - # Schedule-in: add data point - if len(self.points) == 0: - start_time = self.collection.glb.StartTime(self.collection.machine_id) - hregion = self.HRegion(db, query.value(1), query.value(3), start_time) - self.AddPoint(start_time, 1000, None, None, hregion) - time = query.value(0) - comm_id = query.value(4) - thread_id = query.value(2) - hregion = self.HRegion(db, thread_id, comm_id, time) - self.AddPoint(time, 1000, None, None, hregion) - last_time = time - last_comm_id = comm_id - last_thread_id = thread_id - - def NewHRegion(self, db, key, thread_id, comm_id, time): - exec_comm_id = ExecComm(db, thread_id, time) - query = QSqlQuery(db) - QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id)) - if query.next(): - pid = query.value(0) - tid = query.value(1) - else: - pid = -1 - tid = -1 - query = QSqlQuery(db) - QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id)) - if query.next(): - comm = query.value(0) - else: - comm = "" - return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id) - - def HRegion(self, db, thread_id, comm_id, time): - key = str(thread_id) + ":" + str(comm_id) - hregion = self.collection.LookupHRegion(key) - if hregion is None: - hregion = self.NewHRegion(db, key, thread_id, comm_id, time) - self.collection.AddHRegion(key, hregion) - return hregion - -# Graph data collection (multiple related graphs) base class - -class GraphDataCollection(object): - - def __init__(self, glb): - self.glb = glb - self.data = [] - self.hregions = {} - self.xrangelo = None - self.xrangehi = None - self.yrangelo = None - self.yrangehi = None - self.dp = XY(0, 0) - - def AddGraphData(self, data): - self.data.append(data) - - def LookupHRegion(self, key): - if key in self.hregions: - return self.hregions[key] - return None - - def AddHRegion(self, key, hregion): - self.hregions[key] = hregion - -# Switch graph data collection (SwitchGraphData for each CPU) - -class SwitchGraphDataCollection(GraphDataCollection): - - def __init__(self, glb, db, machine_id): - super(SwitchGraphDataCollection, self).__init__(glb) - - self.machine_id = machine_id - self.cpus = self.SelectCPUs(db) - - self.xrangelo = glb.StartTime(machine_id) - self.xrangehi = glb.FinishTime(machine_id) - - self.yrangelo = Decimal(0) - self.yrangehi = Decimal(1000) - - for cpu in self.cpus: - self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo)) - - def SelectCPUs(self, db): - cpus = [] - query = QSqlQuery(db) - QueryExec(query, "SELECT DISTINCT cpu" - " FROM context_switches" - " WHERE machine_id = " + str(self.machine_id)) - while query.next(): - cpus.append(int(query.value(0))) - return sorted(cpus) - -# Switch graph data graphics item displays the graphed data - -class SwitchGraphDataGraphicsItem(QGraphicsItem): - - def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None): - super(SwitchGraphDataGraphicsItem, self).__init__(parent) - - self.data = data - self.graph_width = graph_width - self.graph_height = graph_height - self.attrs = attrs - self.event_handler = event_handler - self.setAcceptHoverEvents(True) - - def boundingRect(self): - return QRectF(0, 0, self.graph_width, self.graph_height) - - def PaintPoint(self, painter, last, x): - if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo): - if last.x < self.attrs.subrange.x.lo: - x0 = self.attrs.subrange.x.lo - else: - x0 = last.x - if x > self.attrs.subrange.x.hi: - x1 = self.attrs.subrange.x.hi - else: - x1 = x - 1 - x0 = self.attrs.XToPixel(x0) - x1 = self.attrs.XToPixel(x1) - - y0 = self.attrs.YToPixel(last.y) - - colour = self.attrs.region_attributes[last.hregion.key].colour - - width = x1 - x0 + 1 - if width < 2: - painter.setPen(colour) - painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height) - else: - painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour) - - def paint(self, painter, option, widget): - last = None - for point in self.data.points: - self.PaintPoint(painter, last, point.x) - if point.x > self.attrs.subrange.x.hi: - break; - last = point - self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1) - - def BinarySearchPoint(self, target): - lower_pos = 0 - higher_pos = len(self.data.points) - while True: - pos = int((lower_pos + higher_pos) / 2) - val = self.data.points[pos].x - if target >= val: - lower_pos = pos - else: - higher_pos = pos - if higher_pos <= lower_pos + 1: - return lower_pos - - def XPixelToData(self, x): - x = self.attrs.PixelToX(x) - if x < self.data.points[0].x: - x = 0 - pos = 0 - low = True - else: - pos = self.BinarySearchPoint(x) - low = False - return (low, pos, self.data.XToData(x)) - - def EventToData(self, event): - no_data = (None,) * 4 - if len(self.data.points) < 1: - return no_data - x = event.pos().x() - if x < 0: - return no_data - low0, pos0, time_from = self.XPixelToData(x) - low1, pos1, time_to = self.XPixelToData(x + 1) - hregions = set() - hregion_times = [] - if not low1: - for i in xrange(pos0, pos1 + 1): - hregion = self.data.points[i].hregion - hregions.add(hregion) - if i == pos0: - time = time_from - else: - time = self.data.XToData(self.data.points[i].x) - hregion_times.append((hregion, time)) - return (time_from, time_to, hregions, hregion_times) - - def hoverMoveEvent(self, event): - time_from, time_to, hregions, hregion_times = self.EventToData(event) - if time_from is not None: - self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions) - - def hoverLeaveEvent(self, event): - self.event_handler.NoPointEvent() - - def mousePressEvent(self, event): - if event.button() != Qt.RightButton: - super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event) - return - time_from, time_to, hregions, hregion_times = self.EventToData(event) - if hregion_times: - self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos()) - -# X-axis graphics item - -class XAxisGraphicsItem(QGraphicsItem): - - def __init__(self, width, parent=None): - super(XAxisGraphicsItem, self).__init__(parent) - - self.width = width - self.max_mark_sz = 4 - self.height = self.max_mark_sz + 1 - - def boundingRect(self): - return QRectF(0, 0, self.width, self.height) - - def Step(self): - attrs = self.parentItem().attrs - subrange = attrs.subrange.x - t = subrange.hi - subrange.lo - s = (3.0 * t) / self.width - n = 1.0 - while s > n: - n = n * 10.0 - return n - - def PaintMarks(self, painter, at_y, lo, hi, step, i): - attrs = self.parentItem().attrs - x = lo - while x <= hi: - xp = attrs.XToPixel(x) - if i % 10: - if i % 5: - sz = 1 - else: - sz = 2 - else: - sz = self.max_mark_sz - i = 0 - painter.drawLine(xp, at_y, xp, at_y + sz) - x += step - i += 1 - - def paint(self, painter, option, widget): - # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1 - painter.drawLine(0, 0, self.width - 1, 0) - n = self.Step() - attrs = self.parentItem().attrs - subrange = attrs.subrange.x - if subrange.lo: - x_offset = n - (subrange.lo % n) - else: - x_offset = 0.0 - x = subrange.lo + x_offset - i = (x / n) % 10 - self.PaintMarks(painter, 0, x, subrange.hi, n, i) - - def ScaleDimensions(self): - n = self.Step() - attrs = self.parentItem().attrs - lo = attrs.subrange.x.lo - hi = (n * 10.0) + lo - width = attrs.XToPixel(hi) - if width > 500: - width = 0 - return (n, lo, hi, width) - - def PaintScale(self, painter, at_x, at_y): - n, lo, hi, width = self.ScaleDimensions() - if not width: - return - painter.drawLine(at_x, at_y, at_x + width, at_y) - self.PaintMarks(painter, at_y, lo, hi, n, 0) - - def ScaleWidth(self): - n, lo, hi, width = self.ScaleDimensions() - return width - - def ScaleHeight(self): - return self.height - - def ScaleUnit(self): - return self.Step() * 10 - -# Scale graphics item base class - -class ScaleGraphicsItem(QGraphicsItem): - - def __init__(self, axis, parent=None): - super(ScaleGraphicsItem, self).__init__(parent) - self.axis = axis - - def boundingRect(self): - scale_width = self.axis.ScaleWidth() - if not scale_width: - return QRectF() - return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight()) - - def paint(self, painter, option, widget): - scale_width = self.axis.ScaleWidth() - if not scale_width: - return - self.axis.PaintScale(painter, 0, 5) - x = scale_width + 4 - painter.drawText(QPointF(x, 10), self.Text()) - - def Unit(self): - return self.axis.ScaleUnit() - - def Text(self): - return "" - -# Switch graph scale graphics item - -class SwitchScaleGraphicsItem(ScaleGraphicsItem): - - def __init__(self, axis, parent=None): - super(SwitchScaleGraphicsItem, self).__init__(axis, parent) - - def Text(self): - unit = self.Unit() - if unit >= 1000000000: - unit = int(unit / 1000000000) - us = "s" - elif unit >= 1000000: - unit = int(unit / 1000000) - us = "ms" - elif unit >= 1000: - unit = int(unit / 1000) - us = "us" - else: - unit = int(unit) - us = "ns" - return " = " + str(unit) + " " + us - -# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data - -class SwitchGraphGraphicsItem(QGraphicsItem): - - def __init__(self, collection, data, attrs, event_handler, first, parent=None): - super(SwitchGraphGraphicsItem, self).__init__(parent) - self.collection = collection - self.data = data - self.attrs = attrs - self.event_handler = event_handler - - margin = 20 - title_width = 50 - - self.title_graphics = QGraphicsSimpleTextItem(data.title, self) - - self.title_graphics.setPos(margin, margin) - graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1 - graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1 - - self.graph_origin_x = margin + title_width + margin - self.graph_origin_y = graph_height + margin - - x_axis_size = 1 - y_axis_size = 1 - self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self) - - self.x_axis = XAxisGraphicsItem(graph_width, self) - self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1) - - if first: - self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self) - self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10) - - self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height) - - self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self) - self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1) - - self.width = self.graph_origin_x + graph_width + margin - self.height = self.graph_origin_y + margin - - self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self) - self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height) - - if parent and 'EnableRubberBand' in dir(parent): - parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self) - - def boundingRect(self): - return QRectF(0, 0, self.width, self.height) - - def paint(self, painter, option, widget): - pass - - def RBXToPixel(self, x): - return self.attrs.PixelToX(x - self.graph_origin_x) - - def RBXRangeToPixel(self, x0, x1): - return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1)) - - def RBPixelToTime(self, x): - if x < self.data.points[0].x: - return self.data.XToData(0) - return self.data.XToData(x) - - def RBEventTimes(self, x0, x1): - x0, x1 = self.RBXRangeToPixel(x0, x1) - time_from = self.RBPixelToTime(x0) - time_to = self.RBPixelToTime(x1) - return (time_from, time_to) - - def RBEvent(self, x0, x1): - time_from, time_to = self.RBEventTimes(x0, x1) - self.event_handler.RangeEvent(time_from, time_to) - - def RBMoveEvent(self, x0, x1): - if x1 < x0: - x0, x1 = x1, x0 - self.RBEvent(x0, x1) - - def RBReleaseEvent(self, x0, x1, selection_state): - if x1 < x0: - x0, x1 = x1, x0 - x0, x1 = self.RBXRangeToPixel(x0, x1) - self.event_handler.SelectEvent(x0, x1, selection_state) - -# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range) - -class VerticalBracketGraphicsItem(QGraphicsItem): - - def __init__(self, parent=None): - super(VerticalBracketGraphicsItem, self).__init__(parent) - - self.width = 0 - self.height = 0 - self.hide() - - def SetSize(self, width, height): - self.width = width + 1 - self.height = height + 1 - - def boundingRect(self): - return QRectF(0, 0, self.width, self.height) - - def paint(self, painter, option, widget): - colour = QColor(255, 255, 0, 32) - painter.fillRect(0, 0, self.width, self.height, colour) - x1 = self.width - 1 - y1 = self.height - 1 - painter.drawLine(0, 0, x1, 0) - painter.drawLine(0, 0, 0, 3) - painter.drawLine(x1, 0, x1, 3) - painter.drawLine(0, y1, x1, y1) - painter.drawLine(0, y1, 0, y1 - 3) - painter.drawLine(x1, y1, x1, y1 - 3) - -# Graphics item to contain graphs arranged vertically - -class VertcalGraphSetGraphicsItem(QGraphicsItem): - - def __init__(self, collection, attrs, event_handler, child_class, parent=None): - super(VertcalGraphSetGraphicsItem, self).__init__(parent) - - self.collection = collection - - self.top = 10 - - self.width = 0 - self.height = self.top - - self.rubber_band = None - self.rb_enabled = False - - first = True - for data in collection.data: - child = child_class(collection, data, attrs, event_handler, first, self) - child.setPos(0, self.height + 1) - rect = child.boundingRect() - if rect.right() > self.width: - self.width = rect.right() - self.height = self.height + rect.bottom() + 1 - first = False - - self.bracket = VerticalBracketGraphicsItem(self) - - def EnableRubberBand(self, xlo, xhi, rb_event_handler): - if self.rb_enabled: - return - self.rb_enabled = True - self.rb_in_view = False - self.setAcceptedMouseButtons(Qt.LeftButton) - self.rb_xlo = xlo - self.rb_xhi = xhi - self.rb_event_handler = rb_event_handler - self.mousePressEvent = self.MousePressEvent - self.mouseMoveEvent = self.MouseMoveEvent - self.mouseReleaseEvent = self.MouseReleaseEvent - - def boundingRect(self): - return QRectF(0, 0, self.width, self.height) - - def paint(self, painter, option, widget): - pass - - def RubberBandParent(self): - scene = self.scene() - view = scene.views()[0] - viewport = view.viewport() - return viewport - - def RubberBandSetGeometry(self, rect): - scene_rectf = self.mapRectToScene(QRectF(rect)) - scene = self.scene() - view = scene.views()[0] - poly = view.mapFromScene(scene_rectf) - self.rubber_band.setGeometry(poly.boundingRect()) - - def SetSelection(self, selection_state): - if self.rubber_band: - if selection_state: - self.RubberBandSetGeometry(selection_state) - self.rubber_band.show() - else: - self.rubber_band.hide() - - def SetBracket(self, rect): - if rect: - x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height() - self.bracket.setPos(x, y) - self.bracket.SetSize(width, height) - self.bracket.show() - else: - self.bracket.hide() - - def RubberBandX(self, event): - x = event.pos().toPoint().x() - if x < self.rb_xlo: - x = self.rb_xlo - elif x > self.rb_xhi: - x = self.rb_xhi - else: - self.rb_in_view = True - return x - - def RubberBandRect(self, x): - if self.rb_origin.x() <= x: - width = x - self.rb_origin.x() - rect = QRect(self.rb_origin, QSize(width, self.height)) - else: - width = self.rb_origin.x() - x - top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y()) - rect = QRect(top_left, QSize(width, self.height)) - return rect - - def MousePressEvent(self, event): - self.rb_in_view = False - x = self.RubberBandX(event) - self.rb_origin = QPoint(x, self.top) - if self.rubber_band is None: - self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent()) - self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height))) - if self.rb_in_view: - self.rubber_band.show() - self.rb_event_handler.RBMoveEvent(x, x) - else: - self.rubber_band.hide() - - def MouseMoveEvent(self, event): - x = self.RubberBandX(event) - rect = self.RubberBandRect(x) - self.RubberBandSetGeometry(rect) - if self.rb_in_view: - self.rubber_band.show() - self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x) - - def MouseReleaseEvent(self, event): - x = self.RubberBandX(event) - if self.rb_in_view: - selection_state = self.RubberBandRect(x) - else: - selection_state = None - self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state) - -# Switch graph legend data model - -class SwitchGraphLegendModel(QAbstractTableModel): - - def __init__(self, collection, region_attributes, parent=None): - super(SwitchGraphLegendModel, self).__init__(parent) - - self.region_attributes = region_attributes - - self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal) - self.child_count = len(self.child_items) - - self.highlight_set = set() - - self.column_headers = ("pid", "tid", "comm") - - def rowCount(self, parent): - return self.child_count - - def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: - return None - if orientation != Qt.Horizontal: - return None - return self.columnHeader(section) - - def index(self, row, column, parent): - return self.createIndex(row, column, self.child_items[row]) - - def columnCount(self, parent=None): - return len(self.column_headers) - - def columnHeader(self, column): - return self.column_headers[column] - - def data(self, index, role): - if role == Qt.BackgroundRole: - child = self.child_items[index.row()] - if child in self.highlight_set: - return self.region_attributes[child.key].colour - return None - if role == Qt.ForegroundRole: - child = self.child_items[index.row()] - if child in self.highlight_set: - return QColor(255, 255, 255) - return self.region_attributes[child.key].colour - if role != Qt.DisplayRole: - return None - hregion = self.child_items[index.row()] - col = index.column() - if col == 0: - return hregion.pid - if col == 1: - return hregion.tid - if col == 2: - return hregion.comm - return None - - def SetHighlight(self, row, set_highlight): - child = self.child_items[row] - top_left = self.createIndex(row, 0, child) - bottom_right = self.createIndex(row, len(self.column_headers) - 1, child) - self.dataChanged.emit(top_left, bottom_right) - - def Highlight(self, highlight_set): - for row in xrange(self.child_count): - child = self.child_items[row] - if child in self.highlight_set: - if child not in highlight_set: - self.SetHighlight(row, False) - elif child in highlight_set: - self.SetHighlight(row, True) - self.highlight_set = highlight_set - -# Switch graph legend is a table - -class SwitchGraphLegend(QWidget): - - def __init__(self, collection, region_attributes, parent=None): - super(SwitchGraphLegend, self).__init__(parent) - - self.data_model = SwitchGraphLegendModel(collection, region_attributes) - - self.model = QSortFilterProxyModel() - self.model.setSourceModel(self.data_model) - - self.view = QTableView() - self.view.setModel(self.model) - self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.view.verticalHeader().setVisible(False) - self.view.sortByColumn(-1, Qt.AscendingOrder) - self.view.setSortingEnabled(True) - self.view.resizeColumnsToContents() - self.view.resizeRowsToContents() - - self.vbox = VBoxLayout(self.view) - self.setLayout(self.vbox) - - sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2 - sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width() - self.saved_size = sz1 - - def resizeEvent(self, event): - self.saved_size = self.size().width() - super(SwitchGraphLegend, self).resizeEvent(event) - - def Highlight(self, highlight_set): - self.data_model.Highlight(highlight_set) - self.update() - - def changeEvent(self, event): - if event.type() == QEvent.FontChange: - self.view.resizeRowsToContents() - self.view.resizeColumnsToContents() - # Need to resize rows again after column resize - self.view.resizeRowsToContents() - super(SwitchGraphLegend, self).changeEvent(event) - -# Random colour generation - -def RGBColourTooLight(r, g, b): - if g > 230: - return True - if g <= 160: - return False - if r <= 180 and g <= 180: - return False - if r < 60: - return False - return True - -def GenerateColours(x): - cs = [0] - for i in xrange(1, x): - cs.append(int((255.0 / i) + 0.5)) - colours = [] - for r in cs: - for g in cs: - for b in cs: - # Exclude black and colours that look too light against a white background - if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b): - continue - colours.append(QColor(r, g, b)) - return colours - -def GenerateNColours(n): - for x in xrange(2, n + 2): - colours = GenerateColours(x) - if len(colours) >= n: - return colours - return [] - -def GenerateNRandomColours(n, seed): - colours = GenerateNColours(n) - random.seed(seed) - random.shuffle(colours) - return colours - -# Graph attributes, in particular the scale and subrange that change when zooming - -class GraphAttributes(): - - def __init__(self, scale, subrange, region_attributes, dp): - self.scale = scale - self.subrange = subrange - self.region_attributes = region_attributes - # Rounding avoids errors due to finite floating point precision - self.dp = dp # data decimal places - self.Update() - - def XToPixel(self, x): - return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x)) - - def YToPixel(self, y): - return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y)) - - def PixelToXRounded(self, px): - return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo - - def PixelToYRounded(self, py): - return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo - - def PixelToX(self, px): - x = self.PixelToXRounded(px) - if self.pdp.x == 0: - rt = self.XToPixel(x) - if rt > px: - return x - 1 - return x - - def PixelToY(self, py): - y = self.PixelToYRounded(py) - if self.pdp.y == 0: - rt = self.YToPixel(y) - if rt > py: - return y - 1 - return y - - def ToPDP(self, dp, scale): - # Calculate pixel decimal places: - # (10 ** dp) is the minimum delta in the data - # scale it to get the minimum delta in pixels - # log10 gives the number of decimals places negatively - # subtrace 1 to divide by 10 - # round to the lower negative number - # change the sign to get the number of decimals positively - x = math.log10((10 ** dp) * scale) - if x < 0: - x -= 1 - x = -int(math.floor(x) - 0.1) - else: - x = 0 - return x - - def Update(self): - x = self.ToPDP(self.dp.x, self.scale.x) - y = self.ToPDP(self.dp.y, self.scale.y) - self.pdp = XY(x, y) # pixel decimal places - -# Switch graph splitter which divides the CPU graphs from the legend - -class SwitchGraphSplitter(QSplitter): - - def __init__(self, parent=None): - super(SwitchGraphSplitter, self).__init__(parent) - - self.first_time = False - - def resizeEvent(self, ev): - if self.first_time: - self.first_time = False - sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2 - sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width() - sz0 = self.size().width() - self.handleWidth() - sz1 - self.setSizes([sz0, sz1]) - elif not(self.widget(1).saved_size is None): - sz1 = self.widget(1).saved_size - sz0 = self.size().width() - self.handleWidth() - sz1 - self.setSizes([sz0, sz1]) - super(SwitchGraphSplitter, self).resizeEvent(ev) - -# Graph widget base class - -class GraphWidget(QWidget): - - graph_title_changed = Signal(object) - - def __init__(self, parent=None): - super(GraphWidget, self).__init__(parent) - - def GraphTitleChanged(self, title): - self.graph_title_changed.emit(title) - - def Title(self): - return "" - -# Display time in s, ms, us or ns - -def ToTimeStr(val): - val = Decimal(val) - if val >= 1000000000: - return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001"))) - if val >= 1000000: - return "{} ms".format((val / 1000000).quantize(Decimal("0.000001"))) - if val >= 1000: - return "{} us".format((val / 1000).quantize(Decimal("0.001"))) - return "{} ns".format(val.quantize(Decimal("1"))) - -# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons - -class SwitchGraphWidget(GraphWidget): - - def __init__(self, glb, collection, parent=None): - super(SwitchGraphWidget, self).__init__(parent) - - self.glb = glb - self.collection = collection - - self.back_state = [] - self.forward_state = [] - self.selection_state = (None, None) - self.fwd_rect = None - self.start_time = self.glb.StartTime(collection.machine_id) - - i = 0 - hregions = collection.hregions.values() - colours = GenerateNRandomColours(len(hregions), 1013) - region_attributes = {} - for hregion in hregions: - if hregion.pid == 0 and hregion.tid == 0: - region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0)) - else: - region_attributes[hregion.key] = GraphRegionAttribute(colours[i]) - i = i + 1 - - # Default to entire range - xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0) - ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0) - subrange = XY(xsubrange, ysubrange) - - scale = self.GetScaleForRange(subrange) - - self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp) - - self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem) - - self.scene = QGraphicsScene() - self.scene.addItem(self.item) - - self.view = QGraphicsView(self.scene) - self.view.centerOn(0, 0) - self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop) - - self.legend = SwitchGraphLegend(collection, region_attributes) - - self.splitter = SwitchGraphSplitter() - self.splitter.addWidget(self.view) - self.splitter.addWidget(self.legend) - - self.point_label = QLabel("") - self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) - - self.back_button = QToolButton() - self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft)) - self.back_button.setDisabled(True) - self.back_button.released.connect(lambda: self.Back()) - - self.forward_button = QToolButton() - self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight)) - self.forward_button.setDisabled(True) - self.forward_button.released.connect(lambda: self.Forward()) - - self.zoom_button = QToolButton() - self.zoom_button.setText("Zoom") - self.zoom_button.setDisabled(True) - self.zoom_button.released.connect(lambda: self.Zoom()) - - self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label) - - self.vbox = VBoxLayout(self.splitter, self.hbox) - - self.setLayout(self.vbox) - - def GetScaleForRangeX(self, xsubrange): - # Default graph 1000 pixels wide - dflt = 1000.0 - r = xsubrange.hi - xsubrange.lo - return dflt / r - - def GetScaleForRangeY(self, ysubrange): - # Default graph 50 pixels high - dflt = 50.0 - r = ysubrange.hi - ysubrange.lo - return dflt / r - - def GetScaleForRange(self, subrange): - # Default graph 1000 pixels wide, 50 pixels high - xscale = self.GetScaleForRangeX(subrange.x) - yscale = self.GetScaleForRangeY(subrange.y) - return XY(xscale, yscale) - - def PointEvent(self, cpu, time_from, time_to, hregions): - text = "CPU: " + str(cpu) - time_from = time_from.quantize(Decimal(1)) - rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id) - text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")" - self.point_label.setText(text) - self.legend.Highlight(hregions) - - def RightClickEvent(self, cpu, hregion_times, pos): - if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"): - return - menu = QMenu(self.view) - for hregion, time in hregion_times: - thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time) - menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time) - menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view)) - menu.exec_(pos) - - def RightClickSelect(self, args): - CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args) - - def NoPointEvent(self): - self.point_label.setText("") - self.legend.Highlight({}) - - def RangeEvent(self, time_from, time_to): - time_from = time_from.quantize(Decimal(1)) - time_to = time_to.quantize(Decimal(1)) - if time_to <= time_from: - self.point_label.setText("") - return - rel_time_from = time_from - self.start_time - rel_time_to = time_to - self.start_time - text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")" - text = text + " duration: " + ToTimeStr(time_to - time_from) - self.point_label.setText(text) - - def BackState(self): - return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect) - - def PushBackState(self): - state = copy.deepcopy(self.BackState()) - self.back_state.append(state) - self.back_button.setEnabled(True) - - def PopBackState(self): - self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop() - self.attrs.Update() - if not self.back_state: - self.back_button.setDisabled(True) - - def PushForwardState(self): - state = copy.deepcopy(self.BackState()) - self.forward_state.append(state) - self.forward_button.setEnabled(True) - - def PopForwardState(self): - self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop() - self.attrs.Update() - if not self.forward_state: - self.forward_button.setDisabled(True) - - def Title(self): - time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo) - time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi) - rel_time_from = time_from - self.start_time - rel_time_to = time_to - self.start_time - title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to) - title = title + " (" + ToTimeStr(time_to - time_from) + ")" - return title - - def Update(self): - selected_subrange, selection_state = self.selection_state - self.item.SetSelection(selection_state) - self.item.SetBracket(self.fwd_rect) - self.zoom_button.setDisabled(selected_subrange is None) - self.GraphTitleChanged(self.Title()) - self.item.update(self.item.boundingRect()) - - def Back(self): - if not self.back_state: - return - self.PushForwardState() - self.PopBackState() - self.Update() - - def Forward(self): - if not self.forward_state: - return - self.PushBackState() - self.PopForwardState() - self.Update() - - def SelectEvent(self, x0, x1, selection_state): - if selection_state is None: - selected_subrange = None - else: - if x1 - x0 < 1.0: - x1 += 1.0 - selected_subrange = Subrange(x0, x1) - self.selection_state = (selected_subrange, selection_state) - self.zoom_button.setDisabled(selected_subrange is None) - - def Zoom(self): - selected_subrange, selection_state = self.selection_state - if selected_subrange is None: - return - self.fwd_rect = selection_state - self.item.SetSelection(None) - self.PushBackState() - self.attrs.subrange.x = selected_subrange - self.forward_state = [] - self.forward_button.setDisabled(True) - self.selection_state = (None, None) - self.fwd_rect = None - self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x) - self.attrs.Update() - self.Update() - -# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting - -class SlowInitClass(): - - def __init__(self, glb, title, init_fn): - self.init_fn = init_fn - self.done = False - self.result = None - - self.msg_box = QMessageBox(glb.mainwindow) - self.msg_box.setText("Initializing " + title + ". Please wait.") - self.msg_box.setWindowTitle("Initializing " + title) - self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation)) - - self.init_thread = Thread(self.ThreadFn, glb) - self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection) - - self.init_thread.start() - - def Done(self): - self.msg_box.done(0) - - def ThreadFn(self, glb): - conn_name = "SlowInitClass" + str(os.getpid()) - db, dbname = glb.dbref.Open(conn_name) - self.result = self.init_fn(db) - self.done = True - return (True, 0) - - def Result(self): - while not self.done: - self.msg_box.exec_() - self.init_thread.wait() - return self.result - -def SlowInit(glb, title, init_fn): - init = SlowInitClass(glb, title, init_fn) - return init.Result() - -# Time chart by CPU window - -class TimeChartByCPUWindow(QMdiSubWindow): - - def __init__(self, glb, parent=None): - super(TimeChartByCPUWindow, self).__init__(parent) - - self.glb = glb - self.machine_id = glb.HostMachineId() - self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id) - - collection = LookupModel(self.collection_name) - if collection is None: - collection = SlowInit(glb, "Time Chart", self.Init) - - self.widget = SwitchGraphWidget(glb, collection, self) - self.view = self.widget - - self.base_title = "Time Chart by CPU" - self.setWindowTitle(self.base_title + self.widget.Title()) - self.widget.graph_title_changed.connect(self.GraphTitleChanged) - - self.setWidget(self.widget) - - AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle()) - - def Init(self, db): - return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id)) - - def GraphTitleChanged(self, title): - self.setWindowTitle(self.base_title + " : " + title) - -# Child data item finder - -class ChildDataItemFinder(): - - def __init__(self, root): - self.root = root - self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5 - self.rows = [] - self.pos = 0 - - def FindSelect(self): - self.rows = [] - if self.pattern: - pattern = re.compile(self.value) - for child in self.root.child_items: - for column_data in child.data: - if re.search(pattern, str(column_data)) is not None: - self.rows.append(child.row) - break - else: - for child in self.root.child_items: - for column_data in child.data: - if self.value in str(column_data): - self.rows.append(child.row) - break - - def FindValue(self): - self.pos = 0 - if self.last_value != self.value or self.pattern != self.last_pattern: - self.FindSelect() - if not len(self.rows): - return -1 - return self.rows[self.pos] - - def FindThread(self): - if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern: - row = self.FindValue() - elif len(self.rows): - if self.direction > 0: - self.pos += 1 - if self.pos >= len(self.rows): - self.pos = 0 - else: - self.pos -= 1 - if self.pos < 0: - self.pos = len(self.rows) - 1 - row = self.rows[self.pos] - else: - row = -1 - return (True, row) - - def Find(self, value, direction, pattern, context, callback): - self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern) - # Use a thread so the UI is not blocked - thread = Thread(self.FindThread) - thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection) - thread.start() - - def FindDone(self, thread, callback, row): - callback(row) - -# Number of database records to fetch in one go - -glb_chunk_sz = 10000 - -# Background process for SQL data fetcher - -class SQLFetcherProcess(): - - def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep): - # Need a unique connection name - conn_name = "SQLFetcher" + str(os.getpid()) - self.db, dbname = dbref.Open(conn_name) - self.sql = sql - self.buffer = buffer - self.head = head - self.tail = tail - self.fetch_count = fetch_count - self.fetching_done = fetching_done - self.process_target = process_target - self.wait_event = wait_event - self.fetched_event = fetched_event - self.prep = prep - self.query = QSqlQuery(self.db) - self.query_limit = 0 if "$$last_id$$" in sql else 2 - self.last_id = -1 - self.fetched = 0 - self.more = True - self.local_head = self.head.value - self.local_tail = self.tail.value - - def Select(self): - if self.query_limit: - if self.query_limit == 1: - return - self.query_limit -= 1 - stmt = self.sql.replace("$$last_id$$", str(self.last_id)) - QueryExec(self.query, stmt) - - def Next(self): - if not self.query.next(): - self.Select() - if not self.query.next(): - return None - self.last_id = self.query.value(0) - return self.prep(self.query) - - def WaitForTarget(self): - while True: - self.wait_event.clear() - target = self.process_target.value - if target > self.fetched or target < 0: - break - self.wait_event.wait() - return target - - def HasSpace(self, sz): - if self.local_tail <= self.local_head: - space = len(self.buffer) - self.local_head - if space > sz: - return True - if space >= glb_nsz: - # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer - nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL) - self.buffer[self.local_head : self.local_head + len(nd)] = nd - self.local_head = 0 - if self.local_tail - self.local_head > sz: - return True - return False - - def WaitForSpace(self, sz): - if self.HasSpace(sz): - return - while True: - self.wait_event.clear() - self.local_tail = self.tail.value - if self.HasSpace(sz): - return - self.wait_event.wait() - - def AddToBuffer(self, obj): - d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) - n = len(d) - nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL) - sz = n + glb_nsz - self.WaitForSpace(sz) - pos = self.local_head - self.buffer[pos : pos + len(nd)] = nd - self.buffer[pos + glb_nsz : pos + sz] = d - self.local_head += sz - - def FetchBatch(self, batch_size): - fetched = 0 - while batch_size > fetched: - obj = self.Next() - if obj is None: - self.more = False - break - self.AddToBuffer(obj) - fetched += 1 - if fetched: - self.fetched += fetched - with self.fetch_count.get_lock(): - self.fetch_count.value += fetched - self.head.value = self.local_head - self.fetched_event.set() - - def Run(self): - while self.more: - target = self.WaitForTarget() - if target < 0: - break - batch_size = min(glb_chunk_sz, target - self.fetched) - self.FetchBatch(batch_size) - self.fetching_done.value = True - self.fetched_event.set() - -def SQLFetcherFn(*x): - process = SQLFetcherProcess(*x) - process.Run() - -# SQL data fetcher - -class SQLFetcher(QObject): - - done = Signal(object) - - def __init__(self, glb, sql, prep, process_data, parent=None): - super(SQLFetcher, self).__init__(parent) - self.process_data = process_data - self.more = True - self.target = 0 - self.last_target = 0 - self.fetched = 0 - self.buffer_size = 16 * 1024 * 1024 - self.buffer = Array(c_char, self.buffer_size, lock=False) - self.head = Value(c_longlong) - self.tail = Value(c_longlong) - self.local_tail = 0 - self.fetch_count = Value(c_longlong) - self.fetching_done = Value(c_bool) - self.last_count = 0 - self.process_target = Value(c_longlong) - self.wait_event = Event() - self.fetched_event = Event() - glb.AddInstanceToShutdownOnExit(self) - 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)) - self.process.start() - self.thread = Thread(self.Thread) - self.thread.done.connect(self.ProcessData, Qt.QueuedConnection) - self.thread.start() - - def Shutdown(self): - # Tell the thread and process to exit - self.process_target.value = -1 - self.wait_event.set() - self.more = False - self.fetching_done.value = True - self.fetched_event.set() - - def Thread(self): - if not self.more: - return True, 0 - while True: - self.fetched_event.clear() - fetch_count = self.fetch_count.value - if fetch_count != self.last_count: - break - if self.fetching_done.value: - self.more = False - return True, 0 - self.fetched_event.wait() - count = fetch_count - self.last_count - self.last_count = fetch_count - self.fetched += count - return False, count - - def Fetch(self, nr): - if not self.more: - # -1 inidcates there are no more - return -1 - result = self.fetched - extra = result + nr - self.target - if extra > 0: - self.target += extra - # process_target < 0 indicates shutting down - if self.process_target.value >= 0: - self.process_target.value = self.target - self.wait_event.set() - return result - - def RemoveFromBuffer(self): - pos = self.local_tail - if len(self.buffer) - pos < glb_nsz: - pos = 0 - n = pickle.loads(self.buffer[pos : pos + glb_nsz]) - if n == 0: - pos = 0 - n = pickle.loads(self.buffer[0 : glb_nsz]) - pos += glb_nsz - obj = pickle.loads(self.buffer[pos : pos + n]) - self.local_tail = pos + n - return obj - - def ProcessData(self, count): - for i in xrange(count): - obj = self.RemoveFromBuffer() - self.process_data(obj) - self.tail.value = self.local_tail - self.wait_event.set() - self.done.emit(count) - -# Fetch more records bar - -class FetchMoreRecordsBar(): - - def __init__(self, model, parent): - self.model = model - - self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:") - self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.fetch_count = QSpinBox() - self.fetch_count.setRange(1, 1000000) - self.fetch_count.setValue(10) - self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.fetch = QPushButton("Go!") - self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - self.fetch.released.connect(self.FetchMoreRecords) - - self.progress = QProgressBar() - self.progress.setRange(0, 100) - self.progress.hide() - - self.done_label = QLabel("All records fetched") - self.done_label.hide() - - self.spacer = QLabel("") - - self.close_button = QToolButton() - self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) - self.close_button.released.connect(self.Deactivate) - - self.hbox = QHBoxLayout() - self.hbox.setContentsMargins(0, 0, 0, 0) - - self.hbox.addWidget(self.label) - self.hbox.addWidget(self.fetch_count) - self.hbox.addWidget(self.fetch) - self.hbox.addWidget(self.spacer) - self.hbox.addWidget(self.progress) - self.hbox.addWidget(self.done_label) - self.hbox.addWidget(self.close_button) - - self.bar = QWidget() - self.bar.setLayout(self.hbox) - self.bar.show() - - self.in_progress = False - self.model.progress.connect(self.Progress) - - self.done = False - - if not model.HasMoreRecords(): - self.Done() - - def Widget(self): - return self.bar - - def Activate(self): - self.bar.show() - self.fetch.setFocus() - - def Deactivate(self): - self.bar.hide() - - def Enable(self, enable): - self.fetch.setEnabled(enable) - self.fetch_count.setEnabled(enable) - - def Busy(self): - self.Enable(False) - self.fetch.hide() - self.spacer.hide() - self.progress.show() - - def Idle(self): - self.in_progress = False - self.Enable(True) - self.progress.hide() - self.fetch.show() - self.spacer.show() - - def Target(self): - return self.fetch_count.value() * glb_chunk_sz - - def Done(self): - self.done = True - self.Idle() - self.label.hide() - self.fetch_count.hide() - self.fetch.hide() - self.spacer.hide() - self.done_label.show() - - def Progress(self, count): - if self.in_progress: - if count: - percent = ((count - self.start) * 100) / self.Target() - if percent >= 100: - self.Idle() - else: - self.progress.setValue(percent) - if not count: - # Count value of zero means no more records - self.Done() - - def FetchMoreRecords(self): - if self.done: - return - self.progress.setValue(0) - self.Busy() - self.in_progress = True - self.start = self.model.FetchMoreRecords(self.Target()) - -# Brance data model level two item - -class BranchLevelTwoItem(): - - def __init__(self, row, col, text, parent_item): - self.row = row - self.parent_item = parent_item - self.data = [""] * (col + 1) - self.data[col] = text - self.level = 2 - - def getParentItem(self): - return self.parent_item - - def getRow(self): - return self.row - - def childCount(self): - return 0 - - def hasChildren(self): - return False - - def getData(self, column): - return self.data[column] - -# Brance data model level one item - -class BranchLevelOneItem(): - - def __init__(self, glb, row, data, parent_item): - self.glb = glb - self.row = row - self.parent_item = parent_item - self.child_count = 0 - self.child_items = [] - self.data = data[1:] - self.dbid = data[0] - self.level = 1 - self.query_done = False - self.br_col = len(self.data) - 1 - - def getChildItem(self, row): - return self.child_items[row] - - def getParentItem(self): - return self.parent_item - - def getRow(self): - return self.row - - def Select(self): - self.query_done = True - - if not self.glb.have_disassembler: - return - - query = QSqlQuery(self.glb.db) - - QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip" - " FROM samples" - " INNER JOIN dsos ON samples.to_dso_id = dsos.id" - " INNER JOIN symbols ON samples.to_symbol_id = symbols.id" - " WHERE samples.id = " + str(self.dbid)) - if not query.next(): - return - cpu = query.value(0) - dso = query.value(1) - sym = query.value(2) - if dso == 0 or sym == 0: - return - off = query.value(3) - short_name = query.value(4) - long_name = query.value(5) - build_id = query.value(6) - sym_start = query.value(7) - ip = query.value(8) - - QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start" - " FROM samples" - " INNER JOIN symbols ON samples.symbol_id = symbols.id" - " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) + - " ORDER BY samples.id" - " LIMIT 1") - if not query.next(): - return - if query.value(0) != dso: - # Cannot disassemble from one dso to another - return - bsym = query.value(1) - boff = query.value(2) - bsym_start = query.value(3) - if bsym == 0: - return - tot = bsym_start + boff + 1 - sym_start - off - if tot <= 0 or tot > 16384: - return - - inst = self.glb.disassembler.Instruction() - f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id) - if not f: - return - mode = 0 if Is64Bit(f) else 1 - self.glb.disassembler.SetMode(inst, mode) - - buf_sz = tot + 16 - buf = create_string_buffer(tot + 16) - f.seek(sym_start + off) - buf.value = f.read(buf_sz) - buf_ptr = addressof(buf) - i = 0 - while tot > 0: - cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip) - if cnt: - byte_str = tohex(ip).rjust(16) - for k in xrange(cnt): - byte_str += " %02x" % ord(buf[i]) - i += 1 - while k < 15: - byte_str += " " - k += 1 - self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self)) - self.child_count += 1 - else: - return - buf_ptr += cnt - tot -= cnt - buf_sz -= cnt - ip += cnt - - def childCount(self): - if not self.query_done: - self.Select() - if not self.child_count: - return -1 - return self.child_count - - def hasChildren(self): - if not self.query_done: - return True - return self.child_count > 0 - - def getData(self, column): - return self.data[column] - -# Brance data model root item - -class BranchRootItem(): - - def __init__(self): - self.child_count = 0 - self.child_items = [] - self.level = 0 - - def getChildItem(self, row): - return self.child_items[row] - - def getParentItem(self): - return None - - def getRow(self): - return 0 - - def childCount(self): - return self.child_count - - def hasChildren(self): - return self.child_count > 0 - - def getData(self, column): - return "" - -# Calculate instructions per cycle - -def CalcIPC(cyc_cnt, insn_cnt): - if cyc_cnt and insn_cnt: - ipc = Decimal(float(insn_cnt) / cyc_cnt) - ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP)) - else: - ipc = "0" - return ipc - -# Branch data preparation - -def BranchDataPrepBr(query, data): - data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) + - " (" + dsoname(query.value(11)) + ")" + " -> " + - tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) + - " (" + dsoname(query.value(15)) + ")") - -def BranchDataPrepIPC(query, data): - insn_cnt = query.value(16) - cyc_cnt = query.value(17) - ipc = CalcIPC(cyc_cnt, insn_cnt) - data.append(insn_cnt) - data.append(cyc_cnt) - data.append(ipc) - -def BranchDataPrep(query): - data = [] - for i in xrange(0, 8): - data.append(query.value(i)) - BranchDataPrepBr(query, data) - return data - -def BranchDataPrepWA(query): - data = [] - data.append(query.value(0)) - # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string - data.append("{:>19}".format(query.value(1))) - for i in xrange(2, 8): - data.append(query.value(i)) - BranchDataPrepBr(query, data) - return data - -def BranchDataWithIPCPrep(query): - data = [] - for i in xrange(0, 8): - data.append(query.value(i)) - BranchDataPrepIPC(query, data) - BranchDataPrepBr(query, data) - return data - -def BranchDataWithIPCPrepWA(query): - data = [] - data.append(query.value(0)) - # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string - data.append("{:>19}".format(query.value(1))) - for i in xrange(2, 8): - data.append(query.value(i)) - BranchDataPrepIPC(query, data) - BranchDataPrepBr(query, data) - return data - -# Branch data model - -class BranchModel(TreeModel): - - progress = Signal(object) - - def __init__(self, glb, event_id, where_clause, parent=None): - super(BranchModel, self).__init__(glb, None, parent) - self.event_id = event_id - self.more = True - self.populated = 0 - self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count") - if self.have_ipc: - select_ipc = ", insn_count, cyc_count" - prep_fn = BranchDataWithIPCPrep - prep_wa_fn = BranchDataWithIPCPrepWA - else: - select_ipc = "" - prep_fn = BranchDataPrep - prep_wa_fn = BranchDataPrepWA - sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name," - " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END," - " ip, symbols.name, sym_offset, dsos.short_name," - " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name" - + select_ipc + - " FROM samples" - " INNER JOIN comms ON comm_id = comms.id" - " INNER JOIN threads ON thread_id = threads.id" - " INNER JOIN branch_types ON branch_type = branch_types.id" - " INNER JOIN symbols ON symbol_id = symbols.id" - " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id" - " INNER JOIN dsos ON samples.dso_id = dsos.id" - " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id" - " WHERE samples.id > $$last_id$$" + where_clause + - " AND evsel_id = " + str(self.event_id) + - " ORDER BY samples.id" - " LIMIT " + str(glb_chunk_sz)) - if pyside_version_1 and sys.version_info[0] == 3: - prep = prep_fn - else: - prep = prep_wa_fn - self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample) - self.fetcher.done.connect(self.Update) - self.fetcher.Fetch(glb_chunk_sz) - - def GetRoot(self): - return BranchRootItem() - - def columnCount(self, parent=None): - if self.have_ipc: - return 11 - else: - return 8 - - def columnHeader(self, column): - if self.have_ipc: - return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column] - else: - return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column] - - def columnFont(self, column): - if self.have_ipc: - br_col = 10 - else: - br_col = 7 - if column != br_col: - return None - return QFont("Monospace") - - def DisplayData(self, item, index): - if item.level == 1: - self.FetchIfNeeded(item.row) - return item.getData(index.column()) - - def AddSample(self, data): - child = BranchLevelOneItem(self.glb, self.populated, data, self.root) - self.root.child_items.append(child) - self.populated += 1 - - def Update(self, fetched): - if not fetched: - self.more = False - self.progress.emit(0) - child_count = self.root.child_count - count = self.populated - child_count - if count > 0: - parent = QModelIndex() - self.beginInsertRows(parent, child_count, child_count + count - 1) - self.insertRows(child_count, count, parent) - self.root.child_count += count - self.endInsertRows() - self.progress.emit(self.root.child_count) - - def FetchMoreRecords(self, count): - current = self.root.child_count - if self.more: - self.fetcher.Fetch(count) - else: - self.progress.emit(0) - return current - - def HasMoreRecords(self): - return self.more - -# Report Variables - -class ReportVars(): - - def __init__(self, name = "", where_clause = "", limit = ""): - self.name = name - self.where_clause = where_clause - self.limit = limit - - def UniqueId(self): - return str(self.where_clause + ";" + self.limit) - -# Branch window - -class BranchWindow(QMdiSubWindow): - - def __init__(self, glb, event_id, report_vars, parent=None): - super(BranchWindow, self).__init__(parent) - - model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId() - - self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause)) - - self.view = QTreeView() - self.view.setUniformRowHeights(True) - self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) - self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard - self.view.setModel(self.model) - - self.ResizeColumnsToContents() - - self.context_menu = TreeContextMenu(self.view) - - self.find_bar = FindBar(self, self, True) - - self.finder = ChildDataItemFinder(self.model.root) - - self.fetch_bar = FetchMoreRecordsBar(self.model, self) - - self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events") - - def ResizeColumnToContents(self, column, n): - # Using the view's resizeColumnToContents() here is extrememly slow - # so implement a crude alternative - mm = "MM" if column else "MMMM" - font = self.view.font() - metrics = QFontMetrics(font) - max = 0 - for row in xrange(n): - val = self.model.root.child_items[row].data[column] - len = metrics.width(str(val) + mm) - max = len if len > max else max - val = self.model.columnHeader(column) - len = metrics.width(str(val) + mm) - max = len if len > max else max - self.view.setColumnWidth(column, max) - - def ResizeColumnsToContents(self): - n = min(self.model.root.child_count, 100) - if n < 1: - # No data yet, so connect a signal to notify when there is - self.model.rowsInserted.connect(self.UpdateColumnWidths) - return - columns = self.model.columnCount() - for i in xrange(columns): - self.ResizeColumnToContents(i, n) - - def UpdateColumnWidths(self, *x): - # This only needs to be done once, so disconnect the signal now - self.model.rowsInserted.disconnect(self.UpdateColumnWidths) - self.ResizeColumnsToContents() - - def Find(self, value, direction, pattern, context): - self.view.setFocus() - self.find_bar.Busy() - self.finder.Find(value, direction, pattern, context, self.FindDone) - - def FindDone(self, row): - self.find_bar.Idle() - if row >= 0: - self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) - else: - self.find_bar.NotFound() - -# Line edit data item - -class LineEditDataItem(object): - - def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): - self.glb = glb - self.label = label - self.placeholder_text = placeholder_text - self.parent = parent - self.id = id - - self.value = default - - self.widget = QLineEdit(default) - self.widget.editingFinished.connect(self.Validate) - self.widget.textChanged.connect(self.Invalidate) - self.red = False - self.error = "" - self.validated = True - - if placeholder_text: - self.widget.setPlaceholderText(placeholder_text) - - def TurnTextRed(self): - if not self.red: - palette = QPalette() - palette.setColor(QPalette.Text,Qt.red) - self.widget.setPalette(palette) - self.red = True - - def TurnTextNormal(self): - if self.red: - palette = QPalette() - self.widget.setPalette(palette) - self.red = False - - def InvalidValue(self, value): - self.value = "" - self.TurnTextRed() - self.error = self.label + " invalid value '" + value + "'" - self.parent.ShowMessage(self.error) - - def Invalidate(self): - self.validated = False - - def DoValidate(self, input_string): - self.value = input_string.strip() - - def Validate(self): - self.validated = True - self.error = "" - self.TurnTextNormal() - self.parent.ClearMessage() - input_string = self.widget.text() - if not len(input_string.strip()): - self.value = "" - return - self.DoValidate(input_string) - - def IsValid(self): - if not self.validated: - self.Validate() - if len(self.error): - self.parent.ShowMessage(self.error) - return False - return True - - def IsNumber(self, value): - try: - x = int(value) - except: - x = 0 - return str(x) == value - -# Non-negative integer ranges dialog data item - -class NonNegativeIntegerRangesDataItem(LineEditDataItem): - - def __init__(self, glb, label, placeholder_text, column_name, parent): - super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent) - - self.column_name = column_name - - def DoValidate(self, input_string): - singles = [] - ranges = [] - for value in [x.strip() for x in input_string.split(",")]: - if "-" in value: - vrange = value.split("-") - if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): - return self.InvalidValue(value) - ranges.append(vrange) - else: - if not self.IsNumber(value): - return self.InvalidValue(value) - singles.append(value) - ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] - if len(singles): - ranges.append(self.column_name + " IN (" + ",".join(singles) + ")") - self.value = " OR ".join(ranges) - -# Positive integer dialog data item - -class PositiveIntegerDataItem(LineEditDataItem): - - def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): - super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default) - - def DoValidate(self, input_string): - if not self.IsNumber(input_string.strip()): - return self.InvalidValue(input_string) - value = int(input_string.strip()) - if value <= 0: - return self.InvalidValue(input_string) - self.value = str(value) - -# Dialog data item converted and validated using a SQL table - -class SQLTableDataItem(LineEditDataItem): - - def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent): - super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent) - - self.table_name = table_name - self.match_column = match_column - self.column_name1 = column_name1 - self.column_name2 = column_name2 - - def ValueToIds(self, value): - ids = [] - query = QSqlQuery(self.glb.db) - stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'" - ret = query.exec_(stmt) - if ret: - while query.next(): - ids.append(str(query.value(0))) - return ids - - def DoValidate(self, input_string): - all_ids = [] - for value in [x.strip() for x in input_string.split(",")]: - ids = self.ValueToIds(value) - if len(ids): - all_ids.extend(ids) - else: - return self.InvalidValue(value) - self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")" - if self.column_name2: - self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )" - -# Sample time ranges dialog data item converted and validated using 'samples' SQL table - -class SampleTimeRangesDataItem(LineEditDataItem): - - def __init__(self, glb, label, placeholder_text, column_name, parent): - self.column_name = column_name - - self.last_id = 0 - self.first_time = 0 - self.last_time = 2 ** 64 - - query = QSqlQuery(glb.db) - QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1") - if query.next(): - self.last_id = int(query.value(0)) - self.first_time = int(glb.HostStartTime()) - self.last_time = int(glb.HostFinishTime()) - if placeholder_text: - placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time) - - super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent) - - def IdBetween(self, query, lower_id, higher_id, order): - QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1") - if query.next(): - return True, int(query.value(0)) - else: - return False, 0 - - def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor): - query = QSqlQuery(self.glb.db) - while True: - next_id = int((lower_id + higher_id) / 2) - QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) - if not query.next(): - ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC") - if not ok: - ok, dbid = self.IdBetween(query, next_id, higher_id, "") - if not ok: - return str(higher_id) - next_id = dbid - QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) - next_time = int(query.value(0)) - if get_floor: - if target_time > next_time: - lower_id = next_id - else: - higher_id = next_id - if higher_id <= lower_id + 1: - return str(higher_id) - else: - if target_time >= next_time: - lower_id = next_id - else: - higher_id = next_id - if higher_id <= lower_id + 1: - return str(lower_id) - - def ConvertRelativeTime(self, val): - mult = 1 - suffix = val[-2:] - if suffix == "ms": - mult = 1000000 - elif suffix == "us": - mult = 1000 - elif suffix == "ns": - mult = 1 - else: - return val - val = val[:-2].strip() - if not self.IsNumber(val): - return val - val = int(val) * mult - if val >= 0: - val += self.first_time - else: - val += self.last_time - return str(val) - - def ConvertTimeRange(self, vrange): - if vrange[0] == "": - vrange[0] = str(self.first_time) - if vrange[1] == "": - vrange[1] = str(self.last_time) - vrange[0] = self.ConvertRelativeTime(vrange[0]) - vrange[1] = self.ConvertRelativeTime(vrange[1]) - if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): - return False - beg_range = max(int(vrange[0]), self.first_time) - end_range = min(int(vrange[1]), self.last_time) - if beg_range > self.last_time or end_range < self.first_time: - return False - vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True) - vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False) - return True - - def AddTimeRange(self, value, ranges): - n = value.count("-") - if n == 1: - pass - elif n == 2: - if value.split("-")[1].strip() == "": - n = 1 - elif n == 3: - n = 2 - else: - return False - pos = findnth(value, "-", n) - vrange = [value[:pos].strip() ,value[pos+1:].strip()] - if self.ConvertTimeRange(vrange): - ranges.append(vrange) - return True - return False - - def DoValidate(self, input_string): - ranges = [] - for value in [x.strip() for x in input_string.split(",")]: - if not self.AddTimeRange(value, ranges): - return self.InvalidValue(value) - ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] - self.value = " OR ".join(ranges) - -# Report Dialog Base - -class ReportDialogBase(QDialog): - - def __init__(self, glb, title, items, partial, parent=None): - super(ReportDialogBase, self).__init__(parent) - - self.glb = glb - - self.report_vars = ReportVars() - - self.setWindowTitle(title) - self.setMinimumWidth(600) - - self.data_items = [x(glb, self) for x in items] - - self.partial = partial - - self.grid = QGridLayout() - - for row in xrange(len(self.data_items)): - self.grid.addWidget(QLabel(self.data_items[row].label), row, 0) - self.grid.addWidget(self.data_items[row].widget, row, 1) - - self.status = QLabel() - - self.ok_button = QPushButton("Ok", self) - self.ok_button.setDefault(True) - self.ok_button.released.connect(self.Ok) - self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.cancel_button = QPushButton("Cancel", self) - self.cancel_button.released.connect(self.reject) - self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) - - self.hbox = QHBoxLayout() - #self.hbox.addStretch() - self.hbox.addWidget(self.status) - self.hbox.addWidget(self.ok_button) - self.hbox.addWidget(self.cancel_button) - - self.vbox = QVBoxLayout() - self.vbox.addLayout(self.grid) - self.vbox.addLayout(self.hbox) - - self.setLayout(self.vbox) - - def Ok(self): - vars = self.report_vars - for d in self.data_items: - if d.id == "REPORTNAME": - vars.name = d.value - if not vars.name: - self.ShowMessage("Report name is required") - return - for d in self.data_items: - if not d.IsValid(): - return - for d in self.data_items[1:]: - if d.id == "LIMIT": - vars.limit = d.value - elif len(d.value): - if len(vars.where_clause): - vars.where_clause += " AND " - vars.where_clause += d.value - if len(vars.where_clause): - if self.partial: - vars.where_clause = " AND ( " + vars.where_clause + " ) " - else: - vars.where_clause = " WHERE " + vars.where_clause + " " - self.accept() - - def ShowMessage(self, msg): - self.status.setText("<font color=#FF0000>" + msg) - - def ClearMessage(self): - self.status.setText("") - -# Selected branch report creation dialog - -class SelectedBranchDialog(ReportDialogBase): - - def __init__(self, glb, parent=None): - title = "Selected Branches" - items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), - lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p), - lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p), - lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p), - lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p), - lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p), - lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p), - lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p), - lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p)) - super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent) - -# Event list - -def GetEventList(db): - events = [] - query = QSqlQuery(db) - QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id") - while query.next(): - events.append(query.value(0)) - return events - -# Is a table selectable - -def IsSelectable(db, table, sql = "", columns = "*"): - query = QSqlQuery(db) - try: - QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1") - except: - return False - return True - -# SQL table data model item - -class SQLTableItem(): - - def __init__(self, row, data): - self.row = row - self.data = data - - def getData(self, column): - return self.data[column] - -# SQL table data model - -class SQLTableModel(TableModel): - - progress = Signal(object) - - def __init__(self, glb, sql, column_headers, parent=None): - super(SQLTableModel, self).__init__(parent) - self.glb = glb - self.more = True - self.populated = 0 - self.column_headers = column_headers - self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample) - self.fetcher.done.connect(self.Update) - self.fetcher.Fetch(glb_chunk_sz) - - def DisplayData(self, item, index): - self.FetchIfNeeded(item.row) - return item.getData(index.column()) - - def AddSample(self, data): - child = SQLTableItem(self.populated, data) - self.child_items.append(child) - self.populated += 1 - - def Update(self, fetched): - if not fetched: - self.more = False - self.progress.emit(0) - child_count = self.child_count - count = self.populated - child_count - if count > 0: - parent = QModelIndex() - self.beginInsertRows(parent, child_count, child_count + count - 1) - self.insertRows(child_count, count, parent) - self.child_count += count - self.endInsertRows() - self.progress.emit(self.child_count) - - def FetchMoreRecords(self, count): - current = self.child_count - if self.more: - self.fetcher.Fetch(count) - else: - self.progress.emit(0) - return current - - def HasMoreRecords(self): - return self.more - - def columnCount(self, parent=None): - return len(self.column_headers) - - def columnHeader(self, column): - return self.column_headers[column] - - def SQLTableDataPrep(self, query, count): - data = [] - for i in xrange(count): - data.append(query.value(i)) - return data - -# SQL automatic table data model - -class SQLAutoTableModel(SQLTableModel): - - def __init__(self, glb, table_name, parent=None): - sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz) - if table_name == "comm_threads_view": - # For now, comm_threads_view has no id column - sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz) - column_headers = [] - query = QSqlQuery(glb.db) - if glb.dbref.is_sqlite3: - QueryExec(query, "PRAGMA table_info(" + table_name + ")") - while query.next(): - column_headers.append(query.value(1)) - if table_name == "sqlite_master": - sql = "SELECT * FROM " + table_name - else: - if table_name[:19] == "information_schema.": - sql = "SELECT * FROM " + table_name - select_table_name = table_name[19:] - schema = "information_schema" - else: - select_table_name = table_name - schema = "public" - QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'") - while query.next(): - column_headers.append(query.value(0)) - if pyside_version_1 and sys.version_info[0] == 3: - if table_name == "samples_view": - self.SQLTableDataPrep = self.samples_view_DataPrep - if table_name == "samples": - self.SQLTableDataPrep = self.samples_DataPrep - super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent) - - def samples_view_DataPrep(self, query, count): - data = [] - data.append(query.value(0)) - # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string - data.append("{:>19}".format(query.value(1))) - for i in xrange(2, count): - data.append(query.value(i)) - return data - - def samples_DataPrep(self, query, count): - data = [] - for i in xrange(9): - data.append(query.value(i)) - # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string - data.append("{:>19}".format(query.value(9))) - for i in xrange(10, count): - data.append(query.value(i)) - return data - -# Base class for custom ResizeColumnsToContents - -class ResizeColumnsToContentsBase(QObject): - - def __init__(self, parent=None): - super(ResizeColumnsToContentsBase, self).__init__(parent) - - def ResizeColumnToContents(self, column, n): - # Using the view's resizeColumnToContents() here is extrememly slow - # so implement a crude alternative - font = self.view.font() - metrics = QFontMetrics(font) - max = 0 - for row in xrange(n): - val = self.data_model.child_items[row].data[column] - len = metrics.width(str(val) + "MM") - max = len if len > max else max - val = self.data_model.columnHeader(column) - len = metrics.width(str(val) + "MM") - max = len if len > max else max - self.view.setColumnWidth(column, max) - - def ResizeColumnsToContents(self): - n = min(self.data_model.child_count, 100) - if n < 1: - # No data yet, so connect a signal to notify when there is - self.data_model.rowsInserted.connect(self.UpdateColumnWidths) - return - columns = self.data_model.columnCount() - for i in xrange(columns): - self.ResizeColumnToContents(i, n) - - def UpdateColumnWidths(self, *x): - # This only needs to be done once, so disconnect the signal now - self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths) - self.ResizeColumnsToContents() - -# Convert value to CSV - -def ToCSValue(val): - if '"' in val: - val = val.replace('"', '""') - if "," in val or '"' in val: - val = '"' + val + '"' - return val - -# Key to sort table model indexes by row / column, assuming fewer than 1000 columns - -glb_max_cols = 1000 - -def RowColumnKey(a): - return a.row() * glb_max_cols + a.column() - -# Copy selected table cells to clipboard - -def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False): - indexes = sorted(view.selectedIndexes(), key=RowColumnKey) - idx_cnt = len(indexes) - if not idx_cnt: - return - if idx_cnt == 1: - with_hdr=False - min_row = indexes[0].row() - max_row = indexes[0].row() - min_col = indexes[0].column() - max_col = indexes[0].column() - for i in indexes: - min_row = min(min_row, i.row()) - max_row = max(max_row, i.row()) - min_col = min(min_col, i.column()) - max_col = max(max_col, i.column()) - if max_col > glb_max_cols: - raise RuntimeError("glb_max_cols is too low") - max_width = [0] * (1 + max_col - min_col) - for i in indexes: - c = i.column() - min_col - max_width[c] = max(max_width[c], len(str(i.data()))) - text = "" - pad = "" - sep = "" - if with_hdr: - model = indexes[0].model() - for col in range(min_col, max_col + 1): - val = model.headerData(col, Qt.Horizontal, Qt.DisplayRole) - if as_csv: - text += sep + ToCSValue(val) - sep = "," - else: - c = col - min_col - max_width[c] = max(max_width[c], len(val)) - width = max_width[c] - align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole) - if align & Qt.AlignRight: - val = val.rjust(width) - text += pad + sep + val - pad = " " * (width - len(val)) - sep = " " - text += "\n" - pad = "" - sep = "" - last_row = min_row - for i in indexes: - if i.row() > last_row: - last_row = i.row() - text += "\n" - pad = "" - sep = "" - if as_csv: - text += sep + ToCSValue(str(i.data())) - sep = "," - else: - width = max_width[i.column() - min_col] - if i.data(Qt.TextAlignmentRole) & Qt.AlignRight: - val = str(i.data()).rjust(width) - else: - val = str(i.data()) - text += pad + sep + val - pad = " " * (width - len(val)) - sep = " " - QApplication.clipboard().setText(text) - -def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False): - indexes = view.selectedIndexes() - if not len(indexes): - return - - selection = view.selectionModel() - - first = None - for i in indexes: - above = view.indexAbove(i) - if not selection.isSelected(above): - first = i - break - - if first is None: - raise RuntimeError("CopyTreeCellsToClipboard internal error") - - model = first.model() - row_cnt = 0 - col_cnt = model.columnCount(first) - max_width = [0] * col_cnt - - indent_sz = 2 - indent_str = " " * indent_sz - - expanded_mark_sz = 2 - if sys.version_info[0] == 3: - expanded_mark = "\u25BC " - not_expanded_mark = "\u25B6 " - else: - expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8") - not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8") - leaf_mark = " " - - if not as_csv: - pos = first - while True: - row_cnt += 1 - row = pos.row() - for c in range(col_cnt): - i = pos.sibling(row, c) - if c: - n = len(str(i.data())) - else: - n = len(str(i.data()).strip()) - n += (i.internalPointer().level - 1) * indent_sz - n += expanded_mark_sz - max_width[c] = max(max_width[c], n) - pos = view.indexBelow(pos) - if not selection.isSelected(pos): - break - - text = "" - pad = "" - sep = "" - if with_hdr: - for c in range(col_cnt): - val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip() - if as_csv: - text += sep + ToCSValue(val) - sep = "," - else: - max_width[c] = max(max_width[c], len(val)) - width = max_width[c] - align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole) - if align & Qt.AlignRight: - val = val.rjust(width) - text += pad + sep + val - pad = " " * (width - len(val)) - sep = " " - text += "\n" - pad = "" - sep = "" - - pos = first - while True: - row = pos.row() - for c in range(col_cnt): - i = pos.sibling(row, c) - val = str(i.data()) - if not c: - if model.hasChildren(i): - if view.isExpanded(i): - mark = expanded_mark - else: - mark = not_expanded_mark - else: - mark = leaf_mark - val = indent_str * (i.internalPointer().level - 1) + mark + val.strip() - if as_csv: - text += sep + ToCSValue(val) - sep = "," - else: - width = max_width[c] - if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight: - val = val.rjust(width) - text += pad + sep + val - pad = " " * (width - len(val)) - sep = " " - pos = view.indexBelow(pos) - if not selection.isSelected(pos): - break - text = text.rstrip() + "\n" - pad = "" - sep = "" - - QApplication.clipboard().setText(text) - -def CopyCellsToClipboard(view, as_csv=False, with_hdr=False): - view.CopyCellsToClipboard(view, as_csv, with_hdr) - -def CopyCellsToClipboardHdr(view): - CopyCellsToClipboard(view, False, True) - -def CopyCellsToClipboardCSV(view): - CopyCellsToClipboard(view, True, True) - -# Context menu - -class ContextMenu(object): - - def __init__(self, view): - self.view = view - self.view.setContextMenuPolicy(Qt.CustomContextMenu) - self.view.customContextMenuRequested.connect(self.ShowContextMenu) - - def ShowContextMenu(self, pos): - menu = QMenu(self.view) - self.AddActions(menu) - menu.exec_(self.view.mapToGlobal(pos)) - - def AddCopy(self, menu): - menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view)) - menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view)) - - def AddActions(self, menu): - self.AddCopy(menu) - -class TreeContextMenu(ContextMenu): - - def __init__(self, view): - super(TreeContextMenu, self).__init__(view) - - def AddActions(self, menu): - i = self.view.currentIndex() - text = str(i.data()).strip() - if len(text): - menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view)) - self.AddCopy(menu) - -# Table window - -class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase): - - def __init__(self, glb, table_name, parent=None): - super(TableWindow, self).__init__(parent) - - self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name)) - - self.model = QSortFilterProxyModel() - self.model.setSourceModel(self.data_model) - - self.view = QTableView() - self.view.setModel(self.model) - self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.view.verticalHeader().setVisible(False) - self.view.sortByColumn(-1, Qt.AscendingOrder) - self.view.setSortingEnabled(True) - self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) - self.view.CopyCellsToClipboard = CopyTableCellsToClipboard - - self.ResizeColumnsToContents() - - self.context_menu = ContextMenu(self.view) - - self.find_bar = FindBar(self, self, True) - - self.finder = ChildDataItemFinder(self.data_model) - - self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) - - self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table") - - def Find(self, value, direction, pattern, context): - self.view.setFocus() - self.find_bar.Busy() - self.finder.Find(value, direction, pattern, context, self.FindDone) - - def FindDone(self, row): - self.find_bar.Idle() - if row >= 0: - self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex()))) - else: - self.find_bar.NotFound() - -# Table list - -def GetTableList(glb): - tables = [] - query = QSqlQuery(glb.db) - if glb.dbref.is_sqlite3: - QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name") - else: - QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name") - while query.next(): - tables.append(query.value(0)) - if glb.dbref.is_sqlite3: - tables.append("sqlite_master") - else: - tables.append("information_schema.tables") - tables.append("information_schema.views") - tables.append("information_schema.columns") - return tables - -# Top Calls data model - -class TopCallsModel(SQLTableModel): - - def __init__(self, glb, report_vars, parent=None): - text = "" - if not glb.dbref.is_sqlite3: - text = "::text" - limit = "" - if len(report_vars.limit): - limit = " LIMIT " + report_vars.limit - sql = ("SELECT comm, pid, tid, name," - " CASE" - " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text + - " ELSE short_name" - " END AS dso," - " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, " - " CASE" - " WHEN (calls.flags = 1) THEN 'no call'" + text + - " WHEN (calls.flags = 2) THEN 'no return'" + text + - " WHEN (calls.flags = 3) THEN 'no call/return'" + text + - " ELSE ''" + text + - " END AS flags" - " FROM calls" - " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" - " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" - " INNER JOIN dsos ON symbols.dso_id = dsos.id" - " INNER JOIN comms ON calls.comm_id = comms.id" - " INNER JOIN threads ON calls.thread_id = threads.id" + - report_vars.where_clause + - " ORDER BY elapsed_time DESC" + - limit - ) - column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags") - self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft) - super(TopCallsModel, self).__init__(glb, sql, column_headers, parent) - - def columnAlignment(self, column): - return self.alignment[column] - -# Top Calls report creation dialog - -class TopCallsDialog(ReportDialogBase): - - def __init__(self, glb, parent=None): - title = "Top Calls by Elapsed Time" - items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), - lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p), - lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p), - lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p), - lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p), - lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p), - lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p), - lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100")) - super(TopCallsDialog, self).__init__(glb, title, items, False, parent) - -# Top Calls window - -class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase): - - def __init__(self, glb, report_vars, parent=None): - super(TopCallsWindow, self).__init__(parent) - - self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars)) - self.model = self.data_model - - self.view = QTableView() - self.view.setModel(self.model) - self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) - self.view.verticalHeader().setVisible(False) - self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) - self.view.CopyCellsToClipboard = CopyTableCellsToClipboard - - self.context_menu = ContextMenu(self.view) - - self.ResizeColumnsToContents() - - self.find_bar = FindBar(self, self, True) - - self.finder = ChildDataItemFinder(self.model) - - self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) - - self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) - - self.setWidget(self.vbox.Widget()) - - AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name) - - def Find(self, value, direction, pattern, context): - self.view.setFocus() - self.find_bar.Busy() - self.finder.Find(value, direction, pattern, context, self.FindDone) - - def FindDone(self, row): - self.find_bar.Idle() - if row >= 0: - self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) - else: - self.find_bar.NotFound() - -# Action Definition - -def CreateAction(label, tip, callback, parent=None, shortcut=None): - action = QAction(label, parent) - if shortcut != None: - action.setShortcuts(shortcut) - action.setStatusTip(tip) - action.triggered.connect(callback) - return action - -# Typical application actions - -def CreateExitAction(app, parent=None): - return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) - -# Typical MDI actions - -def CreateCloseActiveWindowAction(mdi_area): - return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) - -def CreateCloseAllWindowsAction(mdi_area): - return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) - -def CreateTileWindowsAction(mdi_area): - return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) - -def CreateCascadeWindowsAction(mdi_area): - return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) - -def CreateNextWindowAction(mdi_area): - return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) - -def CreatePreviousWindowAction(mdi_area): - return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) - -# Typical MDI window menu - -class WindowMenu(): - - def __init__(self, mdi_area, menu): - self.mdi_area = mdi_area - self.window_menu = menu.addMenu("&Windows") - self.close_active_window = CreateCloseActiveWindowAction(mdi_area) - self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) - self.tile_windows = CreateTileWindowsAction(mdi_area) - self.cascade_windows = CreateCascadeWindowsAction(mdi_area) - self.next_window = CreateNextWindowAction(mdi_area) - self.previous_window = CreatePreviousWindowAction(mdi_area) - self.window_menu.aboutToShow.connect(self.Update) - - def Update(self): - self.window_menu.clear() - sub_window_count = len(self.mdi_area.subWindowList()) - have_sub_windows = sub_window_count != 0 - self.close_active_window.setEnabled(have_sub_windows) - self.close_all_windows.setEnabled(have_sub_windows) - self.tile_windows.setEnabled(have_sub_windows) - self.cascade_windows.setEnabled(have_sub_windows) - self.next_window.setEnabled(have_sub_windows) - self.previous_window.setEnabled(have_sub_windows) - self.window_menu.addAction(self.close_active_window) - self.window_menu.addAction(self.close_all_windows) - self.window_menu.addSeparator() - self.window_menu.addAction(self.tile_windows) - self.window_menu.addAction(self.cascade_windows) - self.window_menu.addSeparator() - self.window_menu.addAction(self.next_window) - self.window_menu.addAction(self.previous_window) - if sub_window_count == 0: - return - self.window_menu.addSeparator() - nr = 1 - for sub_window in self.mdi_area.subWindowList(): - label = str(nr) + " " + sub_window.name - if nr < 10: - label = "&" + label - action = self.window_menu.addAction(label) - action.setCheckable(True) - action.setChecked(sub_window == self.mdi_area.activeSubWindow()) - action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x)) - self.window_menu.addAction(action) - nr += 1 - - def setActiveSubWindow(self, nr): - self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) - -# Help text - -glb_help_text = """ -<h1>Contents</h1> -<style> -p.c1 { - text-indent: 40px; -} -p.c2 { - text-indent: 80px; -} -} -</style> -<p class=c1><a href=#reports>1. Reports</a></p> -<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p> -<p class=c2><a href=#calltree>1.2 Call Tree</a></p> -<p class=c2><a href=#allbranches>1.3 All branches</a></p> -<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p> -<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p> -<p class=c1><a href=#charts>2. Charts</a></p> -<p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p> -<p class=c1><a href=#tables>3. Tables</a></p> -<h1 id=reports>1. Reports</h1> -<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2> -The result is a GUI window with a tree representing a context-sensitive -call-graph. Expanding a couple of levels of the tree and adjusting column -widths to suit will display something like: -<pre> - Call Graph: pt_example -Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) -v- ls - v- 2638:2638 - v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 - |- unknown unknown 1 13198 0.1 1 0.0 - >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 - >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 - v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 - >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 - >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 - >- __libc_csu_init ls 1 10354 0.1 10 0.0 - |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 - v- main ls 1 8182043 99.6 180254 99.9 -</pre> -<h3>Points to note:</h3> -<ul> -<li>The top level is a command name (comm)</li> -<li>The next level is a thread (pid:tid)</li> -<li>Subsequent levels are functions</li> -<li>'Count' is the number of calls</li> -<li>'Time' is the elapsed time until the function returns</li> -<li>Percentages are relative to the level above</li> -<li>'Branch Count' is the total number of branches for that function and all functions that it calls -</ul> -<h3>Find</h3> -Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match. -The pattern matching symbols are ? for any character and * for zero or more characters. -<h2 id=calltree>1.2 Call Tree</h2> -The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated. -Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'. -<h2 id=allbranches>1.3 All branches</h2> -The All branches report displays all branches in chronological order. -Not all data is fetched immediately. More records can be fetched using the Fetch bar provided. -<h3>Disassembly</h3> -Open a branch to display disassembly. This only works if: -<ol> -<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li> -<li>The object code is available. Currently, only the perf build ID cache is searched for object code. -The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR. -One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu), -or alternatively, set environment variable PERF_KCORE to the kcore file name.</li> -</ol> -<h4 id=xed>Intel XED Setup</h4> -To use Intel XED, libxed.so must be present. To build and install libxed.so: -<pre> -git clone https://github.com/intelxed/mbuild.git mbuild -git clone https://github.com/intelxed/xed -cd xed -./mfile.py --share -sudo ./mfile.py --prefix=/usr/local install -sudo ldconfig -</pre> -<h3>Instructions per Cycle (IPC)</h3> -If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'. -<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch. -Due to the granularity of timing information, the number of cycles for some code blocks will not be known. -In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period -since the previous displayed 'IPC'. -<h3>Find</h3> -Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. -Refer to Python documentation for the regular expression syntax. -All columns are searched, but only currently fetched rows are searched. -<h2 id=selectedbranches>1.4 Selected branches</h2> -This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced -by various selection criteria. A dialog box displays available criteria which are AND'ed together. -<h3>1.4.1 Time ranges</h3> -The time ranges hint text shows the total time range. Relative time ranges can also be entered in -ms, us or ns. Also, negative values are relative to the end of trace. Examples: -<pre> - 81073085947329-81073085958238 From 81073085947329 to 81073085958238 - 100us-200us From 100us to 200us - 10ms- From 10ms to the end - -100ns The first 100ns - -10ms- The last 10ms -</pre> -N.B. Due to the granularity of timestamps, there could be no branches in any given time range. -<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2> -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. -The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together. -If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar. -<h1 id=charts>2. Charts</h1> -<h2 id=timechartbycpu>2.1 Time chart by CPU</h2> -This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu. -<h3>Features</h3> -<ol> -<li>Mouse over to highight the task and show the time</li> -<li>Drag the mouse to select a region and zoom by pushing the Zoom button</li> -<li>Go back and forward by pressing the arrow buttons</li> -<li>If call information is available, right-click to show a call tree opened to that task and time. -Note, the call tree may take some time to appear, and there may not be call information for the task or time selected. -</li> -</ol> -<h3>Important</h3> -The graph can be misleading in the following respects: -<ol> -<li>The graph shows the first task on each CPU as running from the beginning of the time range. -Because tracing might start on different CPUs at different times, that is not necessarily the case. -Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li> -<li>Similarly, the last task on each CPU can be showing running longer than it really was. -Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li> -<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> -</ol> -<h1 id=tables>3. Tables</h1> -The Tables menu shows all tables and views in the database. Most tables have an associated view -which displays the information in a more friendly way. Not all data for large tables is fetched -immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted, -but that can be slow for large tables. -<p>There are also tables of database meta-information. -For SQLite3 databases, the sqlite_master table is included. -For PostgreSQL databases, information_schema.tables/views/columns are included. -<h3>Find</h3> -Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. -Refer to Python documentation for the regular expression syntax. -All columns are searched, but only currently fetched rows are searched. -<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous -will go to the next/previous result in id order, instead of display order. -""" - -# Help window - -class HelpWindow(QMdiSubWindow): - - def __init__(self, glb, parent=None): - super(HelpWindow, self).__init__(parent) - - self.text = QTextBrowser() - self.text.setHtml(glb_help_text) - self.text.setReadOnly(True) - self.text.setOpenExternalLinks(True) - - self.setWidget(self.text) - - AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help") - -# Main window that only displays the help text - -class HelpOnlyWindow(QMainWindow): - - def __init__(self, parent=None): - super(HelpOnlyWindow, self).__init__(parent) - - self.setMinimumSize(200, 100) - self.resize(800, 600) - self.setWindowTitle("Exported SQL Viewer Help") - self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation)) - - self.text = QTextBrowser() - self.text.setHtml(glb_help_text) - self.text.setReadOnly(True) - self.text.setOpenExternalLinks(True) - - self.setCentralWidget(self.text) - -# PostqreSQL server version - -def PostqreSQLServerVersion(db): - query = QSqlQuery(db) - QueryExec(query, "SELECT VERSION()") - if query.next(): - v_str = query.value(0) - v_list = v_str.strip().split(" ") - if v_list[0] == "PostgreSQL" and v_list[2] == "on": - return v_list[1] - return v_str - return "Unknown" - -# SQLite version - -def SQLiteVersion(db): - query = QSqlQuery(db) - QueryExec(query, "SELECT sqlite_version()") - if query.next(): - return query.value(0) - return "Unknown" - -# About dialog - -class AboutDialog(QDialog): - - def __init__(self, glb, parent=None): - super(AboutDialog, self).__init__(parent) - - self.setWindowTitle("About Exported SQL Viewer") - self.setMinimumWidth(300) - - pyside_version = "1" if pyside_version_1 else "2" - - text = "<pre>" - text += "Python version: " + sys.version.split(" ")[0] + "\n" - text += "PySide version: " + pyside_version + "\n" - text += "Qt version: " + qVersion() + "\n" - if glb.dbref.is_sqlite3: - text += "SQLite version: " + SQLiteVersion(glb.db) + "\n" - else: - text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n" - text += "</pre>" - - self.text = QTextBrowser() - self.text.setHtml(text) - self.text.setReadOnly(True) - self.text.setOpenExternalLinks(True) - - self.vbox = QVBoxLayout() - self.vbox.addWidget(self.text) - - self.setLayout(self.vbox) - -# Font resize - -def ResizeFont(widget, diff): - font = widget.font() - sz = font.pointSize() - font.setPointSize(sz + diff) - widget.setFont(font) - -def ShrinkFont(widget): - ResizeFont(widget, -1) - -def EnlargeFont(widget): - ResizeFont(widget, 1) - -# Unique name for sub-windows - -def NumberedWindowName(name, nr): - if nr > 1: - name += " <" + str(nr) + ">" - return name - -def UniqueSubWindowName(mdi_area, name): - nr = 1 - while True: - unique_name = NumberedWindowName(name, nr) - ok = True - for sub_window in mdi_area.subWindowList(): - if sub_window.name == unique_name: - ok = False - break - if ok: - return unique_name - nr += 1 - -# Add a sub-window - -def AddSubWindow(mdi_area, sub_window, name): - unique_name = UniqueSubWindowName(mdi_area, name) - sub_window.setMinimumSize(200, 100) - sub_window.resize(800, 600) - sub_window.setWindowTitle(unique_name) - sub_window.setAttribute(Qt.WA_DeleteOnClose) - sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) - sub_window.name = unique_name - mdi_area.addSubWindow(sub_window) - sub_window.show() - -# Main window - -class MainWindow(QMainWindow): - - def __init__(self, glb, parent=None): - super(MainWindow, self).__init__(parent) - - self.glb = glb - - self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) - self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) - self.setMinimumSize(200, 100) - - self.mdi_area = QMdiArea() - self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) - self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) - - self.setCentralWidget(self.mdi_area) - - menu = self.menuBar() - - file_menu = menu.addMenu("&File") - file_menu.addAction(CreateExitAction(glb.app, self)) - - edit_menu = menu.addMenu("&Edit") - edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy)) - edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self)) - edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) - edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)])) - edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")])) - edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")])) - - reports_menu = menu.addMenu("&Reports") - if IsSelectable(glb.db, "calls"): - reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) - - if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"): - reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self)) - - self.EventMenu(GetEventList(glb.db), reports_menu) - - if IsSelectable(glb.db, "calls"): - reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self)) - - if IsSelectable(glb.db, "context_switches"): - charts_menu = menu.addMenu("&Charts") - charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self)) - - self.TableMenu(GetTableList(glb), menu) - - self.window_menu = WindowMenu(self.mdi_area, menu) - - help_menu = menu.addMenu("&Help") - help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents)) - help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self)) - - def Try(self, fn): - win = self.mdi_area.activeSubWindow() - if win: - try: - fn(win.view) - except: - pass - - def CopyToClipboard(self): - self.Try(CopyCellsToClipboardHdr) - - def CopyToClipboardCSV(self): - self.Try(CopyCellsToClipboardCSV) - - def Find(self): - win = self.mdi_area.activeSubWindow() - if win: - try: - win.find_bar.Activate() - except: - pass - - def FetchMoreRecords(self): - win = self.mdi_area.activeSubWindow() - if win: - try: - win.fetch_bar.Activate() - except: - pass - - def ShrinkFont(self): - self.Try(ShrinkFont) - - def EnlargeFont(self): - self.Try(EnlargeFont) - - def EventMenu(self, events, reports_menu): - branches_events = 0 - for event in events: - event = event.split(":")[0] - if event == "branches": - branches_events += 1 - dbid = 0 - for event in events: - dbid += 1 - event = event.split(":")[0] - if event == "branches": - label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")" - reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self)) - label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")" - reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self)) - - def TimeChartByCPU(self): - TimeChartByCPUWindow(self.glb, self) - - def TableMenu(self, tables, menu): - table_menu = menu.addMenu("&Tables") - for table in tables: - table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self)) - - def NewCallGraph(self): - CallGraphWindow(self.glb, self) - - def NewCallTree(self): - CallTreeWindow(self.glb, self) - - def NewTopCalls(self): - dialog = TopCallsDialog(self.glb, self) - ret = dialog.exec_() - if ret: - TopCallsWindow(self.glb, dialog.report_vars, self) - - def NewBranchView(self, event_id): - BranchWindow(self.glb, event_id, ReportVars(), self) - - def NewSelectedBranchView(self, event_id): - dialog = SelectedBranchDialog(self.glb, self) - ret = dialog.exec_() - if ret: - BranchWindow(self.glb, event_id, dialog.report_vars, self) - - def NewTableView(self, table_name): - TableWindow(self.glb, table_name, self) - - def Help(self): - HelpWindow(self.glb, self) - - def About(self): - dialog = AboutDialog(self.glb, self) - dialog.exec_() - -def TryOpen(file_name): - try: - return open(file_name, "rb") - except: - return None - -def Is64Bit(f): - result = sizeof(c_void_p) - # ELF support only - pos = f.tell() - f.seek(0) - header = f.read(7) - f.seek(pos) - magic = header[0:4] - if sys.version_info[0] == 2: - eclass = ord(header[4]) - encoding = ord(header[5]) - version = ord(header[6]) - else: - eclass = header[4] - encoding = header[5] - version = header[6] - if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1: - result = True if eclass == 2 else False - return result - -# Global data - -class Glb(): - - def __init__(self, dbref, db, dbname): - self.dbref = dbref - self.db = db - self.dbname = dbname - self.home_dir = os.path.expanduser("~") - self.buildid_dir = os.getenv("PERF_BUILDID_DIR") - if self.buildid_dir: - self.buildid_dir += "/.build-id/" - else: - self.buildid_dir = self.home_dir + "/.debug/.build-id/" - self.app = None - self.mainwindow = None - self.instances_to_shutdown_on_exit = weakref.WeakSet() - try: - self.disassembler = LibXED() - self.have_disassembler = True - except: - self.have_disassembler = False - self.host_machine_id = 0 - self.host_start_time = 0 - self.host_finish_time = 0 - - def FileFromBuildId(self, build_id): - file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf" - return TryOpen(file_name) - - def FileFromNamesAndBuildId(self, short_name, long_name, build_id): - # Assume current machine i.e. no support for virtualization - if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore": - file_name = os.getenv("PERF_KCORE") - f = TryOpen(file_name) if file_name else None - if f: - return f - # For now, no special handling if long_name is /proc/kcore - f = TryOpen(long_name) - if f: - return f - f = self.FileFromBuildId(build_id) - if f: - return f - return None - - def AddInstanceToShutdownOnExit(self, instance): - self.instances_to_shutdown_on_exit.add(instance) - - # Shutdown any background processes or threads - def ShutdownInstances(self): - for x in self.instances_to_shutdown_on_exit: - try: - x.Shutdown() - except: - pass - - def GetHostMachineId(self): - query = QSqlQuery(self.db) - QueryExec(query, "SELECT id FROM machines WHERE pid = -1") - if query.next(): - self.host_machine_id = query.value(0) - else: - self.host_machine_id = 0 - return self.host_machine_id - - def HostMachineId(self): - if self.host_machine_id: - return self.host_machine_id - return self.GetHostMachineId() - - def SelectValue(self, sql): - query = QSqlQuery(self.db) - try: - QueryExec(query, sql) - except: - return None - if query.next(): - return Decimal(query.value(0)) - return None - - def SwitchesMinTime(self, machine_id): - return self.SelectValue("SELECT time" - " FROM context_switches" - " WHERE time != 0 AND machine_id = " + str(machine_id) + - " ORDER BY id LIMIT 1") - - def SwitchesMaxTime(self, machine_id): - return self.SelectValue("SELECT time" - " FROM context_switches" - " WHERE time != 0 AND machine_id = " + str(machine_id) + - " ORDER BY id DESC LIMIT 1") - - def SamplesMinTime(self, machine_id): - return self.SelectValue("SELECT time" - " FROM samples" - " WHERE time != 0 AND machine_id = " + str(machine_id) + - " ORDER BY id LIMIT 1") - - def SamplesMaxTime(self, machine_id): - return self.SelectValue("SELECT time" - " FROM samples" - " WHERE time != 0 AND machine_id = " + str(machine_id) + - " ORDER BY id DESC LIMIT 1") - - def CallsMinTime(self, machine_id): - return self.SelectValue("SELECT calls.call_time" - " FROM calls" - " INNER JOIN threads ON threads.thread_id = calls.thread_id" - " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) + - " ORDER BY calls.id LIMIT 1") - - def CallsMaxTime(self, machine_id): - return self.SelectValue("SELECT calls.return_time" - " FROM calls" - " INNER JOIN threads ON threads.thread_id = calls.thread_id" - " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) + - " ORDER BY calls.return_time DESC LIMIT 1") - - def GetStartTime(self, machine_id): - t0 = self.SwitchesMinTime(machine_id) - t1 = self.SamplesMinTime(machine_id) - t2 = self.CallsMinTime(machine_id) - if t0 is None or (not(t1 is None) and t1 < t0): - t0 = t1 - if t0 is None or (not(t2 is None) and t2 < t0): - t0 = t2 - return t0 - - def GetFinishTime(self, machine_id): - t0 = self.SwitchesMaxTime(machine_id) - t1 = self.SamplesMaxTime(machine_id) - t2 = self.CallsMaxTime(machine_id) - if t0 is None or (not(t1 is None) and t1 > t0): - t0 = t1 - if t0 is None or (not(t2 is None) and t2 > t0): - t0 = t2 - return t0 - - def HostStartTime(self): - if self.host_start_time: - return self.host_start_time - self.host_start_time = self.GetStartTime(self.HostMachineId()) - return self.host_start_time - - def HostFinishTime(self): - if self.host_finish_time: - return self.host_finish_time - self.host_finish_time = self.GetFinishTime(self.HostMachineId()) - return self.host_finish_time - - def StartTime(self, machine_id): - if machine_id == self.HostMachineId(): - return self.HostStartTime() - return self.GetStartTime(machine_id) - - def FinishTime(self, machine_id): - if machine_id == self.HostMachineId(): - return self.HostFinishTime() - return self.GetFinishTime(machine_id) - -# Database reference - -class DBRef(): - - def __init__(self, is_sqlite3, dbname): - self.is_sqlite3 = is_sqlite3 - self.dbname = dbname - self.TRUE = "TRUE" - self.FALSE = "FALSE" - # SQLite prior to version 3.23 does not support TRUE and FALSE - if self.is_sqlite3: - self.TRUE = "1" - self.FALSE = "0" - - def Open(self, connection_name): - dbname = self.dbname - if self.is_sqlite3: - db = QSqlDatabase.addDatabase("QSQLITE", connection_name) - else: - db = QSqlDatabase.addDatabase("QPSQL", connection_name) - opts = dbname.split() - for opt in opts: - if "=" in opt: - opt = opt.split("=") - if opt[0] == "hostname": - db.setHostName(opt[1]) - elif opt[0] == "port": - db.setPort(int(opt[1])) - elif opt[0] == "username": - db.setUserName(opt[1]) - elif opt[0] == "password": - db.setPassword(opt[1]) - elif opt[0] == "dbname": - dbname = opt[1] - else: - dbname = opt - - db.setDatabaseName(dbname) - if not db.open(): - raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) - return db, dbname - -# Main - -def Main(): - usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \ - " or: exported-sql-viewer.py --help-only" - ap = argparse.ArgumentParser(usage = usage_str, add_help = False) - ap.add_argument("--pyside-version-1", action='store_true') - ap.add_argument("dbname", nargs="?") - ap.add_argument("--help-only", action='store_true') - args = ap.parse_args() - - if args.help_only: - app = QApplication(sys.argv) - mainwindow = HelpOnlyWindow() - mainwindow.show() - err = app.exec_() - sys.exit(err) - - dbname = args.dbname - if dbname is None: - ap.print_usage() - print("Too few arguments") - sys.exit(1) - - is_sqlite3 = False - try: - f = open(dbname, "rb") - if f.read(15) == b'SQLite format 3': - is_sqlite3 = True - f.close() - except: - pass - - dbref = DBRef(is_sqlite3, dbname) - db, dbname = dbref.Open("main") - glb = Glb(dbref, db, dbname) - app = QApplication(sys.argv) - glb.app = app - mainwindow = MainWindow(glb) - glb.mainwindow = mainwindow - mainwindow.show() - err = app.exec_() - glb.ShutdownInstances() - db.close() - sys.exit(err) - -if __name__ == "__main__": - Main() |
