1#!/usr/bin/python2 2# call-graph-from-sql.py: create call-graph from sql database 3# Copyright (c) 2014-2017, Intel Corporation. 4# 5# This program is free software; you can redistribute it and/or modify it 6# under the terms and conditions of the GNU General Public License, 7# version 2, as published by the Free Software Foundation. 8# 9# This program is distributed in the hope it will be useful, but WITHOUT 10# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 12# more details. 13 14# To use this script you will need to have exported data using either the 15# export-to-sqlite.py or the export-to-postgresql.py script. Refer to those 16# scripts for details. 17# 18# Following on from the example in the export scripts, a 19# call-graph can be displayed for the pt_example database like this: 20# 21# python tools/perf/scripts/python/call-graph-from-sql.py pt_example 22# 23# Note that for PostgreSQL, this script supports connecting to remote databases 24# by setting hostname, port, username, password, and dbname e.g. 25# 26# python tools/perf/scripts/python/call-graph-from-sql.py "hostname=myhost username=myuser password=mypassword dbname=pt_example" 27# 28# The result is a GUI window with a tree representing a context-sensitive 29# call-graph. Expanding a couple of levels of the tree and adjusting column 30# widths to suit will display something like: 31# 32# Call Graph: pt_example 33# Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%) 34# v- ls 35# v- 2638:2638 36# v- _start ld-2.19.so 1 10074071 100.0 211135 100.0 37# |- unknown unknown 1 13198 0.1 1 0.0 38# >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3 39# >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3 40# v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4 41# >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1 42# >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0 43# >- __libc_csu_init ls 1 10354 0.1 10 0.0 44# |- _setjmp libc-2.19.so 1 0 0.0 4 0.0 45# v- main ls 1 8182043 99.6 180254 99.9 46# 47# Points to note: 48# The top level is a command name (comm) 49# The next level is a thread (pid:tid) 50# Subsequent levels are functions 51# 'Count' is the number of calls 52# 'Time' is the elapsed time until the function returns 53# Percentages are relative to the level above 54# 'Branch Count' is the total number of branches for that function and all 55# functions that it calls 56 57import sys 58from PySide.QtCore import * 59from PySide.QtGui import * 60from PySide.QtSql import * 61from decimal import * 62 63class TreeItem(): 64 65 def __init__(self, db, row, parent_item): 66 self.db = db 67 self.row = row 68 self.parent_item = parent_item 69 self.query_done = False; 70 self.child_count = 0 71 self.child_items = [] 72 self.data = ["", "", "", "", "", "", ""] 73 self.comm_id = 0 74 self.thread_id = 0 75 self.call_path_id = 1 76 self.branch_count = 0 77 self.time = 0 78 if not parent_item: 79 self.setUpRoot() 80 81 def setUpRoot(self): 82 self.query_done = True 83 query = QSqlQuery(self.db) 84 ret = query.exec_('SELECT id, comm FROM comms') 85 if not ret: 86 raise Exception("Query failed: " + query.lastError().text()) 87 while query.next(): 88 if not query.value(0): 89 continue 90 child_item = TreeItem(self.db, self.child_count, self) 91 self.child_items.append(child_item) 92 self.child_count += 1 93 child_item.setUpLevel1(query.value(0), query.value(1)) 94 95 def setUpLevel1(self, comm_id, comm): 96 self.query_done = True; 97 self.comm_id = comm_id 98 self.data[0] = comm 99 self.child_items = [] 100 self.child_count = 0 101 query = QSqlQuery(self.db) 102 ret = query.exec_('SELECT thread_id, ( SELECT pid FROM threads WHERE id = thread_id ), ( SELECT tid FROM threads WHERE id = thread_id ) FROM comm_threads WHERE comm_id = ' + str(comm_id)) 103 if not ret: 104 raise Exception("Query failed: " + query.lastError().text()) 105 while query.next(): 106 child_item = TreeItem(self.db, self.child_count, self) 107 self.child_items.append(child_item) 108 self.child_count += 1 109 child_item.setUpLevel2(comm_id, query.value(0), query.value(1), query.value(2)) 110 111 def setUpLevel2(self, comm_id, thread_id, pid, tid): 112 self.comm_id = comm_id 113 self.thread_id = thread_id 114 self.data[0] = str(pid) + ":" + str(tid) 115 116 def getChildItem(self, row): 117 return self.child_items[row] 118 119 def getParentItem(self): 120 return self.parent_item 121 122 def getRow(self): 123 return self.row 124 125 def timePercent(self, b): 126 if not self.time: 127 return "0.0" 128 x = (b * Decimal(100)) / self.time 129 return str(x.quantize(Decimal('.1'), rounding=ROUND_HALF_UP)) 130 131 def branchPercent(self, b): 132 if not self.branch_count: 133 return "0.0" 134 x = (b * Decimal(100)) / self.branch_count 135 return str(x.quantize(Decimal('.1'), rounding=ROUND_HALF_UP)) 136 137 def addChild(self, call_path_id, name, dso, count, time, branch_count): 138 child_item = TreeItem(self.db, self.child_count, self) 139 child_item.comm_id = self.comm_id 140 child_item.thread_id = self.thread_id 141 child_item.call_path_id = call_path_id 142 child_item.branch_count = branch_count 143 child_item.time = time 144 child_item.data[0] = name 145 if dso == "[kernel.kallsyms]": 146 dso = "[kernel]" 147 child_item.data[1] = dso 148 child_item.data[2] = str(count) 149 child_item.data[3] = str(time) 150 child_item.data[4] = self.timePercent(time) 151 child_item.data[5] = str(branch_count) 152 child_item.data[6] = self.branchPercent(branch_count) 153 self.child_items.append(child_item) 154 self.child_count += 1 155 156 def selectCalls(self): 157 self.query_done = True; 158 query = QSqlQuery(self.db) 159 ret = query.exec_('SELECT id, call_path_id, branch_count, call_time, return_time, ' 160 '( SELECT name FROM symbols WHERE id = ( SELECT symbol_id FROM call_paths WHERE id = call_path_id ) ), ' 161 '( SELECT short_name FROM dsos WHERE id = ( SELECT dso_id FROM symbols WHERE id = ( SELECT symbol_id FROM call_paths WHERE id = call_path_id ) ) ), ' 162 '( SELECT ip FROM call_paths where id = call_path_id ) ' 163 'FROM calls WHERE parent_call_path_id = ' + str(self.call_path_id) + ' AND comm_id = ' + str(self.comm_id) + ' AND thread_id = ' + str(self.thread_id) + 164 ' ORDER BY call_path_id') 165 if not ret: 166 raise Exception("Query failed: " + query.lastError().text()) 167 last_call_path_id = 0 168 name = "" 169 dso = "" 170 count = 0 171 branch_count = 0 172 total_branch_count = 0 173 time = 0 174 total_time = 0 175 while query.next(): 176 if query.value(1) == last_call_path_id: 177 count += 1 178 branch_count += query.value(2) 179 time += query.value(4) - query.value(3) 180 else: 181 if count: 182 self.addChild(last_call_path_id, name, dso, count, time, branch_count) 183 last_call_path_id = query.value(1) 184 name = query.value(5) 185 dso = query.value(6) 186 count = 1 187 total_branch_count += branch_count 188 total_time += time 189 branch_count = query.value(2) 190 time = query.value(4) - query.value(3) 191 if count: 192 self.addChild(last_call_path_id, name, dso, count, time, branch_count) 193 total_branch_count += branch_count 194 total_time += time 195 # Top level does not have time or branch count, so fix that here 196 if total_branch_count > self.branch_count: 197 self.branch_count = total_branch_count 198 if self.branch_count: 199 for child_item in self.child_items: 200 child_item.data[6] = self.branchPercent(child_item.branch_count) 201 if total_time > self.time: 202 self.time = total_time 203 if self.time: 204 for child_item in self.child_items: 205 child_item.data[4] = self.timePercent(child_item.time) 206 207 def childCount(self): 208 if not self.query_done: 209 self.selectCalls() 210 return self.child_count 211 212 def columnCount(self): 213 return 7 214 215 def columnHeader(self, column): 216 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "] 217 return headers[column] 218 219 def getData(self, column): 220 return self.data[column] 221 222class TreeModel(QAbstractItemModel): 223 224 def __init__(self, db, parent=None): 225 super(TreeModel, self).__init__(parent) 226 self.db = db 227 self.root = TreeItem(db, 0, None) 228 229 def columnCount(self, parent): 230 return self.root.columnCount() 231 232 def rowCount(self, parent): 233 if parent.isValid(): 234 parent_item = parent.internalPointer() 235 else: 236 parent_item = self.root 237 return parent_item.childCount() 238 239 def headerData(self, section, orientation, role): 240 if role == Qt.TextAlignmentRole: 241 if section > 1: 242 return Qt.AlignRight 243 if role != Qt.DisplayRole: 244 return None 245 if orientation != Qt.Horizontal: 246 return None 247 return self.root.columnHeader(section) 248 249 def parent(self, child): 250 child_item = child.internalPointer() 251 if child_item is self.root: 252 return QModelIndex() 253 parent_item = child_item.getParentItem() 254 return self.createIndex(parent_item.getRow(), 0, parent_item) 255 256 def index(self, row, column, parent): 257 if parent.isValid(): 258 parent_item = parent.internalPointer() 259 else: 260 parent_item = self.root 261 child_item = parent_item.getChildItem(row) 262 return self.createIndex(row, column, child_item) 263 264 def data(self, index, role): 265 if role == Qt.TextAlignmentRole: 266 if index.column() > 1: 267 return Qt.AlignRight 268 if role != Qt.DisplayRole: 269 return None 270 index_item = index.internalPointer() 271 return index_item.getData(index.column()) 272 273class MainWindow(QMainWindow): 274 275 def __init__(self, db, dbname, parent=None): 276 super(MainWindow, self).__init__(parent) 277 278 self.setObjectName("MainWindow") 279 self.setWindowTitle("Call Graph: " + dbname) 280 self.move(100, 100) 281 self.resize(800, 600) 282 style = self.style() 283 icon = style.standardIcon(QStyle.SP_MessageBoxInformation) 284 self.setWindowIcon(icon); 285 286 self.model = TreeModel(db) 287 288 self.view = QTreeView() 289 self.view.setModel(self.model) 290 291 self.setCentralWidget(self.view) 292 293if __name__ == '__main__': 294 if (len(sys.argv) < 2): 295 print >> sys.stderr, "Usage is: call-graph-from-sql.py <database name>" 296 raise Exception("Too few arguments") 297 298 dbname = sys.argv[1] 299 300 is_sqlite3 = False 301 try: 302 f = open(dbname) 303 if f.read(15) == "SQLite format 3": 304 is_sqlite3 = True 305 f.close() 306 except: 307 pass 308 309 if is_sqlite3: 310 db = QSqlDatabase.addDatabase('QSQLITE') 311 else: 312 db = QSqlDatabase.addDatabase('QPSQL') 313 opts = dbname.split() 314 for opt in opts: 315 if '=' in opt: 316 opt = opt.split('=') 317 if opt[0] == 'hostname': 318 db.setHostName(opt[1]) 319 elif opt[0] == 'port': 320 db.setPort(int(opt[1])) 321 elif opt[0] == 'username': 322 db.setUserName(opt[1]) 323 elif opt[0] == 'password': 324 db.setPassword(opt[1]) 325 elif opt[0] == 'dbname': 326 dbname = opt[1] 327 else: 328 dbname = opt 329 330 db.setDatabaseName(dbname) 331 if not db.open(): 332 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text()) 333 334 app = QApplication(sys.argv) 335 window = MainWindow(db, dbname) 336 window.show() 337 err = app.exec_() 338 db.close() 339 sys.exit(err) 340