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
100 import cPickle as pickle
101 # size of pickled integer big enough for record size
112 pyside_version_1 = True
113 if not "--pyside-version-1" in sys.argv:
115 from PySide2.QtCore import *
116 from PySide2.QtGui import *
117 from PySide2.QtSql import *
118 from PySide2.QtWidgets import *
119 pyside_version_1 = False
124 from PySide.QtCore import *
125 from PySide.QtGui import *
126 from PySide.QtSql import *
128 from decimal import *
130 from multiprocessing import Process, Array, Value, Event
132 # xrange is range in Python3
138 def printerr(*args, **keyword_args):
139 print(*args, file=sys.stderr, **keyword_args)
141 # Data formatting helpers
150 return "+0x%x" % offset
154 if name == "[kernel.kallsyms]":
158 def findnth(s, sub, n, offs=0):
164 return findnth(s[pos + 1:], sub, n - 1, offs + pos + 1)
166 # Percent to one decimal place
168 def PercentToOneDP(n, d):
171 x = (n * Decimal(100)) / d
172 return str(x.quantize(Decimal(".1"), rounding=ROUND_HALF_UP))
174 # Helper for queries that must not fail
176 def QueryExec(query, stmt):
177 ret = query.exec_(stmt)
179 raise Exception("Query failed: " + query.lastError().text())
183 class Thread(QThread):
185 done = Signal(object)
187 def __init__(self, task, param=None, parent=None):
188 super(Thread, self).__init__(parent)
194 if self.param is None:
195 done, result = self.task()
197 done, result = self.task(self.param)
198 self.done.emit(result)
204 class TreeModel(QAbstractItemModel):
206 def __init__(self, glb, params, parent=None):
207 super(TreeModel, self).__init__(parent)
210 self.root = self.GetRoot()
211 self.last_row_read = 0
213 def Item(self, parent):
215 return parent.internalPointer()
219 def rowCount(self, parent):
220 result = self.Item(parent).childCount()
223 self.dataChanged.emit(parent, parent)
226 def hasChildren(self, parent):
227 return self.Item(parent).hasChildren()
229 def headerData(self, section, orientation, role):
230 if role == Qt.TextAlignmentRole:
231 return self.columnAlignment(section)
232 if role != Qt.DisplayRole:
234 if orientation != Qt.Horizontal:
236 return self.columnHeader(section)
238 def parent(self, child):
239 child_item = child.internalPointer()
240 if child_item is self.root:
242 parent_item = child_item.getParentItem()
243 return self.createIndex(parent_item.getRow(), 0, parent_item)
245 def index(self, row, column, parent):
246 child_item = self.Item(parent).getChildItem(row)
247 return self.createIndex(row, column, child_item)
249 def DisplayData(self, item, index):
250 return item.getData(index.column())
252 def FetchIfNeeded(self, row):
253 if row > self.last_row_read:
254 self.last_row_read = row
255 if row + 10 >= self.root.child_count:
256 self.fetcher.Fetch(glb_chunk_sz)
258 def columnAlignment(self, column):
261 def columnFont(self, column):
264 def data(self, index, role):
265 if role == Qt.TextAlignmentRole:
266 return self.columnAlignment(index.column())
267 if role == Qt.FontRole:
268 return self.columnFont(index.column())
269 if role != Qt.DisplayRole:
271 item = index.internalPointer()
272 return self.DisplayData(item, index)
276 class TableModel(QAbstractTableModel):
278 def __init__(self, parent=None):
279 super(TableModel, self).__init__(parent)
281 self.child_items = []
282 self.last_row_read = 0
284 def Item(self, parent):
286 return parent.internalPointer()
290 def rowCount(self, parent):
291 return self.child_count
293 def headerData(self, section, orientation, role):
294 if role == Qt.TextAlignmentRole:
295 return self.columnAlignment(section)
296 if role != Qt.DisplayRole:
298 if orientation != Qt.Horizontal:
300 return self.columnHeader(section)
302 def index(self, row, column, parent):
303 return self.createIndex(row, column, self.child_items[row])
305 def DisplayData(self, item, index):
306 return item.getData(index.column())
308 def FetchIfNeeded(self, row):
309 if row > self.last_row_read:
310 self.last_row_read = row
311 if row + 10 >= self.child_count:
312 self.fetcher.Fetch(glb_chunk_sz)
314 def columnAlignment(self, column):
317 def columnFont(self, column):
320 def data(self, index, role):
321 if role == Qt.TextAlignmentRole:
322 return self.columnAlignment(index.column())
323 if role == Qt.FontRole:
324 return self.columnFont(index.column())
325 if role != Qt.DisplayRole:
327 item = index.internalPointer()
328 return self.DisplayData(item, index)
332 model_cache = weakref.WeakValueDictionary()
333 model_cache_lock = threading.Lock()
335 def LookupCreateModel(model_name, create_fn):
336 model_cache_lock.acquire()
338 model = model_cache[model_name]
343 model_cache[model_name] = model
344 model_cache_lock.release()
347 def LookupModel(model_name):
348 model_cache_lock.acquire()
350 model = model_cache[model_name]
353 model_cache_lock.release()
360 def __init__(self, parent, finder, is_reg_expr=False):
363 self.last_value = None
364 self.last_pattern = None
366 label = QLabel("Find:")
367 label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
369 self.textbox = QComboBox()
370 self.textbox.setEditable(True)
371 self.textbox.currentIndexChanged.connect(self.ValueChanged)
373 self.progress = QProgressBar()
374 self.progress.setRange(0, 0)
378 self.pattern = QCheckBox("Regular Expression")
380 self.pattern = QCheckBox("Pattern")
381 self.pattern.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
383 self.next_button = QToolButton()
384 self.next_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowDown))
385 self.next_button.released.connect(lambda: self.NextPrev(1))
387 self.prev_button = QToolButton()
388 self.prev_button.setIcon(parent.style().standardIcon(QStyle.SP_ArrowUp))
389 self.prev_button.released.connect(lambda: self.NextPrev(-1))
391 self.close_button = QToolButton()
392 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
393 self.close_button.released.connect(self.Deactivate)
395 self.hbox = QHBoxLayout()
396 self.hbox.setContentsMargins(0, 0, 0, 0)
398 self.hbox.addWidget(label)
399 self.hbox.addWidget(self.textbox)
400 self.hbox.addWidget(self.progress)
401 self.hbox.addWidget(self.pattern)
402 self.hbox.addWidget(self.next_button)
403 self.hbox.addWidget(self.prev_button)
404 self.hbox.addWidget(self.close_button)
407 self.bar.setLayout(self.hbox)
415 self.textbox.lineEdit().selectAll()
416 self.textbox.setFocus()
418 def Deactivate(self):
422 self.textbox.setEnabled(False)
424 self.next_button.hide()
425 self.prev_button.hide()
429 self.textbox.setEnabled(True)
432 self.next_button.show()
433 self.prev_button.show()
435 def Find(self, direction):
436 value = self.textbox.currentText()
437 pattern = self.pattern.isChecked()
438 self.last_value = value
439 self.last_pattern = pattern
440 self.finder.Find(value, direction, pattern, self.context)
442 def ValueChanged(self):
443 value = self.textbox.currentText()
444 pattern = self.pattern.isChecked()
445 index = self.textbox.currentIndex()
446 data = self.textbox.itemData(index)
447 # Store the pattern in the combo box to keep it with the text value
449 self.textbox.setItemData(index, pattern)
451 self.pattern.setChecked(data)
454 def NextPrev(self, direction):
455 value = self.textbox.currentText()
456 pattern = self.pattern.isChecked()
457 if value != self.last_value:
458 index = self.textbox.findText(value)
459 # Allow for a button press before the value has been added to the combo box
461 index = self.textbox.count()
462 self.textbox.addItem(value, pattern)
463 self.textbox.setCurrentIndex(index)
466 self.textbox.setItemData(index, pattern)
467 elif pattern != self.last_pattern:
468 # Keep the pattern recorded in the combo box up to date
469 index = self.textbox.currentIndex()
470 self.textbox.setItemData(index, pattern)
474 QMessageBox.information(self.bar, "Find", "'" + self.textbox.currentText() + "' not found")
476 # Context-sensitive call graph data model item base
478 class CallGraphLevelItemBase(object):
480 def __init__(self, glb, params, row, parent_item):
484 self.parent_item = parent_item
485 self.query_done = False
487 self.child_items = []
489 self.level = parent_item.level + 1
493 def getChildItem(self, row):
494 return self.child_items[row]
496 def getParentItem(self):
497 return self.parent_item
502 def childCount(self):
503 if not self.query_done:
505 if not self.child_count:
507 return self.child_count
509 def hasChildren(self):
510 if not self.query_done:
512 return self.child_count > 0
514 def getData(self, column):
515 return self.data[column]
517 # Context-sensitive call graph data model level 2+ item base
519 class CallGraphLevelTwoPlusItemBase(CallGraphLevelItemBase):
521 def __init__(self, glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item):
522 super(CallGraphLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
523 self.comm_id = comm_id
524 self.thread_id = thread_id
525 self.call_path_id = call_path_id
526 self.insn_cnt = insn_cnt
527 self.cyc_cnt = cyc_cnt
528 self.branch_count = branch_count
532 self.query_done = True
533 query = QSqlQuery(self.glb.db)
534 if self.params.have_ipc:
535 ipc_str = ", SUM(insn_count), SUM(cyc_count)"
538 QueryExec(query, "SELECT call_path_id, name, short_name, COUNT(calls.id), SUM(return_time - call_time)" + ipc_str + ", SUM(branch_count)"
540 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
541 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
542 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
543 " WHERE parent_call_path_id = " + str(self.call_path_id) +
544 " AND comm_id = " + str(self.comm_id) +
545 " AND thread_id = " + str(self.thread_id) +
546 " GROUP BY call_path_id, name, short_name"
547 " ORDER BY call_path_id")
549 if self.params.have_ipc:
550 insn_cnt = int(query.value(5))
551 cyc_cnt = int(query.value(6))
552 branch_count = int(query.value(7))
556 branch_count = int(query.value(5))
557 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)
558 self.child_items.append(child_item)
559 self.child_count += 1
561 # Context-sensitive call graph data model level three item
563 class CallGraphLevelThreeItem(CallGraphLevelTwoPlusItemBase):
565 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):
566 super(CallGraphLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, call_path_id, time, insn_cnt, cyc_cnt, branch_count, parent_item)
568 if self.params.have_ipc:
569 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
570 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
571 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
572 ipc = CalcIPC(cyc_cnt, insn_cnt)
573 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 ]
575 self.data = [ name, dso, str(count), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
576 self.dbid = call_path_id
578 # Context-sensitive call graph data model level two item
580 class CallGraphLevelTwoItem(CallGraphLevelTwoPlusItemBase):
582 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
583 super(CallGraphLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 1, 0, 0, 0, 0, parent_item)
584 if self.params.have_ipc:
585 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
587 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
588 self.dbid = thread_id
591 super(CallGraphLevelTwoItem, self).Select()
592 for child_item in self.child_items:
593 self.time += child_item.time
594 self.insn_cnt += child_item.insn_cnt
595 self.cyc_cnt += child_item.cyc_cnt
596 self.branch_count += child_item.branch_count
597 for child_item in self.child_items:
598 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
599 if self.params.have_ipc:
600 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
601 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
602 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
604 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
606 # Context-sensitive call graph data model level one item
608 class CallGraphLevelOneItem(CallGraphLevelItemBase):
610 def __init__(self, glb, params, row, comm_id, comm, parent_item):
611 super(CallGraphLevelOneItem, self).__init__(glb, params, row, parent_item)
612 if self.params.have_ipc:
613 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
615 self.data = [comm, "", "", "", "", "", ""]
619 self.query_done = True
620 query = QSqlQuery(self.glb.db)
621 QueryExec(query, "SELECT thread_id, pid, tid"
623 " INNER JOIN threads ON thread_id = threads.id"
624 " WHERE comm_id = " + str(self.dbid))
626 child_item = CallGraphLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
627 self.child_items.append(child_item)
628 self.child_count += 1
630 # Context-sensitive call graph data model root item
632 class CallGraphRootItem(CallGraphLevelItemBase):
634 def __init__(self, glb, params):
635 super(CallGraphRootItem, self).__init__(glb, params, 0, None)
637 self.query_done = True
639 if IsSelectable(glb.db, "comms", columns = "has_calls"):
640 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
641 query = QSqlQuery(glb.db)
642 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
644 if not query.value(0):
646 child_item = CallGraphLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
647 self.child_items.append(child_item)
648 self.child_count += 1
650 # Call graph model parameters
652 class CallGraphModelParams():
654 def __init__(self, glb, parent=None):
655 self.have_ipc = IsSelectable(glb.db, "calls", columns = "insn_count, cyc_count")
657 # Context-sensitive call graph data model base
659 class CallGraphModelBase(TreeModel):
661 def __init__(self, glb, parent=None):
662 super(CallGraphModelBase, self).__init__(glb, CallGraphModelParams(glb), parent)
664 def FindSelect(self, value, pattern, query):
666 # postgresql and sqlite pattern patching differences:
667 # postgresql LIKE is case sensitive but sqlite LIKE is not
668 # postgresql LIKE allows % and _ to be escaped with \ but sqlite LIKE does not
669 # postgresql supports ILIKE which is case insensitive
670 # sqlite supports GLOB (text only) which uses * and ? and is case sensitive
671 if not self.glb.dbref.is_sqlite3:
673 s = value.replace("%", "\%")
674 s = s.replace("_", "\_")
675 # Translate * and ? into SQL LIKE pattern characters % and _
676 trans = string.maketrans("*?", "%_")
677 match = " LIKE '" + str(s).translate(trans) + "'"
679 match = " GLOB '" + str(value) + "'"
681 match = " = '" + str(value) + "'"
682 self.DoFindSelect(query, match)
684 def Found(self, query, found):
686 return self.FindPath(query)
689 def FindValue(self, value, pattern, query, last_value, last_pattern):
690 if last_value == value and pattern == last_pattern:
691 found = query.first()
693 self.FindSelect(value, pattern, query)
695 return self.Found(query, found)
697 def FindNext(self, query):
700 found = query.first()
701 return self.Found(query, found)
703 def FindPrev(self, query):
704 found = query.previous()
707 return self.Found(query, found)
709 def FindThread(self, c):
710 if c.direction == 0 or c.value != c.last_value or c.pattern != c.last_pattern:
711 ids = self.FindValue(c.value, c.pattern, c.query, c.last_value, c.last_pattern)
712 elif c.direction > 0:
713 ids = self.FindNext(c.query)
715 ids = self.FindPrev(c.query)
718 def Find(self, value, direction, pattern, context, callback):
720 def __init__(self, *x):
721 self.value, self.direction, self.pattern, self.query, self.last_value, self.last_pattern = x
722 def Update(self, *x):
723 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = x + (self.value, self.pattern)
725 context[0].Update(value, direction, pattern)
727 context.append(Context(value, direction, pattern, QSqlQuery(self.glb.db), None, None))
728 # Use a thread so the UI is not blocked during the SELECT
729 thread = Thread(self.FindThread, context[0])
730 thread.done.connect(lambda ids, t=thread, c=callback: self.FindDone(t, c, ids), Qt.QueuedConnection)
733 def FindDone(self, thread, callback, ids):
736 # Context-sensitive call graph data model
738 class CallGraphModel(CallGraphModelBase):
740 def __init__(self, glb, parent=None):
741 super(CallGraphModel, self).__init__(glb, parent)
744 return CallGraphRootItem(self.glb, self.params)
746 def columnCount(self, parent=None):
747 if self.params.have_ipc:
752 def columnHeader(self, column):
753 if self.params.have_ipc:
754 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
756 headers = ["Call Path", "Object", "Count ", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
757 return headers[column]
759 def columnAlignment(self, column):
760 if self.params.have_ipc:
761 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 ]
763 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
764 return alignment[column]
766 def DoFindSelect(self, query, match):
767 QueryExec(query, "SELECT call_path_id, comm_id, thread_id"
769 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
770 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
771 " WHERE calls.id <> 0"
772 " AND symbols.name" + match +
773 " GROUP BY comm_id, thread_id, call_path_id"
774 " ORDER BY comm_id, thread_id, call_path_id")
776 def FindPath(self, query):
777 # Turn the query result into a list of ids that the tree view can walk
778 # to open the tree at the right place.
780 parent_id = query.value(0)
782 ids.insert(0, parent_id)
783 q2 = QSqlQuery(self.glb.db)
784 QueryExec(q2, "SELECT parent_id"
786 " WHERE id = " + str(parent_id))
789 parent_id = q2.value(0)
790 # The call path root is not used
793 ids.insert(0, query.value(2))
794 ids.insert(0, query.value(1))
797 # Call tree data model level 2+ item base
799 class CallTreeLevelTwoPlusItemBase(CallGraphLevelItemBase):
801 def __init__(self, glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item):
802 super(CallTreeLevelTwoPlusItemBase, self).__init__(glb, params, row, parent_item)
803 self.comm_id = comm_id
804 self.thread_id = thread_id
805 self.calls_id = calls_id
806 self.call_time = call_time
808 self.insn_cnt = insn_cnt
809 self.cyc_cnt = cyc_cnt
810 self.branch_count = branch_count
813 self.query_done = True
814 if self.calls_id == 0:
815 comm_thread = " AND comm_id = " + str(self.comm_id) + " AND thread_id = " + str(self.thread_id)
818 if self.params.have_ipc:
819 ipc_str = ", insn_count, cyc_count"
822 query = QSqlQuery(self.glb.db)
823 QueryExec(query, "SELECT calls.id, name, short_name, call_time, return_time - call_time" + ipc_str + ", branch_count"
825 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
826 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
827 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
828 " WHERE calls.parent_id = " + str(self.calls_id) + comm_thread +
829 " ORDER BY call_time, calls.id")
831 if self.params.have_ipc:
832 insn_cnt = int(query.value(5))
833 cyc_cnt = int(query.value(6))
834 branch_count = int(query.value(7))
838 branch_count = int(query.value(5))
839 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)
840 self.child_items.append(child_item)
841 self.child_count += 1
843 # Call tree data model level three item
845 class CallTreeLevelThreeItem(CallTreeLevelTwoPlusItemBase):
847 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):
848 super(CallTreeLevelThreeItem, self).__init__(glb, params, row, comm_id, thread_id, calls_id, call_time, time, insn_cnt, cyc_cnt, branch_count, parent_item)
850 if self.params.have_ipc:
851 insn_pcnt = PercentToOneDP(insn_cnt, parent_item.insn_cnt)
852 cyc_pcnt = PercentToOneDP(cyc_cnt, parent_item.cyc_cnt)
853 br_pcnt = PercentToOneDP(branch_count, parent_item.branch_count)
854 ipc = CalcIPC(cyc_cnt, insn_cnt)
855 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 ]
857 self.data = [ name, dso, str(call_time), str(time), PercentToOneDP(time, parent_item.time), str(branch_count), PercentToOneDP(branch_count, parent_item.branch_count) ]
860 # Call tree data model level two item
862 class CallTreeLevelTwoItem(CallTreeLevelTwoPlusItemBase):
864 def __init__(self, glb, params, row, comm_id, thread_id, pid, tid, parent_item):
865 super(CallTreeLevelTwoItem, self).__init__(glb, params, row, comm_id, thread_id, 0, 0, 0, 0, 0, 0, parent_item)
866 if self.params.have_ipc:
867 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", "", "", "", "", "", ""]
869 self.data = [str(pid) + ":" + str(tid), "", "", "", "", "", ""]
870 self.dbid = thread_id
873 super(CallTreeLevelTwoItem, self).Select()
874 for child_item in self.child_items:
875 self.time += child_item.time
876 self.insn_cnt += child_item.insn_cnt
877 self.cyc_cnt += child_item.cyc_cnt
878 self.branch_count += child_item.branch_count
879 for child_item in self.child_items:
880 child_item.data[4] = PercentToOneDP(child_item.time, self.time)
881 if self.params.have_ipc:
882 child_item.data[6] = PercentToOneDP(child_item.insn_cnt, self.insn_cnt)
883 child_item.data[8] = PercentToOneDP(child_item.cyc_cnt, self.cyc_cnt)
884 child_item.data[11] = PercentToOneDP(child_item.branch_count, self.branch_count)
886 child_item.data[6] = PercentToOneDP(child_item.branch_count, self.branch_count)
888 # Call tree data model level one item
890 class CallTreeLevelOneItem(CallGraphLevelItemBase):
892 def __init__(self, glb, params, row, comm_id, comm, parent_item):
893 super(CallTreeLevelOneItem, self).__init__(glb, params, row, parent_item)
894 if self.params.have_ipc:
895 self.data = [comm, "", "", "", "", "", "", "", "", "", "", ""]
897 self.data = [comm, "", "", "", "", "", ""]
901 self.query_done = True
902 query = QSqlQuery(self.glb.db)
903 QueryExec(query, "SELECT thread_id, pid, tid"
905 " INNER JOIN threads ON thread_id = threads.id"
906 " WHERE comm_id = " + str(self.dbid))
908 child_item = CallTreeLevelTwoItem(self.glb, self.params, self.child_count, self.dbid, query.value(0), query.value(1), query.value(2), self)
909 self.child_items.append(child_item)
910 self.child_count += 1
912 # Call tree data model root item
914 class CallTreeRootItem(CallGraphLevelItemBase):
916 def __init__(self, glb, params):
917 super(CallTreeRootItem, self).__init__(glb, params, 0, None)
919 self.query_done = True
921 if IsSelectable(glb.db, "comms", columns = "has_calls"):
922 if_has_calls = " WHERE has_calls = " + glb.dbref.TRUE
923 query = QSqlQuery(glb.db)
924 QueryExec(query, "SELECT id, comm FROM comms" + if_has_calls)
926 if not query.value(0):
928 child_item = CallTreeLevelOneItem(glb, params, self.child_count, query.value(0), query.value(1), self)
929 self.child_items.append(child_item)
930 self.child_count += 1
932 # Call Tree data model
934 class CallTreeModel(CallGraphModelBase):
936 def __init__(self, glb, parent=None):
937 super(CallTreeModel, self).__init__(glb, parent)
940 return CallTreeRootItem(self.glb, self.params)
942 def columnCount(self, parent=None):
943 if self.params.have_ipc:
948 def columnHeader(self, column):
949 if self.params.have_ipc:
950 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Insn Cnt", "Insn Cnt (%)", "Cyc Cnt", "Cyc Cnt (%)", "IPC", "Branch Count ", "Branch Count (%) "]
952 headers = ["Call Path", "Object", "Call Time", "Time (ns) ", "Time (%) ", "Branch Count ", "Branch Count (%) "]
953 return headers[column]
955 def columnAlignment(self, column):
956 if self.params.have_ipc:
957 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 ]
959 alignment = [ Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight, Qt.AlignRight ]
960 return alignment[column]
962 def DoFindSelect(self, query, match):
963 QueryExec(query, "SELECT calls.id, comm_id, thread_id"
965 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
966 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
967 " WHERE calls.id <> 0"
968 " AND symbols.name" + match +
969 " ORDER BY comm_id, thread_id, call_time, calls.id")
971 def FindPath(self, query):
972 # Turn the query result into a list of ids that the tree view can walk
973 # to open the tree at the right place.
975 parent_id = query.value(0)
977 ids.insert(0, parent_id)
978 q2 = QSqlQuery(self.glb.db)
979 QueryExec(q2, "SELECT parent_id"
981 " WHERE id = " + str(parent_id))
984 parent_id = q2.value(0)
985 ids.insert(0, query.value(2))
986 ids.insert(0, query.value(1))
991 class HBoxLayout(QHBoxLayout):
993 def __init__(self, *children):
994 super(HBoxLayout, self).__init__()
996 self.layout().setContentsMargins(0, 0, 0, 0)
997 for child in children:
998 if child.isWidgetType():
999 self.layout().addWidget(child)
1001 self.layout().addLayout(child)
1005 class VBoxLayout(QVBoxLayout):
1007 def __init__(self, *children):
1008 super(VBoxLayout, self).__init__()
1010 self.layout().setContentsMargins(0, 0, 0, 0)
1011 for child in children:
1012 if child.isWidgetType():
1013 self.layout().addWidget(child)
1015 self.layout().addLayout(child)
1017 # Vertical layout widget
1021 def __init__(self, *children):
1022 self.vbox = QWidget()
1023 self.vbox.setLayout(VBoxLayout(*children))
1030 class TreeWindowBase(QMdiSubWindow):
1032 def __init__(self, parent=None):
1033 super(TreeWindowBase, self).__init__(parent)
1036 self.find_bar = None
1038 self.view = QTreeView()
1039 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
1040 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
1042 self.context_menu = TreeContextMenu(self.view)
1044 def DisplayFound(self, ids):
1047 parent = QModelIndex()
1050 n = self.model.rowCount(parent)
1051 for row in xrange(n):
1052 child = self.model.index(row, 0, parent)
1053 if child.internalPointer().dbid == dbid:
1055 self.view.setExpanded(parent, True)
1056 self.view.setCurrentIndex(child)
1063 def Find(self, value, direction, pattern, context):
1064 self.view.setFocus()
1065 self.find_bar.Busy()
1066 self.model.Find(value, direction, pattern, context, self.FindDone)
1068 def FindDone(self, ids):
1070 if not self.DisplayFound(ids):
1072 self.find_bar.Idle()
1074 self.find_bar.NotFound()
1077 # Context-sensitive call graph window
1079 class CallGraphWindow(TreeWindowBase):
1081 def __init__(self, glb, parent=None):
1082 super(CallGraphWindow, self).__init__(parent)
1084 self.model = LookupCreateModel("Context-Sensitive Call Graph", lambda x=glb: CallGraphModel(x))
1086 self.view.setModel(self.model)
1088 for c, w in ((0, 250), (1, 100), (2, 60), (3, 70), (4, 70), (5, 100)):
1089 self.view.setColumnWidth(c, w)
1091 self.find_bar = FindBar(self, self)
1093 self.vbox = VBox(self.view, self.find_bar.Widget())
1095 self.setWidget(self.vbox.Widget())
1097 AddSubWindow(glb.mainwindow.mdi_area, self, "Context-Sensitive Call Graph")
1101 class CallTreeWindow(TreeWindowBase):
1103 def __init__(self, glb, parent=None, thread_at_time=None):
1104 super(CallTreeWindow, self).__init__(parent)
1106 self.model = LookupCreateModel("Call Tree", lambda x=glb: CallTreeModel(x))
1108 self.view.setModel(self.model)
1110 for c, w in ((0, 230), (1, 100), (2, 100), (3, 70), (4, 70), (5, 100)):
1111 self.view.setColumnWidth(c, w)
1113 self.find_bar = FindBar(self, self)
1115 self.vbox = VBox(self.view, self.find_bar.Widget())
1117 self.setWidget(self.vbox.Widget())
1119 AddSubWindow(glb.mainwindow.mdi_area, self, "Call Tree")
1122 self.DisplayThreadAtTime(*thread_at_time)
1124 def DisplayThreadAtTime(self, comm_id, thread_id, time):
1125 parent = QModelIndex()
1126 for dbid in (comm_id, thread_id):
1128 n = self.model.rowCount(parent)
1129 for row in xrange(n):
1130 child = self.model.index(row, 0, parent)
1131 if child.internalPointer().dbid == dbid:
1133 self.view.setCurrentIndex(child)
1140 n = self.model.rowCount(parent)
1144 for row in xrange(n):
1145 child = self.model.index(row, 0, parent)
1146 child_call_time = child.internalPointer().call_time
1147 if child_call_time < time:
1149 elif child_call_time == time:
1150 self.view.setCurrentIndex(child)
1152 elif child_call_time > time:
1156 child = self.model.index(0, 0, parent)
1157 self.view.setCurrentIndex(child)
1160 self.view.setCurrentIndex(last_child)
1163 # ExecComm() gets the comm_id of the command string that was set when the process exec'd i.e. the program name
1165 def ExecComm(db, thread_id, time):
1166 query = QSqlQuery(db)
1167 QueryExec(query, "SELECT comm_threads.comm_id, comms.c_time, comms.exec_flag"
1168 " FROM comm_threads"
1169 " INNER JOIN comms ON comms.id = comm_threads.comm_id"
1170 " WHERE comm_threads.thread_id = " + str(thread_id) +
1171 " ORDER BY comms.c_time, comms.id")
1176 first = query.value(0)
1177 if query.value(2) and Decimal(query.value(1)) <= Decimal(time):
1178 last = query.value(0)
1179 if not(last is None):
1183 # Container for (x, y) data
1186 def __init__(self, x=0, y=0):
1191 return "XY({}, {})".format(str(self.x), str(self.y))
1193 # Container for sub-range data
1196 def __init__(self, lo=0, hi=0):
1201 return "Subrange({}, {})".format(str(self.lo), str(self.hi))
1203 # Graph data region base class
1205 class GraphDataRegion(object):
1207 def __init__(self, key, title = "", ordinal = ""):
1210 self.ordinal = ordinal
1212 # Function to sort GraphDataRegion
1214 def GraphDataRegionOrdinal(data_region):
1215 return data_region.ordinal
1217 # Attributes for a graph region
1219 class GraphRegionAttribute():
1221 def __init__(self, colour):
1222 self.colour = colour
1224 # Switch graph data region represents a task
1226 class SwitchGraphDataRegion(GraphDataRegion):
1228 def __init__(self, key, exec_comm_id, pid, tid, comm, thread_id, comm_id):
1229 super(SwitchGraphDataRegion, self).__init__(key)
1231 self.title = str(pid) + " / " + str(tid) + " " + comm
1232 # Order graph legend within exec comm by pid / tid / time
1233 self.ordinal = str(pid).rjust(16) + str(exec_comm_id).rjust(8) + str(tid).rjust(16)
1234 self.exec_comm_id = exec_comm_id
1238 self.thread_id = thread_id
1239 self.comm_id = comm_id
1243 class GraphDataPoint():
1245 def __init__(self, data, index, x, y, altx=None, alty=None, hregion=None, vregion=None):
1252 self.hregion = hregion
1253 self.vregion = vregion
1255 # Graph data (single graph) base class
1257 class GraphData(object):
1259 def __init__(self, collection, xbase=Decimal(0), ybase=Decimal(0)):
1260 self.collection = collection
1266 def AddPoint(self, x, y, altx=None, alty=None, hregion=None, vregion=None):
1267 index = len(self.points)
1269 x = float(Decimal(x) - self.xbase)
1270 y = float(Decimal(y) - self.ybase)
1272 self.points.append(GraphDataPoint(self, index, x, y, altx, alty, hregion, vregion))
1274 def XToData(self, x):
1275 return Decimal(x) + self.xbase
1277 def YToData(self, y):
1278 return Decimal(y) + self.ybase
1280 # Switch graph data (for one CPU)
1282 class SwitchGraphData(GraphData):
1284 def __init__(self, db, collection, cpu, xbase):
1285 super(SwitchGraphData, self).__init__(collection, xbase)
1288 self.title = "CPU " + str(cpu)
1289 self.SelectSwitches(db)
1291 def SelectComms(self, db, thread_id, last_comm_id, start_time, end_time):
1292 query = QSqlQuery(db)
1293 QueryExec(query, "SELECT id, c_time"
1295 " WHERE c_thread_id = " + str(thread_id) +
1296 " AND exec_flag = " + self.collection.glb.dbref.TRUE +
1297 " AND c_time >= " + str(start_time) +
1298 " AND c_time <= " + str(end_time) +
1299 " ORDER BY c_time, id")
1301 comm_id = query.value(0)
1302 if comm_id == last_comm_id:
1304 time = query.value(1)
1305 hregion = self.HRegion(db, thread_id, comm_id, time)
1306 self.AddPoint(time, 1000, None, None, hregion)
1308 def SelectSwitches(self, db):
1311 last_thread_id = None
1312 query = QSqlQuery(db)
1313 QueryExec(query, "SELECT time, thread_out_id, thread_in_id, comm_out_id, comm_in_id, flags"
1314 " FROM context_switches"
1315 " WHERE machine_id = " + str(self.collection.machine_id) +
1316 " AND cpu = " + str(self.cpu) +
1317 " ORDER BY time, id")
1319 flags = int(query.value(5))
1321 # Schedule-out: detect and add exec's
1322 if last_thread_id == query.value(1) and last_comm_id is not None and last_comm_id != query.value(3):
1323 self.SelectComms(db, last_thread_id, last_comm_id, last_time, query.value(0))
1325 # Schedule-in: add data point
1326 if len(self.points) == 0:
1327 start_time = self.collection.glb.StartTime(self.collection.machine_id)
1328 hregion = self.HRegion(db, query.value(1), query.value(3), start_time)
1329 self.AddPoint(start_time, 1000, None, None, hregion)
1330 time = query.value(0)
1331 comm_id = query.value(4)
1332 thread_id = query.value(2)
1333 hregion = self.HRegion(db, thread_id, comm_id, time)
1334 self.AddPoint(time, 1000, None, None, hregion)
1336 last_comm_id = comm_id
1337 last_thread_id = thread_id
1339 def NewHRegion(self, db, key, thread_id, comm_id, time):
1340 exec_comm_id = ExecComm(db, thread_id, time)
1341 query = QSqlQuery(db)
1342 QueryExec(query, "SELECT pid, tid FROM threads WHERE id = " + str(thread_id))
1344 pid = query.value(0)
1345 tid = query.value(1)
1349 query = QSqlQuery(db)
1350 QueryExec(query, "SELECT comm FROM comms WHERE id = " + str(comm_id))
1352 comm = query.value(0)
1355 return SwitchGraphDataRegion(key, exec_comm_id, pid, tid, comm, thread_id, comm_id)
1357 def HRegion(self, db, thread_id, comm_id, time):
1358 key = str(thread_id) + ":" + str(comm_id)
1359 hregion = self.collection.LookupHRegion(key)
1361 hregion = self.NewHRegion(db, key, thread_id, comm_id, time)
1362 self.collection.AddHRegion(key, hregion)
1365 # Graph data collection (multiple related graphs) base class
1367 class GraphDataCollection(object):
1369 def __init__(self, glb):
1373 self.xrangelo = None
1374 self.xrangehi = None
1375 self.yrangelo = None
1376 self.yrangehi = None
1379 def AddGraphData(self, data):
1380 self.data.append(data)
1382 def LookupHRegion(self, key):
1383 if key in self.hregions:
1384 return self.hregions[key]
1387 def AddHRegion(self, key, hregion):
1388 self.hregions[key] = hregion
1390 # Switch graph data collection (SwitchGraphData for each CPU)
1392 class SwitchGraphDataCollection(GraphDataCollection):
1394 def __init__(self, glb, db, machine_id):
1395 super(SwitchGraphDataCollection, self).__init__(glb)
1397 self.machine_id = machine_id
1398 self.cpus = self.SelectCPUs(db)
1400 self.xrangelo = glb.StartTime(machine_id)
1401 self.xrangehi = glb.FinishTime(machine_id)
1403 self.yrangelo = Decimal(0)
1404 self.yrangehi = Decimal(1000)
1406 for cpu in self.cpus:
1407 self.AddGraphData(SwitchGraphData(db, self, cpu, self.xrangelo))
1409 def SelectCPUs(self, db):
1411 query = QSqlQuery(db)
1412 QueryExec(query, "SELECT DISTINCT cpu"
1413 " FROM context_switches"
1414 " WHERE machine_id = " + str(self.machine_id))
1416 cpus.append(int(query.value(0)))
1419 # Switch graph data graphics item displays the graphed data
1421 class SwitchGraphDataGraphicsItem(QGraphicsItem):
1423 def __init__(self, data, graph_width, graph_height, attrs, event_handler, parent=None):
1424 super(SwitchGraphDataGraphicsItem, self).__init__(parent)
1427 self.graph_width = graph_width
1428 self.graph_height = graph_height
1430 self.event_handler = event_handler
1431 self.setAcceptHoverEvents(True)
1433 def boundingRect(self):
1434 return QRectF(0, 0, self.graph_width, self.graph_height)
1436 def PaintPoint(self, painter, last, x):
1437 if not(last is None or last.hregion.pid == 0 or x < self.attrs.subrange.x.lo):
1438 if last.x < self.attrs.subrange.x.lo:
1439 x0 = self.attrs.subrange.x.lo
1442 if x > self.attrs.subrange.x.hi:
1443 x1 = self.attrs.subrange.x.hi
1446 x0 = self.attrs.XToPixel(x0)
1447 x1 = self.attrs.XToPixel(x1)
1449 y0 = self.attrs.YToPixel(last.y)
1451 colour = self.attrs.region_attributes[last.hregion.key].colour
1455 painter.setPen(colour)
1456 painter.drawLine(x0, self.graph_height - y0, x0, self.graph_height)
1458 painter.fillRect(x0, self.graph_height - y0, width, self.graph_height - 1, colour)
1460 def paint(self, painter, option, widget):
1462 for point in self.data.points:
1463 self.PaintPoint(painter, last, point.x)
1464 if point.x > self.attrs.subrange.x.hi:
1467 self.PaintPoint(painter, last, self.attrs.subrange.x.hi + 1)
1469 def BinarySearchPoint(self, target):
1471 higher_pos = len(self.data.points)
1473 pos = int((lower_pos + higher_pos) / 2)
1474 val = self.data.points[pos].x
1479 if higher_pos <= lower_pos + 1:
1482 def XPixelToData(self, x):
1483 x = self.attrs.PixelToX(x)
1484 if x < self.data.points[0].x:
1489 pos = self.BinarySearchPoint(x)
1491 return (low, pos, self.data.XToData(x))
1493 def EventToData(self, event):
1494 no_data = (None,) * 4
1495 if len(self.data.points) < 1:
1500 low0, pos0, time_from = self.XPixelToData(x)
1501 low1, pos1, time_to = self.XPixelToData(x + 1)
1505 for i in xrange(pos0, pos1 + 1):
1506 hregion = self.data.points[i].hregion
1507 hregions.add(hregion)
1511 time = self.data.XToData(self.data.points[i].x)
1512 hregion_times.append((hregion, time))
1513 return (time_from, time_to, hregions, hregion_times)
1515 def hoverMoveEvent(self, event):
1516 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1517 if time_from is not None:
1518 self.event_handler.PointEvent(self.data.cpu, time_from, time_to, hregions)
1520 def hoverLeaveEvent(self, event):
1521 self.event_handler.NoPointEvent()
1523 def mousePressEvent(self, event):
1524 if event.button() != Qt.RightButton:
1525 super(SwitchGraphDataGraphicsItem, self).mousePressEvent(event)
1527 time_from, time_to, hregions, hregion_times = self.EventToData(event)
1529 self.event_handler.RightClickEvent(self.data.cpu, hregion_times, event.screenPos())
1531 # X-axis graphics item
1533 class XAxisGraphicsItem(QGraphicsItem):
1535 def __init__(self, width, parent=None):
1536 super(XAxisGraphicsItem, self).__init__(parent)
1539 self.max_mark_sz = 4
1540 self.height = self.max_mark_sz + 1
1542 def boundingRect(self):
1543 return QRectF(0, 0, self.width, self.height)
1546 attrs = self.parentItem().attrs
1547 subrange = attrs.subrange.x
1548 t = subrange.hi - subrange.lo
1549 s = (3.0 * t) / self.width
1555 def PaintMarks(self, painter, at_y, lo, hi, step, i):
1556 attrs = self.parentItem().attrs
1559 xp = attrs.XToPixel(x)
1566 sz = self.max_mark_sz
1568 painter.drawLine(xp, at_y, xp, at_y + sz)
1572 def paint(self, painter, option, widget):
1573 # Using QPainter::drawLine(int x1, int y1, int x2, int y2) so x2 = width -1
1574 painter.drawLine(0, 0, self.width - 1, 0)
1576 attrs = self.parentItem().attrs
1577 subrange = attrs.subrange.x
1579 x_offset = n - (subrange.lo % n)
1582 x = subrange.lo + x_offset
1584 self.PaintMarks(painter, 0, x, subrange.hi, n, i)
1586 def ScaleDimensions(self):
1588 attrs = self.parentItem().attrs
1589 lo = attrs.subrange.x.lo
1590 hi = (n * 10.0) + lo
1591 width = attrs.XToPixel(hi)
1594 return (n, lo, hi, width)
1596 def PaintScale(self, painter, at_x, at_y):
1597 n, lo, hi, width = self.ScaleDimensions()
1600 painter.drawLine(at_x, at_y, at_x + width, at_y)
1601 self.PaintMarks(painter, at_y, lo, hi, n, 0)
1603 def ScaleWidth(self):
1604 n, lo, hi, width = self.ScaleDimensions()
1607 def ScaleHeight(self):
1610 def ScaleUnit(self):
1611 return self.Step() * 10
1613 # Scale graphics item base class
1615 class ScaleGraphicsItem(QGraphicsItem):
1617 def __init__(self, axis, parent=None):
1618 super(ScaleGraphicsItem, self).__init__(parent)
1621 def boundingRect(self):
1622 scale_width = self.axis.ScaleWidth()
1625 return QRectF(0, 0, self.axis.ScaleWidth() + 100, self.axis.ScaleHeight())
1627 def paint(self, painter, option, widget):
1628 scale_width = self.axis.ScaleWidth()
1631 self.axis.PaintScale(painter, 0, 5)
1633 painter.drawText(QPointF(x, 10), self.Text())
1636 return self.axis.ScaleUnit()
1641 # Switch graph scale graphics item
1643 class SwitchScaleGraphicsItem(ScaleGraphicsItem):
1645 def __init__(self, axis, parent=None):
1646 super(SwitchScaleGraphicsItem, self).__init__(axis, parent)
1650 if unit >= 1000000000:
1651 unit = int(unit / 1000000000)
1653 elif unit >= 1000000:
1654 unit = int(unit / 1000000)
1657 unit = int(unit / 1000)
1662 return " = " + str(unit) + " " + us
1664 # Switch graph graphics item contains graph title, scale, x/y-axis, and the graphed data
1666 class SwitchGraphGraphicsItem(QGraphicsItem):
1668 def __init__(self, collection, data, attrs, event_handler, first, parent=None):
1669 super(SwitchGraphGraphicsItem, self).__init__(parent)
1670 self.collection = collection
1673 self.event_handler = event_handler
1678 self.title_graphics = QGraphicsSimpleTextItem(data.title, self)
1680 self.title_graphics.setPos(margin, margin)
1681 graph_width = attrs.XToPixel(attrs.subrange.x.hi) + 1
1682 graph_height = attrs.YToPixel(attrs.subrange.y.hi) + 1
1684 self.graph_origin_x = margin + title_width + margin
1685 self.graph_origin_y = graph_height + margin
1689 self.yline = QGraphicsLineItem(0, 0, 0, graph_height, self)
1691 self.x_axis = XAxisGraphicsItem(graph_width, self)
1692 self.x_axis.setPos(self.graph_origin_x, self.graph_origin_y + 1)
1695 self.scale_item = SwitchScaleGraphicsItem(self.x_axis, self)
1696 self.scale_item.setPos(self.graph_origin_x, self.graph_origin_y + 10)
1698 self.yline.setPos(self.graph_origin_x - y_axis_size, self.graph_origin_y - graph_height)
1700 self.axis_point = QGraphicsLineItem(0, 0, 0, 0, self)
1701 self.axis_point.setPos(self.graph_origin_x - 1, self.graph_origin_y +1)
1703 self.width = self.graph_origin_x + graph_width + margin
1704 self.height = self.graph_origin_y + margin
1706 self.graph = SwitchGraphDataGraphicsItem(data, graph_width, graph_height, attrs, event_handler, self)
1707 self.graph.setPos(self.graph_origin_x, self.graph_origin_y - graph_height)
1709 if parent and 'EnableRubberBand' in dir(parent):
1710 parent.EnableRubberBand(self.graph_origin_x, self.graph_origin_x + graph_width - 1, self)
1712 def boundingRect(self):
1713 return QRectF(0, 0, self.width, self.height)
1715 def paint(self, painter, option, widget):
1718 def RBXToPixel(self, x):
1719 return self.attrs.PixelToX(x - self.graph_origin_x)
1721 def RBXRangeToPixel(self, x0, x1):
1722 return (self.RBXToPixel(x0), self.RBXToPixel(x1 + 1))
1724 def RBPixelToTime(self, x):
1725 if x < self.data.points[0].x:
1726 return self.data.XToData(0)
1727 return self.data.XToData(x)
1729 def RBEventTimes(self, x0, x1):
1730 x0, x1 = self.RBXRangeToPixel(x0, x1)
1731 time_from = self.RBPixelToTime(x0)
1732 time_to = self.RBPixelToTime(x1)
1733 return (time_from, time_to)
1735 def RBEvent(self, x0, x1):
1736 time_from, time_to = self.RBEventTimes(x0, x1)
1737 self.event_handler.RangeEvent(time_from, time_to)
1739 def RBMoveEvent(self, x0, x1):
1742 self.RBEvent(x0, x1)
1744 def RBReleaseEvent(self, x0, x1, selection_state):
1747 x0, x1 = self.RBXRangeToPixel(x0, x1)
1748 self.event_handler.SelectEvent(x0, x1, selection_state)
1750 # Graphics item to draw a vertical bracket (used to highlight "forward" sub-range)
1752 class VerticalBracketGraphicsItem(QGraphicsItem):
1754 def __init__(self, parent=None):
1755 super(VerticalBracketGraphicsItem, self).__init__(parent)
1761 def SetSize(self, width, height):
1762 self.width = width + 1
1763 self.height = height + 1
1765 def boundingRect(self):
1766 return QRectF(0, 0, self.width, self.height)
1768 def paint(self, painter, option, widget):
1769 colour = QColor(255, 255, 0, 32)
1770 painter.fillRect(0, 0, self.width, self.height, colour)
1772 y1 = self.height - 1
1773 painter.drawLine(0, 0, x1, 0)
1774 painter.drawLine(0, 0, 0, 3)
1775 painter.drawLine(x1, 0, x1, 3)
1776 painter.drawLine(0, y1, x1, y1)
1777 painter.drawLine(0, y1, 0, y1 - 3)
1778 painter.drawLine(x1, y1, x1, y1 - 3)
1780 # Graphics item to contain graphs arranged vertically
1782 class VertcalGraphSetGraphicsItem(QGraphicsItem):
1784 def __init__(self, collection, attrs, event_handler, child_class, parent=None):
1785 super(VertcalGraphSetGraphicsItem, self).__init__(parent)
1787 self.collection = collection
1792 self.height = self.top
1794 self.rubber_band = None
1795 self.rb_enabled = False
1798 for data in collection.data:
1799 child = child_class(collection, data, attrs, event_handler, first, self)
1800 child.setPos(0, self.height + 1)
1801 rect = child.boundingRect()
1802 if rect.right() > self.width:
1803 self.width = rect.right()
1804 self.height = self.height + rect.bottom() + 1
1807 self.bracket = VerticalBracketGraphicsItem(self)
1809 def EnableRubberBand(self, xlo, xhi, rb_event_handler):
1812 self.rb_enabled = True
1813 self.rb_in_view = False
1814 self.setAcceptedMouseButtons(Qt.LeftButton)
1817 self.rb_event_handler = rb_event_handler
1818 self.mousePressEvent = self.MousePressEvent
1819 self.mouseMoveEvent = self.MouseMoveEvent
1820 self.mouseReleaseEvent = self.MouseReleaseEvent
1822 def boundingRect(self):
1823 return QRectF(0, 0, self.width, self.height)
1825 def paint(self, painter, option, widget):
1828 def RubberBandParent(self):
1829 scene = self.scene()
1830 view = scene.views()[0]
1831 viewport = view.viewport()
1834 def RubberBandSetGeometry(self, rect):
1835 scene_rectf = self.mapRectToScene(QRectF(rect))
1836 scene = self.scene()
1837 view = scene.views()[0]
1838 poly = view.mapFromScene(scene_rectf)
1839 self.rubber_band.setGeometry(poly.boundingRect())
1841 def SetSelection(self, selection_state):
1842 if self.rubber_band:
1844 self.RubberBandSetGeometry(selection_state)
1845 self.rubber_band.show()
1847 self.rubber_band.hide()
1849 def SetBracket(self, rect):
1851 x, y, width, height = rect.x(), rect.y(), rect.width(), rect.height()
1852 self.bracket.setPos(x, y)
1853 self.bracket.SetSize(width, height)
1858 def RubberBandX(self, event):
1859 x = event.pos().toPoint().x()
1862 elif x > self.rb_xhi:
1865 self.rb_in_view = True
1868 def RubberBandRect(self, x):
1869 if self.rb_origin.x() <= x:
1870 width = x - self.rb_origin.x()
1871 rect = QRect(self.rb_origin, QSize(width, self.height))
1873 width = self.rb_origin.x() - x
1874 top_left = QPoint(self.rb_origin.x() - width, self.rb_origin.y())
1875 rect = QRect(top_left, QSize(width, self.height))
1878 def MousePressEvent(self, event):
1879 self.rb_in_view = False
1880 x = self.RubberBandX(event)
1881 self.rb_origin = QPoint(x, self.top)
1882 if self.rubber_band is None:
1883 self.rubber_band = QRubberBand(QRubberBand.Rectangle, self.RubberBandParent())
1884 self.RubberBandSetGeometry(QRect(self.rb_origin, QSize(0, self.height)))
1886 self.rubber_band.show()
1887 self.rb_event_handler.RBMoveEvent(x, x)
1889 self.rubber_band.hide()
1891 def MouseMoveEvent(self, event):
1892 x = self.RubberBandX(event)
1893 rect = self.RubberBandRect(x)
1894 self.RubberBandSetGeometry(rect)
1896 self.rubber_band.show()
1897 self.rb_event_handler.RBMoveEvent(self.rb_origin.x(), x)
1899 def MouseReleaseEvent(self, event):
1900 x = self.RubberBandX(event)
1902 selection_state = self.RubberBandRect(x)
1904 selection_state = None
1905 self.rb_event_handler.RBReleaseEvent(self.rb_origin.x(), x, selection_state)
1907 # Switch graph legend data model
1909 class SwitchGraphLegendModel(QAbstractTableModel):
1911 def __init__(self, collection, region_attributes, parent=None):
1912 super(SwitchGraphLegendModel, self).__init__(parent)
1914 self.region_attributes = region_attributes
1916 self.child_items = sorted(collection.hregions.values(), key=GraphDataRegionOrdinal)
1917 self.child_count = len(self.child_items)
1919 self.highlight_set = set()
1921 self.column_headers = ("pid", "tid", "comm")
1923 def rowCount(self, parent):
1924 return self.child_count
1926 def headerData(self, section, orientation, role):
1927 if role != Qt.DisplayRole:
1929 if orientation != Qt.Horizontal:
1931 return self.columnHeader(section)
1933 def index(self, row, column, parent):
1934 return self.createIndex(row, column, self.child_items[row])
1936 def columnCount(self, parent=None):
1937 return len(self.column_headers)
1939 def columnHeader(self, column):
1940 return self.column_headers[column]
1942 def data(self, index, role):
1943 if role == Qt.BackgroundRole:
1944 child = self.child_items[index.row()]
1945 if child in self.highlight_set:
1946 return self.region_attributes[child.key].colour
1948 if role == Qt.ForegroundRole:
1949 child = self.child_items[index.row()]
1950 if child in self.highlight_set:
1951 return QColor(255, 255, 255)
1952 return self.region_attributes[child.key].colour
1953 if role != Qt.DisplayRole:
1955 hregion = self.child_items[index.row()]
1956 col = index.column()
1965 def SetHighlight(self, row, set_highlight):
1966 child = self.child_items[row]
1967 top_left = self.createIndex(row, 0, child)
1968 bottom_right = self.createIndex(row, len(self.column_headers) - 1, child)
1969 self.dataChanged.emit(top_left, bottom_right)
1971 def Highlight(self, highlight_set):
1972 for row in xrange(self.child_count):
1973 child = self.child_items[row]
1974 if child in self.highlight_set:
1975 if child not in highlight_set:
1976 self.SetHighlight(row, False)
1977 elif child in highlight_set:
1978 self.SetHighlight(row, True)
1979 self.highlight_set = highlight_set
1981 # Switch graph legend is a table
1983 class SwitchGraphLegend(QWidget):
1985 def __init__(self, collection, region_attributes, parent=None):
1986 super(SwitchGraphLegend, self).__init__(parent)
1988 self.data_model = SwitchGraphLegendModel(collection, region_attributes)
1990 self.model = QSortFilterProxyModel()
1991 self.model.setSourceModel(self.data_model)
1993 self.view = QTableView()
1994 self.view.setModel(self.model)
1995 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
1996 self.view.verticalHeader().setVisible(False)
1997 self.view.sortByColumn(-1, Qt.AscendingOrder)
1998 self.view.setSortingEnabled(True)
1999 self.view.resizeColumnsToContents()
2000 self.view.resizeRowsToContents()
2002 self.vbox = VBoxLayout(self.view)
2003 self.setLayout(self.vbox)
2005 sz1 = self.view.columnWidth(0) + self.view.columnWidth(1) + self.view.columnWidth(2) + 2
2006 sz1 = sz1 + self.view.verticalScrollBar().sizeHint().width()
2007 self.saved_size = sz1
2009 def resizeEvent(self, event):
2010 self.saved_size = self.size().width()
2011 super(SwitchGraphLegend, self).resizeEvent(event)
2013 def Highlight(self, highlight_set):
2014 self.data_model.Highlight(highlight_set)
2017 def changeEvent(self, event):
2018 if event.type() == QEvent.FontChange:
2019 self.view.resizeRowsToContents()
2020 self.view.resizeColumnsToContents()
2021 # Need to resize rows again after column resize
2022 self.view.resizeRowsToContents()
2023 super(SwitchGraphLegend, self).changeEvent(event)
2025 # Random colour generation
2027 def RGBColourTooLight(r, g, b):
2032 if r <= 180 and g <= 180:
2038 def GenerateColours(x):
2040 for i in xrange(1, x):
2041 cs.append(int((255.0 / i) + 0.5))
2046 # Exclude black and colours that look too light against a white background
2047 if (r, g, b) == (0, 0, 0) or RGBColourTooLight(r, g, b):
2049 colours.append(QColor(r, g, b))
2052 def GenerateNColours(n):
2053 for x in xrange(2, n + 2):
2054 colours = GenerateColours(x)
2055 if len(colours) >= n:
2059 def GenerateNRandomColours(n, seed):
2060 colours = GenerateNColours(n)
2062 random.shuffle(colours)
2065 # Graph attributes, in particular the scale and subrange that change when zooming
2067 class GraphAttributes():
2069 def __init__(self, scale, subrange, region_attributes, dp):
2071 self.subrange = subrange
2072 self.region_attributes = region_attributes
2073 # Rounding avoids errors due to finite floating point precision
2074 self.dp = dp # data decimal places
2077 def XToPixel(self, x):
2078 return int(round((x - self.subrange.x.lo) * self.scale.x, self.pdp.x))
2080 def YToPixel(self, y):
2081 return int(round((y - self.subrange.y.lo) * self.scale.y, self.pdp.y))
2083 def PixelToXRounded(self, px):
2084 return round((round(px, 0) / self.scale.x), self.dp.x) + self.subrange.x.lo
2086 def PixelToYRounded(self, py):
2087 return round((round(py, 0) / self.scale.y), self.dp.y) + self.subrange.y.lo
2089 def PixelToX(self, px):
2090 x = self.PixelToXRounded(px)
2092 rt = self.XToPixel(x)
2097 def PixelToY(self, py):
2098 y = self.PixelToYRounded(py)
2100 rt = self.YToPixel(y)
2105 def ToPDP(self, dp, scale):
2106 # Calculate pixel decimal places:
2107 # (10 ** dp) is the minimum delta in the data
2108 # scale it to get the minimum delta in pixels
2109 # log10 gives the number of decimals places negatively
2110 # subtrace 1 to divide by 10
2111 # round to the lower negative number
2112 # change the sign to get the number of decimals positively
2113 x = math.log10((10 ** dp) * scale)
2116 x = -int(math.floor(x) - 0.1)
2122 x = self.ToPDP(self.dp.x, self.scale.x)
2123 y = self.ToPDP(self.dp.y, self.scale.y)
2124 self.pdp = XY(x, y) # pixel decimal places
2126 # Switch graph splitter which divides the CPU graphs from the legend
2128 class SwitchGraphSplitter(QSplitter):
2130 def __init__(self, parent=None):
2131 super(SwitchGraphSplitter, self).__init__(parent)
2133 self.first_time = False
2135 def resizeEvent(self, ev):
2137 self.first_time = False
2138 sz1 = self.widget(1).view.columnWidth(0) + self.widget(1).view.columnWidth(1) + self.widget(1).view.columnWidth(2) + 2
2139 sz1 = sz1 + self.widget(1).view.verticalScrollBar().sizeHint().width()
2140 sz0 = self.size().width() - self.handleWidth() - sz1
2141 self.setSizes([sz0, sz1])
2142 elif not(self.widget(1).saved_size is None):
2143 sz1 = self.widget(1).saved_size
2144 sz0 = self.size().width() - self.handleWidth() - sz1
2145 self.setSizes([sz0, sz1])
2146 super(SwitchGraphSplitter, self).resizeEvent(ev)
2148 # Graph widget base class
2150 class GraphWidget(QWidget):
2152 graph_title_changed = Signal(object)
2154 def __init__(self, parent=None):
2155 super(GraphWidget, self).__init__(parent)
2157 def GraphTitleChanged(self, title):
2158 self.graph_title_changed.emit(title)
2163 # Display time in s, ms, us or ns
2167 if val >= 1000000000:
2168 return "{} s".format((val / 1000000000).quantize(Decimal("0.000000001")))
2170 return "{} ms".format((val / 1000000).quantize(Decimal("0.000001")))
2172 return "{} us".format((val / 1000).quantize(Decimal("0.001")))
2173 return "{} ns".format(val.quantize(Decimal("1")))
2175 # Switch (i.e. context switch i.e. Time Chart by CPU) graph widget which contains the CPU graphs and the legend and control buttons
2177 class SwitchGraphWidget(GraphWidget):
2179 def __init__(self, glb, collection, parent=None):
2180 super(SwitchGraphWidget, self).__init__(parent)
2183 self.collection = collection
2185 self.back_state = []
2186 self.forward_state = []
2187 self.selection_state = (None, None)
2188 self.fwd_rect = None
2189 self.start_time = self.glb.StartTime(collection.machine_id)
2192 hregions = collection.hregions.values()
2193 colours = GenerateNRandomColours(len(hregions), 1013)
2194 region_attributes = {}
2195 for hregion in hregions:
2196 if hregion.pid == 0 and hregion.tid == 0:
2197 region_attributes[hregion.key] = GraphRegionAttribute(QColor(0, 0, 0))
2199 region_attributes[hregion.key] = GraphRegionAttribute(colours[i])
2202 # Default to entire range
2203 xsubrange = Subrange(0.0, float(collection.xrangehi - collection.xrangelo) + 1.0)
2204 ysubrange = Subrange(0.0, float(collection.yrangehi - collection.yrangelo) + 1.0)
2205 subrange = XY(xsubrange, ysubrange)
2207 scale = self.GetScaleForRange(subrange)
2209 self.attrs = GraphAttributes(scale, subrange, region_attributes, collection.dp)
2211 self.item = VertcalGraphSetGraphicsItem(collection, self.attrs, self, SwitchGraphGraphicsItem)
2213 self.scene = QGraphicsScene()
2214 self.scene.addItem(self.item)
2216 self.view = QGraphicsView(self.scene)
2217 self.view.centerOn(0, 0)
2218 self.view.setAlignment(Qt.AlignLeft | Qt.AlignTop)
2220 self.legend = SwitchGraphLegend(collection, region_attributes)
2222 self.splitter = SwitchGraphSplitter()
2223 self.splitter.addWidget(self.view)
2224 self.splitter.addWidget(self.legend)
2226 self.point_label = QLabel("")
2227 self.point_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
2229 self.back_button = QToolButton()
2230 self.back_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowLeft))
2231 self.back_button.setDisabled(True)
2232 self.back_button.released.connect(lambda: self.Back())
2234 self.forward_button = QToolButton()
2235 self.forward_button.setIcon(self.style().standardIcon(QStyle.SP_ArrowRight))
2236 self.forward_button.setDisabled(True)
2237 self.forward_button.released.connect(lambda: self.Forward())
2239 self.zoom_button = QToolButton()
2240 self.zoom_button.setText("Zoom")
2241 self.zoom_button.setDisabled(True)
2242 self.zoom_button.released.connect(lambda: self.Zoom())
2244 self.hbox = HBoxLayout(self.back_button, self.forward_button, self.zoom_button, self.point_label)
2246 self.vbox = VBoxLayout(self.splitter, self.hbox)
2248 self.setLayout(self.vbox)
2250 def GetScaleForRangeX(self, xsubrange):
2251 # Default graph 1000 pixels wide
2253 r = xsubrange.hi - xsubrange.lo
2256 def GetScaleForRangeY(self, ysubrange):
2257 # Default graph 50 pixels high
2259 r = ysubrange.hi - ysubrange.lo
2262 def GetScaleForRange(self, subrange):
2263 # Default graph 1000 pixels wide, 50 pixels high
2264 xscale = self.GetScaleForRangeX(subrange.x)
2265 yscale = self.GetScaleForRangeY(subrange.y)
2266 return XY(xscale, yscale)
2268 def PointEvent(self, cpu, time_from, time_to, hregions):
2269 text = "CPU: " + str(cpu)
2270 time_from = time_from.quantize(Decimal(1))
2271 rel_time_from = time_from - self.glb.StartTime(self.collection.machine_id)
2272 text = text + " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ")"
2273 self.point_label.setText(text)
2274 self.legend.Highlight(hregions)
2276 def RightClickEvent(self, cpu, hregion_times, pos):
2277 if not IsSelectable(self.glb.db, "calls", "WHERE parent_id >= 0"):
2279 menu = QMenu(self.view)
2280 for hregion, time in hregion_times:
2281 thread_at_time = (hregion.exec_comm_id, hregion.thread_id, time)
2282 menu_text = "Show Call Tree for {} {}:{} at {}".format(hregion.comm, hregion.pid, hregion.tid, time)
2283 menu.addAction(CreateAction(menu_text, "Show Call Tree", lambda a=None, args=thread_at_time: self.RightClickSelect(args), self.view))
2286 def RightClickSelect(self, args):
2287 CallTreeWindow(self.glb, self.glb.mainwindow, thread_at_time=args)
2289 def NoPointEvent(self):
2290 self.point_label.setText("")
2291 self.legend.Highlight({})
2293 def RangeEvent(self, time_from, time_to):
2294 time_from = time_from.quantize(Decimal(1))
2295 time_to = time_to.quantize(Decimal(1))
2296 if time_to <= time_from:
2297 self.point_label.setText("")
2299 rel_time_from = time_from - self.start_time
2300 rel_time_to = time_to - self.start_time
2301 text = " Time: " + str(time_from) + " (+" + ToTimeStr(rel_time_from) + ") to: " + str(time_to) + " (+" + ToTimeStr(rel_time_to) + ")"
2302 text = text + " duration: " + ToTimeStr(time_to - time_from)
2303 self.point_label.setText(text)
2305 def BackState(self):
2306 return (self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect)
2308 def PushBackState(self):
2309 state = copy.deepcopy(self.BackState())
2310 self.back_state.append(state)
2311 self.back_button.setEnabled(True)
2313 def PopBackState(self):
2314 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.back_state.pop()
2316 if not self.back_state:
2317 self.back_button.setDisabled(True)
2319 def PushForwardState(self):
2320 state = copy.deepcopy(self.BackState())
2321 self.forward_state.append(state)
2322 self.forward_button.setEnabled(True)
2324 def PopForwardState(self):
2325 self.attrs.subrange, self.attrs.scale, self.selection_state, self.fwd_rect = self.forward_state.pop()
2327 if not self.forward_state:
2328 self.forward_button.setDisabled(True)
2331 time_from = self.collection.xrangelo + Decimal(self.attrs.subrange.x.lo)
2332 time_to = self.collection.xrangelo + Decimal(self.attrs.subrange.x.hi)
2333 rel_time_from = time_from - self.start_time
2334 rel_time_to = time_to - self.start_time
2335 title = "+" + ToTimeStr(rel_time_from) + " to +" + ToTimeStr(rel_time_to)
2336 title = title + " (" + ToTimeStr(time_to - time_from) + ")"
2340 selected_subrange, selection_state = self.selection_state
2341 self.item.SetSelection(selection_state)
2342 self.item.SetBracket(self.fwd_rect)
2343 self.zoom_button.setDisabled(selected_subrange is None)
2344 self.GraphTitleChanged(self.Title())
2345 self.item.update(self.item.boundingRect())
2348 if not self.back_state:
2350 self.PushForwardState()
2355 if not self.forward_state:
2357 self.PushBackState()
2358 self.PopForwardState()
2361 def SelectEvent(self, x0, x1, selection_state):
2362 if selection_state is None:
2363 selected_subrange = None
2367 selected_subrange = Subrange(x0, x1)
2368 self.selection_state = (selected_subrange, selection_state)
2369 self.zoom_button.setDisabled(selected_subrange is None)
2372 selected_subrange, selection_state = self.selection_state
2373 if selected_subrange is None:
2375 self.fwd_rect = selection_state
2376 self.item.SetSelection(None)
2377 self.PushBackState()
2378 self.attrs.subrange.x = selected_subrange
2379 self.forward_state = []
2380 self.forward_button.setDisabled(True)
2381 self.selection_state = (None, None)
2382 self.fwd_rect = None
2383 self.attrs.scale.x = self.GetScaleForRangeX(self.attrs.subrange.x)
2387 # Slow initialization - perform non-GUI initialization in a separate thread and put up a modal message box while waiting
2389 class SlowInitClass():
2391 def __init__(self, glb, title, init_fn):
2392 self.init_fn = init_fn
2396 self.msg_box = QMessageBox(glb.mainwindow)
2397 self.msg_box.setText("Initializing " + title + ". Please wait.")
2398 self.msg_box.setWindowTitle("Initializing " + title)
2399 self.msg_box.setWindowIcon(glb.mainwindow.style().standardIcon(QStyle.SP_MessageBoxInformation))
2401 self.init_thread = Thread(self.ThreadFn, glb)
2402 self.init_thread.done.connect(lambda: self.Done(), Qt.QueuedConnection)
2404 self.init_thread.start()
2407 self.msg_box.done(0)
2409 def ThreadFn(self, glb):
2410 conn_name = "SlowInitClass" + str(os.getpid())
2411 db, dbname = glb.dbref.Open(conn_name)
2412 self.result = self.init_fn(db)
2417 while not self.done:
2418 self.msg_box.exec_()
2419 self.init_thread.wait()
2422 def SlowInit(glb, title, init_fn):
2423 init = SlowInitClass(glb, title, init_fn)
2424 return init.Result()
2426 # Time chart by CPU window
2428 class TimeChartByCPUWindow(QMdiSubWindow):
2430 def __init__(self, glb, parent=None):
2431 super(TimeChartByCPUWindow, self).__init__(parent)
2434 self.machine_id = glb.HostMachineId()
2435 self.collection_name = "SwitchGraphDataCollection " + str(self.machine_id)
2437 collection = LookupModel(self.collection_name)
2438 if collection is None:
2439 collection = SlowInit(glb, "Time Chart", self.Init)
2441 self.widget = SwitchGraphWidget(glb, collection, self)
2442 self.view = self.widget
2444 self.base_title = "Time Chart by CPU"
2445 self.setWindowTitle(self.base_title + self.widget.Title())
2446 self.widget.graph_title_changed.connect(self.GraphTitleChanged)
2448 self.setWidget(self.widget)
2450 AddSubWindow(glb.mainwindow.mdi_area, self, self.windowTitle())
2453 return LookupCreateModel(self.collection_name, lambda : SwitchGraphDataCollection(self.glb, db, self.machine_id))
2455 def GraphTitleChanged(self, title):
2456 self.setWindowTitle(self.base_title + " : " + title)
2458 # Child data item finder
2460 class ChildDataItemFinder():
2462 def __init__(self, root):
2464 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (None,) * 5
2468 def FindSelect(self):
2471 pattern = re.compile(self.value)
2472 for child in self.root.child_items:
2473 for column_data in child.data:
2474 if re.search(pattern, str(column_data)) is not None:
2475 self.rows.append(child.row)
2478 for child in self.root.child_items:
2479 for column_data in child.data:
2480 if self.value in str(column_data):
2481 self.rows.append(child.row)
2484 def FindValue(self):
2486 if self.last_value != self.value or self.pattern != self.last_pattern:
2488 if not len(self.rows):
2490 return self.rows[self.pos]
2492 def FindThread(self):
2493 if self.direction == 0 or self.value != self.last_value or self.pattern != self.last_pattern:
2494 row = self.FindValue()
2495 elif len(self.rows):
2496 if self.direction > 0:
2498 if self.pos >= len(self.rows):
2503 self.pos = len(self.rows) - 1
2504 row = self.rows[self.pos]
2509 def Find(self, value, direction, pattern, context, callback):
2510 self.value, self.direction, self.pattern, self.last_value, self.last_pattern = (value, direction,pattern, self.value, self.pattern)
2511 # Use a thread so the UI is not blocked
2512 thread = Thread(self.FindThread)
2513 thread.done.connect(lambda row, t=thread, c=callback: self.FindDone(t, c, row), Qt.QueuedConnection)
2516 def FindDone(self, thread, callback, row):
2519 # Number of database records to fetch in one go
2521 glb_chunk_sz = 10000
2523 # Background process for SQL data fetcher
2525 class SQLFetcherProcess():
2527 def __init__(self, dbref, sql, buffer, head, tail, fetch_count, fetching_done, process_target, wait_event, fetched_event, prep):
2528 # Need a unique connection name
2529 conn_name = "SQLFetcher" + str(os.getpid())
2530 self.db, dbname = dbref.Open(conn_name)
2532 self.buffer = buffer
2535 self.fetch_count = fetch_count
2536 self.fetching_done = fetching_done
2537 self.process_target = process_target
2538 self.wait_event = wait_event
2539 self.fetched_event = fetched_event
2541 self.query = QSqlQuery(self.db)
2542 self.query_limit = 0 if "$$last_id$$" in sql else 2
2546 self.local_head = self.head.value
2547 self.local_tail = self.tail.value
2550 if self.query_limit:
2551 if self.query_limit == 1:
2553 self.query_limit -= 1
2554 stmt = self.sql.replace("$$last_id$$", str(self.last_id))
2555 QueryExec(self.query, stmt)
2558 if not self.query.next():
2560 if not self.query.next():
2562 self.last_id = self.query.value(0)
2563 return self.prep(self.query)
2565 def WaitForTarget(self):
2567 self.wait_event.clear()
2568 target = self.process_target.value
2569 if target > self.fetched or target < 0:
2571 self.wait_event.wait()
2574 def HasSpace(self, sz):
2575 if self.local_tail <= self.local_head:
2576 space = len(self.buffer) - self.local_head
2579 if space >= glb_nsz:
2580 # Use 0 (or space < glb_nsz) to mean there is no more at the top of the buffer
2581 nd = pickle.dumps(0, pickle.HIGHEST_PROTOCOL)
2582 self.buffer[self.local_head : self.local_head + len(nd)] = nd
2584 if self.local_tail - self.local_head > sz:
2588 def WaitForSpace(self, sz):
2589 if self.HasSpace(sz):
2592 self.wait_event.clear()
2593 self.local_tail = self.tail.value
2594 if self.HasSpace(sz):
2596 self.wait_event.wait()
2598 def AddToBuffer(self, obj):
2599 d = pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)
2601 nd = pickle.dumps(n, pickle.HIGHEST_PROTOCOL)
2603 self.WaitForSpace(sz)
2604 pos = self.local_head
2605 self.buffer[pos : pos + len(nd)] = nd
2606 self.buffer[pos + glb_nsz : pos + sz] = d
2607 self.local_head += sz
2609 def FetchBatch(self, batch_size):
2611 while batch_size > fetched:
2616 self.AddToBuffer(obj)
2619 self.fetched += fetched
2620 with self.fetch_count.get_lock():
2621 self.fetch_count.value += fetched
2622 self.head.value = self.local_head
2623 self.fetched_event.set()
2627 target = self.WaitForTarget()
2630 batch_size = min(glb_chunk_sz, target - self.fetched)
2631 self.FetchBatch(batch_size)
2632 self.fetching_done.value = True
2633 self.fetched_event.set()
2635 def SQLFetcherFn(*x):
2636 process = SQLFetcherProcess(*x)
2641 class SQLFetcher(QObject):
2643 done = Signal(object)
2645 def __init__(self, glb, sql, prep, process_data, parent=None):
2646 super(SQLFetcher, self).__init__(parent)
2647 self.process_data = process_data
2650 self.last_target = 0
2652 self.buffer_size = 16 * 1024 * 1024
2653 self.buffer = Array(c_char, self.buffer_size, lock=False)
2654 self.head = Value(c_longlong)
2655 self.tail = Value(c_longlong)
2657 self.fetch_count = Value(c_longlong)
2658 self.fetching_done = Value(c_bool)
2660 self.process_target = Value(c_longlong)
2661 self.wait_event = Event()
2662 self.fetched_event = Event()
2663 glb.AddInstanceToShutdownOnExit(self)
2664 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))
2665 self.process.start()
2666 self.thread = Thread(self.Thread)
2667 self.thread.done.connect(self.ProcessData, Qt.QueuedConnection)
2671 # Tell the thread and process to exit
2672 self.process_target.value = -1
2673 self.wait_event.set()
2675 self.fetching_done.value = True
2676 self.fetched_event.set()
2682 self.fetched_event.clear()
2683 fetch_count = self.fetch_count.value
2684 if fetch_count != self.last_count:
2686 if self.fetching_done.value:
2689 self.fetched_event.wait()
2690 count = fetch_count - self.last_count
2691 self.last_count = fetch_count
2692 self.fetched += count
2695 def Fetch(self, nr):
2697 # -1 inidcates there are no more
2699 result = self.fetched
2700 extra = result + nr - self.target
2702 self.target += extra
2703 # process_target < 0 indicates shutting down
2704 if self.process_target.value >= 0:
2705 self.process_target.value = self.target
2706 self.wait_event.set()
2709 def RemoveFromBuffer(self):
2710 pos = self.local_tail
2711 if len(self.buffer) - pos < glb_nsz:
2713 n = pickle.loads(self.buffer[pos : pos + glb_nsz])
2716 n = pickle.loads(self.buffer[0 : glb_nsz])
2718 obj = pickle.loads(self.buffer[pos : pos + n])
2719 self.local_tail = pos + n
2722 def ProcessData(self, count):
2723 for i in xrange(count):
2724 obj = self.RemoveFromBuffer()
2725 self.process_data(obj)
2726 self.tail.value = self.local_tail
2727 self.wait_event.set()
2728 self.done.emit(count)
2730 # Fetch more records bar
2732 class FetchMoreRecordsBar():
2734 def __init__(self, model, parent):
2737 self.label = QLabel("Number of records (x " + "{:,}".format(glb_chunk_sz) + ") to fetch:")
2738 self.label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2740 self.fetch_count = QSpinBox()
2741 self.fetch_count.setRange(1, 1000000)
2742 self.fetch_count.setValue(10)
2743 self.fetch_count.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2745 self.fetch = QPushButton("Go!")
2746 self.fetch.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
2747 self.fetch.released.connect(self.FetchMoreRecords)
2749 self.progress = QProgressBar()
2750 self.progress.setRange(0, 100)
2751 self.progress.hide()
2753 self.done_label = QLabel("All records fetched")
2754 self.done_label.hide()
2756 self.spacer = QLabel("")
2758 self.close_button = QToolButton()
2759 self.close_button.setIcon(parent.style().standardIcon(QStyle.SP_DockWidgetCloseButton))
2760 self.close_button.released.connect(self.Deactivate)
2762 self.hbox = QHBoxLayout()
2763 self.hbox.setContentsMargins(0, 0, 0, 0)
2765 self.hbox.addWidget(self.label)
2766 self.hbox.addWidget(self.fetch_count)
2767 self.hbox.addWidget(self.fetch)
2768 self.hbox.addWidget(self.spacer)
2769 self.hbox.addWidget(self.progress)
2770 self.hbox.addWidget(self.done_label)
2771 self.hbox.addWidget(self.close_button)
2773 self.bar = QWidget()
2774 self.bar.setLayout(self.hbox)
2777 self.in_progress = False
2778 self.model.progress.connect(self.Progress)
2782 if not model.HasMoreRecords():
2790 self.fetch.setFocus()
2792 def Deactivate(self):
2795 def Enable(self, enable):
2796 self.fetch.setEnabled(enable)
2797 self.fetch_count.setEnabled(enable)
2803 self.progress.show()
2806 self.in_progress = False
2808 self.progress.hide()
2813 return self.fetch_count.value() * glb_chunk_sz
2819 self.fetch_count.hide()
2822 self.done_label.show()
2824 def Progress(self, count):
2825 if self.in_progress:
2827 percent = ((count - self.start) * 100) / self.Target()
2831 self.progress.setValue(percent)
2833 # Count value of zero means no more records
2836 def FetchMoreRecords(self):
2839 self.progress.setValue(0)
2841 self.in_progress = True
2842 self.start = self.model.FetchMoreRecords(self.Target())
2844 # Brance data model level two item
2846 class BranchLevelTwoItem():
2848 def __init__(self, row, col, text, parent_item):
2850 self.parent_item = parent_item
2851 self.data = [""] * (col + 1)
2852 self.data[col] = text
2855 def getParentItem(self):
2856 return self.parent_item
2861 def childCount(self):
2864 def hasChildren(self):
2867 def getData(self, column):
2868 return self.data[column]
2870 # Brance data model level one item
2872 class BranchLevelOneItem():
2874 def __init__(self, glb, row, data, parent_item):
2877 self.parent_item = parent_item
2878 self.child_count = 0
2879 self.child_items = []
2880 self.data = data[1:]
2883 self.query_done = False
2884 self.br_col = len(self.data) - 1
2886 def getChildItem(self, row):
2887 return self.child_items[row]
2889 def getParentItem(self):
2890 return self.parent_item
2896 self.query_done = True
2898 if not self.glb.have_disassembler:
2901 query = QSqlQuery(self.glb.db)
2903 QueryExec(query, "SELECT cpu, to_dso_id, to_symbol_id, to_sym_offset, short_name, long_name, build_id, sym_start, to_ip"
2905 " INNER JOIN dsos ON samples.to_dso_id = dsos.id"
2906 " INNER JOIN symbols ON samples.to_symbol_id = symbols.id"
2907 " WHERE samples.id = " + str(self.dbid))
2908 if not query.next():
2910 cpu = query.value(0)
2911 dso = query.value(1)
2912 sym = query.value(2)
2913 if dso == 0 or sym == 0:
2915 off = query.value(3)
2916 short_name = query.value(4)
2917 long_name = query.value(5)
2918 build_id = query.value(6)
2919 sym_start = query.value(7)
2922 QueryExec(query, "SELECT samples.dso_id, symbol_id, sym_offset, sym_start"
2924 " INNER JOIN symbols ON samples.symbol_id = symbols.id"
2925 " WHERE samples.id > " + str(self.dbid) + " AND cpu = " + str(cpu) +
2926 " ORDER BY samples.id"
2928 if not query.next():
2930 if query.value(0) != dso:
2931 # Cannot disassemble from one dso to another
2933 bsym = query.value(1)
2934 boff = query.value(2)
2935 bsym_start = query.value(3)
2938 tot = bsym_start + boff + 1 - sym_start - off
2939 if tot <= 0 or tot > 16384:
2942 inst = self.glb.disassembler.Instruction()
2943 f = self.glb.FileFromNamesAndBuildId(short_name, long_name, build_id)
2946 mode = 0 if Is64Bit(f) else 1
2947 self.glb.disassembler.SetMode(inst, mode)
2950 buf = create_string_buffer(tot + 16)
2951 f.seek(sym_start + off)
2952 buf.value = f.read(buf_sz)
2953 buf_ptr = addressof(buf)
2956 cnt, text = self.glb.disassembler.DisassembleOne(inst, buf_ptr, buf_sz, ip)
2958 byte_str = tohex(ip).rjust(16)
2959 for k in xrange(cnt):
2960 byte_str += " %02x" % ord(buf[i])
2965 self.child_items.append(BranchLevelTwoItem(0, self.br_col, byte_str + " " + text, self))
2966 self.child_count += 1
2974 def childCount(self):
2975 if not self.query_done:
2977 if not self.child_count:
2979 return self.child_count
2981 def hasChildren(self):
2982 if not self.query_done:
2984 return self.child_count > 0
2986 def getData(self, column):
2987 return self.data[column]
2989 # Brance data model root item
2991 class BranchRootItem():
2994 self.child_count = 0
2995 self.child_items = []
2998 def getChildItem(self, row):
2999 return self.child_items[row]
3001 def getParentItem(self):
3007 def childCount(self):
3008 return self.child_count
3010 def hasChildren(self):
3011 return self.child_count > 0
3013 def getData(self, column):
3016 # Calculate instructions per cycle
3018 def CalcIPC(cyc_cnt, insn_cnt):
3019 if cyc_cnt and insn_cnt:
3020 ipc = Decimal(float(insn_cnt) / cyc_cnt)
3021 ipc = str(ipc.quantize(Decimal(".01"), rounding=ROUND_HALF_UP))
3026 # Branch data preparation
3028 def BranchDataPrepBr(query, data):
3029 data.append(tohex(query.value(8)).rjust(16) + " " + query.value(9) + offstr(query.value(10)) +
3030 " (" + dsoname(query.value(11)) + ")" + " -> " +
3031 tohex(query.value(12)) + " " + query.value(13) + offstr(query.value(14)) +
3032 " (" + dsoname(query.value(15)) + ")")
3034 def BranchDataPrepIPC(query, data):
3035 insn_cnt = query.value(16)
3036 cyc_cnt = query.value(17)
3037 ipc = CalcIPC(cyc_cnt, insn_cnt)
3038 data.append(insn_cnt)
3039 data.append(cyc_cnt)
3042 def BranchDataPrep(query):
3044 for i in xrange(0, 8):
3045 data.append(query.value(i))
3046 BranchDataPrepBr(query, data)
3049 def BranchDataPrepWA(query):
3051 data.append(query.value(0))
3052 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3053 data.append("{:>19}".format(query.value(1)))
3054 for i in xrange(2, 8):
3055 data.append(query.value(i))
3056 BranchDataPrepBr(query, data)
3059 def BranchDataWithIPCPrep(query):
3061 for i in xrange(0, 8):
3062 data.append(query.value(i))
3063 BranchDataPrepIPC(query, data)
3064 BranchDataPrepBr(query, data)
3067 def BranchDataWithIPCPrepWA(query):
3069 data.append(query.value(0))
3070 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3071 data.append("{:>19}".format(query.value(1)))
3072 for i in xrange(2, 8):
3073 data.append(query.value(i))
3074 BranchDataPrepIPC(query, data)
3075 BranchDataPrepBr(query, data)
3080 class BranchModel(TreeModel):
3082 progress = Signal(object)
3084 def __init__(self, glb, event_id, where_clause, parent=None):
3085 super(BranchModel, self).__init__(glb, None, parent)
3086 self.event_id = event_id
3089 self.have_ipc = IsSelectable(glb.db, "samples", columns = "insn_count, cyc_count")
3091 select_ipc = ", insn_count, cyc_count"
3092 prep_fn = BranchDataWithIPCPrep
3093 prep_wa_fn = BranchDataWithIPCPrepWA
3096 prep_fn = BranchDataPrep
3097 prep_wa_fn = BranchDataPrepWA
3098 sql = ("SELECT samples.id, time, cpu, comm, pid, tid, branch_types.name,"
3099 " CASE WHEN in_tx = '0' THEN 'No' ELSE 'Yes' END,"
3100 " ip, symbols.name, sym_offset, dsos.short_name,"
3101 " to_ip, to_symbols.name, to_sym_offset, to_dsos.short_name"
3104 " INNER JOIN comms ON comm_id = comms.id"
3105 " INNER JOIN threads ON thread_id = threads.id"
3106 " INNER JOIN branch_types ON branch_type = branch_types.id"
3107 " INNER JOIN symbols ON symbol_id = symbols.id"
3108 " INNER JOIN symbols to_symbols ON to_symbol_id = to_symbols.id"
3109 " INNER JOIN dsos ON samples.dso_id = dsos.id"
3110 " INNER JOIN dsos AS to_dsos ON samples.to_dso_id = to_dsos.id"
3111 " WHERE samples.id > $$last_id$$" + where_clause +
3112 " AND evsel_id = " + str(self.event_id) +
3113 " ORDER BY samples.id"
3114 " LIMIT " + str(glb_chunk_sz))
3115 if pyside_version_1 and sys.version_info[0] == 3:
3119 self.fetcher = SQLFetcher(glb, sql, prep, self.AddSample)
3120 self.fetcher.done.connect(self.Update)
3121 self.fetcher.Fetch(glb_chunk_sz)
3124 return BranchRootItem()
3126 def columnCount(self, parent=None):
3132 def columnHeader(self, column):
3134 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Insn Cnt", "Cyc Cnt", "IPC", "Branch")[column]
3136 return ("Time", "CPU", "Command", "PID", "TID", "Branch Type", "In Tx", "Branch")[column]
3138 def columnFont(self, column):
3143 if column != br_col:
3145 return QFont("Monospace")
3147 def DisplayData(self, item, index):
3149 self.FetchIfNeeded(item.row)
3150 return item.getData(index.column())
3152 def AddSample(self, data):
3153 child = BranchLevelOneItem(self.glb, self.populated, data, self.root)
3154 self.root.child_items.append(child)
3157 def Update(self, fetched):
3160 self.progress.emit(0)
3161 child_count = self.root.child_count
3162 count = self.populated - child_count
3164 parent = QModelIndex()
3165 self.beginInsertRows(parent, child_count, child_count + count - 1)
3166 self.insertRows(child_count, count, parent)
3167 self.root.child_count += count
3168 self.endInsertRows()
3169 self.progress.emit(self.root.child_count)
3171 def FetchMoreRecords(self, count):
3172 current = self.root.child_count
3174 self.fetcher.Fetch(count)
3176 self.progress.emit(0)
3179 def HasMoreRecords(self):
3186 def __init__(self, name = "", where_clause = "", limit = ""):
3188 self.where_clause = where_clause
3192 return str(self.where_clause + ";" + self.limit)
3196 class BranchWindow(QMdiSubWindow):
3198 def __init__(self, glb, event_id, report_vars, parent=None):
3199 super(BranchWindow, self).__init__(parent)
3201 model_name = "Branch Events " + str(event_id) + " " + report_vars.UniqueId()
3203 self.model = LookupCreateModel(model_name, lambda: BranchModel(glb, event_id, report_vars.where_clause))
3205 self.view = QTreeView()
3206 self.view.setUniformRowHeights(True)
3207 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
3208 self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
3209 self.view.setModel(self.model)
3211 self.ResizeColumnsToContents()
3213 self.context_menu = TreeContextMenu(self.view)
3215 self.find_bar = FindBar(self, self, True)
3217 self.finder = ChildDataItemFinder(self.model.root)
3219 self.fetch_bar = FetchMoreRecordsBar(self.model, self)
3221 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
3223 self.setWidget(self.vbox.Widget())
3225 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name + " Branch Events")
3227 def ResizeColumnToContents(self, column, n):
3228 # Using the view's resizeColumnToContents() here is extrememly slow
3229 # so implement a crude alternative
3230 mm = "MM" if column else "MMMM"
3231 font = self.view.font()
3232 metrics = QFontMetrics(font)
3234 for row in xrange(n):
3235 val = self.model.root.child_items[row].data[column]
3236 len = metrics.width(str(val) + mm)
3237 max = len if len > max else max
3238 val = self.model.columnHeader(column)
3239 len = metrics.width(str(val) + mm)
3240 max = len if len > max else max
3241 self.view.setColumnWidth(column, max)
3243 def ResizeColumnsToContents(self):
3244 n = min(self.model.root.child_count, 100)
3246 # No data yet, so connect a signal to notify when there is
3247 self.model.rowsInserted.connect(self.UpdateColumnWidths)
3249 columns = self.model.columnCount()
3250 for i in xrange(columns):
3251 self.ResizeColumnToContents(i, n)
3253 def UpdateColumnWidths(self, *x):
3254 # This only needs to be done once, so disconnect the signal now
3255 self.model.rowsInserted.disconnect(self.UpdateColumnWidths)
3256 self.ResizeColumnsToContents()
3258 def Find(self, value, direction, pattern, context):
3259 self.view.setFocus()
3260 self.find_bar.Busy()
3261 self.finder.Find(value, direction, pattern, context, self.FindDone)
3263 def FindDone(self, row):
3264 self.find_bar.Idle()
3266 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
3268 self.find_bar.NotFound()
3270 # Line edit data item
3272 class LineEditDataItem(object):
3274 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3277 self.placeholder_text = placeholder_text
3278 self.parent = parent
3281 self.value = default
3283 self.widget = QLineEdit(default)
3284 self.widget.editingFinished.connect(self.Validate)
3285 self.widget.textChanged.connect(self.Invalidate)
3288 self.validated = True
3290 if placeholder_text:
3291 self.widget.setPlaceholderText(placeholder_text)
3293 def TurnTextRed(self):
3295 palette = QPalette()
3296 palette.setColor(QPalette.Text,Qt.red)
3297 self.widget.setPalette(palette)
3300 def TurnTextNormal(self):
3302 palette = QPalette()
3303 self.widget.setPalette(palette)
3306 def InvalidValue(self, value):
3309 self.error = self.label + " invalid value '" + value + "'"
3310 self.parent.ShowMessage(self.error)
3312 def Invalidate(self):
3313 self.validated = False
3315 def DoValidate(self, input_string):
3316 self.value = input_string.strip()
3319 self.validated = True
3321 self.TurnTextNormal()
3322 self.parent.ClearMessage()
3323 input_string = self.widget.text()
3324 if not len(input_string.strip()):
3327 self.DoValidate(input_string)
3330 if not self.validated:
3333 self.parent.ShowMessage(self.error)
3337 def IsNumber(self, value):
3342 return str(x) == value
3344 # Non-negative integer ranges dialog data item
3346 class NonNegativeIntegerRangesDataItem(LineEditDataItem):
3348 def __init__(self, glb, label, placeholder_text, column_name, parent):
3349 super(NonNegativeIntegerRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3351 self.column_name = column_name
3353 def DoValidate(self, input_string):
3356 for value in [x.strip() for x in input_string.split(",")]:
3358 vrange = value.split("-")
3359 if len(vrange) != 2 or not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3360 return self.InvalidValue(value)
3361 ranges.append(vrange)
3363 if not self.IsNumber(value):
3364 return self.InvalidValue(value)
3365 singles.append(value)
3366 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3368 ranges.append(self.column_name + " IN (" + ",".join(singles) + ")")
3369 self.value = " OR ".join(ranges)
3371 # Positive integer dialog data item
3373 class PositiveIntegerDataItem(LineEditDataItem):
3375 def __init__(self, glb, label, placeholder_text, parent, id = "", default = ""):
3376 super(PositiveIntegerDataItem, self).__init__(glb, label, placeholder_text, parent, id, default)
3378 def DoValidate(self, input_string):
3379 if not self.IsNumber(input_string.strip()):
3380 return self.InvalidValue(input_string)
3381 value = int(input_string.strip())
3383 return self.InvalidValue(input_string)
3384 self.value = str(value)
3386 # Dialog data item converted and validated using a SQL table
3388 class SQLTableDataItem(LineEditDataItem):
3390 def __init__(self, glb, label, placeholder_text, table_name, match_column, column_name1, column_name2, parent):
3391 super(SQLTableDataItem, self).__init__(glb, label, placeholder_text, parent)
3393 self.table_name = table_name
3394 self.match_column = match_column
3395 self.column_name1 = column_name1
3396 self.column_name2 = column_name2
3398 def ValueToIds(self, value):
3400 query = QSqlQuery(self.glb.db)
3401 stmt = "SELECT id FROM " + self.table_name + " WHERE " + self.match_column + " = '" + value + "'"
3402 ret = query.exec_(stmt)
3405 ids.append(str(query.value(0)))
3408 def DoValidate(self, input_string):
3410 for value in [x.strip() for x in input_string.split(",")]:
3411 ids = self.ValueToIds(value)
3415 return self.InvalidValue(value)
3416 self.value = self.column_name1 + " IN (" + ",".join(all_ids) + ")"
3417 if self.column_name2:
3418 self.value = "( " + self.value + " OR " + self.column_name2 + " IN (" + ",".join(all_ids) + ") )"
3420 # Sample time ranges dialog data item converted and validated using 'samples' SQL table
3422 class SampleTimeRangesDataItem(LineEditDataItem):
3424 def __init__(self, glb, label, placeholder_text, column_name, parent):
3425 self.column_name = column_name
3429 self.last_time = 2 ** 64
3431 query = QSqlQuery(glb.db)
3432 QueryExec(query, "SELECT id, time FROM samples ORDER BY id DESC LIMIT 1")
3434 self.last_id = int(query.value(0))
3435 self.first_time = int(glb.HostStartTime())
3436 self.last_time = int(glb.HostFinishTime())
3437 if placeholder_text:
3438 placeholder_text += ", between " + str(self.first_time) + " and " + str(self.last_time)
3440 super(SampleTimeRangesDataItem, self).__init__(glb, label, placeholder_text, parent)
3442 def IdBetween(self, query, lower_id, higher_id, order):
3443 QueryExec(query, "SELECT id FROM samples WHERE id > " + str(lower_id) + " AND id < " + str(higher_id) + " ORDER BY id " + order + " LIMIT 1")
3445 return True, int(query.value(0))
3449 def BinarySearchTime(self, lower_id, higher_id, target_time, get_floor):
3450 query = QSqlQuery(self.glb.db)
3452 next_id = int((lower_id + higher_id) / 2)
3453 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3454 if not query.next():
3455 ok, dbid = self.IdBetween(query, lower_id, next_id, "DESC")
3457 ok, dbid = self.IdBetween(query, next_id, higher_id, "")
3459 return str(higher_id)
3461 QueryExec(query, "SELECT time FROM samples WHERE id = " + str(next_id))
3462 next_time = int(query.value(0))
3464 if target_time > next_time:
3468 if higher_id <= lower_id + 1:
3469 return str(higher_id)
3471 if target_time >= next_time:
3475 if higher_id <= lower_id + 1:
3476 return str(lower_id)
3478 def ConvertRelativeTime(self, val):
3483 elif suffix == "us":
3485 elif suffix == "ns":
3489 val = val[:-2].strip()
3490 if not self.IsNumber(val):
3492 val = int(val) * mult
3494 val += self.first_time
3496 val += self.last_time
3499 def ConvertTimeRange(self, vrange):
3501 vrange[0] = str(self.first_time)
3503 vrange[1] = str(self.last_time)
3504 vrange[0] = self.ConvertRelativeTime(vrange[0])
3505 vrange[1] = self.ConvertRelativeTime(vrange[1])
3506 if not self.IsNumber(vrange[0]) or not self.IsNumber(vrange[1]):
3508 beg_range = max(int(vrange[0]), self.first_time)
3509 end_range = min(int(vrange[1]), self.last_time)
3510 if beg_range > self.last_time or end_range < self.first_time:
3512 vrange[0] = self.BinarySearchTime(0, self.last_id, beg_range, True)
3513 vrange[1] = self.BinarySearchTime(1, self.last_id + 1, end_range, False)
3516 def AddTimeRange(self, value, ranges):
3517 n = value.count("-")
3521 if value.split("-")[1].strip() == "":
3527 pos = findnth(value, "-", n)
3528 vrange = [value[:pos].strip() ,value[pos+1:].strip()]
3529 if self.ConvertTimeRange(vrange):
3530 ranges.append(vrange)
3534 def DoValidate(self, input_string):
3536 for value in [x.strip() for x in input_string.split(",")]:
3537 if not self.AddTimeRange(value, ranges):
3538 return self.InvalidValue(value)
3539 ranges = [("(" + self.column_name + " >= " + r[0] + " AND " + self.column_name + " <= " + r[1] + ")") for r in ranges]
3540 self.value = " OR ".join(ranges)
3542 # Report Dialog Base
3544 class ReportDialogBase(QDialog):
3546 def __init__(self, glb, title, items, partial, parent=None):
3547 super(ReportDialogBase, self).__init__(parent)
3551 self.report_vars = ReportVars()
3553 self.setWindowTitle(title)
3554 self.setMinimumWidth(600)
3556 self.data_items = [x(glb, self) for x in items]
3558 self.partial = partial
3560 self.grid = QGridLayout()
3562 for row in xrange(len(self.data_items)):
3563 self.grid.addWidget(QLabel(self.data_items[row].label), row, 0)
3564 self.grid.addWidget(self.data_items[row].widget, row, 1)
3566 self.status = QLabel()
3568 self.ok_button = QPushButton("Ok", self)
3569 self.ok_button.setDefault(True)
3570 self.ok_button.released.connect(self.Ok)
3571 self.ok_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3573 self.cancel_button = QPushButton("Cancel", self)
3574 self.cancel_button.released.connect(self.reject)
3575 self.cancel_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
3577 self.hbox = QHBoxLayout()
3578 #self.hbox.addStretch()
3579 self.hbox.addWidget(self.status)
3580 self.hbox.addWidget(self.ok_button)
3581 self.hbox.addWidget(self.cancel_button)
3583 self.vbox = QVBoxLayout()
3584 self.vbox.addLayout(self.grid)
3585 self.vbox.addLayout(self.hbox)
3587 self.setLayout(self.vbox)
3590 vars = self.report_vars
3591 for d in self.data_items:
3592 if d.id == "REPORTNAME":
3595 self.ShowMessage("Report name is required")
3597 for d in self.data_items:
3600 for d in self.data_items[1:]:
3602 vars.limit = d.value
3604 if len(vars.where_clause):
3605 vars.where_clause += " AND "
3606 vars.where_clause += d.value
3607 if len(vars.where_clause):
3609 vars.where_clause = " AND ( " + vars.where_clause + " ) "
3611 vars.where_clause = " WHERE " + vars.where_clause + " "
3614 def ShowMessage(self, msg):
3615 self.status.setText("<font color=#FF0000>" + msg)
3617 def ClearMessage(self):
3618 self.status.setText("")
3620 # Selected branch report creation dialog
3622 class SelectedBranchDialog(ReportDialogBase):
3624 def __init__(self, glb, parent=None):
3625 title = "Selected Branches"
3626 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
3627 lambda g, p: SampleTimeRangesDataItem(g, "Time ranges:", "Enter time ranges", "samples.id", p),
3628 lambda g, p: NonNegativeIntegerRangesDataItem(g, "CPUs:", "Enter CPUs or ranges e.g. 0,5-6", "cpu", p),
3629 lambda g, p: SQLTableDataItem(g, "Commands:", "Only branches with these commands will be included", "comms", "comm", "comm_id", "", p),
3630 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only branches with these process IDs will be included", "threads", "pid", "thread_id", "", p),
3631 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only branches with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
3632 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only branches with these DSOs will be included", "dsos", "short_name", "samples.dso_id", "to_dso_id", p),
3633 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only branches with these symbols will be included", "symbols", "name", "symbol_id", "to_symbol_id", p),
3634 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p))
3635 super(SelectedBranchDialog, self).__init__(glb, title, items, True, parent)
3639 def GetEventList(db):
3641 query = QSqlQuery(db)
3642 QueryExec(query, "SELECT name FROM selected_events WHERE id > 0 ORDER BY id")
3644 events.append(query.value(0))
3647 # Is a table selectable
3649 def IsSelectable(db, table, sql = "", columns = "*"):
3650 query = QSqlQuery(db)
3652 QueryExec(query, "SELECT " + columns + " FROM " + table + " " + sql + " LIMIT 1")
3657 # SQL table data model item
3659 class SQLTableItem():
3661 def __init__(self, row, data):
3665 def getData(self, column):
3666 return self.data[column]
3668 # SQL table data model
3670 class SQLTableModel(TableModel):
3672 progress = Signal(object)
3674 def __init__(self, glb, sql, column_headers, parent=None):
3675 super(SQLTableModel, self).__init__(parent)
3679 self.column_headers = column_headers
3680 self.fetcher = SQLFetcher(glb, sql, lambda x, y=len(column_headers): self.SQLTableDataPrep(x, y), self.AddSample)
3681 self.fetcher.done.connect(self.Update)
3682 self.fetcher.Fetch(glb_chunk_sz)
3684 def DisplayData(self, item, index):
3685 self.FetchIfNeeded(item.row)
3686 return item.getData(index.column())
3688 def AddSample(self, data):
3689 child = SQLTableItem(self.populated, data)
3690 self.child_items.append(child)
3693 def Update(self, fetched):
3696 self.progress.emit(0)
3697 child_count = self.child_count
3698 count = self.populated - child_count
3700 parent = QModelIndex()
3701 self.beginInsertRows(parent, child_count, child_count + count - 1)
3702 self.insertRows(child_count, count, parent)
3703 self.child_count += count
3704 self.endInsertRows()
3705 self.progress.emit(self.child_count)
3707 def FetchMoreRecords(self, count):
3708 current = self.child_count
3710 self.fetcher.Fetch(count)
3712 self.progress.emit(0)
3715 def HasMoreRecords(self):
3718 def columnCount(self, parent=None):
3719 return len(self.column_headers)
3721 def columnHeader(self, column):
3722 return self.column_headers[column]
3724 def SQLTableDataPrep(self, query, count):
3726 for i in xrange(count):
3727 data.append(query.value(i))
3730 # SQL automatic table data model
3732 class SQLAutoTableModel(SQLTableModel):
3734 def __init__(self, glb, table_name, parent=None):
3735 sql = "SELECT * FROM " + table_name + " WHERE id > $$last_id$$ ORDER BY id LIMIT " + str(glb_chunk_sz)
3736 if table_name == "comm_threads_view":
3737 # For now, comm_threads_view has no id column
3738 sql = "SELECT * FROM " + table_name + " WHERE comm_id > $$last_id$$ ORDER BY comm_id LIMIT " + str(glb_chunk_sz)
3740 query = QSqlQuery(glb.db)
3741 if glb.dbref.is_sqlite3:
3742 QueryExec(query, "PRAGMA table_info(" + table_name + ")")
3744 column_headers.append(query.value(1))
3745 if table_name == "sqlite_master":
3746 sql = "SELECT * FROM " + table_name
3748 if table_name[:19] == "information_schema.":
3749 sql = "SELECT * FROM " + table_name
3750 select_table_name = table_name[19:]
3751 schema = "information_schema"
3753 select_table_name = table_name
3755 QueryExec(query, "SELECT column_name FROM information_schema.columns WHERE table_schema = '" + schema + "' and table_name = '" + select_table_name + "'")
3757 column_headers.append(query.value(0))
3758 if pyside_version_1 and sys.version_info[0] == 3:
3759 if table_name == "samples_view":
3760 self.SQLTableDataPrep = self.samples_view_DataPrep
3761 if table_name == "samples":
3762 self.SQLTableDataPrep = self.samples_DataPrep
3763 super(SQLAutoTableModel, self).__init__(glb, sql, column_headers, parent)
3765 def samples_view_DataPrep(self, query, count):
3767 data.append(query.value(0))
3768 # Workaround pyside failing to handle large integers (i.e. time) in python3 by converting to a string
3769 data.append("{:>19}".format(query.value(1)))
3770 for i in xrange(2, count):
3771 data.append(query.value(i))
3774 def samples_DataPrep(self, query, count):
3777 data.append(query.value(i))
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(9)))
3780 for i in xrange(10, count):
3781 data.append(query.value(i))
3784 # Base class for custom ResizeColumnsToContents
3786 class ResizeColumnsToContentsBase(QObject):
3788 def __init__(self, parent=None):
3789 super(ResizeColumnsToContentsBase, self).__init__(parent)
3791 def ResizeColumnToContents(self, column, n):
3792 # Using the view's resizeColumnToContents() here is extrememly slow
3793 # so implement a crude alternative
3794 font = self.view.font()
3795 metrics = QFontMetrics(font)
3797 for row in xrange(n):
3798 val = self.data_model.child_items[row].data[column]
3799 len = metrics.width(str(val) + "MM")
3800 max = len if len > max else max
3801 val = self.data_model.columnHeader(column)
3802 len = metrics.width(str(val) + "MM")
3803 max = len if len > max else max
3804 self.view.setColumnWidth(column, max)
3806 def ResizeColumnsToContents(self):
3807 n = min(self.data_model.child_count, 100)
3809 # No data yet, so connect a signal to notify when there is
3810 self.data_model.rowsInserted.connect(self.UpdateColumnWidths)
3812 columns = self.data_model.columnCount()
3813 for i in xrange(columns):
3814 self.ResizeColumnToContents(i, n)
3816 def UpdateColumnWidths(self, *x):
3817 # This only needs to be done once, so disconnect the signal now
3818 self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
3819 self.ResizeColumnsToContents()
3821 # Convert value to CSV
3825 val = val.replace('"', '""')
3826 if "," in val or '"' in val:
3827 val = '"' + val + '"'
3830 # Key to sort table model indexes by row / column, assuming fewer than 1000 columns
3834 def RowColumnKey(a):
3835 return a.row() * glb_max_cols + a.column()
3837 # Copy selected table cells to clipboard
3839 def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
3840 indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
3841 idx_cnt = len(indexes)
3846 min_row = indexes[0].row()
3847 max_row = indexes[0].row()
3848 min_col = indexes[0].column()
3849 max_col = indexes[0].column()
3851 min_row = min(min_row, i.row())
3852 max_row = max(max_row, i.row())
3853 min_col = min(min_col, i.column())
3854 max_col = max(max_col, i.column())
3855 if max_col > glb_max_cols:
3856 raise RuntimeError("glb_max_cols is too low")
3857 max_width = [0] * (1 + max_col - min_col)
3859 c = i.column() - min_col
3860 max_width[c] = max(max_width[c], len(str(i.data())))
3865 model = indexes[0].model()
3866 for col in range(min_col, max_col + 1):
3867 val = model.headerData(col, Qt.Horizontal)
3869 text += sep + ToCSValue(val)
3873 max_width[c] = max(max_width[c], len(val))
3874 width = max_width[c]
3875 align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
3876 if align & Qt.AlignRight:
3877 val = val.rjust(width)
3878 text += pad + sep + val
3879 pad = " " * (width - len(val))
3886 if i.row() > last_row:
3892 text += sep + ToCSValue(str(i.data()))
3895 width = max_width[i.column() - min_col]
3896 if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
3897 val = str(i.data()).rjust(width)
3900 text += pad + sep + val
3901 pad = " " * (width - len(val))
3903 QApplication.clipboard().setText(text)
3905 def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
3906 indexes = view.selectedIndexes()
3907 if not len(indexes):
3910 selection = view.selectionModel()
3914 above = view.indexAbove(i)
3915 if not selection.isSelected(above):
3920 raise RuntimeError("CopyTreeCellsToClipboard internal error")
3922 model = first.model()
3924 col_cnt = model.columnCount(first)
3925 max_width = [0] * col_cnt
3928 indent_str = " " * indent_sz
3930 expanded_mark_sz = 2
3931 if sys.version_info[0] == 3:
3932 expanded_mark = "\u25BC "
3933 not_expanded_mark = "\u25B6 "
3935 expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
3936 not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
3944 for c in range(col_cnt):
3945 i = pos.sibling(row, c)
3947 n = len(str(i.data()))
3949 n = len(str(i.data()).strip())
3950 n += (i.internalPointer().level - 1) * indent_sz
3951 n += expanded_mark_sz
3952 max_width[c] = max(max_width[c], n)
3953 pos = view.indexBelow(pos)
3954 if not selection.isSelected(pos):
3961 for c in range(col_cnt):
3962 val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
3964 text += sep + ToCSValue(val)
3967 max_width[c] = max(max_width[c], len(val))
3968 width = max_width[c]
3969 align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
3970 if align & Qt.AlignRight:
3971 val = val.rjust(width)
3972 text += pad + sep + val
3973 pad = " " * (width - len(val))
3982 for c in range(col_cnt):
3983 i = pos.sibling(row, c)
3986 if model.hasChildren(i):
3987 if view.isExpanded(i):
3988 mark = expanded_mark
3990 mark = not_expanded_mark
3993 val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
3995 text += sep + ToCSValue(val)
3998 width = max_width[c]
3999 if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
4000 val = val.rjust(width)
4001 text += pad + sep + val
4002 pad = " " * (width - len(val))
4004 pos = view.indexBelow(pos)
4005 if not selection.isSelected(pos):
4007 text = text.rstrip() + "\n"
4011 QApplication.clipboard().setText(text)
4013 def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
4014 view.CopyCellsToClipboard(view, as_csv, with_hdr)
4016 def CopyCellsToClipboardHdr(view):
4017 CopyCellsToClipboard(view, False, True)
4019 def CopyCellsToClipboardCSV(view):
4020 CopyCellsToClipboard(view, True, True)
4024 class ContextMenu(object):
4026 def __init__(self, view):
4028 self.view.setContextMenuPolicy(Qt.CustomContextMenu)
4029 self.view.customContextMenuRequested.connect(self.ShowContextMenu)
4031 def ShowContextMenu(self, pos):
4032 menu = QMenu(self.view)
4033 self.AddActions(menu)
4034 menu.exec_(self.view.mapToGlobal(pos))
4036 def AddCopy(self, menu):
4037 menu.addAction(CreateAction("&Copy selection", "Copy to clipboard", lambda: CopyCellsToClipboardHdr(self.view), self.view))
4038 menu.addAction(CreateAction("Copy selection as CS&V", "Copy to clipboard as CSV", lambda: CopyCellsToClipboardCSV(self.view), self.view))
4040 def AddActions(self, menu):
4043 class TreeContextMenu(ContextMenu):
4045 def __init__(self, view):
4046 super(TreeContextMenu, self).__init__(view)
4048 def AddActions(self, menu):
4049 i = self.view.currentIndex()
4050 text = str(i.data()).strip()
4052 menu.addAction(CreateAction('Copy "' + text + '"', "Copy to clipboard", lambda: QApplication.clipboard().setText(text), self.view))
4057 class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4059 def __init__(self, glb, table_name, parent=None):
4060 super(TableWindow, self).__init__(parent)
4062 self.data_model = LookupCreateModel(table_name + " Table", lambda: SQLAutoTableModel(glb, table_name))
4064 self.model = QSortFilterProxyModel()
4065 self.model.setSourceModel(self.data_model)
4067 self.view = QTableView()
4068 self.view.setModel(self.model)
4069 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4070 self.view.verticalHeader().setVisible(False)
4071 self.view.sortByColumn(-1, Qt.AscendingOrder)
4072 self.view.setSortingEnabled(True)
4073 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4074 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4076 self.ResizeColumnsToContents()
4078 self.context_menu = ContextMenu(self.view)
4080 self.find_bar = FindBar(self, self, True)
4082 self.finder = ChildDataItemFinder(self.data_model)
4084 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4086 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4088 self.setWidget(self.vbox.Widget())
4090 AddSubWindow(glb.mainwindow.mdi_area, self, table_name + " Table")
4092 def Find(self, value, direction, pattern, context):
4093 self.view.setFocus()
4094 self.find_bar.Busy()
4095 self.finder.Find(value, direction, pattern, context, self.FindDone)
4097 def FindDone(self, row):
4098 self.find_bar.Idle()
4100 self.view.setCurrentIndex(self.model.mapFromSource(self.data_model.index(row, 0, QModelIndex())))
4102 self.find_bar.NotFound()
4106 def GetTableList(glb):
4108 query = QSqlQuery(glb.db)
4109 if glb.dbref.is_sqlite3:
4110 QueryExec(query, "SELECT name FROM sqlite_master WHERE type IN ( 'table' , 'view' ) ORDER BY name")
4112 QueryExec(query, "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type IN ( 'BASE TABLE' , 'VIEW' ) ORDER BY table_name")
4114 tables.append(query.value(0))
4115 if glb.dbref.is_sqlite3:
4116 tables.append("sqlite_master")
4118 tables.append("information_schema.tables")
4119 tables.append("information_schema.views")
4120 tables.append("information_schema.columns")
4123 # Top Calls data model
4125 class TopCallsModel(SQLTableModel):
4127 def __init__(self, glb, report_vars, parent=None):
4129 if not glb.dbref.is_sqlite3:
4132 if len(report_vars.limit):
4133 limit = " LIMIT " + report_vars.limit
4134 sql = ("SELECT comm, pid, tid, name,"
4136 " WHEN (short_name = '[kernel.kallsyms]') THEN '[kernel]'" + text +
4139 " call_time, return_time, (return_time - call_time) AS elapsed_time, branch_count, "
4141 " WHEN (calls.flags = 1) THEN 'no call'" + text +
4142 " WHEN (calls.flags = 2) THEN 'no return'" + text +
4143 " WHEN (calls.flags = 3) THEN 'no call/return'" + text +
4147 " INNER JOIN call_paths ON calls.call_path_id = call_paths.id"
4148 " INNER JOIN symbols ON call_paths.symbol_id = symbols.id"
4149 " INNER JOIN dsos ON symbols.dso_id = dsos.id"
4150 " INNER JOIN comms ON calls.comm_id = comms.id"
4151 " INNER JOIN threads ON calls.thread_id = threads.id" +
4152 report_vars.where_clause +
4153 " ORDER BY elapsed_time DESC" +
4156 column_headers = ("Command", "PID", "TID", "Symbol", "Object", "Call Time", "Return Time", "Elapsed Time (ns)", "Branch Count", "Flags")
4157 self.alignment = (Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignLeft, Qt.AlignRight, Qt.AlignRight, Qt.AlignLeft)
4158 super(TopCallsModel, self).__init__(glb, sql, column_headers, parent)
4160 def columnAlignment(self, column):
4161 return self.alignment[column]
4163 # Top Calls report creation dialog
4165 class TopCallsDialog(ReportDialogBase):
4167 def __init__(self, glb, parent=None):
4168 title = "Top Calls by Elapsed Time"
4169 items = (lambda g, p: LineEditDataItem(g, "Report name:", "Enter a name to appear in the window title bar", p, "REPORTNAME"),
4170 lambda g, p: SQLTableDataItem(g, "Commands:", "Only calls with these commands will be included", "comms", "comm", "comm_id", "", p),
4171 lambda g, p: SQLTableDataItem(g, "PIDs:", "Only calls with these process IDs will be included", "threads", "pid", "thread_id", "", p),
4172 lambda g, p: SQLTableDataItem(g, "TIDs:", "Only calls with these thread IDs will be included", "threads", "tid", "thread_id", "", p),
4173 lambda g, p: SQLTableDataItem(g, "DSOs:", "Only calls with these DSOs will be included", "dsos", "short_name", "dso_id", "", p),
4174 lambda g, p: SQLTableDataItem(g, "Symbols:", "Only calls with these symbols will be included", "symbols", "name", "symbol_id", "", p),
4175 lambda g, p: LineEditDataItem(g, "Raw SQL clause: ", "Enter a raw SQL WHERE clause", p),
4176 lambda g, p: PositiveIntegerDataItem(g, "Record limit:", "Limit selection to this number of records", p, "LIMIT", "100"))
4177 super(TopCallsDialog, self).__init__(glb, title, items, False, parent)
4181 class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
4183 def __init__(self, glb, report_vars, parent=None):
4184 super(TopCallsWindow, self).__init__(parent)
4186 self.data_model = LookupCreateModel("Top Calls " + report_vars.UniqueId(), lambda: TopCallsModel(glb, report_vars))
4187 self.model = self.data_model
4189 self.view = QTableView()
4190 self.view.setModel(self.model)
4191 self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
4192 self.view.verticalHeader().setVisible(False)
4193 self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
4194 self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
4196 self.context_menu = ContextMenu(self.view)
4198 self.ResizeColumnsToContents()
4200 self.find_bar = FindBar(self, self, True)
4202 self.finder = ChildDataItemFinder(self.model)
4204 self.fetch_bar = FetchMoreRecordsBar(self.data_model, self)
4206 self.vbox = VBox(self.view, self.find_bar.Widget(), self.fetch_bar.Widget())
4208 self.setWidget(self.vbox.Widget())
4210 AddSubWindow(glb.mainwindow.mdi_area, self, report_vars.name)
4212 def Find(self, value, direction, pattern, context):
4213 self.view.setFocus()
4214 self.find_bar.Busy()
4215 self.finder.Find(value, direction, pattern, context, self.FindDone)
4217 def FindDone(self, row):
4218 self.find_bar.Idle()
4220 self.view.setCurrentIndex(self.model.index(row, 0, QModelIndex()))
4222 self.find_bar.NotFound()
4226 def CreateAction(label, tip, callback, parent=None, shortcut=None):
4227 action = QAction(label, parent)
4228 if shortcut != None:
4229 action.setShortcuts(shortcut)
4230 action.setStatusTip(tip)
4231 action.triggered.connect(callback)
4234 # Typical application actions
4236 def CreateExitAction(app, parent=None):
4237 return CreateAction("&Quit", "Exit the application", app.closeAllWindows, parent, QKeySequence.Quit)
4239 # Typical MDI actions
4241 def CreateCloseActiveWindowAction(mdi_area):
4242 return CreateAction("Cl&ose", "Close the active window", mdi_area.closeActiveSubWindow, mdi_area)
4244 def CreateCloseAllWindowsAction(mdi_area):
4245 return CreateAction("Close &All", "Close all the windows", mdi_area.closeAllSubWindows, mdi_area)
4247 def CreateTileWindowsAction(mdi_area):
4248 return CreateAction("&Tile", "Tile the windows", mdi_area.tileSubWindows, mdi_area)
4250 def CreateCascadeWindowsAction(mdi_area):
4251 return CreateAction("&Cascade", "Cascade the windows", mdi_area.cascadeSubWindows, mdi_area)
4253 def CreateNextWindowAction(mdi_area):
4254 return CreateAction("Ne&xt", "Move the focus to the next window", mdi_area.activateNextSubWindow, mdi_area, QKeySequence.NextChild)
4256 def CreatePreviousWindowAction(mdi_area):
4257 return CreateAction("Pre&vious", "Move the focus to the previous window", mdi_area.activatePreviousSubWindow, mdi_area, QKeySequence.PreviousChild)
4259 # Typical MDI window menu
4263 def __init__(self, mdi_area, menu):
4264 self.mdi_area = mdi_area
4265 self.window_menu = menu.addMenu("&Windows")
4266 self.close_active_window = CreateCloseActiveWindowAction(mdi_area)
4267 self.close_all_windows = CreateCloseAllWindowsAction(mdi_area)
4268 self.tile_windows = CreateTileWindowsAction(mdi_area)
4269 self.cascade_windows = CreateCascadeWindowsAction(mdi_area)
4270 self.next_window = CreateNextWindowAction(mdi_area)
4271 self.previous_window = CreatePreviousWindowAction(mdi_area)
4272 self.window_menu.aboutToShow.connect(self.Update)
4275 self.window_menu.clear()
4276 sub_window_count = len(self.mdi_area.subWindowList())
4277 have_sub_windows = sub_window_count != 0
4278 self.close_active_window.setEnabled(have_sub_windows)
4279 self.close_all_windows.setEnabled(have_sub_windows)
4280 self.tile_windows.setEnabled(have_sub_windows)
4281 self.cascade_windows.setEnabled(have_sub_windows)
4282 self.next_window.setEnabled(have_sub_windows)
4283 self.previous_window.setEnabled(have_sub_windows)
4284 self.window_menu.addAction(self.close_active_window)
4285 self.window_menu.addAction(self.close_all_windows)
4286 self.window_menu.addSeparator()
4287 self.window_menu.addAction(self.tile_windows)
4288 self.window_menu.addAction(self.cascade_windows)
4289 self.window_menu.addSeparator()
4290 self.window_menu.addAction(self.next_window)
4291 self.window_menu.addAction(self.previous_window)
4292 if sub_window_count == 0:
4294 self.window_menu.addSeparator()
4296 for sub_window in self.mdi_area.subWindowList():
4297 label = str(nr) + " " + sub_window.name
4300 action = self.window_menu.addAction(label)
4301 action.setCheckable(True)
4302 action.setChecked(sub_window == self.mdi_area.activeSubWindow())
4303 action.triggered.connect(lambda a=None,x=nr: self.setActiveSubWindow(x))
4304 self.window_menu.addAction(action)
4307 def setActiveSubWindow(self, nr):
4308 self.mdi_area.setActiveSubWindow(self.mdi_area.subWindowList()[nr - 1])
4323 <p class=c1><a href=#reports>1. Reports</a></p>
4324 <p class=c2><a href=#callgraph>1.1 Context-Sensitive Call Graph</a></p>
4325 <p class=c2><a href=#calltree>1.2 Call Tree</a></p>
4326 <p class=c2><a href=#allbranches>1.3 All branches</a></p>
4327 <p class=c2><a href=#selectedbranches>1.4 Selected branches</a></p>
4328 <p class=c2><a href=#topcallsbyelapsedtime>1.5 Top calls by elapsed time</a></p>
4329 <p class=c1><a href=#charts>2. Charts</a></p>
4330 <p class=c2><a href=#timechartbycpu>2.1 Time chart by CPU</a></p>
4331 <p class=c1><a href=#tables>3. Tables</a></p>
4332 <h1 id=reports>1. Reports</h1>
4333 <h2 id=callgraph>1.1 Context-Sensitive Call Graph</h2>
4334 The result is a GUI window with a tree representing a context-sensitive
4335 call-graph. Expanding a couple of levels of the tree and adjusting column
4336 widths to suit will display something like:
4338 Call Graph: pt_example
4339 Call Path Object Count Time(ns) Time(%) Branch Count Branch Count(%)
4342 v- _start ld-2.19.so 1 10074071 100.0 211135 100.0
4343 |- unknown unknown 1 13198 0.1 1 0.0
4344 >- _dl_start ld-2.19.so 1 1400980 13.9 19637 9.3
4345 >- _d_linit_internal ld-2.19.so 1 448152 4.4 11094 5.3
4346 v-__libc_start_main@plt ls 1 8211741 81.5 180397 85.4
4347 >- _dl_fixup ld-2.19.so 1 7607 0.1 108 0.1
4348 >- __cxa_atexit libc-2.19.so 1 11737 0.1 10 0.0
4349 >- __libc_csu_init ls 1 10354 0.1 10 0.0
4350 |- _setjmp libc-2.19.so 1 0 0.0 4 0.0
4351 v- main ls 1 8182043 99.6 180254 99.9
4353 <h3>Points to note:</h3>
4355 <li>The top level is a command name (comm)</li>
4356 <li>The next level is a thread (pid:tid)</li>
4357 <li>Subsequent levels are functions</li>
4358 <li>'Count' is the number of calls</li>
4359 <li>'Time' is the elapsed time until the function returns</li>
4360 <li>Percentages are relative to the level above</li>
4361 <li>'Branch Count' is the total number of branches for that function and all functions that it calls
4364 Ctrl-F displays a Find bar which finds function names by either an exact match or a pattern match.
4365 The pattern matching symbols are ? for any character and * for zero or more characters.
4366 <h2 id=calltree>1.2 Call Tree</h2>
4367 The Call Tree report is very similar to the Context-Sensitive Call Graph, but the data is not aggregated.
4368 Also the 'Count' column, which would be always 1, is replaced by the 'Call Time'.
4369 <h2 id=allbranches>1.3 All branches</h2>
4370 The All branches report displays all branches in chronological order.
4371 Not all data is fetched immediately. More records can be fetched using the Fetch bar provided.
4372 <h3>Disassembly</h3>
4373 Open a branch to display disassembly. This only works if:
4375 <li>The disassembler is available. Currently, only Intel XED is supported - see <a href=#xed>Intel XED Setup</a></li>
4376 <li>The object code is available. Currently, only the perf build ID cache is searched for object code.
4377 The default directory ~/.debug can be overridden by setting environment variable PERF_BUILDID_DIR.
4378 One exception is kcore where the DSO long name is used (refer dsos_view on the Tables menu),
4379 or alternatively, set environment variable PERF_KCORE to the kcore file name.</li>
4381 <h4 id=xed>Intel XED Setup</h4>
4382 To use Intel XED, libxed.so must be present. To build and install libxed.so:
4384 git clone https://github.com/intelxed/mbuild.git mbuild
4385 git clone https://github.com/intelxed/xed
4388 sudo ./mfile.py --prefix=/usr/local install
4391 <h3>Instructions per Cycle (IPC)</h3>
4392 If available, IPC information is displayed in columns 'insn_cnt', 'cyc_cnt' and 'IPC'.
4393 <p><b>Intel PT note:</b> The information applies to the blocks of code ending with, and including, that branch.
4394 Due to the granularity of timing information, the number of cycles for some code blocks will not be known.
4395 In that case, 'insn_cnt', 'cyc_cnt' and 'IPC' are zero, but when 'IPC' is displayed it covers the period
4396 since the previous displayed 'IPC'.
4398 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4399 Refer to Python documentation for the regular expression syntax.
4400 All columns are searched, but only currently fetched rows are searched.
4401 <h2 id=selectedbranches>1.4 Selected branches</h2>
4402 This is the same as the <a href=#allbranches>All branches</a> report but with the data reduced
4403 by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4404 <h3>1.4.1 Time ranges</h3>
4405 The time ranges hint text shows the total time range. Relative time ranges can also be entered in
4406 ms, us or ns. Also, negative values are relative to the end of trace. Examples:
4408 81073085947329-81073085958238 From 81073085947329 to 81073085958238
4409 100us-200us From 100us to 200us
4410 10ms- From 10ms to the end
4411 -100ns The first 100ns
4412 -10ms- The last 10ms
4414 N.B. Due to the granularity of timestamps, there could be no branches in any given time range.
4415 <h2 id=topcallsbyelapsedtime>1.5 Top calls by elapsed time</h2>
4416 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.
4417 The data is reduced by various selection criteria. A dialog box displays available criteria which are AND'ed together.
4418 If not all data is fetched, a Fetch bar is provided. Ctrl-F displays a Find bar.
4419 <h1 id=charts>2. Charts</h1>
4420 <h2 id=timechartbycpu>2.1 Time chart by CPU</h2>
4421 This chart displays context switch information when that data is available. Refer to context_switches_view on the Tables menu.
4424 <li>Mouse over to highight the task and show the time</li>
4425 <li>Drag the mouse to select a region and zoom by pushing the Zoom button</li>
4426 <li>Go back and forward by pressing the arrow buttons</li>
4427 <li>If call information is available, right-click to show a call tree opened to that task and time.
4428 Note, the call tree may take some time to appear, and there may not be call information for the task or time selected.
4432 The graph can be misleading in the following respects:
4434 <li>The graph shows the first task on each CPU as running from the beginning of the time range.
4435 Because tracing might start on different CPUs at different times, that is not necessarily the case.
4436 Refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4437 <li>Similarly, the last task on each CPU can be showing running longer than it really was.
4438 Again, refer to context_switches_view on the Tables menu to understand what data the graph is based upon.</li>
4439 <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>
4441 <h1 id=tables>3. Tables</h1>
4442 The Tables menu shows all tables and views in the database. Most tables have an associated view
4443 which displays the information in a more friendly way. Not all data for large tables is fetched
4444 immediately. More records can be fetched using the Fetch bar provided. Columns can be sorted,
4445 but that can be slow for large tables.
4446 <p>There are also tables of database meta-information.
4447 For SQLite3 databases, the sqlite_master table is included.
4448 For PostgreSQL databases, information_schema.tables/views/columns are included.
4450 Ctrl-F displays a Find bar which finds substrings by either an exact match or a regular expression match.
4451 Refer to Python documentation for the regular expression syntax.
4452 All columns are searched, but only currently fetched rows are searched.
4453 <p>N.B. Results are found in id order, so if the table is re-ordered, find-next and find-previous
4454 will go to the next/previous result in id order, instead of display order.
4459 class HelpWindow(QMdiSubWindow):
4461 def __init__(self, glb, parent=None):
4462 super(HelpWindow, self).__init__(parent)
4464 self.text = QTextBrowser()
4465 self.text.setHtml(glb_help_text)
4466 self.text.setReadOnly(True)
4467 self.text.setOpenExternalLinks(True)
4469 self.setWidget(self.text)
4471 AddSubWindow(glb.mainwindow.mdi_area, self, "Exported SQL Viewer Help")
4473 # Main window that only displays the help text
4475 class HelpOnlyWindow(QMainWindow):
4477 def __init__(self, parent=None):
4478 super(HelpOnlyWindow, self).__init__(parent)
4480 self.setMinimumSize(200, 100)
4481 self.resize(800, 600)
4482 self.setWindowTitle("Exported SQL Viewer Help")
4483 self.setWindowIcon(self.style().standardIcon(QStyle.SP_MessageBoxInformation))
4485 self.text = QTextBrowser()
4486 self.text.setHtml(glb_help_text)
4487 self.text.setReadOnly(True)
4488 self.text.setOpenExternalLinks(True)
4490 self.setCentralWidget(self.text)
4492 # PostqreSQL server version
4494 def PostqreSQLServerVersion(db):
4495 query = QSqlQuery(db)
4496 QueryExec(query, "SELECT VERSION()")
4498 v_str = query.value(0)
4499 v_list = v_str.strip().split(" ")
4500 if v_list[0] == "PostgreSQL" and v_list[2] == "on":
4507 def SQLiteVersion(db):
4508 query = QSqlQuery(db)
4509 QueryExec(query, "SELECT sqlite_version()")
4511 return query.value(0)
4516 class AboutDialog(QDialog):
4518 def __init__(self, glb, parent=None):
4519 super(AboutDialog, self).__init__(parent)
4521 self.setWindowTitle("About Exported SQL Viewer")
4522 self.setMinimumWidth(300)
4524 pyside_version = "1" if pyside_version_1 else "2"
4527 text += "Python version: " + sys.version.split(" ")[0] + "\n"
4528 text += "PySide version: " + pyside_version + "\n"
4529 text += "Qt version: " + qVersion() + "\n"
4530 if glb.dbref.is_sqlite3:
4531 text += "SQLite version: " + SQLiteVersion(glb.db) + "\n"
4533 text += "PostqreSQL version: " + PostqreSQLServerVersion(glb.db) + "\n"
4536 self.text = QTextBrowser()
4537 self.text.setHtml(text)
4538 self.text.setReadOnly(True)
4539 self.text.setOpenExternalLinks(True)
4541 self.vbox = QVBoxLayout()
4542 self.vbox.addWidget(self.text)
4544 self.setLayout(self.vbox)
4548 def ResizeFont(widget, diff):
4549 font = widget.font()
4550 sz = font.pointSize()
4551 font.setPointSize(sz + diff)
4552 widget.setFont(font)
4554 def ShrinkFont(widget):
4555 ResizeFont(widget, -1)
4557 def EnlargeFont(widget):
4558 ResizeFont(widget, 1)
4560 # Unique name for sub-windows
4562 def NumberedWindowName(name, nr):
4564 name += " <" + str(nr) + ">"
4567 def UniqueSubWindowName(mdi_area, name):
4570 unique_name = NumberedWindowName(name, nr)
4572 for sub_window in mdi_area.subWindowList():
4573 if sub_window.name == unique_name:
4582 def AddSubWindow(mdi_area, sub_window, name):
4583 unique_name = UniqueSubWindowName(mdi_area, name)
4584 sub_window.setMinimumSize(200, 100)
4585 sub_window.resize(800, 600)
4586 sub_window.setWindowTitle(unique_name)
4587 sub_window.setAttribute(Qt.WA_DeleteOnClose)
4588 sub_window.setWindowIcon(sub_window.style().standardIcon(QStyle.SP_FileIcon))
4589 sub_window.name = unique_name
4590 mdi_area.addSubWindow(sub_window)
4595 class MainWindow(QMainWindow):
4597 def __init__(self, glb, parent=None):
4598 super(MainWindow, self).__init__(parent)
4602 self.setWindowTitle("Exported SQL Viewer: " + glb.dbname)
4603 self.setWindowIcon(self.style().standardIcon(QStyle.SP_ComputerIcon))
4604 self.setMinimumSize(200, 100)
4606 self.mdi_area = QMdiArea()
4607 self.mdi_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4608 self.mdi_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
4610 self.setCentralWidget(self.mdi_area)
4612 menu = self.menuBar()
4614 file_menu = menu.addMenu("&File")
4615 file_menu.addAction(CreateExitAction(glb.app, self))
4617 edit_menu = menu.addMenu("&Edit")
4618 edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
4619 edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
4620 edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
4621 edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
4622 edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
4623 edit_menu.addAction(CreateAction("&Enlarge Font", "Make text bigger", self.EnlargeFont, self, [QKeySequence("Ctrl++")]))
4625 reports_menu = menu.addMenu("&Reports")
4626 if IsSelectable(glb.db, "calls"):
4627 reports_menu.addAction(CreateAction("Context-Sensitive Call &Graph", "Create a new window containing a context-sensitive call graph", self.NewCallGraph, self))
4629 if IsSelectable(glb.db, "calls", "WHERE parent_id >= 0"):
4630 reports_menu.addAction(CreateAction("Call &Tree", "Create a new window containing a call tree", self.NewCallTree, self))
4632 self.EventMenu(GetEventList(glb.db), reports_menu)
4634 if IsSelectable(glb.db, "calls"):
4635 reports_menu.addAction(CreateAction("&Top calls by elapsed time", "Create a new window displaying top calls by elapsed time", self.NewTopCalls, self))
4637 if IsSelectable(glb.db, "context_switches"):
4638 charts_menu = menu.addMenu("&Charts")
4639 charts_menu.addAction(CreateAction("&Time chart by CPU", "Create a new window displaying time charts by CPU", self.TimeChartByCPU, self))
4641 self.TableMenu(GetTableList(glb), menu)
4643 self.window_menu = WindowMenu(self.mdi_area, menu)
4645 help_menu = menu.addMenu("&Help")
4646 help_menu.addAction(CreateAction("&Exported SQL Viewer Help", "Helpful information", self.Help, self, QKeySequence.HelpContents))
4647 help_menu.addAction(CreateAction("&About Exported SQL Viewer", "About this application", self.About, self))
4650 win = self.mdi_area.activeSubWindow()
4657 def CopyToClipboard(self):
4658 self.Try(CopyCellsToClipboardHdr)
4660 def CopyToClipboardCSV(self):
4661 self.Try(CopyCellsToClipboardCSV)
4664 win = self.mdi_area.activeSubWindow()
4667 win.find_bar.Activate()
4671 def FetchMoreRecords(self):
4672 win = self.mdi_area.activeSubWindow()
4675 win.fetch_bar.Activate()
4679 def ShrinkFont(self):
4680 self.Try(ShrinkFont)
4682 def EnlargeFont(self):
4683 self.Try(EnlargeFont)
4685 def EventMenu(self, events, reports_menu):
4687 for event in events:
4688 event = event.split(":")[0]
4689 if event == "branches":
4690 branches_events += 1
4692 for event in events:
4694 event = event.split(":")[0]
4695 if event == "branches":
4696 label = "All branches" if branches_events == 1 else "All branches " + "(id=" + dbid + ")"
4697 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewBranchView(x), self))
4698 label = "Selected branches" if branches_events == 1 else "Selected branches " + "(id=" + dbid + ")"
4699 reports_menu.addAction(CreateAction(label, "Create a new window displaying branch events", lambda a=None,x=dbid: self.NewSelectedBranchView(x), self))
4701 def TimeChartByCPU(self):
4702 TimeChartByCPUWindow(self.glb, self)
4704 def TableMenu(self, tables, menu):
4705 table_menu = menu.addMenu("&Tables")
4706 for table in tables:
4707 table_menu.addAction(CreateAction(table, "Create a new window containing a table view", lambda a=None,t=table: self.NewTableView(t), self))
4709 def NewCallGraph(self):
4710 CallGraphWindow(self.glb, self)
4712 def NewCallTree(self):
4713 CallTreeWindow(self.glb, self)
4715 def NewTopCalls(self):
4716 dialog = TopCallsDialog(self.glb, self)
4717 ret = dialog.exec_()
4719 TopCallsWindow(self.glb, dialog.report_vars, self)
4721 def NewBranchView(self, event_id):
4722 BranchWindow(self.glb, event_id, ReportVars(), self)
4724 def NewSelectedBranchView(self, event_id):
4725 dialog = SelectedBranchDialog(self.glb, self)
4726 ret = dialog.exec_()
4728 BranchWindow(self.glb, event_id, dialog.report_vars, self)
4730 def NewTableView(self, table_name):
4731 TableWindow(self.glb, table_name, self)
4734 HelpWindow(self.glb, self)
4737 dialog = AboutDialog(self.glb, self)
4742 class xed_state_t(Structure):
4749 class XEDInstruction():
4751 def __init__(self, libxed):
4752 # Current xed_decoded_inst_t structure is 192 bytes. Use 512 to allow for future expansion
4753 xedd_t = c_byte * 512
4754 self.xedd = xedd_t()
4755 self.xedp = addressof(self.xedd)
4756 libxed.xed_decoded_inst_zero(self.xedp)
4757 self.state = xed_state_t()
4758 self.statep = addressof(self.state)
4759 # Buffer for disassembled instruction text
4760 self.buffer = create_string_buffer(256)
4761 self.bufferp = addressof(self.buffer)
4767 self.libxed = CDLL("libxed.so")
4771 self.libxed = CDLL("/usr/local/lib/libxed.so")
4773 self.xed_tables_init = self.libxed.xed_tables_init
4774 self.xed_tables_init.restype = None
4775 self.xed_tables_init.argtypes = []
4777 self.xed_decoded_inst_zero = self.libxed.xed_decoded_inst_zero
4778 self.xed_decoded_inst_zero.restype = None
4779 self.xed_decoded_inst_zero.argtypes = [ c_void_p ]
4781 self.xed_operand_values_set_mode = self.libxed.xed_operand_values_set_mode
4782 self.xed_operand_values_set_mode.restype = None
4783 self.xed_operand_values_set_mode.argtypes = [ c_void_p, c_void_p ]
4785 self.xed_decoded_inst_zero_keep_mode = self.libxed.xed_decoded_inst_zero_keep_mode
4786 self.xed_decoded_inst_zero_keep_mode.restype = None
4787 self.xed_decoded_inst_zero_keep_mode.argtypes = [ c_void_p ]
4789 self.xed_decode = self.libxed.xed_decode
4790 self.xed_decode.restype = c_int
4791 self.xed_decode.argtypes = [ c_void_p, c_void_p, c_uint ]
4793 self.xed_format_context = self.libxed.xed_format_context
4794 self.xed_format_context.restype = c_uint
4795 self.xed_format_context.argtypes = [ c_int, c_void_p, c_void_p, c_int, c_ulonglong, c_void_p, c_void_p ]
4797 self.xed_tables_init()
4799 def Instruction(self):
4800 return XEDInstruction(self)
4802 def SetMode(self, inst, mode):
4804 inst.state.mode = 4 # 32-bit
4805 inst.state.width = 4 # 4 bytes
4807 inst.state.mode = 1 # 64-bit
4808 inst.state.width = 8 # 8 bytes
4809 self.xed_operand_values_set_mode(inst.xedp, inst.statep)
4811 def DisassembleOne(self, inst, bytes_ptr, bytes_cnt, ip):
4812 self.xed_decoded_inst_zero_keep_mode(inst.xedp)
4813 err = self.xed_decode(inst.xedp, bytes_ptr, bytes_cnt)
4816 # Use AT&T mode (2), alternative is Intel (3)
4817 ok = self.xed_format_context(2, inst.xedp, inst.bufferp, sizeof(inst.buffer), ip, 0, 0)
4820 if sys.version_info[0] == 2:
4821 result = inst.buffer.value
4823 result = inst.buffer.value.decode()
4824 # Return instruction length and the disassembled instruction text
4825 # For now, assume the length is in byte 166
4826 return inst.xedd[166], result
4828 def TryOpen(file_name):
4830 return open(file_name, "rb")
4835 result = sizeof(c_void_p)
4842 if sys.version_info[0] == 2:
4843 eclass = ord(header[4])
4844 encoding = ord(header[5])
4845 version = ord(header[6])
4848 encoding = header[5]
4850 if magic == chr(127) + "ELF" and eclass > 0 and eclass < 3 and encoding > 0 and encoding < 3 and version == 1:
4851 result = True if eclass == 2 else False
4858 def __init__(self, dbref, db, dbname):
4861 self.dbname = dbname
4862 self.home_dir = os.path.expanduser("~")
4863 self.buildid_dir = os.getenv("PERF_BUILDID_DIR")
4864 if self.buildid_dir:
4865 self.buildid_dir += "/.build-id/"
4867 self.buildid_dir = self.home_dir + "/.debug/.build-id/"
4869 self.mainwindow = None
4870 self.instances_to_shutdown_on_exit = weakref.WeakSet()
4872 self.disassembler = LibXED()
4873 self.have_disassembler = True
4875 self.have_disassembler = False
4876 self.host_machine_id = 0
4877 self.host_start_time = 0
4878 self.host_finish_time = 0
4880 def FileFromBuildId(self, build_id):
4881 file_name = self.buildid_dir + build_id[0:2] + "/" + build_id[2:] + "/elf"
4882 return TryOpen(file_name)
4884 def FileFromNamesAndBuildId(self, short_name, long_name, build_id):
4885 # Assume current machine i.e. no support for virtualization
4886 if short_name[0:7] == "[kernel" and os.path.basename(long_name) == "kcore":
4887 file_name = os.getenv("PERF_KCORE")
4888 f = TryOpen(file_name) if file_name else None
4891 # For now, no special handling if long_name is /proc/kcore
4892 f = TryOpen(long_name)
4895 f = self.FileFromBuildId(build_id)
4900 def AddInstanceToShutdownOnExit(self, instance):
4901 self.instances_to_shutdown_on_exit.add(instance)
4903 # Shutdown any background processes or threads
4904 def ShutdownInstances(self):
4905 for x in self.instances_to_shutdown_on_exit:
4911 def GetHostMachineId(self):
4912 query = QSqlQuery(self.db)
4913 QueryExec(query, "SELECT id FROM machines WHERE pid = -1")
4915 self.host_machine_id = query.value(0)
4917 self.host_machine_id = 0
4918 return self.host_machine_id
4920 def HostMachineId(self):
4921 if self.host_machine_id:
4922 return self.host_machine_id
4923 return self.GetHostMachineId()
4925 def SelectValue(self, sql):
4926 query = QSqlQuery(self.db)
4928 QueryExec(query, sql)
4932 return Decimal(query.value(0))
4935 def SwitchesMinTime(self, machine_id):
4936 return self.SelectValue("SELECT time"
4937 " FROM context_switches"
4938 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4939 " ORDER BY id LIMIT 1")
4941 def SwitchesMaxTime(self, machine_id):
4942 return self.SelectValue("SELECT time"
4943 " FROM context_switches"
4944 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4945 " ORDER BY id DESC LIMIT 1")
4947 def SamplesMinTime(self, machine_id):
4948 return self.SelectValue("SELECT time"
4950 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4951 " ORDER BY id LIMIT 1")
4953 def SamplesMaxTime(self, machine_id):
4954 return self.SelectValue("SELECT time"
4956 " WHERE time != 0 AND machine_id = " + str(machine_id) +
4957 " ORDER BY id DESC LIMIT 1")
4959 def CallsMinTime(self, machine_id):
4960 return self.SelectValue("SELECT calls.call_time"
4962 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4963 " WHERE calls.call_time != 0 AND threads.machine_id = " + str(machine_id) +
4964 " ORDER BY calls.id LIMIT 1")
4966 def CallsMaxTime(self, machine_id):
4967 return self.SelectValue("SELECT calls.return_time"
4969 " INNER JOIN threads ON threads.thread_id = calls.thread_id"
4970 " WHERE calls.return_time != 0 AND threads.machine_id = " + str(machine_id) +
4971 " ORDER BY calls.return_time DESC LIMIT 1")
4973 def GetStartTime(self, machine_id):
4974 t0 = self.SwitchesMinTime(machine_id)
4975 t1 = self.SamplesMinTime(machine_id)
4976 t2 = self.CallsMinTime(machine_id)
4977 if t0 is None or (not(t1 is None) and t1 < t0):
4979 if t0 is None or (not(t2 is None) and t2 < t0):
4983 def GetFinishTime(self, machine_id):
4984 t0 = self.SwitchesMaxTime(machine_id)
4985 t1 = self.SamplesMaxTime(machine_id)
4986 t2 = self.CallsMaxTime(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 HostStartTime(self):
4994 if self.host_start_time:
4995 return self.host_start_time
4996 self.host_start_time = self.GetStartTime(self.HostMachineId())
4997 return self.host_start_time
4999 def HostFinishTime(self):
5000 if self.host_finish_time:
5001 return self.host_finish_time
5002 self.host_finish_time = self.GetFinishTime(self.HostMachineId())
5003 return self.host_finish_time
5005 def StartTime(self, machine_id):
5006 if machine_id == self.HostMachineId():
5007 return self.HostStartTime()
5008 return self.GetStartTime(machine_id)
5010 def FinishTime(self, machine_id):
5011 if machine_id == self.HostMachineId():
5012 return self.HostFinishTime()
5013 return self.GetFinishTime(machine_id)
5015 # Database reference
5019 def __init__(self, is_sqlite3, dbname):
5020 self.is_sqlite3 = is_sqlite3
5021 self.dbname = dbname
5023 self.FALSE = "FALSE"
5024 # SQLite prior to version 3.23 does not support TRUE and FALSE
5029 def Open(self, connection_name):
5030 dbname = self.dbname
5032 db = QSqlDatabase.addDatabase("QSQLITE", connection_name)
5034 db = QSqlDatabase.addDatabase("QPSQL", connection_name)
5035 opts = dbname.split()
5038 opt = opt.split("=")
5039 if opt[0] == "hostname":
5040 db.setHostName(opt[1])
5041 elif opt[0] == "port":
5042 db.setPort(int(opt[1]))
5043 elif opt[0] == "username":
5044 db.setUserName(opt[1])
5045 elif opt[0] == "password":
5046 db.setPassword(opt[1])
5047 elif opt[0] == "dbname":
5052 db.setDatabaseName(dbname)
5054 raise Exception("Failed to open database " + dbname + " error: " + db.lastError().text())
5060 usage_str = "exported-sql-viewer.py [--pyside-version-1] <database name>\n" \
5061 " or: exported-sql-viewer.py --help-only"
5062 ap = argparse.ArgumentParser(usage = usage_str, add_help = False)
5063 ap.add_argument("--pyside-version-1", action='store_true')
5064 ap.add_argument("dbname", nargs="?")
5065 ap.add_argument("--help-only", action='store_true')
5066 args = ap.parse_args()
5069 app = QApplication(sys.argv)
5070 mainwindow = HelpOnlyWindow()
5075 dbname = args.dbname
5078 print("Too few arguments")
5083 f = open(dbname, "rb")
5084 if f.read(15) == b'SQLite format 3':
5090 dbref = DBRef(is_sqlite3, dbname)
5091 db, dbname = dbref.Open("main")
5092 glb = Glb(dbref, db, dbname)
5093 app = QApplication(sys.argv)
5095 mainwindow = MainWindow(glb)
5096 glb.mainwindow = mainwindow
5099 glb.ShutdownInstances()
5103 if __name__ == "__main__":