1#!/usr/bin/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 49import sys 50import weakref 51import threading 52import string 53from PySide.QtCore import * 54from PySide.QtGui import * 55from PySide.QtSql import * 56from decimal import * 57 58# Data formatting helpers 59 60def dsoname(name): 61 if name == "[kernel.kallsyms]": 62 return "[kernel]" 63 return name 64 65# Percent to one decimal place 66 67def PercentToOneDP(n, d): 68 if not d: 69 return "0.0" 70 x = (n * Decimal(100)) / d 71 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) 72 73# Helper for queries that must not fail 74 75def QueryExec(query, stmt): 76 ret = query.exec_(stmt) 77 if not ret: 78 raise Exception("Query failed: " + query.lastError().text()) 79 80# Background thread 81 82class Thread(QThread): 83 84 done = Signal(object) 85 86 def __init__(self, task, param=None, parent=None): 87 super(Thread, self).__init__(parent) 88 self.task = task 89 self.param = param 90 91 def run(self): 92 while True: 93 if self.param is None: 94 done, result = self.task() 95 else: 96 done, result = self.task(self.param) 97 self.done.emit(result) 98 if done: 99 break 100 101# Tree data model 102 103class TreeModel(QAbstractItemModel): 104 105 def __init__(self, root, parent=None): 106 super(TreeModel, self).__init__(parent) 107 self.root = root 108 self.last_row_read = 0 109 110 def Item(self, parent): 111 if parent.isValid(): 112 return parent.internalPointer() 113 else: 114 return self.root 115 116 def rowCount(self, parent): 117 result = self.Item(parent).childCount() 118 if result < 0: 119 result = 0 120 self.dataChanged.emit(parent, parent) 121 return result 122 123 def hasChildren(self, parent): 124 return self.Item(parent).hasChildren() 125 126 def headerData(self, section, orientation, role): 127 if role == Qt.TextAlignmentRole: 128 return self.columnAlignment(section) 129 if role != Qt.DisplayRole: 130 return None 131 if orientation != Qt.Horizontal: 132 return None 133 return self.columnHeader(section) 134 135 def parent(self, child): 136 child_item = child.internalPointer() 137 if child_item is self.root: 138 return QModelIndex() 139 parent_item = child_item.getParentItem() 140 return self.createIndex(parent_item.getRow(), 0, parent_item) 141 142 def index(self, row, column, parent): 143 child_item = self.Item(parent).getChildItem(row) 144 return self.createIndex(row, column, child_item) 145 146 def DisplayData(self, item, index): 147 return item.getData(index.column()) 148 149 def columnAlignment(self, column): 150 return Qt.AlignLeft 151 152 def columnFont(self, column): 153 return None 154 155 def data(self, index, role): 156 if role == Qt.TextAlignmentRole: 157 return self.columnAlignment(index.column()) 158 if role == Qt.FontRole: 159 return self.columnFont(index.column()) 160 if role != Qt.DisplayRole: 161 return None 162 item = index.internalPointer() 163 return self.DisplayData(item, index) 164 165# Model cache 166 167model_cache = weakref.WeakValueDictionary() 168model_cache_lock = threading.Lock() 169 170def LookupCreateModel(model_name, create_fn): 171 model_cache_lock.acquire() 172 try: 173 model = model_cache[model_name] 174 except: 175 model = None 176 if model is None: 177 model = create_fn() 178 model_cache[model_name] = model 179 model_cache_lock.release() 180 return model 181 182# Find bar 183 184class FindBar(): 185 186 def __init__(self, parent, finder, is_reg_expr=False): 187 self.finder = finder 188 self.context = [] 189 self.last_value = None 190 self.last_pattern = None 191 192 label = QLabel("Find:") 193 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 194 195 self.textbox = QComboBox() 196 self.textbox.setEditable(True) 197 self.textbox.currentIndexChanged.connect(self.ValueChanged) 198 199 self.progress = QProgressBar() 200 self.progress.setRange(0, 0) 201 self.progress.hide() 202 203 if is_reg_expr: 204 self.pattern = QCheckBox("Regular Expression") 205 else: 206 self.pattern = QCheckBox("Pattern") 207 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 208 209 self.next_button = QToolButton() 210 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown)) 211 self.next_button.released.connect(lambda: self.NextPrev(1)) 212 213 self.prev_button = QToolButton() 214 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp)) 215 self.prev_button.released.connect(lambda: self.NextPrev(-1)) 216 217 self.close_button = QToolButton() 218 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton)) 219 self.close_button.released.connect(self.Deactivate) 220 221 self.hbox = QHBoxLayout() 222 self.hbox.setContentsMargins(0, 0, 0, 0) 223 224 self.hbox.addWidget(label) 225 self.hbox.addWidget(self.textbox) 226 self.hbox.addWidget(self.progress) 227 self.hbox.addWidget(self.pattern) 228 self.hbox.addWidget(self.next_button) 229 self.hbox.addWidget(self.prev_button) 230 self.hbox.addWidget(self.close_button) 231 232 self.bar = QWidget() 233 self.bar.setLayout(self.hbox); 234 self.bar.hide() 235 236 def Widget(self): 237 return self.bar 238 239 def Activate(self): 240 self.bar.show() 241 self.textbox.setFocus() 242 243 def Deactivate(self): 244 self.bar.hide() 245 246 def Busy(self): 247 self.textbox.setEnabled(False) 248 self.pattern.hide() 249 self.next_button.hide() 250 self.prev_button.hide() 251 self.progress.show() 252 253 def Idle(self): 254 self.textbox.setEnabled(True) 255 self.progress.hide() 256 self.pattern.show() 257 self.next_button.show() 258 self.prev_button.show() 259 260 def Find(self, direction): 261 value = self.textbox.currentText() 262 pattern = self.pattern.isChecked() 263 self.last_value = value 264 self.last_pattern = pattern 265 self.finder.Find(value, direction, pattern, self.context) 266 267 def ValueChanged(self): 268 value = self.textbox.currentText() 269 pattern = self.pattern.isChecked() 270 index = self.textbox.currentIndex() 271 data = self.textbox.itemData(index) 272 # Store the pattern in the combo box to keep it with the text value 273 if data == None: 274 self.textbox.setItemData(index, pattern) 275 else: 276 self.pattern.setChecked(data) 277 self.Find(0) 278 279 def NextPrev(self, direction): 280 value = self.textbox.currentText() 281 pattern = self.pattern.isChecked() 282 if value != self.last_value: 283 index = self.textbox.findText(value) 284 # Allow for a button press before the value has been added to the combo box 285 if index < 0: 286 index = self.textbox.count() 287 self.textbox.addItem(value, pattern) 288 self.textbox.setCurrentIndex(index) 289 return 290 else: 291 self.textbox.setItemData(index, pattern) 292 elif pattern != self.last_pattern: 293 # Keep the pattern recorded in the combo box up to date 294 index = self.textbox.currentIndex() 295 self.textbox.setItemData(index, pattern) 296 self.Find(direction) 297 298 def NotFound(self): 299 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found") 300 301# Context-sensitive call graph data model item base 302 303class CallGraphLevelItemBase(object): 304 305 def __init__(self, glb, row, parent_item): 306 self.glb = glb 307 self.row = row 308 self.parent_item = parent_item 309 self.query_done = False; 310 self.child_count = 0 311 self.child_items = [] 312 313 def getChildItem(self, row): 314 return self.child_items[row] 315 316 def getParentItem(self): 317 return self.parent_item 318 319 def getRow(self): 320 return self.row 321 322 def childCount(self): 323 if not self.query_done: 324 self.Select() 325 if not self.child_count: 326 return -1 327 return self.child_count 328 329 def hasChildren(self): 330 if not self.query_done: 331 return True 332 return self.child_count > 0 333 334 def getData(self, column): 335 return self.data[column] 336 337# Context-sensitive call graph data model level 2+ item base 338 339class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): 340 341 def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item): 342 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item) 343 self.comm_id = comm_id 344 self.thread_id = thread_id 345 self.call_path_id = call_path_id 346 self.branch_count = branch_count 347 self.time = time 348 349 def Select(self): 350 self.query_done = True; 351 query = QSqlQuery(self.glb.db) 352 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)" 353 " FROM calls" 354 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 355 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 356 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 357 " WHERE parent_call_path_id = " + str(self.call_path_id) + 358 " AND comm_id = " + str(self.comm_id) + 359 " AND thread_id = " + str(self.thread_id) + 360 " GROUP BY call_path_id, name, short_name" 361 " ORDER BY call_path_id") 362 while query.next(): 363 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) 364 self.child_items.append(child_item) 365 self.child_count += 1 366 367# Context-sensitive call graph data model level three item 368 369class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): 370 371 def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item): 372 super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item) 373 dso = dsoname(dso) 374 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 375 self.dbid = call_path_id 376 377# Context-sensitive call graph data model level two item 378 379class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): 380 381 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item): 382 super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item) 383 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 384 self.dbid = thread_id 385 386 def Select(self): 387 super(CallGraphLevelTwoItem, self).Select() 388 for child_item in self.child_items: 389 self.time += child_item.time 390 self.branch_count += child_item.branch_count 391 for child_item in self.child_items: 392 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 393 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 394 395# Context-sensitive call graph data model level one item 396 397class CallGraphLevelOneItem(CallGraphLevelItemBase): 398 399 def __init__(self, glb, row, comm_id, comm, parent_item): 400 super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item) 401 self.data = [comm, "", "", "", "", "", ""] 402 self.dbid = comm_id 403 404 def Select(self): 405 self.query_done = True; 406 query = QSqlQuery(self.glb.db) 407 QueryExec(query, "SELECT thread_id, pid, tid" 408 " FROM comm_threads" 409 " INNER JOIN threads ON thread_id = threads.id" 410 " WHERE comm_id = " + str(self.dbid)) 411 while query.next(): 412 child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 413 self.child_items.append(child_item) 414 self.child_count += 1 415 416# Context-sensitive call graph data model root item 417 418class CallGraphRootItem(CallGraphLevelItemBase): 419 420 def __init__(self, glb): 421 super(CallGraphRootItem, self).__init__(glb, 0, None) 422 self.dbid = 0 423 self.query_done = True; 424 query = QSqlQuery(glb.db) 425 QueryExec(query, "SELECT id, comm FROM comms") 426 while query.next(): 427 if not query.value(0): 428 continue 429 child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self) 430 self.child_items.append(child_item) 431 self.child_count += 1 432 433# Context-sensitive call graph data model 434 435class CallGraphModel(TreeModel): 436 437 def __init__(self, glb, parent=None): 438 super(CallGraphModel, self).__init__(CallGraphRootItem(glb), parent) 439 self.glb = glb 440 441 def columnCount(self, parent=None): 442 return 7 443 444 def columnHeader(self, column): 445 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 446 return headers[column] 447 448 def columnAlignment(self, column): 449 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 450 return alignment[column] 451 452 def FindSelect(self, value, pattern, query): 453 if pattern: 454 # postgresql and sqlite pattern patching differences: 455 # postgresql LIKE is case sensitive but sqlite LIKE is not 456 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not 457 # postgresql supports ILIKE which is case insensitive 458 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive 459 if not self.glb.dbref.is_sqlite3: 460 # Escape % and _ 461 s = value.replace("%", "\%") 462 s = s.replace("_", "\_") 463 # Translate * and ? into SQL LIKE pattern characters % and _ 464 trans = string.maketrans("*?", "%_") 465 match = " LIKE '" + str(s).translate(trans) + "'" 466 else: 467 match = " GLOB '" + str(value) + "'" 468 else: 469 match = " = '" + str(value) + "'" 470 QueryExec(query, "SELECT call_path_id, comm_id, thread_id" 471 " FROM calls" 472 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 473 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 474 " WHERE symbols.name" + match + 475 " GROUP BY comm_id, thread_id, call_path_id" 476 " ORDER BY comm_id, thread_id, call_path_id") 477 478 def FindPath(self, query): 479 # Turn the query result into a list of ids that the tree view can walk 480 # to open the tree at the right place. 481 ids = [] 482 parent_id = query.value(0) 483 while parent_id: 484 ids.insert(0, parent_id) 485 q2 = QSqlQuery(self.glb.db) 486 QueryExec(q2, "SELECT parent_id" 487 " FROM call_paths" 488 " WHERE id = " + str(parent_id)) 489 if not q2.next(): 490 break 491 parent_id = q2.value(0) 492 # The call path root is not used 493 if ids[0] == 1: 494 del ids[0] 495 ids.insert(0, query.value(2)) 496 ids.insert(0, query.value(1)) 497 return ids 498 499 def Found(self, query, found): 500 if found: 501 return self.FindPath(query) 502 return [] 503 504 def FindValue(self, value, pattern, query, last_value, last_pattern): 505 if last_value == value and pattern == last_pattern: 506 found = query.first() 507 else: 508 self.FindSelect(value, pattern, query) 509 found = query.next() 510 return self.Found(query, found) 511 512 def FindNext(self, query): 513 found = query.next() 514 if not found: 515 found = query.first() 516 return self.Found(query, found) 517 518 def FindPrev(self, query): 519 found = query.previous() 520 if not found: 521 found = query.last() 522 return self.Found(query, found) 523 524 def FindThread(self, c): 525 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern: 526 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern) 527 elif c.direction > 0: 528 ids = self.FindNext(c.query) 529 else: 530 ids = self.FindPrev(c.query) 531 return (True, ids) 532 533 def Find(self, value, direction, pattern, context, callback): 534 class Context(): 535 def __init__(self, *x): 536 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x 537 def Update(self, *x): 538 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern) 539 if len(context): 540 context[0].Update(value, direction, pattern) 541 else: 542 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None)) 543 # Use a thread so the UI is not blocked during the SELECT 544 thread = Thread(self.FindThread, context[0]) 545 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection) 546 thread.start() 547 548 def FindDone(self, thread, callback, ids): 549 callback(ids) 550 551# Vertical widget layout 552 553class VBox(): 554 555 def __init__(self, w1, w2, w3=None): 556 self.vbox = QWidget() 557 self.vbox.setLayout(QVBoxLayout()); 558 559 self.vbox.layout().setContentsMargins(0, 0, 0, 0) 560 561 self.vbox.layout().addWidget(w1) 562 self.vbox.layout().addWidget(w2) 563 if w3: 564 self.vbox.layout().addWidget(w3) 565 566 def Widget(self): 567 return self.vbox 568 569# Context-sensitive call graph window 570 571class CallGraphWindow(QMdiSubWindow): 572 573 def __init__(self, glb, parent=None): 574 super(CallGraphWindow, self).__init__(parent) 575 576 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) 577 578 self.view = QTreeView() 579 self.view.setModel(self.model) 580 581 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): 582 self.view.setColumnWidth(c, w) 583 584 self.find_bar = FindBar(self, self) 585 586 self.vbox = VBox(self.view, self.find_bar.Widget()) 587 588 self.setWidget(self.vbox.Widget()) 589 590 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") 591 592 def DisplayFound(self, ids): 593 if not len(ids): 594 return False 595 parent = QModelIndex() 596 for dbid in ids: 597 found = False 598 n = self.model.rowCount(parent) 599 for row in xrange(n): 600 child = self.model.index(row, 0, parent) 601 if child.internalPointer().dbid == dbid: 602 found = True 603 self.view.setCurrentIndex(child) 604 parent = child 605 break 606 if not found: 607 break 608 return found 609 610 def Find(self, value, direction, pattern, context): 611 self.view.setFocus() 612 self.find_bar.Busy() 613 self.model.Find(value, direction, pattern, context, self.FindDone) 614 615 def FindDone(self, ids): 616 found = True 617 if not self.DisplayFound(ids): 618 found = False 619 self.find_bar.Idle() 620 if not found: 621 self.find_bar.NotFound() 622 623# Action Definition 624 625def CreateAction(label, tip, callback, parent=None, shortcut=None): 626 action = QAction(label, parent) 627 if shortcut != None: 628 action.setShortcuts(shortcut) 629 action.setStatusTip(tip) 630 action.triggered.connect(callback) 631 return action 632 633# Typical application actions 634 635def CreateExitAction(app, parent=None): 636 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) 637 638# Typical MDI actions 639 640def CreateCloseActiveWindowAction(mdi_area): 641 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) 642 643def CreateCloseAllWindowsAction(mdi_area): 644 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) 645 646def CreateTileWindowsAction(mdi_area): 647 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) 648 649def CreateCascadeWindowsAction(mdi_area): 650 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) 651 652def CreateNextWindowAction(mdi_area): 653 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) 654 655def CreatePreviousWindowAction(mdi_area): 656 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) 657 658# Typical MDI window menu 659 660class WindowMenu(): 661 662 def __init__(self, mdi_area, menu): 663 self.mdi_area = mdi_area 664 self.window_menu = menu.addMenu("&Windows") 665 self.close_active_window = CreateCloseActiveWindowAction(mdi_area) 666 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) 667 self.tile_windows = CreateTileWindowsAction(mdi_area) 668 self.cascade_windows = CreateCascadeWindowsAction(mdi_area) 669 self.next_window = CreateNextWindowAction(mdi_area) 670 self.previous_window = CreatePreviousWindowAction(mdi_area) 671 self.window_menu.aboutToShow.connect(self.Update) 672 673 def Update(self): 674 self.window_menu.clear() 675 sub_window_count = len(self.mdi_area.subWindowList()) 676 have_sub_windows = sub_window_count != 0 677 self.close_active_window.setEnabled(have_sub_windows) 678 self.close_all_windows.setEnabled(have_sub_windows) 679 self.tile_windows.setEnabled(have_sub_windows) 680 self.cascade_windows.setEnabled(have_sub_windows) 681 self.next_window.setEnabled(have_sub_windows) 682 self.previous_window.setEnabled(have_sub_windows) 683 self.window_menu.addAction(self.close_active_window) 684 self.window_menu.addAction(self.close_all_windows) 685 self.window_menu.addSeparator() 686 self.window_menu.addAction(self.tile_windows) 687 self.window_menu.addAction(self.cascade_windows) 688 self.window_menu.addSeparator() 689 self.window_menu.addAction(self.next_window) 690 self.window_menu.addAction(self.previous_window) 691 if sub_window_count == 0: 692 return 693 self.window_menu.addSeparator() 694 nr = 1 695 for sub_window in self.mdi_area.subWindowList(): 696 label = str(nr) + " " + sub_window.name 697 if nr < 10: 698 label = "&" + label 699 action = self.window_menu.addAction(label) 700 action.setCheckable(True) 701 action.setChecked(sub_window == self.mdi_area.activeSubWindow()) 702 action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x)) 703 self.window_menu.addAction(action) 704 nr += 1 705 706 def setActiveSubWindow(self, nr): 707 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) 708 709# Font resize 710 711def ResizeFont(widget, diff): 712 font = widget.font() 713 sz = font.pointSize() 714 font.setPointSize(sz + diff) 715 widget.setFont(font) 716 717def ShrinkFont(widget): 718 ResizeFont(widget, -1) 719 720def EnlargeFont(widget): 721 ResizeFont(widget, 1) 722 723# Unique name for sub-windows 724 725def NumberedWindowName(name, nr): 726 if nr > 1: 727 name += " <" + str(nr) + ">" 728 return name 729 730def UniqueSubWindowName(mdi_area, name): 731 nr = 1 732 while True: 733 unique_name = NumberedWindowName(name, nr) 734 ok = True 735 for sub_window in mdi_area.subWindowList(): 736 if sub_window.name == unique_name: 737 ok = False 738 break 739 if ok: 740 return unique_name 741 nr += 1 742 743# Add a sub-window 744 745def AddSubWindow(mdi_area, sub_window, name): 746 unique_name = UniqueSubWindowName(mdi_area, name) 747 sub_window.setMinimumSize(200, 100) 748 sub_window.resize(800, 600) 749 sub_window.setWindowTitle(unique_name) 750 sub_window.setAttribute(Qt.WA_DeleteOnClose) 751 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) 752 sub_window.name = unique_name 753 mdi_area.addSubWindow(sub_window) 754 sub_window.show() 755 756# Main window 757 758class MainWindow(QMainWindow): 759 760 def __init__(self, glb, parent=None): 761 super(MainWindow, self).__init__(parent) 762 763 self.glb = glb 764 765 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) 766 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) 767 self.setMinimumSize(200, 100) 768 769 self.mdi_area = QMdiArea() 770 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) 771 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 772 773 self.setCentralWidget(self.mdi_area) 774 775 menu = self.menuBar() 776 777 file_menu = menu.addMenu("&File") 778 file_menu.addAction(CreateExitAction(glb.app, self)) 779 780 edit_menu = menu.addMenu("&Edit") 781 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) 782 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")])) 783 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")])) 784 785 reports_menu = menu.addMenu("&Reports") 786 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) 787 788 self.window_menu = WindowMenu(self.mdi_area, menu) 789 790 def Find(self): 791 win = self.mdi_area.activeSubWindow() 792 if win: 793 try: 794 win.find_bar.Activate() 795 except: 796 pass 797 798 def ShrinkFont(self): 799 win = self.mdi_area.activeSubWindow() 800 ShrinkFont(win.view) 801 802 def EnlargeFont(self): 803 win = self.mdi_area.activeSubWindow() 804 EnlargeFont(win.view) 805 806 def NewCallGraph(self): 807 CallGraphWindow(self.glb, self) 808 809# Global data 810 811class Glb(): 812 813 def __init__(self, dbref, db, dbname): 814 self.dbref = dbref 815 self.db = db 816 self.dbname = dbname 817 self.app = None 818 self.mainwindow = None 819 820# Database reference 821 822class DBRef(): 823 824 def __init__(self, is_sqlite3, dbname): 825 self.is_sqlite3 = is_sqlite3 826 self.dbname = dbname 827 828 def Open(self, connection_name): 829 dbname = self.dbname 830 if self.is_sqlite3: 831 db = QSqlDatabase.addDatabase("QSQLITE", connection_name) 832 else: 833 db = QSqlDatabase.addDatabase("QPSQL", connection_name) 834 opts = dbname.split() 835 for opt in opts: 836 if "=" in opt: 837 opt = opt.split("=") 838 if opt[0] == "hostname": 839 db.setHostName(opt[1]) 840 elif opt[0] == "port": 841 db.setPort(int(opt[1])) 842 elif opt[0] == "username": 843 db.setUserName(opt[1]) 844 elif opt[0] == "password": 845 db.setPassword(opt[1]) 846 elif opt[0] == "dbname": 847 dbname = opt[1] 848 else: 849 dbname = opt 850 851 db.setDatabaseName(dbname) 852 if not db.open(): 853 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) 854 return db, dbname 855 856# Main 857 858def Main(): 859 if (len(sys.argv) < 2): 860 print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>" 861 raise Exception("Too few arguments") 862 863 dbname = sys.argv[1] 864 865 is_sqlite3 = False 866 try: 867 f = open(dbname) 868 if f.read(15) == "SQLite format 3": 869 is_sqlite3 = True 870 f.close() 871 except: 872 pass 873 874 dbref = DBRef(is_sqlite3, dbname) 875 db, dbname = dbref.Open("main") 876 glb = Glb(dbref, db, dbname) 877 app = QApplication(sys.argv) 878 glb.app = app 879 mainwindow = MainWindow(glb) 880 glb.mainwindow = mainwindow 881 mainwindow.show() 882 err = app.exec_() 883 db.close() 884 sys.exit(err) 885 886if __name__ == "__main__": 887 Main() 888