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 | |
| 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')
| -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 | ||
