diff options
author | Adrian Hunter <adrian.hunter@intel.com> | 2018-10-01 02:28:48 -0400 |
---|---|---|
committer | Arnaldo Carvalho de Melo <acme@redhat.com> | 2018-10-23 13:30:33 -0400 |
commit | ebd70c7dc2f5f57315e19d959ddc9cb05e9d48e1 (patch) | |
tree | 70645a0acd9da0ac20936181863f62240cf16032 /tools/perf/scripts/python/exported-sql-viewer.py | |
parent | 1beb5c7b07040b70975a2ae0e90b87d412fabf06 (diff) |
perf scripts python: exported-sql-viewer.py: Add ability to find symbols in the call-graph
Add a Find bar that appears at the bottom of the call-graph window.
Committer testing:
Using:
python tools/perf/scripts/python/exported-sql-viewer.py pt_example branches calls
Using the database built in the first "Committer Testing" section in
this patch series I was able to:
"Reports"
"Context-Sensitive Call Graphs"
Control+F or select "Edit" in the top menu then "Find"
__poll<ENTER>
and find the first place where the "__poll" function appears, then
press the down arrow in the lower right corner and go to the next, etc.
Signed-off-by: Adrian Hunter <adrian.hunter@intel.com>
Tested-by: Arnaldo Carvalho de Melo <acme@redhat.com>
Cc: Andi Kleen <ak@linux.intel.com>
Cc: Jiri Olsa <jolsa@redhat.com>
Link: http://lkml.kernel.org/r/20181001062853.28285-15-adrian.hunter@intel.com
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
Diffstat (limited to 'tools/perf/scripts/python/exported-sql-viewer.py')
-rwxr-xr-x | tools/perf/scripts/python/exported-sql-viewer.py | 306 |
1 files changed, 305 insertions, 1 deletions
diff --git a/tools/perf/scripts/python/exported-sql-viewer.py b/tools/perf/scripts/python/exported-sql-viewer.py index c2f44351821e..0386a600ffc7 100755 --- a/tools/perf/scripts/python/exported-sql-viewer.py +++ b/tools/perf/scripts/python/exported-sql-viewer.py | |||
@@ -49,6 +49,7 @@ | |||
49 | import sys | 49 | import sys |
50 | import weakref | 50 | import weakref |
51 | import threading | 51 | import threading |
52 | import string | ||
52 | from PySide.QtCore import * | 53 | from PySide.QtCore import * |
53 | from PySide.QtGui import * | 54 | from PySide.QtGui import * |
54 | from PySide.QtSql import * | 55 | from PySide.QtSql import * |
@@ -76,6 +77,27 @@ def QueryExec(query, stmt): | |||
76 | if not ret: | 77 | if not ret: |
77 | raise Exception("Query failed: " + query.lastError().text()) | 78 | raise Exception("Query failed: " + query.lastError().text()) |
78 | 79 | ||
80 | # Background thread | ||
81 | |||
82 | class 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 | |||
79 | # Tree data model | 101 | # Tree data model |
80 | 102 | ||
81 | class TreeModel(QAbstractItemModel): | 103 | class TreeModel(QAbstractItemModel): |
@@ -157,6 +179,125 @@ def LookupCreateModel(model_name, create_fn): | |||
157 | model_cache_lock.release() | 179 | model_cache_lock.release() |
158 | return model | 180 | return model |
159 | 181 | ||
182 | # Find bar | ||
183 | |||
184 | class 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 | |||
160 | # Context-sensitive call graph data model item base | 301 | # Context-sensitive call graph data model item base |
161 | 302 | ||
162 | class CallGraphLevelItemBase(object): | 303 | class CallGraphLevelItemBase(object): |
@@ -308,6 +449,123 @@ class CallGraphModel(TreeModel): | |||
308 | alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] | 449 | alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] |
309 | return alignment[column] | 450 | return alignment[column] |
310 | 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 | |||
553 | class 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 | |||
311 | # Context-sensitive call graph window | 569 | # Context-sensitive call graph window |
312 | 570 | ||
313 | class CallGraphWindow(QMdiSubWindow): | 571 | class CallGraphWindow(QMdiSubWindow): |
@@ -323,10 +581,45 @@ class CallGraphWindow(QMdiSubWindow): | |||
323 | for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): | 581 | for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): |
324 | self.view.setColumnWidth(c, w) | 582 | self.view.setColumnWidth(c, w) |
325 | 583 | ||
326 | self.setWidget(self.view) | 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()) | ||
327 | 589 | ||
328 | AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") | 590 | AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph") |
329 | 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 | |||
330 | # Action Definition | 623 | # Action Definition |
331 | 624 | ||
332 | def CreateAction(label, tip, callback, parent=None, shortcut=None): | 625 | def CreateAction(label, tip, callback, parent=None, shortcut=None): |
@@ -470,11 +763,22 @@ class MainWindow(QMainWindow): | |||
470 | file_menu = menu.addMenu("&File") | 763 | file_menu = menu.addMenu("&File") |
471 | file_menu.addAction(CreateExitAction(glb.app, self)) | 764 | file_menu.addAction(CreateExitAction(glb.app, self)) |
472 | 765 | ||
766 | edit_menu = menu.addMenu("&Edit") | ||
767 | edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find)) | ||
768 | |||
473 | reports_menu = menu.addMenu("&Reports") | 769 | 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)) | 770 | reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self)) |
475 | 771 | ||
476 | self.window_menu = WindowMenu(self.mdi_area, menu) | 772 | self.window_menu = WindowMenu(self.mdi_area, menu) |
477 | 773 | ||
774 | def Find(self): | ||
775 | win = self.mdi_area.activeSubWindow() | ||
776 | if win: | ||
777 | try: | ||
778 | win.find_bar.Activate() | ||
779 | except: | ||
780 | pass | ||
781 | |||
478 | def NewCallGraph(self): | 782 | def NewCallGraph(self): |
479 | CallGraphWindow(self.glb, self) | 783 | CallGraphWindow(self.glb, self) |
480 | 784 | ||