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 50from PySide.QtCore import * 51from PySide.QtGui import * 52from PySide.QtSql import * 53from decimal import * 54 55# Data formatting helpers 56 57def dsoname(name): 58 if name == "[kernel.kallsyms]": 59 return "[kernel]" 60 return name 61 62# Percent to one decimal place 63 64def PercentToOneDP(n, d): 65 if not d: 66 return "0.0" 67 x = (n * Decimal(100)) / d 68 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP)) 69 70# Helper for queries that must not fail 71 72def QueryExec(query, stmt): 73 ret = query.exec_(stmt) 74 if not ret: 75 raise Exception("Query failed: " + query.lastError().text()) 76 77# Tree data model 78 79class TreeModel(QAbstractItemModel): 80 81 def __init__(self, root, parent=None): 82 super(TreeModel, self).__init__(parent) 83 self.root = root 84 self.last_row_read = 0 85 86 def Item(self, parent): 87 if parent.isValid(): 88 return parent.internalPointer() 89 else: 90 return self.root 91 92 def rowCount(self, parent): 93 result = self.Item(parent).childCount() 94 if result < 0: 95 result = 0 96 self.dataChanged.emit(parent, parent) 97 return result 98 99 def hasChildren(self, parent): 100 return self.Item(parent).hasChildren() 101 102 def headerData(self, section, orientation, role): 103 if role == Qt.TextAlignmentRole: 104 return self.columnAlignment(section) 105 if role != Qt.DisplayRole: 106 return None 107 if orientation != Qt.Horizontal: 108 return None 109 return self.columnHeader(section) 110 111 def parent(self, child): 112 child_item = child.internalPointer() 113 if child_item is self.root: 114 return QModelIndex() 115 parent_item = child_item.getParentItem() 116 return self.createIndex(parent_item.getRow(), 0, parent_item) 117 118 def index(self, row, column, parent): 119 child_item = self.Item(parent).getChildItem(row) 120 return self.createIndex(row, column, child_item) 121 122 def DisplayData(self, item, index): 123 return item.getData(index.column()) 124 125 def columnAlignment(self, column): 126 return Qt.AlignLeft 127 128 def columnFont(self, column): 129 return None 130 131 def data(self, index, role): 132 if role == Qt.TextAlignmentRole: 133 return self.columnAlignment(index.column()) 134 if role == Qt.FontRole: 135 return self.columnFont(index.column()) 136 if role != Qt.DisplayRole: 137 return None 138 item = index.internalPointer() 139 return self.DisplayData(item, index) 140 141# Context-sensitive call graph data model item base 142 143class CallGraphLevelItemBase(object): 144 145 def __init__(self, glb, row, parent_item): 146 self.glb = glb 147 self.row = row 148 self.parent_item = parent_item 149 self.query_done = False; 150 self.child_count = 0 151 self.child_items = [] 152 153 def getChildItem(self, row): 154 return self.child_items[row] 155 156 def getParentItem(self): 157 return self.parent_item 158 159 def getRow(self): 160 return self.row 161 162 def childCount(self): 163 if not self.query_done: 164 self.Select() 165 if not self.child_count: 166 return -1 167 return self.child_count 168 169 def hasChildren(self): 170 if not self.query_done: 171 return True 172 return self.child_count > 0 173 174 def getData(self, column): 175 return self.data[column] 176 177# Context-sensitive call graph data model level 2+ item base 178 179class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase): 180 181 def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item): 182 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item) 183 self.comm_id = comm_id 184 self.thread_id = thread_id 185 self.call_path_id = call_path_id 186 self.branch_count = branch_count 187 self.time = time 188 189 def Select(self): 190 self.query_done = True; 191 query = QSqlQuery(self.glb.db) 192 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)" 193 " FROM calls" 194 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id" 195 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id" 196 " INNER JOIN dsos ON symbols.dso_id = dsos.id" 197 " WHERE parent_call_path_id = " + str(self.call_path_id) + 198 " AND comm_id = " + str(self.comm_id) + 199 " AND thread_id = " + str(self.thread_id) + 200 " GROUP BY call_path_id, name, short_name" 201 " ORDER BY call_path_id") 202 while query.next(): 203 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) 204 self.child_items.append(child_item) 205 self.child_count += 1 206 207# Context-sensitive call graph data model level three item 208 209class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase): 210 211 def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item): 212 super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item) 213 dso = dsoname(dso) 214 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ] 215 self.dbid = call_path_id 216 217# Context-sensitive call graph data model level two item 218 219class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase): 220 221 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item): 222 super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item) 223 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""] 224 self.dbid = thread_id 225 226 def Select(self): 227 super(CallGraphLevelTwoItem, self).Select() 228 for child_item in self.child_items: 229 self.time += child_item.time 230 self.branch_count += child_item.branch_count 231 for child_item in self.child_items: 232 child_item.data[4] = PercentToOneDP(child_item.time, self.time) 233 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count) 234 235# Context-sensitive call graph data model level one item 236 237class CallGraphLevelOneItem(CallGraphLevelItemBase): 238 239 def __init__(self, glb, row, comm_id, comm, parent_item): 240 super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item) 241 self.data = [comm, "", "", "", "", "", ""] 242 self.dbid = comm_id 243 244 def Select(self): 245 self.query_done = True; 246 query = QSqlQuery(self.glb.db) 247 QueryExec(query, "SELECT thread_id, pid, tid" 248 " FROM comm_threads" 249 " INNER JOIN threads ON thread_id = threads.id" 250 " WHERE comm_id = " + str(self.dbid)) 251 while query.next(): 252 child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self) 253 self.child_items.append(child_item) 254 self.child_count += 1 255 256# Context-sensitive call graph data model root item 257 258class CallGraphRootItem(CallGraphLevelItemBase): 259 260 def __init__(self, glb): 261 super(CallGraphRootItem, self).__init__(glb, 0, None) 262 self.dbid = 0 263 self.query_done = True; 264 query = QSqlQuery(glb.db) 265 QueryExec(query, "SELECT id, comm FROM comms") 266 while query.next(): 267 if not query.value(0): 268 continue 269 child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self) 270 self.child_items.append(child_item) 271 self.child_count += 1 272 273# Context-sensitive call graph data model 274 275class CallGraphModel(TreeModel): 276 277 def __init__(self, glb, parent=None): 278 super(CallGraphModel, self).__init__(CallGraphRootItem(glb), parent) 279 self.glb = glb 280 281 def columnCount(self, parent=None): 282 return 7 283 284 def columnHeader(self, column): 285 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 286 return headers[column] 287 288 def columnAlignment(self, column): 289 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] 290 return alignment[column] 291 292# Main window 293 294class MainWindow(QMainWindow): 295 296 def __init__(self, glb, parent=None): 297 super(MainWindow, self).__init__(parent) 298 299 self.glb = glb 300 301 self.setWindowTitle("Call Graph: " + glb.dbname) 302 self.move(100, 100) 303 self.resize(800, 600) 304 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon)) 305 self.setMinimumSize(200, 100) 306 307 self.model = CallGraphModel(glb) 308 309 self.view = QTreeView() 310 self.view.setModel(self.model) 311 312 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): 313 self.view.setColumnWidth(c, w) 314 315 self.setCentralWidget(self.view) 316 317# Global data 318 319class Glb(): 320 321 def __init__(self, dbref, db, dbname): 322 self.dbref = dbref 323 self.db = db 324 self.dbname = dbname 325 self.app = None 326 self.mainwindow = None 327 328# Database reference 329 330class DBRef(): 331 332 def __init__(self, is_sqlite3, dbname): 333 self.is_sqlite3 = is_sqlite3 334 self.dbname = dbname 335 336 def Open(self, connection_name): 337 dbname = self.dbname 338 if self.is_sqlite3: 339 db = QSqlDatabase.addDatabase("QSQLITE", connection_name) 340 else: 341 db = QSqlDatabase.addDatabase("QPSQL", connection_name) 342 opts = dbname.split() 343 for opt in opts: 344 if "=" in opt: 345 opt = opt.split("=") 346 if opt[0] == "hostname": 347 db.setHostName(opt[1]) 348 elif opt[0] == "port": 349 db.setPort(int(opt[1])) 350 elif opt[0] == "username": 351 db.setUserName(opt[1]) 352 elif opt[0] == "password": 353 db.setPassword(opt[1]) 354 elif opt[0] == "dbname": 355 dbname = opt[1] 356 else: 357 dbname = opt 358 359 db.setDatabaseName(dbname) 360 if not db.open(): 361 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) 362 return db, dbname 363 364# Main 365 366def Main(): 367 if (len(sys.argv) < 2): 368 print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>" 369 raise Exception("Too few arguments") 370 371 dbname = sys.argv[1] 372 373 is_sqlite3 = False 374 try: 375 f = open(dbname) 376 if f.read(15) == "SQLite format 3": 377 is_sqlite3 = True 378 f.close() 379 except: 380 pass 381 382 dbref = DBRef(is_sqlite3, dbname) 383 db, dbname = dbref.Open("main") 384 glb = Glb(dbref, db, dbname) 385 app = QApplication(sys.argv) 386 glb.app = app 387 mainwindow = MainWindow(glb) 388 glb.mainwindow = mainwindow 389 mainwindow.show() 390 err = app.exec_() 391 db.close() 392 sys.exit(err) 393 394if __name__ == "__main__": 395 Main() 396