call-graph-from-sql.py 11 KB

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