1#!/usr/bin/env python 2# SPDX-License-Identifier: GPL-2.0 3# exported-sql-viewer.py: view data from sql database 4# Copyright (c) 2014-2018, Intel Corporation. 5 6# To use this script you will need to have exported data using either the 7# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those 8# scripts for details. 9# 10# Following on from the example in the export scripts, a 11# call-graph can be displayed for the pt_example database like this: 12# 13# python tools/perf/scripts/python/exported-sql-viewer.py pt_example 14# 15# Note that for PostgreSQL, this script supports connecting to remote databases 16# by setting hostname, port, username, password, and dbname e.g. 17# 18# python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example" 19# 20# The result is a GUI window with a tree representing a context-sensitive 21# call-graph. Expanding a couple of levels of the tree and adjusting column 22# widths to suit will display something like: 23# 24# Call Graph: pt_example 25# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) 26# v- ls 27# v- 2638:2638 28# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 29# |- unknown unknown 1 13198 0.1 1 0.0 30# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 31# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 32# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 33# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 34# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 35# >- __libc_csu_init ls 1 10354 0.1 10 0.0 36# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 37# v- main ls 1 8182043 99.6 180254 99.9 38# 39# Points to note: 40# The top level is a command name (comm) 41# The next level is a thread (pid:tid) 42# Subsequent levels are functions 43# 'Count' is the number of calls 44# 'Time' is the elapsed time until the function returns 45# Percentages are relative to the level above 46# 'Branch Count' is the total number of branches for that function and all 47# functions that it calls 48 49# There is also a "All branches" report, which displays branches and 50# possibly disassembly. However, presently, the only supported disassembler is 51# Intel XED, and additionally the object code must be present in perf build ID 52# cache. To use Intel XED, libxed.so must be present. To build and install 53# libxed.so: 54# git clone https://github.com/intelxed/mbuild.git mbuild 55# git clone https://github.com/intelxed/xed 56# cd xed 57# ./mfile.py --share 58# sudo ./mfile.py --prefix=/usr/local install 59# sudo ldconfig 60# 61# Example report: 62# 63# Time CPU Command PID TID Branch Type In Tx Branch 64# 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) 65# 7fab593ea260 48 89 e7 mov %rsp, %rdi 66# 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) 67# 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so) 68# 7fab593ea260 48 89 e7 mov %rsp, %rdi 69# 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930 70# 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so) 71# 7fab593ea930 55 pushq %rbp 72# 7fab593ea931 48 89 e5 mov %rsp, %rbp 73# 7fab593ea934 41 57 pushq %r15 74# 7fab593ea936 41 56 pushq %r14 75# 7fab593ea938 41 55 pushq %r13 76# 7fab593ea93a 41 54 pushq %r12 77# 7fab593ea93c 53 pushq %rbx 78# 7fab593ea93d 48 89 fb mov %rdi, %rbx 79# 7fab593ea940 48 83 ec 68 sub $0x68, %rsp 80# 7fab593ea944 0f 31 rdtsc 81# 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx 82# 7fab593ea94a 89 c0 mov %eax, %eax 83# 7fab593ea94c 48 09 c2 or %rax, %rdx 84# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax 85# 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) 86# 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so) 87# 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax 88# 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip) 89# 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel]) 90 91from __future__ import print_function 92 93import sys 94# Only change warnings if the python -W option was not used 95if not sys.warnoptions: 96 import warnings 97 # PySide2 causes deprecation warnings, ignore them. 98 warnings.filterwarnings("ignore", category=DeprecationWarning) 99import argparse 100import weakref 101import threading 102import string 103try: 104 # Python2 105 import cPickle as pickle 106 # size of pickled integer big enough for record size 107 glb_nsz = 8 108except ImportError: 109 import pickle 110 glb_nsz = 16 111import re 112import os 113 114pyside_version_1 = True 115if not "--pyside-version-1" in sys.argv: 116 try: 117 from PySide2.QtCore import * 118 from PySide2.QtGui import * 119 from PySide2.QtSql import * 120 from PySide2.QtWidgets import * 121 pyside_version_1 = False 122 except: 123 pass 124 125if pyside_version_1: 126 from PySide.QtCore import * 127 from PySide.QtGui import * 128 from PySide.QtSql import * 129 130from decimal import Decimal, ROUND_HALF_UP 131from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \ 132 c_void_p, c_bool, c_byte, c_char, c_int, c_uint, c_longlong, c_ulonglong 133from multiprocessing import Process, Array, Value, Event 134 135# xrange is range in Python3 136try: 137 xrange 138except NameError: 139 xrange = range 140 141def printerr(*args, **keyword_args): 142 print(*args, file=sys.stderr, **keyword_args) 143 144# Data formatting helpers 145 146def tohex(ip): 147 if ip < 0: 148 ip += 1 << 64 149 return "%x" % ip 150 151def offstr(offset): 152 if offset: 153 return "+0x%x" % offset 154 return "" 155 156def dsoname(name): 157 if name == "[kernel.kallsyms]": 158 return "[kernel]" 159 return name 160 161def findnth(s, sub, n, offs=0): 162 pos = s.find(sub) 163 if pos < 0: 164 return pos 165 if n <= 1: 166 return offs + pos 167 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1) 168 169# Percent to one decimal place 170 171def PercentToOneDP(n, d): 172 if not d: 173 return "0.0" 174 x = (n * Decimal(100)) / d 175 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) 176 177# Helper for queries that must not fail 178 179def QueryExec(query, stmt): 180 ret = query.exec_(stmt) 181 if not ret: 182 raise Exception("Query failed: " + query.lastError().text()) 183 184# Background thread 185 186class Thread(QThread): 187 188 done = Signal(object) 189 190 def __init__(self, task, param=None, parent=None): 191 super(Thread, self).__init__(parent) 192 self.task = task 193 self.param = param 194 195 def run(self): 196 while True: 197 if self.param is None: 198 done, result = self.task() 199 else: 200 done, result = self.task(self.param) 201 self.done.emit(result) 202 if done: 203 break 204 205# Tree data model 206 207class TreeModel(QAbstractItemModel): 208 209 def __init__(self, glb, params, parent=None): 210 super(TreeModel, self).__init__(parent) 211 self.glb = glb 212 self.params = params 213 self.root = self.GetRoot() 214 self.last_row_read = 0 215 216 def Item(self, parent): 217 if parent.isValid(): 218 return parent.internalPointer() 219 else: 220 return self.root 221 222 def rowCount(self, parent): 223 result = self.Item(parent).childCount() 224 if result < 0: 225 result = 0 226 self.dataChanged.emit(parent, parent) 227 return result 228 229 def hasChildren(self, parent): 230 return self.Item(parent).hasChildren() 231 232 def headerData(self, section, orientation, role): 233 if role == Qt.TextAlignmentRole: 234 return self.columnAlignment(section) 235 if role != Qt.DisplayRole: 236 return None 237 if orientation != Qt.Horizontal: 238 return None 239 return self.columnHeader(section) 240 241 def parent(self, child): 242 child_item = child.internalPointer() 243 if child_item is self.root: 244 return QModelIndex() 245 parent_item = child_item.getParentItem() 246 return self.createIndex(parent_item.getRow(), 0, parent_item) 247 248 def index(self, row, column, parent): 249 child_item = self.Item(parent).getChildItem(row) 250 return self.createIndex(row, column, child_item) 251 252 def DisplayData(self, item, index): 253 return item.getData(index.column()) 254 255 def FetchIfNeeded(self, row): 256 if row > self.last_row_read: 257 self.last_row_read = row 258 if row + 10 >= self.root.child_count: 259 self.fetcher.Fetch(glb_chunk_sz) 260 261 def columnAlignment(self, column): 262 return Qt.AlignLeft 263 264 def columnFont(self, column): 265 return None 266 267 def data(self, index, role): 268 if role == Qt.TextAlignmentRole: 269 return self.columnAlignment(index.column()) 270 if role == Qt.FontRole: 271 return self.columnFont(index.column()) 272 if role != Qt.DisplayRole: 273 return None 274 item = index.internalPointer() 275 return self.DisplayData(item, index) 276 277# Table data model 278 279class TableModel(QAbstractTableModel): 280 281 def __init__(self, parent=None): 282 super(TableModel, self).__init__(parent) 283 self.child_count = 0 284 self.child_items = [] 285 self.last_row_read = 0 286 287 def Item(self, parent): 288 if parent.isValid(): 289 return parent.internalPointer() 290 else: 291 return self 292 293 def rowCount(self, parent): 294 return self.child_count 295 296 def headerData(self, section, orientation, role): 297 if role == Qt.TextAlignmentRole: 298 return self.columnAlignment(section) 299 if role != Qt.DisplayRole: 300 return None 301 if orientation != Qt.Horizontal: 302 return None 303 return self.columnHeader(section) 304 305 def index(self, row, column, parent): 306 return self.createIndex(row, column, self.child_items[row]) 307 308 def DisplayData(self, item, index): 309 return item.getData(index.column()) 310 311 def FetchIfNeeded(self, row): 312 if row > self.last_row_read: 313 self.last_row_read = row 314 if row + 10 >= self.child_count: 315 self.fetcher.Fetch(glb_chunk_sz) 316 317 def columnAlignment(self, column): 318 return Qt.AlignLeft 319 320 def columnFont(self, column): 321 return None 322 323 def data(self, index, role): 324 if role == Qt.TextAlignmentRole: 325 return self.columnAlignment(index.column()) 326 if role == Qt.FontRole: 327 return self.columnFont(index.column()) 328 if role != Qt.DisplayRole: 329 return None 330 item = index.internalPointer() 331 return self.DisplayData(item, index) 332 333# Model cache 334 335model_cache = weakref.WeakValueDictionary() 336model_cache_lock = threading.Lock() 337 338def LookupCreateModel(model_name, create_fn): 339 model_cache_lock.acquire() 340 try: 341 model = model_cache[model_name] 342 except: 343 model = None 344 if model is None: 345 model = create_fn() 346 model_cache[model_name] = model 347 model_cache_lock.release() 348 return model 349 350# Find bar 351 352class FindBar(): 353 354 def __init__(self, parent, finder, is_reg_expr=False): 355 self.finder = finder 356 self.context = [] 357 self.last_value = None 358 self.last_pattern = None 359 360 label = QLabel("Find:") 361 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 362 363 self.textbox = QComboBox() 364 self.textbox.setEditable(True) 365 self.textbox.currentIndexChanged.connect(self.ValueChanged) 366 367 self.progress = QProgressBar() 368 self.progress.setRange(0, 0) 369 self.progress.hide() 370 371 if is_reg_expr: 372 self.pattern = QCheckBox("Regular Expression") 373 else: 374 self.pattern = QCheckBox("Pattern") 375 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 376 377 self.next_button = QToolButton() 378 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown)) 379 self.next_button.released.connect(lambda: self.NextPrev(1)) 380 381 self.prev_button = QToolButton() 382 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp)) 383 self.prev_button.released.connect(lambda: self.NextPrev(-1)) 384 385 self.close_button = QToolButton() 386 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 387 self.close_button.released.connect(self.Deactivate) 388 389 self.hbox = QHBoxLayout() 390 self.hbox.setContentsMargins(0, 0, 0, 0) 391 392 self.hbox.addWidget(label) 393 self.hbox.addWidget(self.textbox) 394 self.hbox.addWidget(self.progress) 395 self.hbox.addWidget(self.pattern) 396 self.hbox.addWidget(self.next_button) 397 self.hbox.addWidget(self.prev_button) 398 self.hbox.addWidget(self.close_button) 399 400 self.bar = QWidget() 401 self.bar.setLayout(self.hbox) 402 self.bar.hide() 403 404 def Widget(self): 405 return self.bar 406 407 def Activate(self): 408 self.bar.show() 409 self.textbox.lineEdit().selectAll() 410 self.textbox.setFocus() 411 412 def Deactivate(self): 413 self.bar.hide() 414 415 def Busy(self): 416 self.textbox.setEnabled(False) 417 self.pattern.hide() 418 self.next_button.hide() 419 self.prev_button.hide() 420 self.progress.show() 421 422 def Idle(self): 423 self.textbox.setEnabled(True) 424 self.progress.hide() 425 self.pattern.show() 426 self.next_button.show() 427 self.prev_button.show() 428 429 def Find(self, direction): 430 value = self.textbox.currentText() 431 pattern = self.pattern.isChecked() 432 self.last_value = value 433 self.last_pattern = pattern 434 self.finder.Find(value, direction, pattern, self.context) 435 436 def ValueChanged(self): 437 value = self.textbox.currentText() 438 pattern = self.pattern.isChecked() 439 index = self.textbox.currentIndex() 440 data = self.textbox.itemData(index) 441 # Store the pattern in the combo box to keep it with the text value 442 if data == None: 443 self.textbox.setItemData(index, pattern) 444 else: 445 self.pattern.setChecked(data) 446 self.Find(0) 447 448 def NextPrev(self, direction): 449 value = self.textbox.currentText() 450 pattern = self.pattern.isChecked() 451 if value != self.last_value: 452 index = self.textbox.findText(value) 453 # Allow for a button press before the value has been added to the combo box 454 if index < 0: 455 index = self.textbox.count() 456 self.textbox.addItem(value, pattern) 457 self.textbox.setCurrentIndex(index) 458 return 459 else: 460 self.textbox.setItemData(index, pattern) 461 elif pattern != self.last_pattern: 462 # Keep the pattern recorded in the combo box up to date 463 index = self.textbox.currentIndex() 464 self.textbox.setItemData(index, pattern) 465 self.Find(direction) 466 467 def NotFound(self): 468 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found") 469 470# Context-sensitive call graph data model item base 471 472class CallGraphLevelItemBase(object): 473 474 def __init__(self, glb, params, row, parent_item): 475 self.glb = glb 476 self.params = params 477 self.row = row 478 self.parent_item = parent_item 479 self.query_done = False 480 self.child_count = 0 481 self.child_items = [] 482 if parent_item: 483 self.level = parent_item.level + 1 484 else: 485 self.level = 0 486 487 def getChildItem(self, row): 488 return self.child_items[row] 489 490 def getParentItem(self): 491 return self.parent_item 492 493 def getRow(self): 494 return self.row 495 496 def childCount(self): 497 if not self.query_done: 498 self.Select() 499 if not self.child_count: 500 return -1 501 return self.child_count 502 503 def hasChildren(self): 504 if not self.query_done: 505 return True 506 return self.child_count > 0 507 508 def getData(self, column): 509 return self.data[column] 510 511# Context-sensitive call graph data model level 2+ item base 512 513class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): 514 515 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item): 516 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) 517 self.comm_id = comm_id 518 self.thread_id = thread_id 519 self.call_path_id = call_path_id 520 self.insn_cnt = insn_cnt 521 self.cyc_cnt = cyc_cnt 522 self.branch_count = branch_count 523 self.time = time 524 525 def Select(self): 526 self.query_done = True 527 query = QSqlQuery(self.glb.db) 528 if self.params.have_ipc: 529 ipc_str = ", SUM(insn_count), SUM(cyc_count)" 530 else: 531 ipc_str = "" 532 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)" 533 " FROM calls" 534 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 535 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 536 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 537 " WHERE parent_call_path_id = " + str(self.call_path_id) + 538 " AND comm_id = " + str(self.comm_id) + 539 " AND thread_id = " + str(self.thread_id) + 540 " GROUP BY call_path_id, name, short_name" 541 " ORDER BY call_path_id") 542 while query.next(): 543 if self.params.have_ipc: 544 insn_cnt = int(query.value(5)) 545 cyc_cnt = int(query.value(6)) 546 branch_count = int(query.value(7)) 547 else: 548 insn_cnt = 0 549 cyc_cnt = 0 550 branch_count = int(query.value(5)) 551 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) 552 self.child_items.append(child_item) 553 self.child_count += 1 554 555# Context-sensitive call graph data model level three item 556 557class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): 558 559 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): 560 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item) 561 dso = dsoname(dso) 562 if self.params.have_ipc: 563 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) 564 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) 565 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) 566 ipc = CalcIPC(cyc_cnt, insn_cnt) 567 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 ] 568 else: 569 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 570 self.dbid = call_path_id 571 572# Context-sensitive call graph data model level two item 573 574class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): 575 576 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): 577 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item) 578 if self.params.have_ipc: 579 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] 580 else: 581 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 582 self.dbid = thread_id 583 584 def Select(self): 585 super(CallGraphLevelTwoItem, self).Select() 586 for child_item in self.child_items: 587 self.time += child_item.time 588 self.insn_cnt += child_item.insn_cnt 589 self.cyc_cnt += child_item.cyc_cnt 590 self.branch_count += child_item.branch_count 591 for child_item in self.child_items: 592 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 593 if self.params.have_ipc: 594 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) 595 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) 596 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) 597 else: 598 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 599 600# Context-sensitive call graph data model level one item 601 602class CallGraphLevelOneItem(CallGraphLevelItemBase): 603 604 def __init__(self, glb, params, row, comm_id, comm, parent_item): 605 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item) 606 if self.params.have_ipc: 607 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] 608 else: 609 self.data = [comm, "", "", "", "", "", ""] 610 self.dbid = comm_id 611 612 def Select(self): 613 self.query_done = True 614 query = QSqlQuery(self.glb.db) 615 QueryExec(query, "SELECT thread_id, pid, tid" 616 " FROM comm_threads" 617 " INNER JOIN threads ON thread_id = threads.id" 618 " WHERE comm_id = " + str(self.dbid)) 619 while query.next(): 620 child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 621 self.child_items.append(child_item) 622 self.child_count += 1 623 624# Context-sensitive call graph data model root item 625 626class CallGraphRootItem(CallGraphLevelItemBase): 627 628 def __init__(self, glb, params): 629 super(CallGraphRootItem, self).__init__(glb, params, 0, None) 630 self.dbid = 0 631 self.query_done = True 632 if_has_calls = "" 633 if IsSelectable(glb.db, "comms", columns = "has_calls"): 634 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE 635 query = QSqlQuery(glb.db) 636 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) 637 while query.next(): 638 if not query.value(0): 639 continue 640 child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) 641 self.child_items.append(child_item) 642 self.child_count += 1 643 644# Call graph model parameters 645 646class CallGraphModelParams(): 647 648 def __init__(self, glb, parent=None): 649 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count") 650 651# Context-sensitive call graph data model base 652 653class CallGraphModelBase(TreeModel): 654 655 def __init__(self, glb, parent=None): 656 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent) 657 658 def FindSelect(self, value, pattern, query): 659 if pattern: 660 # postgresql and sqlite pattern patching differences: 661 # postgresql LIKE is case sensitive but sqlite LIKE is not 662 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not 663 # postgresql supports ILIKE which is case insensitive 664 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive 665 if not self.glb.dbref.is_sqlite3: 666 # Escape % and _ 667 s = value.replace("%", "\%") 668 s = s.replace("_", "\_") 669 # Translate * and ? into SQL LIKE pattern characters % and _ 670 trans = string.maketrans("*?", "%_") 671 match = " LIKE '" + str(s).translate(trans) + "'" 672 else: 673 match = " GLOB '" + str(value) + "'" 674 else: 675 match = " = '" + str(value) + "'" 676 self.DoFindSelect(query, match) 677 678 def Found(self, query, found): 679 if found: 680 return self.FindPath(query) 681 return [] 682 683 def FindValue(self, value, pattern, query, last_value, last_pattern): 684 if last_value == value and pattern == last_pattern: 685 found = query.first() 686 else: 687 self.FindSelect(value, pattern, query) 688 found = query.next() 689 return self.Found(query, found) 690 691 def FindNext(self, query): 692 found = query.next() 693 if not found: 694 found = query.first() 695 return self.Found(query, found) 696 697 def FindPrev(self, query): 698 found = query.previous() 699 if not found: 700 found = query.last() 701 return self.Found(query, found) 702 703 def FindThread(self, c): 704 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern: 705 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern) 706 elif c.direction > 0: 707 ids = self.FindNext(c.query) 708 else: 709 ids = self.FindPrev(c.query) 710 return (True, ids) 711 712 def Find(self, value, direction, pattern, context, callback): 713 class Context(): 714 def __init__(self, *x): 715 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x 716 def Update(self, *x): 717 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern) 718 if len(context): 719 context[0].Update(value, direction, pattern) 720 else: 721 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None)) 722 # Use a thread so the UI is not blocked during the SELECT 723 thread = Thread(self.FindThread, context[0]) 724 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection) 725 thread.start() 726 727 def FindDone(self, thread, callback, ids): 728 callback(ids) 729 730# Context-sensitive call graph data model 731 732class CallGraphModel(CallGraphModelBase): 733 734 def __init__(self, glb, parent=None): 735 super(CallGraphModel, self).__init__(glb, parent) 736 737 def GetRoot(self): 738 return CallGraphRootItem(self.glb, self.params) 739 740 def columnCount(self, parent=None): 741 if self.params.have_ipc: 742 return 12 743 else: 744 return 7 745 746 def columnHeader(self, column): 747 if self.params.have_ipc: 748 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] 749 else: 750 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 751 return headers[column] 752 753 def columnAlignment(self, column): 754 if self.params.have_ipc: 755 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 ] 756 else: 757 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 758 return alignment[column] 759 760 def DoFindSelect(self, query, match): 761 QueryExec(query, "SELECT call_path_id, comm_id, thread_id" 762 " FROM calls" 763 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 764 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 765 " WHERE calls.id <> 0" 766 " AND symbols.name" + match + 767 " GROUP BY comm_id, thread_id, call_path_id" 768 " ORDER BY comm_id, thread_id, call_path_id") 769 770 def FindPath(self, query): 771 # Turn the query result into a list of ids that the tree view can walk 772 # to open the tree at the right place. 773 ids = [] 774 parent_id = query.value(0) 775 while parent_id: 776 ids.insert(0, parent_id) 777 q2 = QSqlQuery(self.glb.db) 778 QueryExec(q2, "SELECT parent_id" 779 " FROM call_paths" 780 " WHERE id = " + str(parent_id)) 781 if not q2.next(): 782 break 783 parent_id = q2.value(0) 784 # The call path root is not used 785 if ids[0] == 1: 786 del ids[0] 787 ids.insert(0, query.value(2)) 788 ids.insert(0, query.value(1)) 789 return ids 790 791# Call tree data model level 2+ item base 792 793class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase): 794 795 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, time, insn_cnt, cyc_cnt, branch_count, parent_item): 796 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item) 797 self.comm_id = comm_id 798 self.thread_id = thread_id 799 self.calls_id = calls_id 800 self.insn_cnt = insn_cnt 801 self.cyc_cnt = cyc_cnt 802 self.branch_count = branch_count 803 self.time = time 804 805 def Select(self): 806 self.query_done = True 807 if self.calls_id == 0: 808 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id) 809 else: 810 comm_thread = "" 811 if self.params.have_ipc: 812 ipc_str = ", insn_count, cyc_count" 813 else: 814 ipc_str = "" 815 query = QSqlQuery(self.glb.db) 816 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count" 817 " FROM calls" 818 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 819 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 820 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 821 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread + 822 " ORDER BY call_time, calls.id") 823 while query.next(): 824 if self.params.have_ipc: 825 insn_cnt = int(query.value(5)) 826 cyc_cnt = int(query.value(6)) 827 branch_count = int(query.value(7)) 828 else: 829 insn_cnt = 0 830 cyc_cnt = 0 831 branch_count = int(query.value(5)) 832 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) 833 self.child_items.append(child_item) 834 self.child_count += 1 835 836# Call tree data model level three item 837 838class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase): 839 840 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item): 841 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, time, insn_cnt, cyc_cnt, branch_count, parent_item) 842 dso = dsoname(dso) 843 if self.params.have_ipc: 844 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt) 845 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt) 846 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count) 847 ipc = CalcIPC(cyc_cnt, insn_cnt) 848 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 ] 849 else: 850 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 851 self.dbid = calls_id 852 853# Call tree data model level two item 854 855class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase): 856 857 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item): 858 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, parent_item) 859 if self.params.have_ipc: 860 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""] 861 else: 862 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 863 self.dbid = thread_id 864 865 def Select(self): 866 super(CallTreeLevelTwoItem, self).Select() 867 for child_item in self.child_items: 868 self.time += child_item.time 869 self.insn_cnt += child_item.insn_cnt 870 self.cyc_cnt += child_item.cyc_cnt 871 self.branch_count += child_item.branch_count 872 for child_item in self.child_items: 873 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 874 if self.params.have_ipc: 875 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt) 876 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt) 877 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count) 878 else: 879 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 880 881# Call tree data model level one item 882 883class CallTreeLevelOneItem(CallGraphLevelItemBase): 884 885 def __init__(self, glb, params, row, comm_id, comm, parent_item): 886 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item) 887 if self.params.have_ipc: 888 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""] 889 else: 890 self.data = [comm, "", "", "", "", "", ""] 891 self.dbid = comm_id 892 893 def Select(self): 894 self.query_done = True 895 query = QSqlQuery(self.glb.db) 896 QueryExec(query, "SELECT thread_id, pid, tid" 897 " FROM comm_threads" 898 " INNER JOIN threads ON thread_id = threads.id" 899 " WHERE comm_id = " + str(self.dbid)) 900 while query.next(): 901 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 902 self.child_items.append(child_item) 903 self.child_count += 1 904 905# Call tree data model root item 906 907class CallTreeRootItem(CallGraphLevelItemBase): 908 909 def __init__(self, glb, params): 910 super(CallTreeRootItem, self).__init__(glb, params, 0, None) 911 self.dbid = 0 912 self.query_done = True 913 if_has_calls = "" 914 if IsSelectable(glb.db, "comms", columns = "has_calls"): 915 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE 916 query = QSqlQuery(glb.db) 917 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls) 918 while query.next(): 919 if not query.value(0): 920 continue 921 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self) 922 self.child_items.append(child_item) 923 self.child_count += 1 924 925# Call Tree data model 926 927class CallTreeModel(CallGraphModelBase): 928 929 def __init__(self, glb, parent=None): 930 super(CallTreeModel, self).__init__(glb, parent) 931 932 def GetRoot(self): 933 return CallTreeRootItem(self.glb, self.params) 934 935 def columnCount(self, parent=None): 936 if self.params.have_ipc: 937 return 12 938 else: 939 return 7 940 941 def columnHeader(self, column): 942 if self.params.have_ipc: 943 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "] 944 else: 945 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 946 return headers[column] 947 948 def columnAlignment(self, column): 949 if self.params.have_ipc: 950 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 ] 951 else: 952 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 953 return alignment[column] 954 955 def DoFindSelect(self, query, match): 956 QueryExec(query, "SELECT calls.id, comm_id, thread_id" 957 " FROM calls" 958 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 959 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 960 " WHERE calls.id <> 0" 961 " AND symbols.name" + match + 962 " ORDER BY comm_id, thread_id, call_time, calls.id") 963 964 def FindPath(self, query): 965 # Turn the query result into a list of ids that the tree view can walk 966 # to open the tree at the right place. 967 ids = [] 968 parent_id = query.value(0) 969 while parent_id: 970 ids.insert(0, parent_id) 971 q2 = QSqlQuery(self.glb.db) 972 QueryExec(q2, "SELECT parent_id" 973 " FROM calls" 974 " WHERE id = " + str(parent_id)) 975 if not q2.next(): 976 break 977 parent_id = q2.value(0) 978 ids.insert(0, query.value(2)) 979 ids.insert(0, query.value(1)) 980 return ids 981 982# Vertical widget layout 983 984class VBox(): 985 986 def __init__(self, w1, w2, w3=None): 987 self.vbox = QWidget() 988 self.vbox.setLayout(QVBoxLayout()) 989 990 self.vbox.layout().setContentsMargins(0, 0, 0, 0) 991 992 self.vbox.layout().addWidget(w1) 993 self.vbox.layout().addWidget(w2) 994 if w3: 995 self.vbox.layout().addWidget(w3) 996 997 def Widget(self): 998 return self.vbox 999 1000# Tree window base 1001 1002class TreeWindowBase(QMdiSubWindow): 1003 1004 def __init__(self, parent=None): 1005 super(TreeWindowBase, self).__init__(parent) 1006 1007 self.model = None 1008 self.find_bar = None 1009 1010 self.view = QTreeView() 1011 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 1012 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard 1013 1014 self.context_menu = TreeContextMenu(self.view) 1015 1016 def DisplayFound(self, ids): 1017 if not len(ids): 1018 return False 1019 parent = QModelIndex() 1020 for dbid in ids: 1021 found = False 1022 n = self.model.rowCount(parent) 1023 for row in xrange(n): 1024 child = self.model.index(row, 0, parent) 1025 if child.internalPointer().dbid == dbid: 1026 found = True 1027 self.view.setExpanded(parent, True) 1028 self.view.setCurrentIndex(child) 1029 parent = child 1030 break 1031 if not found: 1032 break 1033 return found 1034 1035 def Find(self, value, direction, pattern, context): 1036 self.view.setFocus() 1037 self.find_bar.Busy() 1038 self.model.Find(value, direction, pattern, context, self.FindDone) 1039 1040 def FindDone(self, ids): 1041 found = True 1042 if not self.DisplayFound(ids): 1043 found = False 1044 self.find_bar.Idle() 1045 if not found: 1046 self.find_bar.NotFound() 1047 1048 1049# Context-sensitive call graph window 1050 1051class CallGraphWindow(TreeWindowBase): 1052 1053 def __init__(self, glb, parent=None): 1054 super(CallGraphWindow, self).__init__(parent) 1055 1056 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) 1057 1058 self.view.setModel(self.model) 1059 1060 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): 1061 self.view.setColumnWidth(c, w) 1062 1063 self.find_bar = FindBar(self, self) 1064 1065 self.vbox = VBox(self.view, self.find_bar.Widget()) 1066 1067 self.setWidget(self.vbox.Widget()) 1068 1069 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") 1070 1071# Call tree window 1072 1073class CallTreeWindow(TreeWindowBase): 1074 1075 def __init__(self, glb, parent=None): 1076 super(CallTreeWindow, self).__init__(parent) 1077 1078 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x)) 1079 1080 self.view.setModel(self.model) 1081 1082 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)): 1083 self.view.setColumnWidth(c, w) 1084 1085 self.find_bar = FindBar(self, self) 1086 1087 self.vbox = VBox(self.view, self.find_bar.Widget()) 1088 1089 self.setWidget(self.vbox.Widget()) 1090 1091 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree") 1092 1093# Child data item finder 1094 1095class ChildDataItemFinder(): 1096 1097 def __init__(self, root): 1098 self.root = root 1099 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5 1100 self.rows = [] 1101 self.pos = 0 1102 1103 def FindSelect(self): 1104 self.rows = [] 1105 if self.pattern: 1106 pattern = re.compile(self.value) 1107 for child in self.root.child_items: 1108 for column_data in child.data: 1109 if re.search(pattern, str(column_data)) is not None: 1110 self.rows.append(child.row) 1111 break 1112 else: 1113 for child in self.root.child_items: 1114 for column_data in child.data: 1115 if self.value in str(column_data): 1116 self.rows.append(child.row) 1117 break 1118 1119 def FindValue(self): 1120 self.pos = 0 1121 if self.last_value != self.value or self.pattern != self.last_pattern: 1122 self.FindSelect() 1123 if not len(self.rows): 1124 return -1 1125 return self.rows[self.pos] 1126 1127 def FindThread(self): 1128 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern: 1129 row = self.FindValue() 1130 elif len(self.rows): 1131 if self.direction > 0: 1132 self.pos += 1 1133 if self.pos >= len(self.rows): 1134 self.pos = 0 1135 else: 1136 self.pos -= 1 1137 if self.pos < 0: 1138 self.pos = len(self.rows) - 1 1139 row = self.rows[self.pos] 1140 else: 1141 row = -1 1142 return (True, row) 1143 1144 def Find(self, value, direction, pattern, context, callback): 1145 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern) 1146 # Use a thread so the UI is not blocked 1147 thread = Thread(self.FindThread) 1148 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection) 1149 thread.start() 1150 1151 def FindDone(self, thread, callback, row): 1152 callback(row) 1153 1154# Number of database records to fetch in one go 1155 1156glb_chunk_sz = 10000 1157 1158# Background process for SQL data fetcher 1159 1160class SQLFetcherProcess(): 1161 1162 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep): 1163 # Need a unique connection name 1164 conn_name = "SQLFetcher" + str(os.getpid()) 1165 self.db, dbname = dbref.Open(conn_name) 1166 self.sql = sql 1167 self.buffer = buffer 1168 self.head = head 1169 self.tail = tail 1170 self.fetch_count = fetch_count 1171 self.fetching_done = fetching_done 1172 self.process_target = process_target 1173 self.wait_event = wait_event 1174 self.fetched_event = fetched_event 1175 self.prep = prep 1176 self.query = QSqlQuery(self.db) 1177 self.query_limit = 0 if "$$last_id$$" in sql else 2 1178 self.last_id = -1 1179 self.fetched = 0 1180 self.more = True 1181 self.local_head = self.head.value 1182 self.local_tail = self.tail.value 1183 1184 def Select(self): 1185 if self.query_limit: 1186 if self.query_limit == 1: 1187 return 1188 self.query_limit -= 1 1189 stmt = self.sql.replace("$$last_id$$", str(self.last_id)) 1190 QueryExec(self.query, stmt) 1191 1192 def Next(self): 1193 if not self.query.next(): 1194 self.Select() 1195 if not self.query.next(): 1196 return None 1197 self.last_id = self.query.value(0) 1198 return self.prep(self.query) 1199 1200 def WaitForTarget(self): 1201 while True: 1202 self.wait_event.clear() 1203 target = self.process_target.value 1204 if target > self.fetched or target < 0: 1205 break 1206 self.wait_event.wait() 1207 return target 1208 1209 def HasSpace(self, sz): 1210 if self.local_tail <= self.local_head: 1211 space = len(self.buffer) - self.local_head 1212 if space > sz: 1213 return True 1214 if space >= glb_nsz: 1215 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer 1216 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL) 1217 self.buffer[self.local_head : self.local_head + len(nd)] = nd 1218 self.local_head = 0 1219 if self.local_tail - self.local_head > sz: 1220 return True 1221 return False 1222 1223 def WaitForSpace(self, sz): 1224 if self.HasSpace(sz): 1225 return 1226 while True: 1227 self.wait_event.clear() 1228 self.local_tail = self.tail.value 1229 if self.HasSpace(sz): 1230 return 1231 self.wait_event.wait() 1232 1233 def AddToBuffer(self, obj): 1234 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL) 1235 n = len(d) 1236 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL) 1237 sz = n + glb_nsz 1238 self.WaitForSpace(sz) 1239 pos = self.local_head 1240 self.buffer[pos : pos + len(nd)] = nd 1241 self.buffer[pos + glb_nsz : pos + sz] = d 1242 self.local_head += sz 1243 1244 def FetchBatch(self, batch_size): 1245 fetched = 0 1246 while batch_size > fetched: 1247 obj = self.Next() 1248 if obj is None: 1249 self.more = False 1250 break 1251 self.AddToBuffer(obj) 1252 fetched += 1 1253 if fetched: 1254 self.fetched += fetched 1255 with self.fetch_count.get_lock(): 1256 self.fetch_count.value += fetched 1257 self.head.value = self.local_head 1258 self.fetched_event.set() 1259 1260 def Run(self): 1261 while self.more: 1262 target = self.WaitForTarget() 1263 if target < 0: 1264 break 1265 batch_size = min(glb_chunk_sz, target - self.fetched) 1266 self.FetchBatch(batch_size) 1267 self.fetching_done.value = True 1268 self.fetched_event.set() 1269 1270def SQLFetcherFn(*x): 1271 process = SQLFetcherProcess(*x) 1272 process.Run() 1273 1274# SQL data fetcher 1275 1276class SQLFetcher(QObject): 1277 1278 done = Signal(object) 1279 1280 def __init__(self, glb, sql, prep, process_data, parent=None): 1281 super(SQLFetcher, self).__init__(parent) 1282 self.process_data = process_data 1283 self.more = True 1284 self.target = 0 1285 self.last_target = 0 1286 self.fetched = 0 1287 self.buffer_size = 16 * 1024 * 1024 1288 self.buffer = Array(c_char, self.buffer_size, lock=False) 1289 self.head = Value(c_longlong) 1290 self.tail = Value(c_longlong) 1291 self.local_tail = 0 1292 self.fetch_count = Value(c_longlong) 1293 self.fetching_done = Value(c_bool) 1294 self.last_count = 0 1295 self.process_target = Value(c_longlong) 1296 self.wait_event = Event() 1297 self.fetched_event = Event() 1298 glb.AddInstanceToShutdownOnExit(self) 1299 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)) 1300 self.process.start() 1301 self.thread = Thread(self.Thread) 1302 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection) 1303 self.thread.start() 1304 1305 def Shutdown(self): 1306 # Tell the thread and process to exit 1307 self.process_target.value = -1 1308 self.wait_event.set() 1309 self.more = False 1310 self.fetching_done.value = True 1311 self.fetched_event.set() 1312 1313 def Thread(self): 1314 if not self.more: 1315 return True, 0 1316 while True: 1317 self.fetched_event.clear() 1318 fetch_count = self.fetch_count.value 1319 if fetch_count != self.last_count: 1320 break 1321 if self.fetching_done.value: 1322 self.more = False 1323 return True, 0 1324 self.fetched_event.wait() 1325 count = fetch_count - self.last_count 1326 self.last_count = fetch_count 1327 self.fetched += count 1328 return False, count 1329 1330 def Fetch(self, nr): 1331 if not self.more: 1332 # -1 inidcates there are no more 1333 return -1 1334 result = self.fetched 1335 extra = result + nr - self.target 1336 if extra > 0: 1337 self.target += extra 1338 # process_target < 0 indicates shutting down 1339 if self.process_target.value >= 0: 1340 self.process_target.value = self.target 1341 self.wait_event.set() 1342 return result 1343 1344 def RemoveFromBuffer(self): 1345 pos = self.local_tail 1346 if len(self.buffer) - pos < glb_nsz: 1347 pos = 0 1348 n = pickle.loads(self.buffer[pos : pos + glb_nsz]) 1349 if n == 0: 1350 pos = 0 1351 n = pickle.loads(self.buffer[0 : glb_nsz]) 1352 pos += glb_nsz 1353 obj = pickle.loads(self.buffer[pos : pos + n]) 1354 self.local_tail = pos + n 1355 return obj 1356 1357 def ProcessData(self, count): 1358 for i in xrange(count): 1359 obj = self.RemoveFromBuffer() 1360 self.process_data(obj) 1361 self.tail.value = self.local_tail 1362 self.wait_event.set() 1363 self.done.emit(count) 1364 1365# Fetch more records bar 1366 1367class FetchMoreRecordsBar(): 1368 1369 def __init__(self, model, parent): 1370 self.model = model 1371 1372 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:") 1373 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 1374 1375 self.fetch_count = QSpinBox() 1376 self.fetch_count.setRange(1, 1000000) 1377 self.fetch_count.setValue(10) 1378 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 1379 1380 self.fetch = QPushButton("Go!") 1381 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 1382 self.fetch.released.connect(self.FetchMoreRecords) 1383 1384 self.progress = QProgressBar() 1385 self.progress.setRange(0, 100) 1386 self.progress.hide() 1387 1388 self.done_label = QLabel("All records fetched") 1389 self.done_label.hide() 1390 1391 self.spacer = QLabel("") 1392 1393 self.close_button = QToolButton() 1394 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 1395 self.close_button.released.connect(self.Deactivate) 1396 1397 self.hbox = QHBoxLayout() 1398 self.hbox.setContentsMargins(0, 0, 0, 0) 1399 1400 self.hbox.addWidget(self.label) 1401 self.hbox.addWidget(self.fetch_count) 1402 self.hbox.addWidget(self.fetch) 1403 self.hbox.addWidget(self.spacer) 1404 self.hbox.addWidget(self.progress) 1405 self.hbox.addWidget(self.done_label) 1406 self.hbox.addWidget(self.close_button) 1407 1408 self.bar = QWidget() 1409 self.bar.setLayout(self.hbox) 1410 self.bar.show() 1411 1412 self.in_progress = False 1413 self.model.progress.connect(self.Progress) 1414 1415 self.done = False 1416 1417 if not model.HasMoreRecords(): 1418 self.Done() 1419 1420 def Widget(self): 1421 return self.bar 1422 1423 def Activate(self): 1424 self.bar.show() 1425 self.fetch.setFocus() 1426 1427 def Deactivate(self): 1428 self.bar.hide() 1429 1430 def Enable(self, enable): 1431 self.fetch.setEnabled(enable) 1432 self.fetch_count.setEnabled(enable) 1433 1434 def Busy(self): 1435 self.Enable(False) 1436 self.fetch.hide() 1437 self.spacer.hide() 1438 self.progress.show() 1439 1440 def Idle(self): 1441 self.in_progress = False 1442 self.Enable(True) 1443 self.progress.hide() 1444 self.fetch.show() 1445 self.spacer.show() 1446 1447 def Target(self): 1448 return self.fetch_count.value() * glb_chunk_sz 1449 1450 def Done(self): 1451 self.done = True 1452 self.Idle() 1453 self.label.hide() 1454 self.fetch_count.hide() 1455 self.fetch.hide() 1456 self.spacer.hide() 1457 self.done_label.show() 1458 1459 def Progress(self, count): 1460 if self.in_progress: 1461 if count: 1462 percent = ((count - self.start) * 100) / self.Target() 1463 if percent >= 100: 1464 self.Idle() 1465 else: 1466 self.progress.setValue(percent) 1467 if not count: 1468 # Count value of zero means no more records 1469 self.Done() 1470 1471 def FetchMoreRecords(self): 1472 if self.done: 1473 return 1474 self.progress.setValue(0) 1475 self.Busy() 1476 self.in_progress = True 1477 self.start = self.model.FetchMoreRecords(self.Target()) 1478 1479# Brance data model level two item 1480 1481class BranchLevelTwoItem(): 1482 1483 def __init__(self, row, col, text, parent_item): 1484 self.row = row 1485 self.parent_item = parent_item 1486 self.data = [""] * (col + 1) 1487 self.data[col] = text 1488 self.level = 2 1489 1490 def getParentItem(self): 1491 return self.parent_item 1492 1493 def getRow(self): 1494 return self.row 1495 1496 def childCount(self): 1497 return 0 1498 1499 def hasChildren(self): 1500 return False 1501 1502 def getData(self, column): 1503 return self.data[column] 1504 1505# Brance data model level one item 1506 1507class BranchLevelOneItem(): 1508 1509 def __init__(self, glb, row, data, parent_item): 1510 self.glb = glb 1511 self.row = row 1512 self.parent_item = parent_item 1513 self.child_count = 0 1514 self.child_items = [] 1515 self.data = data[1:] 1516 self.dbid = data[0] 1517 self.level = 1 1518 self.query_done = False 1519 self.br_col = len(self.data) - 1 1520 1521 def getChildItem(self, row): 1522 return self.child_items[row] 1523 1524 def getParentItem(self): 1525 return self.parent_item 1526 1527 def getRow(self): 1528 return self.row 1529 1530 def Select(self): 1531 self.query_done = True 1532 1533 if not self.glb.have_disassembler: 1534 return 1535 1536 query = QSqlQuery(self.glb.db) 1537 1538 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip" 1539 " FROM samples" 1540 " INNER JOIN dsos ON samples.to_dso_id = dsos.id" 1541 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id" 1542 " WHERE samples.id = " + str(self.dbid)) 1543 if not query.next(): 1544 return 1545 cpu = query.value(0) 1546 dso = query.value(1) 1547 sym = query.value(2) 1548 if dso == 0 or sym == 0: 1549 return 1550 off = query.value(3) 1551 short_name = query.value(4) 1552 long_name = query.value(5) 1553 build_id = query.value(6) 1554 sym_start = query.value(7) 1555 ip = query.value(8) 1556 1557 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start" 1558 " FROM samples" 1559 " INNER JOIN symbols ON samples.symbol_id = symbols.id" 1560 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) + 1561 " ORDER BY samples.id" 1562 " LIMIT 1") 1563 if not query.next(): 1564 return 1565 if query.value(0) != dso: 1566 # Cannot disassemble from one dso to another 1567 return 1568 bsym = query.value(1) 1569 boff = query.value(2) 1570 bsym_start = query.value(3) 1571 if bsym == 0: 1572 return 1573 tot = bsym_start + boff + 1 - sym_start - off 1574 if tot <= 0 or tot > 16384: 1575 return 1576 1577 inst = self.glb.disassembler.Instruction() 1578 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id) 1579 if not f: 1580 return 1581 mode = 0 if Is64Bit(f) else 1 1582 self.glb.disassembler.SetMode(inst, mode) 1583 1584 buf_sz = tot + 16 1585 buf = create_string_buffer(tot + 16) 1586 f.seek(sym_start + off) 1587 buf.value = f.read(buf_sz) 1588 buf_ptr = addressof(buf) 1589 i = 0 1590 while tot > 0: 1591 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip) 1592 if cnt: 1593 byte_str = tohex(ip).rjust(16) 1594 for k in xrange(cnt): 1595 byte_str += " %02x" % ord(buf[i]) 1596 i += 1 1597 while k < 15: 1598 byte_str += " " 1599 k += 1 1600 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self)) 1601 self.child_count += 1 1602 else: 1603 return 1604 buf_ptr += cnt 1605 tot -= cnt 1606 buf_sz -= cnt 1607 ip += cnt 1608 1609 def childCount(self): 1610 if not self.query_done: 1611 self.Select() 1612 if not self.child_count: 1613 return -1 1614 return self.child_count 1615 1616 def hasChildren(self): 1617 if not self.query_done: 1618 return True 1619 return self.child_count > 0 1620 1621 def getData(self, column): 1622 return self.data[column] 1623 1624# Brance data model root item 1625 1626class BranchRootItem(): 1627 1628 def __init__(self): 1629 self.child_count = 0 1630 self.child_items = [] 1631 self.level = 0 1632 1633 def getChildItem(self, row): 1634 return self.child_items[row] 1635 1636 def getParentItem(self): 1637 return None 1638 1639 def getRow(self): 1640 return 0 1641 1642 def childCount(self): 1643 return self.child_count 1644 1645 def hasChildren(self): 1646 return self.child_count > 0 1647 1648 def getData(self, column): 1649 return "" 1650 1651# Calculate instructions per cycle 1652 1653def CalcIPC(cyc_cnt, insn_cnt): 1654 if cyc_cnt and insn_cnt: 1655 ipc = Decimal(float(insn_cnt) / cyc_cnt) 1656 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP)) 1657 else: 1658 ipc = "0" 1659 return ipc 1660 1661# Branch data preparation 1662 1663def BranchDataPrepBr(query, data): 1664 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) + 1665 " (" + dsoname(query.value(11)) + ")" + " -> " + 1666 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) + 1667 " (" + dsoname(query.value(15)) + ")") 1668 1669def BranchDataPrepIPC(query, data): 1670 insn_cnt = query.value(16) 1671 cyc_cnt = query.value(17) 1672 ipc = CalcIPC(cyc_cnt, insn_cnt) 1673 data.append(insn_cnt) 1674 data.append(cyc_cnt) 1675 data.append(ipc) 1676 1677def BranchDataPrep(query): 1678 data = [] 1679 for i in xrange(0, 8): 1680 data.append(query.value(i)) 1681 BranchDataPrepBr(query, data) 1682 return data 1683 1684def BranchDataPrepWA(query): 1685 data = [] 1686 data.append(query.value(0)) 1687 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 1688 data.append("{:>19}".format(query.value(1))) 1689 for i in xrange(2, 8): 1690 data.append(query.value(i)) 1691 BranchDataPrepBr(query, data) 1692 return data 1693 1694def BranchDataWithIPCPrep(query): 1695 data = [] 1696 for i in xrange(0, 8): 1697 data.append(query.value(i)) 1698 BranchDataPrepIPC(query, data) 1699 BranchDataPrepBr(query, data) 1700 return data 1701 1702def BranchDataWithIPCPrepWA(query): 1703 data = [] 1704 data.append(query.value(0)) 1705 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 1706 data.append("{:>19}".format(query.value(1))) 1707 for i in xrange(2, 8): 1708 data.append(query.value(i)) 1709 BranchDataPrepIPC(query, data) 1710 BranchDataPrepBr(query, data) 1711 return data 1712 1713# Branch data model 1714 1715class BranchModel(TreeModel): 1716 1717 progress = Signal(object) 1718 1719 def __init__(self, glb, event_id, where_clause, parent=None): 1720 super(BranchModel, self).__init__(glb, None, parent) 1721 self.event_id = event_id 1722 self.more = True 1723 self.populated = 0 1724 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count") 1725 if self.have_ipc: 1726 select_ipc = ", insn_count, cyc_count" 1727 prep_fn = BranchDataWithIPCPrep 1728 prep_wa_fn = BranchDataWithIPCPrepWA 1729 else: 1730 select_ipc = "" 1731 prep_fn = BranchDataPrep 1732 prep_wa_fn = BranchDataPrepWA 1733 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name," 1734 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END," 1735 " ip, symbols.name, sym_offset, dsos.short_name," 1736 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name" 1737 + select_ipc + 1738 " FROM samples" 1739 " INNER JOIN comms ON comm_id = comms.id" 1740 " INNER JOIN threads ON thread_id = threads.id" 1741 " INNER JOIN branch_types ON branch_type = branch_types.id" 1742 " INNER JOIN symbols ON symbol_id = symbols.id" 1743 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id" 1744 " INNER JOIN dsos ON samples.dso_id = dsos.id" 1745 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id" 1746 " WHERE samples.id > $$last_id$$" + where_clause + 1747 " AND evsel_id = " + str(self.event_id) + 1748 " ORDER BY samples.id" 1749 " LIMIT " + str(glb_chunk_sz)) 1750 if pyside_version_1 and sys.version_info[0] == 3: 1751 prep = prep_fn 1752 else: 1753 prep = prep_wa_fn 1754 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample) 1755 self.fetcher.done.connect(self.Update) 1756 self.fetcher.Fetch(glb_chunk_sz) 1757 1758 def GetRoot(self): 1759 return BranchRootItem() 1760 1761 def columnCount(self, parent=None): 1762 if self.have_ipc: 1763 return 11 1764 else: 1765 return 8 1766 1767 def columnHeader(self, column): 1768 if self.have_ipc: 1769 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column] 1770 else: 1771 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column] 1772 1773 def columnFont(self, column): 1774 if self.have_ipc: 1775 br_col = 10 1776 else: 1777 br_col = 7 1778 if column != br_col: 1779 return None 1780 return QFont("Monospace") 1781 1782 def DisplayData(self, item, index): 1783 if item.level == 1: 1784 self.FetchIfNeeded(item.row) 1785 return item.getData(index.column()) 1786 1787 def AddSample(self, data): 1788 child = BranchLevelOneItem(self.glb, self.populated, data, self.root) 1789 self.root.child_items.append(child) 1790 self.populated += 1 1791 1792 def Update(self, fetched): 1793 if not fetched: 1794 self.more = False 1795 self.progress.emit(0) 1796 child_count = self.root.child_count 1797 count = self.populated - child_count 1798 if count > 0: 1799 parent = QModelIndex() 1800 self.beginInsertRows(parent, child_count, child_count + count - 1) 1801 self.insertRows(child_count, count, parent) 1802 self.root.child_count += count 1803 self.endInsertRows() 1804 self.progress.emit(self.root.child_count) 1805 1806 def FetchMoreRecords(self, count): 1807 current = self.root.child_count 1808 if self.more: 1809 self.fetcher.Fetch(count) 1810 else: 1811 self.progress.emit(0) 1812 return current 1813 1814 def HasMoreRecords(self): 1815 return self.more 1816 1817# Report Variables 1818 1819class ReportVars(): 1820 1821 def __init__(self, name = "", where_clause = "", limit = ""): 1822 self.name = name 1823 self.where_clause = where_clause 1824 self.limit = limit 1825 1826 def UniqueId(self): 1827 return str(self.where_clause + ";" + self.limit) 1828 1829# Branch window 1830 1831class BranchWindow(QMdiSubWindow): 1832 1833 def __init__(self, glb, event_id, report_vars, parent=None): 1834 super(BranchWindow, self).__init__(parent) 1835 1836 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId() 1837 1838 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause)) 1839 1840 self.view = QTreeView() 1841 self.view.setUniformRowHeights(True) 1842 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 1843 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard 1844 self.view.setModel(self.model) 1845 1846 self.ResizeColumnsToContents() 1847 1848 self.context_menu = TreeContextMenu(self.view) 1849 1850 self.find_bar = FindBar(self, self, True) 1851 1852 self.finder = ChildDataItemFinder(self.model.root) 1853 1854 self.fetch_bar = FetchMoreRecordsBar(self.model, self) 1855 1856 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 1857 1858 self.setWidget(self.vbox.Widget()) 1859 1860 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events") 1861 1862 def ResizeColumnToContents(self, column, n): 1863 # Using the view's resizeColumnToContents() here is extrememly slow 1864 # so implement a crude alternative 1865 mm = "MM" if column else "MMMM" 1866 font = self.view.font() 1867 metrics = QFontMetrics(font) 1868 max = 0 1869 for row in xrange(n): 1870 val = self.model.root.child_items[row].data[column] 1871 len = metrics.width(str(val) + mm) 1872 max = len if len > max else max 1873 val = self.model.columnHeader(column) 1874 len = metrics.width(str(val) + mm) 1875 max = len if len > max else max 1876 self.view.setColumnWidth(column, max) 1877 1878 def ResizeColumnsToContents(self): 1879 n = min(self.model.root.child_count, 100) 1880 if n < 1: 1881 # No data yet, so connect a signal to notify when there is 1882 self.model.rowsInserted.connect(self.UpdateColumnWidths) 1883 return 1884 columns = self.model.columnCount() 1885 for i in xrange(columns): 1886 self.ResizeColumnToContents(i, n) 1887 1888 def UpdateColumnWidths(self, *x): 1889 # This only needs to be done once, so disconnect the signal now 1890 self.model.rowsInserted.disconnect(self.UpdateColumnWidths) 1891 self.ResizeColumnsToContents() 1892 1893 def Find(self, value, direction, pattern, context): 1894 self.view.setFocus() 1895 self.find_bar.Busy() 1896 self.finder.Find(value, direction, pattern, context, self.FindDone) 1897 1898 def FindDone(self, row): 1899 self.find_bar.Idle() 1900 if row >= 0: 1901 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) 1902 else: 1903 self.find_bar.NotFound() 1904 1905# Line edit data item 1906 1907class LineEditDataItem(object): 1908 1909 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): 1910 self.glb = glb 1911 self.label = label 1912 self.placeholder_text = placeholder_text 1913 self.parent = parent 1914 self.id = id 1915 1916 self.value = default 1917 1918 self.widget = QLineEdit(default) 1919 self.widget.editingFinished.connect(self.Validate) 1920 self.widget.textChanged.connect(self.Invalidate) 1921 self.red = False 1922 self.error = "" 1923 self.validated = True 1924 1925 if placeholder_text: 1926 self.widget.setPlaceholderText(placeholder_text) 1927 1928 def TurnTextRed(self): 1929 if not self.red: 1930 palette = QPalette() 1931 palette.setColor(QPalette.Text,Qt.red) 1932 self.widget.setPalette(palette) 1933 self.red = True 1934 1935 def TurnTextNormal(self): 1936 if self.red: 1937 palette = QPalette() 1938 self.widget.setPalette(palette) 1939 self.red = False 1940 1941 def InvalidValue(self, value): 1942 self.value = "" 1943 self.TurnTextRed() 1944 self.error = self.label + " invalid value '" + value + "'" 1945 self.parent.ShowMessage(self.error) 1946 1947 def Invalidate(self): 1948 self.validated = False 1949 1950 def DoValidate(self, input_string): 1951 self.value = input_string.strip() 1952 1953 def Validate(self): 1954 self.validated = True 1955 self.error = "" 1956 self.TurnTextNormal() 1957 self.parent.ClearMessage() 1958 input_string = self.widget.text() 1959 if not len(input_string.strip()): 1960 self.value = "" 1961 return 1962 self.DoValidate(input_string) 1963 1964 def IsValid(self): 1965 if not self.validated: 1966 self.Validate() 1967 if len(self.error): 1968 self.parent.ShowMessage(self.error) 1969 return False 1970 return True 1971 1972 def IsNumber(self, value): 1973 try: 1974 x = int(value) 1975 except: 1976 x = 0 1977 return str(x) == value 1978 1979# Non-negative integer ranges dialog data item 1980 1981class NonNegativeIntegerRangesDataItem(LineEditDataItem): 1982 1983 def __init__(self, glb, label, placeholder_text, column_name, parent): 1984 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent) 1985 1986 self.column_name = column_name 1987 1988 def DoValidate(self, input_string): 1989 singles = [] 1990 ranges = [] 1991 for value in [x.strip() for x in input_string.split(",")]: 1992 if "-" in value: 1993 vrange = value.split("-") 1994 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): 1995 return self.InvalidValue(value) 1996 ranges.append(vrange) 1997 else: 1998 if not self.IsNumber(value): 1999 return self.InvalidValue(value) 2000 singles.append(value) 2001 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] 2002 if len(singles): 2003 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")") 2004 self.value = " OR ".join(ranges) 2005 2006# Positive integer dialog data item 2007 2008class PositiveIntegerDataItem(LineEditDataItem): 2009 2010 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""): 2011 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default) 2012 2013 def DoValidate(self, input_string): 2014 if not self.IsNumber(input_string.strip()): 2015 return self.InvalidValue(input_string) 2016 value = int(input_string.strip()) 2017 if value <= 0: 2018 return self.InvalidValue(input_string) 2019 self.value = str(value) 2020 2021# Dialog data item converted and validated using a SQL table 2022 2023class SQLTableDataItem(LineEditDataItem): 2024 2025 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent): 2026 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent) 2027 2028 self.table_name = table_name 2029 self.match_column = match_column 2030 self.column_name1 = column_name1 2031 self.column_name2 = column_name2 2032 2033 def ValueToIds(self, value): 2034 ids = [] 2035 query = QSqlQuery(self.glb.db) 2036 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'" 2037 ret = query.exec_(stmt) 2038 if ret: 2039 while query.next(): 2040 ids.append(str(query.value(0))) 2041 return ids 2042 2043 def DoValidate(self, input_string): 2044 all_ids = [] 2045 for value in [x.strip() for x in input_string.split(",")]: 2046 ids = self.ValueToIds(value) 2047 if len(ids): 2048 all_ids.extend(ids) 2049 else: 2050 return self.InvalidValue(value) 2051 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")" 2052 if self.column_name2: 2053 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )" 2054 2055# Sample time ranges dialog data item converted and validated using 'samples' SQL table 2056 2057class SampleTimeRangesDataItem(LineEditDataItem): 2058 2059 def __init__(self, glb, label, placeholder_text, column_name, parent): 2060 self.column_name = column_name 2061 2062 self.last_id = 0 2063 self.first_time = 0 2064 self.last_time = 2 ** 64 2065 2066 query = QSqlQuery(glb.db) 2067 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1") 2068 if query.next(): 2069 self.last_id = int(query.value(0)) 2070 self.last_time = int(query.value(1)) 2071 QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1") 2072 if query.next(): 2073 self.first_time = int(query.value(0)) 2074 if placeholder_text: 2075 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time) 2076 2077 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent) 2078 2079 def IdBetween(self, query, lower_id, higher_id, order): 2080 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1") 2081 if query.next(): 2082 return True, int(query.value(0)) 2083 else: 2084 return False, 0 2085 2086 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor): 2087 query = QSqlQuery(self.glb.db) 2088 while True: 2089 next_id = int((lower_id + higher_id) / 2) 2090 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) 2091 if not query.next(): 2092 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC") 2093 if not ok: 2094 ok, dbid = self.IdBetween(query, next_id, higher_id, "") 2095 if not ok: 2096 return str(higher_id) 2097 next_id = dbid 2098 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) 2099 next_time = int(query.value(0)) 2100 if get_floor: 2101 if target_time > next_time: 2102 lower_id = next_id 2103 else: 2104 higher_id = next_id 2105 if higher_id <= lower_id + 1: 2106 return str(higher_id) 2107 else: 2108 if target_time >= next_time: 2109 lower_id = next_id 2110 else: 2111 higher_id = next_id 2112 if higher_id <= lower_id + 1: 2113 return str(lower_id) 2114 2115 def ConvertRelativeTime(self, val): 2116 mult = 1 2117 suffix = val[-2:] 2118 if suffix == "ms": 2119 mult = 1000000 2120 elif suffix == "us": 2121 mult = 1000 2122 elif suffix == "ns": 2123 mult = 1 2124 else: 2125 return val 2126 val = val[:-2].strip() 2127 if not self.IsNumber(val): 2128 return val 2129 val = int(val) * mult 2130 if val >= 0: 2131 val += self.first_time 2132 else: 2133 val += self.last_time 2134 return str(val) 2135 2136 def ConvertTimeRange(self, vrange): 2137 if vrange[0] == "": 2138 vrange[0] = str(self.first_time) 2139 if vrange[1] == "": 2140 vrange[1] = str(self.last_time) 2141 vrange[0] = self.ConvertRelativeTime(vrange[0]) 2142 vrange[1] = self.ConvertRelativeTime(vrange[1]) 2143 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): 2144 return False 2145 beg_range = max(int(vrange[0]), self.first_time) 2146 end_range = min(int(vrange[1]), self.last_time) 2147 if beg_range > self.last_time or end_range < self.first_time: 2148 return False 2149 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True) 2150 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False) 2151 return True 2152 2153 def AddTimeRange(self, value, ranges): 2154 n = value.count("-") 2155 if n == 1: 2156 pass 2157 elif n == 2: 2158 if value.split("-")[1].strip() == "": 2159 n = 1 2160 elif n == 3: 2161 n = 2 2162 else: 2163 return False 2164 pos = findnth(value, "-", n) 2165 vrange = [value[:pos].strip() ,value[pos+1:].strip()] 2166 if self.ConvertTimeRange(vrange): 2167 ranges.append(vrange) 2168 return True 2169 return False 2170 2171 def DoValidate(self, input_string): 2172 ranges = [] 2173 for value in [x.strip() for x in input_string.split(",")]: 2174 if not self.AddTimeRange(value, ranges): 2175 return self.InvalidValue(value) 2176 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges] 2177 self.value = " OR ".join(ranges) 2178 2179# Report Dialog Base 2180 2181class ReportDialogBase(QDialog): 2182 2183 def __init__(self, glb, title, items, partial, parent=None): 2184 super(ReportDialogBase, self).__init__(parent) 2185 2186 self.glb = glb 2187 2188 self.report_vars = ReportVars() 2189 2190 self.setWindowTitle(title) 2191 self.setMinimumWidth(600) 2192 2193 self.data_items = [x(glb, self) for x in items] 2194 2195 self.partial = partial 2196 2197 self.grid = QGridLayout() 2198 2199 for row in xrange(len(self.data_items)): 2200 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0) 2201 self.grid.addWidget(self.data_items[row].widget, row, 1) 2202 2203 self.status = QLabel() 2204 2205 self.ok_button = QPushButton("Ok", self) 2206 self.ok_button.setDefault(True) 2207 self.ok_button.released.connect(self.Ok) 2208 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 2209 2210 self.cancel_button = QPushButton("Cancel", self) 2211 self.cancel_button.released.connect(self.reject) 2212 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 2213 2214 self.hbox = QHBoxLayout() 2215 #self.hbox.addStretch() 2216 self.hbox.addWidget(self.status) 2217 self.hbox.addWidget(self.ok_button) 2218 self.hbox.addWidget(self.cancel_button) 2219 2220 self.vbox = QVBoxLayout() 2221 self.vbox.addLayout(self.grid) 2222 self.vbox.addLayout(self.hbox) 2223 2224 self.setLayout(self.vbox) 2225 2226 def Ok(self): 2227 vars = self.report_vars 2228 for d in self.data_items: 2229 if d.id == "REPORTNAME": 2230 vars.name = d.value 2231 if not vars.name: 2232 self.ShowMessage("Report name is required") 2233 return 2234 for d in self.data_items: 2235 if not d.IsValid(): 2236 return 2237 for d in self.data_items[1:]: 2238 if d.id == "LIMIT": 2239 vars.limit = d.value 2240 elif len(d.value): 2241 if len(vars.where_clause): 2242 vars.where_clause += " AND " 2243 vars.where_clause += d.value 2244 if len(vars.where_clause): 2245 if self.partial: 2246 vars.where_clause = " AND ( " + vars.where_clause + " ) " 2247 else: 2248 vars.where_clause = " WHERE " + vars.where_clause + " " 2249 self.accept() 2250 2251 def ShowMessage(self, msg): 2252 self.status.setText("<font color=#FF0000>" + msg) 2253 2254 def ClearMessage(self): 2255 self.status.setText("") 2256 2257# Selected branch report creation dialog 2258 2259class SelectedBranchDialog(ReportDialogBase): 2260 2261 def __init__(self, glb, parent=None): 2262 title = "Selected Branches" 2263 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), 2264 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p), 2265 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p), 2266 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p), 2267 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p), 2268 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p), 2269 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p), 2270 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p), 2271 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p)) 2272 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent) 2273 2274# Event list 2275 2276def GetEventList(db): 2277 events = [] 2278 query = QSqlQuery(db) 2279 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id") 2280 while query.next(): 2281 events.append(query.value(0)) 2282 return events 2283 2284# Is a table selectable 2285 2286def IsSelectable(db, table, sql = "", columns = "*"): 2287 query = QSqlQuery(db) 2288 try: 2289 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1") 2290 except: 2291 return False 2292 return True 2293 2294# SQL table data model item 2295 2296class SQLTableItem(): 2297 2298 def __init__(self, row, data): 2299 self.row = row 2300 self.data = data 2301 2302 def getData(self, column): 2303 return self.data[column] 2304 2305# SQL table data model 2306 2307class SQLTableModel(TableModel): 2308 2309 progress = Signal(object) 2310 2311 def __init__(self, glb, sql, column_headers, parent=None): 2312 super(SQLTableModel, self).__init__(parent) 2313 self.glb = glb 2314 self.more = True 2315 self.populated = 0 2316 self.column_headers = column_headers 2317 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample) 2318 self.fetcher.done.connect(self.Update) 2319 self.fetcher.Fetch(glb_chunk_sz) 2320 2321 def DisplayData(self, item, index): 2322 self.FetchIfNeeded(item.row) 2323 return item.getData(index.column()) 2324 2325 def AddSample(self, data): 2326 child = SQLTableItem(self.populated, data) 2327 self.child_items.append(child) 2328 self.populated += 1 2329 2330 def Update(self, fetched): 2331 if not fetched: 2332 self.more = False 2333 self.progress.emit(0) 2334 child_count = self.child_count 2335 count = self.populated - child_count 2336 if count > 0: 2337 parent = QModelIndex() 2338 self.beginInsertRows(parent, child_count, child_count + count - 1) 2339 self.insertRows(child_count, count, parent) 2340 self.child_count += count 2341 self.endInsertRows() 2342 self.progress.emit(self.child_count) 2343 2344 def FetchMoreRecords(self, count): 2345 current = self.child_count 2346 if self.more: 2347 self.fetcher.Fetch(count) 2348 else: 2349 self.progress.emit(0) 2350 return current 2351 2352 def HasMoreRecords(self): 2353 return self.more 2354 2355 def columnCount(self, parent=None): 2356 return len(self.column_headers) 2357 2358 def columnHeader(self, column): 2359 return self.column_headers[column] 2360 2361 def SQLTableDataPrep(self, query, count): 2362 data = [] 2363 for i in xrange(count): 2364 data.append(query.value(i)) 2365 return data 2366 2367# SQL automatic table data model 2368 2369class SQLAutoTableModel(SQLTableModel): 2370 2371 def __init__(self, glb, table_name, parent=None): 2372 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz) 2373 if table_name == "comm_threads_view": 2374 # For now, comm_threads_view has no id column 2375 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz) 2376 column_headers = [] 2377 query = QSqlQuery(glb.db) 2378 if glb.dbref.is_sqlite3: 2379 QueryExec(query, "PRAGMA table_info(" + table_name + ")") 2380 while query.next(): 2381 column_headers.append(query.value(1)) 2382 if table_name == "sqlite_master": 2383 sql = "SELECT * FROM " + table_name 2384 else: 2385 if table_name[:19] == "information_schema.": 2386 sql = "SELECT * FROM " + table_name 2387 select_table_name = table_name[19:] 2388 schema = "information_schema" 2389 else: 2390 select_table_name = table_name 2391 schema = "public" 2392 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'") 2393 while query.next(): 2394 column_headers.append(query.value(0)) 2395 if pyside_version_1 and sys.version_info[0] == 3: 2396 if table_name == "samples_view": 2397 self.SQLTableDataPrep = self.samples_view_DataPrep 2398 if table_name == "samples": 2399 self.SQLTableDataPrep = self.samples_DataPrep 2400 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent) 2401 2402 def samples_view_DataPrep(self, query, count): 2403 data = [] 2404 data.append(query.value(0)) 2405 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 2406 data.append("{:>19}".format(query.value(1))) 2407 for i in xrange(2, count): 2408 data.append(query.value(i)) 2409 return data 2410 2411 def samples_DataPrep(self, query, count): 2412 data = [] 2413 for i in xrange(9): 2414 data.append(query.value(i)) 2415 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string 2416 data.append("{:>19}".format(query.value(9))) 2417 for i in xrange(10, count): 2418 data.append(query.value(i)) 2419 return data 2420 2421# Base class for custom ResizeColumnsToContents 2422 2423class ResizeColumnsToContentsBase(QObject): 2424 2425 def __init__(self, parent=None): 2426 super(ResizeColumnsToContentsBase, self).__init__(parent) 2427 2428 def ResizeColumnToContents(self, column, n): 2429 # Using the view's resizeColumnToContents() here is extrememly slow 2430 # so implement a crude alternative 2431 font = self.view.font() 2432 metrics = QFontMetrics(font) 2433 max = 0 2434 for row in xrange(n): 2435 val = self.data_model.child_items[row].data[column] 2436 len = metrics.width(str(val) + "MM") 2437 max = len if len > max else max 2438 val = self.data_model.columnHeader(column) 2439 len = metrics.width(str(val) + "MM") 2440 max = len if len > max else max 2441 self.view.setColumnWidth(column, max) 2442 2443 def ResizeColumnsToContents(self): 2444 n = min(self.data_model.child_count, 100) 2445 if n < 1: 2446 # No data yet, so connect a signal to notify when there is 2447 self.data_model.rowsInserted.connect(self.UpdateColumnWidths) 2448 return 2449 columns = self.data_model.columnCount() 2450 for i in xrange(columns): 2451 self.ResizeColumnToContents(i, n) 2452 2453 def UpdateColumnWidths(self, *x): 2454 # This only needs to be done once, so disconnect the signal now 2455 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths) 2456 self.ResizeColumnsToContents() 2457 2458# Convert value to CSV 2459 2460def ToCSValue(val): 2461 if '"' in val: 2462 val = val.replace('"', '""') 2463 if "," in val or '"' in val: 2464 val = '"' + val + '"' 2465 return val 2466 2467# Key to sort table model indexes by row / column, assuming fewer than 1000 columns 2468 2469glb_max_cols = 1000 2470 2471def RowColumnKey(a): 2472 return a.row() * glb_max_cols + a.column() 2473 2474# Copy selected table cells to clipboard 2475 2476def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False): 2477 indexes = sorted(view.selectedIndexes(), key=RowColumnKey) 2478 idx_cnt = len(indexes) 2479 if not idx_cnt: 2480 return 2481 if idx_cnt == 1: 2482 with_hdr=False 2483 min_row = indexes[0].row() 2484 max_row = indexes[0].row() 2485 min_col = indexes[0].column() 2486 max_col = indexes[0].column() 2487 for i in indexes: 2488 min_row = min(min_row, i.row()) 2489 max_row = max(max_row, i.row()) 2490 min_col = min(min_col, i.column()) 2491 max_col = max(max_col, i.column()) 2492 if max_col > glb_max_cols: 2493 raise RuntimeError("glb_max_cols is too low") 2494 max_width = [0] * (1 + max_col - min_col) 2495 for i in indexes: 2496 c = i.column() - min_col 2497 max_width[c] = max(max_width[c], len(str(i.data()))) 2498 text = "" 2499 pad = "" 2500 sep = "" 2501 if with_hdr: 2502 model = indexes[0].model() 2503 for col in range(min_col, max_col + 1): 2504 val = model.headerData(col, Qt.Horizontal, Qt.DisplayRole) 2505 if as_csv: 2506 text += sep + ToCSValue(val) 2507 sep = "," 2508 else: 2509 c = col - min_col 2510 max_width[c] = max(max_width[c], len(val)) 2511 width = max_width[c] 2512 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole) 2513 if align & Qt.AlignRight: 2514 val = val.rjust(width) 2515 text += pad + sep + val 2516 pad = " " * (width - len(val)) 2517 sep = " " 2518 text += "\n" 2519 pad = "" 2520 sep = "" 2521 last_row = min_row 2522 for i in indexes: 2523 if i.row() > last_row: 2524 last_row = i.row() 2525 text += "\n" 2526 pad = "" 2527 sep = "" 2528 if as_csv: 2529 text += sep + ToCSValue(str(i.data())) 2530 sep = "," 2531 else: 2532 width = max_width[i.column() - min_col] 2533 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight: 2534 val = str(i.data()).rjust(width) 2535 else: 2536 val = str(i.data()) 2537 text += pad + sep + val 2538 pad = " " * (width - len(val)) 2539 sep = " " 2540 QApplication.clipboard().setText(text) 2541 2542def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False): 2543 indexes = view.selectedIndexes() 2544 if not len(indexes): 2545 return 2546 2547 selection = view.selectionModel() 2548 2549 first = None 2550 for i in indexes: 2551 above = view.indexAbove(i) 2552 if not selection.isSelected(above): 2553 first = i 2554 break 2555 2556 if first is None: 2557 raise RuntimeError("CopyTreeCellsToClipboard internal error") 2558 2559 model = first.model() 2560 row_cnt = 0 2561 col_cnt = model.columnCount(first) 2562 max_width = [0] * col_cnt 2563 2564 indent_sz = 2 2565 indent_str = " " * indent_sz 2566 2567 expanded_mark_sz = 2 2568 if sys.version_info[0] == 3: 2569 expanded_mark = "\u25BC " 2570 not_expanded_mark = "\u25B6 " 2571 else: 2572 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8") 2573 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8") 2574 leaf_mark = " " 2575 2576 if not as_csv: 2577 pos = first 2578 while True: 2579 row_cnt += 1 2580 row = pos.row() 2581 for c in range(col_cnt): 2582 i = pos.sibling(row, c) 2583 if c: 2584 n = len(str(i.data())) 2585 else: 2586 n = len(str(i.data()).strip()) 2587 n += (i.internalPointer().level - 1) * indent_sz 2588 n += expanded_mark_sz 2589 max_width[c] = max(max_width[c], n) 2590 pos = view.indexBelow(pos) 2591 if not selection.isSelected(pos): 2592 break 2593 2594 text = "" 2595 pad = "" 2596 sep = "" 2597 if with_hdr: 2598 for c in range(col_cnt): 2599 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip() 2600 if as_csv: 2601 text += sep + ToCSValue(val) 2602 sep = "," 2603 else: 2604 max_width[c] = max(max_width[c], len(val)) 2605 width = max_width[c] 2606 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole) 2607 if align & Qt.AlignRight: 2608 val = val.rjust(width) 2609 text += pad + sep + val 2610 pad = " " * (width - len(val)) 2611 sep = " " 2612 text += "\n" 2613 pad = "" 2614 sep = "" 2615 2616 pos = first 2617 while True: 2618 row = pos.row() 2619 for c in range(col_cnt): 2620 i = pos.sibling(row, c) 2621 val = str(i.data()) 2622 if not c: 2623 if model.hasChildren(i): 2624 if view.isExpanded(i): 2625 mark = expanded_mark 2626 else: 2627 mark = not_expanded_mark 2628 else: 2629 mark = leaf_mark 2630 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip() 2631 if as_csv: 2632 text += sep + ToCSValue(val) 2633 sep = "," 2634 else: 2635 width = max_width[c] 2636 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight: 2637 val = val.rjust(width) 2638 text += pad + sep + val 2639 pad = " " * (width - len(val)) 2640 sep = " " 2641 pos = view.indexBelow(pos) 2642 if not selection.isSelected(pos): 2643 break 2644 text = text.rstrip() + "\n" 2645 pad = "" 2646 sep = "" 2647 2648 QApplication.clipboard().setText(text) 2649 2650def CopyCellsToClipboard(view, as_csv=False, with_hdr=False): 2651 view.CopyCellsToClipboard(view, as_csv, with_hdr) 2652 2653def CopyCellsToClipboardHdr(view): 2654 CopyCellsToClipboard(view, False, True) 2655 2656def CopyCellsToClipboardCSV(view): 2657 CopyCellsToClipboard(view, True, True) 2658 2659# Context menu 2660 2661class ContextMenu(object): 2662 2663 def __init__(self, view): 2664 self.view = view 2665 self.view.setContextMenuPolicy(Qt.CustomContextMenu) 2666 self.view.customContextMenuRequested.connect(self.ShowContextMenu) 2667 2668 def ShowContextMenu(self, pos): 2669 menu = QMenu(self.view) 2670 self.AddActions(menu) 2671 menu.exec_(self.view.mapToGlobal(pos)) 2672 2673 def AddCopy(self, menu): 2674 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view)) 2675 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view)) 2676 2677 def AddActions(self, menu): 2678 self.AddCopy(menu) 2679 2680class TreeContextMenu(ContextMenu): 2681 2682 def __init__(self, view): 2683 super(TreeContextMenu, self).__init__(view) 2684 2685 def AddActions(self, menu): 2686 i = self.view.currentIndex() 2687 text = str(i.data()).strip() 2688 if len(text): 2689 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view)) 2690 self.AddCopy(menu) 2691 2692# Table window 2693 2694class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase): 2695 2696 def __init__(self, glb, table_name, parent=None): 2697 super(TableWindow, self).__init__(parent) 2698 2699 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name)) 2700 2701 self.model = QSortFilterProxyModel() 2702 self.model.setSourceModel(self.data_model) 2703 2704 self.view = QTableView() 2705 self.view.setModel(self.model) 2706 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) 2707 self.view.verticalHeader().setVisible(False) 2708 self.view.sortByColumn(-1, Qt.AscendingOrder) 2709 self.view.setSortingEnabled(True) 2710 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 2711 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard 2712 2713 self.ResizeColumnsToContents() 2714 2715 self.context_menu = ContextMenu(self.view) 2716 2717 self.find_bar = FindBar(self, self, True) 2718 2719 self.finder = ChildDataItemFinder(self.data_model) 2720 2721 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) 2722 2723 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 2724 2725 self.setWidget(self.vbox.Widget()) 2726 2727 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table") 2728 2729 def Find(self, value, direction, pattern, context): 2730 self.view.setFocus() 2731 self.find_bar.Busy() 2732 self.finder.Find(value, direction, pattern, context, self.FindDone) 2733 2734 def FindDone(self, row): 2735 self.find_bar.Idle() 2736 if row >= 0: 2737 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex()))) 2738 else: 2739 self.find_bar.NotFound() 2740 2741# Table list 2742 2743def GetTableList(glb): 2744 tables = [] 2745 query = QSqlQuery(glb.db) 2746 if glb.dbref.is_sqlite3: 2747 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name") 2748 else: 2749 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name") 2750 while query.next(): 2751 tables.append(query.value(0)) 2752 if glb.dbref.is_sqlite3: 2753 tables.append("sqlite_master") 2754 else: 2755 tables.append("information_schema.tables") 2756 tables.append("information_schema.views") 2757 tables.append("information_schema.columns") 2758 return tables 2759 2760# Top Calls data model 2761 2762class TopCallsModel(SQLTableModel): 2763 2764 def __init__(self, glb, report_vars, parent=None): 2765 text = "" 2766 if not glb.dbref.is_sqlite3: 2767 text = "::text" 2768 limit = "" 2769 if len(report_vars.limit): 2770 limit = " LIMIT " + report_vars.limit 2771 sql = ("SELECT comm, pid, tid, name," 2772 " CASE" 2773 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text + 2774 " ELSE short_name" 2775 " END AS dso," 2776 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, " 2777 " CASE" 2778 " WHEN (calls.flags = 1) THEN 'no call'" + text + 2779 " WHEN (calls.flags = 2) THEN 'no return'" + text + 2780 " WHEN (calls.flags = 3) THEN 'no call/return'" + text + 2781 " ELSE ''" + text + 2782 " END AS flags" 2783 " FROM calls" 2784 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 2785 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 2786 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 2787 " INNER JOIN comms ON calls.comm_id = comms.id" 2788 " INNER JOIN threads ON calls.thread_id = threads.id" + 2789 report_vars.where_clause + 2790 " ORDER BY elapsed_time DESC" + 2791 limit 2792 ) 2793 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags") 2794 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft) 2795 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent) 2796 2797 def columnAlignment(self, column): 2798 return self.alignment[column] 2799 2800# Top Calls report creation dialog 2801 2802class TopCallsDialog(ReportDialogBase): 2803 2804 def __init__(self, glb, parent=None): 2805 title = "Top Calls by Elapsed Time" 2806 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"), 2807 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p), 2808 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p), 2809 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p), 2810 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p), 2811 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p), 2812 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p), 2813 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100")) 2814 super(TopCallsDialog, self).__init__(glb, title, items, False, parent) 2815 2816# Top Calls window 2817 2818class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase): 2819 2820 def __init__(self, glb, report_vars, parent=None): 2821 super(TopCallsWindow, self).__init__(parent) 2822 2823 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars)) 2824 self.model = self.data_model 2825 2826 self.view = QTableView() 2827 self.view.setModel(self.model) 2828 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers) 2829 self.view.verticalHeader().setVisible(False) 2830 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection) 2831 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard 2832 2833 self.context_menu = ContextMenu(self.view) 2834 2835 self.ResizeColumnsToContents() 2836 2837 self.find_bar = FindBar(self, self, True) 2838 2839 self.finder = ChildDataItemFinder(self.model) 2840 2841 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self) 2842 2843 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget()) 2844 2845 self.setWidget(self.vbox.Widget()) 2846 2847 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name) 2848 2849 def Find(self, value, direction, pattern, context): 2850 self.view.setFocus() 2851 self.find_bar.Busy() 2852 self.finder.Find(value, direction, pattern, context, self.FindDone) 2853 2854 def FindDone(self, row): 2855 self.find_bar.Idle() 2856 if row >= 0: 2857 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) 2858 else: 2859 self.find_bar.NotFound() 2860 2861# Action Definition 2862 2863def CreateAction(label, tip, callback, parent=None, shortcut=None): 2864 action = QAction(label, parent) 2865 if shortcut != None: 2866 action.setShortcuts(shortcut) 2867 action.setStatusTip(tip) 2868 action.triggered.connect(callback) 2869 return action 2870 2871# Typical application actions 2872 2873def CreateExitAction(app, parent=None): 2874 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) 2875 2876# Typical MDI actions 2877 2878def CreateCloseActiveWindowAction(mdi_area): 2879 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) 2880 2881def CreateCloseAllWindowsAction(mdi_area): 2882 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) 2883 2884def CreateTileWindowsAction(mdi_area): 2885 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) 2886 2887def CreateCascadeWindowsAction(mdi_area): 2888 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) 2889 2890def CreateNextWindowAction(mdi_area): 2891 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) 2892 2893def CreatePreviousWindowAction(mdi_area): 2894 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) 2895 2896# Typical MDI window menu 2897 2898class WindowMenu(): 2899 2900 def __init__(self, mdi_area, menu): 2901 self.mdi_area = mdi_area 2902 self.window_menu = menu.addMenu("&Windows") 2903 self.close_active_window = CreateCloseActiveWindowAction(mdi_area) 2904 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) 2905 self.tile_windows = CreateTileWindowsAction(mdi_area) 2906 self.cascade_windows = CreateCascadeWindowsAction(mdi_area) 2907 self.next_window = CreateNextWindowAction(mdi_area) 2908 self.previous_window = CreatePreviousWindowAction(mdi_area) 2909 self.window_menu.aboutToShow.connect(self.Update) 2910 2911 def Update(self): 2912 self.window_menu.clear() 2913 sub_window_count = len(self.mdi_area.subWindowList()) 2914 have_sub_windows = sub_window_count != 0 2915 self.close_active_window.setEnabled(have_sub_windows) 2916 self.close_all_windows.setEnabled(have_sub_windows) 2917 self.tile_windows.setEnabled(have_sub_windows) 2918 self.cascade_windows.setEnabled(have_sub_windows) 2919 self.next_window.setEnabled(have_sub_windows) 2920 self.previous_window.setEnabled(have_sub_windows) 2921 self.window_menu.addAction(self.close_active_window) 2922 self.window_menu.addAction(self.close_all_windows) 2923 self.window_menu.addSeparator() 2924 self.window_menu.addAction(self.tile_windows) 2925 self.window_menu.addAction(self.cascade_windows) 2926 self.window_menu.addSeparator() 2927 self.window_menu.addAction(self.next_window) 2928 self.window_menu.addAction(self.previous_window) 2929 if sub_window_count == 0: 2930 return 2931 self.window_menu.addSeparator() 2932 nr = 1 2933 for sub_window in self.mdi_area.subWindowList(): 2934 label = str(nr) + " " + sub_window.name 2935 if nr < 10: 2936 label = "&" + label 2937 action = self.window_menu.addAction(label) 2938 action.setCheckable(True) 2939 action.setChecked(sub_window == self.mdi_area.activeSubWindow()) 2940 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x)) 2941 self.window_menu.addAction(action) 2942 nr += 1 2943 2944 def setActiveSubWindow(self, nr): 2945 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) 2946 2947# Help text 2948 2949glb_help_text = """ 2950<h1>Contents</h1> 2951<style> 2952p.c1 { 2953 text-indent: 40px; 2954} 2955p.c2 { 2956 text-indent: 80px; 2957} 2958} 2959</style> 2960<p class=c1><a href=#reports>1. Reports</a></p> 2961<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p> 2962<p class=c2><a href=#calltree>1.2 Call Tree</a></p> 2963<p class=c2><a href=#allbranches>1.3 All branches</a></p> 2964<p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p> 2965<p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p> 2966<p class=c1><a href=#tables>2. Tables</a></p> 2967<h1 id=reports>1. Reports</h1> 2968<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2> 2969The result is a GUI window with a tree representing a context-sensitive 2970call-graph. Expanding a couple of levels of the tree and adjusting column 2971widths to suit will display something like: 2972<pre> 2973 Call Graph: pt_example 2974Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) 2975v- ls 2976 v- 2638:2638 2977 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 2978 |- unknown unknown 1 13198 0.1 1 0.0 2979 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 2980 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 2981 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 2982 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 2983 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 2984 >- __libc_csu_init ls 1 10354 0.1 10 0.0 2985 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 2986 v- main ls 1 8182043 99.6 180254 99.9 2987</pre> 2988<h3>Points to note:</h3> 2989<ul> 2990<li>The top level is a command name (comm)</li> 2991<li>The next level is a thread (pid:tid)</li> 2992<li>Subsequent levels are functions</li> 2993<li>'Count' is the number of calls</li> 2994<li>'Time' is the elapsed time until the function returns</li> 2995<li>Percentages are relative to the level above</li> 2996<li>'Branch Count' is the total number of branches for that function and all functions that it calls 2997</ul> 2998<h3>Find</h3> 2999Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match. 3000The pattern matching symbols are ? for any character and * for zero or more characters. 3001<h2 id=calltree>1.2 Call Tree</h2> 3002The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated. 3003Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'. 3004<h2 id=allbranches>1.3 All branches</h2> 3005The All branches report displays all branches in chronological order. 3006Not all data is fetched immediately. More records can be fetched using the Fetch bar provided. 3007<h3>Disassembly</h3> 3008Open a branch to display disassembly. This only works if: 3009<ol> 3010<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li> 3011<li>The object code is available. Currently, only the perf build ID cache is searched for object code. 3012The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR. 3013One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu), 3014or alternatively, set environment variable PERF_KCORE to the kcore file name.</li> 3015</ol> 3016<h4 id=xed>Intel XED Setup</h4> 3017To use Intel XED, libxed.so must be present. To build and install libxed.so: 3018<pre> 3019git clone https://github.com/intelxed/mbuild.git mbuild 3020git clone https://github.com/intelxed/xed 3021cd xed 3022./mfile.py --share 3023sudo ./mfile.py --prefix=/usr/local install 3024sudo ldconfig 3025</pre> 3026<h3>Instructions per Cycle (IPC)</h3> 3027If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'. 3028<p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch. 3029Due to the granularity of timing information, the number of cycles for some code blocks will not be known. 3030In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period 3031since the previous displayed 'IPC'. 3032<h3>Find</h3> 3033Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. 3034Refer to Python documentation for the regular expression syntax. 3035All columns are searched, but only currently fetched rows are searched. 3036<h2 id=selectedbranches>1.4 Selected branches</h2> 3037This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced 3038by various selection criteria. A dialog box displays available criteria which are AND'ed together. 3039<h3>1.4.1 Time ranges</h3> 3040The time ranges hint text shows the total time range. Relative time ranges can also be entered in 3041ms, us or ns. Also, negative values are relative to the end of trace. Examples: 3042<pre> 3043 81073085947329-81073085958238 From 81073085947329 to 81073085958238 3044 100us-200us From 100us to 200us 3045 10ms- From 10ms to the end 3046 -100ns The first 100ns 3047 -10ms- The last 10ms 3048</pre> 3049N.B. Due to the granularity of timestamps, there could be no branches in any given time range. 3050<h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2> 3051The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned. 3052The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together. 3053If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar. 3054<h1 id=tables>2. Tables</h1> 3055The Tables menu shows all tables and views in the database. Most tables have an associated view 3056which displays the information in a more friendly way. Not all data for large tables is fetched 3057immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted, 3058but that can be slow for large tables. 3059<p>There are also tables of database meta-information. 3060For SQLite3 databases, the sqlite_master table is included. 3061For PostgreSQL databases, information_schema.tables/views/columns are included. 3062<h3>Find</h3> 3063Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. 3064Refer to Python documentation for the regular expression syntax. 3065All columns are searched, but only currently fetched rows are searched. 3066<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous 3067will go to the next/previous result in id order, instead of display order. 3068""" 3069 3070# Help window 3071 3072class HelpWindow(QMdiSubWindow): 3073 3074 def __init__(self, glb, parent=None): 3075 super(HelpWindow, self).__init__(parent) 3076 3077 self.text = QTextBrowser() 3078 self.text.setHtml(glb_help_text) 3079 self.text.setReadOnly(True) 3080 self.text.setOpenExternalLinks(True) 3081 3082 self.setWidget(self.text) 3083 3084 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help") 3085 3086# Main window that only displays the help text 3087 3088class HelpOnlyWindow(QMainWindow): 3089 3090 def __init__(self, parent=None): 3091 super(HelpOnlyWindow, self).__init__(parent) 3092 3093 self.setMinimumSize(200, 100) 3094 self.resize(800, 600) 3095 self.setWindowTitle("Exported SQL Viewer Help") 3096 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation)) 3097 3098 self.text = QTextBrowser() 3099 self.text.setHtml(glb_help_text) 3100 self.text.setReadOnly(True) 3101 self.text.setOpenExternalLinks(True) 3102 3103 self.setCentralWidget(self.text) 3104 3105# PostqreSQL server version 3106 3107def PostqreSQLServerVersion(db): 3108 query = QSqlQuery(db) 3109 QueryExec(query, "SELECT VERSION()") 3110 if query.next(): 3111 v_str = query.value(0) 3112 v_list = v_str.strip().split(" ") 3113 if v_list[0] == "PostgreSQL" and v_list[2] == "on": 3114 return v_list[1] 3115 return v_str 3116 return "Unknown" 3117 3118# SQLite version 3119 3120def SQLiteVersion(db): 3121 query = QSqlQuery(db) 3122 QueryExec(query, "SELECT sqlite_version()") 3123 if query.next(): 3124 return query.value(0) 3125 return "Unknown" 3126 3127# About dialog 3128 3129class AboutDialog(QDialog): 3130 3131 def __init__(self, glb, parent=None): 3132 super(AboutDialog, self).__init__(parent) 3133 3134 self.setWindowTitle("About Exported SQL Viewer") 3135 self.setMinimumWidth(300) 3136 3137 pyside_version = "1" if pyside_version_1 else "2" 3138 3139 text = "<pre>" 3140 text += "Python version: " + sys.version.split(" ")[0] + "\n" 3141 text += "PySide version: " + pyside_version + "\n" 3142 text += "Qt version: " + qVersion() + "\n" 3143 if glb.dbref.is_sqlite3: 3144 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n" 3145 else: 3146 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n" 3147 text += "</pre>" 3148 3149 self.text = QTextBrowser() 3150 self.text.setHtml(text) 3151 self.text.setReadOnly(True) 3152 self.text.setOpenExternalLinks(True) 3153 3154 self.vbox = QVBoxLayout() 3155 self.vbox.addWidget(self.text) 3156 3157 self.setLayout(self.vbox) 3158 3159# Font resize 3160 3161def ResizeFont(widget, diff): 3162 font = widget.font() 3163 sz = font.pointSize() 3164 font.setPointSize(sz + diff) 3165 widget.setFont(font) 3166 3167def ShrinkFont(widget): 3168 ResizeFont(widget, -1) 3169 3170def EnlargeFont(widget): 3171 ResizeFont(widget, 1) 3172 3173# Unique name for sub-windows 3174 3175def NumberedWindowName(name, nr): 3176 if nr > 1: 3177 name += " <" + str(nr) + ">" 3178 return name 3179 3180def UniqueSubWindowName(mdi_area, name): 3181 nr = 1 3182 while True: 3183 unique_name = NumberedWindowName(name, nr) 3184 ok = True 3185 for sub_window in mdi_area.subWindowList(): 3186 if sub_window.name == unique_name: 3187 ok = False 3188 break 3189 if ok: 3190 return unique_name 3191 nr += 1 3192 3193# Add a sub-window 3194 3195def AddSubWindow(mdi_area, sub_window, name): 3196 unique_name = UniqueSubWindowName(mdi_area, name) 3197 sub_window.setMinimumSize(200, 100) 3198 sub_window.resize(800, 600) 3199 sub_window.setWindowTitle(unique_name) 3200 sub_window.setAttribute(Qt.WA_DeleteOnClose) 3201 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) 3202 sub_window.name = unique_name 3203 mdi_area.addSubWindow(sub_window) 3204 sub_window.show() 3205 3206# Main window 3207 3208class MainWindow(QMainWindow): 3209 3210 def __init__(self, glb, parent=None): 3211 super(MainWindow, self).__init__(parent) 3212 3213 self.glb = glb 3214 3215 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) 3216 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) 3217 self.setMinimumSize(200, 100) 3218 3219 self.mdi_area = QMdiArea() 3220 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) 3221 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 3222 3223 self.setCentralWidget(self.mdi_area) 3224 3225 menu = self.menuBar() 3226 3227 file_menu = menu.addMenu("&File") 3228 file_menu.addAction(CreateExitAction(glb.app, self)) 3229 3230 edit_menu = menu.addMenu("&Edit") 3231 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy)) 3232 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self)) 3233 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) 3234 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)])) 3235 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")])) 3236 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")])) 3237 3238 reports_menu = menu.addMenu("&Reports") 3239 if IsSelectable(glb.db, "calls"): 3240 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) 3241 3242 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"): 3243 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self)) 3244 3245 self.EventMenu(GetEventList(glb.db), reports_menu) 3246 3247 if IsSelectable(glb.db, "calls"): 3248 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self)) 3249 3250 self.TableMenu(GetTableList(glb), menu) 3251 3252 self.window_menu = WindowMenu(self.mdi_area, menu) 3253 3254 help_menu = menu.addMenu("&Help") 3255 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents)) 3256 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self)) 3257 3258 def Try(self, fn): 3259 win = self.mdi_area.activeSubWindow() 3260 if win: 3261 try: 3262 fn(win.view) 3263 except: 3264 pass 3265 3266 def CopyToClipboard(self): 3267 self.Try(CopyCellsToClipboardHdr) 3268 3269 def CopyToClipboardCSV(self): 3270 self.Try(CopyCellsToClipboardCSV) 3271 3272 def Find(self): 3273 win = self.mdi_area.activeSubWindow() 3274 if win: 3275 try: 3276 win.find_bar.Activate() 3277 except: 3278 pass 3279 3280 def FetchMoreRecords(self): 3281 win = self.mdi_area.activeSubWindow() 3282 if win: 3283 try: 3284 win.fetch_bar.Activate() 3285 except: 3286 pass 3287 3288 def ShrinkFont(self): 3289 self.Try(ShrinkFont) 3290 3291 def EnlargeFont(self): 3292 self.Try(EnlargeFont) 3293 3294 def EventMenu(self, events, reports_menu): 3295 branches_events = 0 3296 for event in events: 3297 event = event.split(":")[0] 3298 if event == "branches": 3299 branches_events += 1 3300 dbid = 0 3301 for event in events: 3302 dbid += 1 3303 event = event.split(":")[0] 3304 if event == "branches": 3305 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")" 3306 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self)) 3307 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")" 3308 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self)) 3309 3310 def TableMenu(self, tables, menu): 3311 table_menu = menu.addMenu("&Tables") 3312 for table in tables: 3313 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self)) 3314 3315 def NewCallGraph(self): 3316 CallGraphWindow(self.glb, self) 3317 3318 def NewCallTree(self): 3319 CallTreeWindow(self.glb, self) 3320 3321 def NewTopCalls(self): 3322 dialog = TopCallsDialog(self.glb, self) 3323 ret = dialog.exec_() 3324 if ret: 3325 TopCallsWindow(self.glb, dialog.report_vars, self) 3326 3327 def NewBranchView(self, event_id): 3328 BranchWindow(self.glb, event_id, ReportVars(), self) 3329 3330 def NewSelectedBranchView(self, event_id): 3331 dialog = SelectedBranchDialog(self.glb, self) 3332 ret = dialog.exec_() 3333 if ret: 3334 BranchWindow(self.glb, event_id, dialog.report_vars, self) 3335 3336 def NewTableView(self, table_name): 3337 TableWindow(self.glb, table_name, self) 3338 3339 def Help(self): 3340 HelpWindow(self.glb, self) 3341 3342 def About(self): 3343 dialog = AboutDialog(self.glb, self) 3344 dialog.exec_() 3345 3346# XED Disassembler 3347 3348class xed_state_t(Structure): 3349 3350 _fields_ = [ 3351 ("mode", c_int), 3352 ("width", c_int) 3353 ] 3354 3355class XEDInstruction(): 3356 3357 def __init__(self, libxed): 3358 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion 3359 xedd_t = c_byte * 512 3360 self.xedd = xedd_t() 3361 self.xedp = addressof(self.xedd) 3362 libxed.xed_decoded_inst_zero(self.xedp) 3363 self.state = xed_state_t() 3364 self.statep = addressof(self.state) 3365 # Buffer for disassembled instruction text 3366 self.buffer = create_string_buffer(256) 3367 self.bufferp = addressof(self.buffer) 3368 3369class LibXED(): 3370 3371 def __init__(self): 3372 try: 3373 self.libxed = CDLL("libxed.so") 3374 except: 3375 self.libxed = None 3376 if not self.libxed: 3377 self.libxed = CDLL("/usr/local/lib/libxed.so") 3378 3379 self.xed_tables_init = self.libxed.xed_tables_init 3380 self.xed_tables_init.restype = None 3381 self.xed_tables_init.argtypes = [] 3382 3383 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero 3384 self.xed_decoded_inst_zero.restype = None 3385 self.xed_decoded_inst_zero.argtypes = [ c_void_p ] 3386 3387 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode 3388 self.xed_operand_values_set_mode.restype = None 3389 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ] 3390 3391 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode 3392 self.xed_decoded_inst_zero_keep_mode.restype = None 3393 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ] 3394 3395 self.xed_decode = self.libxed.xed_decode 3396 self.xed_decode.restype = c_int 3397 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ] 3398 3399 self.xed_format_context = self.libxed.xed_format_context 3400 self.xed_format_context.restype = c_uint 3401 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ] 3402 3403 self.xed_tables_init() 3404 3405 def Instruction(self): 3406 return XEDInstruction(self) 3407 3408 def SetMode(self, inst, mode): 3409 if mode: 3410 inst.state.mode = 4 # 32-bit 3411 inst.state.width = 4 # 4 bytes 3412 else: 3413 inst.state.mode = 1 # 64-bit 3414 inst.state.width = 8 # 8 bytes 3415 self.xed_operand_values_set_mode(inst.xedp, inst.statep) 3416 3417 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip): 3418 self.xed_decoded_inst_zero_keep_mode(inst.xedp) 3419 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt) 3420 if err: 3421 return 0, "" 3422 # Use AT&T mode (2), alternative is Intel (3) 3423 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0) 3424 if not ok: 3425 return 0, "" 3426 if sys.version_info[0] == 2: 3427 result = inst.buffer.value 3428 else: 3429 result = inst.buffer.value.decode() 3430 # Return instruction length and the disassembled instruction text 3431 # For now, assume the length is in byte 166 3432 return inst.xedd[166], result 3433 3434def TryOpen(file_name): 3435 try: 3436 return open(file_name, "rb") 3437 except: 3438 return None 3439 3440def Is64Bit(f): 3441 result = sizeof(c_void_p) 3442 # ELF support only 3443 pos = f.tell() 3444 f.seek(0) 3445 header = f.read(7) 3446 f.seek(pos) 3447 magic = header[0:4] 3448 if sys.version_info[0] == 2: 3449 eclass = ord(header[4]) 3450 encoding = ord(header[5]) 3451 version = ord(header[6]) 3452 else: 3453 eclass = header[4] 3454 encoding = header[5] 3455 version = header[6] 3456 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1: 3457 result = True if eclass == 2 else False 3458 return result 3459 3460# Global data 3461 3462class Glb(): 3463 3464 def __init__(self, dbref, db, dbname): 3465 self.dbref = dbref 3466 self.db = db 3467 self.dbname = dbname 3468 self.home_dir = os.path.expanduser("~") 3469 self.buildid_dir = os.getenv("PERF_BUILDID_DIR") 3470 if self.buildid_dir: 3471 self.buildid_dir += "/.build-id/" 3472 else: 3473 self.buildid_dir = self.home_dir + "/.debug/.build-id/" 3474 self.app = None 3475 self.mainwindow = None 3476 self.instances_to_shutdown_on_exit = weakref.WeakSet() 3477 try: 3478 self.disassembler = LibXED() 3479 self.have_disassembler = True 3480 except: 3481 self.have_disassembler = False 3482 3483 def FileFromBuildId(self, build_id): 3484 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf" 3485 return TryOpen(file_name) 3486 3487 def FileFromNamesAndBuildId(self, short_name, long_name, build_id): 3488 # Assume current machine i.e. no support for virtualization 3489 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore": 3490 file_name = os.getenv("PERF_KCORE") 3491 f = TryOpen(file_name) if file_name else None 3492 if f: 3493 return f 3494 # For now, no special handling if long_name is /proc/kcore 3495 f = TryOpen(long_name) 3496 if f: 3497 return f 3498 f = self.FileFromBuildId(build_id) 3499 if f: 3500 return f 3501 return None 3502 3503 def AddInstanceToShutdownOnExit(self, instance): 3504 self.instances_to_shutdown_on_exit.add(instance) 3505 3506 # Shutdown any background processes or threads 3507 def ShutdownInstances(self): 3508 for x in self.instances_to_shutdown_on_exit: 3509 try: 3510 x.Shutdown() 3511 except: 3512 pass 3513 3514# Database reference 3515 3516class DBRef(): 3517 3518 def __init__(self, is_sqlite3, dbname): 3519 self.is_sqlite3 = is_sqlite3 3520 self.dbname = dbname 3521 self.TRUE = "TRUE" 3522 self.FALSE = "FALSE" 3523 # SQLite prior to version 3.23 does not support TRUE and FALSE 3524 if self.is_sqlite3: 3525 self.TRUE = "1" 3526 self.FALSE = "0" 3527 3528 def Open(self, connection_name): 3529 dbname = self.dbname 3530 if self.is_sqlite3: 3531 db = QSqlDatabase.addDatabase("QSQLITE", connection_name) 3532 else: 3533 db = QSqlDatabase.addDatabase("QPSQL", connection_name) 3534 opts = dbname.split() 3535 for opt in opts: 3536 if "=" in opt: 3537 opt = opt.split("=") 3538 if opt[0] == "hostname": 3539 db.setHostName(opt[1]) 3540 elif opt[0] == "port": 3541 db.setPort(int(opt[1])) 3542 elif opt[0] == "username": 3543 db.setUserName(opt[1]) 3544 elif opt[0] == "password": 3545 db.setPassword(opt[1]) 3546 elif opt[0] == "dbname": 3547 dbname = opt[1] 3548 else: 3549 dbname = opt 3550 3551 db.setDatabaseName(dbname) 3552 if not db.open(): 3553 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) 3554 return db, dbname 3555 3556# Main 3557 3558def Main(): 3559 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \ 3560 " or: exported-sql-viewer.py --help-only" 3561 ap = argparse.ArgumentParser(usage = usage_str, add_help = False) 3562 ap.add_argument("--pyside-version-1", action='store_true') 3563 ap.add_argument("dbname", nargs="?") 3564 ap.add_argument("--help-only", action='store_true') 3565 args = ap.parse_args() 3566 3567 if args.help_only: 3568 app = QApplication(sys.argv) 3569 mainwindow = HelpOnlyWindow() 3570 mainwindow.show() 3571 err = app.exec_() 3572 sys.exit(err) 3573 3574 dbname = args.dbname 3575 if dbname is None: 3576 ap.print_usage() 3577 print("Too few arguments") 3578 sys.exit(1) 3579 3580 is_sqlite3 = False 3581 try: 3582 f = open(dbname, "rb") 3583 if f.read(15) == b'SQLite format 3': 3584 is_sqlite3 = True 3585 f.close() 3586 except: 3587 pass 3588 3589 dbref = DBRef(is_sqlite3, dbname) 3590 db, dbname = dbref.Open("main") 3591 glb = Glb(dbref, db, dbname) 3592 app = QApplication(sys.argv) 3593 glb.app = app 3594 mainwindow = MainWindow(glb) 3595 glb.mainwindow = mainwindow 3596 mainwindow.show() 3597 err = app.exec_() 3598 glb.ShutdownInstances() 3599 db.close() 3600 sys.exit(err) 3601 3602if __name__ == "__main__": 3603 Main() 3604