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