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