2 # SPDX-License-Identifier: GPL-2.0
3 # exported-sql-viewer.py: view data from sql database
4 # Copyright (c) 2014-2018, Intel Corporation.
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
10 # Following on from the example in the export scripts, a
11 # call-graph can be displayed for the pt_example database like this:
13 # python tools/perf/scripts/python/exported-sql-viewer.py pt_example
15 # Note that for PostgreSQL, this script supports connecting to remote databases
16 # by setting hostname, port, username, password, and dbname e.g.
18 # python tools/perf/scripts/python/exported-sql-viewer.py "hostname=myhost username=myuser password=mypassword dbname=pt_example"
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:
24 # Call Graph: pt_example
25 # Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
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
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
49 # There is also a "All branches" report, which displays branches and
50 # possibly disassembly. However, presently, the only supported disassembler is
51 # Intel XED, and additionally the object code must be present in perf build ID
52 # cache. To use Intel XED, libxed.so must be present. To build and install
54 # git clone https://github.com/intelxed/mbuild.git mbuild
55 # git clone https://github.com/intelxed/xed
58 # sudo ./mfile.py --prefix=/usr/local install
63 # Time CPU Command PID TID Branch Type In Tx Branch
64 # 8107675239590 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
65 # 7fab593ea260 48 89 e7 mov %rsp, %rdi
66 # 8107675239899 2 ls 22011 22011 hardware interrupt No 7fab593ea260 _start (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
67 # 8107675241900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea260 _start (ld-2.19.so)
68 # 7fab593ea260 48 89 e7 mov %rsp, %rdi
69 # 7fab593ea263 e8 c8 06 00 00 callq 0x7fab593ea930
70 # 8107675241900 2 ls 22011 22011 call No 7fab593ea263 _start+0x3 (ld-2.19.so) -> 7fab593ea930 _dl_start (ld-2.19.so)
71 # 7fab593ea930 55 pushq %rbp
72 # 7fab593ea931 48 89 e5 mov %rsp, %rbp
73 # 7fab593ea934 41 57 pushq %r15
74 # 7fab593ea936 41 56 pushq %r14
75 # 7fab593ea938 41 55 pushq %r13
76 # 7fab593ea93a 41 54 pushq %r12
77 # 7fab593ea93c 53 pushq %rbx
78 # 7fab593ea93d 48 89 fb mov %rdi, %rbx
79 # 7fab593ea940 48 83 ec 68 sub $0x68, %rsp
80 # 7fab593ea944 0f 31 rdtsc
81 # 7fab593ea946 48 c1 e2 20 shl $0x20, %rdx
82 # 7fab593ea94a 89 c0 mov %eax, %eax
83 # 7fab593ea94c 48 09 c2 or %rax, %rdx
84 # 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
85 # 8107675242232 2 ls 22011 22011 hardware interrupt No 7fab593ea94f _dl_start+0x1f (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
86 # 8107675242900 2 ls 22011 22011 return from interrupt No ffffffff86a00a67 native_irq_return_iret ([kernel]) -> 7fab593ea94f _dl_start+0x1f (ld-2.19.so)
87 # 7fab593ea94f 48 8b 05 1a 15 22 00 movq 0x22151a(%rip), %rax
88 # 7fab593ea956 48 89 15 3b 13 22 00 movq %rdx, 0x22133b(%rip)
89 # 8107675243232 2 ls 22011 22011 hardware interrupt No 7fab593ea956 _dl_start+0x26 (ld-2.19.so) -> ffffffff86a012e0 page_fault ([kernel])
91 from __future__ import print_function
99 import cPickle as pickle
100 # size of pickled integer big enough for record size
107 from PySide.QtCore import *
108 from PySide.QtGui import *
109 from PySide.QtSql import *
110 from decimal import *
112 from multiprocessing import Process, Array, Value, Event
114 # xrange is range in Python3
120 def printerr(*args, **keyword_args):
121 print(*args, file=sys.stderr, **keyword_args)
123 # Data formatting helpers
132 return "+0x%x" % offset
136 if name == "[kernel.kallsyms]":
140 def findnth(s, sub, n, offs=0):
146 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
148 # Percent to one decimal place
150 def PercentToOneDP(n, d):
153 x = (n * Decimal(100)) / d
154 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
156 # Helper for queries that must not fail
158 def QueryExec(query, stmt):
159 ret = query.exec_(stmt)
161 raise Exception("Query failed: " + query.lastError().text())
165 class Thread(QThread):
167 done = Signal(object)
169 def __init__(self, task, param=None, parent=None):
170 super(Thread, self).__init__(parent)
176 if self.param is None:
177 done, result = self.task()
179 done, result = self.task(self.param)
180 self.done.emit(result)
186 class TreeModel(QAbstractItemModel):
188 def __init__(self, glb, parent=None):
189 super(TreeModel, self).__init__(parent)
191 self.root = self.GetRoot()
192 self.last_row_read = 0
194 def Item(self, parent):
196 return parent.internalPointer()
200 def rowCount(self, parent):
201 result = self.Item(parent).childCount()
204 self.dataChanged.emit(parent, parent)
207 def hasChildren(self, parent):
208 return self.Item(parent).hasChildren()
210 def headerData(self, section, orientation, role):
211 if role == Qt.TextAlignmentRole:
212 return self.columnAlignment(section)
213 if role != Qt.DisplayRole:
215 if orientation != Qt.Horizontal:
217 return self.columnHeader(section)
219 def parent(self, child):
220 child_item = child.internalPointer()
221 if child_item is self.root:
223 parent_item = child_item.getParentItem()
224 return self.createIndex(parent_item.getRow(), 0, parent_item)
226 def index(self, row, column, parent):
227 child_item = self.Item(parent).getChildItem(row)
228 return self.createIndex(row, column, child_item)
230 def DisplayData(self, item, index):
231 return item.getData(index.column())
233 def FetchIfNeeded(self, row):
234 if row > self.last_row_read:
235 self.last_row_read = row
236 if row + 10 >= self.root.child_count:
237 self.fetcher.Fetch(glb_chunk_sz)
239 def columnAlignment(self, column):
242 def columnFont(self, column):
245 def data(self, index, role):
246 if role == Qt.TextAlignmentRole:
247 return self.columnAlignment(index.column())
248 if role == Qt.FontRole:
249 return self.columnFont(index.column())
250 if role != Qt.DisplayRole:
252 item = index.internalPointer()
253 return self.DisplayData(item, index)
257 class TableModel(QAbstractTableModel):
259 def __init__(self, parent=None):
260 super(TableModel, self).__init__(parent)
262 self.child_items = []
263 self.last_row_read = 0
265 def Item(self, parent):
267 return parent.internalPointer()
271 def rowCount(self, parent):
272 return self.child_count
274 def headerData(self, section, orientation, role):
275 if role == Qt.TextAlignmentRole:
276 return self.columnAlignment(section)
277 if role != Qt.DisplayRole:
279 if orientation != Qt.Horizontal:
281 return self.columnHeader(section)
283 def index(self, row, column, parent):
284 return self.createIndex(row, column, self.child_items[row])
286 def DisplayData(self, item, index):
287 return item.getData(index.column())
289 def FetchIfNeeded(self, row):
290 if row > self.last_row_read:
291 self.last_row_read = row
292 if row + 10 >= self.child_count:
293 self.fetcher.Fetch(glb_chunk_sz)
295 def columnAlignment(self, column):
298 def columnFont(self, column):
301 def data(self, index, role):
302 if role == Qt.TextAlignmentRole:
303 return self.columnAlignment(index.column())
304 if role == Qt.FontRole:
305 return self.columnFont(index.column())
306 if role != Qt.DisplayRole:
308 item = index.internalPointer()
309 return self.DisplayData(item, index)
313 model_cache = weakref.WeakValueDictionary()
314 model_cache_lock = threading.Lock()
316 def LookupCreateModel(model_name, create_fn):
317 model_cache_lock.acquire()
319 model = model_cache[model_name]
324 model_cache[model_name] = model
325 model_cache_lock.release()
332 def __init__(self, parent, finder, is_reg_expr=False):
335 self.last_value = None
336 self.last_pattern = None
338 label = QLabel("Find:")
339 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
341 self.textbox = QComboBox()
342 self.textbox.setEditable(True)
343 self.textbox.currentIndexChanged.connect(self.ValueChanged)
345 self.progress = QProgressBar()
346 self.progress.setRange(0, 0)
350 self.pattern = QCheckBox("Regular Expression")
352 self.pattern = QCheckBox("Pattern")
353 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
355 self.next_button = QToolButton()
356 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
357 self.next_button.released.connect(lambda: self.NextPrev(1))
359 self.prev_button = QToolButton()
360 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
361 self.prev_button.released.connect(lambda: self.NextPrev(-1))
363 self.close_button = QToolButton()
364 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
365 self.close_button.released.connect(self.Deactivate)
367 self.hbox = QHBoxLayout()
368 self.hbox.setContentsMargins(0, 0, 0, 0)
370 self.hbox.addWidget(label)
371 self.hbox.addWidget(self.textbox)
372 self.hbox.addWidget(self.progress)
373 self.hbox.addWidget(self.pattern)
374 self.hbox.addWidget(self.next_button)
375 self.hbox.addWidget(self.prev_button)
376 self.hbox.addWidget(self.close_button)
379 self.bar.setLayout(self.hbox);
387 self.textbox.setFocus()
389 def Deactivate(self):
393 self.textbox.setEnabled(False)
395 self.next_button.hide()
396 self.prev_button.hide()
400 self.textbox.setEnabled(True)
403 self.next_button.show()
404 self.prev_button.show()
406 def Find(self, direction):
407 value = self.textbox.currentText()
408 pattern = self.pattern.isChecked()
409 self.last_value = value
410 self.last_pattern = pattern
411 self.finder.Find(value, direction, pattern, self.context)
413 def ValueChanged(self):
414 value = self.textbox.currentText()
415 pattern = self.pattern.isChecked()
416 index = self.textbox.currentIndex()
417 data = self.textbox.itemData(index)
418 # Store the pattern in the combo box to keep it with the text value
420 self.textbox.setItemData(index, pattern)
422 self.pattern.setChecked(data)
425 def NextPrev(self, direction):
426 value = self.textbox.currentText()
427 pattern = self.pattern.isChecked()
428 if value != self.last_value:
429 index = self.textbox.findText(value)
430 # Allow for a button press before the value has been added to the combo box
432 index = self.textbox.count()
433 self.textbox.addItem(value, pattern)
434 self.textbox.setCurrentIndex(index)
437 self.textbox.setItemData(index, pattern)
438 elif pattern != self.last_pattern:
439 # Keep the pattern recorded in the combo box up to date
440 index = self.textbox.currentIndex()
441 self.textbox.setItemData(index, pattern)
445 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
447 # Context-sensitive call graph data model item base
449 class CallGraphLevelItemBase(object):
451 def __init__(self, glb, row, parent_item):
454 self.parent_item = parent_item
455 self.query_done = False;
457 self.child_items = []
459 def getChildItem(self, row):
460 return self.child_items[row]
462 def getParentItem(self):
463 return self.parent_item
468 def childCount(self):
469 if not self.query_done:
471 if not self.child_count:
473 return self.child_count
475 def hasChildren(self):
476 if not self.query_done:
478 return self.child_count > 0
480 def getData(self, column):
481 return self.data[column]
483 # Context-sensitive call graph data model level 2+ item base
485 class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
487 def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
488 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
489 self.comm_id = comm_id
490 self.thread_id = thread_id
491 self.call_path_id = call_path_id
492 self.branch_count = branch_count
496 self.query_done = True;
497 query = QSqlQuery(self.glb.db)
498 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
500 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
501 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
502 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
503 " WHERE parent_call_path_id = " + str(self.call_path_id) +
504 " AND comm_id = " + str(self.comm_id) +
505 " AND thread_id = " + str(self.thread_id) +
506 " GROUP BY call_path_id, name, short_name"
507 " ORDER BY call_path_id")
509 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)
510 self.child_items.append(child_item)
511 self.child_count += 1
513 # Context-sensitive call graph data model level three item
515 class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
517 def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
518 super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
520 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
521 self.dbid = call_path_id
523 # Context-sensitive call graph data model level two item
525 class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
527 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
528 super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
529 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
530 self.dbid = thread_id
533 super(CallGraphLevelTwoItem, self).Select()
534 for child_item in self.child_items:
535 self.time += child_item.time
536 self.branch_count += child_item.branch_count
537 for child_item in self.child_items:
538 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
539 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
541 # Context-sensitive call graph data model level one item
543 class CallGraphLevelOneItem(CallGraphLevelItemBase):
545 def __init__(self, glb, row, comm_id, comm, parent_item):
546 super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
547 self.data = [comm, "", "", "", "", "", ""]
551 self.query_done = True;
552 query = QSqlQuery(self.glb.db)
553 QueryExec(query, "SELECT thread_id, pid, tid"
555 " INNER JOIN threads ON thread_id = threads.id"
556 " WHERE comm_id = " + str(self.dbid))
558 child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
559 self.child_items.append(child_item)
560 self.child_count += 1
562 # Context-sensitive call graph data model root item
564 class CallGraphRootItem(CallGraphLevelItemBase):
566 def __init__(self, glb):
567 super(CallGraphRootItem, self).__init__(glb, 0, None)
569 self.query_done = True;
570 query = QSqlQuery(glb.db)
571 QueryExec(query, "SELECT id, comm FROM comms")
573 if not query.value(0):
575 child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
576 self.child_items.append(child_item)
577 self.child_count += 1
579 # Context-sensitive call graph data model base
581 class CallGraphModelBase(TreeModel):
583 def __init__(self, glb, parent=None):
584 super(CallGraphModelBase, self).__init__(glb, parent)
586 def FindSelect(self, value, pattern, query):
588 # postgresql and sqlite pattern patching differences:
589 # postgresql LIKE is case sensitive but sqlite LIKE is not
590 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
591 # postgresql supports ILIKE which is case insensitive
592 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
593 if not self.glb.dbref.is_sqlite3:
595 s = value.replace("%", "\%")
596 s = s.replace("_", "\_")
597 # Translate * and ? into SQL LIKE pattern characters % and _
598 trans = string.maketrans("*?", "%_")
599 match = " LIKE '" + str(s).translate(trans) + "'"
601 match = " GLOB '" + str(value) + "'"
603 match = " = '" + str(value) + "'"
604 self.DoFindSelect(query, match)
606 def Found(self, query, found):
608 return self.FindPath(query)
611 def FindValue(self, value, pattern, query, last_value, last_pattern):
612 if last_value == value and pattern == last_pattern:
613 found = query.first()
615 self.FindSelect(value, pattern, query)
617 return self.Found(query, found)
619 def FindNext(self, query):
622 found = query.first()
623 return self.Found(query, found)
625 def FindPrev(self, query):
626 found = query.previous()
629 return self.Found(query, found)
631 def FindThread(self, c):
632 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
633 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
634 elif c.direction > 0:
635 ids = self.FindNext(c.query)
637 ids = self.FindPrev(c.query)
640 def Find(self, value, direction, pattern, context, callback):
642 def __init__(self, *x):
643 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
644 def Update(self, *x):
645 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
647 context[0].Update(value, direction, pattern)
649 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
650 # Use a thread so the UI is not blocked during the SELECT
651 thread = Thread(self.FindThread, context[0])
652 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
655 def FindDone(self, thread, callback, ids):
658 # Context-sensitive call graph data model
660 class CallGraphModel(CallGraphModelBase):
662 def __init__(self, glb, parent=None):
663 super(CallGraphModel, self).__init__(glb, parent)
666 return CallGraphRootItem(self.glb)
668 def columnCount(self, parent=None):
671 def columnHeader(self, column):
672 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
673 return headers[column]
675 def columnAlignment(self, column):
676 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
677 return alignment[column]
679 def DoFindSelect(self, query, match):
680 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
682 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
683 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
684 " WHERE symbols.name" + match +
685 " GROUP BY comm_id, thread_id, call_path_id"
686 " ORDER BY comm_id, thread_id, call_path_id")
688 def FindPath(self, query):
689 # Turn the query result into a list of ids that the tree view can walk
690 # to open the tree at the right place.
692 parent_id = query.value(0)
694 ids.insert(0, parent_id)
695 q2 = QSqlQuery(self.glb.db)
696 QueryExec(q2, "SELECT parent_id"
698 " WHERE id = " + str(parent_id))
701 parent_id = q2.value(0)
702 # The call path root is not used
705 ids.insert(0, query.value(2))
706 ids.insert(0, query.value(1))
709 # Call tree data model level 2+ item base
711 class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
713 def __init__(self, glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item):
714 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
715 self.comm_id = comm_id
716 self.thread_id = thread_id
717 self.calls_id = calls_id
718 self.branch_count = branch_count
722 self.query_done = True;
723 if self.calls_id == 0:
724 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
727 query = QSqlQuery(self.glb.db)
728 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time, branch_count"
730 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
731 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
732 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
733 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
734 " ORDER BY call_time, calls.id")
736 child_item = CallTreeLevelThreeItem(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)
737 self.child_items.append(child_item)
738 self.child_count += 1
740 # Call tree data model level three item
742 class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
744 def __init__(self, glb, row, comm_id, thread_id, calls_id, name, dso, count, time, branch_count, parent_item):
745 super(CallTreeLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item)
747 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
750 # Call tree data model level two item
752 class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
754 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
755 super(CallTreeLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 0, 0, 0, parent_item)
756 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
757 self.dbid = thread_id
760 super(CallTreeLevelTwoItem, self).Select()
761 for child_item in self.child_items:
762 self.time += child_item.time
763 self.branch_count += child_item.branch_count
764 for child_item in self.child_items:
765 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
766 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
768 # Call tree data model level one item
770 class CallTreeLevelOneItem(CallGraphLevelItemBase):
772 def __init__(self, glb, row, comm_id, comm, parent_item):
773 super(CallTreeLevelOneItem, self).__init__(glb, row, parent_item)
774 self.data = [comm, "", "", "", "", "", ""]
778 self.query_done = True;
779 query = QSqlQuery(self.glb.db)
780 QueryExec(query, "SELECT thread_id, pid, tid"
782 " INNER JOIN threads ON thread_id = threads.id"
783 " WHERE comm_id = " + str(self.dbid))
785 child_item = CallTreeLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
786 self.child_items.append(child_item)
787 self.child_count += 1
789 # Call tree data model root item
791 class CallTreeRootItem(CallGraphLevelItemBase):
793 def __init__(self, glb):
794 super(CallTreeRootItem, self).__init__(glb, 0, None)
796 self.query_done = True;
797 query = QSqlQuery(glb.db)
798 QueryExec(query, "SELECT id, comm FROM comms")
800 if not query.value(0):
802 child_item = CallTreeLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
803 self.child_items.append(child_item)
804 self.child_count += 1
806 # Call Tree data model
808 class CallTreeModel(CallGraphModelBase):
810 def __init__(self, glb, parent=None):
811 super(CallTreeModel, self).__init__(glb, parent)
814 return CallTreeRootItem(self.glb)
816 def columnCount(self, parent=None):
819 def columnHeader(self, column):
820 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
821 return headers[column]
823 def columnAlignment(self, column):
824 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
825 return alignment[column]
827 def DoFindSelect(self, query, match):
828 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
830 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
831 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
832 " WHERE symbols.name" + match +
833 " ORDER BY comm_id, thread_id, call_time, calls.id")
835 def FindPath(self, query):
836 # Turn the query result into a list of ids that the tree view can walk
837 # to open the tree at the right place.
839 parent_id = query.value(0)
841 ids.insert(0, parent_id)
842 q2 = QSqlQuery(self.glb.db)
843 QueryExec(q2, "SELECT parent_id"
845 " WHERE id = " + str(parent_id))
848 parent_id = q2.value(0)
849 ids.insert(0, query.value(2))
850 ids.insert(0, query.value(1))
853 # Vertical widget layout
857 def __init__(self, w1, w2, w3=None):
858 self.vbox = QWidget()
859 self.vbox.setLayout(QVBoxLayout());
861 self.vbox.layout().setContentsMargins(0, 0, 0, 0)
863 self.vbox.layout().addWidget(w1)
864 self.vbox.layout().addWidget(w2)
866 self.vbox.layout().addWidget(w3)
873 class TreeWindowBase(QMdiSubWindow):
875 def __init__(self, parent=None):
876 super(TreeWindowBase, self).__init__(parent)
882 def DisplayFound(self, ids):
885 parent = QModelIndex()
888 n = self.model.rowCount(parent)
889 for row in xrange(n):
890 child = self.model.index(row, 0, parent)
891 if child.internalPointer().dbid == dbid:
893 self.view.setCurrentIndex(child)
900 def Find(self, value, direction, pattern, context):
903 self.model.Find(value, direction, pattern, context, self.FindDone)
905 def FindDone(self, ids):
907 if not self.DisplayFound(ids):
911 self.find_bar.NotFound()
914 # Context-sensitive call graph window
916 class CallGraphWindow(TreeWindowBase):
918 def __init__(self, glb, parent=None):
919 super(CallGraphWindow, self).__init__(parent)
921 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
923 self.view = QTreeView()
924 self.view.setModel(self.model)
926 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
927 self.view.setColumnWidth(c, w)
929 self.find_bar = FindBar(self, self)
931 self.vbox = VBox(self.view, self.find_bar.Widget())
933 self.setWidget(self.vbox.Widget())
935 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
939 class CallTreeWindow(TreeWindowBase):
941 def __init__(self, glb, parent=None):
942 super(CallTreeWindow, self).__init__(parent)
944 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
946 self.view = QTreeView()
947 self.view.setModel(self.model)
949 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
950 self.view.setColumnWidth(c, w)
952 self.find_bar = FindBar(self, self)
954 self.vbox = VBox(self.view, self.find_bar.Widget())
956 self.setWidget(self.vbox.Widget())
958 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
960 # Child data item finder
962 class ChildDataItemFinder():
964 def __init__(self, root):
966 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
970 def FindSelect(self):
973 pattern = re.compile(self.value)
974 for child in self.root.child_items:
975 for column_data in child.data:
976 if re.search(pattern, str(column_data)) is not None:
977 self.rows.append(child.row)
980 for child in self.root.child_items:
981 for column_data in child.data:
982 if self.value in str(column_data):
983 self.rows.append(child.row)
988 if self.last_value != self.value or self.pattern != self.last_pattern:
990 if not len(self.rows):
992 return self.rows[self.pos]
994 def FindThread(self):
995 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
996 row = self.FindValue()
998 if self.direction > 0:
1000 if self.pos >= len(self.rows):
1005 self.pos = len(self.rows) - 1
1006 row = self.rows[self.pos]
1011 def Find(self, value, direction, pattern, context, callback):
1012 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
1013 # Use a thread so the UI is not blocked
1014 thread = Thread(self.FindThread)
1015 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
1018 def FindDone(self, thread, callback, row):
1021 # Number of database records to fetch in one go
1023 glb_chunk_sz = 10000
1025 # Background process for SQL data fetcher
1027 class SQLFetcherProcess():
1029 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
1030 # Need a unique connection name
1031 conn_name = "SQLFetcher" + str(os.getpid())
1032 self.db, dbname = dbref.Open(conn_name)
1034 self.buffer = buffer
1037 self.fetch_count = fetch_count
1038 self.fetching_done = fetching_done
1039 self.process_target = process_target
1040 self.wait_event = wait_event
1041 self.fetched_event = fetched_event
1043 self.query = QSqlQuery(self.db)
1044 self.query_limit = 0 if "$$last_id$$" in sql else 2
1048 self.local_head = self.head.value
1049 self.local_tail = self.tail.value
1052 if self.query_limit:
1053 if self.query_limit == 1:
1055 self.query_limit -= 1
1056 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
1057 QueryExec(self.query, stmt)
1060 if not self.query.next():
1062 if not self.query.next():
1064 self.last_id = self.query.value(0)
1065 return self.prep(self.query)
1067 def WaitForTarget(self):
1069 self.wait_event.clear()
1070 target = self.process_target.value
1071 if target > self.fetched or target < 0:
1073 self.wait_event.wait()
1076 def HasSpace(self, sz):
1077 if self.local_tail <= self.local_head:
1078 space = len(self.buffer) - self.local_head
1081 if space >= glb_nsz:
1082 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
1083 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
1084 self.buffer[self.local_head : self.local_head + len(nd)] = nd
1086 if self.local_tail - self.local_head > sz:
1090 def WaitForSpace(self, sz):
1091 if self.HasSpace(sz):
1094 self.wait_event.clear()
1095 self.local_tail = self.tail.value
1096 if self.HasSpace(sz):
1098 self.wait_event.wait()
1100 def AddToBuffer(self, obj):
1101 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
1103 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
1105 self.WaitForSpace(sz)
1106 pos = self.local_head
1107 self.buffer[pos : pos + len(nd)] = nd
1108 self.buffer[pos + glb_nsz : pos + sz] = d
1109 self.local_head += sz
1111 def FetchBatch(self, batch_size):
1113 while batch_size > fetched:
1118 self.AddToBuffer(obj)
1121 self.fetched += fetched
1122 with self.fetch_count.get_lock():
1123 self.fetch_count.value += fetched
1124 self.head.value = self.local_head
1125 self.fetched_event.set()
1129 target = self.WaitForTarget()
1132 batch_size = min(glb_chunk_sz, target - self.fetched)
1133 self.FetchBatch(batch_size)
1134 self.fetching_done.value = True
1135 self.fetched_event.set()
1137 def SQLFetcherFn(*x):
1138 process = SQLFetcherProcess(*x)
1143 class SQLFetcher(QObject):
1145 done = Signal(object)
1147 def __init__(self, glb, sql, prep, process_data, parent=None):
1148 super(SQLFetcher, self).__init__(parent)
1149 self.process_data = process_data
1152 self.last_target = 0
1154 self.buffer_size = 16 * 1024 * 1024
1155 self.buffer = Array(c_char, self.buffer_size, lock=False)
1156 self.head = Value(c_longlong)
1157 self.tail = Value(c_longlong)
1159 self.fetch_count = Value(c_longlong)
1160 self.fetching_done = Value(c_bool)
1162 self.process_target = Value(c_longlong)
1163 self.wait_event = Event()
1164 self.fetched_event = Event()
1165 glb.AddInstanceToShutdownOnExit(self)
1166 self.process = Process(target=SQLFetcherFn, args=(glb.dbref, sql, self.buffer, self.head, self.tail, self.fetch_count, self.fetching_done, self.process_target, self.wait_event, self.fetched_event, prep))
1167 self.process.start()
1168 self.thread = Thread(self.Thread)
1169 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
1173 # Tell the thread and process to exit
1174 self.process_target.value = -1
1175 self.wait_event.set()
1177 self.fetching_done.value = True
1178 self.fetched_event.set()
1184 self.fetched_event.clear()
1185 fetch_count = self.fetch_count.value
1186 if fetch_count != self.last_count:
1188 if self.fetching_done.value:
1191 self.fetched_event.wait()
1192 count = fetch_count - self.last_count
1193 self.last_count = fetch_count
1194 self.fetched += count
1197 def Fetch(self, nr):
1199 # -1 inidcates there are no more
1201 result = self.fetched
1202 extra = result + nr - self.target
1204 self.target += extra
1205 # process_target < 0 indicates shutting down
1206 if self.process_target.value >= 0:
1207 self.process_target.value = self.target
1208 self.wait_event.set()
1211 def RemoveFromBuffer(self):
1212 pos = self.local_tail
1213 if len(self.buffer) - pos < glb_nsz:
1215 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
1218 n = pickle.loads(self.buffer[0 : glb_nsz])
1220 obj = pickle.loads(self.buffer[pos : pos + n])
1221 self.local_tail = pos + n
1224 def ProcessData(self, count):
1225 for i in xrange(count):
1226 obj = self.RemoveFromBuffer()
1227 self.process_data(obj)
1228 self.tail.value = self.local_tail
1229 self.wait_event.set()
1230 self.done.emit(count)
1232 # Fetch more records bar
1234 class FetchMoreRecordsBar():
1236 def __init__(self, model, parent):
1239 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
1240 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1242 self.fetch_count = QSpinBox()
1243 self.fetch_count.setRange(1, 1000000)
1244 self.fetch_count.setValue(10)
1245 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1247 self.fetch = QPushButton("Go!")
1248 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1249 self.fetch.released.connect(self.FetchMoreRecords)
1251 self.progress = QProgressBar()
1252 self.progress.setRange(0, 100)
1253 self.progress.hide()
1255 self.done_label = QLabel("All records fetched")
1256 self.done_label.hide()
1258 self.spacer = QLabel("")
1260 self.close_button = QToolButton()
1261 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
1262 self.close_button.released.connect(self.Deactivate)
1264 self.hbox = QHBoxLayout()
1265 self.hbox.setContentsMargins(0, 0, 0, 0)
1267 self.hbox.addWidget(self.label)
1268 self.hbox.addWidget(self.fetch_count)
1269 self.hbox.addWidget(self.fetch)
1270 self.hbox.addWidget(self.spacer)
1271 self.hbox.addWidget(self.progress)
1272 self.hbox.addWidget(self.done_label)
1273 self.hbox.addWidget(self.close_button)
1275 self.bar = QWidget()
1276 self.bar.setLayout(self.hbox);
1279 self.in_progress = False
1280 self.model.progress.connect(self.Progress)
1284 if not model.HasMoreRecords():
1292 self.fetch.setFocus()
1294 def Deactivate(self):
1297 def Enable(self, enable):
1298 self.fetch.setEnabled(enable)
1299 self.fetch_count.setEnabled(enable)
1305 self.progress.show()
1308 self.in_progress = False
1310 self.progress.hide()
1315 return self.fetch_count.value() * glb_chunk_sz
1321 self.fetch_count.hide()
1324 self.done_label.show()
1326 def Progress(self, count):
1327 if self.in_progress:
1329 percent = ((count - self.start) * 100) / self.Target()
1333 self.progress.setValue(percent)
1335 # Count value of zero means no more records
1338 def FetchMoreRecords(self):
1341 self.progress.setValue(0)
1343 self.in_progress = True
1344 self.start = self.model.FetchMoreRecords(self.Target())
1346 # Brance data model level two item
1348 class BranchLevelTwoItem():
1350 def __init__(self, row, text, parent_item):
1352 self.parent_item = parent_item
1353 self.data = [""] * 8
1357 def getParentItem(self):
1358 return self.parent_item
1363 def childCount(self):
1366 def hasChildren(self):
1369 def getData(self, column):
1370 return self.data[column]
1372 # Brance data model level one item
1374 class BranchLevelOneItem():
1376 def __init__(self, glb, row, data, parent_item):
1379 self.parent_item = parent_item
1380 self.child_count = 0
1381 self.child_items = []
1382 self.data = data[1:]
1385 self.query_done = False
1387 def getChildItem(self, row):
1388 return self.child_items[row]
1390 def getParentItem(self):
1391 return self.parent_item
1397 self.query_done = True
1399 if not self.glb.have_disassembler:
1402 query = QSqlQuery(self.glb.db)
1404 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
1406 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
1407 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
1408 " WHERE samples.id = " + str(self.dbid))
1409 if not query.next():
1411 cpu = query.value(0)
1412 dso = query.value(1)
1413 sym = query.value(2)
1414 if dso == 0 or sym == 0:
1416 off = query.value(3)
1417 short_name = query.value(4)
1418 long_name = query.value(5)
1419 build_id = query.value(6)
1420 sym_start = query.value(7)
1423 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
1425 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
1426 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
1427 " ORDER BY samples.id"
1429 if not query.next():
1431 if query.value(0) != dso:
1432 # Cannot disassemble from one dso to another
1434 bsym = query.value(1)
1435 boff = query.value(2)
1436 bsym_start = query.value(3)
1439 tot = bsym_start + boff + 1 - sym_start - off
1440 if tot <= 0 or tot > 16384:
1443 inst = self.glb.disassembler.Instruction()
1444 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
1447 mode = 0 if Is64Bit(f) else 1
1448 self.glb.disassembler.SetMode(inst, mode)
1451 buf = create_string_buffer(tot + 16)
1452 f.seek(sym_start + off)
1453 buf.value = f.read(buf_sz)
1454 buf_ptr = addressof(buf)
1457 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
1459 byte_str = tohex(ip).rjust(16)
1460 for k in xrange(cnt):
1461 byte_str += " %02x" % ord(buf[i])
1466 self.child_items.append(BranchLevelTwoItem(0, byte_str + " " + text, self))
1467 self.child_count += 1
1475 def childCount(self):
1476 if not self.query_done:
1478 if not self.child_count:
1480 return self.child_count
1482 def hasChildren(self):
1483 if not self.query_done:
1485 return self.child_count > 0
1487 def getData(self, column):
1488 return self.data[column]
1490 # Brance data model root item
1492 class BranchRootItem():
1495 self.child_count = 0
1496 self.child_items = []
1499 def getChildItem(self, row):
1500 return self.child_items[row]
1502 def getParentItem(self):
1508 def childCount(self):
1509 return self.child_count
1511 def hasChildren(self):
1512 return self.child_count > 0
1514 def getData(self, column):
1517 # Branch data preparation
1519 def BranchDataPrep(query):
1521 for i in xrange(0, 8):
1522 data.append(query.value(i))
1523 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1524 " (" + dsoname(query.value(11)) + ")" + " -> " +
1525 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1526 " (" + dsoname(query.value(15)) + ")")
1531 class BranchModel(TreeModel):
1533 progress = Signal(object)
1535 def __init__(self, glb, event_id, where_clause, parent=None):
1536 super(BranchModel, self).__init__(glb, parent)
1537 self.event_id = event_id
1540 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
1541 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
1542 " ip, symbols.name, sym_offset, dsos.short_name,"
1543 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
1545 " INNER JOIN comms ON comm_id = comms.id"
1546 " INNER JOIN threads ON thread_id = threads.id"
1547 " INNER JOIN branch_types ON branch_type = branch_types.id"
1548 " INNER JOIN symbols ON symbol_id = symbols.id"
1549 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
1550 " INNER JOIN dsos ON samples.dso_id = dsos.id"
1551 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
1552 " WHERE samples.id > $$last_id$$" + where_clause +
1553 " AND evsel_id = " + str(self.event_id) +
1554 " ORDER BY samples.id"
1555 " LIMIT " + str(glb_chunk_sz))
1556 self.fetcher = SQLFetcher(glb, sql, BranchDataPrep, self.AddSample)
1557 self.fetcher.done.connect(self.Update)
1558 self.fetcher.Fetch(glb_chunk_sz)
1561 return BranchRootItem()
1563 def columnCount(self, parent=None):
1566 def columnHeader(self, column):
1567 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
1569 def columnFont(self, column):
1572 return QFont("Monospace")
1574 def DisplayData(self, item, index):
1576 self.FetchIfNeeded(item.row)
1577 return item.getData(index.column())
1579 def AddSample(self, data):
1580 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
1581 self.root.child_items.append(child)
1584 def Update(self, fetched):
1587 self.progress.emit(0)
1588 child_count = self.root.child_count
1589 count = self.populated - child_count
1591 parent = QModelIndex()
1592 self.beginInsertRows(parent, child_count, child_count + count - 1)
1593 self.insertRows(child_count, count, parent)
1594 self.root.child_count += count
1595 self.endInsertRows()
1596 self.progress.emit(self.root.child_count)
1598 def FetchMoreRecords(self, count):
1599 current = self.root.child_count
1601 self.fetcher.Fetch(count)
1603 self.progress.emit(0)
1606 def HasMoreRecords(self):
1613 def __init__(self, name = "", where_clause = "", limit = ""):
1615 self.where_clause = where_clause
1619 return str(self.where_clause + ";" + self.limit)
1623 class BranchWindow(QMdiSubWindow):
1625 def __init__(self, glb, event_id, report_vars, parent=None):
1626 super(BranchWindow, self).__init__(parent)
1628 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
1630 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
1632 self.view = QTreeView()
1633 self.view.setUniformRowHeights(True)
1634 self.view.setModel(self.model)
1636 self.ResizeColumnsToContents()
1638 self.find_bar = FindBar(self, self, True)
1640 self.finder = ChildDataItemFinder(self.model.root)
1642 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
1644 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
1646 self.setWidget(self.vbox.Widget())
1648 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
1650 def ResizeColumnToContents(self, column, n):
1651 # Using the view's resizeColumnToContents() here is extrememly slow
1652 # so implement a crude alternative
1653 mm = "MM" if column else "MMMM"
1654 font = self.view.font()
1655 metrics = QFontMetrics(font)
1657 for row in xrange(n):
1658 val = self.model.root.child_items[row].data[column]
1659 len = metrics.width(str(val) + mm)
1660 max = len if len > max else max
1661 val = self.model.columnHeader(column)
1662 len = metrics.width(str(val) + mm)
1663 max = len if len > max else max
1664 self.view.setColumnWidth(column, max)
1666 def ResizeColumnsToContents(self):
1667 n = min(self.model.root.child_count, 100)
1669 # No data yet, so connect a signal to notify when there is
1670 self.model.rowsInserted.connect(self.UpdateColumnWidths)
1672 columns = self.model.columnCount()
1673 for i in xrange(columns):
1674 self.ResizeColumnToContents(i, n)
1676 def UpdateColumnWidths(self, *x):
1677 # This only needs to be done once, so disconnect the signal now
1678 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
1679 self.ResizeColumnsToContents()
1681 def Find(self, value, direction, pattern, context):
1682 self.view.setFocus()
1683 self.find_bar.Busy()
1684 self.finder.Find(value, direction, pattern, context, self.FindDone)
1686 def FindDone(self, row):
1687 self.find_bar.Idle()
1689 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
1691 self.find_bar.NotFound()
1693 # Line edit data item
1695 class LineEditDataItem(object):
1697 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1700 self.placeholder_text = placeholder_text
1701 self.parent = parent
1704 self.value = default
1706 self.widget = QLineEdit(default)
1707 self.widget.editingFinished.connect(self.Validate)
1708 self.widget.textChanged.connect(self.Invalidate)
1711 self.validated = True
1713 if placeholder_text:
1714 self.widget.setPlaceholderText(placeholder_text)
1716 def TurnTextRed(self):
1718 palette = QPalette()
1719 palette.setColor(QPalette.Text,Qt.red)
1720 self.widget.setPalette(palette)
1723 def TurnTextNormal(self):
1725 palette = QPalette()
1726 self.widget.setPalette(palette)
1729 def InvalidValue(self, value):
1732 self.error = self.label + " invalid value '" + value + "'"
1733 self.parent.ShowMessage(self.error)
1735 def Invalidate(self):
1736 self.validated = False
1738 def DoValidate(self, input_string):
1739 self.value = input_string.strip()
1742 self.validated = True
1744 self.TurnTextNormal()
1745 self.parent.ClearMessage()
1746 input_string = self.widget.text()
1747 if not len(input_string.strip()):
1750 self.DoValidate(input_string)
1753 if not self.validated:
1756 self.parent.ShowMessage(self.error)
1760 def IsNumber(self, value):
1765 return str(x) == value
1767 # Non-negative integer ranges dialog data item
1769 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
1771 def __init__(self, glb, label, placeholder_text, column_name, parent):
1772 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1774 self.column_name = column_name
1776 def DoValidate(self, input_string):
1779 for value in [x.strip() for x in input_string.split(",")]:
1781 vrange = value.split("-")
1782 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1783 return self.InvalidValue(value)
1784 ranges.append(vrange)
1786 if not self.IsNumber(value):
1787 return self.InvalidValue(value)
1788 singles.append(value)
1789 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1791 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
1792 self.value = " OR ".join(ranges)
1794 # Positive integer dialog data item
1796 class PositiveIntegerDataItem(LineEditDataItem):
1798 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1799 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
1801 def DoValidate(self, input_string):
1802 if not self.IsNumber(input_string.strip()):
1803 return self.InvalidValue(input_string)
1804 value = int(input_string.strip())
1806 return self.InvalidValue(input_string)
1807 self.value = str(value)
1809 # Dialog data item converted and validated using a SQL table
1811 class SQLTableDataItem(LineEditDataItem):
1813 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
1814 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
1816 self.table_name = table_name
1817 self.match_column = match_column
1818 self.column_name1 = column_name1
1819 self.column_name2 = column_name2
1821 def ValueToIds(self, value):
1823 query = QSqlQuery(self.glb.db)
1824 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
1825 ret = query.exec_(stmt)
1828 ids.append(str(query.value(0)))
1831 def DoValidate(self, input_string):
1833 for value in [x.strip() for x in input_string.split(",")]:
1834 ids = self.ValueToIds(value)
1838 return self.InvalidValue(value)
1839 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
1840 if self.column_name2:
1841 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
1843 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
1845 class SampleTimeRangesDataItem(LineEditDataItem):
1847 def __init__(self, glb, label, placeholder_text, column_name, parent):
1848 self.column_name = column_name
1852 self.last_time = 2 ** 64
1854 query = QSqlQuery(glb.db)
1855 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
1857 self.last_id = int(query.value(0))
1858 self.last_time = int(query.value(1))
1859 QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1")
1861 self.first_time = int(query.value(0))
1862 if placeholder_text:
1863 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
1865 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1867 def IdBetween(self, query, lower_id, higher_id, order):
1868 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
1870 return True, int(query.value(0))
1874 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
1875 query = QSqlQuery(self.glb.db)
1877 next_id = int((lower_id + higher_id) / 2)
1878 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1879 if not query.next():
1880 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
1882 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
1884 return str(higher_id)
1886 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1887 next_time = int(query.value(0))
1889 if target_time > next_time:
1893 if higher_id <= lower_id + 1:
1894 return str(higher_id)
1896 if target_time >= next_time:
1900 if higher_id <= lower_id + 1:
1901 return str(lower_id)
1903 def ConvertRelativeTime(self, val):
1908 elif suffix == "us":
1910 elif suffix == "ns":
1914 val = val[:-2].strip()
1915 if not self.IsNumber(val):
1917 val = int(val) * mult
1919 val += self.first_time
1921 val += self.last_time
1924 def ConvertTimeRange(self, vrange):
1926 vrange[0] = str(self.first_time)
1928 vrange[1] = str(self.last_time)
1929 vrange[0] = self.ConvertRelativeTime(vrange[0])
1930 vrange[1] = self.ConvertRelativeTime(vrange[1])
1931 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1933 beg_range = max(int(vrange[0]), self.first_time)
1934 end_range = min(int(vrange[1]), self.last_time)
1935 if beg_range > self.last_time or end_range < self.first_time:
1937 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
1938 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
1941 def AddTimeRange(self, value, ranges):
1942 n = value.count("-")
1946 if value.split("-")[1].strip() == "":
1952 pos = findnth(value, "-", n)
1953 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
1954 if self.ConvertTimeRange(vrange):
1955 ranges.append(vrange)
1959 def DoValidate(self, input_string):
1961 for value in [x.strip() for x in input_string.split(",")]:
1962 if not self.AddTimeRange(value, ranges):
1963 return self.InvalidValue(value)
1964 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1965 self.value = " OR ".join(ranges)
1967 # Report Dialog Base
1969 class ReportDialogBase(QDialog):
1971 def __init__(self, glb, title, items, partial, parent=None):
1972 super(ReportDialogBase, self).__init__(parent)
1976 self.report_vars = ReportVars()
1978 self.setWindowTitle(title)
1979 self.setMinimumWidth(600)
1981 self.data_items = [x(glb, self) for x in items]
1983 self.partial = partial
1985 self.grid = QGridLayout()
1987 for row in xrange(len(self.data_items)):
1988 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
1989 self.grid.addWidget(self.data_items[row].widget, row, 1)
1991 self.status = QLabel()
1993 self.ok_button = QPushButton("Ok", self)
1994 self.ok_button.setDefault(True)
1995 self.ok_button.released.connect(self.Ok)
1996 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1998 self.cancel_button = QPushButton("Cancel", self)
1999 self.cancel_button.released.connect(self.reject)
2000 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2002 self.hbox = QHBoxLayout()
2003 #self.hbox.addStretch()
2004 self.hbox.addWidget(self.status)
2005 self.hbox.addWidget(self.ok_button)
2006 self.hbox.addWidget(self.cancel_button)
2008 self.vbox = QVBoxLayout()
2009 self.vbox.addLayout(self.grid)
2010 self.vbox.addLayout(self.hbox)
2012 self.setLayout(self.vbox);
2015 vars = self.report_vars
2016 for d in self.data_items:
2017 if d.id == "REPORTNAME":
2020 self.ShowMessage("Report name is required")
2022 for d in self.data_items:
2025 for d in self.data_items[1:]:
2027 vars.limit = d.value
2029 if len(vars.where_clause):
2030 vars.where_clause += " AND "
2031 vars.where_clause += d.value
2032 if len(vars.where_clause):
2034 vars.where_clause = " AND ( " + vars.where_clause + " ) "
2036 vars.where_clause = " WHERE " + vars.where_clause + " "
2039 def ShowMessage(self, msg):
2040 self.status.setText("<font color=#FF0000>" + msg)
2042 def ClearMessage(self):
2043 self.status.setText("")
2045 # Selected branch report creation dialog
2047 class SelectedBranchDialog(ReportDialogBase):
2049 def __init__(self, glb, parent=None):
2050 title = "Selected Branches"
2051 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2052 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
2053 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
2054 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
2055 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2056 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2057 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
2058 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
2059 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
2060 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
2064 def GetEventList(db):
2066 query = QSqlQuery(db)
2067 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
2069 events.append(query.value(0))
2072 # Is a table selectable
2074 def IsSelectable(db, table, sql = ""):
2075 query = QSqlQuery(db)
2077 QueryExec(query, "SELECT * FROM " + table + " " + sql + " LIMIT 1")
2082 # SQL data preparation
2084 def SQLTableDataPrep(query, count):
2086 for i in xrange(count):
2087 data.append(query.value(i))
2090 # SQL table data model item
2092 class SQLTableItem():
2094 def __init__(self, row, data):
2098 def getData(self, column):
2099 return self.data[column]
2101 # SQL table data model
2103 class SQLTableModel(TableModel):
2105 progress = Signal(object)
2107 def __init__(self, glb, sql, column_headers, parent=None):
2108 super(SQLTableModel, self).__init__(parent)
2112 self.column_headers = column_headers
2113 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): SQLTableDataPrep(x, y), self.AddSample)
2114 self.fetcher.done.connect(self.Update)
2115 self.fetcher.Fetch(glb_chunk_sz)
2117 def DisplayData(self, item, index):
2118 self.FetchIfNeeded(item.row)
2119 return item.getData(index.column())
2121 def AddSample(self, data):
2122 child = SQLTableItem(self.populated, data)
2123 self.child_items.append(child)
2126 def Update(self, fetched):
2129 self.progress.emit(0)
2130 child_count = self.child_count
2131 count = self.populated - child_count
2133 parent = QModelIndex()
2134 self.beginInsertRows(parent, child_count, child_count + count - 1)
2135 self.insertRows(child_count, count, parent)
2136 self.child_count += count
2137 self.endInsertRows()
2138 self.progress.emit(self.child_count)
2140 def FetchMoreRecords(self, count):
2141 current = self.child_count
2143 self.fetcher.Fetch(count)
2145 self.progress.emit(0)
2148 def HasMoreRecords(self):
2151 def columnCount(self, parent=None):
2152 return len(self.column_headers)
2154 def columnHeader(self, column):
2155 return self.column_headers[column]
2157 # SQL automatic table data model
2159 class SQLAutoTableModel(SQLTableModel):
2161 def __init__(self, glb, table_name, parent=None):
2162 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
2163 if table_name == "comm_threads_view":
2164 # For now, comm_threads_view has no id column
2165 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
2167 query = QSqlQuery(glb.db)
2168 if glb.dbref.is_sqlite3:
2169 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
2171 column_headers.append(query.value(1))
2172 if table_name == "sqlite_master":
2173 sql = "SELECT * FROM " + table_name
2175 if table_name[:19] == "information_schema.":
2176 sql = "SELECT * FROM " + table_name
2177 select_table_name = table_name[19:]
2178 schema = "information_schema"
2180 select_table_name = table_name
2182 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
2184 column_headers.append(query.value(0))
2185 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
2187 # Base class for custom ResizeColumnsToContents
2189 class ResizeColumnsToContentsBase(QObject):
2191 def __init__(self, parent=None):
2192 super(ResizeColumnsToContentsBase, self).__init__(parent)
2194 def ResizeColumnToContents(self, column, n):
2195 # Using the view's resizeColumnToContents() here is extrememly slow
2196 # so implement a crude alternative
2197 font = self.view.font()
2198 metrics = QFontMetrics(font)
2200 for row in xrange(n):
2201 val = self.data_model.child_items[row].data[column]
2202 len = metrics.width(str(val) + "MM")
2203 max = len if len > max else max
2204 val = self.data_model.columnHeader(column)
2205 len = metrics.width(str(val) + "MM")
2206 max = len if len > max else max
2207 self.view.setColumnWidth(column, max)
2209 def ResizeColumnsToContents(self):
2210 n = min(self.data_model.child_count, 100)
2212 # No data yet, so connect a signal to notify when there is
2213 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
2215 columns = self.data_model.columnCount()
2216 for i in xrange(columns):
2217 self.ResizeColumnToContents(i, n)
2219 def UpdateColumnWidths(self, *x):
2220 # This only needs to be done once, so disconnect the signal now
2221 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
2222 self.ResizeColumnsToContents()
2226 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2228 def __init__(self, glb, table_name, parent=None):
2229 super(TableWindow, self).__init__(parent)
2231 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
2233 self.model = QSortFilterProxyModel()
2234 self.model.setSourceModel(self.data_model)
2236 self.view = QTableView()
2237 self.view.setModel(self.model)
2238 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2239 self.view.verticalHeader().setVisible(False)
2240 self.view.sortByColumn(-1, Qt.AscendingOrder)
2241 self.view.setSortingEnabled(True)
2243 self.ResizeColumnsToContents()
2245 self.find_bar = FindBar(self, self, True)
2247 self.finder = ChildDataItemFinder(self.data_model)
2249 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2251 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2253 self.setWidget(self.vbox.Widget())
2255 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
2257 def Find(self, value, direction, pattern, context):
2258 self.view.setFocus()
2259 self.find_bar.Busy()
2260 self.finder.Find(value, direction, pattern, context, self.FindDone)
2262 def FindDone(self, row):
2263 self.find_bar.Idle()
2265 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
2267 self.find_bar.NotFound()
2271 def GetTableList(glb):
2273 query = QSqlQuery(glb.db)
2274 if glb.dbref.is_sqlite3:
2275 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
2277 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
2279 tables.append(query.value(0))
2280 if glb.dbref.is_sqlite3:
2281 tables.append("sqlite_master")
2283 tables.append("information_schema.tables")
2284 tables.append("information_schema.views")
2285 tables.append("information_schema.columns")
2288 # Top Calls data model
2290 class TopCallsModel(SQLTableModel):
2292 def __init__(self, glb, report_vars, parent=None):
2294 if not glb.dbref.is_sqlite3:
2297 if len(report_vars.limit):
2298 limit = " LIMIT " + report_vars.limit
2299 sql = ("SELECT comm, pid, tid, name,"
2301 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
2304 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
2306 " WHEN (calls.flags = 1) THEN 'no call'" + text +
2307 " WHEN (calls.flags = 2) THEN 'no return'" + text +
2308 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
2312 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
2313 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
2314 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
2315 " INNER JOIN comms ON calls.comm_id = comms.id"
2316 " INNER JOIN threads ON calls.thread_id = threads.id" +
2317 report_vars.where_clause +
2318 " ORDER BY elapsed_time DESC" +
2321 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
2322 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
2323 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
2325 def columnAlignment(self, column):
2326 return self.alignment[column]
2328 # Top Calls report creation dialog
2330 class TopCallsDialog(ReportDialogBase):
2332 def __init__(self, glb, parent=None):
2333 title = "Top Calls by Elapsed Time"
2334 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2335 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
2336 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2337 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2338 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
2339 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
2340 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
2341 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
2342 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
2346 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2348 def __init__(self, glb, report_vars, parent=None):
2349 super(TopCallsWindow, self).__init__(parent)
2351 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
2352 self.model = self.data_model
2354 self.view = QTableView()
2355 self.view.setModel(self.model)
2356 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2357 self.view.verticalHeader().setVisible(False)
2359 self.ResizeColumnsToContents()
2361 self.find_bar = FindBar(self, self, True)
2363 self.finder = ChildDataItemFinder(self.model)
2365 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2367 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2369 self.setWidget(self.vbox.Widget())
2371 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
2373 def Find(self, value, direction, pattern, context):
2374 self.view.setFocus()
2375 self.find_bar.Busy()
2376 self.finder.Find(value, direction, pattern, context, self.FindDone)
2378 def FindDone(self, row):
2379 self.find_bar.Idle()
2381 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
2383 self.find_bar.NotFound()
2387 def CreateAction(label, tip, callback, parent=None, shortcut=None):
2388 action = QAction(label, parent)
2389 if shortcut != None:
2390 action.setShortcuts(shortcut)
2391 action.setStatusTip(tip)
2392 action.triggered.connect(callback)
2395 # Typical application actions
2397 def CreateExitAction(app, parent=None):
2398 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
2400 # Typical MDI actions
2402 def CreateCloseActiveWindowAction(mdi_area):
2403 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
2405 def CreateCloseAllWindowsAction(mdi_area):
2406 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
2408 def CreateTileWindowsAction(mdi_area):
2409 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
2411 def CreateCascadeWindowsAction(mdi_area):
2412 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
2414 def CreateNextWindowAction(mdi_area):
2415 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
2417 def CreatePreviousWindowAction(mdi_area):
2418 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
2420 # Typical MDI window menu
2424 def __init__(self, mdi_area, menu):
2425 self.mdi_area = mdi_area
2426 self.window_menu = menu.addMenu("&Windows")
2427 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
2428 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
2429 self.tile_windows = CreateTileWindowsAction(mdi_area)
2430 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
2431 self.next_window = CreateNextWindowAction(mdi_area)
2432 self.previous_window = CreatePreviousWindowAction(mdi_area)
2433 self.window_menu.aboutToShow.connect(self.Update)
2436 self.window_menu.clear()
2437 sub_window_count = len(self.mdi_area.subWindowList())
2438 have_sub_windows = sub_window_count != 0
2439 self.close_active_window.setEnabled(have_sub_windows)
2440 self.close_all_windows.setEnabled(have_sub_windows)
2441 self.tile_windows.setEnabled(have_sub_windows)
2442 self.cascade_windows.setEnabled(have_sub_windows)
2443 self.next_window.setEnabled(have_sub_windows)
2444 self.previous_window.setEnabled(have_sub_windows)
2445 self.window_menu.addAction(self.close_active_window)
2446 self.window_menu.addAction(self.close_all_windows)
2447 self.window_menu.addSeparator()
2448 self.window_menu.addAction(self.tile_windows)
2449 self.window_menu.addAction(self.cascade_windows)
2450 self.window_menu.addSeparator()
2451 self.window_menu.addAction(self.next_window)
2452 self.window_menu.addAction(self.previous_window)
2453 if sub_window_count == 0:
2455 self.window_menu.addSeparator()
2457 for sub_window in self.mdi_area.subWindowList():
2458 label = str(nr) + " " + sub_window.name
2461 action = self.window_menu.addAction(label)
2462 action.setCheckable(True)
2463 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
2464 action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
2465 self.window_menu.addAction(action)
2468 def setActiveSubWindow(self, nr):
2469 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
2484 <p class=c1><a href=#reports>1. Reports</a></p>
2485 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
2486 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
2487 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
2488 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
2489 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
2490 <p class=c1><a href=#tables>2. Tables</a></p>
2491 <h1 id=reports>1. Reports</h1>
2492 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
2493 The result is a GUI window with a tree representing a context-sensitive
2494 call-graph. Expanding a couple of levels of the tree and adjusting column
2495 widths to suit will display something like:
2497 Call Graph: pt_example
2498 Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
2501 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
2502 |- unknown unknown 1 13198 0.1 1 0.0
2503 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
2504 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
2505 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
2506 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
2507 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
2508 >- __libc_csu_init ls 1 10354 0.1 10 0.0
2509 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
2510 v- main ls 1 8182043 99.6 180254 99.9
2512 <h3>Points to note:</h3>
2514 <li>The top level is a command name (comm)</li>
2515 <li>The next level is a thread (pid:tid)</li>
2516 <li>Subsequent levels are functions</li>
2517 <li>'Count' is the number of calls</li>
2518 <li>'Time' is the elapsed time until the function returns</li>
2519 <li>Percentages are relative to the level above</li>
2520 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
2523 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
2524 The pattern matching symbols are ? for any character and * for zero or more characters.
2525 <h2 id=calltree>1.2 Call Tree</h2>
2526 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
2527 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
2528 <h2 id=allbranches>1.3 All branches</h2>
2529 The All branches report displays all branches in chronological order.
2530 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
2531 <h3>Disassembly</h3>
2532 Open a branch to display disassembly. This only works if:
2534 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
2535 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
2536 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
2537 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
2538 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
2540 <h4 id=xed>Intel XED Setup</h4>
2541 To use Intel XED, libxed.so must be present. To build and install libxed.so:
2543 git clone https://github.com/intelxed/mbuild.git mbuild
2544 git clone https://github.com/intelxed/xed
2547 sudo ./mfile.py --prefix=/usr/local install
2551 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2552 Refer to Python documentation for the regular expression syntax.
2553 All columns are searched, but only currently fetched rows are searched.
2554 <h2 id=selectedbranches>1.4 Selected branches</h2>
2555 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
2556 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2557 <h3>1.4.1 Time ranges</h3>
2558 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
2559 ms, us or ns. Also, negative values are relative to the end of trace. Examples:
2561 81073085947329-81073085958238 From 81073085947329 to 81073085958238
2562 100us-200us From 100us to 200us
2563 10ms- From 10ms to the end
2564 -100ns The first 100ns
2565 -10ms- The last 10ms
2567 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
2568 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
2569 The Top calls by elapsed time report displays calls in descending order of time elapsed between when the function was called and when it returned.
2570 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2571 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
2572 <h1 id=tables>2. Tables</h1>
2573 The Tables menu shows all tables and views in the database. Most tables have an associated view
2574 which displays the information in a more friendly way. Not all data for large tables is fetched
2575 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
2576 but that can be slow for large tables.
2577 <p>There are also tables of database meta-information.
2578 For SQLite3 databases, the sqlite_master table is included.
2579 For PostgreSQL databases, information_schema.tables/views/columns are included.
2581 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2582 Refer to Python documentation for the regular expression syntax.
2583 All columns are searched, but only currently fetched rows are searched.
2584 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
2585 will go to the next/previous result in id order, instead of display order.
2590 class HelpWindow(QMdiSubWindow):
2592 def __init__(self, glb, parent=None):
2593 super(HelpWindow, self).__init__(parent)
2595 self.text = QTextBrowser()
2596 self.text.setHtml(glb_help_text)
2597 self.text.setReadOnly(True)
2598 self.text.setOpenExternalLinks(True)
2600 self.setWidget(self.text)
2602 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
2604 # Main window that only displays the help text
2606 class HelpOnlyWindow(QMainWindow):
2608 def __init__(self, parent=None):
2609 super(HelpOnlyWindow, self).__init__(parent)
2611 self.setMinimumSize(200, 100)
2612 self.resize(800, 600)
2613 self.setWindowTitle("Exported SQL Viewer Help")
2614 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
2616 self.text = QTextBrowser()
2617 self.text.setHtml(glb_help_text)
2618 self.text.setReadOnly(True)
2619 self.text.setOpenExternalLinks(True)
2621 self.setCentralWidget(self.text)
2625 def ResizeFont(widget, diff):
2626 font = widget.font()
2627 sz = font.pointSize()
2628 font.setPointSize(sz + diff)
2629 widget.setFont(font)
2631 def ShrinkFont(widget):
2632 ResizeFont(widget, -1)
2634 def EnlargeFont(widget):
2635 ResizeFont(widget, 1)
2637 # Unique name for sub-windows
2639 def NumberedWindowName(name, nr):
2641 name += " <" + str(nr) + ">"
2644 def UniqueSubWindowName(mdi_area, name):
2647 unique_name = NumberedWindowName(name, nr)
2649 for sub_window in mdi_area.subWindowList():
2650 if sub_window.name == unique_name:
2659 def AddSubWindow(mdi_area, sub_window, name):
2660 unique_name = UniqueSubWindowName(mdi_area, name)
2661 sub_window.setMinimumSize(200, 100)
2662 sub_window.resize(800, 600)
2663 sub_window.setWindowTitle(unique_name)
2664 sub_window.setAttribute(Qt.WA_DeleteOnClose)
2665 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
2666 sub_window.name = unique_name
2667 mdi_area.addSubWindow(sub_window)
2672 class MainWindow(QMainWindow):
2674 def __init__(self, glb, parent=None):
2675 super(MainWindow, self).__init__(parent)
2679 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
2680 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
2681 self.setMinimumSize(200, 100)
2683 self.mdi_area = QMdiArea()
2684 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2685 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2687 self.setCentralWidget(self.mdi_area)
2689 menu = self.menuBar()
2691 file_menu = menu.addMenu("&File")
2692 file_menu.addAction(CreateExitAction(glb.app, self))
2694 edit_menu = menu.addMenu("&Edit")
2695 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
2696 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
2697 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
2698 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
2700 reports_menu = menu.addMenu("&Reports")
2701 if IsSelectable(glb.db, "calls"):
2702 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
2704 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
2705 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
2707 self.EventMenu(GetEventList(glb.db), reports_menu)
2709 if IsSelectable(glb.db, "calls"):
2710 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
2712 self.TableMenu(GetTableList(glb), menu)
2714 self.window_menu = WindowMenu(self.mdi_area, menu)
2716 help_menu = menu.addMenu("&Help")
2717 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
2720 win = self.mdi_area.activeSubWindow()
2723 win.find_bar.Activate()
2727 def FetchMoreRecords(self):
2728 win = self.mdi_area.activeSubWindow()
2731 win.fetch_bar.Activate()
2735 def ShrinkFont(self):
2736 win = self.mdi_area.activeSubWindow()
2737 ShrinkFont(win.view)
2739 def EnlargeFont(self):
2740 win = self.mdi_area.activeSubWindow()
2741 EnlargeFont(win.view)
2743 def EventMenu(self, events, reports_menu):
2745 for event in events:
2746 event = event.split(":")[0]
2747 if event == "branches":
2748 branches_events += 1
2750 for event in events:
2752 event = event.split(":")[0]
2753 if event == "branches":
2754 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
2755 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewBranchView(x), self))
2756 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
2757 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewSelectedBranchView(x), self))
2759 def TableMenu(self, tables, menu):
2760 table_menu = menu.addMenu("&Tables")
2761 for table in tables:
2762 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda t=table: self.NewTableView(t), self))
2764 def NewCallGraph(self):
2765 CallGraphWindow(self.glb, self)
2767 def NewCallTree(self):
2768 CallTreeWindow(self.glb, self)
2770 def NewTopCalls(self):
2771 dialog = TopCallsDialog(self.glb, self)
2772 ret = dialog.exec_()
2774 TopCallsWindow(self.glb, dialog.report_vars, self)
2776 def NewBranchView(self, event_id):
2777 BranchWindow(self.glb, event_id, ReportVars(), self)
2779 def NewSelectedBranchView(self, event_id):
2780 dialog = SelectedBranchDialog(self.glb, self)
2781 ret = dialog.exec_()
2783 BranchWindow(self.glb, event_id, dialog.report_vars, self)
2785 def NewTableView(self, table_name):
2786 TableWindow(self.glb, table_name, self)
2789 HelpWindow(self.glb, self)
2793 class xed_state_t(Structure):
2800 class XEDInstruction():
2802 def __init__(self, libxed):
2803 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
2804 xedd_t = c_byte * 512
2805 self.xedd = xedd_t()
2806 self.xedp = addressof(self.xedd)
2807 libxed.xed_decoded_inst_zero(self.xedp)
2808 self.state = xed_state_t()
2809 self.statep = addressof(self.state)
2810 # Buffer for disassembled instruction text
2811 self.buffer = create_string_buffer(256)
2812 self.bufferp = addressof(self.buffer)
2818 self.libxed = CDLL("libxed.so")
2822 self.libxed = CDLL("/usr/local/lib/libxed.so")
2824 self.xed_tables_init = self.libxed.xed_tables_init
2825 self.xed_tables_init.restype = None
2826 self.xed_tables_init.argtypes = []
2828 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
2829 self.xed_decoded_inst_zero.restype = None
2830 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
2832 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
2833 self.xed_operand_values_set_mode.restype = None
2834 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
2836 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
2837 self.xed_decoded_inst_zero_keep_mode.restype = None
2838 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
2840 self.xed_decode = self.libxed.xed_decode
2841 self.xed_decode.restype = c_int
2842 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
2844 self.xed_format_context = self.libxed.xed_format_context
2845 self.xed_format_context.restype = c_uint
2846 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
2848 self.xed_tables_init()
2850 def Instruction(self):
2851 return XEDInstruction(self)
2853 def SetMode(self, inst, mode):
2855 inst.state.mode = 4 # 32-bit
2856 inst.state.width = 4 # 4 bytes
2858 inst.state.mode = 1 # 64-bit
2859 inst.state.width = 8 # 8 bytes
2860 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
2862 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
2863 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
2864 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
2867 # Use AT&T mode (2), alternative is Intel (3)
2868 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
2871 # Return instruction length and the disassembled instruction text
2872 # For now, assume the length is in byte 166
2873 return inst.xedd[166], inst.buffer.value
2875 def TryOpen(file_name):
2877 return open(file_name, "rb")
2882 result = sizeof(c_void_p)
2889 eclass = ord(header[4])
2890 encoding = ord(header[5])
2891 version = ord(header[6])
2892 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
2893 result = True if eclass == 2 else False
2900 def __init__(self, dbref, db, dbname):
2903 self.dbname = dbname
2904 self.home_dir = os.path.expanduser("~")
2905 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
2906 if self.buildid_dir:
2907 self.buildid_dir += "/.build-id/"
2909 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
2911 self.mainwindow = None
2912 self.instances_to_shutdown_on_exit = weakref.WeakSet()
2914 self.disassembler = LibXED()
2915 self.have_disassembler = True
2917 self.have_disassembler = False
2919 def FileFromBuildId(self, build_id):
2920 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
2921 return TryOpen(file_name)
2923 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
2924 # Assume current machine i.e. no support for virtualization
2925 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
2926 file_name = os.getenv("PERF_KCORE")
2927 f = TryOpen(file_name) if file_name else None
2930 # For now, no special handling if long_name is /proc/kcore
2931 f = TryOpen(long_name)
2934 f = self.FileFromBuildId(build_id)
2939 def AddInstanceToShutdownOnExit(self, instance):
2940 self.instances_to_shutdown_on_exit.add(instance)
2942 # Shutdown any background processes or threads
2943 def ShutdownInstances(self):
2944 for x in self.instances_to_shutdown_on_exit:
2950 # Database reference
2954 def __init__(self, is_sqlite3, dbname):
2955 self.is_sqlite3 = is_sqlite3
2956 self.dbname = dbname
2958 def Open(self, connection_name):
2959 dbname = self.dbname
2961 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
2963 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
2964 opts = dbname.split()
2967 opt = opt.split("=")
2968 if opt[0] == "hostname":
2969 db.setHostName(opt[1])
2970 elif opt[0] == "port":
2971 db.setPort(int(opt[1]))
2972 elif opt[0] == "username":
2973 db.setUserName(opt[1])
2974 elif opt[0] == "password":
2975 db.setPassword(opt[1])
2976 elif opt[0] == "dbname":
2981 db.setDatabaseName(dbname)
2983 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
2989 if (len(sys.argv) < 2):
2990 printerr("Usage is: exported-sql-viewer.py {<database name> | --help-only}");
2991 raise Exception("Too few arguments")
2993 dbname = sys.argv[1]
2994 if dbname == "--help-only":
2995 app = QApplication(sys.argv)
2996 mainwindow = HelpOnlyWindow()
3003 f = open(dbname, "rb")
3004 if f.read(15) == b'SQLite format 3':
3010 dbref = DBRef(is_sqlite3, dbname)
3011 db, dbname = dbref.Open("main")
3012 glb = Glb(dbref, db, dbname)
3013 app = QApplication(sys.argv)
3015 mainwindow = MainWindow(glb)
3016 glb.mainwindow = mainwindow
3019 glb.ShutdownInstances()
3023 if __name__ == "__main__":