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])
98 from PySide.QtCore import *
99 from PySide.QtGui import *
100 from PySide.QtSql import *
101 from decimal import *
103 from multiprocessing import Process, Array, Value, Event
105 # Data formatting helpers
114 return "+0x%x" % offset
118 if name == "[kernel.kallsyms]":
122 def findnth(s, sub, n, offs=0):
128 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
130 # Percent to one decimal place
132 def PercentToOneDP(n, d):
135 x = (n * Decimal(100)) / d
136 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
138 # Helper for queries that must not fail
140 def QueryExec(query, stmt):
141 ret = query.exec_(stmt)
143 raise Exception("Query failed: " + query.lastError().text())
147 class Thread(QThread):
149 done = Signal(object)
151 def __init__(self, task, param=None, parent=None):
152 super(Thread, self).__init__(parent)
158 if self.param is None:
159 done, result = self.task()
161 done, result = self.task(self.param)
162 self.done.emit(result)
168 class TreeModel(QAbstractItemModel):
170 def __init__(self, glb, parent=None):
171 super(TreeModel, self).__init__(parent)
173 self.root = self.GetRoot()
174 self.last_row_read = 0
176 def Item(self, parent):
178 return parent.internalPointer()
182 def rowCount(self, parent):
183 result = self.Item(parent).childCount()
186 self.dataChanged.emit(parent, parent)
189 def hasChildren(self, parent):
190 return self.Item(parent).hasChildren()
192 def headerData(self, section, orientation, role):
193 if role == Qt.TextAlignmentRole:
194 return self.columnAlignment(section)
195 if role != Qt.DisplayRole:
197 if orientation != Qt.Horizontal:
199 return self.columnHeader(section)
201 def parent(self, child):
202 child_item = child.internalPointer()
203 if child_item is self.root:
205 parent_item = child_item.getParentItem()
206 return self.createIndex(parent_item.getRow(), 0, parent_item)
208 def index(self, row, column, parent):
209 child_item = self.Item(parent).getChildItem(row)
210 return self.createIndex(row, column, child_item)
212 def DisplayData(self, item, index):
213 return item.getData(index.column())
215 def FetchIfNeeded(self, row):
216 if row > self.last_row_read:
217 self.last_row_read = row
218 if row + 10 >= self.root.child_count:
219 self.fetcher.Fetch(glb_chunk_sz)
221 def columnAlignment(self, column):
224 def columnFont(self, column):
227 def data(self, index, role):
228 if role == Qt.TextAlignmentRole:
229 return self.columnAlignment(index.column())
230 if role == Qt.FontRole:
231 return self.columnFont(index.column())
232 if role != Qt.DisplayRole:
234 item = index.internalPointer()
235 return self.DisplayData(item, index)
239 class TableModel(QAbstractTableModel):
241 def __init__(self, parent=None):
242 super(TableModel, self).__init__(parent)
244 self.child_items = []
245 self.last_row_read = 0
247 def Item(self, parent):
249 return parent.internalPointer()
253 def rowCount(self, parent):
254 return self.child_count
256 def headerData(self, section, orientation, role):
257 if role == Qt.TextAlignmentRole:
258 return self.columnAlignment(section)
259 if role != Qt.DisplayRole:
261 if orientation != Qt.Horizontal:
263 return self.columnHeader(section)
265 def index(self, row, column, parent):
266 return self.createIndex(row, column, self.child_items[row])
268 def DisplayData(self, item, index):
269 return item.getData(index.column())
271 def FetchIfNeeded(self, row):
272 if row > self.last_row_read:
273 self.last_row_read = row
274 if row + 10 >= self.child_count:
275 self.fetcher.Fetch(glb_chunk_sz)
277 def columnAlignment(self, column):
280 def columnFont(self, column):
283 def data(self, index, role):
284 if role == Qt.TextAlignmentRole:
285 return self.columnAlignment(index.column())
286 if role == Qt.FontRole:
287 return self.columnFont(index.column())
288 if role != Qt.DisplayRole:
290 item = index.internalPointer()
291 return self.DisplayData(item, index)
295 model_cache = weakref.WeakValueDictionary()
296 model_cache_lock = threading.Lock()
298 def LookupCreateModel(model_name, create_fn):
299 model_cache_lock.acquire()
301 model = model_cache[model_name]
306 model_cache[model_name] = model
307 model_cache_lock.release()
314 def __init__(self, parent, finder, is_reg_expr=False):
317 self.last_value = None
318 self.last_pattern = None
320 label = QLabel("Find:")
321 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
323 self.textbox = QComboBox()
324 self.textbox.setEditable(True)
325 self.textbox.currentIndexChanged.connect(self.ValueChanged)
327 self.progress = QProgressBar()
328 self.progress.setRange(0, 0)
332 self.pattern = QCheckBox("Regular Expression")
334 self.pattern = QCheckBox("Pattern")
335 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
337 self.next_button = QToolButton()
338 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
339 self.next_button.released.connect(lambda: self.NextPrev(1))
341 self.prev_button = QToolButton()
342 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
343 self.prev_button.released.connect(lambda: self.NextPrev(-1))
345 self.close_button = QToolButton()
346 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
347 self.close_button.released.connect(self.Deactivate)
349 self.hbox = QHBoxLayout()
350 self.hbox.setContentsMargins(0, 0, 0, 0)
352 self.hbox.addWidget(label)
353 self.hbox.addWidget(self.textbox)
354 self.hbox.addWidget(self.progress)
355 self.hbox.addWidget(self.pattern)
356 self.hbox.addWidget(self.next_button)
357 self.hbox.addWidget(self.prev_button)
358 self.hbox.addWidget(self.close_button)
361 self.bar.setLayout(self.hbox);
369 self.textbox.setFocus()
371 def Deactivate(self):
375 self.textbox.setEnabled(False)
377 self.next_button.hide()
378 self.prev_button.hide()
382 self.textbox.setEnabled(True)
385 self.next_button.show()
386 self.prev_button.show()
388 def Find(self, direction):
389 value = self.textbox.currentText()
390 pattern = self.pattern.isChecked()
391 self.last_value = value
392 self.last_pattern = pattern
393 self.finder.Find(value, direction, pattern, self.context)
395 def ValueChanged(self):
396 value = self.textbox.currentText()
397 pattern = self.pattern.isChecked()
398 index = self.textbox.currentIndex()
399 data = self.textbox.itemData(index)
400 # Store the pattern in the combo box to keep it with the text value
402 self.textbox.setItemData(index, pattern)
404 self.pattern.setChecked(data)
407 def NextPrev(self, direction):
408 value = self.textbox.currentText()
409 pattern = self.pattern.isChecked()
410 if value != self.last_value:
411 index = self.textbox.findText(value)
412 # Allow for a button press before the value has been added to the combo box
414 index = self.textbox.count()
415 self.textbox.addItem(value, pattern)
416 self.textbox.setCurrentIndex(index)
419 self.textbox.setItemData(index, pattern)
420 elif pattern != self.last_pattern:
421 # Keep the pattern recorded in the combo box up to date
422 index = self.textbox.currentIndex()
423 self.textbox.setItemData(index, pattern)
427 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
429 # Context-sensitive call graph data model item base
431 class CallGraphLevelItemBase(object):
433 def __init__(self, glb, row, parent_item):
436 self.parent_item = parent_item
437 self.query_done = False;
439 self.child_items = []
441 def getChildItem(self, row):
442 return self.child_items[row]
444 def getParentItem(self):
445 return self.parent_item
450 def childCount(self):
451 if not self.query_done:
453 if not self.child_count:
455 return self.child_count
457 def hasChildren(self):
458 if not self.query_done:
460 return self.child_count > 0
462 def getData(self, column):
463 return self.data[column]
465 # Context-sensitive call graph data model level 2+ item base
467 class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
469 def __init__(self, glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item):
470 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
471 self.comm_id = comm_id
472 self.thread_id = thread_id
473 self.call_path_id = call_path_id
474 self.branch_count = branch_count
478 self.query_done = True;
479 query = QSqlQuery(self.glb.db)
480 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time), SUM(branch_count)"
482 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
483 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
484 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
485 " WHERE parent_call_path_id = " + str(self.call_path_id) +
486 " AND comm_id = " + str(self.comm_id) +
487 " AND thread_id = " + str(self.thread_id) +
488 " GROUP BY call_path_id, name, short_name"
489 " ORDER BY call_path_id")
491 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)
492 self.child_items.append(child_item)
493 self.child_count += 1
495 # Context-sensitive call graph data model level three item
497 class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
499 def __init__(self, glb, row, comm_id, thread_id, call_path_id, name, dso, count, time, branch_count, parent_item):
500 super(CallGraphLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, call_path_id, time, branch_count, parent_item)
502 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
503 self.dbid = call_path_id
505 # Context-sensitive call graph data model level two item
507 class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
509 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
510 super(CallGraphLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 1, 0, 0, parent_item)
511 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
512 self.dbid = thread_id
515 super(CallGraphLevelTwoItem, self).Select()
516 for child_item in self.child_items:
517 self.time += child_item.time
518 self.branch_count += child_item.branch_count
519 for child_item in self.child_items:
520 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
521 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
523 # Context-sensitive call graph data model level one item
525 class CallGraphLevelOneItem(CallGraphLevelItemBase):
527 def __init__(self, glb, row, comm_id, comm, parent_item):
528 super(CallGraphLevelOneItem, self).__init__(glb, row, parent_item)
529 self.data = [comm, "", "", "", "", "", ""]
533 self.query_done = True;
534 query = QSqlQuery(self.glb.db)
535 QueryExec(query, "SELECT thread_id, pid, tid"
537 " INNER JOIN threads ON thread_id = threads.id"
538 " WHERE comm_id = " + str(self.dbid))
540 child_item = CallGraphLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
541 self.child_items.append(child_item)
542 self.child_count += 1
544 # Context-sensitive call graph data model root item
546 class CallGraphRootItem(CallGraphLevelItemBase):
548 def __init__(self, glb):
549 super(CallGraphRootItem, self).__init__(glb, 0, None)
551 self.query_done = True;
552 query = QSqlQuery(glb.db)
553 QueryExec(query, "SELECT id, comm FROM comms")
555 if not query.value(0):
557 child_item = CallGraphLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
558 self.child_items.append(child_item)
559 self.child_count += 1
561 # Context-sensitive call graph data model base
563 class CallGraphModelBase(TreeModel):
565 def __init__(self, glb, parent=None):
566 super(CallGraphModelBase, self).__init__(glb, parent)
568 def FindSelect(self, value, pattern, query):
570 # postgresql and sqlite pattern patching differences:
571 # postgresql LIKE is case sensitive but sqlite LIKE is not
572 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
573 # postgresql supports ILIKE which is case insensitive
574 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
575 if not self.glb.dbref.is_sqlite3:
577 s = value.replace("%", "\%")
578 s = s.replace("_", "\_")
579 # Translate * and ? into SQL LIKE pattern characters % and _
580 trans = string.maketrans("*?", "%_")
581 match = " LIKE '" + str(s).translate(trans) + "'"
583 match = " GLOB '" + str(value) + "'"
585 match = " = '" + str(value) + "'"
586 self.DoFindSelect(query, match)
588 def Found(self, query, found):
590 return self.FindPath(query)
593 def FindValue(self, value, pattern, query, last_value, last_pattern):
594 if last_value == value and pattern == last_pattern:
595 found = query.first()
597 self.FindSelect(value, pattern, query)
599 return self.Found(query, found)
601 def FindNext(self, query):
604 found = query.first()
605 return self.Found(query, found)
607 def FindPrev(self, query):
608 found = query.previous()
611 return self.Found(query, found)
613 def FindThread(self, c):
614 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
615 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
616 elif c.direction > 0:
617 ids = self.FindNext(c.query)
619 ids = self.FindPrev(c.query)
622 def Find(self, value, direction, pattern, context, callback):
624 def __init__(self, *x):
625 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
626 def Update(self, *x):
627 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
629 context[0].Update(value, direction, pattern)
631 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
632 # Use a thread so the UI is not blocked during the SELECT
633 thread = Thread(self.FindThread, context[0])
634 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
637 def FindDone(self, thread, callback, ids):
640 # Context-sensitive call graph data model
642 class CallGraphModel(CallGraphModelBase):
644 def __init__(self, glb, parent=None):
645 super(CallGraphModel, self).__init__(glb, parent)
648 return CallGraphRootItem(self.glb)
650 def columnCount(self, parent=None):
653 def columnHeader(self, column):
654 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
655 return headers[column]
657 def columnAlignment(self, column):
658 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
659 return alignment[column]
661 def DoFindSelect(self, query, match):
662 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
664 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
665 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
666 " WHERE symbols.name" + match +
667 " GROUP BY comm_id, thread_id, call_path_id"
668 " ORDER BY comm_id, thread_id, call_path_id")
670 def FindPath(self, query):
671 # Turn the query result into a list of ids that the tree view can walk
672 # to open the tree at the right place.
674 parent_id = query.value(0)
676 ids.insert(0, parent_id)
677 q2 = QSqlQuery(self.glb.db)
678 QueryExec(q2, "SELECT parent_id"
680 " WHERE id = " + str(parent_id))
683 parent_id = q2.value(0)
684 # The call path root is not used
687 ids.insert(0, query.value(2))
688 ids.insert(0, query.value(1))
691 # Call tree data model level 2+ item base
693 class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
695 def __init__(self, glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item):
696 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, row, parent_item)
697 self.comm_id = comm_id
698 self.thread_id = thread_id
699 self.calls_id = calls_id
700 self.branch_count = branch_count
704 self.query_done = True;
705 if self.calls_id == 0:
706 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
709 query = QSqlQuery(self.glb.db)
710 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time, branch_count"
712 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
713 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
714 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
715 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
716 " ORDER BY call_time, calls.id")
718 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)
719 self.child_items.append(child_item)
720 self.child_count += 1
722 # Call tree data model level three item
724 class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
726 def __init__(self, glb, row, comm_id, thread_id, calls_id, name, dso, count, time, branch_count, parent_item):
727 super(CallTreeLevelThreeItem, self).__init__(glb, row, comm_id, thread_id, calls_id, time, branch_count, parent_item)
729 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
732 # Call tree data model level two item
734 class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
736 def __init__(self, glb, row, comm_id, thread_id, pid, tid, parent_item):
737 super(CallTreeLevelTwoItem, self).__init__(glb, row, comm_id, thread_id, 0, 0, 0, parent_item)
738 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
739 self.dbid = thread_id
742 super(CallTreeLevelTwoItem, self).Select()
743 for child_item in self.child_items:
744 self.time += child_item.time
745 self.branch_count += child_item.branch_count
746 for child_item in self.child_items:
747 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
748 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
750 # Call tree data model level one item
752 class CallTreeLevelOneItem(CallGraphLevelItemBase):
754 def __init__(self, glb, row, comm_id, comm, parent_item):
755 super(CallTreeLevelOneItem, self).__init__(glb, row, parent_item)
756 self.data = [comm, "", "", "", "", "", ""]
760 self.query_done = True;
761 query = QSqlQuery(self.glb.db)
762 QueryExec(query, "SELECT thread_id, pid, tid"
764 " INNER JOIN threads ON thread_id = threads.id"
765 " WHERE comm_id = " + str(self.dbid))
767 child_item = CallTreeLevelTwoItem(self.glb, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
768 self.child_items.append(child_item)
769 self.child_count += 1
771 # Call tree data model root item
773 class CallTreeRootItem(CallGraphLevelItemBase):
775 def __init__(self, glb):
776 super(CallTreeRootItem, self).__init__(glb, 0, None)
778 self.query_done = True;
779 query = QSqlQuery(glb.db)
780 QueryExec(query, "SELECT id, comm FROM comms")
782 if not query.value(0):
784 child_item = CallTreeLevelOneItem(glb, self.child_count, query.value(0), query.value(1), self)
785 self.child_items.append(child_item)
786 self.child_count += 1
788 # Call Tree data model
790 class CallTreeModel(CallGraphModelBase):
792 def __init__(self, glb, parent=None):
793 super(CallTreeModel, self).__init__(glb, parent)
796 return CallTreeRootItem(self.glb)
798 def columnCount(self, parent=None):
801 def columnHeader(self, column):
802 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
803 return headers[column]
805 def columnAlignment(self, column):
806 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
807 return alignment[column]
809 def DoFindSelect(self, query, match):
810 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
812 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
813 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
814 " WHERE symbols.name" + match +
815 " ORDER BY comm_id, thread_id, call_time, calls.id")
817 def FindPath(self, query):
818 # Turn the query result into a list of ids that the tree view can walk
819 # to open the tree at the right place.
821 parent_id = query.value(0)
823 ids.insert(0, parent_id)
824 q2 = QSqlQuery(self.glb.db)
825 QueryExec(q2, "SELECT parent_id"
827 " WHERE id = " + str(parent_id))
830 parent_id = q2.value(0)
831 ids.insert(0, query.value(2))
832 ids.insert(0, query.value(1))
835 # Vertical widget layout
839 def __init__(self, w1, w2, w3=None):
840 self.vbox = QWidget()
841 self.vbox.setLayout(QVBoxLayout());
843 self.vbox.layout().setContentsMargins(0, 0, 0, 0)
845 self.vbox.layout().addWidget(w1)
846 self.vbox.layout().addWidget(w2)
848 self.vbox.layout().addWidget(w3)
855 class TreeWindowBase(QMdiSubWindow):
857 def __init__(self, parent=None):
858 super(TreeWindowBase, self).__init__(parent)
864 def DisplayFound(self, ids):
867 parent = QModelIndex()
870 n = self.model.rowCount(parent)
871 for row in xrange(n):
872 child = self.model.index(row, 0, parent)
873 if child.internalPointer().dbid == dbid:
875 self.view.setCurrentIndex(child)
882 def Find(self, value, direction, pattern, context):
885 self.model.Find(value, direction, pattern, context, self.FindDone)
887 def FindDone(self, ids):
889 if not self.DisplayFound(ids):
893 self.find_bar.NotFound()
896 # Context-sensitive call graph window
898 class CallGraphWindow(TreeWindowBase):
900 def __init__(self, glb, parent=None):
901 super(CallGraphWindow, self).__init__(parent)
903 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
905 self.view = QTreeView()
906 self.view.setModel(self.model)
908 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
909 self.view.setColumnWidth(c, w)
911 self.find_bar = FindBar(self, self)
913 self.vbox = VBox(self.view, self.find_bar.Widget())
915 self.setWidget(self.vbox.Widget())
917 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
921 class CallTreeWindow(TreeWindowBase):
923 def __init__(self, glb, parent=None):
924 super(CallTreeWindow, self).__init__(parent)
926 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
928 self.view = QTreeView()
929 self.view.setModel(self.model)
931 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
932 self.view.setColumnWidth(c, w)
934 self.find_bar = FindBar(self, self)
936 self.vbox = VBox(self.view, self.find_bar.Widget())
938 self.setWidget(self.vbox.Widget())
940 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
942 # Child data item finder
944 class ChildDataItemFinder():
946 def __init__(self, root):
948 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
952 def FindSelect(self):
955 pattern = re.compile(self.value)
956 for child in self.root.child_items:
957 for column_data in child.data:
958 if re.search(pattern, str(column_data)) is not None:
959 self.rows.append(child.row)
962 for child in self.root.child_items:
963 for column_data in child.data:
964 if self.value in str(column_data):
965 self.rows.append(child.row)
970 if self.last_value != self.value or self.pattern != self.last_pattern:
972 if not len(self.rows):
974 return self.rows[self.pos]
976 def FindThread(self):
977 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
978 row = self.FindValue()
980 if self.direction > 0:
982 if self.pos >= len(self.rows):
987 self.pos = len(self.rows) - 1
988 row = self.rows[self.pos]
993 def Find(self, value, direction, pattern, context, callback):
994 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
995 # Use a thread so the UI is not blocked
996 thread = Thread(self.FindThread)
997 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
1000 def FindDone(self, thread, callback, row):
1003 # Number of database records to fetch in one go
1005 glb_chunk_sz = 10000
1007 # size of pickled integer big enough for record size
1011 # Background process for SQL data fetcher
1013 class SQLFetcherProcess():
1015 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
1016 # Need a unique connection name
1017 conn_name = "SQLFetcher" + str(os.getpid())
1018 self.db, dbname = dbref.Open(conn_name)
1020 self.buffer = buffer
1023 self.fetch_count = fetch_count
1024 self.fetching_done = fetching_done
1025 self.process_target = process_target
1026 self.wait_event = wait_event
1027 self.fetched_event = fetched_event
1029 self.query = QSqlQuery(self.db)
1030 self.query_limit = 0 if "$$last_id$$" in sql else 2
1034 self.local_head = self.head.value
1035 self.local_tail = self.tail.value
1038 if self.query_limit:
1039 if self.query_limit == 1:
1041 self.query_limit -= 1
1042 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
1043 QueryExec(self.query, stmt)
1046 if not self.query.next():
1048 if not self.query.next():
1050 self.last_id = self.query.value(0)
1051 return self.prep(self.query)
1053 def WaitForTarget(self):
1055 self.wait_event.clear()
1056 target = self.process_target.value
1057 if target > self.fetched or target < 0:
1059 self.wait_event.wait()
1062 def HasSpace(self, sz):
1063 if self.local_tail <= self.local_head:
1064 space = len(self.buffer) - self.local_head
1067 if space >= glb_nsz:
1068 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
1069 nd = cPickle.dumps(0, cPickle.HIGHEST_PROTOCOL)
1070 self.buffer[self.local_head : self.local_head + len(nd)] = nd
1072 if self.local_tail - self.local_head > sz:
1076 def WaitForSpace(self, sz):
1077 if self.HasSpace(sz):
1080 self.wait_event.clear()
1081 self.local_tail = self.tail.value
1082 if self.HasSpace(sz):
1084 self.wait_event.wait()
1086 def AddToBuffer(self, obj):
1087 d = cPickle.dumps(obj, cPickle.HIGHEST_PROTOCOL)
1089 nd = cPickle.dumps(n, cPickle.HIGHEST_PROTOCOL)
1091 self.WaitForSpace(sz)
1092 pos = self.local_head
1093 self.buffer[pos : pos + len(nd)] = nd
1094 self.buffer[pos + glb_nsz : pos + sz] = d
1095 self.local_head += sz
1097 def FetchBatch(self, batch_size):
1099 while batch_size > fetched:
1104 self.AddToBuffer(obj)
1107 self.fetched += fetched
1108 with self.fetch_count.get_lock():
1109 self.fetch_count.value += fetched
1110 self.head.value = self.local_head
1111 self.fetched_event.set()
1115 target = self.WaitForTarget()
1118 batch_size = min(glb_chunk_sz, target - self.fetched)
1119 self.FetchBatch(batch_size)
1120 self.fetching_done.value = True
1121 self.fetched_event.set()
1123 def SQLFetcherFn(*x):
1124 process = SQLFetcherProcess(*x)
1129 class SQLFetcher(QObject):
1131 done = Signal(object)
1133 def __init__(self, glb, sql, prep, process_data, parent=None):
1134 super(SQLFetcher, self).__init__(parent)
1135 self.process_data = process_data
1138 self.last_target = 0
1140 self.buffer_size = 16 * 1024 * 1024
1141 self.buffer = Array(c_char, self.buffer_size, lock=False)
1142 self.head = Value(c_longlong)
1143 self.tail = Value(c_longlong)
1145 self.fetch_count = Value(c_longlong)
1146 self.fetching_done = Value(c_bool)
1148 self.process_target = Value(c_longlong)
1149 self.wait_event = Event()
1150 self.fetched_event = Event()
1151 glb.AddInstanceToShutdownOnExit(self)
1152 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))
1153 self.process.start()
1154 self.thread = Thread(self.Thread)
1155 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
1159 # Tell the thread and process to exit
1160 self.process_target.value = -1
1161 self.wait_event.set()
1163 self.fetching_done.value = True
1164 self.fetched_event.set()
1170 self.fetched_event.clear()
1171 fetch_count = self.fetch_count.value
1172 if fetch_count != self.last_count:
1174 if self.fetching_done.value:
1177 self.fetched_event.wait()
1178 count = fetch_count - self.last_count
1179 self.last_count = fetch_count
1180 self.fetched += count
1183 def Fetch(self, nr):
1185 # -1 inidcates there are no more
1187 result = self.fetched
1188 extra = result + nr - self.target
1190 self.target += extra
1191 # process_target < 0 indicates shutting down
1192 if self.process_target.value >= 0:
1193 self.process_target.value = self.target
1194 self.wait_event.set()
1197 def RemoveFromBuffer(self):
1198 pos = self.local_tail
1199 if len(self.buffer) - pos < glb_nsz:
1201 n = cPickle.loads(self.buffer[pos : pos + glb_nsz])
1204 n = cPickle.loads(self.buffer[0 : glb_nsz])
1206 obj = cPickle.loads(self.buffer[pos : pos + n])
1207 self.local_tail = pos + n
1210 def ProcessData(self, count):
1211 for i in xrange(count):
1212 obj = self.RemoveFromBuffer()
1213 self.process_data(obj)
1214 self.tail.value = self.local_tail
1215 self.wait_event.set()
1216 self.done.emit(count)
1218 # Fetch more records bar
1220 class FetchMoreRecordsBar():
1222 def __init__(self, model, parent):
1225 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
1226 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1228 self.fetch_count = QSpinBox()
1229 self.fetch_count.setRange(1, 1000000)
1230 self.fetch_count.setValue(10)
1231 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1233 self.fetch = QPushButton("Go!")
1234 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1235 self.fetch.released.connect(self.FetchMoreRecords)
1237 self.progress = QProgressBar()
1238 self.progress.setRange(0, 100)
1239 self.progress.hide()
1241 self.done_label = QLabel("All records fetched")
1242 self.done_label.hide()
1244 self.spacer = QLabel("")
1246 self.close_button = QToolButton()
1247 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
1248 self.close_button.released.connect(self.Deactivate)
1250 self.hbox = QHBoxLayout()
1251 self.hbox.setContentsMargins(0, 0, 0, 0)
1253 self.hbox.addWidget(self.label)
1254 self.hbox.addWidget(self.fetch_count)
1255 self.hbox.addWidget(self.fetch)
1256 self.hbox.addWidget(self.spacer)
1257 self.hbox.addWidget(self.progress)
1258 self.hbox.addWidget(self.done_label)
1259 self.hbox.addWidget(self.close_button)
1261 self.bar = QWidget()
1262 self.bar.setLayout(self.hbox);
1265 self.in_progress = False
1266 self.model.progress.connect(self.Progress)
1270 if not model.HasMoreRecords():
1278 self.fetch.setFocus()
1280 def Deactivate(self):
1283 def Enable(self, enable):
1284 self.fetch.setEnabled(enable)
1285 self.fetch_count.setEnabled(enable)
1291 self.progress.show()
1294 self.in_progress = False
1296 self.progress.hide()
1301 return self.fetch_count.value() * glb_chunk_sz
1307 self.fetch_count.hide()
1310 self.done_label.show()
1312 def Progress(self, count):
1313 if self.in_progress:
1315 percent = ((count - self.start) * 100) / self.Target()
1319 self.progress.setValue(percent)
1321 # Count value of zero means no more records
1324 def FetchMoreRecords(self):
1327 self.progress.setValue(0)
1329 self.in_progress = True
1330 self.start = self.model.FetchMoreRecords(self.Target())
1332 # Brance data model level two item
1334 class BranchLevelTwoItem():
1336 def __init__(self, row, text, parent_item):
1338 self.parent_item = parent_item
1339 self.data = [""] * 8
1343 def getParentItem(self):
1344 return self.parent_item
1349 def childCount(self):
1352 def hasChildren(self):
1355 def getData(self, column):
1356 return self.data[column]
1358 # Brance data model level one item
1360 class BranchLevelOneItem():
1362 def __init__(self, glb, row, data, parent_item):
1365 self.parent_item = parent_item
1366 self.child_count = 0
1367 self.child_items = []
1368 self.data = data[1:]
1371 self.query_done = False
1373 def getChildItem(self, row):
1374 return self.child_items[row]
1376 def getParentItem(self):
1377 return self.parent_item
1383 self.query_done = True
1385 if not self.glb.have_disassembler:
1388 query = QSqlQuery(self.glb.db)
1390 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
1392 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
1393 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
1394 " WHERE samples.id = " + str(self.dbid))
1395 if not query.next():
1397 cpu = query.value(0)
1398 dso = query.value(1)
1399 sym = query.value(2)
1400 if dso == 0 or sym == 0:
1402 off = query.value(3)
1403 short_name = query.value(4)
1404 long_name = query.value(5)
1405 build_id = query.value(6)
1406 sym_start = query.value(7)
1409 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
1411 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
1412 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
1413 " ORDER BY samples.id"
1415 if not query.next():
1417 if query.value(0) != dso:
1418 # Cannot disassemble from one dso to another
1420 bsym = query.value(1)
1421 boff = query.value(2)
1422 bsym_start = query.value(3)
1425 tot = bsym_start + boff + 1 - sym_start - off
1426 if tot <= 0 or tot > 16384:
1429 inst = self.glb.disassembler.Instruction()
1430 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
1433 mode = 0 if Is64Bit(f) else 1
1434 self.glb.disassembler.SetMode(inst, mode)
1437 buf = create_string_buffer(tot + 16)
1438 f.seek(sym_start + off)
1439 buf.value = f.read(buf_sz)
1440 buf_ptr = addressof(buf)
1443 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
1445 byte_str = tohex(ip).rjust(16)
1446 for k in xrange(cnt):
1447 byte_str += " %02x" % ord(buf[i])
1452 self.child_items.append(BranchLevelTwoItem(0, byte_str + " " + text, self))
1453 self.child_count += 1
1461 def childCount(self):
1462 if not self.query_done:
1464 if not self.child_count:
1466 return self.child_count
1468 def hasChildren(self):
1469 if not self.query_done:
1471 return self.child_count > 0
1473 def getData(self, column):
1474 return self.data[column]
1476 # Brance data model root item
1478 class BranchRootItem():
1481 self.child_count = 0
1482 self.child_items = []
1485 def getChildItem(self, row):
1486 return self.child_items[row]
1488 def getParentItem(self):
1494 def childCount(self):
1495 return self.child_count
1497 def hasChildren(self):
1498 return self.child_count > 0
1500 def getData(self, column):
1503 # Branch data preparation
1505 def BranchDataPrep(query):
1507 for i in xrange(0, 8):
1508 data.append(query.value(i))
1509 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
1510 " (" + dsoname(query.value(11)) + ")" + " -> " +
1511 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
1512 " (" + dsoname(query.value(15)) + ")")
1517 class BranchModel(TreeModel):
1519 progress = Signal(object)
1521 def __init__(self, glb, event_id, where_clause, parent=None):
1522 super(BranchModel, self).__init__(glb, parent)
1523 self.event_id = event_id
1526 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
1527 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
1528 " ip, symbols.name, sym_offset, dsos.short_name,"
1529 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
1531 " INNER JOIN comms ON comm_id = comms.id"
1532 " INNER JOIN threads ON thread_id = threads.id"
1533 " INNER JOIN branch_types ON branch_type = branch_types.id"
1534 " INNER JOIN symbols ON symbol_id = symbols.id"
1535 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
1536 " INNER JOIN dsos ON samples.dso_id = dsos.id"
1537 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
1538 " WHERE samples.id > $$last_id$$" + where_clause +
1539 " AND evsel_id = " + str(self.event_id) +
1540 " ORDER BY samples.id"
1541 " LIMIT " + str(glb_chunk_sz))
1542 self.fetcher = SQLFetcher(glb, sql, BranchDataPrep, self.AddSample)
1543 self.fetcher.done.connect(self.Update)
1544 self.fetcher.Fetch(glb_chunk_sz)
1547 return BranchRootItem()
1549 def columnCount(self, parent=None):
1552 def columnHeader(self, column):
1553 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
1555 def columnFont(self, column):
1558 return QFont("Monospace")
1560 def DisplayData(self, item, index):
1562 self.FetchIfNeeded(item.row)
1563 return item.getData(index.column())
1565 def AddSample(self, data):
1566 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
1567 self.root.child_items.append(child)
1570 def Update(self, fetched):
1573 self.progress.emit(0)
1574 child_count = self.root.child_count
1575 count = self.populated - child_count
1577 parent = QModelIndex()
1578 self.beginInsertRows(parent, child_count, child_count + count - 1)
1579 self.insertRows(child_count, count, parent)
1580 self.root.child_count += count
1581 self.endInsertRows()
1582 self.progress.emit(self.root.child_count)
1584 def FetchMoreRecords(self, count):
1585 current = self.root.child_count
1587 self.fetcher.Fetch(count)
1589 self.progress.emit(0)
1592 def HasMoreRecords(self):
1599 def __init__(self, name = "", where_clause = "", limit = ""):
1601 self.where_clause = where_clause
1605 return str(self.where_clause + ";" + self.limit)
1609 class BranchWindow(QMdiSubWindow):
1611 def __init__(self, glb, event_id, report_vars, parent=None):
1612 super(BranchWindow, self).__init__(parent)
1614 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
1616 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
1618 self.view = QTreeView()
1619 self.view.setUniformRowHeights(True)
1620 self.view.setModel(self.model)
1622 self.ResizeColumnsToContents()
1624 self.find_bar = FindBar(self, self, True)
1626 self.finder = ChildDataItemFinder(self.model.root)
1628 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
1630 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
1632 self.setWidget(self.vbox.Widget())
1634 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
1636 def ResizeColumnToContents(self, column, n):
1637 # Using the view's resizeColumnToContents() here is extrememly slow
1638 # so implement a crude alternative
1639 mm = "MM" if column else "MMMM"
1640 font = self.view.font()
1641 metrics = QFontMetrics(font)
1643 for row in xrange(n):
1644 val = self.model.root.child_items[row].data[column]
1645 len = metrics.width(str(val) + mm)
1646 max = len if len > max else max
1647 val = self.model.columnHeader(column)
1648 len = metrics.width(str(val) + mm)
1649 max = len if len > max else max
1650 self.view.setColumnWidth(column, max)
1652 def ResizeColumnsToContents(self):
1653 n = min(self.model.root.child_count, 100)
1655 # No data yet, so connect a signal to notify when there is
1656 self.model.rowsInserted.connect(self.UpdateColumnWidths)
1658 columns = self.model.columnCount()
1659 for i in xrange(columns):
1660 self.ResizeColumnToContents(i, n)
1662 def UpdateColumnWidths(self, *x):
1663 # This only needs to be done once, so disconnect the signal now
1664 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
1665 self.ResizeColumnsToContents()
1667 def Find(self, value, direction, pattern, context):
1668 self.view.setFocus()
1669 self.find_bar.Busy()
1670 self.finder.Find(value, direction, pattern, context, self.FindDone)
1672 def FindDone(self, row):
1673 self.find_bar.Idle()
1675 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
1677 self.find_bar.NotFound()
1679 # Line edit data item
1681 class LineEditDataItem(object):
1683 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1686 self.placeholder_text = placeholder_text
1687 self.parent = parent
1690 self.value = default
1692 self.widget = QLineEdit(default)
1693 self.widget.editingFinished.connect(self.Validate)
1694 self.widget.textChanged.connect(self.Invalidate)
1697 self.validated = True
1699 if placeholder_text:
1700 self.widget.setPlaceholderText(placeholder_text)
1702 def TurnTextRed(self):
1704 palette = QPalette()
1705 palette.setColor(QPalette.Text,Qt.red)
1706 self.widget.setPalette(palette)
1709 def TurnTextNormal(self):
1711 palette = QPalette()
1712 self.widget.setPalette(palette)
1715 def InvalidValue(self, value):
1718 self.error = self.label + " invalid value '" + value + "'"
1719 self.parent.ShowMessage(self.error)
1721 def Invalidate(self):
1722 self.validated = False
1724 def DoValidate(self, input_string):
1725 self.value = input_string.strip()
1728 self.validated = True
1730 self.TurnTextNormal()
1731 self.parent.ClearMessage()
1732 input_string = self.widget.text()
1733 if not len(input_string.strip()):
1736 self.DoValidate(input_string)
1739 if not self.validated:
1742 self.parent.ShowMessage(self.error)
1746 def IsNumber(self, value):
1751 return str(x) == value
1753 # Non-negative integer ranges dialog data item
1755 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
1757 def __init__(self, glb, label, placeholder_text, column_name, parent):
1758 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1760 self.column_name = column_name
1762 def DoValidate(self, input_string):
1765 for value in [x.strip() for x in input_string.split(",")]:
1767 vrange = value.split("-")
1768 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1769 return self.InvalidValue(value)
1770 ranges.append(vrange)
1772 if not self.IsNumber(value):
1773 return self.InvalidValue(value)
1774 singles.append(value)
1775 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1777 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
1778 self.value = " OR ".join(ranges)
1780 # Positive integer dialog data item
1782 class PositiveIntegerDataItem(LineEditDataItem):
1784 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
1785 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
1787 def DoValidate(self, input_string):
1788 if not self.IsNumber(input_string.strip()):
1789 return self.InvalidValue(input_string)
1790 value = int(input_string.strip())
1792 return self.InvalidValue(input_string)
1793 self.value = str(value)
1795 # Dialog data item converted and validated using a SQL table
1797 class SQLTableDataItem(LineEditDataItem):
1799 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
1800 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
1802 self.table_name = table_name
1803 self.match_column = match_column
1804 self.column_name1 = column_name1
1805 self.column_name2 = column_name2
1807 def ValueToIds(self, value):
1809 query = QSqlQuery(self.glb.db)
1810 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
1811 ret = query.exec_(stmt)
1814 ids.append(str(query.value(0)))
1817 def DoValidate(self, input_string):
1819 for value in [x.strip() for x in input_string.split(",")]:
1820 ids = self.ValueToIds(value)
1824 return self.InvalidValue(value)
1825 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
1826 if self.column_name2:
1827 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
1829 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
1831 class SampleTimeRangesDataItem(LineEditDataItem):
1833 def __init__(self, glb, label, placeholder_text, column_name, parent):
1834 self.column_name = column_name
1838 self.last_time = 2 ** 64
1840 query = QSqlQuery(glb.db)
1841 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
1843 self.last_id = int(query.value(0))
1844 self.last_time = int(query.value(1))
1845 QueryExec(query, "SELECT time FROM samples WHERE time != 0 ORDER BY id LIMIT 1")
1847 self.first_time = int(query.value(0))
1848 if placeholder_text:
1849 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
1851 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
1853 def IdBetween(self, query, lower_id, higher_id, order):
1854 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
1856 return True, int(query.value(0))
1860 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
1861 query = QSqlQuery(self.glb.db)
1863 next_id = int((lower_id + higher_id) / 2)
1864 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1865 if not query.next():
1866 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
1868 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
1870 return str(higher_id)
1872 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
1873 next_time = int(query.value(0))
1875 if target_time > next_time:
1879 if higher_id <= lower_id + 1:
1880 return str(higher_id)
1882 if target_time >= next_time:
1886 if higher_id <= lower_id + 1:
1887 return str(lower_id)
1889 def ConvertRelativeTime(self, val):
1894 elif suffix == "us":
1896 elif suffix == "ns":
1900 val = val[:-2].strip()
1901 if not self.IsNumber(val):
1903 val = int(val) * mult
1905 val += self.first_time
1907 val += self.last_time
1910 def ConvertTimeRange(self, vrange):
1912 vrange[0] = str(self.first_time)
1914 vrange[1] = str(self.last_time)
1915 vrange[0] = self.ConvertRelativeTime(vrange[0])
1916 vrange[1] = self.ConvertRelativeTime(vrange[1])
1917 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
1919 beg_range = max(int(vrange[0]), self.first_time)
1920 end_range = min(int(vrange[1]), self.last_time)
1921 if beg_range > self.last_time or end_range < self.first_time:
1923 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
1924 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
1927 def AddTimeRange(self, value, ranges):
1928 n = value.count("-")
1932 if value.split("-")[1].strip() == "":
1938 pos = findnth(value, "-", n)
1939 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
1940 if self.ConvertTimeRange(vrange):
1941 ranges.append(vrange)
1945 def DoValidate(self, input_string):
1947 for value in [x.strip() for x in input_string.split(",")]:
1948 if not self.AddTimeRange(value, ranges):
1949 return self.InvalidValue(value)
1950 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
1951 self.value = " OR ".join(ranges)
1953 # Report Dialog Base
1955 class ReportDialogBase(QDialog):
1957 def __init__(self, glb, title, items, partial, parent=None):
1958 super(ReportDialogBase, self).__init__(parent)
1962 self.report_vars = ReportVars()
1964 self.setWindowTitle(title)
1965 self.setMinimumWidth(600)
1967 self.data_items = [x(glb, self) for x in items]
1969 self.partial = partial
1971 self.grid = QGridLayout()
1973 for row in xrange(len(self.data_items)):
1974 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
1975 self.grid.addWidget(self.data_items[row].widget, row, 1)
1977 self.status = QLabel()
1979 self.ok_button = QPushButton("Ok", self)
1980 self.ok_button.setDefault(True)
1981 self.ok_button.released.connect(self.Ok)
1982 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1984 self.cancel_button = QPushButton("Cancel", self)
1985 self.cancel_button.released.connect(self.reject)
1986 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
1988 self.hbox = QHBoxLayout()
1989 #self.hbox.addStretch()
1990 self.hbox.addWidget(self.status)
1991 self.hbox.addWidget(self.ok_button)
1992 self.hbox.addWidget(self.cancel_button)
1994 self.vbox = QVBoxLayout()
1995 self.vbox.addLayout(self.grid)
1996 self.vbox.addLayout(self.hbox)
1998 self.setLayout(self.vbox);
2001 vars = self.report_vars
2002 for d in self.data_items:
2003 if d.id == "REPORTNAME":
2006 self.ShowMessage("Report name is required")
2008 for d in self.data_items:
2011 for d in self.data_items[1:]:
2013 vars.limit = d.value
2015 if len(vars.where_clause):
2016 vars.where_clause += " AND "
2017 vars.where_clause += d.value
2018 if len(vars.where_clause):
2020 vars.where_clause = " AND ( " + vars.where_clause + " ) "
2022 vars.where_clause = " WHERE " + vars.where_clause + " "
2025 def ShowMessage(self, msg):
2026 self.status.setText("<font color=#FF0000>" + msg)
2028 def ClearMessage(self):
2029 self.status.setText("")
2031 # Selected branch report creation dialog
2033 class SelectedBranchDialog(ReportDialogBase):
2035 def __init__(self, glb, parent=None):
2036 title = "Selected Branches"
2037 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2038 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
2039 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
2040 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
2041 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2042 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2043 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
2044 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
2045 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
2046 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
2050 def GetEventList(db):
2052 query = QSqlQuery(db)
2053 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
2055 events.append(query.value(0))
2058 # Is a table selectable
2060 def IsSelectable(db, table, sql = ""):
2061 query = QSqlQuery(db)
2063 QueryExec(query, "SELECT * FROM " + table + " " + sql + " LIMIT 1")
2068 # SQL data preparation
2070 def SQLTableDataPrep(query, count):
2072 for i in xrange(count):
2073 data.append(query.value(i))
2076 # SQL table data model item
2078 class SQLTableItem():
2080 def __init__(self, row, data):
2084 def getData(self, column):
2085 return self.data[column]
2087 # SQL table data model
2089 class SQLTableModel(TableModel):
2091 progress = Signal(object)
2093 def __init__(self, glb, sql, column_headers, parent=None):
2094 super(SQLTableModel, self).__init__(parent)
2098 self.column_headers = column_headers
2099 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): SQLTableDataPrep(x, y), self.AddSample)
2100 self.fetcher.done.connect(self.Update)
2101 self.fetcher.Fetch(glb_chunk_sz)
2103 def DisplayData(self, item, index):
2104 self.FetchIfNeeded(item.row)
2105 return item.getData(index.column())
2107 def AddSample(self, data):
2108 child = SQLTableItem(self.populated, data)
2109 self.child_items.append(child)
2112 def Update(self, fetched):
2115 self.progress.emit(0)
2116 child_count = self.child_count
2117 count = self.populated - child_count
2119 parent = QModelIndex()
2120 self.beginInsertRows(parent, child_count, child_count + count - 1)
2121 self.insertRows(child_count, count, parent)
2122 self.child_count += count
2123 self.endInsertRows()
2124 self.progress.emit(self.child_count)
2126 def FetchMoreRecords(self, count):
2127 current = self.child_count
2129 self.fetcher.Fetch(count)
2131 self.progress.emit(0)
2134 def HasMoreRecords(self):
2137 def columnCount(self, parent=None):
2138 return len(self.column_headers)
2140 def columnHeader(self, column):
2141 return self.column_headers[column]
2143 # SQL automatic table data model
2145 class SQLAutoTableModel(SQLTableModel):
2147 def __init__(self, glb, table_name, parent=None):
2148 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
2149 if table_name == "comm_threads_view":
2150 # For now, comm_threads_view has no id column
2151 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
2153 query = QSqlQuery(glb.db)
2154 if glb.dbref.is_sqlite3:
2155 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
2157 column_headers.append(query.value(1))
2158 if table_name == "sqlite_master":
2159 sql = "SELECT * FROM " + table_name
2161 if table_name[:19] == "information_schema.":
2162 sql = "SELECT * FROM " + table_name
2163 select_table_name = table_name[19:]
2164 schema = "information_schema"
2166 select_table_name = table_name
2168 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
2170 column_headers.append(query.value(0))
2171 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
2173 # Base class for custom ResizeColumnsToContents
2175 class ResizeColumnsToContentsBase(QObject):
2177 def __init__(self, parent=None):
2178 super(ResizeColumnsToContentsBase, self).__init__(parent)
2180 def ResizeColumnToContents(self, column, n):
2181 # Using the view's resizeColumnToContents() here is extrememly slow
2182 # so implement a crude alternative
2183 font = self.view.font()
2184 metrics = QFontMetrics(font)
2186 for row in xrange(n):
2187 val = self.data_model.child_items[row].data[column]
2188 len = metrics.width(str(val) + "MM")
2189 max = len if len > max else max
2190 val = self.data_model.columnHeader(column)
2191 len = metrics.width(str(val) + "MM")
2192 max = len if len > max else max
2193 self.view.setColumnWidth(column, max)
2195 def ResizeColumnsToContents(self):
2196 n = min(self.data_model.child_count, 100)
2198 # No data yet, so connect a signal to notify when there is
2199 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
2201 columns = self.data_model.columnCount()
2202 for i in xrange(columns):
2203 self.ResizeColumnToContents(i, n)
2205 def UpdateColumnWidths(self, *x):
2206 # This only needs to be done once, so disconnect the signal now
2207 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
2208 self.ResizeColumnsToContents()
2212 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2214 def __init__(self, glb, table_name, parent=None):
2215 super(TableWindow, self).__init__(parent)
2217 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
2219 self.model = QSortFilterProxyModel()
2220 self.model.setSourceModel(self.data_model)
2222 self.view = QTableView()
2223 self.view.setModel(self.model)
2224 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2225 self.view.verticalHeader().setVisible(False)
2226 self.view.sortByColumn(-1, Qt.AscendingOrder)
2227 self.view.setSortingEnabled(True)
2229 self.ResizeColumnsToContents()
2231 self.find_bar = FindBar(self, self, True)
2233 self.finder = ChildDataItemFinder(self.data_model)
2235 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2237 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2239 self.setWidget(self.vbox.Widget())
2241 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
2243 def Find(self, value, direction, pattern, context):
2244 self.view.setFocus()
2245 self.find_bar.Busy()
2246 self.finder.Find(value, direction, pattern, context, self.FindDone)
2248 def FindDone(self, row):
2249 self.find_bar.Idle()
2251 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
2253 self.find_bar.NotFound()
2257 def GetTableList(glb):
2259 query = QSqlQuery(glb.db)
2260 if glb.dbref.is_sqlite3:
2261 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
2263 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
2265 tables.append(query.value(0))
2266 if glb.dbref.is_sqlite3:
2267 tables.append("sqlite_master")
2269 tables.append("information_schema.tables")
2270 tables.append("information_schema.views")
2271 tables.append("information_schema.columns")
2274 # Top Calls data model
2276 class TopCallsModel(SQLTableModel):
2278 def __init__(self, glb, report_vars, parent=None):
2280 if not glb.dbref.is_sqlite3:
2283 if len(report_vars.limit):
2284 limit = " LIMIT " + report_vars.limit
2285 sql = ("SELECT comm, pid, tid, name,"
2287 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
2290 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
2292 " WHEN (calls.flags = 1) THEN 'no call'" + text +
2293 " WHEN (calls.flags = 2) THEN 'no return'" + text +
2294 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
2298 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
2299 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
2300 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
2301 " INNER JOIN comms ON calls.comm_id = comms.id"
2302 " INNER JOIN threads ON calls.thread_id = threads.id" +
2303 report_vars.where_clause +
2304 " ORDER BY elapsed_time DESC" +
2307 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
2308 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
2309 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
2311 def columnAlignment(self, column):
2312 return self.alignment[column]
2314 # Top Calls report creation dialog
2316 class TopCallsDialog(ReportDialogBase):
2318 def __init__(self, glb, parent=None):
2319 title = "Top Calls by Elapsed Time"
2320 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
2321 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
2322 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
2323 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
2324 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
2325 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
2326 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
2327 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
2328 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
2332 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
2334 def __init__(self, glb, report_vars, parent=None):
2335 super(TopCallsWindow, self).__init__(parent)
2337 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
2338 self.model = self.data_model
2340 self.view = QTableView()
2341 self.view.setModel(self.model)
2342 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2343 self.view.verticalHeader().setVisible(False)
2345 self.ResizeColumnsToContents()
2347 self.find_bar = FindBar(self, self, True)
2349 self.finder = ChildDataItemFinder(self.model)
2351 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
2353 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
2355 self.setWidget(self.vbox.Widget())
2357 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
2359 def Find(self, value, direction, pattern, context):
2360 self.view.setFocus()
2361 self.find_bar.Busy()
2362 self.finder.Find(value, direction, pattern, context, self.FindDone)
2364 def FindDone(self, row):
2365 self.find_bar.Idle()
2367 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
2369 self.find_bar.NotFound()
2373 def CreateAction(label, tip, callback, parent=None, shortcut=None):
2374 action = QAction(label, parent)
2375 if shortcut != None:
2376 action.setShortcuts(shortcut)
2377 action.setStatusTip(tip)
2378 action.triggered.connect(callback)
2381 # Typical application actions
2383 def CreateExitAction(app, parent=None):
2384 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
2386 # Typical MDI actions
2388 def CreateCloseActiveWindowAction(mdi_area):
2389 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
2391 def CreateCloseAllWindowsAction(mdi_area):
2392 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
2394 def CreateTileWindowsAction(mdi_area):
2395 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
2397 def CreateCascadeWindowsAction(mdi_area):
2398 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
2400 def CreateNextWindowAction(mdi_area):
2401 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
2403 def CreatePreviousWindowAction(mdi_area):
2404 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
2406 # Typical MDI window menu
2410 def __init__(self, mdi_area, menu):
2411 self.mdi_area = mdi_area
2412 self.window_menu = menu.addMenu("&Windows")
2413 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
2414 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
2415 self.tile_windows = CreateTileWindowsAction(mdi_area)
2416 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
2417 self.next_window = CreateNextWindowAction(mdi_area)
2418 self.previous_window = CreatePreviousWindowAction(mdi_area)
2419 self.window_menu.aboutToShow.connect(self.Update)
2422 self.window_menu.clear()
2423 sub_window_count = len(self.mdi_area.subWindowList())
2424 have_sub_windows = sub_window_count != 0
2425 self.close_active_window.setEnabled(have_sub_windows)
2426 self.close_all_windows.setEnabled(have_sub_windows)
2427 self.tile_windows.setEnabled(have_sub_windows)
2428 self.cascade_windows.setEnabled(have_sub_windows)
2429 self.next_window.setEnabled(have_sub_windows)
2430 self.previous_window.setEnabled(have_sub_windows)
2431 self.window_menu.addAction(self.close_active_window)
2432 self.window_menu.addAction(self.close_all_windows)
2433 self.window_menu.addSeparator()
2434 self.window_menu.addAction(self.tile_windows)
2435 self.window_menu.addAction(self.cascade_windows)
2436 self.window_menu.addSeparator()
2437 self.window_menu.addAction(self.next_window)
2438 self.window_menu.addAction(self.previous_window)
2439 if sub_window_count == 0:
2441 self.window_menu.addSeparator()
2443 for sub_window in self.mdi_area.subWindowList():
2444 label = str(nr) + " " + sub_window.name
2447 action = self.window_menu.addAction(label)
2448 action.setCheckable(True)
2449 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
2450 action.triggered.connect(lambda x=nr: self.setActiveSubWindow(x))
2451 self.window_menu.addAction(action)
2454 def setActiveSubWindow(self, nr):
2455 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
2470 <p class=c1><a href=#reports>1. Reports</a></p>
2471 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
2472 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
2473 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
2474 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
2475 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
2476 <p class=c1><a href=#tables>2. Tables</a></p>
2477 <h1 id=reports>1. Reports</h1>
2478 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
2479 The result is a GUI window with a tree representing a context-sensitive
2480 call-graph. Expanding a couple of levels of the tree and adjusting column
2481 widths to suit will display something like:
2483 Call Graph: pt_example
2484 Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
2487 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
2488 |- unknown unknown 1 13198 0.1 1 0.0
2489 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
2490 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
2491 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
2492 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
2493 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
2494 >- __libc_csu_init ls 1 10354 0.1 10 0.0
2495 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
2496 v- main ls 1 8182043 99.6 180254 99.9
2498 <h3>Points to note:</h3>
2500 <li>The top level is a command name (comm)</li>
2501 <li>The next level is a thread (pid:tid)</li>
2502 <li>Subsequent levels are functions</li>
2503 <li>'Count' is the number of calls</li>
2504 <li>'Time' is the elapsed time until the function returns</li>
2505 <li>Percentages are relative to the level above</li>
2506 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
2509 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
2510 The pattern matching symbols are ? for any character and * for zero or more characters.
2511 <h2 id=calltree>1.2 Call Tree</h2>
2512 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
2513 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
2514 <h2 id=allbranches>1.3 All branches</h2>
2515 The All branches report displays all branches in chronological order.
2516 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
2517 <h3>Disassembly</h3>
2518 Open a branch to display disassembly. This only works if:
2520 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
2521 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
2522 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
2523 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
2524 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
2526 <h4 id=xed>Intel XED Setup</h4>
2527 To use Intel XED, libxed.so must be present. To build and install libxed.so:
2529 git clone https://github.com/intelxed/mbuild.git mbuild
2530 git clone https://github.com/intelxed/xed
2533 sudo ./mfile.py --prefix=/usr/local install
2537 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2538 Refer to Python documentation for the regular expression syntax.
2539 All columns are searched, but only currently fetched rows are searched.
2540 <h2 id=selectedbranches>1.4 Selected branches</h2>
2541 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
2542 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2543 <h3>1.4.1 Time ranges</h3>
2544 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
2545 ms, us or ns. Also, negative values are relative to the end of trace. Examples:
2547 81073085947329-81073085958238 From 81073085947329 to 81073085958238
2548 100us-200us From 100us to 200us
2549 10ms- From 10ms to the end
2550 -100ns The first 100ns
2551 -10ms- The last 10ms
2553 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
2554 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
2555 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.
2556 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
2557 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
2558 <h1 id=tables>2. Tables</h1>
2559 The Tables menu shows all tables and views in the database. Most tables have an associated view
2560 which displays the information in a more friendly way. Not all data for large tables is fetched
2561 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
2562 but that can be slow for large tables.
2563 <p>There are also tables of database meta-information.
2564 For SQLite3 databases, the sqlite_master table is included.
2565 For PostgreSQL databases, information_schema.tables/views/columns are included.
2567 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
2568 Refer to Python documentation for the regular expression syntax.
2569 All columns are searched, but only currently fetched rows are searched.
2570 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
2571 will go to the next/previous result in id order, instead of display order.
2576 class HelpWindow(QMdiSubWindow):
2578 def __init__(self, glb, parent=None):
2579 super(HelpWindow, self).__init__(parent)
2581 self.text = QTextBrowser()
2582 self.text.setHtml(glb_help_text)
2583 self.text.setReadOnly(True)
2584 self.text.setOpenExternalLinks(True)
2586 self.setWidget(self.text)
2588 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
2590 # Main window that only displays the help text
2592 class HelpOnlyWindow(QMainWindow):
2594 def __init__(self, parent=None):
2595 super(HelpOnlyWindow, self).__init__(parent)
2597 self.setMinimumSize(200, 100)
2598 self.resize(800, 600)
2599 self.setWindowTitle("Exported SQL Viewer Help")
2600 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
2602 self.text = QTextBrowser()
2603 self.text.setHtml(glb_help_text)
2604 self.text.setReadOnly(True)
2605 self.text.setOpenExternalLinks(True)
2607 self.setCentralWidget(self.text)
2611 def ResizeFont(widget, diff):
2612 font = widget.font()
2613 sz = font.pointSize()
2614 font.setPointSize(sz + diff)
2615 widget.setFont(font)
2617 def ShrinkFont(widget):
2618 ResizeFont(widget, -1)
2620 def EnlargeFont(widget):
2621 ResizeFont(widget, 1)
2623 # Unique name for sub-windows
2625 def NumberedWindowName(name, nr):
2627 name += " <" + str(nr) + ">"
2630 def UniqueSubWindowName(mdi_area, name):
2633 unique_name = NumberedWindowName(name, nr)
2635 for sub_window in mdi_area.subWindowList():
2636 if sub_window.name == unique_name:
2645 def AddSubWindow(mdi_area, sub_window, name):
2646 unique_name = UniqueSubWindowName(mdi_area, name)
2647 sub_window.setMinimumSize(200, 100)
2648 sub_window.resize(800, 600)
2649 sub_window.setWindowTitle(unique_name)
2650 sub_window.setAttribute(Qt.WA_DeleteOnClose)
2651 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
2652 sub_window.name = unique_name
2653 mdi_area.addSubWindow(sub_window)
2658 class MainWindow(QMainWindow):
2660 def __init__(self, glb, parent=None):
2661 super(MainWindow, self).__init__(parent)
2665 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
2666 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
2667 self.setMinimumSize(200, 100)
2669 self.mdi_area = QMdiArea()
2670 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2671 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
2673 self.setCentralWidget(self.mdi_area)
2675 menu = self.menuBar()
2677 file_menu = menu.addMenu("&File")
2678 file_menu.addAction(CreateExitAction(glb.app, self))
2680 edit_menu = menu.addMenu("&Edit")
2681 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
2682 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
2683 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
2684 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
2686 reports_menu = menu.addMenu("&Reports")
2687 if IsSelectable(glb.db, "calls"):
2688 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
2690 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
2691 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
2693 self.EventMenu(GetEventList(glb.db), reports_menu)
2695 if IsSelectable(glb.db, "calls"):
2696 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
2698 self.TableMenu(GetTableList(glb), menu)
2700 self.window_menu = WindowMenu(self.mdi_area, menu)
2702 help_menu = menu.addMenu("&Help")
2703 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
2706 win = self.mdi_area.activeSubWindow()
2709 win.find_bar.Activate()
2713 def FetchMoreRecords(self):
2714 win = self.mdi_area.activeSubWindow()
2717 win.fetch_bar.Activate()
2721 def ShrinkFont(self):
2722 win = self.mdi_area.activeSubWindow()
2723 ShrinkFont(win.view)
2725 def EnlargeFont(self):
2726 win = self.mdi_area.activeSubWindow()
2727 EnlargeFont(win.view)
2729 def EventMenu(self, events, reports_menu):
2731 for event in events:
2732 event = event.split(":")[0]
2733 if event == "branches":
2734 branches_events += 1
2736 for event in events:
2738 event = event.split(":")[0]
2739 if event == "branches":
2740 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
2741 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewBranchView(x), self))
2742 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
2743 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda x=dbid: self.NewSelectedBranchView(x), self))
2745 def TableMenu(self, tables, menu):
2746 table_menu = menu.addMenu("&Tables")
2747 for table in tables:
2748 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda t=table: self.NewTableView(t), self))
2750 def NewCallGraph(self):
2751 CallGraphWindow(self.glb, self)
2753 def NewCallTree(self):
2754 CallTreeWindow(self.glb, self)
2756 def NewTopCalls(self):
2757 dialog = TopCallsDialog(self.glb, self)
2758 ret = dialog.exec_()
2760 TopCallsWindow(self.glb, dialog.report_vars, self)
2762 def NewBranchView(self, event_id):
2763 BranchWindow(self.glb, event_id, ReportVars(), self)
2765 def NewSelectedBranchView(self, event_id):
2766 dialog = SelectedBranchDialog(self.glb, self)
2767 ret = dialog.exec_()
2769 BranchWindow(self.glb, event_id, dialog.report_vars, self)
2771 def NewTableView(self, table_name):
2772 TableWindow(self.glb, table_name, self)
2775 HelpWindow(self.glb, self)
2779 class xed_state_t(Structure):
2786 class XEDInstruction():
2788 def __init__(self, libxed):
2789 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
2790 xedd_t = c_byte * 512
2791 self.xedd = xedd_t()
2792 self.xedp = addressof(self.xedd)
2793 libxed.xed_decoded_inst_zero(self.xedp)
2794 self.state = xed_state_t()
2795 self.statep = addressof(self.state)
2796 # Buffer for disassembled instruction text
2797 self.buffer = create_string_buffer(256)
2798 self.bufferp = addressof(self.buffer)
2804 self.libxed = CDLL("libxed.so")
2808 self.libxed = CDLL("/usr/local/lib/libxed.so")
2810 self.xed_tables_init = self.libxed.xed_tables_init
2811 self.xed_tables_init.restype = None
2812 self.xed_tables_init.argtypes = []
2814 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
2815 self.xed_decoded_inst_zero.restype = None
2816 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
2818 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
2819 self.xed_operand_values_set_mode.restype = None
2820 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
2822 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
2823 self.xed_decoded_inst_zero_keep_mode.restype = None
2824 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
2826 self.xed_decode = self.libxed.xed_decode
2827 self.xed_decode.restype = c_int
2828 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
2830 self.xed_format_context = self.libxed.xed_format_context
2831 self.xed_format_context.restype = c_uint
2832 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
2834 self.xed_tables_init()
2836 def Instruction(self):
2837 return XEDInstruction(self)
2839 def SetMode(self, inst, mode):
2841 inst.state.mode = 4 # 32-bit
2842 inst.state.width = 4 # 4 bytes
2844 inst.state.mode = 1 # 64-bit
2845 inst.state.width = 8 # 8 bytes
2846 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
2848 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
2849 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
2850 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
2853 # Use AT&T mode (2), alternative is Intel (3)
2854 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
2857 # Return instruction length and the disassembled instruction text
2858 # For now, assume the length is in byte 166
2859 return inst.xedd[166], inst.buffer.value
2861 def TryOpen(file_name):
2863 return open(file_name, "rb")
2868 result = sizeof(c_void_p)
2875 eclass = ord(header[4])
2876 encoding = ord(header[5])
2877 version = ord(header[6])
2878 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
2879 result = True if eclass == 2 else False
2886 def __init__(self, dbref, db, dbname):
2889 self.dbname = dbname
2890 self.home_dir = os.path.expanduser("~")
2891 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
2892 if self.buildid_dir:
2893 self.buildid_dir += "/.build-id/"
2895 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
2897 self.mainwindow = None
2898 self.instances_to_shutdown_on_exit = weakref.WeakSet()
2900 self.disassembler = LibXED()
2901 self.have_disassembler = True
2903 self.have_disassembler = False
2905 def FileFromBuildId(self, build_id):
2906 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
2907 return TryOpen(file_name)
2909 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
2910 # Assume current machine i.e. no support for virtualization
2911 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
2912 file_name = os.getenv("PERF_KCORE")
2913 f = TryOpen(file_name) if file_name else None
2916 # For now, no special handling if long_name is /proc/kcore
2917 f = TryOpen(long_name)
2920 f = self.FileFromBuildId(build_id)
2925 def AddInstanceToShutdownOnExit(self, instance):
2926 self.instances_to_shutdown_on_exit.add(instance)
2928 # Shutdown any background processes or threads
2929 def ShutdownInstances(self):
2930 for x in self.instances_to_shutdown_on_exit:
2936 # Database reference
2940 def __init__(self, is_sqlite3, dbname):
2941 self.is_sqlite3 = is_sqlite3
2942 self.dbname = dbname
2944 def Open(self, connection_name):
2945 dbname = self.dbname
2947 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
2949 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
2950 opts = dbname.split()
2953 opt = opt.split("=")
2954 if opt[0] == "hostname":
2955 db.setHostName(opt[1])
2956 elif opt[0] == "port":
2957 db.setPort(int(opt[1]))
2958 elif opt[0] == "username":
2959 db.setUserName(opt[1])
2960 elif opt[0] == "password":
2961 db.setPassword(opt[1])
2962 elif opt[0] == "dbname":
2967 db.setDatabaseName(dbname)
2969 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
2975 if (len(sys.argv) < 2):
2976 print >> sys.stderr, "Usage is: exported-sql-viewer.py {<database name> | --help-only}"
2977 raise Exception("Too few arguments")
2979 dbname = sys.argv[1]
2980 if dbname == "--help-only":
2981 app = QApplication(sys.argv)
2982 mainwindow = HelpOnlyWindow()
2990 if f.read(15) == "SQLite format 3":
2996 dbref = DBRef(is_sqlite3, dbname)
2997 db, dbname = dbref.Open("main")
2998 glb = Glb(dbref, db, dbname)
2999 app = QApplication(sys.argv)
3001 mainwindow = MainWindow(glb)
3002 glb.mainwindow = mainwindow
3005 glb.ShutdownInstances()
3009 if __name__ == "__main__":