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