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
52import string
53from PySide.QtCore import *
54from PySide.QtGui import *
55from PySide.QtSql import *
56from decimal import *
57
58# Data formatting helpers
59
60def dsoname(name):
61	if name == "[kernel.kallsyms]":
62		return "[kernel]"
63	return name
64
65# Percent to one decimal place
66
67def PercentToOneDP(n, d):
68	if not d:
69		return "0.0"
70	x = (n * Decimal(100)) / d
71	return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
72
73# Helper for queries that must not fail
74
75def QueryExec(query, stmt):
76	ret = query.exec_(stmt)
77	if not ret:
78		raise Exception("Query failed: " + query.lastError().text())
79
80# Background thread
81
82class Thread(QThread):
83
84	done = Signal(object)
85
86	def __init__(self, task, param=None, parent=None):
87		super(Thread, self).__init__(parent)
88		self.task = task
89		self.param = param
90
91	def run(self):
92		while True:
93			if self.param is None:
94				done, result = self.task()
95			else:
96				done, result = self.task(self.param)
97			self.done.emit(result)
98			if done:
99				break
100
101# Tree data model
102
103class TreeModel(QAbstractItemModel):
104
105	def __init__(self, root, parent=None):
106		super(TreeModel, self).__init__(parent)
107		self.root = root
108		self.last_row_read = 0
109
110	def Item(self, parent):
111		if parent.isValid():
112			return parent.internalPointer()
113		else:
114			return self.root
115
116	def rowCount(self, parent):
117		result = self.Item(parent).childCount()
118		if result < 0:
119			result = 0
120			self.dataChanged.emit(parent, parent)
121		return result
122
123	def hasChildren(self, parent):
124		return self.Item(parent).hasChildren()
125
126	def headerData(self, section, orientation, role):
127		if role == Qt.TextAlignmentRole:
128			return self.columnAlignment(section)
129		if role != Qt.DisplayRole:
130			return None
131		if orientation != Qt.Horizontal:
132			return None
133		return self.columnHeader(section)
134
135	def parent(self, child):
136		child_item = child.internalPointer()
137		if child_item is self.root:
138			return QModelIndex()
139		parent_item = child_item.getParentItem()
140		return self.createIndex(parent_item.getRow(), 0, parent_item)
141
142	def index(self, row, column, parent):
143		child_item = self.Item(parent).getChildItem(row)
144		return self.createIndex(row, column, child_item)
145
146	def DisplayData(self, item, index):
147		return item.getData(index.column())
148
149	def columnAlignment(self, column):
150		return Qt.AlignLeft
151
152	def columnFont(self, column):
153		return None
154
155	def data(self, index, role):
156		if role == Qt.TextAlignmentRole:
157			return self.columnAlignment(index.column())
158		if role == Qt.FontRole:
159			return self.columnFont(index.column())
160		if role != Qt.DisplayRole:
161			return None
162		item = index.internalPointer()
163		return self.DisplayData(item, index)
164
165# Model cache
166
167model_cache = weakref.WeakValueDictionary()
168model_cache_lock = threading.Lock()
169
170def LookupCreateModel(model_name, create_fn):
171	model_cache_lock.acquire()
172	try:
173		model = model_cache[model_name]
174	except:
175		model = None
176	if model is None:
177		model = create_fn()
178		model_cache[model_name] = model
179	model_cache_lock.release()
180	return model
181
182# Find bar
183
184class FindBar():
185
186	def __init__(self, parent, finder, is_reg_expr=False):
187		self.finder = finder
188		self.context = []
189		self.last_value = None
190		self.last_pattern = None
191
192		label = QLabel("Find:")
193		label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
194
195		self.textbox = QComboBox()
196		self.textbox.setEditable(True)
197		self.textbox.currentIndexChanged.connect(self.ValueChanged)
198
199		self.progress = QProgressBar()
200		self.progress.setRange(0, 0)
201		self.progress.hide()
202
203		if is_reg_expr:
204			self.pattern = QCheckBox("Regular Expression")
205		else:
206			self.pattern = QCheckBox("Pattern")
207		self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
208
209		self.next_button = QToolButton()
210		self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
211		self.next_button.released.connect(lambda: self.NextPrev(1))
212
213		self.prev_button = QToolButton()
214		self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
215		self.prev_button.released.connect(lambda: self.NextPrev(-1))
216
217		self.close_button = QToolButton()
218		self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
219		self.close_button.released.connect(self.Deactivate)
220
221		self.hbox = QHBoxLayout()
222		self.hbox.setContentsMargins(0, 0, 0, 0)
223
224		self.hbox.addWidget(label)
225		self.hbox.addWidget(self.textbox)
226		self.hbox.addWidget(self.progress)
227		self.hbox.addWidget(self.pattern)
228		self.hbox.addWidget(self.next_button)
229		self.hbox.addWidget(self.prev_button)
230		self.hbox.addWidget(self.close_button)
231
232		self.bar = QWidget()
233		self.bar.setLayout(self.hbox);
234		self.bar.hide()
235
236	def Widget(self):
237		return self.bar
238
239	def Activate(self):
240		self.bar.show()
241		self.textbox.setFocus()
242
243	def Deactivate(self):
244		self.bar.hide()
245
246	def Busy(self):
247		self.textbox.setEnabled(False)
248		self.pattern.hide()
249		self.next_button.hide()
250		self.prev_button.hide()
251		self.progress.show()
252
253	def Idle(self):
254		self.textbox.setEnabled(True)
255		self.progress.hide()
256		self.pattern.show()
257		self.next_button.show()
258		self.prev_button.show()
259
260	def Find(self, direction):
261		value = self.textbox.currentText()
262		pattern = self.pattern.isChecked()
263		self.last_value = value
264		self.last_pattern = pattern
265		self.finder.Find(value, direction, pattern, self.context)
266
267	def ValueChanged(self):
268		value = self.textbox.currentText()
269		pattern = self.pattern.isChecked()
270		index = self.textbox.currentIndex()
271		data = self.textbox.itemData(index)
272		# Store the pattern in the combo box to keep it with the text value
273		if data == None:
274			self.textbox.setItemData(index, pattern)
275		else:
276			self.pattern.setChecked(data)
277		self.Find(0)
278
279	def NextPrev(self, direction):
280		value = self.textbox.currentText()
281		pattern = self.pattern.isChecked()
282		if value != self.last_value:
283			index = self.textbox.findText(value)
284			# Allow for a button press before the value has been added to the combo box
285			if index < 0:
286				index = self.textbox.count()
287				self.textbox.addItem(value, pattern)
288				self.textbox.setCurrentIndex(index)
289				return
290			else:
291				self.textbox.setItemData(index, pattern)
292		elif pattern != self.last_pattern:
293			# Keep the pattern recorded in the combo box up to date
294			index = self.textbox.currentIndex()
295			self.textbox.setItemData(index, pattern)
296		self.Find(direction)
297
298	def NotFound(self):
299		QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
300
301# Context-sensitive call graph data model item base
302
303class CallGraphLevelItemBase(object):
304
305	def __init__(self, glb, row, parent_item):
306		self.glb = glb
307		self.row = row
308		self.parent_item = parent_item
309		self.query_done = False;
310		self.child_count = 0
311		self.child_items = []
312
313	def getChildItem(self, row):
314		return self.child_items[row]
315
316	def getParentItem(self):
317		return self.parent_item
318
319	def getRow(self):
320		return self.row
321
322	def childCount(self):
323		if not self.query_done:
324			self.Select()
325			if not self.child_count:
326				return -1
327		return self.child_count
328
329	def hasChildren(self):
330		if not self.query_done:
331			return True
332		return self.child_count > 0
333
334	def getData(self, column):
335		return self.data[column]
336
337# Context-sensitive call graph data model level 2+ item base
338
339class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
340
341	def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
342		super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
343		self.comm_id = comm_id
344		self.thread_id = thread_id
345		self.call_path_id = call_path_id
346		self.branch_count = branch_count
347		self.time = time
348
349	def Select(self):
350		self.query_done = True;
351		query = QSqlQuery(self.glb.db)
352		QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
353					" FROM calls"
354					" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
355					" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
356					" INNER JOIN dsos ON symbols.dso_id = dsos.id"
357					" WHERE parent_call_path_id = " + str(self.call_path_id) +
358					" AND comm_id = " + str(self.comm_id) +
359					" AND thread_id = " + str(self.thread_id) +
360					" GROUP BY call_path_id, name, short_name"
361					" ORDER BY call_path_id")
362		while query.next():
363			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)
364			self.child_items.append(child_item)
365			self.child_count += 1
366
367# Context-sensitive call graph data model level three item
368
369class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
370
371	def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
372		super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
373		dso = dsoname(dso)
374		self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
375		self.dbid = call_path_id
376
377# Context-sensitive call graph data model level two item
378
379class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
380
381	def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
382		super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
383		self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
384		self.dbid = thread_id
385
386	def Select(self):
387		super(CallGraphLevelTwoItem, self).Select()
388		for child_item in self.child_items:
389			self.time += child_item.time
390			self.branch_count += child_item.branch_count
391		for child_item in self.child_items:
392			child_item.data[4] = PercentToOneDP(child_item.time, self.time)
393			child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
394
395# Context-sensitive call graph data model level one item
396
397class CallGraphLevelOneItem(CallGraphLevelItemBase):
398
399	def __init__(self, glb, row, comm_id, comm, parent_item):
400		super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
401		self.data = [comm, "", "", "", "", "", ""]
402		self.dbid = comm_id
403
404	def Select(self):
405		self.query_done = True;
406		query = QSqlQuery(self.glb.db)
407		QueryExec(query, "SELECT thread_id, pid, tid"
408					" FROM comm_threads"
409					" INNER JOIN threads ON thread_id = threads.id"
410					" WHERE comm_id = " + str(self.dbid))
411		while query.next():
412			child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
413			self.child_items.append(child_item)
414			self.child_count += 1
415
416# Context-sensitive call graph data model root item
417
418class CallGraphRootItem(CallGraphLevelItemBase):
419
420	def __init__(self, glb):
421		super(CallGraphRootItem, self).__init__(glb, 0, None)
422		self.dbid = 0
423		self.query_done = True;
424		query = QSqlQuery(glb.db)
425		QueryExec(query, "SELECT id, comm FROM comms")
426		while query.next():
427			if not query.value(0):
428				continue
429			child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
430			self.child_items.append(child_item)
431			self.child_count += 1
432
433# Context-sensitive call graph data model
434
435class CallGraphModel(TreeModel):
436
437	def __init__(self, glb, parent=None):
438		super(CallGraphModel, self).__init__(CallGraphRootItem(glb), parent)
439		self.glb = glb
440
441	def columnCount(self, parent=None):
442		return 7
443
444	def columnHeader(self, column):
445		headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
446		return headers[column]
447
448	def columnAlignment(self, column):
449		alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
450		return alignment[column]
451
452	def FindSelect(self, value, pattern, query):
453		if pattern:
454			# postgresql and sqlite pattern patching differences:
455			#   postgresql LIKE is case sensitive but sqlite LIKE is not
456			#   postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
457			#   postgresql supports ILIKE which is case insensitive
458			#   sqlite supports GLOB (text only) which uses * and ? and is case sensitive
459			if not self.glb.dbref.is_sqlite3:
460				# Escape % and _
461				s = value.replace("%", "\%")
462				s = s.replace("_", "\_")
463				# Translate * and ? into SQL LIKE pattern characters % and _
464				trans = string.maketrans("*?", "%_")
465				match = " LIKE '" + str(s).translate(trans) + "'"
466			else:
467				match = " GLOB '" + str(value) + "'"
468		else:
469			match = " = '" + str(value) + "'"
470		QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
471						" FROM calls"
472						" INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
473						" INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
474						" WHERE symbols.name" + match +
475						" GROUP BY comm_id, thread_id, call_path_id"
476						" ORDER BY comm_id, thread_id, call_path_id")
477
478	def FindPath(self, query):
479		# Turn the query result into a list of ids that the tree view can walk
480		# to open the tree at the right place.
481		ids = []
482		parent_id = query.value(0)
483		while parent_id:
484			ids.insert(0, parent_id)
485			q2 = QSqlQuery(self.glb.db)
486			QueryExec(q2, "SELECT parent_id"
487					" FROM call_paths"
488					" WHERE id = " + str(parent_id))
489			if not q2.next():
490				break
491			parent_id = q2.value(0)
492		# The call path root is not used
493		if ids[0] == 1:
494			del ids[0]
495		ids.insert(0, query.value(2))
496		ids.insert(0, query.value(1))
497		return ids
498
499	def Found(self, query, found):
500		if found:
501			return self.FindPath(query)
502		return []
503
504	def FindValue(self, value, pattern, query, last_value, last_pattern):
505		if last_value == value and pattern == last_pattern:
506			found = query.first()
507		else:
508			self.FindSelect(value, pattern, query)
509			found = query.next()
510		return self.Found(query, found)
511
512	def FindNext(self, query):
513		found = query.next()
514		if not found:
515			found = query.first()
516		return self.Found(query, found)
517
518	def FindPrev(self, query):
519		found = query.previous()
520		if not found:
521			found = query.last()
522		return self.Found(query, found)
523
524	def FindThread(self, c):
525		if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
526			ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
527		elif c.direction > 0:
528			ids = self.FindNext(c.query)
529		else:
530			ids = self.FindPrev(c.query)
531		return (True, ids)
532
533	def Find(self, value, direction, pattern, context, callback):
534		class Context():
535			def __init__(self, *x):
536				self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
537			def Update(self, *x):
538				self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
539		if len(context):
540			context[0].Update(value, direction, pattern)
541		else:
542			context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
543		# Use a thread so the UI is not blocked during the SELECT
544		thread = Thread(self.FindThread, context[0])
545		thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
546		thread.start()
547
548	def FindDone(self, thread, callback, ids):
549		callback(ids)
550
551# Vertical widget layout
552
553class VBox():
554
555	def __init__(self, w1, w2, w3=None):
556		self.vbox = QWidget()
557		self.vbox.setLayout(QVBoxLayout());
558
559		self.vbox.layout().setContentsMargins(0, 0, 0, 0)
560
561		self.vbox.layout().addWidget(w1)
562		self.vbox.layout().addWidget(w2)
563		if w3:
564			self.vbox.layout().addWidget(w3)
565
566	def Widget(self):
567		return self.vbox
568
569# Context-sensitive call graph window
570
571class CallGraphWindow(QMdiSubWindow):
572
573	def __init__(self, glb, parent=None):
574		super(CallGraphWindow, self).__init__(parent)
575
576		self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
577
578		self.view = QTreeView()
579		self.view.setModel(self.model)
580
581		for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
582			self.view.setColumnWidth(c, w)
583
584		self.find_bar = FindBar(self, self)
585
586		self.vbox = VBox(self.view, self.find_bar.Widget())
587
588		self.setWidget(self.vbox.Widget())
589
590		AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
591
592	def DisplayFound(self, ids):
593		if not len(ids):
594			return False
595		parent = QModelIndex()
596		for dbid in ids:
597			found = False
598			n = self.model.rowCount(parent)
599			for row in xrange(n):
600				child = self.model.index(row, 0, parent)
601				if child.internalPointer().dbid == dbid:
602					found = True
603					self.view.setCurrentIndex(child)
604					parent = child
605					break
606			if not found:
607				break
608		return found
609
610	def Find(self, value, direction, pattern, context):
611		self.view.setFocus()
612		self.find_bar.Busy()
613		self.model.Find(value, direction, pattern, context, self.FindDone)
614
615	def FindDone(self, ids):
616		found = True
617		if not self.DisplayFound(ids):
618			found = False
619		self.find_bar.Idle()
620		if not found:
621			self.find_bar.NotFound()
622
623# Action Definition
624
625def CreateAction(label, tip, callback, parent=None, shortcut=None):
626	action = QAction(label, parent)
627	if shortcut != None:
628		action.setShortcuts(shortcut)
629	action.setStatusTip(tip)
630	action.triggered.connect(callback)
631	return action
632
633# Typical application actions
634
635def CreateExitAction(app, parent=None):
636	return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
637
638# Typical MDI actions
639
640def CreateCloseActiveWindowAction(mdi_area):
641	return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
642
643def CreateCloseAllWindowsAction(mdi_area):
644	return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
645
646def CreateTileWindowsAction(mdi_area):
647	return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
648
649def CreateCascadeWindowsAction(mdi_area):
650	return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
651
652def CreateNextWindowAction(mdi_area):
653	return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
654
655def CreatePreviousWindowAction(mdi_area):
656	return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
657
658# Typical MDI window menu
659
660class WindowMenu():
661
662	def __init__(self, mdi_area, menu):
663		self.mdi_area = mdi_area
664		self.window_menu = menu.addMenu("&Windows")
665		self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
666		self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
667		self.tile_windows = CreateTileWindowsAction(mdi_area)
668		self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
669		self.next_window = CreateNextWindowAction(mdi_area)
670		self.previous_window = CreatePreviousWindowAction(mdi_area)
671		self.window_menu.aboutToShow.connect(self.Update)
672
673	def Update(self):
674		self.window_menu.clear()
675		sub_window_count = len(self.mdi_area.subWindowList())
676		have_sub_windows = sub_window_count != 0
677		self.close_active_window.setEnabled(have_sub_windows)
678		self.close_all_windows.setEnabled(have_sub_windows)
679		self.tile_windows.setEnabled(have_sub_windows)
680		self.cascade_windows.setEnabled(have_sub_windows)
681		self.next_window.setEnabled(have_sub_windows)
682		self.previous_window.setEnabled(have_sub_windows)
683		self.window_menu.addAction(self.close_active_window)
684		self.window_menu.addAction(self.close_all_windows)
685		self.window_menu.addSeparator()
686		self.window_menu.addAction(self.tile_windows)
687		self.window_menu.addAction(self.cascade_windows)
688		self.window_menu.addSeparator()
689		self.window_menu.addAction(self.next_window)
690		self.window_menu.addAction(self.previous_window)
691		if sub_window_count == 0:
692			return
693		self.window_menu.addSeparator()
694		nr = 1
695		for sub_window in self.mdi_area.subWindowList():
696			label = str(nr) + " " + sub_window.name
697			if nr < 10:
698				label = "&" + label
699			action = self.window_menu.addAction(label)
700			action.setCheckable(True)
701			action.setChecked(sub_window == self.mdi_area.activeSubWindow())
702			action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
703			self.window_menu.addAction(action)
704			nr += 1
705
706	def setActiveSubWindow(self, nr):
707		self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
708
709# Font resize
710
711def ResizeFont(widget, diff):
712	font = widget.font()
713	sz = font.pointSize()
714	font.setPointSize(sz + diff)
715	widget.setFont(font)
716
717def ShrinkFont(widget):
718	ResizeFont(widget, -1)
719
720def EnlargeFont(widget):
721	ResizeFont(widget, 1)
722
723# Unique name for sub-windows
724
725def NumberedWindowName(name, nr):
726	if nr > 1:
727		name += " <" + str(nr) + ">"
728	return name
729
730def UniqueSubWindowName(mdi_area, name):
731	nr = 1
732	while True:
733		unique_name = NumberedWindowName(name, nr)
734		ok = True
735		for sub_window in mdi_area.subWindowList():
736			if sub_window.name == unique_name:
737				ok = False
738				break
739		if ok:
740			return unique_name
741		nr += 1
742
743# Add a sub-window
744
745def AddSubWindow(mdi_area, sub_window, name):
746	unique_name = UniqueSubWindowName(mdi_area, name)
747	sub_window.setMinimumSize(200, 100)
748	sub_window.resize(800, 600)
749	sub_window.setWindowTitle(unique_name)
750	sub_window.setAttribute(Qt.WA_DeleteOnClose)
751	sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
752	sub_window.name = unique_name
753	mdi_area.addSubWindow(sub_window)
754	sub_window.show()
755
756# Main window
757
758class MainWindow(QMainWindow):
759
760	def __init__(self, glb, parent=None):
761		super(MainWindow, self).__init__(parent)
762
763		self.glb = glb
764
765		self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
766		self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
767		self.setMinimumSize(200, 100)
768
769		self.mdi_area = QMdiArea()
770		self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
771		self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
772
773		self.setCentralWidget(self.mdi_area)
774
775		menu = self.menuBar()
776
777		file_menu = menu.addMenu("&File")
778		file_menu.addAction(CreateExitAction(glb.app, self))
779
780		edit_menu = menu.addMenu("&Edit")
781		edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
782		edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
783		edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
784
785		reports_menu = menu.addMenu("&Reports")
786		reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
787
788		self.window_menu = WindowMenu(self.mdi_area, menu)
789
790	def Find(self):
791		win = self.mdi_area.activeSubWindow()
792		if win:
793			try:
794				win.find_bar.Activate()
795			except:
796				pass
797
798	def ShrinkFont(self):
799		win = self.mdi_area.activeSubWindow()
800		ShrinkFont(win.view)
801
802	def EnlargeFont(self):
803		win = self.mdi_area.activeSubWindow()
804		EnlargeFont(win.view)
805
806	def NewCallGraph(self):
807		CallGraphWindow(self.glb, self)
808
809# Global data
810
811class Glb():
812
813	def __init__(self, dbref, db, dbname):
814		self.dbref = dbref
815		self.db = db
816		self.dbname = dbname
817		self.app = None
818		self.mainwindow = None
819
820# Database reference
821
822class DBRef():
823
824	def __init__(self, is_sqlite3, dbname):
825		self.is_sqlite3 = is_sqlite3
826		self.dbname = dbname
827
828	def Open(self, connection_name):
829		dbname = self.dbname
830		if self.is_sqlite3:
831			db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
832		else:
833			db = QSqlDatabase.addDatabase("QPSQL", connection_name)
834			opts = dbname.split()
835			for opt in opts:
836				if "=" in opt:
837					opt = opt.split("=")
838					if opt[0] == "hostname":
839						db.setHostName(opt[1])
840					elif opt[0] == "port":
841						db.setPort(int(opt[1]))
842					elif opt[0] == "username":
843						db.setUserName(opt[1])
844					elif opt[0] == "password":
845						db.setPassword(opt[1])
846					elif opt[0] == "dbname":
847						dbname = opt[1]
848				else:
849					dbname = opt
850
851		db.setDatabaseName(dbname)
852		if not db.open():
853			raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
854		return db, dbname
855
856# Main
857
858def Main():
859	if (len(sys.argv) < 2):
860		print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>"
861		raise Exception("Too few arguments")
862
863	dbname = sys.argv[1]
864
865	is_sqlite3 = False
866	try:
867		f = open(dbname)
868		if f.read(15) == "SQLite format 3":
869			is_sqlite3 = True
870		f.close()
871	except:
872		pass
873
874	dbref = DBRef(is_sqlite3, dbname)
875	db, dbname = dbref.Open("main")
876	glb = Glb(dbref, db, dbname)
877	app = QApplication(sys.argv)
878	glb.app = app
879	mainwindow = MainWindow(glb)
880	glb.mainwindow = mainwindow
881	mainwindow.show()
882	err = app.exec_()
883	db.close()
884	sys.exit(err)
885
886if __name__ == "__main__":
887	Main()
888