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