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.
Showing the changes to tools/perf/scripts/python (1 of 1 file). Show all
Unified · Split
| @@ -49,6 +49,7 @@ · 48 unmodified lines | ||
| 49 | 49 | import sys |
| 50 | 50 | import weakref |
| 51 | 51 | import threading |
| 52 | +import string | |
| 52 | 53 | from PySide.QtCore import * |
| 53 | 54 | from PySide.QtGui import * |
| 54 | 55 | from PySide.QtSql import * |
| @@ -76,6 +77,27 @@ def QueryExec(query, stmt): · 21 unmodified lines | ||
| 76 | 77 | if not ret: |
| 77 | 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 | 101 | # Tree data model |
| 80 | 102 | |
| 81 | 103 | class TreeModel(QAbstractItemModel): |
| @@ -157,6 +179,125 @@ def LookupCreateModel(model_name, create_fn): · 75 unmodified lines | ||
| 157 | 179 | model_cache_lock.release() |
| 158 | 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 | 301 | # Context-sensitive call graph data model item base |
| 161 | 302 | |
| 162 | 303 | class CallGraphLevelItemBase(object): |
| @@ -308,6 +449,123 @@ class CallGraphModel(TreeModel): · 145 unmodified lines | ||
| 308 | 449 | alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ] |
| 309 | 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 | 569 | # Context-sensitive call graph window |
| 312 | 570 | |
| 313 | 571 | class CallGraphWindow(QMdiSubWindow): |
| @@ -323,10 +581,45 @@ class CallGraphWindow(QMdiSubWindow): · 9 unmodified lines | ||
| 323 | 581 | for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)): |
| 324 | 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 | 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 | 623 | # Action Definition |
| 331 | 624 | |
| 332 | 625 | def CreateAction(label, tip, callback, parent=None, shortcut=None): |
| @@ -470,11 +763,22 @@ class MainWindow(QMainWindow): · 137 unmodified lines | ||
| 470 | 763 | file_menu = menu.addMenu("&File") |
| 471 | 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 | 769 | reports_menu = menu.addMenu("&Reports") |
| 474 | 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 | 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 | 782 | def NewCallGraph(self): |
| 479 | 783 | CallGraphWindow(self.glb, self) |
| 480 | 784 | |