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