exported-sql-viewer.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  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. # To use this script you will need to have exported data using either the
  6. # export-to-sqlite.py or the export-to-postgresql.py script. Refer to those
  7. # scripts for details.
  8. #
  9. # Following on from the example in the export scripts, a
  10. # call-graph can be displayed for the pt_example database like this:
  11. #
  12. # python tools/perf/scripts/python/exported-sql-viewer.py pt_example
  13. #
  14. # Note that for PostgreSQL, this script supports connecting to remote databases
  15. # by setting hostname, port, username, password, and dbname e.g.
  16. #
  17. # python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
  18. #
  19. # The result is a GUI window with a tree representing a context-sensitive
  20. # call-graph. Expanding a couple of levels of the tree and adjusting column
  21. # widths to suit will display something like:
  22. #
  23. # Call Graph: pt_example
  24. # Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
  25. # v- ls
  26. # v- 2638:2638
  27. # v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
  28. # |- unknown unknown 1 13198 0.1 1 0.0
  29. # >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
  30. # >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
  31. # v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
  32. # >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
  33. # >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
  34. # >- __libc_csu_init ls 1 10354 0.1 10 0.0
  35. # |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
  36. # v- main ls 1 8182043 99.6 180254 99.9
  37. #
  38. # Points to note:
  39. # The top level is a command name (comm)
  40. # The next level is a thread (pid:tid)
  41. # Subsequent levels are functions
  42. # 'Count' is the number of calls
  43. # 'Time' is the elapsed time until the function returns
  44. # Percentages are relative to the level above
  45. # 'Branch Count' is the total number of branches for that function and all
  46. # functions that it calls
  47. import sys
  48. import weakref
  49. import threading
  50. from PySide.QtCore import *
  51. from PySide.QtGui import *
  52. from PySide.QtSql import *
  53. from decimal import *
  54. # Data formatting helpers
  55. def dsoname(name):
  56. if name == "[kernel.kallsyms]":
  57. return "[kernel]"
  58. return name
  59. # Percent to one decimal place
  60. def PercentToOneDP(n, d):
  61. if not d:
  62. return "0.0"
  63. x = (n * Decimal(100)) / d
  64. return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
  65. # Helper for queries that must not fail
  66. def QueryExec(query, stmt):
  67. ret = query.exec_(stmt)
  68. if not ret:
  69. raise Exception("Query failed: " + query.lastError().text())
  70. # Tree data model
  71. class TreeModel(QAbstractItemModel):
  72. def __init__(self, root, parent=None):
  73. super(TreeModel, self).__init__(parent)
  74. self.root = root
  75. self.last_row_read = 0
  76. def Item(self, parent):
  77. if parent.isValid():
  78. return parent.internalPointer()
  79. else:
  80. return self.root
  81. def rowCount(self, parent):
  82. result = self.Item(parent).childCount()
  83. if result < 0:
  84. result = 0
  85. self.dataChanged.emit(parent, parent)
  86. return result
  87. def hasChildren(self, parent):
  88. return self.Item(parent).hasChildren()
  89. def headerData(self, section, orientation, role):
  90. if role == Qt.TextAlignmentRole:
  91. return self.columnAlignment(section)
  92. if role != Qt.DisplayRole:
  93. return None
  94. if orientation != Qt.Horizontal:
  95. return None
  96. return self.columnHeader(section)
  97. def parent(self, child):
  98. child_item = child.internalPointer()
  99. if child_item is self.root:
  100. return QModelIndex()
  101. parent_item = child_item.getParentItem()
  102. return self.createIndex(parent_item.getRow(), 0, parent_item)
  103. def index(self, row, column, parent):
  104. child_item = self.Item(parent).getChildItem(row)
  105. return self.createIndex(row, column, child_item)
  106. def DisplayData(self, item, index):
  107. return item.getData(index.column())
  108. def columnAlignment(self, column):
  109. return Qt.AlignLeft
  110. def columnFont(self, column):
  111. return None
  112. def data(self, index, role):
  113. if role == Qt.TextAlignmentRole:
  114. return self.columnAlignment(index.column())
  115. if role == Qt.FontRole:
  116. return self.columnFont(index.column())
  117. if role != Qt.DisplayRole:
  118. return None
  119. item = index.internalPointer()
  120. return self.DisplayData(item, index)
  121. # Model cache
  122. model_cache = weakref.WeakValueDictionary()
  123. model_cache_lock = threading.Lock()
  124. def LookupCreateModel(model_name, create_fn):
  125. model_cache_lock.acquire()
  126. try:
  127. model = model_cache[model_name]
  128. except:
  129. model = None
  130. if model is None:
  131. model = create_fn()
  132. model_cache[model_name] = model
  133. model_cache_lock.release()
  134. return model
  135. # Context-sensitive call graph data model item base
  136. class CallGraphLevelItemBase(object):
  137. def __init__(self, glb, row, parent_item):
  138. self.glb = glb
  139. self.row = row
  140. self.parent_item = parent_item
  141. self.query_done = False;
  142. self.child_count = 0
  143. self.child_items = []
  144. def getChildItem(self, row):
  145. return self.child_items[row]
  146. def getParentItem(self):
  147. return self.parent_item
  148. def getRow(self):
  149. return self.row
  150. def childCount(self):
  151. if not self.query_done:
  152. self.Select()
  153. if not self.child_count:
  154. return -1
  155. return self.child_count
  156. def hasChildren(self):
  157. if not self.query_done:
  158. return True
  159. return self.child_count > 0
  160. def getData(self, column):
  161. return self.data[column]
  162. # Context-sensitive call graph data model level 2+ item base
  163. class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
  164. def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
  165. super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
  166. self.comm_id = comm_id
  167. self.thread_id = thread_id
  168. self.call_path_id = call_path_id
  169. self.branch_count = branch_count
  170. self.time = time
  171. def Select(self):
  172. self.query_done = True;
  173. query = QSqlQuery(self.glb.db)
  174. QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
  175. " FROM calls"
  176. " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
  177. " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
  178. " INNER JOIN dsos ON symbols.dso_id = dsos.id"
  179. " WHERE parent_call_path_id = " + str(self.call_path_id) +
  180. " AND comm_id = " + str(self.comm_id) +
  181. " AND thread_id = " + str(self.thread_id) +
  182. " GROUP BY call_path_id, name, short_name"
  183. " ORDER BY call_path_id")
  184. while query.next():
  185. 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)
  186. self.child_items.append(child_item)
  187. self.child_count += 1
  188. # Context-sensitive call graph data model level three item
  189. class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
  190. def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
  191. super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
  192. dso = dsoname(dso)
  193. self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
  194. self.dbid = call_path_id
  195. # Context-sensitive call graph data model level two item
  196. class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
  197. def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
  198. super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
  199. self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
  200. self.dbid = thread_id
  201. def Select(self):
  202. super(CallGraphLevelTwoItem, self).Select()
  203. for child_item in self.child_items:
  204. self.time += child_item.time
  205. self.branch_count += child_item.branch_count
  206. for child_item in self.child_items:
  207. child_item.data[4] = PercentToOneDP(child_item.time, self.time)
  208. child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
  209. # Context-sensitive call graph data model level one item
  210. class CallGraphLevelOneItem(CallGraphLevelItemBase):
  211. def __init__(self, glb, row, comm_id, comm, parent_item):
  212. super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
  213. self.data = [comm, "", "", "", "", "", ""]
  214. self.dbid = comm_id
  215. def Select(self):
  216. self.query_done = True;
  217. query = QSqlQuery(self.glb.db)
  218. QueryExec(query, "SELECT thread_id, pid, tid"
  219. " FROM comm_threads"
  220. " INNER JOIN threads ON thread_id = threads.id"
  221. " WHERE comm_id = " + str(self.dbid))
  222. while query.next():
  223. child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
  224. self.child_items.append(child_item)
  225. self.child_count += 1
  226. # Context-sensitive call graph data model root item
  227. class CallGraphRootItem(CallGraphLevelItemBase):
  228. def __init__(self, glb):
  229. super(CallGraphRootItem, self).__init__(glb, 0, None)
  230. self.dbid = 0
  231. self.query_done = True;
  232. query = QSqlQuery(glb.db)
  233. QueryExec(query, "SELECT id, comm FROM comms")
  234. while query.next():
  235. if not query.value(0):
  236. continue
  237. child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
  238. self.child_items.append(child_item)
  239. self.child_count += 1
  240. # Context-sensitive call graph data model
  241. class CallGraphModel(TreeModel):
  242. def __init__(self, glb, parent=None):
  243. super(CallGraphModel, self).__init__(CallGraphRootItem(glb), parent)
  244. self.glb = glb
  245. def columnCount(self, parent=None):
  246. return 7
  247. def columnHeader(self, column):
  248. headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
  249. return headers[column]
  250. def columnAlignment(self, column):
  251. alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
  252. return alignment[column]
  253. # Context-sensitive call graph window
  254. class CallGraphWindow(QMdiSubWindow):
  255. def __init__(self, glb, parent=None):
  256. super(CallGraphWindow, self).__init__(parent)
  257. self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
  258. self.view = QTreeView()
  259. self.view.setModel(self.model)
  260. for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
  261. self.view.setColumnWidth(c, w)
  262. self.setWidget(self.view)
  263. AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
  264. # Action Definition
  265. def CreateAction(label, tip, callback, parent=None, shortcut=None):
  266. action = QAction(label, parent)
  267. if shortcut != None:
  268. action.setShortcuts(shortcut)
  269. action.setStatusTip(tip)
  270. action.triggered.connect(callback)
  271. return action
  272. # Typical application actions
  273. def CreateExitAction(app, parent=None):
  274. return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
  275. # Typical MDI actions
  276. def CreateCloseActiveWindowAction(mdi_area):
  277. return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
  278. def CreateCloseAllWindowsAction(mdi_area):
  279. return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
  280. def CreateTileWindowsAction(mdi_area):
  281. return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
  282. def CreateCascadeWindowsAction(mdi_area):
  283. return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
  284. def CreateNextWindowAction(mdi_area):
  285. return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
  286. def CreatePreviousWindowAction(mdi_area):
  287. return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
  288. # Typical MDI window menu
  289. class WindowMenu():
  290. def __init__(self, mdi_area, menu):
  291. self.mdi_area = mdi_area
  292. self.window_menu = menu.addMenu("&Windows")
  293. self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
  294. self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
  295. self.tile_windows = CreateTileWindowsAction(mdi_area)
  296. self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
  297. self.next_window = CreateNextWindowAction(mdi_area)
  298. self.previous_window = CreatePreviousWindowAction(mdi_area)
  299. self.window_menu.aboutToShow.connect(self.Update)
  300. def Update(self):
  301. self.window_menu.clear()
  302. sub_window_count = len(self.mdi_area.subWindowList())
  303. have_sub_windows = sub_window_count != 0
  304. self.close_active_window.setEnabled(have_sub_windows)
  305. self.close_all_windows.setEnabled(have_sub_windows)
  306. self.tile_windows.setEnabled(have_sub_windows)
  307. self.cascade_windows.setEnabled(have_sub_windows)
  308. self.next_window.setEnabled(have_sub_windows)
  309. self.previous_window.setEnabled(have_sub_windows)
  310. self.window_menu.addAction(self.close_active_window)
  311. self.window_menu.addAction(self.close_all_windows)
  312. self.window_menu.addSeparator()
  313. self.window_menu.addAction(self.tile_windows)
  314. self.window_menu.addAction(self.cascade_windows)
  315. self.window_menu.addSeparator()
  316. self.window_menu.addAction(self.next_window)
  317. self.window_menu.addAction(self.previous_window)
  318. if sub_window_count == 0:
  319. return
  320. self.window_menu.addSeparator()
  321. nr = 1
  322. for sub_window in self.mdi_area.subWindowList():
  323. label = str(nr) + " " + sub_window.name
  324. if nr < 10:
  325. label = "&" + label
  326. action = self.window_menu.addAction(label)
  327. action.setCheckable(True)
  328. action.setChecked(sub_window == self.mdi_area.activeSubWindow())
  329. action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
  330. self.window_menu.addAction(action)
  331. nr += 1
  332. def setActiveSubWindow(self, nr):
  333. self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
  334. # Unique name for sub-windows
  335. def NumberedWindowName(name, nr):
  336. if nr > 1:
  337. name += " <" + str(nr) + ">"
  338. return name
  339. def UniqueSubWindowName(mdi_area, name):
  340. nr = 1
  341. while True:
  342. unique_name = NumberedWindowName(name, nr)
  343. ok = True
  344. for sub_window in mdi_area.subWindowList():
  345. if sub_window.name == unique_name:
  346. ok = False
  347. break
  348. if ok:
  349. return unique_name
  350. nr += 1
  351. # Add a sub-window
  352. def AddSubWindow(mdi_area, sub_window, name):
  353. unique_name = UniqueSubWindowName(mdi_area, name)
  354. sub_window.setMinimumSize(200, 100)
  355. sub_window.resize(800, 600)
  356. sub_window.setWindowTitle(unique_name)
  357. sub_window.setAttribute(Qt.WA_DeleteOnClose)
  358. sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
  359. sub_window.name = unique_name
  360. mdi_area.addSubWindow(sub_window)
  361. sub_window.show()
  362. # Main window
  363. class MainWindow(QMainWindow):
  364. def __init__(self, glb, parent=None):
  365. super(MainWindow, self).__init__(parent)
  366. self.glb = glb
  367. self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
  368. self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
  369. self.setMinimumSize(200, 100)
  370. self.mdi_area = QMdiArea()
  371. self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
  372. self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
  373. self.setCentralWidget(self.mdi_area)
  374. menu = self.menuBar()
  375. file_menu = menu.addMenu("&File")
  376. file_menu.addAction(CreateExitAction(glb.app, self))
  377. reports_menu = menu.addMenu("&Reports")
  378. reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
  379. self.window_menu = WindowMenu(self.mdi_area, menu)
  380. def NewCallGraph(self):
  381. CallGraphWindow(self.glb, self)
  382. # Global data
  383. class Glb():
  384. def __init__(self, dbref, db, dbname):
  385. self.dbref = dbref
  386. self.db = db
  387. self.dbname = dbname
  388. self.app = None
  389. self.mainwindow = None
  390. # Database reference
  391. class DBRef():
  392. def __init__(self, is_sqlite3, dbname):
  393. self.is_sqlite3 = is_sqlite3
  394. self.dbname = dbname
  395. def Open(self, connection_name):
  396. dbname = self.dbname
  397. if self.is_sqlite3:
  398. db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
  399. else:
  400. db = QSqlDatabase.addDatabase("QPSQL", connection_name)
  401. opts = dbname.split()
  402. for opt in opts:
  403. if "=" in opt:
  404. opt = opt.split("=")
  405. if opt[0] == "hostname":
  406. db.setHostName(opt[1])
  407. elif opt[0] == "port":
  408. db.setPort(int(opt[1]))
  409. elif opt[0] == "username":
  410. db.setUserName(opt[1])
  411. elif opt[0] == "password":
  412. db.setPassword(opt[1])
  413. elif opt[0] == "dbname":
  414. dbname = opt[1]
  415. else:
  416. dbname = opt
  417. db.setDatabaseName(dbname)
  418. if not db.open():
  419. raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
  420. return db, dbname
  421. # Main
  422. def Main():
  423. if (len(sys.argv) < 2):
  424. print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>"
  425. raise Exception("Too few arguments")
  426. dbname = sys.argv[1]
  427. is_sqlite3 = False
  428. try:
  429. f = open(dbname)
  430. if f.read(15) == "SQLite format 3":
  431. is_sqlite3 = True
  432. f.close()
  433. except:
  434. pass
  435. dbref = DBRef(is_sqlite3, dbname)
  436. db, dbname = dbref.Open("main")
  437. glb = Glb(dbref, db, dbname)
  438. app = QApplication(sys.argv)
  439. glb.app = app
  440. mainwindow = MainWindow(glb)
  441. glb.mainwindow = mainwindow
  442. mainwindow.show()
  443. err = app.exec_()
  444. db.close()
  445. sys.exit(err)
  446. if __name__ == "__main__":
  447. Main()