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
94 # Only change warnings if the python -W option was not used
95 if not sys.warnoptions:
97 # PySide2 causes deprecation warnings, ignore them.
98 warnings.filterwarnings("ignore", category=DeprecationWarning)
105 import cPickle as pickle
106 # size of pickled integer big enough for record size
117 pyside_version_1 = True
118 if not "--pyside-version-1" in sys.argv:
120 from PySide2.QtCore import *
121 from PySide2.QtGui import *
122 from PySide2.QtSql import *
123 from PySide2.QtWidgets import *
124 pyside_version_1 = False
129 from PySide.QtCore import *
130 from PySide.QtGui import *
131 from PySide.QtSql import *
133 from decimal import Decimal, ROUND_HALF_UP
134 from ctypes import CDLL, Structure, create_string_buffer, addressof, sizeof, \
135 c_void_p, c_bool, c_byte, c_char, c_int, c_uint, c_longlong, c_ulonglong
136 from multiprocessing import Process, Array, Value, Event
138 # xrange is range in Python3
144 def printerr(*args, **keyword_args):
145 print(*args, file=sys.stderr, **keyword_args)
147 # Data formatting helpers
156 return "+0x%x" % offset
160 if name == "[kernel.kallsyms]":
164 def findnth(s, sub, n, offs=0):
170 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
172 # Percent to one decimal place
174 def PercentToOneDP(n, d):
177 x = (n * Decimal(100)) / d
178 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
180 # Helper for queries that must not fail
182 def QueryExec(query, stmt):
183 ret = query.exec_(stmt)
185 raise Exception("Query failed: " + query.lastError().text())
189 class Thread(QThread):
191 done = Signal(object)
193 def __init__(self, task, param=None, parent=None):
194 super(Thread, self).__init__(parent)
200 if self.param is None:
201 done, result = self.task()
203 done, result = self.task(self.param)
204 self.done.emit(result)
210 class TreeModel(QAbstractItemModel):
212 def __init__(self, glb, params, parent=None):
213 super(TreeModel, self).__init__(parent)
216 self.root = self.GetRoot()
217 self.last_row_read = 0
219 def Item(self, parent):
221 return parent.internalPointer()
225 def rowCount(self, parent):
226 result = self.Item(parent).childCount()
229 self.dataChanged.emit(parent, parent)
232 def hasChildren(self, parent):
233 return self.Item(parent).hasChildren()
235 def headerData(self, section, orientation, role):
236 if role == Qt.TextAlignmentRole:
237 return self.columnAlignment(section)
238 if role != Qt.DisplayRole:
240 if orientation != Qt.Horizontal:
242 return self.columnHeader(section)
244 def parent(self, child):
245 child_item = child.internalPointer()
246 if child_item is self.root:
248 parent_item = child_item.getParentItem()
249 return self.createIndex(parent_item.getRow(), 0, parent_item)
251 def index(self, row, column, parent):
252 child_item = self.Item(parent).getChildItem(row)
253 return self.createIndex(row, column, child_item)
255 def DisplayData(self, item, index):
256 return item.getData(index.column())
258 def FetchIfNeeded(self, row):
259 if row > self.last_row_read:
260 self.last_row_read = row
261 if row + 10 >= self.root.child_count:
262 self.fetcher.Fetch(glb_chunk_sz)
264 def columnAlignment(self, column):
267 def columnFont(self, column):
270 def data(self, index, role):
271 if role == Qt.TextAlignmentRole:
272 return self.columnAlignment(index.column())
273 if role == Qt.FontRole:
274 return self.columnFont(index.column())
275 if role != Qt.DisplayRole:
277 item = index.internalPointer()
278 return self.DisplayData(item, index)
282 class TableModel(QAbstractTableModel):
284 def __init__(self, parent=None):
285 super(TableModel, self).__init__(parent)
287 self.child_items = []
288 self.last_row_read = 0
290 def Item(self, parent):
292 return parent.internalPointer()
296 def rowCount(self, parent):
297 return self.child_count
299 def headerData(self, section, orientation, role):
300 if role == Qt.TextAlignmentRole:
301 return self.columnAlignment(section)
302 if role != Qt.DisplayRole:
304 if orientation != Qt.Horizontal:
306 return self.columnHeader(section)
308 def index(self, row, column, parent):
309 return self.createIndex(row, column, self.child_items[row])
311 def DisplayData(self, item, index):
312 return item.getData(index.column())
314 def FetchIfNeeded(self, row):
315 if row > self.last_row_read:
316 self.last_row_read = row
317 if row + 10 >= self.child_count:
318 self.fetcher.Fetch(glb_chunk_sz)
320 def columnAlignment(self, column):
323 def columnFont(self, column):
326 def data(self, index, role):
327 if role == Qt.TextAlignmentRole:
328 return self.columnAlignment(index.column())
329 if role == Qt.FontRole:
330 return self.columnFont(index.column())
331 if role != Qt.DisplayRole:
333 item = index.internalPointer()
334 return self.DisplayData(item, index)
338 model_cache = weakref.WeakValueDictionary()
339 model_cache_lock = threading.Lock()
341 def LookupCreateModel(model_name, create_fn):
342 model_cache_lock.acquire()
344 model = model_cache[model_name]
349 model_cache[model_name] = model
350 model_cache_lock.release()
353 def LookupModel(model_name):
354 model_cache_lock.acquire()
356 model = model_cache[model_name]
359 model_cache_lock.release()
366 def __init__(self, parent, finder, is_reg_expr=False):
369 self.last_value = None
370 self.last_pattern = None
372 label = QLabel("Find:")
373 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
375 self.textbox = QComboBox()
376 self.textbox.setEditable(True)
377 self.textbox.currentIndexChanged.connect(self.ValueChanged)
379 self.progress = QProgressBar()
380 self.progress.setRange(0, 0)
384 self.pattern = QCheckBox("Regular Expression")
386 self.pattern = QCheckBox("Pattern")
387 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
389 self.next_button = QToolButton()
390 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
391 self.next_button.released.connect(lambda: self.NextPrev(1))
393 self.prev_button = QToolButton()
394 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
395 self.prev_button.released.connect(lambda: self.NextPrev(-1))
397 self.close_button = QToolButton()
398 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
399 self.close_button.released.connect(self.Deactivate)
401 self.hbox = QHBoxLayout()
402 self.hbox.setContentsMargins(0, 0, 0, 0)
404 self.hbox.addWidget(label)
405 self.hbox.addWidget(self.textbox)
406 self.hbox.addWidget(self.progress)
407 self.hbox.addWidget(self.pattern)
408 self.hbox.addWidget(self.next_button)
409 self.hbox.addWidget(self.prev_button)
410 self.hbox.addWidget(self.close_button)
413 self.bar.setLayout(self.hbox)
421 self.textbox.lineEdit().selectAll()
422 self.textbox.setFocus()
424 def Deactivate(self):
428 self.textbox.setEnabled(False)
430 self.next_button.hide()
431 self.prev_button.hide()
435 self.textbox.setEnabled(True)
438 self.next_button.show()
439 self.prev_button.show()
441 def Find(self, direction):
442 value = self.textbox.currentText()
443 pattern = self.pattern.isChecked()
444 self.last_value = value
445 self.last_pattern = pattern
446 self.finder.Find(value, direction, pattern, self.context)
448 def ValueChanged(self):
449 value = self.textbox.currentText()
450 pattern = self.pattern.isChecked()
451 index = self.textbox.currentIndex()
452 data = self.textbox.itemData(index)
453 # Store the pattern in the combo box to keep it with the text value
455 self.textbox.setItemData(index, pattern)
457 self.pattern.setChecked(data)
460 def NextPrev(self, direction):
461 value = self.textbox.currentText()
462 pattern = self.pattern.isChecked()
463 if value != self.last_value:
464 index = self.textbox.findText(value)
465 # Allow for a button press before the value has been added to the combo box
467 index = self.textbox.count()
468 self.textbox.addItem(value, pattern)
469 self.textbox.setCurrentIndex(index)
472 self.textbox.setItemData(index, pattern)
473 elif pattern != self.last_pattern:
474 # Keep the pattern recorded in the combo box up to date
475 index = self.textbox.currentIndex()
476 self.textbox.setItemData(index, pattern)
480 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
482 # Context-sensitive call graph data model item base
484 class CallGraphLevelItemBase(object):
486 def __init__(self, glb, params, row, parent_item):
490 self.parent_item = parent_item
491 self.query_done = False
493 self.child_items = []
495 self.level = parent_item.level + 1
499 def getChildItem(self, row):
500 return self.child_items[row]
502 def getParentItem(self):
503 return self.parent_item
508 def childCount(self):
509 if not self.query_done:
511 if not self.child_count:
513 return self.child_count
515 def hasChildren(self):
516 if not self.query_done:
518 return self.child_count > 0
520 def getData(self, column):
521 return self.data[column]
523 # Context-sensitive call graph data model level 2+ item base
525 class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
527 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
528 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
529 self.comm_id = comm_id
530 self.thread_id = thread_id
531 self.call_path_id = call_path_id
532 self.insn_cnt = insn_cnt
533 self.cyc_cnt = cyc_cnt
534 self.branch_count = branch_count
538 self.query_done = True
539 query = QSqlQuery(self.glb.db)
540 if self.params.have_ipc:
541 ipc_str = ", SUM(insn_count), SUM(cyc_count)"
544 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
546 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
547 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
548 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
549 " WHERE parent_call_path_id = " + str(self.call_path_id) +
550 " AND comm_id = " + str(self.comm_id) +
551 " AND thread_id = " + str(self.thread_id) +
552 " GROUP BY call_path_id, name, short_name"
553 " ORDER BY call_path_id")
555 if self.params.have_ipc:
556 insn_cnt = int(query.value(5))
557 cyc_cnt = int(query.value(6))
558 branch_count = int(query.value(7))
562 branch_count = int(query.value(5))
563 child_item = CallGraphLevelThreeItem(self.glb, self.params, 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)), insn_cnt, cyc_cnt, branch_count, self)
564 self.child_items.append(child_item)
565 self.child_count += 1
567 # Context-sensitive call graph data model level three item
569 class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
571 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, name, dso, count, time, insn_cnt, cyc_cnt, branch_count, parent_item):
572 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
574 if self.params.have_ipc:
575 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
576 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
577 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
578 ipc = CalcIPC(cyc_cnt, insn_cnt)
579 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
581 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
582 self.dbid = call_path_id
584 # Context-sensitive call graph data model level two item
586 class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
588 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
589 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)
590 if self.params.have_ipc:
591 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
593 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
594 self.dbid = thread_id
597 super(CallGraphLevelTwoItem, self).Select()
598 for child_item in self.child_items:
599 self.time += child_item.time
600 self.insn_cnt += child_item.insn_cnt
601 self.cyc_cnt += child_item.cyc_cnt
602 self.branch_count += child_item.branch_count
603 for child_item in self.child_items:
604 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
605 if self.params.have_ipc:
606 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
607 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
608 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
610 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
612 # Context-sensitive call graph data model level one item
614 class CallGraphLevelOneItem(CallGraphLevelItemBase):
616 def __init__(self, glb, params, row, comm_id, comm, parent_item):
617 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)
618 if self.params.have_ipc:
619 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
621 self.data = [comm, "", "", "", "", "", ""]
625 self.query_done = True
626 query = QSqlQuery(self.glb.db)
627 QueryExec(query, "SELECT thread_id, pid, tid"
629 " INNER JOIN threads ON thread_id = threads.id"
630 " WHERE comm_id = " + str(self.dbid))
632 child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
633 self.child_items.append(child_item)
634 self.child_count += 1
636 # Context-sensitive call graph data model root item
638 class CallGraphRootItem(CallGraphLevelItemBase):
640 def __init__(self, glb, params):
641 super(CallGraphRootItem, self).__init__(glb, params, 0, None)
643 self.query_done = True
645 if IsSelectable(glb.db, "comms", columns = "has_calls"):
646 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
647 query = QSqlQuery(glb.db)
648 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
650 if not query.value(0):
652 child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
653 self.child_items.append(child_item)
654 self.child_count += 1
656 # Call graph model parameters
658 class CallGraphModelParams():
660 def __init__(self, glb, parent=None):
661 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")
663 # Context-sensitive call graph data model base
665 class CallGraphModelBase(TreeModel):
667 def __init__(self, glb, parent=None):
668 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)
670 def FindSelect(self, value, pattern, query):
672 # postgresql and sqlite pattern patching differences:
673 # postgresql LIKE is case sensitive but sqlite LIKE is not
674 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
675 # postgresql supports ILIKE which is case insensitive
676 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
677 if not self.glb.dbref.is_sqlite3:
679 s = value.replace("%", "\%")
680 s = s.replace("_", "\_")
681 # Translate * and ? into SQL LIKE pattern characters % and _
682 trans = string.maketrans("*?", "%_")
683 match = " LIKE '" + str(s).translate(trans) + "'"
685 match = " GLOB '" + str(value) + "'"
687 match = " = '" + str(value) + "'"
688 self.DoFindSelect(query, match)
690 def Found(self, query, found):
692 return self.FindPath(query)
695 def FindValue(self, value, pattern, query, last_value, last_pattern):
696 if last_value == value and pattern == last_pattern:
697 found = query.first()
699 self.FindSelect(value, pattern, query)
701 return self.Found(query, found)
703 def FindNext(self, query):
706 found = query.first()
707 return self.Found(query, found)
709 def FindPrev(self, query):
710 found = query.previous()
713 return self.Found(query, found)
715 def FindThread(self, c):
716 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
717 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
718 elif c.direction > 0:
719 ids = self.FindNext(c.query)
721 ids = self.FindPrev(c.query)
724 def Find(self, value, direction, pattern, context, callback):
726 def __init__(self, *x):
727 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
728 def Update(self, *x):
729 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
731 context[0].Update(value, direction, pattern)
733 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
734 # Use a thread so the UI is not blocked during the SELECT
735 thread = Thread(self.FindThread, context[0])
736 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
739 def FindDone(self, thread, callback, ids):
742 # Context-sensitive call graph data model
744 class CallGraphModel(CallGraphModelBase):
746 def __init__(self, glb, parent=None):
747 super(CallGraphModel, self).__init__(glb, parent)
750 return CallGraphRootItem(self.glb, self.params)
752 def columnCount(self, parent=None):
753 if self.params.have_ipc:
758 def columnHeader(self, column):
759 if self.params.have_ipc:
760 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
762 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
763 return headers[column]
765 def columnAlignment(self, column):
766 if self.params.have_ipc:
767 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
769 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
770 return alignment[column]
772 def DoFindSelect(self, query, match):
773 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
775 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
776 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
777 " WHERE calls.id <> 0"
778 " AND symbols.name" + match +
779 " GROUP BY comm_id, thread_id, call_path_id"
780 " ORDER BY comm_id, thread_id, call_path_id")
782 def FindPath(self, query):
783 # Turn the query result into a list of ids that the tree view can walk
784 # to open the tree at the right place.
786 parent_id = query.value(0)
788 ids.insert(0, parent_id)
789 q2 = QSqlQuery(self.glb.db)
790 QueryExec(q2, "SELECT parent_id"
792 " WHERE id = " + str(parent_id))
795 parent_id = q2.value(0)
796 # The call path root is not used
799 ids.insert(0, query.value(2))
800 ids.insert(0, query.value(1))
803 # Call tree data model level 2+ item base
805 class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
807 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
808 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
809 self.comm_id = comm_id
810 self.thread_id = thread_id
811 self.calls_id = calls_id
812 self.call_time = call_time
814 self.insn_cnt = insn_cnt
815 self.cyc_cnt = cyc_cnt
816 self.branch_count = branch_count
819 self.query_done = True
820 if self.calls_id == 0:
821 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
824 if self.params.have_ipc:
825 ipc_str = ", insn_count, cyc_count"
828 query = QSqlQuery(self.glb.db)
829 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"
831 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
832 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
833 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
834 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
835 " ORDER BY call_time, calls.id")
837 if self.params.have_ipc:
838 insn_cnt = int(query.value(5))
839 cyc_cnt = int(query.value(6))
840 branch_count = int(query.value(7))
844 branch_count = int(query.value(5))
845 child_item = CallTreeLevelThreeItem(self.glb, self.params, 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)), insn_cnt, cyc_cnt, branch_count, self)
846 self.child_items.append(child_item)
847 self.child_count += 1
849 # Call tree data model level three item
851 class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
853 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, name, dso, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
854 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item)
856 if self.params.have_ipc:
857 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
858 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
859 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
860 ipc = CalcIPC(cyc_cnt, insn_cnt)
861 self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(insn_cnt), insn_pcnt, str(cyc_cnt), cyc_pcnt, ipc, str(branch_count), br_pcnt ]
863 self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
866 # Call tree data model level two item
868 class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
870 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
871 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item)
872 if self.params.have_ipc:
873 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
875 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
876 self.dbid = thread_id
879 super(CallTreeLevelTwoItem, self).Select()
880 for child_item in self.child_items:
881 self.time += child_item.time
882 self.insn_cnt += child_item.insn_cnt
883 self.cyc_cnt += child_item.cyc_cnt
884 self.branch_count += child_item.branch_count
885 for child_item in self.child_items:
886 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
887 if self.params.have_ipc:
888 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
889 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
890 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
892 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
894 # Call tree data model level one item
896 class CallTreeLevelOneItem(CallGraphLevelItemBase):
898 def __init__(self, glb, params, row, comm_id, comm, parent_item):
899 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
900 if self.params.have_ipc:
901 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
903 self.data = [comm, "", "", "", "", "", ""]
907 self.query_done = True
908 query = QSqlQuery(self.glb.db)
909 QueryExec(query, "SELECT thread_id, pid, tid"
911 " INNER JOIN threads ON thread_id = threads.id"
912 " WHERE comm_id = " + str(self.dbid))
914 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
915 self.child_items.append(child_item)
916 self.child_count += 1
918 # Call tree data model root item
920 class CallTreeRootItem(CallGraphLevelItemBase):
922 def __init__(self, glb, params):
923 super(CallTreeRootItem, self).__init__(glb, params, 0, None)
925 self.query_done = True
927 if IsSelectable(glb.db, "comms", columns = "has_calls"):
928 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
929 query = QSqlQuery(glb.db)
930 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
932 if not query.value(0):
934 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
935 self.child_items.append(child_item)
936 self.child_count += 1
938 # Call Tree data model
940 class CallTreeModel(CallGraphModelBase):
942 def __init__(self, glb, parent=None):
943 super(CallTreeModel, self).__init__(glb, parent)
946 return CallTreeRootItem(self.glb, self.params)
948 def columnCount(self, parent=None):
949 if self.params.have_ipc:
954 def columnHeader(self, column):
955 if self.params.have_ipc:
956 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
958 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
959 return headers[column]
961 def columnAlignment(self, column):
962 if self.params.have_ipc:
963 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
965 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
966 return alignment[column]
968 def DoFindSelect(self, query, match):
969 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
971 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
972 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
973 " WHERE calls.id <> 0"
974 " AND symbols.name" + match +
975 " ORDER BY comm_id, thread_id, call_time, calls.id")
977 def FindPath(self, query):
978 # Turn the query result into a list of ids that the tree view can walk
979 # to open the tree at the right place.
981 parent_id = query.value(0)
983 ids.insert(0, parent_id)
984 q2 = QSqlQuery(self.glb.db)
985 QueryExec(q2, "SELECT parent_id"
987 " WHERE id = " + str(parent_id))
990 parent_id = q2.value(0)
991 ids.insert(0, query.value(2))
992 ids.insert(0, query.value(1))
997 class HBoxLayout(QHBoxLayout):
999 def __init__(self, *children):
1000 super(HBoxLayout, self).__init__()
1002 self.layout().setContentsMargins(0, 0, 0, 0)
1003 for child in children:
1004 if child.isWidgetType():
1005 self.layout().addWidget(child)
1007 self.layout().addLayout(child)
1011 class VBoxLayout(QVBoxLayout):
1013 def __init__(self, *children):
1014 super(VBoxLayout, self).__init__()
1016 self.layout().setContentsMargins(0, 0, 0, 0)
1017 for child in children:
1018 if child.isWidgetType():
1019 self.layout().addWidget(child)
1021 self.layout().addLayout(child)
1023 # Vertical layout widget
1027 def __init__(self, *children):
1028 self.vbox = QWidget()
1029 self.vbox.setLayout(VBoxLayout(*children))
1036 class TreeWindowBase(QMdiSubWindow):
1038 def __init__(self, parent=None):
1039 super(TreeWindowBase, self).__init__(parent)
1042 self.find_bar = None
1044 self.view = QTreeView()
1045 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1046 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1048 self.context_menu = TreeContextMenu(self.view)
1050 def DisplayFound(self, ids):
1053 parent = QModelIndex()
1056 n = self.model.rowCount(parent)
1057 for row in xrange(n):
1058 child = self.model.index(row, 0, parent)
1059 if child.internalPointer().dbid == dbid:
1061 self.view.setExpanded(parent, True)
1062 self.view.setCurrentIndex(child)
1069 def Find(self, value, direction, pattern, context):
1070 self.view.setFocus()
1071 self.find_bar.Busy()
1072 self.model.Find(value, direction, pattern, context, self.FindDone)
1074 def FindDone(self, ids):
1076 if not self.DisplayFound(ids):
1078 self.find_bar.Idle()
1080 self.find_bar.NotFound()
1083 # Context-sensitive call graph window
1085 class CallGraphWindow(TreeWindowBase):
1087 def __init__(self, glb, parent=None):
1088 super(CallGraphWindow, self).__init__(parent)
1090 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
1092 self.view.setModel(self.model)
1094 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
1095 self.view.setColumnWidth(c, w)
1097 self.find_bar = FindBar(self, self)
1099 self.vbox = VBox(self.view, self.find_bar.Widget())
1101 self.setWidget(self.vbox.Widget())
1103 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
1107 class CallTreeWindow(TreeWindowBase):
1109 def __init__(self, glb, parent=None, thread_at_time=None):
1110 super(CallTreeWindow, self).__init__(parent)
1112 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
1114 self.view.setModel(self.model)
1116 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
1117 self.view.setColumnWidth(c, w)
1119 self.find_bar = FindBar(self, self)
1121 self.vbox = VBox(self.view, self.find_bar.Widget())
1123 self.setWidget(self.vbox.Widget())
1125 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
1128 self.DisplayThreadAtTime(*thread_at_time)
1130 def DisplayThreadAtTime(self, comm_id, thread_id, time):
1131 parent = QModelIndex()
1132 for dbid in (comm_id, thread_id):
1134 n = self.model.rowCount(parent)
1135 for row in xrange(n):
1136 child = self.model.index(row, 0, parent)
1137 if child.internalPointer().dbid == dbid:
1139 self.view.setExpanded(parent, True)
1140 self.view.setCurrentIndex(child)
1147 n = self.model.rowCount(parent)
1151 for row in xrange(n):
1152 self.view.setExpanded(parent, True)
1153 child = self.model.index(row, 0, parent)
1154 child_call_time = child.internalPointer().call_time
1155 if child_call_time < time:
1157 elif child_call_time == time:
1158 self.view.setCurrentIndex(child)
1160 elif child_call_time > time:
1164 child = self.model.index(0, 0, parent)
1165 self.view.setExpanded(parent, True)
1166 self.view.setCurrentIndex(child)
1169 self.view.setExpanded(parent, True)
1170 self.view.setCurrentIndex(last_child)
1173 # ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
1175 def ExecComm(db, thread_id, time):
1176 query = QSqlQuery(db)
1177 QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
1178 " FROM comm_threads"
1179 " INNER JOIN comms ON comms.id = comm_threads.comm_id"
1180 " WHERE comm_threads.thread_id = " + str(thread_id) +
1181 " ORDER BY comms.c_time, comms.id")
1186 first = query.value(0)
1187 if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
1188 last = query.value(0)
1189 if not(last is None):
1193 # Container for (x, y) data
1196 def __init__(self, x=0, y=0):
1201 return "XY({}, {})".format(str(self.x), str(self.y))
1203 # Container for sub-range data
1206 def __init__(self, lo=0, hi=0):
1211 return "Subrange({}, {})".format(str(self.lo), str(self.hi))
1213 # Graph data region base class
1215 class GraphDataRegion(object):
1217 def __init__(self, key, title = "", ordinal = ""):
1220 self.ordinal = ordinal
1222 # Function to sort GraphDataRegion
1224 def GraphDataRegionOrdinal(data_region):
1225 return data_region.ordinal
1227 # Attributes for a graph region
1229 class GraphRegionAttribute():
1231 def __init__(self, colour):
1232 self.colour = colour
1234 # Switch graph data region represents a task
1236 class SwitchGraphDataRegion(GraphDataRegion):
1238 def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
1239 super(SwitchGraphDataRegion, self).__init__(key)
1241 self.title = str(pid) + " / " + str(tid) + " " + comm
1242 # Order graph legend within exec comm by pid / tid / time
1243 self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
1244 self.exec_comm_id = exec_comm_id
1248 self.thread_id = thread_id
1249 self.comm_id = comm_id
1253 class GraphDataPoint():
1255 def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
1262 self.hregion = hregion
1263 self.vregion = vregion
1265 # Graph data (single graph) base class
1267 class GraphData(object):
1269 def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
1270 self.collection = collection
1276 def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
1277 index = len(self.points)
1279 x = float(Decimal(x) - self.xbase)
1280 y = float(Decimal(y) - self.ybase)
1282 self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
1284 def XToData(self, x):
1285 return Decimal(x) + self.xbase
1287 def YToData(self, y):
1288 return Decimal(y) + self.ybase
1290 # Switch graph data (for one CPU)
1292 class SwitchGraphData(GraphData):
1294 def __init__(self, db, collection, cpu, xbase):
1295 super(SwitchGraphData, self).__init__(collection, xbase)
1298 self.title = "CPU " + str(cpu)
1299 self.SelectSwitches(db)
1301 def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
1302 query = QSqlQuery(db)
1303 QueryExec(query, "SELECT id, c_time"
1305 " WHERE c_thread_id = " + str(thread_id) +
1306 " AND exec_flag = " + self.collection.glb.dbref.TRUE +
1307 " AND c_time >= " + str(start_time) +
1308 " AND c_time <= " + str(end_time) +
1309 " ORDER BY c_time, id")
1311 comm_id = query.value(0)
1312 if comm_id == last_comm_id:
1314 time = query.value(1)
1315 hregion = self.HRegion(db, thread_id, comm_id, time)
1316 self.AddPoint(time, 1000, None, None, hregion)
1318 def SelectSwitches(self, db):
1321 last_thread_id = None
1322 query = QSqlQuery(db)
1323 QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
1324 " FROM context_switches"
1325 " WHERE machine_id = " + str(self.collection.machine_id) +
1326 " AND cpu = " + str(self.cpu) +
1327 " ORDER BY time, id")
1329 flags = int(query.value(5))
1331 # Schedule-out: detect and add exec's
1332 if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
1333 self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
1335 # Schedule-in: add data point
1336 if len(self.points) == 0:
1337 start_time = self.collection.glb.StartTime(self.collection.machine_id)
1338 hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
1339 self.AddPoint(start_time, 1000, None, None, hregion)
1340 time = query.value(0)
1341 comm_id = query.value(4)
1342 thread_id = query.value(2)
1343 hregion = self.HRegion(db, thread_id, comm_id, time)
1344 self.AddPoint(time, 1000, None, None, hregion)
1346 last_comm_id = comm_id
1347 last_thread_id = thread_id
1349 def NewHRegion(self, db, key, thread_id, comm_id, time):
1350 exec_comm_id = ExecComm(db, thread_id, time)
1351 query = QSqlQuery(db)
1352 QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
1354 pid = query.value(0)
1355 tid = query.value(1)
1359 query = QSqlQuery(db)
1360 QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
1362 comm = query.value(0)
1365 return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
1367 def HRegion(self, db, thread_id, comm_id, time):
1368 key = str(thread_id) + ":" + str(comm_id)
1369 hregion = self.collection.LookupHRegion(key)
1371 hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
1372 self.collection.AddHRegion(key, hregion)
1375 # Graph data collection (multiple related graphs) base class
1377 class GraphDataCollection(object):
1379 def __init__(self, glb):
1383 self.xrangelo = None
1384 self.xrangehi = None
1385 self.yrangelo = None
1386 self.yrangehi = None
1389 def AddGraphData(self, data):
1390 self.data.append(data)
1392 def LookupHRegion(self, key):
1393 if key in self.hregions:
1394 return self.hregions[key]
1397 def AddHRegion(self, key, hregion):
1398 self.hregions[key] = hregion
1400 # Switch graph data collection (SwitchGraphData for each CPU)
1402 class SwitchGraphDataCollection(GraphDataCollection):
1404 def __init__(self, glb, db, machine_id):
1405 super(SwitchGraphDataCollection, self).__init__(glb)
1407 self.machine_id = machine_id
1408 self.cpus = self.SelectCPUs(db)
1410 self.xrangelo = glb.StartTime(machine_id)
1411 self.xrangehi = glb.FinishTime(machine_id)
1413 self.yrangelo = Decimal(0)
1414 self.yrangehi = Decimal(1000)
1416 for cpu in self.cpus:
1417 self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
1419 def SelectCPUs(self, db):
1421 query = QSqlQuery(db)
1422 QueryExec(query, "SELECT DISTINCT cpu"
1423 " FROM context_switches"
1424 " WHERE machine_id = " + str(self.machine_id))
1426 cpus.append(int(query.value(0)))
1429 # Switch graph data graphics item displays the graphed data
1431 class SwitchGraphDataGraphicsItem(QGraphicsItem):
1433 def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
1434 super(SwitchGraphDataGraphicsItem, self).__init__(parent)
1437 self.graph_width = graph_width
1438 self.graph_height = graph_height
1440 self.event_handler = event_handler
1441 self.setAcceptHoverEvents(True)
1443 def boundingRect(self):
1444 return QRectF(0, 0, self.graph_width, self.graph_height)
1446 def PaintPoint(self, painter, last, x):
1447 if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
1448 if last.x < self.attrs.subrange.x.lo:
1449 x0 = self.attrs.subrange.x.lo
1452 if x > self.attrs.subrange.x.hi:
1453 x1 = self.attrs.subrange.x.hi
1456 x0 = self.attrs.XToPixel(x0)
1457 x1 = self.attrs.XToPixel(x1)
1459 y0 = self.attrs.YToPixel(last.y)
1461 colour = self.attrs.region_attributes[last.hregion.key].colour
1465 painter.setPen(colour)
1466 painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
1468 painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
1470 def paint(self, painter, option, widget):
1472 for point in self.data.points:
1473 self.PaintPoint(painter, last, point.x)
1474 if point.x > self.attrs.subrange.x.hi:
1477 self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
1479 def BinarySearchPoint(self, target):
1481 higher_pos = len(self.data.points)
1483 pos = int((lower_pos + higher_pos) / 2)
1484 val = self.data.points[pos].x
1489 if higher_pos <= lower_pos + 1:
1492 def XPixelToData(self, x):
1493 x = self.attrs.PixelToX(x)
1494 if x < self.data.points[0].x:
1499 pos = self.BinarySearchPoint(x)
1501 return (low, pos, self.data.XToData(x))
1503 def EventToData(self, event):
1504 no_data = (None,) * 4
1505 if len(self.data.points) < 1:
1510 low0, pos0, time_from = self.XPixelToData(x)
1511 low1, pos1, time_to = self.XPixelToData(x + 1)
1515 for i in xrange(pos0, pos1 + 1):
1516 hregion = self.data.points[i].hregion
1517 hregions.add(hregion)
1521 time = self.data.XToData(self.data.points[i].x)
1522 hregion_times.append((hregion, time))
1523 return (time_from, time_to, hregions, hregion_times)
1525 def hoverMoveEvent(self, event):
1526 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1527 if time_from is not None:
1528 self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
1530 def hoverLeaveEvent(self, event):
1531 self.event_handler.NoPointEvent()
1533 def mousePressEvent(self, event):
1534 if event.button() != Qt.RightButton:
1535 super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
1537 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1539 self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
1541 # X-axis graphics item
1543 class XAxisGraphicsItem(QGraphicsItem):
1545 def __init__(self, width, parent=None):
1546 super(XAxisGraphicsItem, self).__init__(parent)
1549 self.max_mark_sz = 4
1550 self.height = self.max_mark_sz + 1
1552 def boundingRect(self):
1553 return QRectF(0, 0, self.width, self.height)
1556 attrs = self.parentItem().attrs
1557 subrange = attrs.subrange.x
1558 t = subrange.hi - subrange.lo
1559 s = (3.0 * t) / self.width
1565 def PaintMarks(self, painter, at_y, lo, hi, step, i):
1566 attrs = self.parentItem().attrs
1569 xp = attrs.XToPixel(x)
1576 sz = self.max_mark_sz
1578 painter.drawLine(xp, at_y, xp, at_y + sz)
1582 def paint(self, painter, option, widget):
1583 # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
1584 painter.drawLine(0, 0, self.width - 1, 0)
1586 attrs = self.parentItem().attrs
1587 subrange = attrs.subrange.x
1589 x_offset = n - (subrange.lo % n)
1592 x = subrange.lo + x_offset
1594 self.PaintMarks(painter, 0, x, subrange.hi, n, i)
1596 def ScaleDimensions(self):
1598 attrs = self.parentItem().attrs
1599 lo = attrs.subrange.x.lo
1600 hi = (n * 10.0) + lo
1601 width = attrs.XToPixel(hi)
1604 return (n, lo, hi, width)
1606 def PaintScale(self, painter, at_x, at_y):
1607 n, lo, hi, width = self.ScaleDimensions()
1610 painter.drawLine(at_x, at_y, at_x + width, at_y)
1611 self.PaintMarks(painter, at_y, lo, hi, n, 0)
1613 def ScaleWidth(self):
1614 n, lo, hi, width = self.ScaleDimensions()
1617 def ScaleHeight(self):
1620 def ScaleUnit(self):
1621 return self.Step() * 10
1623 # Scale graphics item base class
1625 class ScaleGraphicsItem(QGraphicsItem):
1627 def __init__(self, axis, parent=None):
1628 super(ScaleGraphicsItem, self).__init__(parent)
1631 def boundingRect(self):
1632 scale_width = self.axis.ScaleWidth()
1635 return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
1637 def paint(self, painter, option, widget):
1638 scale_width = self.axis.ScaleWidth()
1641 self.axis.PaintScale(painter, 0, 5)
1643 painter.drawText(QPointF(x, 10), self.Text())
1646 return self.axis.ScaleUnit()
1651 # Switch graph scale graphics item
1653 class SwitchScaleGraphicsItem(ScaleGraphicsItem):
1655 def __init__(self, axis, parent=None):
1656 super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
1660 if unit >= 1000000000:
1661 unit = int(unit / 1000000000)
1663 elif unit >= 1000000:
1664 unit = int(unit / 1000000)
1667 unit = int(unit / 1000)
1672 return " = " + str(unit) + " " + us
1674 # Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
1676 class SwitchGraphGraphicsItem(QGraphicsItem):
1678 def __init__(self, collection, data, attrs, event_handler, first, parent=None):
1679 super(SwitchGraphGraphicsItem, self).__init__(parent)
1680 self.collection = collection
1683 self.event_handler = event_handler
1688 self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
1690 self.title_graphics.setPos(margin, margin)
1691 graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
1692 graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
1694 self.graph_origin_x = margin + title_width + margin
1695 self.graph_origin_y = graph_height + margin
1699 self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
1701 self.x_axis = XAxisGraphicsItem(graph_width, self)
1702 self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
1705 self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
1706 self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
1708 self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
1710 self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
1711 self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
1713 self.width = self.graph_origin_x + graph_width + margin
1714 self.height = self.graph_origin_y + margin
1716 self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
1717 self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
1719 if parent and 'EnableRubberBand' in dir(parent):
1720 parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
1722 def boundingRect(self):
1723 return QRectF(0, 0, self.width, self.height)
1725 def paint(self, painter, option, widget):
1728 def RBXToPixel(self, x):
1729 return self.attrs.PixelToX(x - self.graph_origin_x)
1731 def RBXRangeToPixel(self, x0, x1):
1732 return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
1734 def RBPixelToTime(self, x):
1735 if x < self.data.points[0].x:
1736 return self.data.XToData(0)
1737 return self.data.XToData(x)
1739 def RBEventTimes(self, x0, x1):
1740 x0, x1 = self.RBXRangeToPixel(x0, x1)
1741 time_from = self.RBPixelToTime(x0)
1742 time_to = self.RBPixelToTime(x1)
1743 return (time_from, time_to)
1745 def RBEvent(self, x0, x1):
1746 time_from, time_to = self.RBEventTimes(x0, x1)
1747 self.event_handler.RangeEvent(time_from, time_to)
1749 def RBMoveEvent(self, x0, x1):
1752 self.RBEvent(x0, x1)
1754 def RBReleaseEvent(self, x0, x1, selection_state):
1757 x0, x1 = self.RBXRangeToPixel(x0, x1)
1758 self.event_handler.SelectEvent(x0, x1, selection_state)
1760 # Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
1762 class VerticalBracketGraphicsItem(QGraphicsItem):
1764 def __init__(self, parent=None):
1765 super(VerticalBracketGraphicsItem, self).__init__(parent)
1771 def SetSize(self, width, height):
1772 self.width = width + 1
1773 self.height = height + 1
1775 def boundingRect(self):
1776 return QRectF(0, 0, self.width, self.height)
1778 def paint(self, painter, option, widget):
1779 colour = QColor(255, 255, 0, 32)
1780 painter.fillRect(0, 0, self.width, self.height, colour)
1782 y1 = self.height - 1
1783 painter.drawLine(0, 0, x1, 0)
1784 painter.drawLine(0, 0, 0, 3)
1785 painter.drawLine(x1, 0, x1, 3)
1786 painter.drawLine(0, y1, x1, y1)
1787 painter.drawLine(0, y1, 0, y1 - 3)
1788 painter.drawLine(x1, y1, x1, y1 - 3)
1790 # Graphics item to contain graphs arranged vertically
1792 class VertcalGraphSetGraphicsItem(QGraphicsItem):
1794 def __init__(self, collection, attrs, event_handler, child_class, parent=None):
1795 super(VertcalGraphSetGraphicsItem, self).__init__(parent)
1797 self.collection = collection
1802 self.height = self.top
1804 self.rubber_band = None
1805 self.rb_enabled = False
1808 for data in collection.data:
1809 child = child_class(collection, data, attrs, event_handler, first, self)
1810 child.setPos(0, self.height + 1)
1811 rect = child.boundingRect()
1812 if rect.right() > self.width:
1813 self.width = rect.right()
1814 self.height = self.height + rect.bottom() + 1
1817 self.bracket = VerticalBracketGraphicsItem(self)
1819 def EnableRubberBand(self, xlo, xhi, rb_event_handler):
1822 self.rb_enabled = True
1823 self.rb_in_view = False
1824 self.setAcceptedMouseButtons(Qt.LeftButton)
1827 self.rb_event_handler = rb_event_handler
1828 self.mousePressEvent = self.MousePressEvent
1829 self.mouseMoveEvent = self.MouseMoveEvent
1830 self.mouseReleaseEvent = self.MouseReleaseEvent
1832 def boundingRect(self):
1833 return QRectF(0, 0, self.width, self.height)
1835 def paint(self, painter, option, widget):
1838 def RubberBandParent(self):
1839 scene = self.scene()
1840 view = scene.views()[0]
1841 viewport = view.viewport()
1844 def RubberBandSetGeometry(self, rect):
1845 scene_rectf = self.mapRectToScene(QRectF(rect))
1846 scene = self.scene()
1847 view = scene.views()[0]
1848 poly = view.mapFromScene(scene_rectf)
1849 self.rubber_band.setGeometry(poly.boundingRect())
1851 def SetSelection(self, selection_state):
1852 if self.rubber_band:
1854 self.RubberBandSetGeometry(selection_state)
1855 self.rubber_band.show()
1857 self.rubber_band.hide()
1859 def SetBracket(self, rect):
1861 x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
1862 self.bracket.setPos(x, y)
1863 self.bracket.SetSize(width, height)
1868 def RubberBandX(self, event):
1869 x = event.pos().toPoint().x()
1872 elif x > self.rb_xhi:
1875 self.rb_in_view = True
1878 def RubberBandRect(self, x):
1879 if self.rb_origin.x() <= x:
1880 width = x - self.rb_origin.x()
1881 rect = QRect(self.rb_origin, QSize(width, self.height))
1883 width = self.rb_origin.x() - x
1884 top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
1885 rect = QRect(top_left, QSize(width, self.height))
1888 def MousePressEvent(self, event):
1889 self.rb_in_view = False
1890 x = self.RubberBandX(event)
1891 self.rb_origin = QPoint(x, self.top)
1892 if self.rubber_band is None:
1893 self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
1894 self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
1896 self.rubber_band.show()
1897 self.rb_event_handler.RBMoveEvent(x, x)
1899 self.rubber_band.hide()
1901 def MouseMoveEvent(self, event):
1902 x = self.RubberBandX(event)
1903 rect = self.RubberBandRect(x)
1904 self.RubberBandSetGeometry(rect)
1906 self.rubber_band.show()
1907 self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
1909 def MouseReleaseEvent(self, event):
1910 x = self.RubberBandX(event)
1912 selection_state = self.RubberBandRect(x)
1914 selection_state = None
1915 self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
1917 # Switch graph legend data model
1919 class SwitchGraphLegendModel(QAbstractTableModel):
1921 def __init__(self, collection, region_attributes, parent=None):
1922 super(SwitchGraphLegendModel, self).__init__(parent)
1924 self.region_attributes = region_attributes
1926 self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
1927 self.child_count = len(self.child_items)
1929 self.highlight_set = set()
1931 self.column_headers = ("pid", "tid", "comm")
1933 def rowCount(self, parent):
1934 return self.child_count
1936 def headerData(self, section, orientation, role):
1937 if role != Qt.DisplayRole:
1939 if orientation != Qt.Horizontal:
1941 return self.columnHeader(section)
1943 def index(self, row, column, parent):
1944 return self.createIndex(row, column, self.child_items[row])
1946 def columnCount(self, parent=None):
1947 return len(self.column_headers)
1949 def columnHeader(self, column):
1950 return self.column_headers[column]
1952 def data(self, index, role):
1953 if role == Qt.BackgroundRole:
1954 child = self.child_items[index.row()]
1955 if child in self.highlight_set:
1956 return self.region_attributes[child.key].colour
1958 if role == Qt.ForegroundRole:
1959 child = self.child_items[index.row()]
1960 if child in self.highlight_set:
1961 return QColor(255, 255, 255)
1962 return self.region_attributes[child.key].colour
1963 if role != Qt.DisplayRole:
1965 hregion = self.child_items[index.row()]
1966 col = index.column()
1975 def SetHighlight(self, row, set_highlight):
1976 child = self.child_items[row]
1977 top_left = self.createIndex(row, 0, child)
1978 bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
1979 self.dataChanged.emit(top_left, bottom_right)
1981 def Highlight(self, highlight_set):
1982 for row in xrange(self.child_count):
1983 child = self.child_items[row]
1984 if child in self.highlight_set:
1985 if child not in highlight_set:
1986 self.SetHighlight(row, False)
1987 elif child in highlight_set:
1988 self.SetHighlight(row, True)
1989 self.highlight_set = highlight_set
1991 # Switch graph legend is a table
1993 class SwitchGraphLegend(QWidget):
1995 def __init__(self, collection, region_attributes, parent=None):
1996 super(SwitchGraphLegend, self).__init__(parent)
1998 self.data_model = SwitchGraphLegendModel(collection, region_attributes)
2000 self.model = QSortFilterProxyModel()
2001 self.model.setSourceModel(self.data_model)
2003 self.view = QTableView()
2004 self.view.setModel(self.model)
2005 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
2006 self.view.verticalHeader().setVisible(False)
2007 self.view.sortByColumn(-1, Qt.AscendingOrder)
2008 self.view.setSortingEnabled(True)
2009 self.view.resizeColumnsToContents()
2010 self.view.resizeRowsToContents()
2012 self.vbox = VBoxLayout(self.view)
2013 self.setLayout(self.vbox)
2015 sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
2016 sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
2017 self.saved_size = sz1
2019 def resizeEvent(self, event):
2020 self.saved_size = self.size().width()
2021 super(SwitchGraphLegend, self).resizeEvent(event)
2023 def Highlight(self, highlight_set):
2024 self.data_model.Highlight(highlight_set)
2027 def changeEvent(self, event):
2028 if event.type() == QEvent.FontChange:
2029 self.view.resizeRowsToContents()
2030 self.view.resizeColumnsToContents()
2031 # Need to resize rows again after column resize
2032 self.view.resizeRowsToContents()
2033 super(SwitchGraphLegend, self).changeEvent(event)
2035 # Random colour generation
2037 def RGBColourTooLight(r, g, b):
2042 if r <= 180 and g <= 180:
2048 def GenerateColours(x):
2050 for i in xrange(1, x):
2051 cs.append(int((255.0 / i) + 0.5))
2056 # Exclude black and colours that look too light against a white background
2057 if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
2059 colours.append(QColor(r, g, b))
2062 def GenerateNColours(n):
2063 for x in xrange(2, n + 2):
2064 colours = GenerateColours(x)
2065 if len(colours) >= n:
2069 def GenerateNRandomColours(n, seed):
2070 colours = GenerateNColours(n)
2072 random.shuffle(colours)
2075 # Graph attributes, in particular the scale and subrange that change when zooming
2077 class GraphAttributes():
2079 def __init__(self, scale, subrange, region_attributes, dp):
2081 self.subrange = subrange
2082 self.region_attributes = region_attributes
2083 # Rounding avoids errors due to finite floating point precision
2084 self.dp = dp # data decimal places
2087 def XToPixel(self, x):
2088 return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
2090 def YToPixel(self, y):
2091 return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
2093 def PixelToXRounded(self, px):
2094 return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
2096 def PixelToYRounded(self, py):
2097 return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
2099 def PixelToX(self, px):
2100 x = self.PixelToXRounded(px)
2102 rt = self.XToPixel(x)
2107 def PixelToY(self, py):
2108 y = self.PixelToYRounded(py)
2110 rt = self.YToPixel(y)
2115 def ToPDP(self, dp, scale):
2116 # Calculate pixel decimal places:
2117 # (10 ** dp) is the minimum delta in the data
2118 # scale it to get the minimum delta in pixels
2119 # log10 gives the number of decimals places negatively
2120 # subtrace 1 to divide by 10
2121 # round to the lower negative number
2122 # change the sign to get the number of decimals positively
2123 x = math.log10((10 ** dp) * scale)
2126 x = -int(math.floor(x) - 0.1)
2132 x = self.ToPDP(self.dp.x, self.scale.x)
2133 y = self.ToPDP(self.dp.y, self.scale.y)
2134 self.pdp = XY(x, y) # pixel decimal places
2136 # Switch graph splitter which divides the CPU graphs from the legend
2138 class SwitchGraphSplitter(QSplitter):
2140 def __init__(self, parent=None):
2141 super(SwitchGraphSplitter, self).__init__(parent)
2143 self.first_time = False
2145 def resizeEvent(self, ev):
2147 self.first_time = False
2148 sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
2149 sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
2150 sz0 = self.size().width() - self.handleWidth() - sz1
2151 self.setSizes([sz0, sz1])
2152 elif not(self.widget(1).saved_size is None):
2153 sz1 = self.widget(1).saved_size
2154 sz0 = self.size().width() - self.handleWidth() - sz1
2155 self.setSizes([sz0, sz1])
2156 super(SwitchGraphSplitter, self).resizeEvent(ev)
2158 # Graph widget base class
2160 class GraphWidget(QWidget):
2162 graph_title_changed = Signal(object)
2164 def __init__(self, parent=None):
2165 super(GraphWidget, self).__init__(parent)
2167 def GraphTitleChanged(self, title):
2168 self.graph_title_changed.emit(title)
2173 # Display time in s, ms, us or ns
2177 if val >= 1000000000:
2178 return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
2180 return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
2182 return "{} us".format((val / 1000).quantize(Decimal("0.001")))
2183 return "{} ns".format(val.quantize(Decimal("1")))
2185 # Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
2187 class SwitchGraphWidget(GraphWidget):
2189 def __init__(self, glb, collection, parent=None):
2190 super(SwitchGraphWidget, self).__init__(parent)
2193 self.collection = collection
2195 self.back_state = []
2196 self.forward_state = []
2197 self.selection_state = (None, None)
2198 self.fwd_rect = None
2199 self.start_time = self.glb.StartTime(collection.machine_id)
2202 hregions = collection.hregions.values()
2203 colours = GenerateNRandomColours(len(hregions), 1013)
2204 region_attributes = {}
2205 for hregion in hregions:
2206 if hregion.pid == 0 and hregion.tid == 0:
2207 region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
2209 region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
2212 # Default to entire range
2213 xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
2214 ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
2215 subrange = XY(xsubrange, ysubrange)
2217 scale = self.GetScaleForRange(subrange)
2219 self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
2221 self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
2223 self.scene = QGraphicsScene()
2224 self.scene.addItem(self.item)
2226 self.view = QGraphicsView(self.scene)
2227 self.view.centerOn(0, 0)
2228 self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
2230 self.legend = SwitchGraphLegend(collection, region_attributes)
2232 self.splitter = SwitchGraphSplitter()
2233 self.splitter.addWidget(self.view)
2234 self.splitter.addWidget(self.legend)
2236 self.point_label = QLabel("")
2237 self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
2239 self.back_button = QToolButton()
2240 self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
2241 self.back_button.setDisabled(True)
2242 self.back_button.released.connect(lambda: self.Back())
2244 self.forward_button = QToolButton()
2245 self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
2246 self.forward_button.setDisabled(True)
2247 self.forward_button.released.connect(lambda: self.Forward())
2249 self.zoom_button = QToolButton()
2250 self.zoom_button.setText("Zoom")
2251 self.zoom_button.setDisabled(True)
2252 self.zoom_button.released.connect(lambda: self.Zoom())
2254 self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
2256 self.vbox = VBoxLayout(self.splitter, self.hbox)
2258 self.setLayout(self.vbox)
2260 def GetScaleForRangeX(self, xsubrange):
2261 # Default graph 1000 pixels wide
2263 r = xsubrange.hi - xsubrange.lo
2266 def GetScaleForRangeY(self, ysubrange):
2267 # Default graph 50 pixels high
2269 r = ysubrange.hi - ysubrange.lo
2272 def GetScaleForRange(self, subrange):
2273 # Default graph 1000 pixels wide, 50 pixels high
2274 xscale = self.GetScaleForRangeX(subrange.x)
2275 yscale = self.GetScaleForRangeY(subrange.y)
2276 return XY(xscale, yscale)
2278 def PointEvent(self, cpu, time_from, time_to, hregions):
2279 text = "CPU: " + str(cpu)
2280 time_from = time_from.quantize(Decimal(1))
2281 rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
2282 text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
2283 self.point_label.setText(text)
2284 self.legend.Highlight(hregions)
2286 def RightClickEvent(self, cpu, hregion_times, pos):
2287 if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
2289 menu = QMenu(self.view)
2290 for hregion, time in hregion_times:
2291 thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
2292 menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
2293 menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
2296 def RightClickSelect(self, args):
2297 CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
2299 def NoPointEvent(self):
2300 self.point_label.setText("")
2301 self.legend.Highlight({})
2303 def RangeEvent(self, time_from, time_to):
2304 time_from = time_from.quantize(Decimal(1))
2305 time_to = time_to.quantize(Decimal(1))
2306 if time_to <= time_from:
2307 self.point_label.setText("")
2309 rel_time_from = time_from - self.start_time
2310 rel_time_to = time_to - self.start_time
2311 text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
2312 text = text + " duration: " + ToTimeStr(time_to - time_from)
2313 self.point_label.setText(text)
2315 def BackState(self):
2316 return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
2318 def PushBackState(self):
2319 state = copy.deepcopy(self.BackState())
2320 self.back_state.append(state)
2321 self.back_button.setEnabled(True)
2323 def PopBackState(self):
2324 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
2326 if not self.back_state:
2327 self.back_button.setDisabled(True)
2329 def PushForwardState(self):
2330 state = copy.deepcopy(self.BackState())
2331 self.forward_state.append(state)
2332 self.forward_button.setEnabled(True)
2334 def PopForwardState(self):
2335 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
2337 if not self.forward_state:
2338 self.forward_button.setDisabled(True)
2341 time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
2342 time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
2343 rel_time_from = time_from - self.start_time
2344 rel_time_to = time_to - self.start_time
2345 title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
2346 title = title + " (" + ToTimeStr(time_to - time_from) + ")"
2350 selected_subrange, selection_state = self.selection_state
2351 self.item.SetSelection(selection_state)
2352 self.item.SetBracket(self.fwd_rect)
2353 self.zoom_button.setDisabled(selected_subrange is None)
2354 self.GraphTitleChanged(self.Title())
2355 self.item.update(self.item.boundingRect())
2358 if not self.back_state:
2360 self.PushForwardState()
2365 if not self.forward_state:
2367 self.PushBackState()
2368 self.PopForwardState()
2371 def SelectEvent(self, x0, x1, selection_state):
2372 if selection_state is None:
2373 selected_subrange = None
2377 selected_subrange = Subrange(x0, x1)
2378 self.selection_state = (selected_subrange, selection_state)
2379 self.zoom_button.setDisabled(selected_subrange is None)
2382 selected_subrange, selection_state = self.selection_state
2383 if selected_subrange is None:
2385 self.fwd_rect = selection_state
2386 self.item.SetSelection(None)
2387 self.PushBackState()
2388 self.attrs.subrange.x = selected_subrange
2389 self.forward_state = []
2390 self.forward_button.setDisabled(True)
2391 self.selection_state = (None, None)
2392 self.fwd_rect = None
2393 self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
2397 # Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
2399 class SlowInitClass():
2401 def __init__(self, glb, title, init_fn):
2402 self.init_fn = init_fn
2406 self.msg_box = QMessageBox(glb.mainwindow)
2407 self.msg_box.setText("Initializing " + title + ". Please wait.")
2408 self.msg_box.setWindowTitle("Initializing " + title)
2409 self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
2411 self.init_thread = Thread(self.ThreadFn, glb)
2412 self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
2414 self.init_thread.start()
2417 self.msg_box.done(0)
2419 def ThreadFn(self, glb):
2420 conn_name = "SlowInitClass" + str(os.getpid())
2421 db, dbname = glb.dbref.Open(conn_name)
2422 self.result = self.init_fn(db)
2427 while not self.done:
2428 self.msg_box.exec_()
2429 self.init_thread.wait()
2432 def SlowInit(glb, title, init_fn):
2433 init = SlowInitClass(glb, title, init_fn)
2434 return init.Result()
2436 # Time chart by CPU window
2438 class TimeChartByCPUWindow(QMdiSubWindow):
2440 def __init__(self, glb, parent=None):
2441 super(TimeChartByCPUWindow, self).__init__(parent)
2444 self.machine_id = glb.HostMachineId()
2445 self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
2447 collection = LookupModel(self.collection_name)
2448 if collection is None:
2449 collection = SlowInit(glb, "Time Chart", self.Init)
2451 self.widget = SwitchGraphWidget(glb, collection, self)
2452 self.view = self.widget
2454 self.base_title = "Time Chart by CPU"
2455 self.setWindowTitle(self.base_title + self.widget.Title())
2456 self.widget.graph_title_changed.connect(self.GraphTitleChanged)
2458 self.setWidget(self.widget)
2460 AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
2463 return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
2465 def GraphTitleChanged(self, title):
2466 self.setWindowTitle(self.base_title + " : " + title)
2468 # Child data item finder
2470 class ChildDataItemFinder():
2472 def __init__(self, root):
2474 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
2478 def FindSelect(self):
2481 pattern = re.compile(self.value)
2482 for child in self.root.child_items:
2483 for column_data in child.data:
2484 if re.search(pattern, str(column_data)) is not None:
2485 self.rows.append(child.row)
2488 for child in self.root.child_items:
2489 for column_data in child.data:
2490 if self.value in str(column_data):
2491 self.rows.append(child.row)
2494 def FindValue(self):
2496 if self.last_value != self.value or self.pattern != self.last_pattern:
2498 if not len(self.rows):
2500 return self.rows[self.pos]
2502 def FindThread(self):
2503 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
2504 row = self.FindValue()
2505 elif len(self.rows):
2506 if self.direction > 0:
2508 if self.pos >= len(self.rows):
2513 self.pos = len(self.rows) - 1
2514 row = self.rows[self.pos]
2519 def Find(self, value, direction, pattern, context, callback):
2520 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
2521 # Use a thread so the UI is not blocked
2522 thread = Thread(self.FindThread)
2523 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
2526 def FindDone(self, thread, callback, row):
2529 # Number of database records to fetch in one go
2531 glb_chunk_sz = 10000
2533 # Background process for SQL data fetcher
2535 class SQLFetcherProcess():
2537 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
2538 # Need a unique connection name
2539 conn_name = "SQLFetcher" + str(os.getpid())
2540 self.db, dbname = dbref.Open(conn_name)
2542 self.buffer = buffer
2545 self.fetch_count = fetch_count
2546 self.fetching_done = fetching_done
2547 self.process_target = process_target
2548 self.wait_event = wait_event
2549 self.fetched_event = fetched_event
2551 self.query = QSqlQuery(self.db)
2552 self.query_limit = 0 if "$$last_id$$" in sql else 2
2556 self.local_head = self.head.value
2557 self.local_tail = self.tail.value
2560 if self.query_limit:
2561 if self.query_limit == 1:
2563 self.query_limit -= 1
2564 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
2565 QueryExec(self.query, stmt)
2568 if not self.query.next():
2570 if not self.query.next():
2572 self.last_id = self.query.value(0)
2573 return self.prep(self.query)
2575 def WaitForTarget(self):
2577 self.wait_event.clear()
2578 target = self.process_target.value
2579 if target > self.fetched or target < 0:
2581 self.wait_event.wait()
2584 def HasSpace(self, sz):
2585 if self.local_tail <= self.local_head:
2586 space = len(self.buffer) - self.local_head
2589 if space >= glb_nsz:
2590 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
2591 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
2592 self.buffer[self.local_head : self.local_head + len(nd)] = nd
2594 if self.local_tail - self.local_head > sz:
2598 def WaitForSpace(self, sz):
2599 if self.HasSpace(sz):
2602 self.wait_event.clear()
2603 self.local_tail = self.tail.value
2604 if self.HasSpace(sz):
2606 self.wait_event.wait()
2608 def AddToBuffer(self, obj):
2609 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
2611 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
2613 self.WaitForSpace(sz)
2614 pos = self.local_head
2615 self.buffer[pos : pos + len(nd)] = nd
2616 self.buffer[pos + glb_nsz : pos + sz] = d
2617 self.local_head += sz
2619 def FetchBatch(self, batch_size):
2621 while batch_size > fetched:
2626 self.AddToBuffer(obj)
2629 self.fetched += fetched
2630 with self.fetch_count.get_lock():
2631 self.fetch_count.value += fetched
2632 self.head.value = self.local_head
2633 self.fetched_event.set()
2637 target = self.WaitForTarget()
2640 batch_size = min(glb_chunk_sz, target - self.fetched)
2641 self.FetchBatch(batch_size)
2642 self.fetching_done.value = True
2643 self.fetched_event.set()
2645 def SQLFetcherFn(*x):
2646 process = SQLFetcherProcess(*x)
2651 class SQLFetcher(QObject):
2653 done = Signal(object)
2655 def __init__(self, glb, sql, prep, process_data, parent=None):
2656 super(SQLFetcher, self).__init__(parent)
2657 self.process_data = process_data
2660 self.last_target = 0
2662 self.buffer_size = 16 * 1024 * 1024
2663 self.buffer = Array(c_char, self.buffer_size, lock=False)
2664 self.head = Value(c_longlong)
2665 self.tail = Value(c_longlong)
2667 self.fetch_count = Value(c_longlong)
2668 self.fetching_done = Value(c_bool)
2670 self.process_target = Value(c_longlong)
2671 self.wait_event = Event()
2672 self.fetched_event = Event()
2673 glb.AddInstanceToShutdownOnExit(self)
2674 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))
2675 self.process.start()
2676 self.thread = Thread(self.Thread)
2677 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
2681 # Tell the thread and process to exit
2682 self.process_target.value = -1
2683 self.wait_event.set()
2685 self.fetching_done.value = True
2686 self.fetched_event.set()
2692 self.fetched_event.clear()
2693 fetch_count = self.fetch_count.value
2694 if fetch_count != self.last_count:
2696 if self.fetching_done.value:
2699 self.fetched_event.wait()
2700 count = fetch_count - self.last_count
2701 self.last_count = fetch_count
2702 self.fetched += count
2705 def Fetch(self, nr):
2707 # -1 inidcates there are no more
2709 result = self.fetched
2710 extra = result + nr - self.target
2712 self.target += extra
2713 # process_target < 0 indicates shutting down
2714 if self.process_target.value >= 0:
2715 self.process_target.value = self.target
2716 self.wait_event.set()
2719 def RemoveFromBuffer(self):
2720 pos = self.local_tail
2721 if len(self.buffer) - pos < glb_nsz:
2723 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
2726 n = pickle.loads(self.buffer[0 : glb_nsz])
2728 obj = pickle.loads(self.buffer[pos : pos + n])
2729 self.local_tail = pos + n
2732 def ProcessData(self, count):
2733 for i in xrange(count):
2734 obj = self.RemoveFromBuffer()
2735 self.process_data(obj)
2736 self.tail.value = self.local_tail
2737 self.wait_event.set()
2738 self.done.emit(count)
2740 # Fetch more records bar
2742 class FetchMoreRecordsBar():
2744 def __init__(self, model, parent):
2747 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
2748 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2750 self.fetch_count = QSpinBox()
2751 self.fetch_count.setRange(1, 1000000)
2752 self.fetch_count.setValue(10)
2753 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2755 self.fetch = QPushButton("Go!")
2756 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2757 self.fetch.released.connect(self.FetchMoreRecords)
2759 self.progress = QProgressBar()
2760 self.progress.setRange(0, 100)
2761 self.progress.hide()
2763 self.done_label = QLabel("All records fetched")
2764 self.done_label.hide()
2766 self.spacer = QLabel("")
2768 self.close_button = QToolButton()
2769 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
2770 self.close_button.released.connect(self.Deactivate)
2772 self.hbox = QHBoxLayout()
2773 self.hbox.setContentsMargins(0, 0, 0, 0)
2775 self.hbox.addWidget(self.label)
2776 self.hbox.addWidget(self.fetch_count)
2777 self.hbox.addWidget(self.fetch)
2778 self.hbox.addWidget(self.spacer)
2779 self.hbox.addWidget(self.progress)
2780 self.hbox.addWidget(self.done_label)
2781 self.hbox.addWidget(self.close_button)
2783 self.bar = QWidget()
2784 self.bar.setLayout(self.hbox)
2787 self.in_progress = False
2788 self.model.progress.connect(self.Progress)
2792 if not model.HasMoreRecords():
2800 self.fetch.setFocus()
2802 def Deactivate(self):
2805 def Enable(self, enable):
2806 self.fetch.setEnabled(enable)
2807 self.fetch_count.setEnabled(enable)
2813 self.progress.show()
2816 self.in_progress = False
2818 self.progress.hide()
2823 return self.fetch_count.value() * glb_chunk_sz
2829 self.fetch_count.hide()
2832 self.done_label.show()
2834 def Progress(self, count):
2835 if self.in_progress:
2837 percent = ((count - self.start) * 100) / self.Target()
2841 self.progress.setValue(percent)
2843 # Count value of zero means no more records
2846 def FetchMoreRecords(self):
2849 self.progress.setValue(0)
2851 self.in_progress = True
2852 self.start = self.model.FetchMoreRecords(self.Target())
2854 # Brance data model level two item
2856 class BranchLevelTwoItem():
2858 def __init__(self, row, col, text, parent_item):
2860 self.parent_item = parent_item
2861 self.data = [""] * (col + 1)
2862 self.data[col] = text
2865 def getParentItem(self):
2866 return self.parent_item
2871 def childCount(self):
2874 def hasChildren(self):
2877 def getData(self, column):
2878 return self.data[column]
2880 # Brance data model level one item
2882 class BranchLevelOneItem():
2884 def __init__(self, glb, row, data, parent_item):
2887 self.parent_item = parent_item
2888 self.child_count = 0
2889 self.child_items = []
2890 self.data = data[1:]
2893 self.query_done = False
2894 self.br_col = len(self.data) - 1
2896 def getChildItem(self, row):
2897 return self.child_items[row]
2899 def getParentItem(self):
2900 return self.parent_item
2906 self.query_done = True
2908 if not self.glb.have_disassembler:
2911 query = QSqlQuery(self.glb.db)
2913 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
2915 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
2916 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
2917 " WHERE samples.id = " + str(self.dbid))
2918 if not query.next():
2920 cpu = query.value(0)
2921 dso = query.value(1)
2922 sym = query.value(2)
2923 if dso == 0 or sym == 0:
2925 off = query.value(3)
2926 short_name = query.value(4)
2927 long_name = query.value(5)
2928 build_id = query.value(6)
2929 sym_start = query.value(7)
2932 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
2934 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
2935 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
2936 " ORDER BY samples.id"
2938 if not query.next():
2940 if query.value(0) != dso:
2941 # Cannot disassemble from one dso to another
2943 bsym = query.value(1)
2944 boff = query.value(2)
2945 bsym_start = query.value(3)
2948 tot = bsym_start + boff + 1 - sym_start - off
2949 if tot <= 0 or tot > 16384:
2952 inst = self.glb.disassembler.Instruction()
2953 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
2956 mode = 0 if Is64Bit(f) else 1
2957 self.glb.disassembler.SetMode(inst, mode)
2960 buf = create_string_buffer(tot + 16)
2961 f.seek(sym_start + off)
2962 buf.value = f.read(buf_sz)
2963 buf_ptr = addressof(buf)
2966 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
2968 byte_str = tohex(ip).rjust(16)
2969 for k in xrange(cnt):
2970 byte_str += " %02x" % ord(buf[i])
2975 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
2976 self.child_count += 1
2984 def childCount(self):
2985 if not self.query_done:
2987 if not self.child_count:
2989 return self.child_count
2991 def hasChildren(self):
2992 if not self.query_done:
2994 return self.child_count > 0
2996 def getData(self, column):
2997 return self.data[column]
2999 # Brance data model root item
3001 class BranchRootItem():
3004 self.child_count = 0
3005 self.child_items = []
3008 def getChildItem(self, row):
3009 return self.child_items[row]
3011 def getParentItem(self):
3017 def childCount(self):
3018 return self.child_count
3020 def hasChildren(self):
3021 return self.child_count > 0
3023 def getData(self, column):
3026 # Calculate instructions per cycle
3028 def CalcIPC(cyc_cnt, insn_cnt):
3029 if cyc_cnt and insn_cnt:
3030 ipc = Decimal(float(insn_cnt) / cyc_cnt)
3031 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
3036 # Branch data preparation
3038 def BranchDataPrepBr(query, data):
3039 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
3040 " (" + dsoname(query.value(11)) + ")" + " -> " +
3041 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
3042 " (" + dsoname(query.value(15)) + ")")
3044 def BranchDataPrepIPC(query, data):
3045 insn_cnt = query.value(16)
3046 cyc_cnt = query.value(17)
3047 ipc = CalcIPC(cyc_cnt, insn_cnt)
3048 data.append(insn_cnt)
3049 data.append(cyc_cnt)
3052 def BranchDataPrep(query):
3054 for i in xrange(0, 8):
3055 data.append(query.value(i))
3056 BranchDataPrepBr(query, data)
3059 def BranchDataPrepWA(query):
3061 data.append(query.value(0))
3062 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3063 data.append("{:>19}".format(query.value(1)))
3064 for i in xrange(2, 8):
3065 data.append(query.value(i))
3066 BranchDataPrepBr(query, data)
3069 def BranchDataWithIPCPrep(query):
3071 for i in xrange(0, 8):
3072 data.append(query.value(i))
3073 BranchDataPrepIPC(query, data)
3074 BranchDataPrepBr(query, data)
3077 def BranchDataWithIPCPrepWA(query):
3079 data.append(query.value(0))
3080 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3081 data.append("{:>19}".format(query.value(1)))
3082 for i in xrange(2, 8):
3083 data.append(query.value(i))
3084 BranchDataPrepIPC(query, data)
3085 BranchDataPrepBr(query, data)
3090 class BranchModel(TreeModel):
3092 progress = Signal(object)
3094 def __init__(self, glb, event_id, where_clause, parent=None):
3095 super(BranchModel, self).__init__(glb, None, parent)
3096 self.event_id = event_id
3099 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
3101 select_ipc = ", insn_count, cyc_count"
3102 prep_fn = BranchDataWithIPCPrep
3103 prep_wa_fn = BranchDataWithIPCPrepWA
3106 prep_fn = BranchDataPrep
3107 prep_wa_fn = BranchDataPrepWA
3108 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
3109 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
3110 " ip, symbols.name, sym_offset, dsos.short_name,"
3111 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
3114 " INNER JOIN comms ON comm_id = comms.id"
3115 " INNER JOIN threads ON thread_id = threads.id"
3116 " INNER JOIN branch_types ON branch_type = branch_types.id"
3117 " INNER JOIN symbols ON symbol_id = symbols.id"
3118 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
3119 " INNER JOIN dsos ON samples.dso_id = dsos.id"
3120 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
3121 " WHERE samples.id > $$last_id$$" + where_clause +
3122 " AND evsel_id = " + str(self.event_id) +
3123 " ORDER BY samples.id"
3124 " LIMIT " + str(glb_chunk_sz))
3125 if pyside_version_1 and sys.version_info[0] == 3:
3129 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
3130 self.fetcher.done.connect(self.Update)
3131 self.fetcher.Fetch(glb_chunk_sz)
3134 return BranchRootItem()
3136 def columnCount(self, parent=None):
3142 def columnHeader(self, column):
3144 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
3146 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
3148 def columnFont(self, column):
3153 if column != br_col:
3155 return QFont("Monospace")
3157 def DisplayData(self, item, index):
3159 self.FetchIfNeeded(item.row)
3160 return item.getData(index.column())
3162 def AddSample(self, data):
3163 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
3164 self.root.child_items.append(child)
3167 def Update(self, fetched):
3170 self.progress.emit(0)
3171 child_count = self.root.child_count
3172 count = self.populated - child_count
3174 parent = QModelIndex()
3175 self.beginInsertRows(parent, child_count, child_count + count - 1)
3176 self.insertRows(child_count, count, parent)
3177 self.root.child_count += count
3178 self.endInsertRows()
3179 self.progress.emit(self.root.child_count)
3181 def FetchMoreRecords(self, count):
3182 current = self.root.child_count
3184 self.fetcher.Fetch(count)
3186 self.progress.emit(0)
3189 def HasMoreRecords(self):
3196 def __init__(self, name = "", where_clause = "", limit = ""):
3198 self.where_clause = where_clause
3202 return str(self.where_clause + ";" + self.limit)
3206 class BranchWindow(QMdiSubWindow):
3208 def __init__(self, glb, event_id, report_vars, parent=None):
3209 super(BranchWindow, self).__init__(parent)
3211 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
3213 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
3215 self.view = QTreeView()
3216 self.view.setUniformRowHeights(True)
3217 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
3218 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
3219 self.view.setModel(self.model)
3221 self.ResizeColumnsToContents()
3223 self.context_menu = TreeContextMenu(self.view)
3225 self.find_bar = FindBar(self, self, True)
3227 self.finder = ChildDataItemFinder(self.model.root)
3229 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
3231 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
3233 self.setWidget(self.vbox.Widget())
3235 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
3237 def ResizeColumnToContents(self, column, n):
3238 # Using the view's resizeColumnToContents() here is extrememly slow
3239 # so implement a crude alternative
3240 mm = "MM" if column else "MMMM"
3241 font = self.view.font()
3242 metrics = QFontMetrics(font)
3244 for row in xrange(n):
3245 val = self.model.root.child_items[row].data[column]
3246 len = metrics.width(str(val) + mm)
3247 max = len if len > max else max
3248 val = self.model.columnHeader(column)
3249 len = metrics.width(str(val) + mm)
3250 max = len if len > max else max
3251 self.view.setColumnWidth(column, max)
3253 def ResizeColumnsToContents(self):
3254 n = min(self.model.root.child_count, 100)
3256 # No data yet, so connect a signal to notify when there is
3257 self.model.rowsInserted.connect(self.UpdateColumnWidths)
3259 columns = self.model.columnCount()
3260 for i in xrange(columns):
3261 self.ResizeColumnToContents(i, n)
3263 def UpdateColumnWidths(self, *x):
3264 # This only needs to be done once, so disconnect the signal now
3265 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
3266 self.ResizeColumnsToContents()
3268 def Find(self, value, direction, pattern, context):
3269 self.view.setFocus()
3270 self.find_bar.Busy()
3271 self.finder.Find(value, direction, pattern, context, self.FindDone)
3273 def FindDone(self, row):
3274 self.find_bar.Idle()
3276 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
3278 self.find_bar.NotFound()
3280 # Line edit data item
3282 class LineEditDataItem(object):
3284 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3287 self.placeholder_text = placeholder_text
3288 self.parent = parent
3291 self.value = default
3293 self.widget = QLineEdit(default)
3294 self.widget.editingFinished.connect(self.Validate)
3295 self.widget.textChanged.connect(self.Invalidate)
3298 self.validated = True
3300 if placeholder_text:
3301 self.widget.setPlaceholderText(placeholder_text)
3303 def TurnTextRed(self):
3305 palette = QPalette()
3306 palette.setColor(QPalette.Text,Qt.red)
3307 self.widget.setPalette(palette)
3310 def TurnTextNormal(self):
3312 palette = QPalette()
3313 self.widget.setPalette(palette)
3316 def InvalidValue(self, value):
3319 self.error = self.label + " invalid value '" + value + "'"
3320 self.parent.ShowMessage(self.error)
3322 def Invalidate(self):
3323 self.validated = False
3325 def DoValidate(self, input_string):
3326 self.value = input_string.strip()
3329 self.validated = True
3331 self.TurnTextNormal()
3332 self.parent.ClearMessage()
3333 input_string = self.widget.text()
3334 if not len(input_string.strip()):
3337 self.DoValidate(input_string)
3340 if not self.validated:
3343 self.parent.ShowMessage(self.error)
3347 def IsNumber(self, value):
3352 return str(x) == value
3354 # Non-negative integer ranges dialog data item
3356 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
3358 def __init__(self, glb, label, placeholder_text, column_name, parent):
3359 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3361 self.column_name = column_name
3363 def DoValidate(self, input_string):
3366 for value in [x.strip() for x in input_string.split(",")]:
3368 vrange = value.split("-")
3369 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3370 return self.InvalidValue(value)
3371 ranges.append(vrange)
3373 if not self.IsNumber(value):
3374 return self.InvalidValue(value)
3375 singles.append(value)
3376 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3378 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
3379 self.value = " OR ".join(ranges)
3381 # Positive integer dialog data item
3383 class PositiveIntegerDataItem(LineEditDataItem):
3385 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3386 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
3388 def DoValidate(self, input_string):
3389 if not self.IsNumber(input_string.strip()):
3390 return self.InvalidValue(input_string)
3391 value = int(input_string.strip())
3393 return self.InvalidValue(input_string)
3394 self.value = str(value)
3396 # Dialog data item converted and validated using a SQL table
3398 class SQLTableDataItem(LineEditDataItem):
3400 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
3401 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
3403 self.table_name = table_name
3404 self.match_column = match_column
3405 self.column_name1 = column_name1
3406 self.column_name2 = column_name2
3408 def ValueToIds(self, value):
3410 query = QSqlQuery(self.glb.db)
3411 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
3412 ret = query.exec_(stmt)
3415 ids.append(str(query.value(0)))
3418 def DoValidate(self, input_string):
3420 for value in [x.strip() for x in input_string.split(",")]:
3421 ids = self.ValueToIds(value)
3425 return self.InvalidValue(value)
3426 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
3427 if self.column_name2:
3428 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
3430 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
3432 class SampleTimeRangesDataItem(LineEditDataItem):
3434 def __init__(self, glb, label, placeholder_text, column_name, parent):
3435 self.column_name = column_name
3439 self.last_time = 2 ** 64
3441 query = QSqlQuery(glb.db)
3442 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
3444 self.last_id = int(query.value(0))
3445 self.first_time = int(glb.HostStartTime())
3446 self.last_time = int(glb.HostFinishTime())
3447 if placeholder_text:
3448 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
3450 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3452 def IdBetween(self, query, lower_id, higher_id, order):
3453 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
3455 return True, int(query.value(0))
3459 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
3460 query = QSqlQuery(self.glb.db)
3462 next_id = int((lower_id + higher_id) / 2)
3463 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3464 if not query.next():
3465 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
3467 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
3469 return str(higher_id)
3471 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3472 next_time = int(query.value(0))
3474 if target_time > next_time:
3478 if higher_id <= lower_id + 1:
3479 return str(higher_id)
3481 if target_time >= next_time:
3485 if higher_id <= lower_id + 1:
3486 return str(lower_id)
3488 def ConvertRelativeTime(self, val):
3493 elif suffix == "us":
3495 elif suffix == "ns":
3499 val = val[:-2].strip()
3500 if not self.IsNumber(val):
3502 val = int(val) * mult
3504 val += self.first_time
3506 val += self.last_time
3509 def ConvertTimeRange(self, vrange):
3511 vrange[0] = str(self.first_time)
3513 vrange[1] = str(self.last_time)
3514 vrange[0] = self.ConvertRelativeTime(vrange[0])
3515 vrange[1] = self.ConvertRelativeTime(vrange[1])
3516 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3518 beg_range = max(int(vrange[0]), self.first_time)
3519 end_range = min(int(vrange[1]), self.last_time)
3520 if beg_range > self.last_time or end_range < self.first_time:
3522 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
3523 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
3526 def AddTimeRange(self, value, ranges):
3527 n = value.count("-")
3531 if value.split("-")[1].strip() == "":
3537 pos = findnth(value, "-", n)
3538 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
3539 if self.ConvertTimeRange(vrange):
3540 ranges.append(vrange)
3544 def DoValidate(self, input_string):
3546 for value in [x.strip() for x in input_string.split(",")]:
3547 if not self.AddTimeRange(value, ranges):
3548 return self.InvalidValue(value)
3549 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3550 self.value = " OR ".join(ranges)
3552 # Report Dialog Base
3554 class ReportDialogBase(QDialog):
3556 def __init__(self, glb, title, items, partial, parent=None):
3557 super(ReportDialogBase, self).__init__(parent)
3561 self.report_vars = ReportVars()
3563 self.setWindowTitle(title)
3564 self.setMinimumWidth(600)
3566 self.data_items = [x(glb, self) for x in items]
3568 self.partial = partial
3570 self.grid = QGridLayout()
3572 for row in xrange(len(self.data_items)):
3573 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
3574 self.grid.addWidget(self.data_items[row].widget, row, 1)
3576 self.status = QLabel()
3578 self.ok_button = QPushButton("Ok", self)
3579 self.ok_button.setDefault(True)
3580 self.ok_button.released.connect(self.Ok)
3581 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3583 self.cancel_button = QPushButton("Cancel", self)
3584 self.cancel_button.released.connect(self.reject)
3585 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3587 self.hbox = QHBoxLayout()
3588 #self.hbox.addStretch()
3589 self.hbox.addWidget(self.status)
3590 self.hbox.addWidget(self.ok_button)
3591 self.hbox.addWidget(self.cancel_button)
3593 self.vbox = QVBoxLayout()
3594 self.vbox.addLayout(self.grid)
3595 self.vbox.addLayout(self.hbox)
3597 self.setLayout(self.vbox)
3600 vars = self.report_vars
3601 for d in self.data_items:
3602 if d.id == "REPORTNAME":
3605 self.ShowMessage("Report name is required")
3607 for d in self.data_items:
3610 for d in self.data_items[1:]:
3612 vars.limit = d.value
3614 if len(vars.where_clause):
3615 vars.where_clause += " AND "
3616 vars.where_clause += d.value
3617 if len(vars.where_clause):
3619 vars.where_clause = " AND ( " + vars.where_clause + " ) "
3621 vars.where_clause = " WHERE " + vars.where_clause + " "
3624 def ShowMessage(self, msg):
3625 self.status.setText("<font color=#FF0000>" + msg)
3627 def ClearMessage(self):
3628 self.status.setText("")
3630 # Selected branch report creation dialog
3632 class SelectedBranchDialog(ReportDialogBase):
3634 def __init__(self, glb, parent=None):
3635 title = "Selected Branches"
3636 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
3637 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
3638 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
3639 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
3640 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
3641 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
3642 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
3643 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
3644 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
3645 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
3649 def GetEventList(db):
3651 query = QSqlQuery(db)
3652 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
3654 events.append(query.value(0))
3657 # Is a table selectable
3659 def IsSelectable(db, table, sql = "", columns = "*"):
3660 query = QSqlQuery(db)
3662 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
3667 # SQL table data model item
3669 class SQLTableItem():
3671 def __init__(self, row, data):
3675 def getData(self, column):
3676 return self.data[column]
3678 # SQL table data model
3680 class SQLTableModel(TableModel):
3682 progress = Signal(object)
3684 def __init__(self, glb, sql, column_headers, parent=None):
3685 super(SQLTableModel, self).__init__(parent)
3689 self.column_headers = column_headers
3690 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
3691 self.fetcher.done.connect(self.Update)
3692 self.fetcher.Fetch(glb_chunk_sz)
3694 def DisplayData(self, item, index):
3695 self.FetchIfNeeded(item.row)
3696 return item.getData(index.column())
3698 def AddSample(self, data):
3699 child = SQLTableItem(self.populated, data)
3700 self.child_items.append(child)
3703 def Update(self, fetched):
3706 self.progress.emit(0)
3707 child_count = self.child_count
3708 count = self.populated - child_count
3710 parent = QModelIndex()
3711 self.beginInsertRows(parent, child_count, child_count + count - 1)
3712 self.insertRows(child_count, count, parent)
3713 self.child_count += count
3714 self.endInsertRows()
3715 self.progress.emit(self.child_count)
3717 def FetchMoreRecords(self, count):
3718 current = self.child_count
3720 self.fetcher.Fetch(count)
3722 self.progress.emit(0)
3725 def HasMoreRecords(self):
3728 def columnCount(self, parent=None):
3729 return len(self.column_headers)
3731 def columnHeader(self, column):
3732 return self.column_headers[column]
3734 def SQLTableDataPrep(self, query, count):
3736 for i in xrange(count):
3737 data.append(query.value(i))
3740 # SQL automatic table data model
3742 class SQLAutoTableModel(SQLTableModel):
3744 def __init__(self, glb, table_name, parent=None):
3745 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
3746 if table_name == "comm_threads_view":
3747 # For now, comm_threads_view has no id column
3748 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
3750 query = QSqlQuery(glb.db)
3751 if glb.dbref.is_sqlite3:
3752 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
3754 column_headers.append(query.value(1))
3755 if table_name == "sqlite_master":
3756 sql = "SELECT * FROM " + table_name
3758 if table_name[:19] == "information_schema.":
3759 sql = "SELECT * FROM " + table_name
3760 select_table_name = table_name[19:]
3761 schema = "information_schema"
3763 select_table_name = table_name
3765 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
3767 column_headers.append(query.value(0))
3768 if pyside_version_1 and sys.version_info[0] == 3:
3769 if table_name == "samples_view":
3770 self.SQLTableDataPrep = self.samples_view_DataPrep
3771 if table_name == "samples":
3772 self.SQLTableDataPrep = self.samples_DataPrep
3773 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
3775 def samples_view_DataPrep(self, query, count):
3777 data.append(query.value(0))
3778 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3779 data.append("{:>19}".format(query.value(1)))
3780 for i in xrange(2, count):
3781 data.append(query.value(i))
3784 def samples_DataPrep(self, query, count):
3787 data.append(query.value(i))
3788 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3789 data.append("{:>19}".format(query.value(9)))
3790 for i in xrange(10, count):
3791 data.append(query.value(i))
3794 # Base class for custom ResizeColumnsToContents
3796 class ResizeColumnsToContentsBase(QObject):
3798 def __init__(self, parent=None):
3799 super(ResizeColumnsToContentsBase, self).__init__(parent)
3801 def ResizeColumnToContents(self, column, n):
3802 # Using the view's resizeColumnToContents() here is extrememly slow
3803 # so implement a crude alternative
3804 font = self.view.font()
3805 metrics = QFontMetrics(font)
3807 for row in xrange(n):
3808 val = self.data_model.child_items[row].data[column]
3809 len = metrics.width(str(val) + "MM")
3810 max = len if len > max else max
3811 val = self.data_model.columnHeader(column)
3812 len = metrics.width(str(val) + "MM")
3813 max = len if len > max else max
3814 self.view.setColumnWidth(column, max)
3816 def ResizeColumnsToContents(self):
3817 n = min(self.data_model.child_count, 100)
3819 # No data yet, so connect a signal to notify when there is
3820 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
3822 columns = self.data_model.columnCount()
3823 for i in xrange(columns):
3824 self.ResizeColumnToContents(i, n)
3826 def UpdateColumnWidths(self, *x):
3827 # This only needs to be done once, so disconnect the signal now
3828 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
3829 self.ResizeColumnsToContents()
3831 # Convert value to CSV
3835 val = val.replace('"', '""')
3836 if "," in val or '"' in val:
3837 val = '"' + val + '"'
3840 # Key to sort table model indexes by row / column, assuming fewer than 1000 columns
3844 def RowColumnKey(a):
3845 return a.row() * glb_max_cols + a.column()
3847 # Copy selected table cells to clipboard
3849 def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
3850 indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
3851 idx_cnt = len(indexes)
3856 min_row = indexes[0].row()
3857 max_row = indexes[0].row()
3858 min_col = indexes[0].column()
3859 max_col = indexes[0].column()
3861 min_row = min(min_row, i.row())
3862 max_row = max(max_row, i.row())
3863 min_col = min(min_col, i.column())
3864 max_col = max(max_col, i.column())
3865 if max_col > glb_max_cols:
3866 raise RuntimeError("glb_max_cols is too low")
3867 max_width = [0] * (1 + max_col - min_col)
3869 c = i.column() - min_col
3870 max_width[c] = max(max_width[c], len(str(i.data())))
3875 model = indexes[0].model()
3876 for col in range(min_col, max_col + 1):
3877 val = model.headerData(col, Qt.Horizontal, Qt.DisplayRole)
3879 text += sep + ToCSValue(val)
3883 max_width[c] = max(max_width[c], len(val))
3884 width = max_width[c]
3885 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
3886 if align & Qt.AlignRight:
3887 val = val.rjust(width)
3888 text += pad + sep + val
3889 pad = " " * (width - len(val))
3896 if i.row() > last_row:
3902 text += sep + ToCSValue(str(i.data()))
3905 width = max_width[i.column() - min_col]
3906 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3907 val = str(i.data()).rjust(width)
3910 text += pad + sep + val
3911 pad = " " * (width - len(val))
3913 QApplication.clipboard().setText(text)
3915 def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
3916 indexes = view.selectedIndexes()
3917 if not len(indexes):
3920 selection = view.selectionModel()
3924 above = view.indexAbove(i)
3925 if not selection.isSelected(above):
3930 raise RuntimeError("CopyTreeCellsToClipboard internal error")
3932 model = first.model()
3934 col_cnt = model.columnCount(first)
3935 max_width = [0] * col_cnt
3938 indent_str = " " * indent_sz
3940 expanded_mark_sz = 2
3941 if sys.version_info[0] == 3:
3942 expanded_mark = "\u25BC "
3943 not_expanded_mark = "\u25B6 "
3945 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
3946 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
3954 for c in range(col_cnt):
3955 i = pos.sibling(row, c)
3957 n = len(str(i.data()))
3959 n = len(str(i.data()).strip())
3960 n += (i.internalPointer().level - 1) * indent_sz
3961 n += expanded_mark_sz
3962 max_width[c] = max(max_width[c], n)
3963 pos = view.indexBelow(pos)
3964 if not selection.isSelected(pos):
3971 for c in range(col_cnt):
3972 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
3974 text += sep + ToCSValue(val)
3977 max_width[c] = max(max_width[c], len(val))
3978 width = max_width[c]
3979 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
3980 if align & Qt.AlignRight:
3981 val = val.rjust(width)
3982 text += pad + sep + val
3983 pad = " " * (width - len(val))
3992 for c in range(col_cnt):
3993 i = pos.sibling(row, c)
3996 if model.hasChildren(i):
3997 if view.isExpanded(i):
3998 mark = expanded_mark
4000 mark = not_expanded_mark
4003 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
4005 text += sep + ToCSValue(val)
4008 width = max_width[c]
4009 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
4010 val = val.rjust(width)
4011 text += pad + sep + val
4012 pad = " " * (width - len(val))
4014 pos = view.indexBelow(pos)
4015 if not selection.isSelected(pos):
4017 text = text.rstrip() + "\n"
4021 QApplication.clipboard().setText(text)
4023 def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
4024 view.CopyCellsToClipboard(view, as_csv, with_hdr)
4026 def CopyCellsToClipboardHdr(view):
4027 CopyCellsToClipboard(view, False, True)
4029 def CopyCellsToClipboardCSV(view):
4030 CopyCellsToClipboard(view, True, True)
4034 class ContextMenu(object):
4036 def __init__(self, view):
4038 self.view.setContextMenuPolicy(Qt.CustomContextMenu)
4039 self.view.customContextMenuRequested.connect(self.ShowContextMenu)
4041 def ShowContextMenu(self, pos):
4042 menu = QMenu(self.view)
4043 self.AddActions(menu)
4044 menu.exec_(self.view.mapToGlobal(pos))
4046 def AddCopy(self, menu):
4047 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
4048 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
4050 def AddActions(self, menu):
4053 class TreeContextMenu(ContextMenu):
4055 def __init__(self, view):
4056 super(TreeContextMenu, self).__init__(view)
4058 def AddActions(self, menu):
4059 i = self.view.currentIndex()
4060 text = str(i.data()).strip()
4062 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
4067 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4069 def __init__(self, glb, table_name, parent=None):
4070 super(TableWindow, self).__init__(parent)
4072 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
4074 self.model = QSortFilterProxyModel()
4075 self.model.setSourceModel(self.data_model)
4077 self.view = QTableView()
4078 self.view.setModel(self.model)
4079 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4080 self.view.verticalHeader().setVisible(False)
4081 self.view.sortByColumn(-1, Qt.AscendingOrder)
4082 self.view.setSortingEnabled(True)
4083 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4084 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4086 self.ResizeColumnsToContents()
4088 self.context_menu = ContextMenu(self.view)
4090 self.find_bar = FindBar(self, self, True)
4092 self.finder = ChildDataItemFinder(self.data_model)
4094 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4096 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4098 self.setWidget(self.vbox.Widget())
4100 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
4102 def Find(self, value, direction, pattern, context):
4103 self.view.setFocus()
4104 self.find_bar.Busy()
4105 self.finder.Find(value, direction, pattern, context, self.FindDone)
4107 def FindDone(self, row):
4108 self.find_bar.Idle()
4110 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
4112 self.find_bar.NotFound()
4116 def GetTableList(glb):
4118 query = QSqlQuery(glb.db)
4119 if glb.dbref.is_sqlite3:
4120 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
4122 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
4124 tables.append(query.value(0))
4125 if glb.dbref.is_sqlite3:
4126 tables.append("sqlite_master")
4128 tables.append("information_schema.tables")
4129 tables.append("information_schema.views")
4130 tables.append("information_schema.columns")
4133 # Top Calls data model
4135 class TopCallsModel(SQLTableModel):
4137 def __init__(self, glb, report_vars, parent=None):
4139 if not glb.dbref.is_sqlite3:
4142 if len(report_vars.limit):
4143 limit = " LIMIT " + report_vars.limit
4144 sql = ("SELECT comm, pid, tid, name,"
4146 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
4149 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
4151 " WHEN (calls.flags = 1) THEN 'no call'" + text +
4152 " WHEN (calls.flags = 2) THEN 'no return'" + text +
4153 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
4157 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
4158 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
4159 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
4160 " INNER JOIN comms ON calls.comm_id = comms.id"
4161 " INNER JOIN threads ON calls.thread_id = threads.id" +
4162 report_vars.where_clause +
4163 " ORDER BY elapsed_time DESC" +
4166 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
4167 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
4168 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
4170 def columnAlignment(self, column):
4171 return self.alignment[column]
4173 # Top Calls report creation dialog
4175 class TopCallsDialog(ReportDialogBase):
4177 def __init__(self, glb, parent=None):
4178 title = "Top Calls by Elapsed Time"
4179 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
4180 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
4181 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
4182 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
4183 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
4184 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
4185 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
4186 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
4187 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
4191 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4193 def __init__(self, glb, report_vars, parent=None):
4194 super(TopCallsWindow, self).__init__(parent)
4196 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
4197 self.model = self.data_model
4199 self.view = QTableView()
4200 self.view.setModel(self.model)
4201 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4202 self.view.verticalHeader().setVisible(False)
4203 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4204 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4206 self.context_menu = ContextMenu(self.view)
4208 self.ResizeColumnsToContents()
4210 self.find_bar = FindBar(self, self, True)
4212 self.finder = ChildDataItemFinder(self.model)
4214 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4216 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4218 self.setWidget(self.vbox.Widget())
4220 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
4222 def Find(self, value, direction, pattern, context):
4223 self.view.setFocus()
4224 self.find_bar.Busy()
4225 self.finder.Find(value, direction, pattern, context, self.FindDone)
4227 def FindDone(self, row):
4228 self.find_bar.Idle()
4230 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
4232 self.find_bar.NotFound()
4236 def CreateAction(label, tip, callback, parent=None, shortcut=None):
4237 action = QAction(label, parent)
4238 if shortcut != None:
4239 action.setShortcuts(shortcut)
4240 action.setStatusTip(tip)
4241 action.triggered.connect(callback)
4244 # Typical application actions
4246 def CreateExitAction(app, parent=None):
4247 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
4249 # Typical MDI actions
4251 def CreateCloseActiveWindowAction(mdi_area):
4252 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
4254 def CreateCloseAllWindowsAction(mdi_area):
4255 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
4257 def CreateTileWindowsAction(mdi_area):
4258 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
4260 def CreateCascadeWindowsAction(mdi_area):
4261 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
4263 def CreateNextWindowAction(mdi_area):
4264 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
4266 def CreatePreviousWindowAction(mdi_area):
4267 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
4269 # Typical MDI window menu
4273 def __init__(self, mdi_area, menu):
4274 self.mdi_area = mdi_area
4275 self.window_menu = menu.addMenu("&Windows")
4276 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
4277 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
4278 self.tile_windows = CreateTileWindowsAction(mdi_area)
4279 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
4280 self.next_window = CreateNextWindowAction(mdi_area)
4281 self.previous_window = CreatePreviousWindowAction(mdi_area)
4282 self.window_menu.aboutToShow.connect(self.Update)
4285 self.window_menu.clear()
4286 sub_window_count = len(self.mdi_area.subWindowList())
4287 have_sub_windows = sub_window_count != 0
4288 self.close_active_window.setEnabled(have_sub_windows)
4289 self.close_all_windows.setEnabled(have_sub_windows)
4290 self.tile_windows.setEnabled(have_sub_windows)
4291 self.cascade_windows.setEnabled(have_sub_windows)
4292 self.next_window.setEnabled(have_sub_windows)
4293 self.previous_window.setEnabled(have_sub_windows)
4294 self.window_menu.addAction(self.close_active_window)
4295 self.window_menu.addAction(self.close_all_windows)
4296 self.window_menu.addSeparator()
4297 self.window_menu.addAction(self.tile_windows)
4298 self.window_menu.addAction(self.cascade_windows)
4299 self.window_menu.addSeparator()
4300 self.window_menu.addAction(self.next_window)
4301 self.window_menu.addAction(self.previous_window)
4302 if sub_window_count == 0:
4304 self.window_menu.addSeparator()
4306 for sub_window in self.mdi_area.subWindowList():
4307 label = str(nr) + " " + sub_window.name
4310 action = self.window_menu.addAction(label)
4311 action.setCheckable(True)
4312 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
4313 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
4314 self.window_menu.addAction(action)
4317 def setActiveSubWindow(self, nr):
4318 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
4333 <p class=c1><a href=#reports>1. Reports</a></p>
4334 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
4335 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
4336 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
4337 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
4338 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
4339 <p class=c1><a href=#charts>2. Charts</a></p>
4340 <p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
4341 <p class=c1><a href=#tables>3. Tables</a></p>
4342 <h1 id=reports>1. Reports</h1>
4343 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
4344 The result is a GUI window with a tree representing a context-sensitive
4345 call-graph. Expanding a couple of levels of the tree and adjusting column
4346 widths to suit will display something like:
4348 Call Graph: pt_example
4349 Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
4352 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
4353 |- unknown unknown 1 13198 0.1 1 0.0
4354 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
4355 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
4356 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
4357 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
4358 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
4359 >- __libc_csu_init ls 1 10354 0.1 10 0.0
4360 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
4361 v- main ls 1 8182043 99.6 180254 99.9
4363 <h3>Points to note:</h3>
4365 <li>The top level is a command name (comm)</li>
4366 <li>The next level is a thread (pid:tid)</li>
4367 <li>Subsequent levels are functions</li>
4368 <li>'Count' is the number of calls</li>
4369 <li>'Time' is the elapsed time until the function returns</li>
4370 <li>Percentages are relative to the level above</li>
4371 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
4374 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
4375 The pattern matching symbols are ? for any character and * for zero or more characters.
4376 <h2 id=calltree>1.2 Call Tree</h2>
4377 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
4378 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
4379 <h2 id=allbranches>1.3 All branches</h2>
4380 The All branches report displays all branches in chronological order.
4381 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
4382 <h3>Disassembly</h3>
4383 Open a branch to display disassembly. This only works if:
4385 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
4386 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
4387 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
4388 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
4389 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
4391 <h4 id=xed>Intel XED Setup</h4>
4392 To use Intel XED, libxed.so must be present. To build and install libxed.so:
4394 git clone https://github.com/intelxed/mbuild.git mbuild
4395 git clone https://github.com/intelxed/xed
4398 sudo ./mfile.py --prefix=/usr/local install
4401 <h3>Instructions per Cycle (IPC)</h3>
4402 If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
4403 <p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
4404 Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
4405 In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
4406 since the previous displayed 'IPC'.
4408 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4409 Refer to Python documentation for the regular expression syntax.
4410 All columns are searched, but only currently fetched rows are searched.
4411 <h2 id=selectedbranches>1.4 Selected branches</h2>
4412 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
4413 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4414 <h3>1.4.1 Time ranges</h3>
4415 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
4416 ms, us or ns. Also, negative values are relative to the end of trace. Examples:
4418 81073085947329-81073085958238 From 81073085947329 to 81073085958238
4419 100us-200us From 100us to 200us
4420 10ms- From 10ms to the end
4421 -100ns The first 100ns
4422 -10ms- The last 10ms
4424 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
4425 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
4426 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.
4427 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4428 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
4429 <h1 id=charts>2. Charts</h1>
4430 <h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
4431 This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
4434 <li>Mouse over to highight the task and show the time</li>
4435 <li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
4436 <li>Go back and forward by pressing the arrow buttons</li>
4437 <li>If call information is available, right-click to show a call tree opened to that task and time.
4438 Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
4442 The graph can be misleading in the following respects:
4444 <li>The graph shows the first task on each CPU as running from the beginning of the time range.
4445 Because tracing might start on different CPUs at different times, that is not necessarily the case.
4446 Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4447 <li>Similarly, the last task on each CPU can be showing running longer than it really was.
4448 Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4449 <li>When the mouse is over a task, the highlighted task might not be visible on the legend without scrolling if the legend does not fit fully in the window</li>
4451 <h1 id=tables>3. Tables</h1>
4452 The Tables menu shows all tables and views in the database. Most tables have an associated view
4453 which displays the information in a more friendly way. Not all data for large tables is fetched
4454 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
4455 but that can be slow for large tables.
4456 <p>There are also tables of database meta-information.
4457 For SQLite3 databases, the sqlite_master table is included.
4458 For PostgreSQL databases, information_schema.tables/views/columns are included.
4460 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4461 Refer to Python documentation for the regular expression syntax.
4462 All columns are searched, but only currently fetched rows are searched.
4463 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
4464 will go to the next/previous result in id order, instead of display order.
4469 class HelpWindow(QMdiSubWindow):
4471 def __init__(self, glb, parent=None):
4472 super(HelpWindow, self).__init__(parent)
4474 self.text = QTextBrowser()
4475 self.text.setHtml(glb_help_text)
4476 self.text.setReadOnly(True)
4477 self.text.setOpenExternalLinks(True)
4479 self.setWidget(self.text)
4481 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
4483 # Main window that only displays the help text
4485 class HelpOnlyWindow(QMainWindow):
4487 def __init__(self, parent=None):
4488 super(HelpOnlyWindow, self).__init__(parent)
4490 self.setMinimumSize(200, 100)
4491 self.resize(800, 600)
4492 self.setWindowTitle("Exported SQL Viewer Help")
4493 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
4495 self.text = QTextBrowser()
4496 self.text.setHtml(glb_help_text)
4497 self.text.setReadOnly(True)
4498 self.text.setOpenExternalLinks(True)
4500 self.setCentralWidget(self.text)
4502 # PostqreSQL server version
4504 def PostqreSQLServerVersion(db):
4505 query = QSqlQuery(db)
4506 QueryExec(query, "SELECT VERSION()")
4508 v_str = query.value(0)
4509 v_list = v_str.strip().split(" ")
4510 if v_list[0] == "PostgreSQL" and v_list[2] == "on":
4517 def SQLiteVersion(db):
4518 query = QSqlQuery(db)
4519 QueryExec(query, "SELECT sqlite_version()")
4521 return query.value(0)
4526 class AboutDialog(QDialog):
4528 def __init__(self, glb, parent=None):
4529 super(AboutDialog, self).__init__(parent)
4531 self.setWindowTitle("About Exported SQL Viewer")
4532 self.setMinimumWidth(300)
4534 pyside_version = "1" if pyside_version_1 else "2"
4537 text += "Python version: " + sys.version.split(" ")[0] + "\n"
4538 text += "PySide version: " + pyside_version + "\n"
4539 text += "Qt version: " + qVersion() + "\n"
4540 if glb.dbref.is_sqlite3:
4541 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n"
4543 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
4546 self.text = QTextBrowser()
4547 self.text.setHtml(text)
4548 self.text.setReadOnly(True)
4549 self.text.setOpenExternalLinks(True)
4551 self.vbox = QVBoxLayout()
4552 self.vbox.addWidget(self.text)
4554 self.setLayout(self.vbox)
4558 def ResizeFont(widget, diff):
4559 font = widget.font()
4560 sz = font.pointSize()
4561 font.setPointSize(sz + diff)
4562 widget.setFont(font)
4564 def ShrinkFont(widget):
4565 ResizeFont(widget, -1)
4567 def EnlargeFont(widget):
4568 ResizeFont(widget, 1)
4570 # Unique name for sub-windows
4572 def NumberedWindowName(name, nr):
4574 name += " <" + str(nr) + ">"
4577 def UniqueSubWindowName(mdi_area, name):
4580 unique_name = NumberedWindowName(name, nr)
4582 for sub_window in mdi_area.subWindowList():
4583 if sub_window.name == unique_name:
4592 def AddSubWindow(mdi_area, sub_window, name):
4593 unique_name = UniqueSubWindowName(mdi_area, name)
4594 sub_window.setMinimumSize(200, 100)
4595 sub_window.resize(800, 600)
4596 sub_window.setWindowTitle(unique_name)
4597 sub_window.setAttribute(Qt.WA_DeleteOnClose)
4598 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
4599 sub_window.name = unique_name
4600 mdi_area.addSubWindow(sub_window)
4605 class MainWindow(QMainWindow):
4607 def __init__(self, glb, parent=None):
4608 super(MainWindow, self).__init__(parent)
4612 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
4613 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
4614 self.setMinimumSize(200, 100)
4616 self.mdi_area = QMdiArea()
4617 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4618 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4620 self.setCentralWidget(self.mdi_area)
4622 menu = self.menuBar()
4624 file_menu = menu.addMenu("&File")
4625 file_menu.addAction(CreateExitAction(glb.app, self))
4627 edit_menu = menu.addMenu("&Edit")
4628 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
4629 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
4630 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
4631 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
4632 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
4633 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
4635 reports_menu = menu.addMenu("&Reports")
4636 if IsSelectable(glb.db, "calls"):
4637 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
4639 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
4640 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
4642 self.EventMenu(GetEventList(glb.db), reports_menu)
4644 if IsSelectable(glb.db, "calls"):
4645 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
4647 if IsSelectable(glb.db, "context_switches"):
4648 charts_menu = menu.addMenu("&Charts")
4649 charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
4651 self.TableMenu(GetTableList(glb), menu)
4653 self.window_menu = WindowMenu(self.mdi_area, menu)
4655 help_menu = menu.addMenu("&Help")
4656 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
4657 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
4660 win = self.mdi_area.activeSubWindow()
4667 def CopyToClipboard(self):
4668 self.Try(CopyCellsToClipboardHdr)
4670 def CopyToClipboardCSV(self):
4671 self.Try(CopyCellsToClipboardCSV)
4674 win = self.mdi_area.activeSubWindow()
4677 win.find_bar.Activate()
4681 def FetchMoreRecords(self):
4682 win = self.mdi_area.activeSubWindow()
4685 win.fetch_bar.Activate()
4689 def ShrinkFont(self):
4690 self.Try(ShrinkFont)
4692 def EnlargeFont(self):
4693 self.Try(EnlargeFont)
4695 def EventMenu(self, events, reports_menu):
4697 for event in events:
4698 event = event.split(":")[0]
4699 if event == "branches":
4700 branches_events += 1
4702 for event in events:
4704 event = event.split(":")[0]
4705 if event == "branches":
4706 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
4707 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
4708 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
4709 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
4711 def TimeChartByCPU(self):
4712 TimeChartByCPUWindow(self.glb, self)
4714 def TableMenu(self, tables, menu):
4715 table_menu = menu.addMenu("&Tables")
4716 for table in tables:
4717 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
4719 def NewCallGraph(self):
4720 CallGraphWindow(self.glb, self)
4722 def NewCallTree(self):
4723 CallTreeWindow(self.glb, self)
4725 def NewTopCalls(self):
4726 dialog = TopCallsDialog(self.glb, self)
4727 ret = dialog.exec_()
4729 TopCallsWindow(self.glb, dialog.report_vars, self)
4731 def NewBranchView(self, event_id):
4732 BranchWindow(self.glb, event_id, ReportVars(), self)
4734 def NewSelectedBranchView(self, event_id):
4735 dialog = SelectedBranchDialog(self.glb, self)
4736 ret = dialog.exec_()
4738 BranchWindow(self.glb, event_id, dialog.report_vars, self)
4740 def NewTableView(self, table_name):
4741 TableWindow(self.glb, table_name, self)
4744 HelpWindow(self.glb, self)
4747 dialog = AboutDialog(self.glb, self)
4752 class xed_state_t(Structure):
4759 class XEDInstruction():
4761 def __init__(self, libxed):
4762 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
4763 xedd_t = c_byte * 512
4764 self.xedd = xedd_t()
4765 self.xedp = addressof(self.xedd)
4766 libxed.xed_decoded_inst_zero(self.xedp)
4767 self.state = xed_state_t()
4768 self.statep = addressof(self.state)
4769 # Buffer for disassembled instruction text
4770 self.buffer = create_string_buffer(256)
4771 self.bufferp = addressof(self.buffer)
4777 self.libxed = CDLL("libxed.so")
4781 self.libxed = CDLL("/usr/local/lib/libxed.so")
4783 self.xed_tables_init = self.libxed.xed_tables_init
4784 self.xed_tables_init.restype = None
4785 self.xed_tables_init.argtypes = []
4787 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
4788 self.xed_decoded_inst_zero.restype = None
4789 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
4791 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
4792 self.xed_operand_values_set_mode.restype = None
4793 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
4795 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
4796 self.xed_decoded_inst_zero_keep_mode.restype = None
4797 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
4799 self.xed_decode = self.libxed.xed_decode
4800 self.xed_decode.restype = c_int
4801 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
4803 self.xed_format_context = self.libxed.xed_format_context
4804 self.xed_format_context.restype = c_uint
4805 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
4807 self.xed_tables_init()
4809 def Instruction(self):
4810 return XEDInstruction(self)
4812 def SetMode(self, inst, mode):
4814 inst.state.mode = 4 # 32-bit
4815 inst.state.width = 4 # 4 bytes
4817 inst.state.mode = 1 # 64-bit
4818 inst.state.width = 8 # 8 bytes
4819 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
4821 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
4822 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
4823 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
4826 # Use AT&T mode (2), alternative is Intel (3)
4827 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
4830 if sys.version_info[0] == 2:
4831 result = inst.buffer.value
4833 result = inst.buffer.value.decode()
4834 # Return instruction length and the disassembled instruction text
4835 # For now, assume the length is in byte 166
4836 return inst.xedd[166], result
4838 def TryOpen(file_name):
4840 return open(file_name, "rb")
4845 result = sizeof(c_void_p)
4852 if sys.version_info[0] == 2:
4853 eclass = ord(header[4])
4854 encoding = ord(header[5])
4855 version = ord(header[6])
4858 encoding = header[5]
4860 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
4861 result = True if eclass == 2 else False
4868 def __init__(self, dbref, db, dbname):
4871 self.dbname = dbname
4872 self.home_dir = os.path.expanduser("~")
4873 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
4874 if self.buildid_dir:
4875 self.buildid_dir += "/.build-id/"
4877 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
4879 self.mainwindow = None
4880 self.instances_to_shutdown_on_exit = weakref.WeakSet()
4882 self.disassembler = LibXED()
4883 self.have_disassembler = True
4885 self.have_disassembler = False
4886 self.host_machine_id = 0
4887 self.host_start_time = 0
4888 self.host_finish_time = 0
4890 def FileFromBuildId(self, build_id):
4891 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
4892 return TryOpen(file_name)
4894 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
4895 # Assume current machine i.e. no support for virtualization
4896 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
4897 file_name = os.getenv("PERF_KCORE")
4898 f = TryOpen(file_name) if file_name else None
4901 # For now, no special handling if long_name is /proc/kcore
4902 f = TryOpen(long_name)
4905 f = self.FileFromBuildId(build_id)
4910 def AddInstanceToShutdownOnExit(self, instance):
4911 self.instances_to_shutdown_on_exit.add(instance)
4913 # Shutdown any background processes or threads
4914 def ShutdownInstances(self):
4915 for x in self.instances_to_shutdown_on_exit:
4921 def GetHostMachineId(self):
4922 query = QSqlQuery(self.db)
4923 QueryExec(query, "SELECT id FROM machines WHERE pid = -1")
4925 self.host_machine_id = query.value(0)
4927 self.host_machine_id = 0
4928 return self.host_machine_id
4930 def HostMachineId(self):
4931 if self.host_machine_id:
4932 return self.host_machine_id
4933 return self.GetHostMachineId()
4935 def SelectValue(self, sql):
4936 query = QSqlQuery(self.db)
4938 QueryExec(query, sql)
4942 return Decimal(query.value(0))
4945 def SwitchesMinTime(self, machine_id):
4946 return self.SelectValue("SELECT time"
4947 " FROM context_switches"
4948 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4949 " ORDER BY id LIMIT 1")
4951 def SwitchesMaxTime(self, machine_id):
4952 return self.SelectValue("SELECT time"
4953 " FROM context_switches"
4954 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4955 " ORDER BY id DESC LIMIT 1")
4957 def SamplesMinTime(self, machine_id):
4958 return self.SelectValue("SELECT time"
4960 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4961 " ORDER BY id LIMIT 1")
4963 def SamplesMaxTime(self, machine_id):
4964 return self.SelectValue("SELECT time"
4966 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4967 " ORDER BY id DESC LIMIT 1")
4969 def CallsMinTime(self, machine_id):
4970 return self.SelectValue("SELECT calls.call_time"
4972 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4973 " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +
4974 " ORDER BY calls.id LIMIT 1")
4976 def CallsMaxTime(self, machine_id):
4977 return self.SelectValue("SELECT calls.return_time"
4979 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4980 " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +
4981 " ORDER BY calls.return_time DESC LIMIT 1")
4983 def GetStartTime(self, machine_id):
4984 t0 = self.SwitchesMinTime(machine_id)
4985 t1 = self.SamplesMinTime(machine_id)
4986 t2 = self.CallsMinTime(machine_id)
4987 if t0 is None or (not(t1 is None) and t1 < t0):
4989 if t0 is None or (not(t2 is None) and t2 < t0):
4993 def GetFinishTime(self, machine_id):
4994 t0 = self.SwitchesMaxTime(machine_id)
4995 t1 = self.SamplesMaxTime(machine_id)
4996 t2 = self.CallsMaxTime(machine_id)
4997 if t0 is None or (not(t1 is None) and t1 > t0):
4999 if t0 is None or (not(t2 is None) and t2 > t0):
5003 def HostStartTime(self):
5004 if self.host_start_time:
5005 return self.host_start_time
5006 self.host_start_time = self.GetStartTime(self.HostMachineId())
5007 return self.host_start_time
5009 def HostFinishTime(self):
5010 if self.host_finish_time:
5011 return self.host_finish_time
5012 self.host_finish_time = self.GetFinishTime(self.HostMachineId())
5013 return self.host_finish_time
5015 def StartTime(self, machine_id):
5016 if machine_id == self.HostMachineId():
5017 return self.HostStartTime()
5018 return self.GetStartTime(machine_id)
5020 def FinishTime(self, machine_id):
5021 if machine_id == self.HostMachineId():
5022 return self.HostFinishTime()
5023 return self.GetFinishTime(machine_id)
5025 # Database reference
5029 def __init__(self, is_sqlite3, dbname):
5030 self.is_sqlite3 = is_sqlite3
5031 self.dbname = dbname
5033 self.FALSE = "FALSE"
5034 # SQLite prior to version 3.23 does not support TRUE and FALSE
5039 def Open(self, connection_name):
5040 dbname = self.dbname
5042 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
5044 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
5045 opts = dbname.split()
5048 opt = opt.split("=")
5049 if opt[0] == "hostname":
5050 db.setHostName(opt[1])
5051 elif opt[0] == "port":
5052 db.setPort(int(opt[1]))
5053 elif opt[0] == "username":
5054 db.setUserName(opt[1])
5055 elif opt[0] == "password":
5056 db.setPassword(opt[1])
5057 elif opt[0] == "dbname":
5062 db.setDatabaseName(dbname)
5064 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
5070 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
5071 " or: exported-sql-viewer.py --help-only"
5072 ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
5073 ap.add_argument("--pyside-version-1", action='store_true')
5074 ap.add_argument("dbname", nargs="?")
5075 ap.add_argument("--help-only", action='store_true')
5076 args = ap.parse_args()
5079 app = QApplication(sys.argv)
5080 mainwindow = HelpOnlyWindow()
5085 dbname = args.dbname
5088 print("Too few arguments")
5093 f = open(dbname, "rb")
5094 if f.read(15) == b'SQLite format 3':
5100 dbref = DBRef(is_sqlite3, dbname)
5101 db, dbname = dbref.Open("main")
5102 glb = Glb(dbref, db, dbname)
5103 app = QApplication(sys.argv)
5105 mainwindow = MainWindow(glb)
5106 glb.mainwindow = mainwindow
5109 glb.ShutdownInstances()
5113 if __name__ == "__main__":