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