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