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