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