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