5028 lines · python
1#!/usr/bin/env python2# SPDX-License-Identifier: GPL-2.03# exported-sql-viewer.py: view data from sql database4# Copyright (c) 2014-2018, Intel Corporation.5 6# To use this script you will need to have exported data using either the7# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those8# scripts for details.9#10# Following on from the example in the export scripts, a11# call-graph can be displayed for the pt_example database like this:12#13# python tools/perf/scripts/python/exported-sql-viewer.py pt_example14#15# Note that for PostgreSQL, this script supports connecting to remote databases16# by setting hostname, port, username, password, and dbname e.g.17#18# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"19#20# The result is a GUI window with a tree representing a context-sensitive21# call-graph. Expanding a couple of levels of the tree and adjusting column22# widths to suit will display something like:23#24# Call Graph: pt_example25# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)26# v- ls27# v- 2638:263828# v- _start ld-2.19.so 1 10074071 100.0 211135 100.029# |- unknown unknown 1 13198 0.1 1 0.030# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.331# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.332# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.433# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.134# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.035# >- __libc_csu_init ls 1 10354 0.1 10 0.036# |- _setjmp libc-2.19.so 1 0 0.0 4 0.037# v- main ls 1 8182043 99.6 180254 99.938#39# Points to note:40# The top level is a command name (comm)41# The next level is a thread (pid:tid)42# Subsequent levels are functions43# 'Count' is the number of calls44# 'Time' is the elapsed time until the function returns45# Percentages are relative to the level above46# 'Branch Count' is the total number of branches for that function and all47# functions that it calls48 49# There is also a "All branches" report, which displays branches and50# possibly disassembly. However, presently, the only supported disassembler is51# Intel XED, and additionally the object code must be present in perf build ID52# cache. To use Intel XED, libxed.so must be present. To build and install53# libxed.so:54# git clone https://github.com/intelxed/mbuild.git mbuild55# git clone https://github.com/intelxed/xed56# cd xed57# ./mfile.py --share58# sudo ./mfile.py --prefix=/usr/local install59# sudo ldconfig60#61# Example report:62#63# Time CPU Command PID TID Branch Type In Tx Branch64# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)65# 7fab593ea260 48 89 e7 mov %rsp, %rdi66# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])67# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)68# 7fab593ea260 48 89 e7 mov %rsp, %rdi69# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea93070# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)71# 7fab593ea930 55 pushq %rbp72# 7fab593ea931 48 89 e5 mov %rsp, %rbp73# 7fab593ea934 41 57 pushq %r1574# 7fab593ea936 41 56 pushq %r1475# 7fab593ea938 41 55 pushq %r1376# 7fab593ea93a 41 54 pushq %r1277# 7fab593ea93c 53 pushq %rbx78# 7fab593ea93d 48 89 fb mov %rdi, %rbx79# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp80# 7fab593ea944 0f 31 rdtsc81# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx82# 7fab593ea94a 89 c0 mov %eax, %eax83# 7fab593ea94c 48 09 c2 or %rax, %rdx84# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax85# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])86# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)87# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax88# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip)89# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])90 91from __future__ import print_function92 93import sys94# Only change warnings if the python -W option was not used95if not sys.warnoptions:96 import warnings97 # PySide2 causes deprecation warnings, ignore them.98 warnings.filterwarnings("ignore", category=DeprecationWarning)99import argparse100import weakref101import threading102import string103try:104 # Python2105 import cPickle as pickle106 # size of pickled integer big enough for record size107 glb_nsz = 8108except ImportError:109 import pickle110 glb_nsz = 16111import re112import os113import random114import copy115import math116from libxed import LibXED117 118pyside_version_1 = True119if not "--pyside-version-1" in sys.argv:120 try:121 from PySide2.QtCore import *122 from PySide2.QtGui import *123 from PySide2.QtSql import *124 from PySide2.QtWidgets import *125 pyside_version_1 = False126 except:127 pass128 129if pyside_version_1:130 from PySide.QtCore import *131 from PySide.QtGui import *132 from PySide.QtSql import *133 134from decimal import Decimal, ROUND_HALF_UP135from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \136 c_void_p, c_bool, c_byte, c_char, c_int, c_uint, c_longlong, c_ulonglong137from multiprocessing import Process, Array, Value, Event138 139# xrange is range in Python3140try:141 xrange142except NameError:143 xrange = range144 145def printerr(*args, **keyword_args):146 print(*args, file=sys.stderr, **keyword_args)147 148# Data formatting helpers149 150def tohex(ip):151 if ip < 0:152 ip += 1 << 64153 return "%x" % ip154 155def offstr(offset):156 if offset:157 return "+0x%x" % offset158 return ""159 160def dsoname(name):161 if name == "[kernel.kallsyms]":162 return "[kernel]"163 return name164 165def findnth(s, sub, n, offs=0):166 pos = s.find(sub)167 if pos < 0:168 return pos169 if n <= 1:170 return offs + pos171 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)172 173# Percent to one decimal place174 175def PercentToOneDP(n, d):176 if not d:177 return "0.0"178 x = (n * Decimal(100)) / d179 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))180 181# Helper for queries that must not fail182 183def QueryExec(query, stmt):184 ret = query.exec_(stmt)185 if not ret:186 raise Exception("Query failed: " + query.lastError().text())187 188# Background thread189 190class Thread(QThread):191 192 done = Signal(object)193 194 def __init__(self, task, param=None, parent=None):195 super(Thread, self).__init__(parent)196 self.task = task197 self.param = param198 199 def run(self):200 while True:201 if self.param is None:202 done, result = self.task()203 else:204 done, result = self.task(self.param)205 self.done.emit(result)206 if done:207 break208 209# Tree data model210 211class TreeModel(QAbstractItemModel):212 213 def __init__(self, glb, params, parent=None):214 super(TreeModel, self).__init__(parent)215 self.glb = glb216 self.params = params217 self.root = self.GetRoot()218 self.last_row_read = 0219 220 def Item(self, parent):221 if parent.isValid():222 return parent.internalPointer()223 else:224 return self.root225 226 def rowCount(self, parent):227 result = self.Item(parent).childCount()228 if result < 0:229 result = 0230 self.dataChanged.emit(parent, parent)231 return result232 233 def hasChildren(self, parent):234 return self.Item(parent).hasChildren()235 236 def headerData(self, section, orientation, role):237 if role == Qt.TextAlignmentRole:238 return self.columnAlignment(section)239 if role != Qt.DisplayRole:240 return None241 if orientation != Qt.Horizontal:242 return None243 return self.columnHeader(section)244 245 def parent(self, child):246 child_item = child.internalPointer()247 if child_item is self.root:248 return QModelIndex()249 parent_item = child_item.getParentItem()250 return self.createIndex(parent_item.getRow(), 0, parent_item)251 252 def index(self, row, column, parent):253 child_item = self.Item(parent).getChildItem(row)254 return self.createIndex(row, column, child_item)255 256 def DisplayData(self, item, index):257 return item.getData(index.column())258 259 def FetchIfNeeded(self, row):260 if row > self.last_row_read:261 self.last_row_read = row262 if row + 10 >= self.root.child_count:263 self.fetcher.Fetch(glb_chunk_sz)264 265 def columnAlignment(self, column):266 return Qt.AlignLeft267 268 def columnFont(self, column):269 return None270 271 def data(self, index, role):272 if role == Qt.TextAlignmentRole:273 return self.columnAlignment(index.column())274 if role == Qt.FontRole:275 return self.columnFont(index.column())276 if role != Qt.DisplayRole:277 return None278 item = index.internalPointer()279 return self.DisplayData(item, index)280 281# Table data model282 283class TableModel(QAbstractTableModel):284 285 def __init__(self, parent=None):286 super(TableModel, self).__init__(parent)287 self.child_count = 0288 self.child_items = []289 self.last_row_read = 0290 291 def Item(self, parent):292 if parent.isValid():293 return parent.internalPointer()294 else:295 return self296 297 def rowCount(self, parent):298 return self.child_count299 300 def headerData(self, section, orientation, role):301 if role == Qt.TextAlignmentRole:302 return self.columnAlignment(section)303 if role != Qt.DisplayRole:304 return None305 if orientation != Qt.Horizontal:306 return None307 return self.columnHeader(section)308 309 def index(self, row, column, parent):310 return self.createIndex(row, column, self.child_items[row])311 312 def DisplayData(self, item, index):313 return item.getData(index.column())314 315 def FetchIfNeeded(self, row):316 if row > self.last_row_read:317 self.last_row_read = row318 if row + 10 >= self.child_count:319 self.fetcher.Fetch(glb_chunk_sz)320 321 def columnAlignment(self, column):322 return Qt.AlignLeft323 324 def columnFont(self, column):325 return None326 327 def data(self, index, role):328 if role == Qt.TextAlignmentRole:329 return self.columnAlignment(index.column())330 if role == Qt.FontRole:331 return self.columnFont(index.column())332 if role != Qt.DisplayRole:333 return None334 item = index.internalPointer()335 return self.DisplayData(item, index)336 337# Model cache338 339model_cache = weakref.WeakValueDictionary()340model_cache_lock = threading.Lock()341 342def LookupCreateModel(model_name, create_fn):343 model_cache_lock.acquire()344 try:345 model = model_cache[model_name]346 except:347 model = None348 if model is None:349 model = create_fn()350 model_cache[model_name] = model351 model_cache_lock.release()352 return model353 354def LookupModel(model_name):355 model_cache_lock.acquire()356 try:357 model = model_cache[model_name]358 except:359 model = None360 model_cache_lock.release()361 return model362 363# Find bar364 365class FindBar():366 367 def __init__(self, parent, finder, is_reg_expr=False):368 self.finder = finder369 self.context = []370 self.last_value = None371 self.last_pattern = None372 373 label = QLabel("Find:")374 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)375 376 self.textbox = QComboBox()377 self.textbox.setEditable(True)378 self.textbox.currentIndexChanged.connect(self.ValueChanged)379 380 self.progress = QProgressBar()381 self.progress.setRange(0, 0)382 self.progress.hide()383 384 if is_reg_expr:385 self.pattern = QCheckBox("Regular Expression")386 else:387 self.pattern = QCheckBox("Pattern")388 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)389 390 self.next_button = QToolButton()391 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))392 self.next_button.released.connect(lambda: self.NextPrev(1))393 394 self.prev_button = QToolButton()395 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))396 self.prev_button.released.connect(lambda: self.NextPrev(-1))397 398 self.close_button = QToolButton()399 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))400 self.close_button.released.connect(self.Deactivate)401 402 self.hbox = QHBoxLayout()403 self.hbox.setContentsMargins(0, 0, 0, 0)404 405 self.hbox.addWidget(label)406 self.hbox.addWidget(self.textbox)407 self.hbox.addWidget(self.progress)408 self.hbox.addWidget(self.pattern)409 self.hbox.addWidget(self.next_button)410 self.hbox.addWidget(self.prev_button)411 self.hbox.addWidget(self.close_button)412 413 self.bar = QWidget()414 self.bar.setLayout(self.hbox)415 self.bar.hide()416 417 def Widget(self):418 return self.bar419 420 def Activate(self):421 self.bar.show()422 self.textbox.lineEdit().selectAll()423 self.textbox.setFocus()424 425 def Deactivate(self):426 self.bar.hide()427 428 def Busy(self):429 self.textbox.setEnabled(False)430 self.pattern.hide()431 self.next_button.hide()432 self.prev_button.hide()433 self.progress.show()434 435 def Idle(self):436 self.textbox.setEnabled(True)437 self.progress.hide()438 self.pattern.show()439 self.next_button.show()440 self.prev_button.show()441 442 def Find(self, direction):443 value = self.textbox.currentText()444 pattern = self.pattern.isChecked()445 self.last_value = value446 self.last_pattern = pattern447 self.finder.Find(value, direction, pattern, self.context)448 449 def ValueChanged(self):450 value = self.textbox.currentText()451 pattern = self.pattern.isChecked()452 index = self.textbox.currentIndex()453 data = self.textbox.itemData(index)454 # Store the pattern in the combo box to keep it with the text value455 if data == None:456 self.textbox.setItemData(index, pattern)457 else:458 self.pattern.setChecked(data)459 self.Find(0)460 461 def NextPrev(self, direction):462 value = self.textbox.currentText()463 pattern = self.pattern.isChecked()464 if value != self.last_value:465 index = self.textbox.findText(value)466 # Allow for a button press before the value has been added to the combo box467 if index < 0:468 index = self.textbox.count()469 self.textbox.addItem(value, pattern)470 self.textbox.setCurrentIndex(index)471 return472 else:473 self.textbox.setItemData(index, pattern)474 elif pattern != self.last_pattern:475 # Keep the pattern recorded in the combo box up to date476 index = self.textbox.currentIndex()477 self.textbox.setItemData(index, pattern)478 self.Find(direction)479 480 def NotFound(self):481 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")482 483# Context-sensitive call graph data model item base484 485class CallGraphLevelItemBase(object):486 487 def __init__(self, glb, params, row, parent_item):488 self.glb = glb489 self.params = params490 self.row = row491 self.parent_item = parent_item492 self.query_done = False493 self.child_count = 0494 self.child_items = []495 if parent_item:496 self.level = parent_item.level + 1497 else:498 self.level = 0499 500 def getChildItem(self, row):501 return self.child_items[row]502 503 def getParentItem(self):504 return self.parent_item505 506 def getRow(self):507 return self.row508 509 def childCount(self):510 if not self.query_done:511 self.Select()512 if not self.child_count:513 return -1514 return self.child_count515 516 def hasChildren(self):517 if not self.query_done:518 return True519 return self.child_count > 0520 521 def getData(self, column):522 return self.data[column]523 524# Context-sensitive call graph data model level 2+ item base525 526class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):527 528 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):529 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)530 self.comm_id = comm_id531 self.thread_id = thread_id532 self.call_path_id = call_path_id533 self.insn_cnt = insn_cnt534 self.cyc_cnt = cyc_cnt535 self.branch_count = branch_count536 self.time = time537 538 def Select(self):539 self.query_done = True540 query = QSqlQuery(self.glb.db)541 if self.params.have_ipc:542 ipc_str = ", SUM(insn_count), SUM(cyc_count)"543 else:544 ipc_str = ""545 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"546 " FROM calls"547 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"548 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"549 " INNER JOIN dsos ON symbols.dso_id = dsos.id"550 " WHERE parent_call_path_id = " + str(self.call_path_id) +551 " AND comm_id = " + str(self.comm_id) +552 " AND thread_id = " + str(self.thread_id) +553 " GROUP BY call_path_id, name, short_name"554 " ORDER BY call_path_id")555 while query.next():556 if self.params.have_ipc:557 insn_cnt = int(query.value(5))558 cyc_cnt = int(query.value(6))559 branch_count = int(query.value(7))560 else:561 insn_cnt = 0562 cyc_cnt = 0563 branch_count = int(query.value(5))564 child_item = CallGraphLevelThreeItem(self.glb, self.params, self.child_count, self.comm_id, self.thread_id, query.value(0), query.value(1), query.value(2), query.value(3), int(query.value(4)), insn_cnt, cyc_cnt, branch_count, self)565 self.child_items.append(child_item)566 self.child_count += 1567 568# Context-sensitive call graph data model level three item569 570class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):571 572 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item):573 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)574 dso = dsoname(dso)575 if self.params.have_ipc:576 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)577 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)578 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)579 ipc = CalcIPC(cyc_cnt, insn_cnt)580 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]581 else:582 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]583 self.dbid = call_path_id584 585# Context-sensitive call graph data model level two item586 587class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):588 589 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):590 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)591 if self.params.have_ipc:592 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]593 else:594 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]595 self.dbid = thread_id596 597 def Select(self):598 super(CallGraphLevelTwoItem, self).Select()599 for child_item in self.child_items:600 self.time += child_item.time601 self.insn_cnt += child_item.insn_cnt602 self.cyc_cnt += child_item.cyc_cnt603 self.branch_count += child_item.branch_count604 for child_item in self.child_items:605 child_item.data[4] = PercentToOneDP(child_item.time, self.time)606 if self.params.have_ipc:607 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)608 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)609 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)610 else:611 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)612 613# Context-sensitive call graph data model level one item614 615class CallGraphLevelOneItem(CallGraphLevelItemBase):616 617 def __init__(self, glb, params, row, comm_id, comm, parent_item):618 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)619 if self.params.have_ipc:620 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]621 else:622 self.data = [comm, "", "", "", "", "", ""]623 self.dbid = comm_id624 625 def Select(self):626 self.query_done = True627 query = QSqlQuery(self.glb.db)628 QueryExec(query, "SELECT thread_id, pid, tid"629 " FROM comm_threads"630 " INNER JOIN threads ON thread_id = threads.id"631 " WHERE comm_id = " + str(self.dbid))632 while query.next():633 child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)634 self.child_items.append(child_item)635 self.child_count += 1636 637# Context-sensitive call graph data model root item638 639class CallGraphRootItem(CallGraphLevelItemBase):640 641 def __init__(self, glb, params):642 super(CallGraphRootItem, self).__init__(glb, params, 0, None)643 self.dbid = 0644 self.query_done = True645 if_has_calls = ""646 if IsSelectable(glb.db, "comms", columns = "has_calls"):647 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE648 query = QSqlQuery(glb.db)649 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)650 while query.next():651 if not query.value(0):652 continue653 child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)654 self.child_items.append(child_item)655 self.child_count += 1656 657# Call graph model parameters658 659class CallGraphModelParams():660 661 def __init__(self, glb, parent=None):662 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")663 664# Context-sensitive call graph data model base665 666class CallGraphModelBase(TreeModel):667 668 def __init__(self, glb, parent=None):669 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)670 671 def FindSelect(self, value, pattern, query):672 if pattern:673 # postgresql and sqlite pattern patching differences:674 # postgresql LIKE is case sensitive but sqlite LIKE is not675 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not676 # postgresql supports ILIKE which is case insensitive677 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive678 if not self.glb.dbref.is_sqlite3:679 # Escape % and _680 s = value.replace("%", "\\%")681 s = s.replace("_", "\\_")682 # Translate * and ? into SQL LIKE pattern characters % and _683 trans = string.maketrans("*?", "%_")684 match = " LIKE '" + str(s).translate(trans) + "'"685 else:686 match = " GLOB '" + str(value) + "'"687 else:688 match = " = '" + str(value) + "'"689 self.DoFindSelect(query, match)690 691 def Found(self, query, found):692 if found:693 return self.FindPath(query)694 return []695 696 def FindValue(self, value, pattern, query, last_value, last_pattern):697 if last_value == value and pattern == last_pattern:698 found = query.first()699 else:700 self.FindSelect(value, pattern, query)701 found = query.next()702 return self.Found(query, found)703 704 def FindNext(self, query):705 found = query.next()706 if not found:707 found = query.first()708 return self.Found(query, found)709 710 def FindPrev(self, query):711 found = query.previous()712 if not found:713 found = query.last()714 return self.Found(query, found)715 716 def FindThread(self, c):717 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:718 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)719 elif c.direction > 0:720 ids = self.FindNext(c.query)721 else:722 ids = self.FindPrev(c.query)723 return (True, ids)724 725 def Find(self, value, direction, pattern, context, callback):726 class Context():727 def __init__(self, *x):728 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x729 def Update(self, *x):730 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)731 if len(context):732 context[0].Update(value, direction, pattern)733 else:734 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))735 # Use a thread so the UI is not blocked during the SELECT736 thread = Thread(self.FindThread, context[0])737 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)738 thread.start()739 740 def FindDone(self, thread, callback, ids):741 callback(ids)742 743# Context-sensitive call graph data model744 745class CallGraphModel(CallGraphModelBase):746 747 def __init__(self, glb, parent=None):748 super(CallGraphModel, self).__init__(glb, parent)749 750 def GetRoot(self):751 return CallGraphRootItem(self.glb, self.params)752 753 def columnCount(self, parent=None):754 if self.params.have_ipc:755 return 12756 else:757 return 7758 759 def columnHeader(self, column):760 if self.params.have_ipc:761 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]762 else:763 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]764 return headers[column]765 766 def columnAlignment(self, column):767 if self.params.have_ipc:768 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 ]769 else:770 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]771 return alignment[column]772 773 def DoFindSelect(self, query, match):774 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"775 " FROM calls"776 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"777 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"778 " WHERE calls.id <> 0"779 " AND symbols.name" + match +780 " GROUP BY comm_id, thread_id, call_path_id"781 " ORDER BY comm_id, thread_id, call_path_id")782 783 def FindPath(self, query):784 # Turn the query result into a list of ids that the tree view can walk785 # to open the tree at the right place.786 ids = []787 parent_id = query.value(0)788 while parent_id:789 ids.insert(0, parent_id)790 q2 = QSqlQuery(self.glb.db)791 QueryExec(q2, "SELECT parent_id"792 " FROM call_paths"793 " WHERE id = " + str(parent_id))794 if not q2.next():795 break796 parent_id = q2.value(0)797 # The call path root is not used798 if ids[0] == 1:799 del ids[0]800 ids.insert(0, query.value(2))801 ids.insert(0, query.value(1))802 return ids803 804# Call tree data model level 2+ item base805 806class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):807 808 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):809 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)810 self.comm_id = comm_id811 self.thread_id = thread_id812 self.calls_id = calls_id813 self.call_time = call_time814 self.time = time815 self.insn_cnt = insn_cnt816 self.cyc_cnt = cyc_cnt817 self.branch_count = branch_count818 819 def Select(self):820 self.query_done = True821 if self.calls_id == 0:822 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)823 else:824 comm_thread = ""825 if self.params.have_ipc:826 ipc_str = ", insn_count, cyc_count"827 else:828 ipc_str = ""829 query = QSqlQuery(self.glb.db)830 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"831 " FROM calls"832 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"833 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"834 " INNER JOIN dsos ON symbols.dso_id = dsos.id"835 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +836 " ORDER BY call_time, calls.id")837 while query.next():838 if self.params.have_ipc:839 insn_cnt = int(query.value(5))840 cyc_cnt = int(query.value(6))841 branch_count = int(query.value(7))842 else:843 insn_cnt = 0844 cyc_cnt = 0845 branch_count = int(query.value(5))846 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)847 self.child_items.append(child_item)848 self.child_count += 1849 850# Call tree data model level three item851 852class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):853 854 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):855 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item)856 dso = dsoname(dso)857 if self.params.have_ipc:858 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)859 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)860 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)861 ipc = CalcIPC(cyc_cnt, insn_cnt)862 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 ]863 else:864 self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]865 self.dbid = calls_id866 867# Call tree data model level two item868 869class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):870 871 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):872 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item)873 if self.params.have_ipc:874 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]875 else:876 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]877 self.dbid = thread_id878 879 def Select(self):880 super(CallTreeLevelTwoItem, self).Select()881 for child_item in self.child_items:882 self.time += child_item.time883 self.insn_cnt += child_item.insn_cnt884 self.cyc_cnt += child_item.cyc_cnt885 self.branch_count += child_item.branch_count886 for child_item in self.child_items:887 child_item.data[4] = PercentToOneDP(child_item.time, self.time)888 if self.params.have_ipc:889 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)890 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)891 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)892 else:893 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)894 895# Call tree data model level one item896 897class CallTreeLevelOneItem(CallGraphLevelItemBase):898 899 def __init__(self, glb, params, row, comm_id, comm, parent_item):900 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)901 if self.params.have_ipc:902 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]903 else:904 self.data = [comm, "", "", "", "", "", ""]905 self.dbid = comm_id906 907 def Select(self):908 self.query_done = True909 query = QSqlQuery(self.glb.db)910 QueryExec(query, "SELECT thread_id, pid, tid"911 " FROM comm_threads"912 " INNER JOIN threads ON thread_id = threads.id"913 " WHERE comm_id = " + str(self.dbid))914 while query.next():915 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)916 self.child_items.append(child_item)917 self.child_count += 1918 919# Call tree data model root item920 921class CallTreeRootItem(CallGraphLevelItemBase):922 923 def __init__(self, glb, params):924 super(CallTreeRootItem, self).__init__(glb, params, 0, None)925 self.dbid = 0926 self.query_done = True927 if_has_calls = ""928 if IsSelectable(glb.db, "comms", columns = "has_calls"):929 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE930 query = QSqlQuery(glb.db)931 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)932 while query.next():933 if not query.value(0):934 continue935 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)936 self.child_items.append(child_item)937 self.child_count += 1938 939# Call Tree data model940 941class CallTreeModel(CallGraphModelBase):942 943 def __init__(self, glb, parent=None):944 super(CallTreeModel, self).__init__(glb, parent)945 946 def GetRoot(self):947 return CallTreeRootItem(self.glb, self.params)948 949 def columnCount(self, parent=None):950 if self.params.have_ipc:951 return 12952 else:953 return 7954 955 def columnHeader(self, column):956 if self.params.have_ipc:957 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]958 else:959 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]960 return headers[column]961 962 def columnAlignment(self, column):963 if self.params.have_ipc:964 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 ]965 else:966 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]967 return alignment[column]968 969 def DoFindSelect(self, query, match):970 QueryExec(query, "SELECT calls.id, comm_id, thread_id"971 " FROM calls"972 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"973 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"974 " WHERE calls.id <> 0"975 " AND symbols.name" + match +976 " ORDER BY comm_id, thread_id, call_time, calls.id")977 978 def FindPath(self, query):979 # Turn the query result into a list of ids that the tree view can walk980 # to open the tree at the right place.981 ids = []982 parent_id = query.value(0)983 while parent_id:984 ids.insert(0, parent_id)985 q2 = QSqlQuery(self.glb.db)986 QueryExec(q2, "SELECT parent_id"987 " FROM calls"988 " WHERE id = " + str(parent_id))989 if not q2.next():990 break991 parent_id = q2.value(0)992 ids.insert(0, query.value(2))993 ids.insert(0, query.value(1))994 return ids995 996# Vertical layout997 998class HBoxLayout(QHBoxLayout):999 1000 def __init__(self, *children):1001 super(HBoxLayout, self).__init__()1002 1003 self.layout().setContentsMargins(0, 0, 0, 0)1004 for child in children:1005 if child.isWidgetType():1006 self.layout().addWidget(child)1007 else:1008 self.layout().addLayout(child)1009 1010# Horizontal layout1011 1012class VBoxLayout(QVBoxLayout):1013 1014 def __init__(self, *children):1015 super(VBoxLayout, self).__init__()1016 1017 self.layout().setContentsMargins(0, 0, 0, 0)1018 for child in children:1019 if child.isWidgetType():1020 self.layout().addWidget(child)1021 else:1022 self.layout().addLayout(child)1023 1024# Vertical layout widget1025 1026class VBox():1027 1028 def __init__(self, *children):1029 self.vbox = QWidget()1030 self.vbox.setLayout(VBoxLayout(*children))1031 1032 def Widget(self):1033 return self.vbox1034 1035# Tree window base1036 1037class TreeWindowBase(QMdiSubWindow):1038 1039 def __init__(self, parent=None):1040 super(TreeWindowBase, self).__init__(parent)1041 1042 self.model = None1043 self.find_bar = None1044 1045 self.view = QTreeView()1046 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)1047 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard1048 1049 self.context_menu = TreeContextMenu(self.view)1050 1051 def DisplayFound(self, ids):1052 if not len(ids):1053 return False1054 parent = QModelIndex()1055 for dbid in ids:1056 found = False1057 n = self.model.rowCount(parent)1058 for row in xrange(n):1059 child = self.model.index(row, 0, parent)1060 if child.internalPointer().dbid == dbid:1061 found = True1062 self.view.setExpanded(parent, True)1063 self.view.setCurrentIndex(child)1064 parent = child1065 break1066 if not found:1067 break1068 return found1069 1070 def Find(self, value, direction, pattern, context):1071 self.view.setFocus()1072 self.find_bar.Busy()1073 self.model.Find(value, direction, pattern, context, self.FindDone)1074 1075 def FindDone(self, ids):1076 found = True1077 if not self.DisplayFound(ids):1078 found = False1079 self.find_bar.Idle()1080 if not found:1081 self.find_bar.NotFound()1082 1083 1084# Context-sensitive call graph window1085 1086class CallGraphWindow(TreeWindowBase):1087 1088 def __init__(self, glb, parent=None):1089 super(CallGraphWindow, self).__init__(parent)1090 1091 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))1092 1093 self.view.setModel(self.model)1094 1095 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):1096 self.view.setColumnWidth(c, w)1097 1098 self.find_bar = FindBar(self, self)1099 1100 self.vbox = VBox(self.view, self.find_bar.Widget())1101 1102 self.setWidget(self.vbox.Widget())1103 1104 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")1105 1106# Call tree window1107 1108class CallTreeWindow(TreeWindowBase):1109 1110 def __init__(self, glb, parent=None, thread_at_time=None):1111 super(CallTreeWindow, self).__init__(parent)1112 1113 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))1114 1115 self.view.setModel(self.model)1116 1117 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):1118 self.view.setColumnWidth(c, w)1119 1120 self.find_bar = FindBar(self, self)1121 1122 self.vbox = VBox(self.view, self.find_bar.Widget())1123 1124 self.setWidget(self.vbox.Widget())1125 1126 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")1127 1128 if thread_at_time:1129 self.DisplayThreadAtTime(*thread_at_time)1130 1131 def DisplayThreadAtTime(self, comm_id, thread_id, time):1132 parent = QModelIndex()1133 for dbid in (comm_id, thread_id):1134 found = False1135 n = self.model.rowCount(parent)1136 for row in xrange(n):1137 child = self.model.index(row, 0, parent)1138 if child.internalPointer().dbid == dbid:1139 found = True1140 self.view.setExpanded(parent, True)1141 self.view.setCurrentIndex(child)1142 parent = child1143 break1144 if not found:1145 return1146 found = False1147 while True:1148 n = self.model.rowCount(parent)1149 if not n:1150 return1151 last_child = None1152 for row in xrange(n):1153 self.view.setExpanded(parent, True)1154 child = self.model.index(row, 0, parent)1155 child_call_time = child.internalPointer().call_time1156 if child_call_time < time:1157 last_child = child1158 elif child_call_time == time:1159 self.view.setCurrentIndex(child)1160 return1161 elif child_call_time > time:1162 break1163 if not last_child:1164 if not found:1165 child = self.model.index(0, 0, parent)1166 self.view.setExpanded(parent, True)1167 self.view.setCurrentIndex(child)1168 return1169 found = True1170 self.view.setExpanded(parent, True)1171 self.view.setCurrentIndex(last_child)1172 parent = last_child1173 1174# ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name1175 1176def ExecComm(db, thread_id, time):1177 query = QSqlQuery(db)1178 QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"1179 " FROM comm_threads"1180 " INNER JOIN comms ON comms.id = comm_threads.comm_id"1181 " WHERE comm_threads.thread_id = " + str(thread_id) +1182 " ORDER BY comms.c_time, comms.id")1183 first = None1184 last = None1185 while query.next():1186 if first is None:1187 first = query.value(0)1188 if query.value(2) and Decimal(query.value(1)) <= Decimal(time):1189 last = query.value(0)1190 if not(last is None):1191 return last1192 return first1193 1194# Container for (x, y) data1195 1196class XY():1197 def __init__(self, x=0, y=0):1198 self.x = x1199 self.y = y1200 1201 def __str__(self):1202 return "XY({}, {})".format(str(self.x), str(self.y))1203 1204# Container for sub-range data1205 1206class Subrange():1207 def __init__(self, lo=0, hi=0):1208 self.lo = lo1209 self.hi = hi1210 1211 def __str__(self):1212 return "Subrange({}, {})".format(str(self.lo), str(self.hi))1213 1214# Graph data region base class1215 1216class GraphDataRegion(object):1217 1218 def __init__(self, key, title = "", ordinal = ""):1219 self.key = key1220 self.title = title1221 self.ordinal = ordinal1222 1223# Function to sort GraphDataRegion1224 1225def GraphDataRegionOrdinal(data_region):1226 return data_region.ordinal1227 1228# Attributes for a graph region1229 1230class GraphRegionAttribute():1231 1232 def __init__(self, colour):1233 self.colour = colour1234 1235# Switch graph data region represents a task1236 1237class SwitchGraphDataRegion(GraphDataRegion):1238 1239 def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):1240 super(SwitchGraphDataRegion, self).__init__(key)1241 1242 self.title = str(pid) + " / " + str(tid) + " " + comm1243 # Order graph legend within exec comm by pid / tid / time1244 self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)1245 self.exec_comm_id = exec_comm_id1246 self.pid = pid1247 self.tid = tid1248 self.comm = comm1249 self.thread_id = thread_id1250 self.comm_id = comm_id1251 1252# Graph data point1253 1254class GraphDataPoint():1255 1256 def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):1257 self.data = data1258 self.index = index1259 self.x = x1260 self.y = y1261 self.altx = altx1262 self.alty = alty1263 self.hregion = hregion1264 self.vregion = vregion1265 1266# Graph data (single graph) base class1267 1268class GraphData(object):1269 1270 def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):1271 self.collection = collection1272 self.points = []1273 self.xbase = xbase1274 self.ybase = ybase1275 self.title = ""1276 1277 def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):1278 index = len(self.points)1279 1280 x = float(Decimal(x) - self.xbase)1281 y = float(Decimal(y) - self.ybase)1282 1283 self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))1284 1285 def XToData(self, x):1286 return Decimal(x) + self.xbase1287 1288 def YToData(self, y):1289 return Decimal(y) + self.ybase1290 1291# Switch graph data (for one CPU)1292 1293class SwitchGraphData(GraphData):1294 1295 def __init__(self, db, collection, cpu, xbase):1296 super(SwitchGraphData, self).__init__(collection, xbase)1297 1298 self.cpu = cpu1299 self.title = "CPU " + str(cpu)1300 self.SelectSwitches(db)1301 1302 def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):1303 query = QSqlQuery(db)1304 QueryExec(query, "SELECT id, c_time"1305 " FROM comms"1306 " WHERE c_thread_id = " + str(thread_id) +1307 " AND exec_flag = " + self.collection.glb.dbref.TRUE +1308 " AND c_time >= " + str(start_time) +1309 " AND c_time <= " + str(end_time) +1310 " ORDER BY c_time, id")1311 while query.next():1312 comm_id = query.value(0)1313 if comm_id == last_comm_id:1314 continue1315 time = query.value(1)1316 hregion = self.HRegion(db, thread_id, comm_id, time)1317 self.AddPoint(time, 1000, None, None, hregion)1318 1319 def SelectSwitches(self, db):1320 last_time = None1321 last_comm_id = None1322 last_thread_id = None1323 query = QSqlQuery(db)1324 QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"1325 " FROM context_switches"1326 " WHERE machine_id = " + str(self.collection.machine_id) +1327 " AND cpu = " + str(self.cpu) +1328 " ORDER BY time, id")1329 while query.next():1330 flags = int(query.value(5))1331 if flags & 1:1332 # Schedule-out: detect and add exec's1333 if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):1334 self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))1335 continue1336 # Schedule-in: add data point1337 if len(self.points) == 0:1338 start_time = self.collection.glb.StartTime(self.collection.machine_id)1339 hregion = self.HRegion(db, query.value(1), query.value(3), start_time)1340 self.AddPoint(start_time, 1000, None, None, hregion)1341 time = query.value(0)1342 comm_id = query.value(4)1343 thread_id = query.value(2)1344 hregion = self.HRegion(db, thread_id, comm_id, time)1345 self.AddPoint(time, 1000, None, None, hregion)1346 last_time = time1347 last_comm_id = comm_id1348 last_thread_id = thread_id1349 1350 def NewHRegion(self, db, key, thread_id, comm_id, time):1351 exec_comm_id = ExecComm(db, thread_id, time)1352 query = QSqlQuery(db)1353 QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))1354 if query.next():1355 pid = query.value(0)1356 tid = query.value(1)1357 else:1358 pid = -11359 tid = -11360 query = QSqlQuery(db)1361 QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))1362 if query.next():1363 comm = query.value(0)1364 else:1365 comm = ""1366 return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)1367 1368 def HRegion(self, db, thread_id, comm_id, time):1369 key = str(thread_id) + ":" + str(comm_id)1370 hregion = self.collection.LookupHRegion(key)1371 if hregion is None:1372 hregion = self.NewHRegion(db, key, thread_id, comm_id, time)1373 self.collection.AddHRegion(key, hregion)1374 return hregion1375 1376# Graph data collection (multiple related graphs) base class1377 1378class GraphDataCollection(object):1379 1380 def __init__(self, glb):1381 self.glb = glb1382 self.data = []1383 self.hregions = {}1384 self.xrangelo = None1385 self.xrangehi = None1386 self.yrangelo = None1387 self.yrangehi = None1388 self.dp = XY(0, 0)1389 1390 def AddGraphData(self, data):1391 self.data.append(data)1392 1393 def LookupHRegion(self, key):1394 if key in self.hregions:1395 return self.hregions[key]1396 return None1397 1398 def AddHRegion(self, key, hregion):1399 self.hregions[key] = hregion1400 1401# Switch graph data collection (SwitchGraphData for each CPU)1402 1403class SwitchGraphDataCollection(GraphDataCollection):1404 1405 def __init__(self, glb, db, machine_id):1406 super(SwitchGraphDataCollection, self).__init__(glb)1407 1408 self.machine_id = machine_id1409 self.cpus = self.SelectCPUs(db)1410 1411 self.xrangelo = glb.StartTime(machine_id)1412 self.xrangehi = glb.FinishTime(machine_id)1413 1414 self.yrangelo = Decimal(0)1415 self.yrangehi = Decimal(1000)1416 1417 for cpu in self.cpus:1418 self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))1419 1420 def SelectCPUs(self, db):1421 cpus = []1422 query = QSqlQuery(db)1423 QueryExec(query, "SELECT DISTINCT cpu"1424 " FROM context_switches"1425 " WHERE machine_id = " + str(self.machine_id))1426 while query.next():1427 cpus.append(int(query.value(0)))1428 return sorted(cpus)1429 1430# Switch graph data graphics item displays the graphed data1431 1432class SwitchGraphDataGraphicsItem(QGraphicsItem):1433 1434 def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):1435 super(SwitchGraphDataGraphicsItem, self).__init__(parent)1436 1437 self.data = data1438 self.graph_width = graph_width1439 self.graph_height = graph_height1440 self.attrs = attrs1441 self.event_handler = event_handler1442 self.setAcceptHoverEvents(True)1443 1444 def boundingRect(self):1445 return QRectF(0, 0, self.graph_width, self.graph_height)1446 1447 def PaintPoint(self, painter, last, x):1448 if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):1449 if last.x < self.attrs.subrange.x.lo:1450 x0 = self.attrs.subrange.x.lo1451 else:1452 x0 = last.x1453 if x > self.attrs.subrange.x.hi:1454 x1 = self.attrs.subrange.x.hi1455 else:1456 x1 = x - 11457 x0 = self.attrs.XToPixel(x0)1458 x1 = self.attrs.XToPixel(x1)1459 1460 y0 = self.attrs.YToPixel(last.y)1461 1462 colour = self.attrs.region_attributes[last.hregion.key].colour1463 1464 width = x1 - x0 + 11465 if width < 2:1466 painter.setPen(colour)1467 painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)1468 else:1469 painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)1470 1471 def paint(self, painter, option, widget):1472 last = None1473 for point in self.data.points:1474 self.PaintPoint(painter, last, point.x)1475 if point.x > self.attrs.subrange.x.hi:1476 break;1477 last = point1478 self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)1479 1480 def BinarySearchPoint(self, target):1481 lower_pos = 01482 higher_pos = len(self.data.points)1483 while True:1484 pos = int((lower_pos + higher_pos) / 2)1485 val = self.data.points[pos].x1486 if target >= val:1487 lower_pos = pos1488 else:1489 higher_pos = pos1490 if higher_pos <= lower_pos + 1:1491 return lower_pos1492 1493 def XPixelToData(self, x):1494 x = self.attrs.PixelToX(x)1495 if x < self.data.points[0].x:1496 x = 01497 pos = 01498 low = True1499 else:1500 pos = self.BinarySearchPoint(x)1501 low = False1502 return (low, pos, self.data.XToData(x))1503 1504 def EventToData(self, event):1505 no_data = (None,) * 41506 if len(self.data.points) < 1:1507 return no_data1508 x = event.pos().x()1509 if x < 0:1510 return no_data1511 low0, pos0, time_from = self.XPixelToData(x)1512 low1, pos1, time_to = self.XPixelToData(x + 1)1513 hregions = set()1514 hregion_times = []1515 if not low1:1516 for i in xrange(pos0, pos1 + 1):1517 hregion = self.data.points[i].hregion1518 hregions.add(hregion)1519 if i == pos0:1520 time = time_from1521 else:1522 time = self.data.XToData(self.data.points[i].x)1523 hregion_times.append((hregion, time))1524 return (time_from, time_to, hregions, hregion_times)1525 1526 def hoverMoveEvent(self, event):1527 time_from, time_to, hregions, hregion_times = self.EventToData(event)1528 if time_from is not None:1529 self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)1530 1531 def hoverLeaveEvent(self, event):1532 self.event_handler.NoPointEvent()1533 1534 def mousePressEvent(self, event):1535 if event.button() != Qt.RightButton:1536 super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)1537 return1538 time_from, time_to, hregions, hregion_times = self.EventToData(event)1539 if hregion_times:1540 self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())1541 1542# X-axis graphics item1543 1544class XAxisGraphicsItem(QGraphicsItem):1545 1546 def __init__(self, width, parent=None):1547 super(XAxisGraphicsItem, self).__init__(parent)1548 1549 self.width = width1550 self.max_mark_sz = 41551 self.height = self.max_mark_sz + 11552 1553 def boundingRect(self):1554 return QRectF(0, 0, self.width, self.height)1555 1556 def Step(self):1557 attrs = self.parentItem().attrs1558 subrange = attrs.subrange.x1559 t = subrange.hi - subrange.lo1560 s = (3.0 * t) / self.width1561 n = 1.01562 while s > n:1563 n = n * 10.01564 return n1565 1566 def PaintMarks(self, painter, at_y, lo, hi, step, i):1567 attrs = self.parentItem().attrs1568 x = lo1569 while x <= hi:1570 xp = attrs.XToPixel(x)1571 if i % 10:1572 if i % 5:1573 sz = 11574 else:1575 sz = 21576 else:1577 sz = self.max_mark_sz1578 i = 01579 painter.drawLine(xp, at_y, xp, at_y + sz)1580 x += step1581 i += 11582 1583 def paint(self, painter, option, widget):1584 # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -11585 painter.drawLine(0, 0, self.width - 1, 0)1586 n = self.Step()1587 attrs = self.parentItem().attrs1588 subrange = attrs.subrange.x1589 if subrange.lo:1590 x_offset = n - (subrange.lo % n)1591 else:1592 x_offset = 0.01593 x = subrange.lo + x_offset1594 i = (x / n) % 101595 self.PaintMarks(painter, 0, x, subrange.hi, n, i)1596 1597 def ScaleDimensions(self):1598 n = self.Step()1599 attrs = self.parentItem().attrs1600 lo = attrs.subrange.x.lo1601 hi = (n * 10.0) + lo1602 width = attrs.XToPixel(hi)1603 if width > 500:1604 width = 01605 return (n, lo, hi, width)1606 1607 def PaintScale(self, painter, at_x, at_y):1608 n, lo, hi, width = self.ScaleDimensions()1609 if not width:1610 return1611 painter.drawLine(at_x, at_y, at_x + width, at_y)1612 self.PaintMarks(painter, at_y, lo, hi, n, 0)1613 1614 def ScaleWidth(self):1615 n, lo, hi, width = self.ScaleDimensions()1616 return width1617 1618 def ScaleHeight(self):1619 return self.height1620 1621 def ScaleUnit(self):1622 return self.Step() * 101623 1624# Scale graphics item base class1625 1626class ScaleGraphicsItem(QGraphicsItem):1627 1628 def __init__(self, axis, parent=None):1629 super(ScaleGraphicsItem, self).__init__(parent)1630 self.axis = axis1631 1632 def boundingRect(self):1633 scale_width = self.axis.ScaleWidth()1634 if not scale_width:1635 return QRectF()1636 return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())1637 1638 def paint(self, painter, option, widget):1639 scale_width = self.axis.ScaleWidth()1640 if not scale_width:1641 return1642 self.axis.PaintScale(painter, 0, 5)1643 x = scale_width + 41644 painter.drawText(QPointF(x, 10), self.Text())1645 1646 def Unit(self):1647 return self.axis.ScaleUnit()1648 1649 def Text(self):1650 return ""1651 1652# Switch graph scale graphics item1653 1654class SwitchScaleGraphicsItem(ScaleGraphicsItem):1655 1656 def __init__(self, axis, parent=None):1657 super(SwitchScaleGraphicsItem, self).__init__(axis, parent)1658 1659 def Text(self):1660 unit = self.Unit()1661 if unit >= 1000000000:1662 unit = int(unit / 1000000000)1663 us = "s"1664 elif unit >= 1000000:1665 unit = int(unit / 1000000)1666 us = "ms"1667 elif unit >= 1000:1668 unit = int(unit / 1000)1669 us = "us"1670 else:1671 unit = int(unit)1672 us = "ns"1673 return " = " + str(unit) + " " + us1674 1675# Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data1676 1677class SwitchGraphGraphicsItem(QGraphicsItem):1678 1679 def __init__(self, collection, data, attrs, event_handler, first, parent=None):1680 super(SwitchGraphGraphicsItem, self).__init__(parent)1681 self.collection = collection1682 self.data = data1683 self.attrs = attrs1684 self.event_handler = event_handler1685 1686 margin = 201687 title_width = 501688 1689 self.title_graphics = QGraphicsSimpleTextItem(data.title, self)1690 1691 self.title_graphics.setPos(margin, margin)1692 graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 11693 graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 11694 1695 self.graph_origin_x = margin + title_width + margin1696 self.graph_origin_y = graph_height + margin1697 1698 x_axis_size = 11699 y_axis_size = 11700 self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)1701 1702 self.x_axis = XAxisGraphicsItem(graph_width, self)1703 self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)1704 1705 if first:1706 self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)1707 self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)1708 1709 self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)1710 1711 self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)1712 self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)1713 1714 self.width = self.graph_origin_x + graph_width + margin1715 self.height = self.graph_origin_y + margin1716 1717 self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)1718 self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)1719 1720 if parent and 'EnableRubberBand' in dir(parent):1721 parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)1722 1723 def boundingRect(self):1724 return QRectF(0, 0, self.width, self.height)1725 1726 def paint(self, painter, option, widget):1727 pass1728 1729 def RBXToPixel(self, x):1730 return self.attrs.PixelToX(x - self.graph_origin_x)1731 1732 def RBXRangeToPixel(self, x0, x1):1733 return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))1734 1735 def RBPixelToTime(self, x):1736 if x < self.data.points[0].x:1737 return self.data.XToData(0)1738 return self.data.XToData(x)1739 1740 def RBEventTimes(self, x0, x1):1741 x0, x1 = self.RBXRangeToPixel(x0, x1)1742 time_from = self.RBPixelToTime(x0)1743 time_to = self.RBPixelToTime(x1)1744 return (time_from, time_to)1745 1746 def RBEvent(self, x0, x1):1747 time_from, time_to = self.RBEventTimes(x0, x1)1748 self.event_handler.RangeEvent(time_from, time_to)1749 1750 def RBMoveEvent(self, x0, x1):1751 if x1 < x0:1752 x0, x1 = x1, x01753 self.RBEvent(x0, x1)1754 1755 def RBReleaseEvent(self, x0, x1, selection_state):1756 if x1 < x0:1757 x0, x1 = x1, x01758 x0, x1 = self.RBXRangeToPixel(x0, x1)1759 self.event_handler.SelectEvent(x0, x1, selection_state)1760 1761# Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)1762 1763class VerticalBracketGraphicsItem(QGraphicsItem):1764 1765 def __init__(self, parent=None):1766 super(VerticalBracketGraphicsItem, self).__init__(parent)1767 1768 self.width = 01769 self.height = 01770 self.hide()1771 1772 def SetSize(self, width, height):1773 self.width = width + 11774 self.height = height + 11775 1776 def boundingRect(self):1777 return QRectF(0, 0, self.width, self.height)1778 1779 def paint(self, painter, option, widget):1780 colour = QColor(255, 255, 0, 32)1781 painter.fillRect(0, 0, self.width, self.height, colour)1782 x1 = self.width - 11783 y1 = self.height - 11784 painter.drawLine(0, 0, x1, 0)1785 painter.drawLine(0, 0, 0, 3)1786 painter.drawLine(x1, 0, x1, 3)1787 painter.drawLine(0, y1, x1, y1)1788 painter.drawLine(0, y1, 0, y1 - 3)1789 painter.drawLine(x1, y1, x1, y1 - 3)1790 1791# Graphics item to contain graphs arranged vertically1792 1793class VertcalGraphSetGraphicsItem(QGraphicsItem):1794 1795 def __init__(self, collection, attrs, event_handler, child_class, parent=None):1796 super(VertcalGraphSetGraphicsItem, self).__init__(parent)1797 1798 self.collection = collection1799 1800 self.top = 101801 1802 self.width = 01803 self.height = self.top1804 1805 self.rubber_band = None1806 self.rb_enabled = False1807 1808 first = True1809 for data in collection.data:1810 child = child_class(collection, data, attrs, event_handler, first, self)1811 child.setPos(0, self.height + 1)1812 rect = child.boundingRect()1813 if rect.right() > self.width:1814 self.width = rect.right()1815 self.height = self.height + rect.bottom() + 11816 first = False1817 1818 self.bracket = VerticalBracketGraphicsItem(self)1819 1820 def EnableRubberBand(self, xlo, xhi, rb_event_handler):1821 if self.rb_enabled:1822 return1823 self.rb_enabled = True1824 self.rb_in_view = False1825 self.setAcceptedMouseButtons(Qt.LeftButton)1826 self.rb_xlo = xlo1827 self.rb_xhi = xhi1828 self.rb_event_handler = rb_event_handler1829 self.mousePressEvent = self.MousePressEvent1830 self.mouseMoveEvent = self.MouseMoveEvent1831 self.mouseReleaseEvent = self.MouseReleaseEvent1832 1833 def boundingRect(self):1834 return QRectF(0, 0, self.width, self.height)1835 1836 def paint(self, painter, option, widget):1837 pass1838 1839 def RubberBandParent(self):1840 scene = self.scene()1841 view = scene.views()[0]1842 viewport = view.viewport()1843 return viewport1844 1845 def RubberBandSetGeometry(self, rect):1846 scene_rectf = self.mapRectToScene(QRectF(rect))1847 scene = self.scene()1848 view = scene.views()[0]1849 poly = view.mapFromScene(scene_rectf)1850 self.rubber_band.setGeometry(poly.boundingRect())1851 1852 def SetSelection(self, selection_state):1853 if self.rubber_band:1854 if selection_state:1855 self.RubberBandSetGeometry(selection_state)1856 self.rubber_band.show()1857 else:1858 self.rubber_band.hide()1859 1860 def SetBracket(self, rect):1861 if rect:1862 x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()1863 self.bracket.setPos(x, y)1864 self.bracket.SetSize(width, height)1865 self.bracket.show()1866 else:1867 self.bracket.hide()1868 1869 def RubberBandX(self, event):1870 x = event.pos().toPoint().x()1871 if x < self.rb_xlo:1872 x = self.rb_xlo1873 elif x > self.rb_xhi:1874 x = self.rb_xhi1875 else:1876 self.rb_in_view = True1877 return x1878 1879 def RubberBandRect(self, x):1880 if self.rb_origin.x() <= x:1881 width = x - self.rb_origin.x()1882 rect = QRect(self.rb_origin, QSize(width, self.height))1883 else:1884 width = self.rb_origin.x() - x1885 top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())1886 rect = QRect(top_left, QSize(width, self.height))1887 return rect1888 1889 def MousePressEvent(self, event):1890 self.rb_in_view = False1891 x = self.RubberBandX(event)1892 self.rb_origin = QPoint(x, self.top)1893 if self.rubber_band is None:1894 self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())1895 self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))1896 if self.rb_in_view:1897 self.rubber_band.show()1898 self.rb_event_handler.RBMoveEvent(x, x)1899 else:1900 self.rubber_band.hide()1901 1902 def MouseMoveEvent(self, event):1903 x = self.RubberBandX(event)1904 rect = self.RubberBandRect(x)1905 self.RubberBandSetGeometry(rect)1906 if self.rb_in_view:1907 self.rubber_band.show()1908 self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)1909 1910 def MouseReleaseEvent(self, event):1911 x = self.RubberBandX(event)1912 if self.rb_in_view:1913 selection_state = self.RubberBandRect(x)1914 else:1915 selection_state = None1916 self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)1917 1918# Switch graph legend data model1919 1920class SwitchGraphLegendModel(QAbstractTableModel):1921 1922 def __init__(self, collection, region_attributes, parent=None):1923 super(SwitchGraphLegendModel, self).__init__(parent)1924 1925 self.region_attributes = region_attributes1926 1927 self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)1928 self.child_count = len(self.child_items)1929 1930 self.highlight_set = set()1931 1932 self.column_headers = ("pid", "tid", "comm")1933 1934 def rowCount(self, parent):1935 return self.child_count1936 1937 def headerData(self, section, orientation, role):1938 if role != Qt.DisplayRole:1939 return None1940 if orientation != Qt.Horizontal:1941 return None1942 return self.columnHeader(section)1943 1944 def index(self, row, column, parent):1945 return self.createIndex(row, column, self.child_items[row])1946 1947 def columnCount(self, parent=None):1948 return len(self.column_headers)1949 1950 def columnHeader(self, column):1951 return self.column_headers[column]1952 1953 def data(self, index, role):1954 if role == Qt.BackgroundRole:1955 child = self.child_items[index.row()]1956 if child in self.highlight_set:1957 return self.region_attributes[child.key].colour1958 return None1959 if role == Qt.ForegroundRole:1960 child = self.child_items[index.row()]1961 if child in self.highlight_set:1962 return QColor(255, 255, 255)1963 return self.region_attributes[child.key].colour1964 if role != Qt.DisplayRole:1965 return None1966 hregion = self.child_items[index.row()]1967 col = index.column()1968 if col == 0:1969 return hregion.pid1970 if col == 1:1971 return hregion.tid1972 if col == 2:1973 return hregion.comm1974 return None1975 1976 def SetHighlight(self, row, set_highlight):1977 child = self.child_items[row]1978 top_left = self.createIndex(row, 0, child)1979 bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)1980 self.dataChanged.emit(top_left, bottom_right)1981 1982 def Highlight(self, highlight_set):1983 for row in xrange(self.child_count):1984 child = self.child_items[row]1985 if child in self.highlight_set:1986 if child not in highlight_set:1987 self.SetHighlight(row, False)1988 elif child in highlight_set:1989 self.SetHighlight(row, True)1990 self.highlight_set = highlight_set1991 1992# Switch graph legend is a table1993 1994class SwitchGraphLegend(QWidget):1995 1996 def __init__(self, collection, region_attributes, parent=None):1997 super(SwitchGraphLegend, self).__init__(parent)1998 1999 self.data_model = SwitchGraphLegendModel(collection, region_attributes)2000 2001 self.model = QSortFilterProxyModel()2002 self.model.setSourceModel(self.data_model)2003 2004 self.view = QTableView()2005 self.view.setModel(self.model)2006 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)2007 self.view.verticalHeader().setVisible(False)2008 self.view.sortByColumn(-1, Qt.AscendingOrder)2009 self.view.setSortingEnabled(True)2010 self.view.resizeColumnsToContents()2011 self.view.resizeRowsToContents()2012 2013 self.vbox = VBoxLayout(self.view)2014 self.setLayout(self.vbox)2015 2016 sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 22017 sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()2018 self.saved_size = sz12019 2020 def resizeEvent(self, event):2021 self.saved_size = self.size().width()2022 super(SwitchGraphLegend, self).resizeEvent(event)2023 2024 def Highlight(self, highlight_set):2025 self.data_model.Highlight(highlight_set)2026 self.update()2027 2028 def changeEvent(self, event):2029 if event.type() == QEvent.FontChange:2030 self.view.resizeRowsToContents()2031 self.view.resizeColumnsToContents()2032 # Need to resize rows again after column resize2033 self.view.resizeRowsToContents()2034 super(SwitchGraphLegend, self).changeEvent(event)2035 2036# Random colour generation2037 2038def RGBColourTooLight(r, g, b):2039 if g > 230:2040 return True2041 if g <= 160:2042 return False2043 if r <= 180 and g <= 180:2044 return False2045 if r < 60:2046 return False2047 return True2048 2049def GenerateColours(x):2050 cs = [0]2051 for i in xrange(1, x):2052 cs.append(int((255.0 / i) + 0.5))2053 colours = []2054 for r in cs:2055 for g in cs:2056 for b in cs:2057 # Exclude black and colours that look too light against a white background2058 if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):2059 continue2060 colours.append(QColor(r, g, b))2061 return colours2062 2063def GenerateNColours(n):2064 for x in xrange(2, n + 2):2065 colours = GenerateColours(x)2066 if len(colours) >= n:2067 return colours2068 return []2069 2070def GenerateNRandomColours(n, seed):2071 colours = GenerateNColours(n)2072 random.seed(seed)2073 random.shuffle(colours)2074 return colours2075 2076# Graph attributes, in particular the scale and subrange that change when zooming2077 2078class GraphAttributes():2079 2080 def __init__(self, scale, subrange, region_attributes, dp):2081 self.scale = scale2082 self.subrange = subrange2083 self.region_attributes = region_attributes2084 # Rounding avoids errors due to finite floating point precision2085 self.dp = dp # data decimal places2086 self.Update()2087 2088 def XToPixel(self, x):2089 return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))2090 2091 def YToPixel(self, y):2092 return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))2093 2094 def PixelToXRounded(self, px):2095 return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo2096 2097 def PixelToYRounded(self, py):2098 return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo2099 2100 def PixelToX(self, px):2101 x = self.PixelToXRounded(px)2102 if self.pdp.x == 0:2103 rt = self.XToPixel(x)2104 if rt > px:2105 return x - 12106 return x2107 2108 def PixelToY(self, py):2109 y = self.PixelToYRounded(py)2110 if self.pdp.y == 0:2111 rt = self.YToPixel(y)2112 if rt > py:2113 return y - 12114 return y2115 2116 def ToPDP(self, dp, scale):2117 # Calculate pixel decimal places:2118 # (10 ** dp) is the minimum delta in the data2119 # scale it to get the minimum delta in pixels2120 # log10 gives the number of decimals places negatively2121 # subtrace 1 to divide by 102122 # round to the lower negative number2123 # change the sign to get the number of decimals positively2124 x = math.log10((10 ** dp) * scale)2125 if x < 0:2126 x -= 12127 x = -int(math.floor(x) - 0.1)2128 else:2129 x = 02130 return x2131 2132 def Update(self):2133 x = self.ToPDP(self.dp.x, self.scale.x)2134 y = self.ToPDP(self.dp.y, self.scale.y)2135 self.pdp = XY(x, y) # pixel decimal places2136 2137# Switch graph splitter which divides the CPU graphs from the legend2138 2139class SwitchGraphSplitter(QSplitter):2140 2141 def __init__(self, parent=None):2142 super(SwitchGraphSplitter, self).__init__(parent)2143 2144 self.first_time = False2145 2146 def resizeEvent(self, ev):2147 if self.first_time:2148 self.first_time = False2149 sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 22150 sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()2151 sz0 = self.size().width() - self.handleWidth() - sz12152 self.setSizes([sz0, sz1])2153 elif not(self.widget(1).saved_size is None):2154 sz1 = self.widget(1).saved_size2155 sz0 = self.size().width() - self.handleWidth() - sz12156 self.setSizes([sz0, sz1])2157 super(SwitchGraphSplitter, self).resizeEvent(ev)2158 2159# Graph widget base class2160 2161class GraphWidget(QWidget):2162 2163 graph_title_changed = Signal(object)2164 2165 def __init__(self, parent=None):2166 super(GraphWidget, self).__init__(parent)2167 2168 def GraphTitleChanged(self, title):2169 self.graph_title_changed.emit(title)2170 2171 def Title(self):2172 return ""2173 2174# Display time in s, ms, us or ns2175 2176def ToTimeStr(val):2177 val = Decimal(val)2178 if val >= 1000000000:2179 return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))2180 if val >= 1000000:2181 return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))2182 if val >= 1000:2183 return "{} us".format((val / 1000).quantize(Decimal("0.001")))2184 return "{} ns".format(val.quantize(Decimal("1")))2185 2186# Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons2187 2188class SwitchGraphWidget(GraphWidget):2189 2190 def __init__(self, glb, collection, parent=None):2191 super(SwitchGraphWidget, self).__init__(parent)2192 2193 self.glb = glb2194 self.collection = collection2195 2196 self.back_state = []2197 self.forward_state = []2198 self.selection_state = (None, None)2199 self.fwd_rect = None2200 self.start_time = self.glb.StartTime(collection.machine_id)2201 2202 i = 02203 hregions = collection.hregions.values()2204 colours = GenerateNRandomColours(len(hregions), 1013)2205 region_attributes = {}2206 for hregion in hregions:2207 if hregion.pid == 0 and hregion.tid == 0:2208 region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))2209 else:2210 region_attributes[hregion.key] = GraphRegionAttribute(colours[i])2211 i = i + 12212 2213 # Default to entire range2214 xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)2215 ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)2216 subrange = XY(xsubrange, ysubrange)2217 2218 scale = self.GetScaleForRange(subrange)2219 2220 self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)2221 2222 self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)2223 2224 self.scene = QGraphicsScene()2225 self.scene.addItem(self.item)2226 2227 self.view = QGraphicsView(self.scene)2228 self.view.centerOn(0, 0)2229 self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)2230 2231 self.legend = SwitchGraphLegend(collection, region_attributes)2232 2233 self.splitter = SwitchGraphSplitter()2234 self.splitter.addWidget(self.view)2235 self.splitter.addWidget(self.legend)2236 2237 self.point_label = QLabel("")2238 self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)2239 2240 self.back_button = QToolButton()2241 self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))2242 self.back_button.setDisabled(True)2243 self.back_button.released.connect(lambda: self.Back())2244 2245 self.forward_button = QToolButton()2246 self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))2247 self.forward_button.setDisabled(True)2248 self.forward_button.released.connect(lambda: self.Forward())2249 2250 self.zoom_button = QToolButton()2251 self.zoom_button.setText("Zoom")2252 self.zoom_button.setDisabled(True)2253 self.zoom_button.released.connect(lambda: self.Zoom())2254 2255 self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)2256 2257 self.vbox = VBoxLayout(self.splitter, self.hbox)2258 2259 self.setLayout(self.vbox)2260 2261 def GetScaleForRangeX(self, xsubrange):2262 # Default graph 1000 pixels wide2263 dflt = 1000.02264 r = xsubrange.hi - xsubrange.lo2265 return dflt / r2266 2267 def GetScaleForRangeY(self, ysubrange):2268 # Default graph 50 pixels high2269 dflt = 50.02270 r = ysubrange.hi - ysubrange.lo2271 return dflt / r2272 2273 def GetScaleForRange(self, subrange):2274 # Default graph 1000 pixels wide, 50 pixels high2275 xscale = self.GetScaleForRangeX(subrange.x)2276 yscale = self.GetScaleForRangeY(subrange.y)2277 return XY(xscale, yscale)2278 2279 def PointEvent(self, cpu, time_from, time_to, hregions):2280 text = "CPU: " + str(cpu)2281 time_from = time_from.quantize(Decimal(1))2282 rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)2283 text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"2284 self.point_label.setText(text)2285 self.legend.Highlight(hregions)2286 2287 def RightClickEvent(self, cpu, hregion_times, pos):2288 if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):2289 return2290 menu = QMenu(self.view)2291 for hregion, time in hregion_times:2292 thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)2293 menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)2294 menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))2295 menu.exec_(pos)2296 2297 def RightClickSelect(self, args):2298 CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)2299 2300 def NoPointEvent(self):2301 self.point_label.setText("")2302 self.legend.Highlight({})2303 2304 def RangeEvent(self, time_from, time_to):2305 time_from = time_from.quantize(Decimal(1))2306 time_to = time_to.quantize(Decimal(1))2307 if time_to <= time_from:2308 self.point_label.setText("")2309 return2310 rel_time_from = time_from - self.start_time2311 rel_time_to = time_to - self.start_time2312 text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"2313 text = text + " duration: " + ToTimeStr(time_to - time_from)2314 self.point_label.setText(text)2315 2316 def BackState(self):2317 return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)2318 2319 def PushBackState(self):2320 state = copy.deepcopy(self.BackState())2321 self.back_state.append(state)2322 self.back_button.setEnabled(True)2323 2324 def PopBackState(self):2325 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()2326 self.attrs.Update()2327 if not self.back_state:2328 self.back_button.setDisabled(True)2329 2330 def PushForwardState(self):2331 state = copy.deepcopy(self.BackState())2332 self.forward_state.append(state)2333 self.forward_button.setEnabled(True)2334 2335 def PopForwardState(self):2336 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()2337 self.attrs.Update()2338 if not self.forward_state:2339 self.forward_button.setDisabled(True)2340 2341 def Title(self):2342 time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)2343 time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)2344 rel_time_from = time_from - self.start_time2345 rel_time_to = time_to - self.start_time2346 title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)2347 title = title + " (" + ToTimeStr(time_to - time_from) + ")"2348 return title2349 2350 def Update(self):2351 selected_subrange, selection_state = self.selection_state2352 self.item.SetSelection(selection_state)2353 self.item.SetBracket(self.fwd_rect)2354 self.zoom_button.setDisabled(selected_subrange is None)2355 self.GraphTitleChanged(self.Title())2356 self.item.update(self.item.boundingRect())2357 2358 def Back(self):2359 if not self.back_state:2360 return2361 self.PushForwardState()2362 self.PopBackState()2363 self.Update()2364 2365 def Forward(self):2366 if not self.forward_state:2367 return2368 self.PushBackState()2369 self.PopForwardState()2370 self.Update()2371 2372 def SelectEvent(self, x0, x1, selection_state):2373 if selection_state is None:2374 selected_subrange = None2375 else:2376 if x1 - x0 < 1.0:2377 x1 += 1.02378 selected_subrange = Subrange(x0, x1)2379 self.selection_state = (selected_subrange, selection_state)2380 self.zoom_button.setDisabled(selected_subrange is None)2381 2382 def Zoom(self):2383 selected_subrange, selection_state = self.selection_state2384 if selected_subrange is None:2385 return2386 self.fwd_rect = selection_state2387 self.item.SetSelection(None)2388 self.PushBackState()2389 self.attrs.subrange.x = selected_subrange2390 self.forward_state = []2391 self.forward_button.setDisabled(True)2392 self.selection_state = (None, None)2393 self.fwd_rect = None2394 self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)2395 self.attrs.Update()2396 self.Update()2397 2398# Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting2399 2400class SlowInitClass():2401 2402 def __init__(self, glb, title, init_fn):2403 self.init_fn = init_fn2404 self.done = False2405 self.result = None2406 2407 self.msg_box = QMessageBox(glb.mainwindow)2408 self.msg_box.setText("Initializing " + title + ". Please wait.")2409 self.msg_box.setWindowTitle("Initializing " + title)2410 self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))2411 2412 self.init_thread = Thread(self.ThreadFn, glb)2413 self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)2414 2415 self.init_thread.start()2416 2417 def Done(self):2418 self.msg_box.done(0)2419 2420 def ThreadFn(self, glb):2421 conn_name = "SlowInitClass" + str(os.getpid())2422 db, dbname = glb.dbref.Open(conn_name)2423 self.result = self.init_fn(db)2424 self.done = True2425 return (True, 0)2426 2427 def Result(self):2428 while not self.done:2429 self.msg_box.exec_()2430 self.init_thread.wait()2431 return self.result2432 2433def SlowInit(glb, title, init_fn):2434 init = SlowInitClass(glb, title, init_fn)2435 return init.Result()2436 2437# Time chart by CPU window2438 2439class TimeChartByCPUWindow(QMdiSubWindow):2440 2441 def __init__(self, glb, parent=None):2442 super(TimeChartByCPUWindow, self).__init__(parent)2443 2444 self.glb = glb2445 self.machine_id = glb.HostMachineId()2446 self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)2447 2448 collection = LookupModel(self.collection_name)2449 if collection is None:2450 collection = SlowInit(glb, "Time Chart", self.Init)2451 2452 self.widget = SwitchGraphWidget(glb, collection, self)2453 self.view = self.widget2454 2455 self.base_title = "Time Chart by CPU"2456 self.setWindowTitle(self.base_title + self.widget.Title())2457 self.widget.graph_title_changed.connect(self.GraphTitleChanged)2458 2459 self.setWidget(self.widget)2460 2461 AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())2462 2463 def Init(self, db):2464 return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))2465 2466 def GraphTitleChanged(self, title):2467 self.setWindowTitle(self.base_title + " : " + title)2468 2469# Child data item finder2470 2471class ChildDataItemFinder():2472 2473 def __init__(self, root):2474 self.root = root2475 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 52476 self.rows = []2477 self.pos = 02478 2479 def FindSelect(self):2480 self.rows = []2481 if self.pattern:2482 pattern = re.compile(self.value)2483 for child in self.root.child_items:2484 for column_data in child.data:2485 if re.search(pattern, str(column_data)) is not None:2486 self.rows.append(child.row)2487 break2488 else:2489 for child in self.root.child_items:2490 for column_data in child.data:2491 if self.value in str(column_data):2492 self.rows.append(child.row)2493 break2494 2495 def FindValue(self):2496 self.pos = 02497 if self.last_value != self.value or self.pattern != self.last_pattern:2498 self.FindSelect()2499 if not len(self.rows):2500 return -12501 return self.rows[self.pos]2502 2503 def FindThread(self):2504 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:2505 row = self.FindValue()2506 elif len(self.rows):2507 if self.direction > 0:2508 self.pos += 12509 if self.pos >= len(self.rows):2510 self.pos = 02511 else:2512 self.pos -= 12513 if self.pos < 0:2514 self.pos = len(self.rows) - 12515 row = self.rows[self.pos]2516 else:2517 row = -12518 return (True, row)2519 2520 def Find(self, value, direction, pattern, context, callback):2521 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)2522 # Use a thread so the UI is not blocked2523 thread = Thread(self.FindThread)2524 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)2525 thread.start()2526 2527 def FindDone(self, thread, callback, row):2528 callback(row)2529 2530# Number of database records to fetch in one go2531 2532glb_chunk_sz = 100002533 2534# Background process for SQL data fetcher2535 2536class SQLFetcherProcess():2537 2538 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):2539 # Need a unique connection name2540 conn_name = "SQLFetcher" + str(os.getpid())2541 self.db, dbname = dbref.Open(conn_name)2542 self.sql = sql2543 self.buffer = buffer2544 self.head = head2545 self.tail = tail2546 self.fetch_count = fetch_count2547 self.fetching_done = fetching_done2548 self.process_target = process_target2549 self.wait_event = wait_event2550 self.fetched_event = fetched_event2551 self.prep = prep2552 self.query = QSqlQuery(self.db)2553 self.query_limit = 0 if "$$last_id$$" in sql else 22554 self.last_id = -12555 self.fetched = 02556 self.more = True2557 self.local_head = self.head.value2558 self.local_tail = self.tail.value2559 2560 def Select(self):2561 if self.query_limit:2562 if self.query_limit == 1:2563 return2564 self.query_limit -= 12565 stmt = self.sql.replace("$$last_id$$", str(self.last_id))2566 QueryExec(self.query, stmt)2567 2568 def Next(self):2569 if not self.query.next():2570 self.Select()2571 if not self.query.next():2572 return None2573 self.last_id = self.query.value(0)2574 return self.prep(self.query)2575 2576 def WaitForTarget(self):2577 while True:2578 self.wait_event.clear()2579 target = self.process_target.value2580 if target > self.fetched or target < 0:2581 break2582 self.wait_event.wait()2583 return target2584 2585 def HasSpace(self, sz):2586 if self.local_tail <= self.local_head:2587 space = len(self.buffer) - self.local_head2588 if space > sz:2589 return True2590 if space >= glb_nsz:2591 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer2592 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)2593 self.buffer[self.local_head : self.local_head + len(nd)] = nd2594 self.local_head = 02595 if self.local_tail - self.local_head > sz:2596 return True2597 return False2598 2599 def WaitForSpace(self, sz):2600 if self.HasSpace(sz):2601 return2602 while True:2603 self.wait_event.clear()2604 self.local_tail = self.tail.value2605 if self.HasSpace(sz):2606 return2607 self.wait_event.wait()2608 2609 def AddToBuffer(self, obj):2610 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)2611 n = len(d)2612 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)2613 sz = n + glb_nsz2614 self.WaitForSpace(sz)2615 pos = self.local_head2616 self.buffer[pos : pos + len(nd)] = nd2617 self.buffer[pos + glb_nsz : pos + sz] = d2618 self.local_head += sz2619 2620 def FetchBatch(self, batch_size):2621 fetched = 02622 while batch_size > fetched:2623 obj = self.Next()2624 if obj is None:2625 self.more = False2626 break2627 self.AddToBuffer(obj)2628 fetched += 12629 if fetched:2630 self.fetched += fetched2631 with self.fetch_count.get_lock():2632 self.fetch_count.value += fetched2633 self.head.value = self.local_head2634 self.fetched_event.set()2635 2636 def Run(self):2637 while self.more:2638 target = self.WaitForTarget()2639 if target < 0:2640 break2641 batch_size = min(glb_chunk_sz, target - self.fetched)2642 self.FetchBatch(batch_size)2643 self.fetching_done.value = True2644 self.fetched_event.set()2645 2646def SQLFetcherFn(*x):2647 process = SQLFetcherProcess(*x)2648 process.Run()2649 2650# SQL data fetcher2651 2652class SQLFetcher(QObject):2653 2654 done = Signal(object)2655 2656 def __init__(self, glb, sql, prep, process_data, parent=None):2657 super(SQLFetcher, self).__init__(parent)2658 self.process_data = process_data2659 self.more = True2660 self.target = 02661 self.last_target = 02662 self.fetched = 02663 self.buffer_size = 16 * 1024 * 10242664 self.buffer = Array(c_char, self.buffer_size, lock=False)2665 self.head = Value(c_longlong)2666 self.tail = Value(c_longlong)2667 self.local_tail = 02668 self.fetch_count = Value(c_longlong)2669 self.fetching_done = Value(c_bool)2670 self.last_count = 02671 self.process_target = Value(c_longlong)2672 self.wait_event = Event()2673 self.fetched_event = Event()2674 glb.AddInstanceToShutdownOnExit(self)2675 self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep))2676 self.process.start()2677 self.thread = Thread(self.Thread)2678 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)2679 self.thread.start()2680 2681 def Shutdown(self):2682 # Tell the thread and process to exit2683 self.process_target.value = -12684 self.wait_event.set()2685 self.more = False2686 self.fetching_done.value = True2687 self.fetched_event.set()2688 2689 def Thread(self):2690 if not self.more:2691 return True, 02692 while True:2693 self.fetched_event.clear()2694 fetch_count = self.fetch_count.value2695 if fetch_count != self.last_count:2696 break2697 if self.fetching_done.value:2698 self.more = False2699 return True, 02700 self.fetched_event.wait()2701 count = fetch_count - self.last_count2702 self.last_count = fetch_count2703 self.fetched += count2704 return False, count2705 2706 def Fetch(self, nr):2707 if not self.more:2708 # -1 inidcates there are no more2709 return -12710 result = self.fetched2711 extra = result + nr - self.target2712 if extra > 0:2713 self.target += extra2714 # process_target < 0 indicates shutting down2715 if self.process_target.value >= 0:2716 self.process_target.value = self.target2717 self.wait_event.set()2718 return result2719 2720 def RemoveFromBuffer(self):2721 pos = self.local_tail2722 if len(self.buffer) - pos < glb_nsz:2723 pos = 02724 n = pickle.loads(self.buffer[pos : pos + glb_nsz])2725 if n == 0:2726 pos = 02727 n = pickle.loads(self.buffer[0 : glb_nsz])2728 pos += glb_nsz2729 obj = pickle.loads(self.buffer[pos : pos + n])2730 self.local_tail = pos + n2731 return obj2732 2733 def ProcessData(self, count):2734 for i in xrange(count):2735 obj = self.RemoveFromBuffer()2736 self.process_data(obj)2737 self.tail.value = self.local_tail2738 self.wait_event.set()2739 self.done.emit(count)2740 2741# Fetch more records bar2742 2743class FetchMoreRecordsBar():2744 2745 def __init__(self, model, parent):2746 self.model = model2747 2748 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")2749 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)2750 2751 self.fetch_count = QSpinBox()2752 self.fetch_count.setRange(1, 1000000)2753 self.fetch_count.setValue(10)2754 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)2755 2756 self.fetch = QPushButton("Go!")2757 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)2758 self.fetch.released.connect(self.FetchMoreRecords)2759 2760 self.progress = QProgressBar()2761 self.progress.setRange(0, 100)2762 self.progress.hide()2763 2764 self.done_label = QLabel("All records fetched")2765 self.done_label.hide()2766 2767 self.spacer = QLabel("")2768 2769 self.close_button = QToolButton()2770 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))2771 self.close_button.released.connect(self.Deactivate)2772 2773 self.hbox = QHBoxLayout()2774 self.hbox.setContentsMargins(0, 0, 0, 0)2775 2776 self.hbox.addWidget(self.label)2777 self.hbox.addWidget(self.fetch_count)2778 self.hbox.addWidget(self.fetch)2779 self.hbox.addWidget(self.spacer)2780 self.hbox.addWidget(self.progress)2781 self.hbox.addWidget(self.done_label)2782 self.hbox.addWidget(self.close_button)2783 2784 self.bar = QWidget()2785 self.bar.setLayout(self.hbox)2786 self.bar.show()2787 2788 self.in_progress = False2789 self.model.progress.connect(self.Progress)2790 2791 self.done = False2792 2793 if not model.HasMoreRecords():2794 self.Done()2795 2796 def Widget(self):2797 return self.bar2798 2799 def Activate(self):2800 self.bar.show()2801 self.fetch.setFocus()2802 2803 def Deactivate(self):2804 self.bar.hide()2805 2806 def Enable(self, enable):2807 self.fetch.setEnabled(enable)2808 self.fetch_count.setEnabled(enable)2809 2810 def Busy(self):2811 self.Enable(False)2812 self.fetch.hide()2813 self.spacer.hide()2814 self.progress.show()2815 2816 def Idle(self):2817 self.in_progress = False2818 self.Enable(True)2819 self.progress.hide()2820 self.fetch.show()2821 self.spacer.show()2822 2823 def Target(self):2824 return self.fetch_count.value() * glb_chunk_sz2825 2826 def Done(self):2827 self.done = True2828 self.Idle()2829 self.label.hide()2830 self.fetch_count.hide()2831 self.fetch.hide()2832 self.spacer.hide()2833 self.done_label.show()2834 2835 def Progress(self, count):2836 if self.in_progress:2837 if count:2838 percent = ((count - self.start) * 100) / self.Target()2839 if percent >= 100:2840 self.Idle()2841 else:2842 self.progress.setValue(percent)2843 if not count:2844 # Count value of zero means no more records2845 self.Done()2846 2847 def FetchMoreRecords(self):2848 if self.done:2849 return2850 self.progress.setValue(0)2851 self.Busy()2852 self.in_progress = True2853 self.start = self.model.FetchMoreRecords(self.Target())2854 2855# Brance data model level two item2856 2857class BranchLevelTwoItem():2858 2859 def __init__(self, row, col, text, parent_item):2860 self.row = row2861 self.parent_item = parent_item2862 self.data = [""] * (col + 1)2863 self.data[col] = text2864 self.level = 22865 2866 def getParentItem(self):2867 return self.parent_item2868 2869 def getRow(self):2870 return self.row2871 2872 def childCount(self):2873 return 02874 2875 def hasChildren(self):2876 return False2877 2878 def getData(self, column):2879 return self.data[column]2880 2881# Brance data model level one item2882 2883class BranchLevelOneItem():2884 2885 def __init__(self, glb, row, data, parent_item):2886 self.glb = glb2887 self.row = row2888 self.parent_item = parent_item2889 self.child_count = 02890 self.child_items = []2891 self.data = data[1:]2892 self.dbid = data[0]2893 self.level = 12894 self.query_done = False2895 self.br_col = len(self.data) - 12896 2897 def getChildItem(self, row):2898 return self.child_items[row]2899 2900 def getParentItem(self):2901 return self.parent_item2902 2903 def getRow(self):2904 return self.row2905 2906 def Select(self):2907 self.query_done = True2908 2909 if not self.glb.have_disassembler:2910 return2911 2912 query = QSqlQuery(self.glb.db)2913 2914 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"2915 " FROM samples"2916 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"2917 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"2918 " WHERE samples.id = " + str(self.dbid))2919 if not query.next():2920 return2921 cpu = query.value(0)2922 dso = query.value(1)2923 sym = query.value(2)2924 if dso == 0 or sym == 0:2925 return2926 off = query.value(3)2927 short_name = query.value(4)2928 long_name = query.value(5)2929 build_id = query.value(6)2930 sym_start = query.value(7)2931 ip = query.value(8)2932 2933 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"2934 " FROM samples"2935 " INNER JOIN symbols ON samples.symbol_id = symbols.id"2936 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +2937 " ORDER BY samples.id"2938 " LIMIT 1")2939 if not query.next():2940 return2941 if query.value(0) != dso:2942 # Cannot disassemble from one dso to another2943 return2944 bsym = query.value(1)2945 boff = query.value(2)2946 bsym_start = query.value(3)2947 if bsym == 0:2948 return2949 tot = bsym_start + boff + 1 - sym_start - off2950 if tot <= 0 or tot > 16384:2951 return2952 2953 inst = self.glb.disassembler.Instruction()2954 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)2955 if not f:2956 return2957 mode = 0 if Is64Bit(f) else 12958 self.glb.disassembler.SetMode(inst, mode)2959 2960 buf_sz = tot + 162961 buf = create_string_buffer(tot + 16)2962 f.seek(sym_start + off)2963 buf.value = f.read(buf_sz)2964 buf_ptr = addressof(buf)2965 i = 02966 while tot > 0:2967 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)2968 if cnt:2969 byte_str = tohex(ip).rjust(16)2970 for k in xrange(cnt):2971 byte_str += " %02x" % ord(buf[i])2972 i += 12973 while k < 15:2974 byte_str += " "2975 k += 12976 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))2977 self.child_count += 12978 else:2979 return2980 buf_ptr += cnt2981 tot -= cnt2982 buf_sz -= cnt2983 ip += cnt2984 2985 def childCount(self):2986 if not self.query_done:2987 self.Select()2988 if not self.child_count:2989 return -12990 return self.child_count2991 2992 def hasChildren(self):2993 if not self.query_done:2994 return True2995 return self.child_count > 02996 2997 def getData(self, column):2998 return self.data[column]2999 3000# Brance data model root item3001 3002class BranchRootItem():3003 3004 def __init__(self):3005 self.child_count = 03006 self.child_items = []3007 self.level = 03008 3009 def getChildItem(self, row):3010 return self.child_items[row]3011 3012 def getParentItem(self):3013 return None3014 3015 def getRow(self):3016 return 03017 3018 def childCount(self):3019 return self.child_count3020 3021 def hasChildren(self):3022 return self.child_count > 03023 3024 def getData(self, column):3025 return ""3026 3027# Calculate instructions per cycle3028 3029def CalcIPC(cyc_cnt, insn_cnt):3030 if cyc_cnt and insn_cnt:3031 ipc = Decimal(float(insn_cnt) / cyc_cnt)3032 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))3033 else:3034 ipc = "0"3035 return ipc3036 3037# Branch data preparation3038 3039def BranchDataPrepBr(query, data):3040 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +3041 " (" + dsoname(query.value(11)) + ")" + " -> " +3042 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +3043 " (" + dsoname(query.value(15)) + ")")3044 3045def BranchDataPrepIPC(query, data):3046 insn_cnt = query.value(16)3047 cyc_cnt = query.value(17)3048 ipc = CalcIPC(cyc_cnt, insn_cnt)3049 data.append(insn_cnt)3050 data.append(cyc_cnt)3051 data.append(ipc)3052 3053def BranchDataPrep(query):3054 data = []3055 for i in xrange(0, 8):3056 data.append(query.value(i))3057 BranchDataPrepBr(query, data)3058 return data3059 3060def BranchDataPrepWA(query):3061 data = []3062 data.append(query.value(0))3063 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string3064 data.append("{:>19}".format(query.value(1)))3065 for i in xrange(2, 8):3066 data.append(query.value(i))3067 BranchDataPrepBr(query, data)3068 return data3069 3070def BranchDataWithIPCPrep(query):3071 data = []3072 for i in xrange(0, 8):3073 data.append(query.value(i))3074 BranchDataPrepIPC(query, data)3075 BranchDataPrepBr(query, data)3076 return data3077 3078def BranchDataWithIPCPrepWA(query):3079 data = []3080 data.append(query.value(0))3081 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string3082 data.append("{:>19}".format(query.value(1)))3083 for i in xrange(2, 8):3084 data.append(query.value(i))3085 BranchDataPrepIPC(query, data)3086 BranchDataPrepBr(query, data)3087 return data3088 3089# Branch data model3090 3091class BranchModel(TreeModel):3092 3093 progress = Signal(object)3094 3095 def __init__(self, glb, event_id, where_clause, parent=None):3096 super(BranchModel, self).__init__(glb, None, parent)3097 self.event_id = event_id3098 self.more = True3099 self.populated = 03100 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")3101 if self.have_ipc:3102 select_ipc = ", insn_count, cyc_count"3103 prep_fn = BranchDataWithIPCPrep3104 prep_wa_fn = BranchDataWithIPCPrepWA3105 else:3106 select_ipc = ""3107 prep_fn = BranchDataPrep3108 prep_wa_fn = BranchDataPrepWA3109 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"3110 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"3111 " ip, symbols.name, sym_offset, dsos.short_name,"3112 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"3113 + select_ipc +3114 " FROM samples"3115 " INNER JOIN comms ON comm_id = comms.id"3116 " INNER JOIN threads ON thread_id = threads.id"3117 " INNER JOIN branch_types ON branch_type = branch_types.id"3118 " INNER JOIN symbols ON symbol_id = symbols.id"3119 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"3120 " INNER JOIN dsos ON samples.dso_id = dsos.id"3121 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"3122 " WHERE samples.id > $$last_id$$" + where_clause +3123 " AND evsel_id = " + str(self.event_id) +3124 " ORDER BY samples.id"3125 " LIMIT " + str(glb_chunk_sz))3126 if pyside_version_1 and sys.version_info[0] == 3:3127 prep = prep_fn3128 else:3129 prep = prep_wa_fn3130 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)3131 self.fetcher.done.connect(self.Update)3132 self.fetcher.Fetch(glb_chunk_sz)3133 3134 def GetRoot(self):3135 return BranchRootItem()3136 3137 def columnCount(self, parent=None):3138 if self.have_ipc:3139 return 113140 else:3141 return 83142 3143 def columnHeader(self, column):3144 if self.have_ipc:3145 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]3146 else:3147 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]3148 3149 def columnFont(self, column):3150 if self.have_ipc:3151 br_col = 103152 else:3153 br_col = 73154 if column != br_col:3155 return None3156 return QFont("Monospace")3157 3158 def DisplayData(self, item, index):3159 if item.level == 1:3160 self.FetchIfNeeded(item.row)3161 return item.getData(index.column())3162 3163 def AddSample(self, data):3164 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)3165 self.root.child_items.append(child)3166 self.populated += 13167 3168 def Update(self, fetched):3169 if not fetched:3170 self.more = False3171 self.progress.emit(0)3172 child_count = self.root.child_count3173 count = self.populated - child_count3174 if count > 0:3175 parent = QModelIndex()3176 self.beginInsertRows(parent, child_count, child_count + count - 1)3177 self.insertRows(child_count, count, parent)3178 self.root.child_count += count3179 self.endInsertRows()3180 self.progress.emit(self.root.child_count)3181 3182 def FetchMoreRecords(self, count):3183 current = self.root.child_count3184 if self.more:3185 self.fetcher.Fetch(count)3186 else:3187 self.progress.emit(0)3188 return current3189 3190 def HasMoreRecords(self):3191 return self.more3192 3193# Report Variables3194 3195class ReportVars():3196 3197 def __init__(self, name = "", where_clause = "", limit = ""):3198 self.name = name3199 self.where_clause = where_clause3200 self.limit = limit3201 3202 def UniqueId(self):3203 return str(self.where_clause + ";" + self.limit)3204 3205# Branch window3206 3207class BranchWindow(QMdiSubWindow):3208 3209 def __init__(self, glb, event_id, report_vars, parent=None):3210 super(BranchWindow, self).__init__(parent)3211 3212 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()3213 3214 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))3215 3216 self.view = QTreeView()3217 self.view.setUniformRowHeights(True)3218 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)3219 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard3220 self.view.setModel(self.model)3221 3222 self.ResizeColumnsToContents()3223 3224 self.context_menu = TreeContextMenu(self.view)3225 3226 self.find_bar = FindBar(self, self, True)3227 3228 self.finder = ChildDataItemFinder(self.model.root)3229 3230 self.fetch_bar = FetchMoreRecordsBar(self.model, self)3231 3232 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())3233 3234 self.setWidget(self.vbox.Widget())3235 3236 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")3237 3238 def ResizeColumnToContents(self, column, n):3239 # Using the view's resizeColumnToContents() here is extrememly slow3240 # so implement a crude alternative3241 mm = "MM" if column else "MMMM"3242 font = self.view.font()3243 metrics = QFontMetrics(font)3244 max = 03245 for row in xrange(n):3246 val = self.model.root.child_items[row].data[column]3247 len = metrics.width(str(val) + mm)3248 max = len if len > max else max3249 val = self.model.columnHeader(column)3250 len = metrics.width(str(val) + mm)3251 max = len if len > max else max3252 self.view.setColumnWidth(column, max)3253 3254 def ResizeColumnsToContents(self):3255 n = min(self.model.root.child_count, 100)3256 if n < 1:3257 # No data yet, so connect a signal to notify when there is3258 self.model.rowsInserted.connect(self.UpdateColumnWidths)3259 return3260 columns = self.model.columnCount()3261 for i in xrange(columns):3262 self.ResizeColumnToContents(i, n)3263 3264 def UpdateColumnWidths(self, *x):3265 # This only needs to be done once, so disconnect the signal now3266 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)3267 self.ResizeColumnsToContents()3268 3269 def Find(self, value, direction, pattern, context):3270 self.view.setFocus()3271 self.find_bar.Busy()3272 self.finder.Find(value, direction, pattern, context, self.FindDone)3273 3274 def FindDone(self, row):3275 self.find_bar.Idle()3276 if row >= 0:3277 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))3278 else:3279 self.find_bar.NotFound()3280 3281# Line edit data item3282 3283class LineEditDataItem(object):3284 3285 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):3286 self.glb = glb3287 self.label = label3288 self.placeholder_text = placeholder_text3289 self.parent = parent3290 self.id = id3291 3292 self.value = default3293 3294 self.widget = QLineEdit(default)3295 self.widget.editingFinished.connect(self.Validate)3296 self.widget.textChanged.connect(self.Invalidate)3297 self.red = False3298 self.error = ""3299 self.validated = True3300 3301 if placeholder_text:3302 self.widget.setPlaceholderText(placeholder_text)3303 3304 def TurnTextRed(self):3305 if not self.red:3306 palette = QPalette()3307 palette.setColor(QPalette.Text,Qt.red)3308 self.widget.setPalette(palette)3309 self.red = True3310 3311 def TurnTextNormal(self):3312 if self.red:3313 palette = QPalette()3314 self.widget.setPalette(palette)3315 self.red = False3316 3317 def InvalidValue(self, value):3318 self.value = ""3319 self.TurnTextRed()3320 self.error = self.label + " invalid value '" + value + "'"3321 self.parent.ShowMessage(self.error)3322 3323 def Invalidate(self):3324 self.validated = False3325 3326 def DoValidate(self, input_string):3327 self.value = input_string.strip()3328 3329 def Validate(self):3330 self.validated = True3331 self.error = ""3332 self.TurnTextNormal()3333 self.parent.ClearMessage()3334 input_string = self.widget.text()3335 if not len(input_string.strip()):3336 self.value = ""3337 return3338 self.DoValidate(input_string)3339 3340 def IsValid(self):3341 if not self.validated:3342 self.Validate()3343 if len(self.error):3344 self.parent.ShowMessage(self.error)3345 return False3346 return True3347 3348 def IsNumber(self, value):3349 try:3350 x = int(value)3351 except:3352 x = 03353 return str(x) == value3354 3355# Non-negative integer ranges dialog data item3356 3357class NonNegativeIntegerRangesDataItem(LineEditDataItem):3358 3359 def __init__(self, glb, label, placeholder_text, column_name, parent):3360 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)3361 3362 self.column_name = column_name3363 3364 def DoValidate(self, input_string):3365 singles = []3366 ranges = []3367 for value in [x.strip() for x in input_string.split(",")]:3368 if "-" in value:3369 vrange = value.split("-")3370 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):3371 return self.InvalidValue(value)3372 ranges.append(vrange)3373 else:3374 if not self.IsNumber(value):3375 return self.InvalidValue(value)3376 singles.append(value)3377 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]3378 if len(singles):3379 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")3380 self.value = " OR ".join(ranges)3381 3382# Positive integer dialog data item3383 3384class PositiveIntegerDataItem(LineEditDataItem):3385 3386 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):3387 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)3388 3389 def DoValidate(self, input_string):3390 if not self.IsNumber(input_string.strip()):3391 return self.InvalidValue(input_string)3392 value = int(input_string.strip())3393 if value <= 0:3394 return self.InvalidValue(input_string)3395 self.value = str(value)3396 3397# Dialog data item converted and validated using a SQL table3398 3399class SQLTableDataItem(LineEditDataItem):3400 3401 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):3402 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)3403 3404 self.table_name = table_name3405 self.match_column = match_column3406 self.column_name1 = column_name13407 self.column_name2 = column_name23408 3409 def ValueToIds(self, value):3410 ids = []3411 query = QSqlQuery(self.glb.db)3412 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"3413 ret = query.exec_(stmt)3414 if ret:3415 while query.next():3416 ids.append(str(query.value(0)))3417 return ids3418 3419 def DoValidate(self, input_string):3420 all_ids = []3421 for value in [x.strip() for x in input_string.split(",")]:3422 ids = self.ValueToIds(value)3423 if len(ids):3424 all_ids.extend(ids)3425 else:3426 return self.InvalidValue(value)3427 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"3428 if self.column_name2:3429 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"3430 3431# Sample time ranges dialog data item converted and validated using 'samples' SQL table3432 3433class SampleTimeRangesDataItem(LineEditDataItem):3434 3435 def __init__(self, glb, label, placeholder_text, column_name, parent):3436 self.column_name = column_name3437 3438 self.last_id = 03439 self.first_time = 03440 self.last_time = 2 ** 643441 3442 query = QSqlQuery(glb.db)3443 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")3444 if query.next():3445 self.last_id = int(query.value(0))3446 self.first_time = int(glb.HostStartTime())3447 self.last_time = int(glb.HostFinishTime())3448 if placeholder_text:3449 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)3450 3451 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)3452 3453 def IdBetween(self, query, lower_id, higher_id, order):3454 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")3455 if query.next():3456 return True, int(query.value(0))3457 else:3458 return False, 03459 3460 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):3461 query = QSqlQuery(self.glb.db)3462 while True:3463 next_id = int((lower_id + higher_id) / 2)3464 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))3465 if not query.next():3466 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")3467 if not ok:3468 ok, dbid = self.IdBetween(query, next_id, higher_id, "")3469 if not ok:3470 return str(higher_id)3471 next_id = dbid3472 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))3473 next_time = int(query.value(0))3474 if get_floor:3475 if target_time > next_time:3476 lower_id = next_id3477 else:3478 higher_id = next_id3479 if higher_id <= lower_id + 1:3480 return str(higher_id)3481 else:3482 if target_time >= next_time:3483 lower_id = next_id3484 else:3485 higher_id = next_id3486 if higher_id <= lower_id + 1:3487 return str(lower_id)3488 3489 def ConvertRelativeTime(self, val):3490 mult = 13491 suffix = val[-2:]3492 if suffix == "ms":3493 mult = 10000003494 elif suffix == "us":3495 mult = 10003496 elif suffix == "ns":3497 mult = 13498 else:3499 return val3500 val = val[:-2].strip()3501 if not self.IsNumber(val):3502 return val3503 val = int(val) * mult3504 if val >= 0:3505 val += self.first_time3506 else:3507 val += self.last_time3508 return str(val)3509 3510 def ConvertTimeRange(self, vrange):3511 if vrange[0] == "":3512 vrange[0] = str(self.first_time)3513 if vrange[1] == "":3514 vrange[1] = str(self.last_time)3515 vrange[0] = self.ConvertRelativeTime(vrange[0])3516 vrange[1] = self.ConvertRelativeTime(vrange[1])3517 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):3518 return False3519 beg_range = max(int(vrange[0]), self.first_time)3520 end_range = min(int(vrange[1]), self.last_time)3521 if beg_range > self.last_time or end_range < self.first_time:3522 return False3523 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)3524 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)3525 return True3526 3527 def AddTimeRange(self, value, ranges):3528 n = value.count("-")3529 if n == 1:3530 pass3531 elif n == 2:3532 if value.split("-")[1].strip() == "":3533 n = 13534 elif n == 3:3535 n = 23536 else:3537 return False3538 pos = findnth(value, "-", n)3539 vrange = [value[:pos].strip() ,value[pos+1:].strip()]3540 if self.ConvertTimeRange(vrange):3541 ranges.append(vrange)3542 return True3543 return False3544 3545 def DoValidate(self, input_string):3546 ranges = []3547 for value in [x.strip() for x in input_string.split(",")]:3548 if not self.AddTimeRange(value, ranges):3549 return self.InvalidValue(value)3550 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]3551 self.value = " OR ".join(ranges)3552 3553# Report Dialog Base3554 3555class ReportDialogBase(QDialog):3556 3557 def __init__(self, glb, title, items, partial, parent=None):3558 super(ReportDialogBase, self).__init__(parent)3559 3560 self.glb = glb3561 3562 self.report_vars = ReportVars()3563 3564 self.setWindowTitle(title)3565 self.setMinimumWidth(600)3566 3567 self.data_items = [x(glb, self) for x in items]3568 3569 self.partial = partial3570 3571 self.grid = QGridLayout()3572 3573 for row in xrange(len(self.data_items)):3574 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)3575 self.grid.addWidget(self.data_items[row].widget, row, 1)3576 3577 self.status = QLabel()3578 3579 self.ok_button = QPushButton("Ok", self)3580 self.ok_button.setDefault(True)3581 self.ok_button.released.connect(self.Ok)3582 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)3583 3584 self.cancel_button = QPushButton("Cancel", self)3585 self.cancel_button.released.connect(self.reject)3586 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)3587 3588 self.hbox = QHBoxLayout()3589 #self.hbox.addStretch()3590 self.hbox.addWidget(self.status)3591 self.hbox.addWidget(self.ok_button)3592 self.hbox.addWidget(self.cancel_button)3593 3594 self.vbox = QVBoxLayout()3595 self.vbox.addLayout(self.grid)3596 self.vbox.addLayout(self.hbox)3597 3598 self.setLayout(self.vbox)3599 3600 def Ok(self):3601 vars = self.report_vars3602 for d in self.data_items:3603 if d.id == "REPORTNAME":3604 vars.name = d.value3605 if not vars.name:3606 self.ShowMessage("Report name is required")3607 return3608 for d in self.data_items:3609 if not d.IsValid():3610 return3611 for d in self.data_items[1:]:3612 if d.id == "LIMIT":3613 vars.limit = d.value3614 elif len(d.value):3615 if len(vars.where_clause):3616 vars.where_clause += " AND "3617 vars.where_clause += d.value3618 if len(vars.where_clause):3619 if self.partial:3620 vars.where_clause = " AND ( " + vars.where_clause + " ) "3621 else:3622 vars.where_clause = " WHERE " + vars.where_clause + " "3623 self.accept()3624 3625 def ShowMessage(self, msg):3626 self.status.setText("<font color=#FF0000>" + msg)3627 3628 def ClearMessage(self):3629 self.status.setText("")3630 3631# Selected branch report creation dialog3632 3633class SelectedBranchDialog(ReportDialogBase):3634 3635 def __init__(self, glb, parent=None):3636 title = "Selected Branches"3637 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),3638 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),3639 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),3640 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),3641 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),3642 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),3643 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),3644 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),3645 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))3646 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)3647 3648# Event list3649 3650def GetEventList(db):3651 events = []3652 query = QSqlQuery(db)3653 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")3654 while query.next():3655 events.append(query.value(0))3656 return events3657 3658# Is a table selectable3659 3660def IsSelectable(db, table, sql = "", columns = "*"):3661 query = QSqlQuery(db)3662 try:3663 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")3664 except:3665 return False3666 return True3667 3668# SQL table data model item3669 3670class SQLTableItem():3671 3672 def __init__(self, row, data):3673 self.row = row3674 self.data = data3675 3676 def getData(self, column):3677 return self.data[column]3678 3679# SQL table data model3680 3681class SQLTableModel(TableModel):3682 3683 progress = Signal(object)3684 3685 def __init__(self, glb, sql, column_headers, parent=None):3686 super(SQLTableModel, self).__init__(parent)3687 self.glb = glb3688 self.more = True3689 self.populated = 03690 self.column_headers = column_headers3691 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)3692 self.fetcher.done.connect(self.Update)3693 self.fetcher.Fetch(glb_chunk_sz)3694 3695 def DisplayData(self, item, index):3696 self.FetchIfNeeded(item.row)3697 return item.getData(index.column())3698 3699 def AddSample(self, data):3700 child = SQLTableItem(self.populated, data)3701 self.child_items.append(child)3702 self.populated += 13703 3704 def Update(self, fetched):3705 if not fetched:3706 self.more = False3707 self.progress.emit(0)3708 child_count = self.child_count3709 count = self.populated - child_count3710 if count > 0:3711 parent = QModelIndex()3712 self.beginInsertRows(parent, child_count, child_count + count - 1)3713 self.insertRows(child_count, count, parent)3714 self.child_count += count3715 self.endInsertRows()3716 self.progress.emit(self.child_count)3717 3718 def FetchMoreRecords(self, count):3719 current = self.child_count3720 if self.more:3721 self.fetcher.Fetch(count)3722 else:3723 self.progress.emit(0)3724 return current3725 3726 def HasMoreRecords(self):3727 return self.more3728 3729 def columnCount(self, parent=None):3730 return len(self.column_headers)3731 3732 def columnHeader(self, column):3733 return self.column_headers[column]3734 3735 def SQLTableDataPrep(self, query, count):3736 data = []3737 for i in xrange(count):3738 data.append(query.value(i))3739 return data3740 3741# SQL automatic table data model3742 3743class SQLAutoTableModel(SQLTableModel):3744 3745 def __init__(self, glb, table_name, parent=None):3746 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)3747 if table_name == "comm_threads_view":3748 # For now, comm_threads_view has no id column3749 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)3750 column_headers = []3751 query = QSqlQuery(glb.db)3752 if glb.dbref.is_sqlite3:3753 QueryExec(query, "PRAGMA table_info(" + table_name + ")")3754 while query.next():3755 column_headers.append(query.value(1))3756 if table_name == "sqlite_master":3757 sql = "SELECT * FROM " + table_name3758 else:3759 if table_name[:19] == "information_schema.":3760 sql = "SELECT * FROM " + table_name3761 select_table_name = table_name[19:]3762 schema = "information_schema"3763 else:3764 select_table_name = table_name3765 schema = "public"3766 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")3767 while query.next():3768 column_headers.append(query.value(0))3769 if pyside_version_1 and sys.version_info[0] == 3:3770 if table_name == "samples_view":3771 self.SQLTableDataPrep = self.samples_view_DataPrep3772 if table_name == "samples":3773 self.SQLTableDataPrep = self.samples_DataPrep3774 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)3775 3776 def samples_view_DataPrep(self, query, count):3777 data = []3778 data.append(query.value(0))3779 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string3780 data.append("{:>19}".format(query.value(1)))3781 for i in xrange(2, count):3782 data.append(query.value(i))3783 return data3784 3785 def samples_DataPrep(self, query, count):3786 data = []3787 for i in xrange(9):3788 data.append(query.value(i))3789 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string3790 data.append("{:>19}".format(query.value(9)))3791 for i in xrange(10, count):3792 data.append(query.value(i))3793 return data3794 3795# Base class for custom ResizeColumnsToContents3796 3797class ResizeColumnsToContentsBase(QObject):3798 3799 def __init__(self, parent=None):3800 super(ResizeColumnsToContentsBase, self).__init__(parent)3801 3802 def ResizeColumnToContents(self, column, n):3803 # Using the view's resizeColumnToContents() here is extrememly slow3804 # so implement a crude alternative3805 font = self.view.font()3806 metrics = QFontMetrics(font)3807 max = 03808 for row in xrange(n):3809 val = self.data_model.child_items[row].data[column]3810 len = metrics.width(str(val) + "MM")3811 max = len if len > max else max3812 val = self.data_model.columnHeader(column)3813 len = metrics.width(str(val) + "MM")3814 max = len if len > max else max3815 self.view.setColumnWidth(column, max)3816 3817 def ResizeColumnsToContents(self):3818 n = min(self.data_model.child_count, 100)3819 if n < 1:3820 # No data yet, so connect a signal to notify when there is3821 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)3822 return3823 columns = self.data_model.columnCount()3824 for i in xrange(columns):3825 self.ResizeColumnToContents(i, n)3826 3827 def UpdateColumnWidths(self, *x):3828 # This only needs to be done once, so disconnect the signal now3829 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)3830 self.ResizeColumnsToContents()3831 3832# Convert value to CSV3833 3834def ToCSValue(val):3835 if '"' in val:3836 val = val.replace('"', '""')3837 if "," in val or '"' in val:3838 val = '"' + val + '"'3839 return val3840 3841# Key to sort table model indexes by row / column, assuming fewer than 1000 columns3842 3843glb_max_cols = 10003844 3845def RowColumnKey(a):3846 return a.row() * glb_max_cols + a.column()3847 3848# Copy selected table cells to clipboard3849 3850def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):3851 indexes = sorted(view.selectedIndexes(), key=RowColumnKey)3852 idx_cnt = len(indexes)3853 if not idx_cnt:3854 return3855 if idx_cnt == 1:3856 with_hdr=False3857 min_row = indexes[0].row()3858 max_row = indexes[0].row()3859 min_col = indexes[0].column()3860 max_col = indexes[0].column()3861 for i in indexes:3862 min_row = min(min_row, i.row())3863 max_row = max(max_row, i.row())3864 min_col = min(min_col, i.column())3865 max_col = max(max_col, i.column())3866 if max_col > glb_max_cols:3867 raise RuntimeError("glb_max_cols is too low")3868 max_width = [0] * (1 + max_col - min_col)3869 for i in indexes:3870 c = i.column() - min_col3871 max_width[c] = max(max_width[c], len(str(i.data())))3872 text = ""3873 pad = ""3874 sep = ""3875 if with_hdr:3876 model = indexes[0].model()3877 for col in range(min_col, max_col + 1):3878 val = model.headerData(col, Qt.Horizontal, Qt.DisplayRole)3879 if as_csv:3880 text += sep + ToCSValue(val)3881 sep = ","3882 else:3883 c = col - min_col3884 max_width[c] = max(max_width[c], len(val))3885 width = max_width[c]3886 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)3887 if align & Qt.AlignRight:3888 val = val.rjust(width)3889 text += pad + sep + val3890 pad = " " * (width - len(val))3891 sep = " "3892 text += "\n"3893 pad = ""3894 sep = ""3895 last_row = min_row3896 for i in indexes:3897 if i.row() > last_row:3898 last_row = i.row()3899 text += "\n"3900 pad = ""3901 sep = ""3902 if as_csv:3903 text += sep + ToCSValue(str(i.data()))3904 sep = ","3905 else:3906 width = max_width[i.column() - min_col]3907 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:3908 val = str(i.data()).rjust(width)3909 else:3910 val = str(i.data())3911 text += pad + sep + val3912 pad = " " * (width - len(val))3913 sep = " "3914 QApplication.clipboard().setText(text)3915 3916def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):3917 indexes = view.selectedIndexes()3918 if not len(indexes):3919 return3920 3921 selection = view.selectionModel()3922 3923 first = None3924 for i in indexes:3925 above = view.indexAbove(i)3926 if not selection.isSelected(above):3927 first = i3928 break3929 3930 if first is None:3931 raise RuntimeError("CopyTreeCellsToClipboard internal error")3932 3933 model = first.model()3934 row_cnt = 03935 col_cnt = model.columnCount(first)3936 max_width = [0] * col_cnt3937 3938 indent_sz = 23939 indent_str = " " * indent_sz3940 3941 expanded_mark_sz = 23942 if sys.version_info[0] == 3:3943 expanded_mark = "\u25BC "3944 not_expanded_mark = "\u25B6 "3945 else:3946 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")3947 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")3948 leaf_mark = " "3949 3950 if not as_csv:3951 pos = first3952 while True:3953 row_cnt += 13954 row = pos.row()3955 for c in range(col_cnt):3956 i = pos.sibling(row, c)3957 if c:3958 n = len(str(i.data()))3959 else:3960 n = len(str(i.data()).strip())3961 n += (i.internalPointer().level - 1) * indent_sz3962 n += expanded_mark_sz3963 max_width[c] = max(max_width[c], n)3964 pos = view.indexBelow(pos)3965 if not selection.isSelected(pos):3966 break3967 3968 text = ""3969 pad = ""3970 sep = ""3971 if with_hdr:3972 for c in range(col_cnt):3973 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()3974 if as_csv:3975 text += sep + ToCSValue(val)3976 sep = ","3977 else:3978 max_width[c] = max(max_width[c], len(val))3979 width = max_width[c]3980 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)3981 if align & Qt.AlignRight:3982 val = val.rjust(width)3983 text += pad + sep + val3984 pad = " " * (width - len(val))3985 sep = " "3986 text += "\n"3987 pad = ""3988 sep = ""3989 3990 pos = first3991 while True:3992 row = pos.row()3993 for c in range(col_cnt):3994 i = pos.sibling(row, c)3995 val = str(i.data())3996 if not c:3997 if model.hasChildren(i):3998 if view.isExpanded(i):3999 mark = expanded_mark4000 else:4001 mark = not_expanded_mark4002 else:4003 mark = leaf_mark4004 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()4005 if as_csv:4006 text += sep + ToCSValue(val)4007 sep = ","4008 else:4009 width = max_width[c]4010 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:4011 val = val.rjust(width)4012 text += pad + sep + val4013 pad = " " * (width - len(val))4014 sep = " "4015 pos = view.indexBelow(pos)4016 if not selection.isSelected(pos):4017 break4018 text = text.rstrip() + "\n"4019 pad = ""4020 sep = ""4021 4022 QApplication.clipboard().setText(text)4023 4024def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):4025 view.CopyCellsToClipboard(view, as_csv, with_hdr)4026 4027def CopyCellsToClipboardHdr(view):4028 CopyCellsToClipboard(view, False, True)4029 4030def CopyCellsToClipboardCSV(view):4031 CopyCellsToClipboard(view, True, True)4032 4033# Context menu4034 4035class ContextMenu(object):4036 4037 def __init__(self, view):4038 self.view = view4039 self.view.setContextMenuPolicy(Qt.CustomContextMenu)4040 self.view.customContextMenuRequested.connect(self.ShowContextMenu)4041 4042 def ShowContextMenu(self, pos):4043 menu = QMenu(self.view)4044 self.AddActions(menu)4045 menu.exec_(self.view.mapToGlobal(pos))4046 4047 def AddCopy(self, menu):4048 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))4049 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))4050 4051 def AddActions(self, menu):4052 self.AddCopy(menu)4053 4054class TreeContextMenu(ContextMenu):4055 4056 def __init__(self, view):4057 super(TreeContextMenu, self).__init__(view)4058 4059 def AddActions(self, menu):4060 i = self.view.currentIndex()4061 text = str(i.data()).strip()4062 if len(text):4063 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))4064 self.AddCopy(menu)4065 4066# Table window4067 4068class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):4069 4070 def __init__(self, glb, table_name, parent=None):4071 super(TableWindow, self).__init__(parent)4072 4073 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))4074 4075 self.model = QSortFilterProxyModel()4076 self.model.setSourceModel(self.data_model)4077 4078 self.view = QTableView()4079 self.view.setModel(self.model)4080 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)4081 self.view.verticalHeader().setVisible(False)4082 self.view.sortByColumn(-1, Qt.AscendingOrder)4083 self.view.setSortingEnabled(True)4084 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)4085 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard4086 4087 self.ResizeColumnsToContents()4088 4089 self.context_menu = ContextMenu(self.view)4090 4091 self.find_bar = FindBar(self, self, True)4092 4093 self.finder = ChildDataItemFinder(self.data_model)4094 4095 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)4096 4097 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())4098 4099 self.setWidget(self.vbox.Widget())4100 4101 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")4102 4103 def Find(self, value, direction, pattern, context):4104 self.view.setFocus()4105 self.find_bar.Busy()4106 self.finder.Find(value, direction, pattern, context, self.FindDone)4107 4108 def FindDone(self, row):4109 self.find_bar.Idle()4110 if row >= 0:4111 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))4112 else:4113 self.find_bar.NotFound()4114 4115# Table list4116 4117def GetTableList(glb):4118 tables = []4119 query = QSqlQuery(glb.db)4120 if glb.dbref.is_sqlite3:4121 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")4122 else:4123 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")4124 while query.next():4125 tables.append(query.value(0))4126 if glb.dbref.is_sqlite3:4127 tables.append("sqlite_master")4128 else:4129 tables.append("information_schema.tables")4130 tables.append("information_schema.views")4131 tables.append("information_schema.columns")4132 return tables4133 4134# Top Calls data model4135 4136class TopCallsModel(SQLTableModel):4137 4138 def __init__(self, glb, report_vars, parent=None):4139 text = ""4140 if not glb.dbref.is_sqlite3:4141 text = "::text"4142 limit = ""4143 if len(report_vars.limit):4144 limit = " LIMIT " + report_vars.limit4145 sql = ("SELECT comm, pid, tid, name,"4146 " CASE"4147 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +4148 " ELSE short_name"4149 " END AS dso,"4150 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "4151 " CASE"4152 " WHEN (calls.flags = 1) THEN 'no call'" + text +4153 " WHEN (calls.flags = 2) THEN 'no return'" + text +4154 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +4155 " ELSE ''" + text +4156 " END AS flags"4157 " FROM calls"4158 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"4159 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"4160 " INNER JOIN dsos ON symbols.dso_id = dsos.id"4161 " INNER JOIN comms ON calls.comm_id = comms.id"4162 " INNER JOIN threads ON calls.thread_id = threads.id" +4163 report_vars.where_clause +4164 " ORDER BY elapsed_time DESC" +4165 limit4166 )4167 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")4168 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)4169 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)4170 4171 def columnAlignment(self, column):4172 return self.alignment[column]4173 4174# Top Calls report creation dialog4175 4176class TopCallsDialog(ReportDialogBase):4177 4178 def __init__(self, glb, parent=None):4179 title = "Top Calls by Elapsed Time"4180 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),4181 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),4182 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),4183 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),4184 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),4185 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),4186 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),4187 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))4188 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)4189 4190# Top Calls window4191 4192class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):4193 4194 def __init__(self, glb, report_vars, parent=None):4195 super(TopCallsWindow, self).__init__(parent)4196 4197 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))4198 self.model = self.data_model4199 4200 self.view = QTableView()4201 self.view.setModel(self.model)4202 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)4203 self.view.verticalHeader().setVisible(False)4204 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)4205 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard4206 4207 self.context_menu = ContextMenu(self.view)4208 4209 self.ResizeColumnsToContents()4210 4211 self.find_bar = FindBar(self, self, True)4212 4213 self.finder = ChildDataItemFinder(self.model)4214 4215 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)4216 4217 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())4218 4219 self.setWidget(self.vbox.Widget())4220 4221 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)4222 4223 def Find(self, value, direction, pattern, context):4224 self.view.setFocus()4225 self.find_bar.Busy()4226 self.finder.Find(value, direction, pattern, context, self.FindDone)4227 4228 def FindDone(self, row):4229 self.find_bar.Idle()4230 if row >= 0:4231 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))4232 else:4233 self.find_bar.NotFound()4234 4235# Action Definition4236 4237def CreateAction(label, tip, callback, parent=None, shortcut=None):4238 action = QAction(label, parent)4239 if shortcut != None:4240 action.setShortcuts(shortcut)4241 action.setStatusTip(tip)4242 action.triggered.connect(callback)4243 return action4244 4245# Typical application actions4246 4247def CreateExitAction(app, parent=None):4248 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)4249 4250# Typical MDI actions4251 4252def CreateCloseActiveWindowAction(mdi_area):4253 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)4254 4255def CreateCloseAllWindowsAction(mdi_area):4256 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)4257 4258def CreateTileWindowsAction(mdi_area):4259 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)4260 4261def CreateCascadeWindowsAction(mdi_area):4262 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)4263 4264def CreateNextWindowAction(mdi_area):4265 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)4266 4267def CreatePreviousWindowAction(mdi_area):4268 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)4269 4270# Typical MDI window menu4271 4272class WindowMenu():4273 4274 def __init__(self, mdi_area, menu):4275 self.mdi_area = mdi_area4276 self.window_menu = menu.addMenu("&Windows")4277 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)4278 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)4279 self.tile_windows = CreateTileWindowsAction(mdi_area)4280 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)4281 self.next_window = CreateNextWindowAction(mdi_area)4282 self.previous_window = CreatePreviousWindowAction(mdi_area)4283 self.window_menu.aboutToShow.connect(self.Update)4284 4285 def Update(self):4286 self.window_menu.clear()4287 sub_window_count = len(self.mdi_area.subWindowList())4288 have_sub_windows = sub_window_count != 04289 self.close_active_window.setEnabled(have_sub_windows)4290 self.close_all_windows.setEnabled(have_sub_windows)4291 self.tile_windows.setEnabled(have_sub_windows)4292 self.cascade_windows.setEnabled(have_sub_windows)4293 self.next_window.setEnabled(have_sub_windows)4294 self.previous_window.setEnabled(have_sub_windows)4295 self.window_menu.addAction(self.close_active_window)4296 self.window_menu.addAction(self.close_all_windows)4297 self.window_menu.addSeparator()4298 self.window_menu.addAction(self.tile_windows)4299 self.window_menu.addAction(self.cascade_windows)4300 self.window_menu.addSeparator()4301 self.window_menu.addAction(self.next_window)4302 self.window_menu.addAction(self.previous_window)4303 if sub_window_count == 0:4304 return4305 self.window_menu.addSeparator()4306 nr = 14307 for sub_window in self.mdi_area.subWindowList():4308 label = str(nr) + " " + sub_window.name4309 if nr < 10:4310 label = "&" + label4311 action = self.window_menu.addAction(label)4312 action.setCheckable(True)4313 action.setChecked(sub_window == self.mdi_area.activeSubWindow())4314 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))4315 self.window_menu.addAction(action)4316 nr += 14317 4318 def setActiveSubWindow(self, nr):4319 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])4320 4321# Help text4322 4323glb_help_text = """4324<h1>Contents</h1>4325<style>4326p.c1 {4327 text-indent: 40px;4328}4329p.c2 {4330 text-indent: 80px;4331}4332}4333</style>4334<p class=c1><a href=#reports>1. Reports</a></p>4335<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>4336<p class=c2><a href=#calltree>1.2 Call Tree</a></p>4337<p class=c2><a href=#allbranches>1.3 All branches</a></p>4338<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>4339<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>4340<p class=c1><a href=#charts>2. Charts</a></p>4341<p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>4342<p class=c1><a href=#tables>3. Tables</a></p>4343<h1 id=reports>1. Reports</h1>4344<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>4345The result is a GUI window with a tree representing a context-sensitive4346call-graph. Expanding a couple of levels of the tree and adjusting column4347widths to suit will display something like:4348<pre>4349 Call Graph: pt_example4350Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)4351v- ls4352 v- 2638:26384353 v- _start ld-2.19.so 1 10074071 100.0 211135 100.04354 |- unknown unknown 1 13198 0.1 1 0.04355 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.34356 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.34357 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.44358 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.14359 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.04360 >- __libc_csu_init ls 1 10354 0.1 10 0.04361 |- _setjmp libc-2.19.so 1 0 0.0 4 0.04362 v- main ls 1 8182043 99.6 180254 99.94363</pre>4364<h3>Points to note:</h3>4365<ul>4366<li>The top level is a command name (comm)</li>4367<li>The next level is a thread (pid:tid)</li>4368<li>Subsequent levels are functions</li>4369<li>'Count' is the number of calls</li>4370<li>'Time' is the elapsed time until the function returns</li>4371<li>Percentages are relative to the level above</li>4372<li>'Branch Count' is the total number of branches for that function and all functions that it calls4373</ul>4374<h3>Find</h3>4375Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.4376The pattern matching symbols are ? for any character and * for zero or more characters.4377<h2 id=calltree>1.2 Call Tree</h2>4378The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.4379Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.4380<h2 id=allbranches>1.3 All branches</h2>4381The All branches report displays all branches in chronological order.4382Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.4383<h3>Disassembly</h3>4384Open a branch to display disassembly. This only works if:4385<ol>4386<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>4387<li>The object code is available. Currently, only the perf build ID cache is searched for object code.4388The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.4389One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),4390or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>4391</ol>4392<h4 id=xed>Intel XED Setup</h4>4393To use Intel XED, libxed.so must be present. To build and install libxed.so:4394<pre>4395git clone https://github.com/intelxed/mbuild.git mbuild4396git clone https://github.com/intelxed/xed4397cd xed4398./mfile.py --share4399sudo ./mfile.py --prefix=/usr/local install4400sudo ldconfig4401</pre>4402<h3>Instructions per Cycle (IPC)</h3>4403If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.4404<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.4405Due to the granularity of timing information, the number of cycles for some code blocks will not be known.4406In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period4407since the previous displayed 'IPC'.4408<h3>Find</h3>4409Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.4410Refer to Python documentation for the regular expression syntax.4411All columns are searched, but only currently fetched rows are searched.4412<h2 id=selectedbranches>1.4 Selected branches</h2>4413This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced4414by various selection criteria. A dialog box displays available criteria which are AND'ed together.4415<h3>1.4.1 Time ranges</h3>4416The time ranges hint text shows the total time range. Relative time ranges can also be entered in4417ms, us or ns. Also, negative values are relative to the end of trace. Examples:4418<pre>4419 81073085947329-81073085958238 From 81073085947329 to 810730859582384420 100us-200us From 100us to 200us4421 10ms- From 10ms to the end4422 -100ns The first 100ns4423 -10ms- The last 10ms4424</pre>4425N.B. Due to the granularity of timestamps, there could be no branches in any given time range.4426<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>4427The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.4428The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.4429If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.4430<h1 id=charts>2. Charts</h1>4431<h2 id=timechartbycpu>2.1 Time chart by CPU</h2>4432This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.4433<h3>Features</h3>4434<ol>4435<li>Mouse over to highight the task and show the time</li>4436<li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>4437<li>Go back and forward by pressing the arrow buttons</li>4438<li>If call information is available, right-click to show a call tree opened to that task and time.4439Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.4440</li>4441</ol>4442<h3>Important</h3>4443The graph can be misleading in the following respects:4444<ol>4445<li>The graph shows the first task on each CPU as running from the beginning of the time range.4446Because tracing might start on different CPUs at different times, that is not necessarily the case.4447Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>4448<li>Similarly, the last task on each CPU can be showing running longer than it really was.4449Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>4450<li>When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window</li>4451</ol>4452<h1 id=tables>3. Tables</h1>4453The Tables menu shows all tables and views in the database. Most tables have an associated view4454which displays the information in a more friendly way. Not all data for large tables is fetched4455immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,4456but that can be slow for large tables.4457<p>There are also tables of database meta-information.4458For SQLite3 databases, the sqlite_master table is included.4459For PostgreSQL databases, information_schema.tables/views/columns are included.4460<h3>Find</h3>4461Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.4462Refer to Python documentation for the regular expression syntax.4463All columns are searched, but only currently fetched rows are searched.4464<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous4465will go to the next/previous result in id order, instead of display order.4466"""4467 4468# Help window4469 4470class HelpWindow(QMdiSubWindow):4471 4472 def __init__(self, glb, parent=None):4473 super(HelpWindow, self).__init__(parent)4474 4475 self.text = QTextBrowser()4476 self.text.setHtml(glb_help_text)4477 self.text.setReadOnly(True)4478 self.text.setOpenExternalLinks(True)4479 4480 self.setWidget(self.text)4481 4482 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")4483 4484# Main window that only displays the help text4485 4486class HelpOnlyWindow(QMainWindow):4487 4488 def __init__(self, parent=None):4489 super(HelpOnlyWindow, self).__init__(parent)4490 4491 self.setMinimumSize(200, 100)4492 self.resize(800, 600)4493 self.setWindowTitle("Exported SQL Viewer Help")4494 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))4495 4496 self.text = QTextBrowser()4497 self.text.setHtml(glb_help_text)4498 self.text.setReadOnly(True)4499 self.text.setOpenExternalLinks(True)4500 4501 self.setCentralWidget(self.text)4502 4503# PostqreSQL server version4504 4505def PostqreSQLServerVersion(db):4506 query = QSqlQuery(db)4507 QueryExec(query, "SELECT VERSION()")4508 if query.next():4509 v_str = query.value(0)4510 v_list = v_str.strip().split(" ")4511 if v_list[0] == "PostgreSQL" and v_list[2] == "on":4512 return v_list[1]4513 return v_str4514 return "Unknown"4515 4516# SQLite version4517 4518def SQLiteVersion(db):4519 query = QSqlQuery(db)4520 QueryExec(query, "SELECT sqlite_version()")4521 if query.next():4522 return query.value(0)4523 return "Unknown"4524 4525# About dialog4526 4527class AboutDialog(QDialog):4528 4529 def __init__(self, glb, parent=None):4530 super(AboutDialog, self).__init__(parent)4531 4532 self.setWindowTitle("About Exported SQL Viewer")4533 self.setMinimumWidth(300)4534 4535 pyside_version = "1" if pyside_version_1 else "2"4536 4537 text = "<pre>"4538 text += "Python version: " + sys.version.split(" ")[0] + "\n"4539 text += "PySide version: " + pyside_version + "\n"4540 text += "Qt version: " + qVersion() + "\n"4541 if glb.dbref.is_sqlite3:4542 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n"4543 else:4544 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"4545 text += "</pre>"4546 4547 self.text = QTextBrowser()4548 self.text.setHtml(text)4549 self.text.setReadOnly(True)4550 self.text.setOpenExternalLinks(True)4551 4552 self.vbox = QVBoxLayout()4553 self.vbox.addWidget(self.text)4554 4555 self.setLayout(self.vbox)4556 4557# Font resize4558 4559def ResizeFont(widget, diff):4560 font = widget.font()4561 sz = font.pointSize()4562 font.setPointSize(sz + diff)4563 widget.setFont(font)4564 4565def ShrinkFont(widget):4566 ResizeFont(widget, -1)4567 4568def EnlargeFont(widget):4569 ResizeFont(widget, 1)4570 4571# Unique name for sub-windows4572 4573def NumberedWindowName(name, nr):4574 if nr > 1:4575 name += " <" + str(nr) + ">"4576 return name4577 4578def UniqueSubWindowName(mdi_area, name):4579 nr = 14580 while True:4581 unique_name = NumberedWindowName(name, nr)4582 ok = True4583 for sub_window in mdi_area.subWindowList():4584 if sub_window.name == unique_name:4585 ok = False4586 break4587 if ok:4588 return unique_name4589 nr += 14590 4591# Add a sub-window4592 4593def AddSubWindow(mdi_area, sub_window, name):4594 unique_name = UniqueSubWindowName(mdi_area, name)4595 sub_window.setMinimumSize(200, 100)4596 sub_window.resize(800, 600)4597 sub_window.setWindowTitle(unique_name)4598 sub_window.setAttribute(Qt.WA_DeleteOnClose)4599 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))4600 sub_window.name = unique_name4601 mdi_area.addSubWindow(sub_window)4602 sub_window.show()4603 4604# Main window4605 4606class MainWindow(QMainWindow):4607 4608 def __init__(self, glb, parent=None):4609 super(MainWindow, self).__init__(parent)4610 4611 self.glb = glb4612 4613 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)4614 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))4615 self.setMinimumSize(200, 100)4616 4617 self.mdi_area = QMdiArea()4618 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)4619 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)4620 4621 self.setCentralWidget(self.mdi_area)4622 4623 menu = self.menuBar()4624 4625 file_menu = menu.addMenu("&File")4626 file_menu.addAction(CreateExitAction(glb.app, self))4627 4628 edit_menu = menu.addMenu("&Edit")4629 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))4630 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))4631 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))4632 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))4633 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))4634 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))4635 4636 reports_menu = menu.addMenu("&Reports")4637 if IsSelectable(glb.db, "calls"):4638 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))4639 4640 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):4641 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))4642 4643 self.EventMenu(GetEventList(glb.db), reports_menu)4644 4645 if IsSelectable(glb.db, "calls"):4646 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))4647 4648 if IsSelectable(glb.db, "context_switches"):4649 charts_menu = menu.addMenu("&Charts")4650 charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))4651 4652 self.TableMenu(GetTableList(glb), menu)4653 4654 self.window_menu = WindowMenu(self.mdi_area, menu)4655 4656 help_menu = menu.addMenu("&Help")4657 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))4658 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))4659 4660 def Try(self, fn):4661 win = self.mdi_area.activeSubWindow()4662 if win:4663 try:4664 fn(win.view)4665 except:4666 pass4667 4668 def CopyToClipboard(self):4669 self.Try(CopyCellsToClipboardHdr)4670 4671 def CopyToClipboardCSV(self):4672 self.Try(CopyCellsToClipboardCSV)4673 4674 def Find(self):4675 win = self.mdi_area.activeSubWindow()4676 if win:4677 try:4678 win.find_bar.Activate()4679 except:4680 pass4681 4682 def FetchMoreRecords(self):4683 win = self.mdi_area.activeSubWindow()4684 if win:4685 try:4686 win.fetch_bar.Activate()4687 except:4688 pass4689 4690 def ShrinkFont(self):4691 self.Try(ShrinkFont)4692 4693 def EnlargeFont(self):4694 self.Try(EnlargeFont)4695 4696 def EventMenu(self, events, reports_menu):4697 branches_events = 04698 for event in events:4699 event = event.split(":")[0]4700 if event == "branches":4701 branches_events += 14702 dbid = 04703 for event in events:4704 dbid += 14705 event = event.split(":")[0]4706 if event == "branches":4707 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"4708 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))4709 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"4710 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))4711 4712 def TimeChartByCPU(self):4713 TimeChartByCPUWindow(self.glb, self)4714 4715 def TableMenu(self, tables, menu):4716 table_menu = menu.addMenu("&Tables")4717 for table in tables:4718 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))4719 4720 def NewCallGraph(self):4721 CallGraphWindow(self.glb, self)4722 4723 def NewCallTree(self):4724 CallTreeWindow(self.glb, self)4725 4726 def NewTopCalls(self):4727 dialog = TopCallsDialog(self.glb, self)4728 ret = dialog.exec_()4729 if ret:4730 TopCallsWindow(self.glb, dialog.report_vars, self)4731 4732 def NewBranchView(self, event_id):4733 BranchWindow(self.glb, event_id, ReportVars(), self)4734 4735 def NewSelectedBranchView(self, event_id):4736 dialog = SelectedBranchDialog(self.glb, self)4737 ret = dialog.exec_()4738 if ret:4739 BranchWindow(self.glb, event_id, dialog.report_vars, self)4740 4741 def NewTableView(self, table_name):4742 TableWindow(self.glb, table_name, self)4743 4744 def Help(self):4745 HelpWindow(self.glb, self)4746 4747 def About(self):4748 dialog = AboutDialog(self.glb, self)4749 dialog.exec_()4750 4751def TryOpen(file_name):4752 try:4753 return open(file_name, "rb")4754 except:4755 return None4756 4757def Is64Bit(f):4758 result = sizeof(c_void_p)4759 # ELF support only4760 pos = f.tell()4761 f.seek(0)4762 header = f.read(7)4763 f.seek(pos)4764 magic = header[0:4]4765 if sys.version_info[0] == 2:4766 eclass = ord(header[4])4767 encoding = ord(header[5])4768 version = ord(header[6])4769 else:4770 eclass = header[4]4771 encoding = header[5]4772 version = header[6]4773 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:4774 result = True if eclass == 2 else False4775 return result4776 4777# Global data4778 4779class Glb():4780 4781 def __init__(self, dbref, db, dbname):4782 self.dbref = dbref4783 self.db = db4784 self.dbname = dbname4785 self.home_dir = os.path.expanduser("~")4786 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")4787 if self.buildid_dir:4788 self.buildid_dir += "/.build-id/"4789 else:4790 self.buildid_dir = self.home_dir + "/.debug/.build-id/"4791 self.app = None4792 self.mainwindow = None4793 self.instances_to_shutdown_on_exit = weakref.WeakSet()4794 try:4795 self.disassembler = LibXED()4796 self.have_disassembler = True4797 except:4798 self.have_disassembler = False4799 self.host_machine_id = 04800 self.host_start_time = 04801 self.host_finish_time = 04802 4803 def FileFromBuildId(self, build_id):4804 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"4805 return TryOpen(file_name)4806 4807 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):4808 # Assume current machine i.e. no support for virtualization4809 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":4810 file_name = os.getenv("PERF_KCORE")4811 f = TryOpen(file_name) if file_name else None4812 if f:4813 return f4814 # For now, no special handling if long_name is /proc/kcore4815 f = TryOpen(long_name)4816 if f:4817 return f4818 f = self.FileFromBuildId(build_id)4819 if f:4820 return f4821 return None4822 4823 def AddInstanceToShutdownOnExit(self, instance):4824 self.instances_to_shutdown_on_exit.add(instance)4825 4826 # Shutdown any background processes or threads4827 def ShutdownInstances(self):4828 for x in self.instances_to_shutdown_on_exit:4829 try:4830 x.Shutdown()4831 except:4832 pass4833 4834 def GetHostMachineId(self):4835 query = QSqlQuery(self.db)4836 QueryExec(query, "SELECT id FROM machines WHERE pid = -1")4837 if query.next():4838 self.host_machine_id = query.value(0)4839 else:4840 self.host_machine_id = 04841 return self.host_machine_id4842 4843 def HostMachineId(self):4844 if self.host_machine_id:4845 return self.host_machine_id4846 return self.GetHostMachineId()4847 4848 def SelectValue(self, sql):4849 query = QSqlQuery(self.db)4850 try:4851 QueryExec(query, sql)4852 except:4853 return None4854 if query.next():4855 return Decimal(query.value(0))4856 return None4857 4858 def SwitchesMinTime(self, machine_id):4859 return self.SelectValue("SELECT time"4860 " FROM context_switches"4861 " WHERE time != 0 AND machine_id = " + str(machine_id) +4862 " ORDER BY id LIMIT 1")4863 4864 def SwitchesMaxTime(self, machine_id):4865 return self.SelectValue("SELECT time"4866 " FROM context_switches"4867 " WHERE time != 0 AND machine_id = " + str(machine_id) +4868 " ORDER BY id DESC LIMIT 1")4869 4870 def SamplesMinTime(self, machine_id):4871 return self.SelectValue("SELECT time"4872 " FROM samples"4873 " WHERE time != 0 AND machine_id = " + str(machine_id) +4874 " ORDER BY id LIMIT 1")4875 4876 def SamplesMaxTime(self, machine_id):4877 return self.SelectValue("SELECT time"4878 " FROM samples"4879 " WHERE time != 0 AND machine_id = " + str(machine_id) +4880 " ORDER BY id DESC LIMIT 1")4881 4882 def CallsMinTime(self, machine_id):4883 return self.SelectValue("SELECT calls.call_time"4884 " FROM calls"4885 " INNER JOIN threads ON threads.thread_id = calls.thread_id"4886 " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +4887 " ORDER BY calls.id LIMIT 1")4888 4889 def CallsMaxTime(self, machine_id):4890 return self.SelectValue("SELECT calls.return_time"4891 " FROM calls"4892 " INNER JOIN threads ON threads.thread_id = calls.thread_id"4893 " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +4894 " ORDER BY calls.return_time DESC LIMIT 1")4895 4896 def GetStartTime(self, machine_id):4897 t0 = self.SwitchesMinTime(machine_id)4898 t1 = self.SamplesMinTime(machine_id)4899 t2 = self.CallsMinTime(machine_id)4900 if t0 is None or (not(t1 is None) and t1 < t0):4901 t0 = t14902 if t0 is None or (not(t2 is None) and t2 < t0):4903 t0 = t24904 return t04905 4906 def GetFinishTime(self, machine_id):4907 t0 = self.SwitchesMaxTime(machine_id)4908 t1 = self.SamplesMaxTime(machine_id)4909 t2 = self.CallsMaxTime(machine_id)4910 if t0 is None or (not(t1 is None) and t1 > t0):4911 t0 = t14912 if t0 is None or (not(t2 is None) and t2 > t0):4913 t0 = t24914 return t04915 4916 def HostStartTime(self):4917 if self.host_start_time:4918 return self.host_start_time4919 self.host_start_time = self.GetStartTime(self.HostMachineId())4920 return self.host_start_time4921 4922 def HostFinishTime(self):4923 if self.host_finish_time:4924 return self.host_finish_time4925 self.host_finish_time = self.GetFinishTime(self.HostMachineId())4926 return self.host_finish_time4927 4928 def StartTime(self, machine_id):4929 if machine_id == self.HostMachineId():4930 return self.HostStartTime()4931 return self.GetStartTime(machine_id)4932 4933 def FinishTime(self, machine_id):4934 if machine_id == self.HostMachineId():4935 return self.HostFinishTime()4936 return self.GetFinishTime(machine_id)4937 4938# Database reference4939 4940class DBRef():4941 4942 def __init__(self, is_sqlite3, dbname):4943 self.is_sqlite3 = is_sqlite34944 self.dbname = dbname4945 self.TRUE = "TRUE"4946 self.FALSE = "FALSE"4947 # SQLite prior to version 3.23 does not support TRUE and FALSE4948 if self.is_sqlite3:4949 self.TRUE = "1"4950 self.FALSE = "0"4951 4952 def Open(self, connection_name):4953 dbname = self.dbname4954 if self.is_sqlite3:4955 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)4956 else:4957 db = QSqlDatabase.addDatabase("QPSQL", connection_name)4958 opts = dbname.split()4959 for opt in opts:4960 if "=" in opt:4961 opt = opt.split("=")4962 if opt[0] == "hostname":4963 db.setHostName(opt[1])4964 elif opt[0] == "port":4965 db.setPort(int(opt[1]))4966 elif opt[0] == "username":4967 db.setUserName(opt[1])4968 elif opt[0] == "password":4969 db.setPassword(opt[1])4970 elif opt[0] == "dbname":4971 dbname = opt[1]4972 else:4973 dbname = opt4974 4975 db.setDatabaseName(dbname)4976 if not db.open():4977 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())4978 return db, dbname4979 4980# Main4981 4982def Main():4983 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \4984 " or: exported-sql-viewer.py --help-only"4985 ap = argparse.ArgumentParser(usage = usage_str, add_help = False)4986 ap.add_argument("--pyside-version-1", action='store_true')4987 ap.add_argument("dbname", nargs="?")4988 ap.add_argument("--help-only", action='store_true')4989 args = ap.parse_args()4990 4991 if args.help_only:4992 app = QApplication(sys.argv)4993 mainwindow = HelpOnlyWindow()4994 mainwindow.show()4995 err = app.exec_()4996 sys.exit(err)4997 4998 dbname = args.dbname4999 if dbname is None:5000 ap.print_usage()5001 print("Too few arguments")5002 sys.exit(1)5003 5004 is_sqlite3 = False5005 try:5006 f = open(dbname, "rb")5007 if f.read(15) == b'SQLite format 3':5008 is_sqlite3 = True5009 f.close()5010 except:5011 pass5012 5013 dbref = DBRef(is_sqlite3, dbname)5014 db, dbname = dbref.Open("main")5015 glb = Glb(dbref, db, dbname)5016 app = QApplication(sys.argv)5017 glb.app = app5018 mainwindow = MainWindow(glb)5019 glb.mainwindow = mainwindow5020 mainwindow.show()5021 err = app.exec_()5022 glb.ShutdownInstances()5023 db.close()5024 sys.exit(err)5025 5026if __name__ == "__main__":5027 Main()5028