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 52from PySide.QtCore import * 53from PySide.QtGui import * 54from PySide.QtSql import * 55from decimal import * 56 57# Data formatting helpers 58 59def dsoname(name): 60 if name == "[kernel.kallsyms]": 61 return "[kernel]" 62 return name 63 64# Percent to one decimal place 65 66def PercentToOneDP(n, d): 67 if not d: 68 return "0.0" 69 x = (n * Decimal(100)) / d 70 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) 71 72# Helper for queries that must not fail 73 74def QueryExec(query, stmt): 75 ret = query.exec_(stmt) 76 if not ret: 77 raise Exception("Query failed: " + query.lastError().text()) 78 79# Tree data model 80 81class TreeModel(QAbstractItemModel): 82 83 def __init__(self, root, parent=None): 84 super(TreeModel, self).__init__(parent) 85 self.root = root 86 self.last_row_read = 0 87 88 def Item(self, parent): 89 if parent.isValid(): 90 return parent.internalPointer() 91 else: 92 return self.root 93 94 def rowCount(self, parent): 95 result = self.Item(parent).childCount() 96 if result < 0: 97 result = 0 98 self.dataChanged.emit(parent, parent) 99 return result 100 101 def hasChildren(self, parent): 102 return self.Item(parent).hasChildren() 103 104 def headerData(self, section, orientation, role): 105 if role == Qt.TextAlignmentRole: 106 return self.columnAlignment(section) 107 if role != Qt.DisplayRole: 108 return None 109 if orientation != Qt.Horizontal: 110 return None 111 return self.columnHeader(section) 112 113 def parent(self, child): 114 child_item = child.internalPointer() 115 if child_item is self.root: 116 return QModelIndex() 117 parent_item = child_item.getParentItem() 118 return self.createIndex(parent_item.getRow(), 0, parent_item) 119 120 def index(self, row, column, parent): 121 child_item = self.Item(parent).getChildItem(row) 122 return self.createIndex(row, column, child_item) 123 124 def DisplayData(self, item, index): 125 return item.getData(index.column()) 126 127 def columnAlignment(self, column): 128 return Qt.AlignLeft 129 130 def columnFont(self, column): 131 return None 132 133 def data(self, index, role): 134 if role == Qt.TextAlignmentRole: 135 return self.columnAlignment(index.column()) 136 if role == Qt.FontRole: 137 return self.columnFont(index.column()) 138 if role != Qt.DisplayRole: 139 return None 140 item = index.internalPointer() 141 return self.DisplayData(item, index) 142 143# Model cache 144 145model_cache = weakref.WeakValueDictionary() 146model_cache_lock = threading.Lock() 147 148def LookupCreateModel(model_name, create_fn): 149 model_cache_lock.acquire() 150 try: 151 model = model_cache[model_name] 152 except: 153 model = None 154 if model is None: 155 model = create_fn() 156 model_cache[model_name] = model 157 model_cache_lock.release() 158 return model 159 160# Context-sensitive call graph data model item base 161 162class CallGraphLevelItemBase(object): 163 164 def __init__(self, glb, row, parent_item): 165 self.glb = glb 166 self.row = row 167 self.parent_item = parent_item 168 self.query_done = False; 169 self.child_count = 0 170 self.child_items = [] 171 172 def getChildItem(self, row): 173 return self.child_items[row] 174 175 def getParentItem(self): 176 return self.parent_item 177 178 def getRow(self): 179 return self.row 180 181 def childCount(self): 182 if not self.query_done: 183 self.Select() 184 if not self.child_count: 185 return -1 186 return self.child_count 187 188 def hasChildren(self): 189 if not self.query_done: 190 return True 191 return self.child_count > 0 192 193 def getData(self, column): 194 return self.data[column] 195 196# Context-sensitive call graph data model level 2+ item base 197 198class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): 199 200 def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item): 201 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item) 202 self.comm_id = comm_id 203 self.thread_id = thread_id 204 self.call_path_id = call_path_id 205 self.branch_count = branch_count 206 self.time = time 207 208 def Select(self): 209 self.query_done = True; 210 query = QSqlQuery(self.glb.db) 211 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)" 212 " FROM calls" 213 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 214 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 215 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 216 " WHERE parent_call_path_id = " + str(self.call_path_id) + 217 " AND comm_id = " + str(self.comm_id) + 218 " AND thread_id = " + str(self.thread_id) + 219 " GROUP BY call_path_id, name, short_name" 220 " ORDER BY call_path_id") 221 while query.next(): 222 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) 223 self.child_items.append(child_item) 224 self.child_count += 1 225 226# Context-sensitive call graph data model level three item 227 228class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): 229 230 def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item): 231 super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item) 232 dso = dsoname(dso) 233 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 234 self.dbid = call_path_id 235 236# Context-sensitive call graph data model level two item 237 238class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): 239 240 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item): 241 super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item) 242 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 243 self.dbid = thread_id 244 245 def Select(self): 246 super(CallGraphLevelTwoItem, self).Select() 247 for child_item in self.child_items: 248 self.time += child_item.time 249 self.branch_count += child_item.branch_count 250 for child_item in self.child_items: 251 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 252 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 253 254# Context-sensitive call graph data model level one item 255 256class CallGraphLevelOneItem(CallGraphLevelItemBase): 257 258 def __init__(self, glb, row, comm_id, comm, parent_item): 259 super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item) 260 self.data = [comm, "", "", "", "", "", ""] 261 self.dbid = comm_id 262 263 def Select(self): 264 self.query_done = True; 265 query = QSqlQuery(self.glb.db) 266 QueryExec(query, "SELECT thread_id, pid, tid" 267 " FROM comm_threads" 268 " INNER JOIN threads ON thread_id = threads.id" 269 " WHERE comm_id = " + str(self.dbid)) 270 while query.next(): 271 child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 272 self.child_items.append(child_item) 273 self.child_count += 1 274 275# Context-sensitive call graph data model root item 276 277class CallGraphRootItem(CallGraphLevelItemBase): 278 279 def __init__(self, glb): 280 super(CallGraphRootItem, self).__init__(glb, 0, None) 281 self.dbid = 0 282 self.query_done = True; 283 query = QSqlQuery(glb.db) 284 QueryExec(query, "SELECT id, comm FROM comms") 285 while query.next(): 286 if not query.value(0): 287 continue 288 child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self) 289 self.child_items.append(child_item) 290 self.child_count += 1 291 292# Context-sensitive call graph data model 293 294class CallGraphModel(TreeModel): 295 296 def __init__(self, glb, parent=None): 297 super(CallGraphModel, self).__init__(CallGraphRootItem(glb), parent) 298 self.glb = glb 299 300 def columnCount(self, parent=None): 301 return 7 302 303 def columnHeader(self, column): 304 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 305 return headers[column] 306 307 def columnAlignment(self, column): 308 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 309 return alignment[column] 310 311# Context-sensitive call graph window 312 313class CallGraphWindow(QMdiSubWindow): 314 315 def __init__(self, glb, parent=None): 316 super(CallGraphWindow, self).__init__(parent) 317 318 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x)) 319 320 self.view = QTreeView() 321 self.view.setModel(self.model) 322 323 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): 324 self.view.setColumnWidth(c, w) 325 326 self.setWidget(self.view) 327 328 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") 329 330# Action Definition 331 332def CreateAction(label, tip, callback, parent=None, shortcut=None): 333 action = QAction(label, parent) 334 if shortcut != None: 335 action.setShortcuts(shortcut) 336 action.setStatusTip(tip) 337 action.triggered.connect(callback) 338 return action 339 340# Typical application actions 341 342def CreateExitAction(app, parent=None): 343 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit) 344 345# Typical MDI actions 346 347def CreateCloseActiveWindowAction(mdi_area): 348 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area) 349 350def CreateCloseAllWindowsAction(mdi_area): 351 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area) 352 353def CreateTileWindowsAction(mdi_area): 354 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area) 355 356def CreateCascadeWindowsAction(mdi_area): 357 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area) 358 359def CreateNextWindowAction(mdi_area): 360 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild) 361 362def CreatePreviousWindowAction(mdi_area): 363 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild) 364 365# Typical MDI window menu 366 367class WindowMenu(): 368 369 def __init__(self, mdi_area, menu): 370 self.mdi_area = mdi_area 371 self.window_menu = menu.addMenu("&Windows") 372 self.close_active_window = CreateCloseActiveWindowAction(mdi_area) 373 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area) 374 self.tile_windows = CreateTileWindowsAction(mdi_area) 375 self.cascade_windows = CreateCascadeWindowsAction(mdi_area) 376 self.next_window = CreateNextWindowAction(mdi_area) 377 self.previous_window = CreatePreviousWindowAction(mdi_area) 378 self.window_menu.aboutToShow.connect(self.Update) 379 380 def Update(self): 381 self.window_menu.clear() 382 sub_window_count = len(self.mdi_area.subWindowList()) 383 have_sub_windows = sub_window_count != 0 384 self.close_active_window.setEnabled(have_sub_windows) 385 self.close_all_windows.setEnabled(have_sub_windows) 386 self.tile_windows.setEnabled(have_sub_windows) 387 self.cascade_windows.setEnabled(have_sub_windows) 388 self.next_window.setEnabled(have_sub_windows) 389 self.previous_window.setEnabled(have_sub_windows) 390 self.window_menu.addAction(self.close_active_window) 391 self.window_menu.addAction(self.close_all_windows) 392 self.window_menu.addSeparator() 393 self.window_menu.addAction(self.tile_windows) 394 self.window_menu.addAction(self.cascade_windows) 395 self.window_menu.addSeparator() 396 self.window_menu.addAction(self.next_window) 397 self.window_menu.addAction(self.previous_window) 398 if sub_window_count == 0: 399 return 400 self.window_menu.addSeparator() 401 nr = 1 402 for sub_window in self.mdi_area.subWindowList(): 403 label = str(nr) + " " + sub_window.name 404 if nr < 10: 405 label = "&" + label 406 action = self.window_menu.addAction(label) 407 action.setCheckable(True) 408 action.setChecked(sub_window == self.mdi_area.activeSubWindow()) 409 action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x)) 410 self.window_menu.addAction(action) 411 nr += 1 412 413 def setActiveSubWindow(self, nr): 414 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) 415 416# Unique name for sub-windows 417 418def NumberedWindowName(name, nr): 419 if nr > 1: 420 name += " <" + str(nr) + ">" 421 return name 422 423def UniqueSubWindowName(mdi_area, name): 424 nr = 1 425 while True: 426 unique_name = NumberedWindowName(name, nr) 427 ok = True 428 for sub_window in mdi_area.subWindowList(): 429 if sub_window.name == unique_name: 430 ok = False 431 break 432 if ok: 433 return unique_name 434 nr += 1 435 436# Add a sub-window 437 438def AddSubWindow(mdi_area, sub_window, name): 439 unique_name = UniqueSubWindowName(mdi_area, name) 440 sub_window.setMinimumSize(200, 100) 441 sub_window.resize(800, 600) 442 sub_window.setWindowTitle(unique_name) 443 sub_window.setAttribute(Qt.WA_DeleteOnClose) 444 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon)) 445 sub_window.name = unique_name 446 mdi_area.addSubWindow(sub_window) 447 sub_window.show() 448 449# Main window 450 451class MainWindow(QMainWindow): 452 453 def __init__(self, glb, parent=None): 454 super(MainWindow, self).__init__(parent) 455 456 self.glb = glb 457 458 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname) 459 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) 460 self.setMinimumSize(200, 100) 461 462 self.mdi_area = QMdiArea() 463 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) 464 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) 465 466 self.setCentralWidget(self.mdi_area) 467 468 menu = self.menuBar() 469 470 file_menu = menu.addMenu("&File") 471 file_menu.addAction(CreateExitAction(glb.app, self)) 472 473 reports_menu = menu.addMenu("&Reports") 474 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) 475 476 self.window_menu = WindowMenu(self.mdi_area, menu) 477 478 def NewCallGraph(self): 479 CallGraphWindow(self.glb, self) 480 481# Global data 482 483class Glb(): 484 485 def __init__(self, dbref, db, dbname): 486 self.dbref = dbref 487 self.db = db 488 self.dbname = dbname 489 self.app = None 490 self.mainwindow = None 491 492# Database reference 493 494class DBRef(): 495 496 def __init__(self, is_sqlite3, dbname): 497 self.is_sqlite3 = is_sqlite3 498 self.dbname = dbname 499 500 def Open(self, connection_name): 501 dbname = self.dbname 502 if self.is_sqlite3: 503 db = QSqlDatabase.addDatabase("QSQLITE", connection_name) 504 else: 505 db = QSqlDatabase.addDatabase("QPSQL", connection_name) 506 opts = dbname.split() 507 for opt in opts: 508 if "=" in opt: 509 opt = opt.split("=") 510 if opt[0] == "hostname": 511 db.setHostName(opt[1]) 512 elif opt[0] == "port": 513 db.setPort(int(opt[1])) 514 elif opt[0] == "username": 515 db.setUserName(opt[1]) 516 elif opt[0] == "password": 517 db.setPassword(opt[1]) 518 elif opt[0] == "dbname": 519 dbname = opt[1] 520 else: 521 dbname = opt 522 523 db.setDatabaseName(dbname) 524 if not db.open(): 525 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) 526 return db, dbname 527 528# Main 529 530def Main(): 531 if (len(sys.argv) < 2): 532 print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>" 533 raise Exception("Too few arguments") 534 535 dbname = sys.argv[1] 536 537 is_sqlite3 = False 538 try: 539 f = open(dbname) 540 if f.read(15) == "SQLite format 3": 541 is_sqlite3 = True 542 f.close() 543 except: 544 pass 545 546 dbref = DBRef(is_sqlite3, dbname) 547 db, dbname = dbref.Open("main") 548 glb = Glb(dbref, db, dbname) 549 app = QApplication(sys.argv) 550 glb.app = app 551 mainwindow = MainWindow(glb) 552 glb.mainwindow = mainwindow 553 mainwindow.show() 554 err = app.exec_() 555 db.close() 556 sys.exit(err) 557 558if __name__ == "__main__": 559 Main() 560