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