diff options
Diffstat (limited to 'tools/perf/scripts/python/exported-sql-viewer.py')
| -rwxr-xr-x | tools/perf/scripts/python/exported-sql-viewer.py | 493 | 
1 files changed, 490 insertions, 3 deletions
| diff --git a/tools/perf/scripts/python/exported-sql-viewer.py b/tools/perf/scripts/python/exported-sql-viewer.py index 24cb0bd56afa..f278ce5ebab7 100755 --- a/tools/perf/scripts/python/exported-sql-viewer.py +++ b/tools/perf/scripts/python/exported-sql-viewer.py @@ -119,6 +119,14 @@ def dsoname(name):  		return "[kernel]"  	return name +def findnth(s, sub, n, offs=0): +	pos = s.find(sub) +	if pos < 0: +		return pos +	if n <= 1: +		return offs + pos +	return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1) +  # Percent to one decimal place  def PercentToOneDP(n, d): @@ -1464,6 +1472,317 @@ class BranchWindow(QMdiSubWindow):  		else:  			self.find_bar.NotFound() +# Dialog data item converted and validated using a SQL table + +class SQLTableDialogDataItem(): + +	def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent): +		self.glb = glb +		self.label = label +		self.placeholder_text = placeholder_text +		self.table_name = table_name +		self.match_column = match_column +		self.column_name1 = column_name1 +		self.column_name2 = column_name2 +		self.parent = parent + +		self.value = "" + +		self.widget = QLineEdit() +		self.widget.editingFinished.connect(self.Validate) +		self.widget.textChanged.connect(self.Invalidate) +		self.red = False +		self.error = "" +		self.validated = True + +		self.last_id = 0 +		self.first_time = 0 +		self.last_time = 2 ** 64 +		if self.table_name == "<timeranges>": +			query = QSqlQuery(self.glb.db) +			QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1") +			if query.next(): +				self.last_id = int(query.value(0)) +				self.last_time = int(query.value(1)) +			QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1") +			if query.next(): +				self.first_time = int(query.value(0)) +			if placeholder_text: +				placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time) + +		if placeholder_text: +			self.widget.setPlaceholderText(placeholder_text) + +	def ValueToIds(self, value): +		ids = [] +		query = QSqlQuery(self.glb.db) +		stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'" +		ret = query.exec_(stmt) +		if ret: +			while query.next(): +				ids.append(str(query.value(0))) +		return ids + +	def IdBetween(self, query, lower_id, higher_id, order): +		QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1") +		if query.next(): +			return True, int(query.value(0)) +		else: +			return False, 0 + +	def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor): +		query = QSqlQuery(self.glb.db) +		while True: +			next_id = int((lower_id + higher_id) / 2) +			QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) +			if not query.next(): +				ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC") +				if not ok: +					ok, dbid = self.IdBetween(query, next_id, higher_id, "") +					if not ok: +						return str(higher_id) +				next_id = dbid +				QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id)) +			next_time = int(query.value(0)) +			if get_floor: +				if target_time > next_time: +					lower_id = next_id +				else: +					higher_id = next_id +				if higher_id <= lower_id + 1: +					return str(higher_id) +			else: +				if target_time >= next_time: +					lower_id = next_id +				else: +					higher_id = next_id +				if higher_id <= lower_id + 1: +					return str(lower_id) + +	def ConvertRelativeTime(self, val): +		print "val ", val +		mult = 1 +		suffix = val[-2:] +		if suffix == "ms": +			mult = 1000000 +		elif suffix == "us": +			mult = 1000 +		elif suffix == "ns": +			mult = 1 +		else: +			return val +		val = val[:-2].strip() +		if not self.IsNumber(val): +			return val +		val = int(val) * mult +		if val >= 0: +			val += self.first_time +		else: +			val += self.last_time +		return str(val) + +	def ConvertTimeRange(self, vrange): +		print "vrange ", vrange +		if vrange[0] == "": +			vrange[0] = str(self.first_time) +		if vrange[1] == "": +			vrange[1] = str(self.last_time) +		vrange[0] = self.ConvertRelativeTime(vrange[0]) +		vrange[1] = self.ConvertRelativeTime(vrange[1]) +		print "vrange2 ", vrange +		if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): +			return False +		print "ok1" +		beg_range = max(int(vrange[0]), self.first_time) +		end_range = min(int(vrange[1]), self.last_time) +		if beg_range > self.last_time or end_range < self.first_time: +			return False +		print "ok2" +		vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True) +		vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False) +		print "vrange3 ", vrange +		return True + +	def AddTimeRange(self, value, ranges): +		print "value ", value +		n = value.count("-") +		if n == 1: +			pass +		elif n == 2: +			if value.split("-")[1].strip() == "": +				n = 1 +		elif n == 3: +			n = 2 +		else: +			return False +		pos = findnth(value, "-", n) +		vrange = [value[:pos].strip() ,value[pos+1:].strip()] +		if self.ConvertTimeRange(vrange): +			ranges.append(vrange) +			return True +		return False + +	def InvalidValue(self, value): +		self.value = "" +		palette = QPalette() +		palette.setColor(QPalette.Text,Qt.red) +		self.widget.setPalette(palette) +		self.red = True +		self.error = self.label + " invalid value '" + value + "'" +		self.parent.ShowMessage(self.error) + +	def IsNumber(self, value): +		try: +			x = int(value) +		except: +			x = 0 +		return str(x) == value + +	def Invalidate(self): +		self.validated = False + +	def Validate(self): +		input_string = self.widget.text() +		self.validated = True +		if self.red: +			palette = QPalette() +			self.widget.setPalette(palette) +			self.red = False +		if not len(input_string.strip()): +			self.error = "" +			self.value = "" +			return +		if self.table_name == "<timeranges>": +			ranges = [] +			for value in [x.strip() for x in input_string.split(",")]: +				if not self.AddTimeRange(value, ranges): +					return self.InvalidValue(value) +			ranges = [("(" + self.column_name1 + " >= " + r[0] + " AND " + self.column_name1 + " <= " + r[1] + ")") for r in ranges] +			self.value = " OR ".join(ranges) +		elif self.table_name == "<ranges>": +			singles = [] +			ranges = [] +			for value in [x.strip() for x in input_string.split(",")]: +				if "-" in value: +					vrange = value.split("-") +					if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]): +						return self.InvalidValue(value) +					ranges.append(vrange) +				else: +					if not self.IsNumber(value): +						return self.InvalidValue(value) +					singles.append(value) +			ranges = [("(" + self.column_name1 + " >= " + r[0] + " AND " + self.column_name1 + " <= " + r[1] + ")") for r in ranges] +			if len(singles): +				ranges.append(self.column_name1 + " IN (" + ",".join(singles) + ")") +			self.value = " OR ".join(ranges) +		elif self.table_name: +			all_ids = [] +			for value in [x.strip() for x in input_string.split(",")]: +				ids = self.ValueToIds(value) +				if len(ids): +					all_ids.extend(ids) +				else: +					return self.InvalidValue(value) +			self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")" +			if self.column_name2: +				self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )" +		else: +			self.value = input_string.strip() +		self.error = "" +		self.parent.ClearMessage() + +	def IsValid(self): +		if not self.validated: +			self.Validate() +		if len(self.error): +			self.parent.ShowMessage(self.error) +			return False +		return True + +# Selected branch report creation dialog + +class SelectedBranchDialog(QDialog): + +	def __init__(self, glb, parent=None): +		super(SelectedBranchDialog, self).__init__(parent) + +		self.glb = glb + +		self.name = "" +		self.where_clause = "" + +		self.setWindowTitle("Selected Branches") +		self.setMinimumWidth(600) + +		items = ( +			("Report name:", "Enter a name to appear in the window title bar", "", "", "", ""), +			("Time ranges:", "Enter time ranges", "<timeranges>", "", "samples.id", ""), +			("CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "<ranges>", "", "cpu", ""), +			("Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", ""), +			("PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", ""), +			("TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", ""), +			("DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id"), +			("Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id"), +			("Raw SQL clause: ", "Enter a raw SQL WHERE clause", "", "", "", ""), +			) +		self.data_items = [SQLTableDialogDataItem(glb, *x, parent=self) for x in items] + +		self.grid = QGridLayout() + +		for row in xrange(len(self.data_items)): +			self.grid.addWidget(QLabel(self.data_items[row].label), row, 0) +			self.grid.addWidget(self.data_items[row].widget, row, 1) + +		self.status = QLabel() + +		self.ok_button = QPushButton("Ok", self) +		self.ok_button.setDefault(True) +		self.ok_button.released.connect(self.Ok) +		self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + +		self.cancel_button = QPushButton("Cancel", self) +		self.cancel_button.released.connect(self.reject) +		self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + +		self.hbox = QHBoxLayout() +		#self.hbox.addStretch() +		self.hbox.addWidget(self.status) +		self.hbox.addWidget(self.ok_button) +		self.hbox.addWidget(self.cancel_button) + +		self.vbox = QVBoxLayout() +		self.vbox.addLayout(self.grid) +		self.vbox.addLayout(self.hbox) + +		self.setLayout(self.vbox); + +	def Ok(self): +		self.name = self.data_items[0].value +		if not self.name: +			self.ShowMessage("Report name is required") +			return +		for d in self.data_items: +			if not d.IsValid(): +				return +		for d in self.data_items[1:]: +			if len(d.value): +				if len(self.where_clause): +					self.where_clause += " AND " +				self.where_clause += d.value +		if len(self.where_clause): +			self.where_clause = " AND ( " + self.where_clause + " ) " +		else: +			self.ShowMessage("No selection") +			return +		self.accept() + +	def ShowMessage(self, msg): +		self.status.setText("<font color=#FF0000>" + msg) + +	def ClearMessage(self): +		self.status.setText("") +  # Event list  def GetEventList(db): @@ -1656,7 +1975,7 @@ class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):  	def FindDone(self, row):  		self.find_bar.Idle()  		if row >= 0: -			self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex())) +			self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))  		else:  			self.find_bar.NotFound() @@ -1765,6 +2084,149 @@ class WindowMenu():  	def setActiveSubWindow(self, nr):  		self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1]) +# Help text + +glb_help_text = """ +<h1>Contents</h1> +<style> +p.c1 { +    text-indent: 40px; +} +p.c2 { +    text-indent: 80px; +} +} +</style> +<p class=c1><a href=#reports>1. Reports</a></p> +<p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p> +<p class=c2><a href=#allbranches>1.2 All branches</a></p> +<p class=c2><a href=#selectedbranches>1.3 Selected branches</a></p> +<p class=c1><a href=#tables>2. Tables</a></p> +<h1 id=reports>1. Reports</h1> +<h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2> +The result is a GUI window with a tree representing a context-sensitive +call-graph. Expanding a couple of levels of the tree and adjusting column +widths to suit will display something like: +<pre> +                                         Call Graph: pt_example +Call Path                          Object      Count   Time(ns)  Time(%)  Branch Count   Branch Count(%) +v- ls +    v- 2638:2638 +        v- _start                  ld-2.19.so    1     10074071   100.0         211135            100.0 +          |- unknown               unknown       1        13198     0.1              1              0.0 +          >- _dl_start             ld-2.19.so    1      1400980    13.9          19637              9.3 +          >- _d_linit_internal     ld-2.19.so    1       448152     4.4          11094              5.3 +          v-__libc_start_main@plt  ls            1      8211741    81.5         180397             85.4 +             >- _dl_fixup          ld-2.19.so    1         7607     0.1            108              0.1 +             >- __cxa_atexit       libc-2.19.so  1        11737     0.1             10              0.0 +             >- __libc_csu_init    ls            1        10354     0.1             10              0.0 +             |- _setjmp            libc-2.19.so  1            0     0.0              4              0.0 +             v- main               ls            1      8182043    99.6         180254             99.9 +</pre> +<h3>Points to note:</h3> +<ul> +<li>The top level is a command name (comm)</li> +<li>The next level is a thread (pid:tid)</li> +<li>Subsequent levels are functions</li> +<li>'Count' is the number of calls</li> +<li>'Time' is the elapsed time until the function returns</li> +<li>Percentages are relative to the level above</li> +<li>'Branch Count' is the total number of branches for that function and all functions that it calls +</ul> +<h3>Find</h3> +Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match. +The pattern matching symbols are ? for any character and * for zero or more characters. +<h2 id=allbranches>1.2 All branches</h2> +The All branches report displays all branches in chronological order. +Not all data is fetched immediately. More records can be fetched using the Fetch bar provided. +<h3>Disassembly</h3> +Open a branch to display disassembly. This only works if: +<ol> +<li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li> +<li>The object code is available. Currently, only the perf build ID cache is searched for object code. +The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR. +One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu), +or alternatively, set environment variable PERF_KCORE to the kcore file name.</li> +</ol> +<h4 id=xed>Intel XED Setup</h4> +To use Intel XED, libxed.so must be present.  To build and install libxed.so: +<pre> +git clone https://github.com/intelxed/mbuild.git mbuild +git clone https://github.com/intelxed/xed +cd xed +./mfile.py --share +sudo ./mfile.py --prefix=/usr/local install +sudo ldconfig +</pre> +<h3>Find</h3> +Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. +Refer to Python documentation for the regular expression syntax. +All columns are searched, but only currently fetched rows are searched. +<h2 id=selectedbranches>1.3 Selected branches</h2> +This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced +by various selection criteria. A dialog box displays available criteria which are AND'ed together. +<h3>1.3.1 Time ranges</h3> +The time ranges hint text shows the total time range. Relative time ranges can also be entered in +ms, us or ns. Also, negative values are relative to the end of trace.  Examples: +<pre> +	81073085947329-81073085958238	From 81073085947329 to 81073085958238 +	100us-200us		From 100us to 200us +	10ms-			From 10ms to the end +	-100ns			The first 100ns +	-10ms-			The last 10ms +</pre> +N.B. Due to the granularity of timestamps, there could be no branches in any given time range. +<h1 id=tables>2. Tables</h1> +The Tables menu shows all tables and views in the database. Most tables have an associated view +which displays the information in a more friendly way. Not all data for large tables is fetched +immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted, +but that can be slow for large tables. +<p>There are also tables of database meta-information. +For SQLite3 databases, the sqlite_master table is included. +For PostgreSQL databases, information_schema.tables/views/columns are included. +<h3>Find</h3> +Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match. +Refer to Python documentation for the regular expression syntax. +All columns are searched, but only currently fetched rows are searched. +<p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous +will go to the next/previous result in id order, instead of display order. +""" + +# Help window + +class HelpWindow(QMdiSubWindow): + +	def __init__(self, glb, parent=None): +		super(HelpWindow, self).__init__(parent) + +		self.text = QTextBrowser() +		self.text.setHtml(glb_help_text) +		self.text.setReadOnly(True) +		self.text.setOpenExternalLinks(True) + +		self.setWidget(self.text) + +		AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help") + +# Main window that only displays the help text + +class HelpOnlyWindow(QMainWindow): + +	def __init__(self, parent=None): +		super(HelpOnlyWindow, self).__init__(parent) + +		self.setMinimumSize(200, 100) +		self.resize(800, 600) +		self.setWindowTitle("Exported SQL Viewer Help") +		self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation)) + +		self.text = QTextBrowser() +		self.text.setHtml(glb_help_text) +		self.text.setReadOnly(True) +		self.text.setOpenExternalLinks(True) + +		self.setCentralWidget(self.text) +  # Font resize  def ResizeFont(widget, diff): @@ -1851,6 +2313,9 @@ class MainWindow(QMainWindow):  		self.window_menu = WindowMenu(self.mdi_area, menu) +		help_menu = menu.addMenu("&Help") +		help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents)) +  	def Find(self):  		win = self.mdi_area.activeSubWindow()  		if win: @@ -1888,6 +2353,8 @@ class MainWindow(QMainWindow):  			if event == "branches":  				label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"  				reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewBranchView(x), self)) +				label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")" +				reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewSelectedBranchView(x), self))  	def TableMenu(self, tables, menu):  		table_menu = menu.addMenu("&Tables") @@ -1900,9 +2367,18 @@ class MainWindow(QMainWindow):  	def NewBranchView(self, event_id):  		BranchWindow(self.glb, event_id, "", "", self) +	def NewSelectedBranchView(self, event_id): +		dialog = SelectedBranchDialog(self.glb, self) +		ret = dialog.exec_() +		if ret: +			BranchWindow(self.glb, event_id, dialog.name, dialog.where_clause, self) +  	def NewTableView(self, table_name):  		TableWindow(self.glb, table_name, self) +	def Help(self): +		HelpWindow(self.glb, self) +  # XED Disassembler  class xed_state_t(Structure): @@ -1929,7 +2405,12 @@ class XEDInstruction():  class LibXED():  	def __init__(self): -		self.libxed = CDLL("libxed.so") +		try: +			self.libxed = CDLL("libxed.so") +		except: +			self.libxed = None +		if not self.libxed: +			self.libxed = CDLL("/usr/local/lib/libxed.so")  		self.xed_tables_init = self.libxed.xed_tables_init  		self.xed_tables_init.restype = None @@ -2097,10 +2578,16 @@ class DBRef():  def Main():  	if (len(sys.argv) < 2): -		print >> sys.stderr, "Usage is: exported-sql-viewer.py <database name>" +		print >> sys.stderr, "Usage is: exported-sql-viewer.py {<database name> | --help-only}"  		raise Exception("Too few arguments")  	dbname = sys.argv[1] +	if dbname == "--help-only": +		app = QApplication(sys.argv) +		mainwindow = HelpOnlyWindow() +		mainwindow.show() +		err = app.exec_() +		sys.exit(err)  	is_sqlite3 = False  	try: |