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