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