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
50import weakref
51import threading
52from PySide.QtCore import *
53from PySide.QtGui import *
54from PySide.QtSql import *
55from decimal import *
56
57# Data formatting helpers
58
59def dsoname(name):
60	if name == "[kernel.kallsyms]":
61		return "[kernel]"
62	return name
63
64# Percent to one decimal place
65
66def PercentToOneDP(n, d):
67	if not d:
68		return "0.0"
69	x = (n * Decimal(100)) / d
70	return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
71
72# Helper for queries that must not fail
73
74def QueryExec(query, stmt):
75	ret = query.exec_(stmt)
76	if not ret:
77		raise Exception("Query failed: " + query.lastError().text())
78
79# Tree data model
80
81class TreeModel(QAbstractItemModel):
82
83	def __init__(self, root, parent=None):
84		super(TreeModel, self).__init__(parent)
85		self.root = root
86		self.last_row_read = 0
87
88	def Item(self, parent):
89		if parent.isValid():
90			return parent.internalPointer()
91		else:
92			return self.root
93
94	def rowCount(self, parent):
95		result = self.Item(parent).childCount()
96		if result < 0:
97			result = 0
98			self.dataChanged.emit(parent, parent)
99		return result
100
101	def hasChildren(self, parent):
102		return self.Item(parent).hasChildren()
103
104	def headerData(self, section, orientation, role):
105		if role == Qt.TextAlignmentRole:
106			return self.columnAlignment(section)
107		if role != Qt.DisplayRole:
108			return None
109		if orientation != Qt.Horizontal:
110			return None
111		return self.columnHeader(section)
112
113	def parent(self, child):
114		child_item = child.internalPointer()
115		if child_item is self.root:
116			return QModelIndex()
117		parent_item = child_item.getParentItem()
118		return self.createIndex(parent_item.getRow(), 0, parent_item)
119
120	def index(self, row, column, parent):
121		child_item = self.Item(parent).getChildItem(row)
122		return self.createIndex(row, column, child_item)
123
124	def DisplayData(self, item, index):
125		return item.getData(index.column())
126
127	def columnAlignment(self, column):
128		return Qt.AlignLeft
129
130	def columnFont(self, column):
131		return None
132
133	def data(self, index, role):
134		if role == Qt.TextAlignmentRole:
135			return self.columnAlignment(index.column())
136		if role == Qt.FontRole:
137			return self.columnFont(index.column())
138		if role != Qt.DisplayRole:
139			return None
140		item = index.internalPointer()
141		return self.DisplayData(item, index)
142
143# Model cache
144
145model_cache = weakref.WeakValueDictionary()
146model_cache_lock = threading.Lock()
147
148def LookupCreateModel(model_name, create_fn):
149	model_cache_lock.acquire()
150	try:
151		model = model_cache[model_name]
152	except:
153		model = None
154	if model is None:
155		model = create_fn()
156		model_cache[model_name] = model
157	model_cache_lock.release()
158	return model
159
160# Context-sensitive call graph data model item base
161
162class CallGraphLevelItemBase(object):
163
164	def __init__(self, glb, row, parent_item):
165		self.glb = glb
166		self.row = row
167		self.parent_item = parent_item
168		self.query_done = False;
169		self.child_count = 0
170		self.child_items = []
171
172	def getChildItem(self, row):
173		return self.child_items[row]
174
175	def getParentItem(self):
176		return self.parent_item
177
178	def getRow(self):
179		return self.row
180
181	def childCount(self):
182		if not self.query_done:
183			self.Select()
184			if not self.child_count:
185				return -1
186		return self.child_count
187
188	def hasChildren(self):
189		if not self.query_done:
190			return True
191		return self.child_count > 0
192
193	def getData(self, column):
194		return self.data[column]
195
196# Context-sensitive call graph data model level 2+ item base
197
198class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
199
200	def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
201		super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
202		self.comm_id = comm_id
203		self.thread_id = thread_id
204		self.call_path_id = call_path_id
205		self.branch_count = branch_count
206		self.time = time
207
208	def Select(self):
209		self.query_done = True;
210		query = QSqlQuery(self.glb.db)
211		QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
212					" FROM calls"
213					" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
214					" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
215					" INNER JOIN dsos ON symbols.dso_id = dsos.id"
216					" WHERE parent_call_path_id = " + str(self.call_path_id) +
217					" AND comm_id = " + str(self.comm_id) +
218					" AND thread_id = " + str(self.thread_id) +
219					" GROUP BY call_path_id, name, short_name"
220					" ORDER BY call_path_id")
221		while query.next():
222			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)
223			self.child_items.append(child_item)
224			self.child_count += 1
225
226# Context-sensitive call graph data model level three item
227
228class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
229
230	def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
231		super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
232		dso = dsoname(dso)
233		self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
234		self.dbid = call_path_id
235
236# Context-sensitive call graph data model level two item
237
238class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
239
240	def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
241		super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
242		self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
243		self.dbid = thread_id
244
245	def Select(self):
246		super(CallGraphLevelTwoItem, self).Select()
247		for child_item in self.child_items:
248			self.time += child_item.time
249			self.branch_count += child_item.branch_count
250		for child_item in self.child_items:
251			child_item.data[4] = PercentToOneDP(child_item.time, self.time)
252			child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
253
254# Context-sensitive call graph data model level one item
255
256class CallGraphLevelOneItem(CallGraphLevelItemBase):
257
258	def __init__(self, glb, row, comm_id, comm, parent_item):
259		super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
260		self.data = [comm, "", "", "", "", "", ""]
261		self.dbid = comm_id
262
263	def Select(self):
264		self.query_done = True;
265		query = QSqlQuery(self.glb.db)
266		QueryExec(query, "SELECT thread_id, pid, tid"
267					" FROM comm_threads"
268					" INNER JOIN threads ON thread_id = threads.id"
269					" WHERE comm_id = " + str(self.dbid))
270		while query.next():
271			child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
272			self.child_items.append(child_item)
273			self.child_count += 1
274
275# Context-sensitive call graph data model root item
276
277class CallGraphRootItem(CallGraphLevelItemBase):
278
279	def __init__(self, glb):
280		super(CallGraphRootItem, self).__init__(glb, 0, None)
281		self.dbid = 0
282		self.query_done = True;
283		query = QSqlQuery(glb.db)
284		QueryExec(query, "SELECT id, comm FROM comms")
285		while query.next():
286			if not query.value(0):
287				continue
288			child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
289			self.child_items.append(child_item)
290			self.child_count += 1
291
292# Context-sensitive call graph data model
293
294class CallGraphModel(TreeModel):
295
296	def __init__(self, glb, parent=None):
297		super(CallGraphModel, self).__init__(CallGraphRootItem(glb), parent)
298		self.glb = glb
299
300	def columnCount(self, parent=None):
301		return 7
302
303	def columnHeader(self, column):
304		headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
305		return headers[column]
306
307	def columnAlignment(self, column):
308		alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
309		return alignment[column]
310
311# Context-sensitive call graph window
312
313class CallGraphWindow(QMdiSubWindow):
314
315	def __init__(self, glb, parent=None):
316		super(CallGraphWindow, self).__init__(parent)
317
318		self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
319
320		self.view = QTreeView()
321		self.view.setModel(self.model)
322
323		for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
324			self.view.setColumnWidth(c, w)
325
326		self.setWidget(self.view)
327
328		AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
329
330# Action Definition
331
332def CreateAction(label, tip, callback, parent=None, shortcut=None):
333	action = QAction(label, parent)
334	if shortcut != None:
335		action.setShortcuts(shortcut)
336	action.setStatusTip(tip)
337	action.triggered.connect(callback)
338	return action
339
340# Typical application actions
341
342def CreateExitAction(app, parent=None):
343	return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
344
345# Typical MDI actions
346
347def CreateCloseActiveWindowAction(mdi_area):
348	return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
349
350def CreateCloseAllWindowsAction(mdi_area):
351	return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
352
353def CreateTileWindowsAction(mdi_area):
354	return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
355
356def CreateCascadeWindowsAction(mdi_area):
357	return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
358
359def CreateNextWindowAction(mdi_area):
360	return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
361
362def CreatePreviousWindowAction(mdi_area):
363	return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
364
365# Typical MDI window menu
366
367class WindowMenu():
368
369	def __init__(self, mdi_area, menu):
370		self.mdi_area = mdi_area
371		self.window_menu = menu.addMenu("&Windows")
372		self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
373		self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
374		self.tile_windows = CreateTileWindowsAction(mdi_area)
375		self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
376		self.next_window = CreateNextWindowAction(mdi_area)
377		self.previous_window = CreatePreviousWindowAction(mdi_area)
378		self.window_menu.aboutToShow.connect(self.Update)
379
380	def Update(self):
381		self.window_menu.clear()
382		sub_window_count = len(self.mdi_area.subWindowList())
383		have_sub_windows = sub_window_count != 0
384		self.close_active_window.setEnabled(have_sub_windows)
385		self.close_all_windows.setEnabled(have_sub_windows)
386		self.tile_windows.setEnabled(have_sub_windows)
387		self.cascade_windows.setEnabled(have_sub_windows)
388		self.next_window.setEnabled(have_sub_windows)
389		self.previous_window.setEnabled(have_sub_windows)
390		self.window_menu.addAction(self.close_active_window)
391		self.window_menu.addAction(self.close_all_windows)
392		self.window_menu.addSeparator()
393		self.window_menu.addAction(self.tile_windows)
394		self.window_menu.addAction(self.cascade_windows)
395		self.window_menu.addSeparator()
396		self.window_menu.addAction(self.next_window)
397		self.window_menu.addAction(self.previous_window)
398		if sub_window_count == 0:
399			return
400		self.window_menu.addSeparator()
401		nr = 1
402		for sub_window in self.mdi_area.subWindowList():
403			label = str(nr) + " " + sub_window.name
404			if nr < 10:
405				label = "&" + label
406			action = self.window_menu.addAction(label)
407			action.setCheckable(True)
408			action.setChecked(sub_window == self.mdi_area.activeSubWindow())
409			action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
410			self.window_menu.addAction(action)
411			nr += 1
412
413	def setActiveSubWindow(self, nr):
414		self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
415
416# Unique name for sub-windows
417
418def NumberedWindowName(name, nr):
419	if nr > 1:
420		name += " <" + str(nr) + ">"
421	return name
422
423def UniqueSubWindowName(mdi_area, name):
424	nr = 1
425	while True:
426		unique_name = NumberedWindowName(name, nr)
427		ok = True
428		for sub_window in mdi_area.subWindowList():
429			if sub_window.name == unique_name:
430				ok = False
431				break
432		if ok:
433			return unique_name
434		nr += 1
435
436# Add a sub-window
437
438def AddSubWindow(mdi_area, sub_window, name):
439	unique_name = UniqueSubWindowName(mdi_area, name)
440	sub_window.setMinimumSize(200, 100)
441	sub_window.resize(800, 600)
442	sub_window.setWindowTitle(unique_name)
443	sub_window.setAttribute(Qt.WA_DeleteOnClose)
444	sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
445	sub_window.name = unique_name
446	mdi_area.addSubWindow(sub_window)
447	sub_window.show()
448
449# Main window
450
451class MainWindow(QMainWindow):
452
453	def __init__(self, glb, parent=None):
454		super(MainWindow, self).__init__(parent)
455
456		self.glb = glb
457
458		self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
459		self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
460		self.setMinimumSize(200, 100)
461
462		self.mdi_area = QMdiArea()
463		self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
464		self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
465
466		self.setCentralWidget(self.mdi_area)
467
468		menu = self.menuBar()
469
470		file_menu = menu.addMenu("&File")
471		file_menu.addAction(CreateExitAction(glb.app, self))
472
473		reports_menu = menu.addMenu("&Reports")
474		reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
475
476		self.window_menu = WindowMenu(self.mdi_area, menu)
477
478	def NewCallGraph(self):
479		CallGraphWindow(self.glb, self)
480
481# Global data
482
483class Glb():
484
485	def __init__(self, dbref, db, dbname):
486		self.dbref = dbref
487		self.db = db
488		self.dbname = dbname
489		self.app = None
490		self.mainwindow = None
491
492# Database reference
493
494class DBRef():
495
496	def __init__(self, is_sqlite3, dbname):
497		self.is_sqlite3 = is_sqlite3
498		self.dbname = dbname
499
500	def Open(self, connection_name):
501		dbname = self.dbname
502		if self.is_sqlite3:
503			db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
504		else:
505			db = QSqlDatabase.addDatabase("QPSQL", connection_name)
506			opts = dbname.split()
507			for opt in opts:
508				if "=" in opt:
509					opt = opt.split("=")
510					if opt[0] == "hostname":
511						db.setHostName(opt[1])
512					elif opt[0] == "port":
513						db.setPort(int(opt[1]))
514					elif opt[0] == "username":
515						db.setUserName(opt[1])
516					elif opt[0] == "password":
517						db.setPassword(opt[1])
518					elif opt[0] == "dbname":
519						dbname = opt[1]
520				else:
521					dbname = opt
522
523		db.setDatabaseName(dbname)
524		if not db.open():
525			raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
526		return db, dbname
527
528# Main
529
530def Main():
531	if (len(sys.argv) < 2):
532		print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>"
533		raise Exception("Too few arguments")
534
535	dbname = sys.argv[1]
536
537	is_sqlite3 = False
538	try:
539		f = open(dbname)
540		if f.read(15) == "SQLite format 3":
541			is_sqlite3 = True
542		f.close()
543	except:
544		pass
545
546	dbref = DBRef(is_sqlite3, dbname)
547	db, dbname = dbref.Open("main")
548	glb = Glb(dbref, db, dbname)
549	app = QApplication(sys.argv)
550	glb.app = app
551	mainwindow = MainWindow(glb)
552	glb.mainwindow = mainwindow
553	mainwindow.show()
554	err = app.exec_()
555	db.close()
556	sys.exit(err)
557
558if __name__ == "__main__":
559	Main()
560